Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c700f2f760 | |||
| 9382ace3a0 |
@@ -0,0 +1,92 @@
|
|||||||
|
# 计划:QMT Bridge MCP 能力内建(神之一手自注册 MCP,去三方插件依赖)(阶段航点)
|
||||||
|
|
||||||
|
> 编号:PLAN-018 | 粒度:阶段航点 | 创建:2026-09-08 | 状态:待实施(拟迭代 18)
|
||||||
|
> 派生自终极目标:目标-001(DSH 插件形态·复用宿主 MCP 能力)、目标-008(真实交易系统接入)
|
||||||
|
> 依据需求:**R-020(已定稿,2026-09-08,Q1-Q6 老师拍板)** —— 符合入范围门槛
|
||||||
|
> 设计约束:技术约束-003(REST 直连不变)、技术约束-008(连接配置存储/热切换)、产品约束-012(会话头部指示灯体系)、UI约束-002(连接卡片);沿用技术约束-004/005/006/007
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让神之一手插件自带 QMT Bridge 的 MCP 能力:插件在自己的 apply 内按 QMT 连接配置动态挂载 **DSH 官方 @deepseek-ai/dsh-mcp-client**(url 自动派生自激活连接 baseUrl + /mcp,serverName 固定 QMT_Bridge_MCP,工具以 mcp__QMT_Bridge_MCP__* 注册给模型;挂载/切换/卸载/状态均由神之一手在插件内管理);连接 CRUD/激活切换与 MCP 实例全生命周期联动;**卸载三方 dsh-skill-mcp-panel 并移除其宿主 cordis.patch.yml 受管块**,不再依赖三方面板加入 MCP 能力。REST 直连架构不变(技术约束-003)。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
**做**(对应 R-020 定稿边界,决策编号 Q1-Q6):
|
||||||
|
1. **依赖与可行性前置(R-020 计划阶段待办 #1,已实证 2026-09-08)**:@deepseek-ai/dsh-mcp-client@0.1.1-rc.2 已加为 one-divine-lot dependencies,从插件 lib 可 import(),模块导出 {name:'mcp-client', inject:['tools'], apply, Config},可用 ctx.plugin() 动态挂载(实证通过);
|
||||||
|
2. **服务端 MCP 管理器(新增 src/mcp/QmtMcpManager.js)**:
|
||||||
|
- 唯一 dsh-mcp-client 实例,serverName 固定 `QMT_Bridge_MCP`,url = 激活连接 baseUrl + `/mcp`(自动派生,Q3);
|
||||||
|
- 启动:解析启动连接(默认→上次激活→第一条,空表回退 cordis qmtBaseUrl)后挂载(对齐 R-004 resolveStartupConnection);
|
||||||
|
- 生命周期编排:activate/update(地址变)/remove(激活被删切默认)→ 先卸后挂(对齐 qmt-connections API 现有热切换点,Q1 仅激活连接挂载);
|
||||||
|
- 失败语义(Q5):failOnStartupError:false + reconnect(对齐宿主现有受管块配置:initial 500ms / max 30s / maxAttempts 10);
|
||||||
|
- 状态缓存:{ state: connected|connecting|disconnected|disabled, serverName, url, toolCount, error, checkedAt },供状态端点与指示灯读取;
|
||||||
|
- 释放:ctx.effect dispose 时卸载实例;
|
||||||
|
3. **服务端状态出口(api/qmt-connections.js 或 market.js 扩展)**:
|
||||||
|
- mcp-status 端点(含 refresh 即时重查)→ 设置页连接卡片 MCP 状态行 + 会话头部指示灯读取;
|
||||||
|
- sync-status 增加 mcp 域(Q4:头部指示灯数据源合并一处);
|
||||||
|
4. **设置页「QMT 连接配置」卡片 MCP 状态(UI约束-002 形态内扩展)**:卡片显示 MCP 状态(绿=已连接 + N 工具 / 红=失败原因 / 灰=未启用),「检查 MCP」按钮(手动触发,Q4);
|
||||||
|
5. **会话头部指示灯新增 MCP 状态灯(产品约束-012 体系扩展)**:SyncIndicators 三灯 → 四灯(QMT连接|持仓数据|行情数据|MCP),MCP 灯三态对齐(绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用),点击=refresh(Q4);
|
||||||
|
6. **移除三方 dsh-skill-mcp-panel(Q2,2026-09-08 老师澄清)**:dsh plugin remove dsh-skill-mcp-panel(连带其「技能 / MCP」设置菜单);编辑 ~/.dsh/profiles/web/cordis.patch.yml 删除其受管块(QMT_Bridge_MCP 条目,块外内容逐字节保留)——属宿主组成变更,按 editing-cordis-compositions 技能流程执行 + 老师确认;
|
||||||
|
7. **构建安装与验收**。
|
||||||
|
|
||||||
|
**不做**(本期,R-020 边界):通用 MCP 服务器管理面板(Q6,含自建任意 MCP 管理 UI);插件 UI/tab 数据源改走 MCP(技术约束-003);多 serverName 并存;自研 MCP 协议客户端(老师定:官方 dsh-mcp-client 可依赖,不重造);认证/鉴权增强(QMT Bridge 无鉴权)。
|
||||||
|
|
||||||
|
## 程序结构(新增/改动)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.js # 服务端入口(改):注入 mcpManager,启动挂载 + 释放卸载;把 mcpManager 传入 registerApi
|
||||||
|
├── mcp/ # (新增域目录,技术约束-016 语义:无合适域先讨论,此处讨论定 = 新增 mcp 域)
|
||||||
|
│ └── QmtMcpManager.js # MCP 实例管理(官方 dsh-mcp-client 动态挂载:唯一实例 + 生命周期编排 + 状态缓存)
|
||||||
|
├── settings.js # (小改):暴露当前激活连接解析辅助(已有 getActiveQmtConnection 可复用,必要时补暴露)
|
||||||
|
├── api/
|
||||||
|
│ ├── index.js # (改):runtime 增 mcpManager,注册 mcp-status 方法分发
|
||||||
|
│ ├── qmt-connections.js # (改):热切换点(activate/update/remove)联动 mcpManager 重挂;新增 mcp-status 处理
|
||||||
|
│ └── market.js # (改):sync-status 增加 mcp 域
|
||||||
|
└── client/views/
|
||||||
|
├── QmtConnectionChip.jsx # (改):内置 SyncIndicators 传 mcp 状态读取
|
||||||
|
├── SyncIndicators.jsx # (改):三灯 → 四灯(+MCP),轮询 sync-status 读 mcp 域,点击 refresh
|
||||||
|
└── SettingsSection.jsx # (改):QmtConnectionCard 增 MCP 状态行 + 「检查 MCP」按钮
|
||||||
|
package.json # (改,已完成):dependencies + @deepseek-ai/dsh-mcp-client@0.1.1-rc.2 + @modelcontextprotocol/sdk@1.30.0
|
||||||
|
~/.dsh/profiles/web/cordis.patch.yml # (改):移除 dsh-skill-mcp-panel 受管块(Q2,宿主配置,独立步骤;插件本体 dsh plugin remove)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现步骤(建议顺序)
|
||||||
|
|
||||||
|
1. **依赖可行性实证**(任务 1 决策门):声明 peer + 构建后在宿主实测 import 解析;不可解析则停在这里与老师讨论替代通道;
|
||||||
|
2. **服务端 mcpManager**:QmtMcpManager 骨架 + 启动挂载/卸载 + 状态缓存(先不接 API,日志验证挂载与工具注册);
|
||||||
|
3. **热切换联动**:qmt-connections activate/update/remove 编排点追加 mcpManager.resync()(复用现有 dataSource.setBaseUrl 同点);
|
||||||
|
4. **状态端点**:mcp-status + sync-status.mcp 域;本地 curl/HTTP 验证;
|
||||||
|
5. **设置页卡片**:QmtConnectionCard 增 MCP 状态 + 检查按钮(复用测试连接交互模式与 Toast);
|
||||||
|
6. **会话头部 MCP 灯**:SyncIndicators 四灯改造;验证与三灯并存样式一致(对齐 IndicatorDot/sep 布局与主题 token);
|
||||||
|
7. **宿主受管块移除**:编辑 cordis.patch.yml(editing-cordis-compositions 流程)→ 重载宿主 → 验证 mcp__QMT_Bridge_MCP__* 工具仍可用(这次来自神之一手);
|
||||||
|
8. **构建安装验收**:服务端+客户端构建,dsh plugin add / 重载;逐条核对验收标准。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 移除宿主 cordis.patch.yml QMT_Bridge_MCP 受管块后,模型仍能使用 mcp__QMT_Bridge_MCP__qmt_* 工具(来源=神之一手动态挂载实例),工具集与移除前一致(同一 serverName/url);
|
||||||
|
2. 激活连接切换 → MCP 实例 url 跟随切换(工具可用目标随激活配置变化,观察工具调用返回目标数据源变化);
|
||||||
|
3. 编辑激活连接地址、删除激活连接(自动切默认)→ MCP 实例同步重挂,无残留旧连接工具;
|
||||||
|
4. 连接列表为空(无激活)→ 不挂 MCP 实例(mcp-status=disabled),插件其它功能不受影响;
|
||||||
|
5. QMT Bridge 不可达 → MCP 灯=红(断开),插件不崩、其余灯正常;恢复可达后自动重连至绿灯(Q5 语义);
|
||||||
|
6. 设置页连接卡片显示 MCP 状态(已连接 + N 工具 / 失败原因 / 未启用),「检查 MCP」可手动触发并刷新;
|
||||||
|
7. 会话头部出现第四个指示灯「MCP」:绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用,点击触发即时检查(Q4);
|
||||||
|
8. 现有功能(分仓/策略/交易/设置子 tab/三灯)无回归;typecheck + build 通过。
|
||||||
|
|
||||||
|
## 验收方法
|
||||||
|
|
||||||
|
- 移除受管块前先完成 1-6 步并在旧块存在时验证(同名冲突预期:两实例同 serverName 后加载报错——因此**验证顺序必须先移除受管块再加载新实例**,步骤 7 与 1-6 的顺序需在实施时按 HMR 重载节奏小心编排,见实施时核查点);
|
||||||
|
- 用 qmt 工具的 listTools/实际调用观察目标切换;断开 QMT 观察灯态与自动恢复;
|
||||||
|
- settings 卡片与头部灯截图/人工核验。
|
||||||
|
|
||||||
|
> **实施注意(同名冲突时序)**:宿主现有受管块与神之一手新实例同用 serverName QMT_Bridge_MCP,存活实例重复会报错。安全顺序:① 插件侧完整实现并构建 → ② 移除宿主受管块并重载(此窗口工具短暂消失可接受)→ ③ 插件实例接管(工具恢复,来源变为神之一手)。禁止两实例并存重载。
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- 需求:R-020(已定稿,2026-09-08)| 参考:mcp_router 动态挂载实证、dsh-skill-mcp-panel 受管块现状
|
||||||
|
- 设计约束新增(定稿时补充):技术约束:「插件内建 MCP 客户端注册规范」(唯一实例 serverName 固定 + 随激活连接 url 派生 + 生命周期联动);产品约束:「MCP 状态出口(设置卡片 + 会话头部 MCP 灯)」
|
||||||
|
|
||||||
|
## 记录
|
||||||
|
|
||||||
|
| 日期 | 变更 |
|
||||||
|
|---|---|
|
||||||
|
| 2026-09-08 | 依据 R-020 定稿(Q1-Q6)创建 PLAN-018 |
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 计划:策略会话——会话以策略当前数据为依据做讨论(阶段航点)
|
||||||
|
|
||||||
|
> 来源:R-019(**已定稿**,2026-09-08)| 状态:执行中(设计阶段先行)| 所属迭代:17-策略会话
|
||||||
|
> 粒度:阶段航点(覆盖设计 → 宿主调研 → 实现 → 验收全过程,随进展拆分小任务)
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让 AI 会话可以"以策略为 Workspace"(数据依据版):**对话开始前选「讨论策略」→ 服务端生成该策略当前数据摘要 → 预注入会话上下文 → 以该策略当前数据为依据进行复盘/分析讨论**;只读、会话中可换/刷新、产物不落盘。
|
||||||
|
|
||||||
|
## 范围与分阶段
|
||||||
|
|
||||||
|
1. **设计阶段(当前,老师指令:先做产品逻辑 + UI 交互逻辑)**:产出《产品逻辑设计》《UI交互设计》,D 系列问题老师拍板后定稿;
|
||||||
|
2. **宿主注入通道调研**:确认把外部摘要注入会话上下文的可行通道/形态(决定"折叠数据依据条"还是"文本消息形态"呈现;影响客户端渲染方案与注入动作实现);
|
||||||
|
3. **技术实现方案**(技术约束核对、服务端摘要服务 + 客户端选择 UI + 注入动作 + 回归);
|
||||||
|
4. **实现 + 验收**。
|
||||||
|
|
||||||
|
## 验收指向
|
||||||
|
|
||||||
|
见 `docs/04-迭代记录/17-策略会话/验收标准.md`(实现阶段补充细化)。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
见 R-019 定稿边界:不用宿主工作区;不开放账本写;不做按需取数工具;复盘产物不落盘;本期不做通用视图(全部持仓/交易记录)作依据。
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# 迭代 17 UI 交互设计:策略会话(讨论依据控件)
|
||||||
|
|
||||||
|
> 依据:R-019(已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
|
||||||
|
> 视觉基线:UI约束-004(--dsw-* token)/ UI约束-001(头部 chip 形态)/ 现有 QmtConnectionChip 交互模式;复用 Toast、外点关闭、RPC(useRpc) 机制。
|
||||||
|
|
||||||
|
## 1. 交互总览(一句话)
|
||||||
|
|
||||||
|
对话视图输入区上方有一枚**「讨论依据」chip**(仿 DSH workspace chip:未选=「讨论依据:请选择 ▾」;已选=「依据:做T ▾」+刷新);选择策略 → 服务端生成数据摘要 → 注入并在对话流顶部出现一条**数据依据条目**,之后即可基于该策略数据讨论。
|
||||||
|
|
||||||
|
## 2. 控件布局(示意)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ 对话视图 ──────────────────────────────┐
|
||||||
|
│ …对话消息流… │
|
||||||
|
│ [数据依据条 ▾ 做T · 快照 14:32 · 3仓/42笔] ← 选后注入显示(可展开全文)
|
||||||
|
│ ─────────────────────────────────────── │
|
||||||
|
│ [依据:做T ▾] [↻] │ ← accessory 行(输入框上方左侧)
|
||||||
|
│ ┌ 输入框(composer)────────────────┐ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. chip 状态机
|
||||||
|
|
||||||
|
| 状态 | 外观 | 交互 |
|
||||||
|
|---|---|---|
|
||||||
|
| 未加载 | 不显示(静默,与 QMT chip 一致) | — |
|
||||||
|
| 未选择 | `讨论依据:请选择 ▾`(tertiary 色) | 点开 → 策略下拉(空态见 §5) |
|
||||||
|
| 生成中 | `依据:做T …`(opacity .6,右侧小 spinner/省略) | 不可再点(防抖) |
|
||||||
|
| 已选择 | `依据:做T ▾` + 右侧「↻ 刷新」小钮 | 点 chip 换策略;点 ↻ 刷新注入 |
|
||||||
|
| 策略已删除 | `依据:做T(已删除)▾`(warning 色) | 下拉选其它/清除 |
|
||||||
|
| 错误 | toast 错误 + chip 保留原态 | 可重试 |
|
||||||
|
|
||||||
|
## 4. 交互细节
|
||||||
|
|
||||||
|
1. **选择策略(F1 主流程)**
|
||||||
|
- 点击 chip → 下拉菜单(与 QmtConnectionChip 同一套 Menu 视觉):
|
||||||
|
- 标题区「以哪个策略讨论?」;
|
||||||
|
- 策略项:每项 = 策略名 + 副行「当前 N 只持仓 · 最近交易 …」;
|
||||||
|
- 底部「清除依据」(仅已选时出现);当前依据项前打 ✓;
|
||||||
|
- 点击某策略 → chip 进入生成中 → 服务端取摘要 → 注入会话 + 对话流顶部出现依据条目 → toast.success(`已载入「做T」数据依据:3 持仓 / 42 笔交易(快照 14:32:05)`)。
|
||||||
|
2. **注入条目(D-1 建议形态)**
|
||||||
|
- 视觉:淡色卡片/引用样式(bg-layer 弱化 + 左侧竖条 accent),标题行「⛭ 神之一手数据依据」+ `做T · 快照 2026-09-08 14:32:05`,行 2 摘要「当前持仓 3 只 / 历史交易 42 笔(含已清仓)」;
|
||||||
|
- 默认折叠,点击展开全文(markdown 摘要内容);
|
||||||
|
- 多依据/多刷新并存时按时间堆叠,最近一次在消息流最新处;不伪装用户/助手消息(若通道仅支持文本消息,则呈现为最顶部一段普通消息文本——实现以调研为准)。
|
||||||
|
3. **换策略**:chip 下拉选另一策略 → 同"生成中→注入"流程;chip 文案与已注入条目更新。
|
||||||
|
4. **刷新依据**:点 chip 右侧 ↻ → 生成新快照并注入新条目(含新快照时间);toast「已更新依据(快照 14:35:10)」。不会删除旧条目。
|
||||||
|
5. **清除依据**:下拉「清除依据」→ chip 复位「请选择」;不清除历史注入条目。
|
||||||
|
6. **键盘/可及**:Esc 或外点关闭下拉;chip 提供 title 提示「以某个策略的数据为依据开始讨论(只读)」。
|
||||||
|
7. **只读表达**:选择器下拉仅含策略与其状态信息,无任何写/操作入口;文案提示"会话只读,不执行交易操作"(依赖尾注与依据条目样式即可,不打断输入)。
|
||||||
|
|
||||||
|
## 5. 空态与边界 UI
|
||||||
|
|
||||||
|
| 场景 | UI |
|
||||||
|
|---|---|
|
||||||
|
| 无策略可配置 | 下拉空态:「暂无策略,请到 设置 → 神之一手 → 策略分组 添加」+「去设置」快捷键位 |
|
||||||
|
| 策略持仓为空 | 摘要条目持仓区显示「当前无持仓」;交易摘要照常 |
|
||||||
|
| 行情缺失 | 摘要中现价列显示 —,条目副行提示「部分行情缺失(--)」,不阻塞 |
|
||||||
|
| 生成失败/服务不可用 | toast.error(原因);chip 回原态可重试 |
|
||||||
|
| 已在数据 tab(.odl-root)| 不显示该控件(现状 CSS 隐藏 composer 的区域一致,仅聊天视图展示) |
|
||||||
|
|
||||||
|
## 6. 组件落点(初拟,技术方案细化)
|
||||||
|
|
||||||
|
- 客户端新组件 `StrategyBasisChip.jsx`(chip + 下拉 + 刷新),复刻 QmtConnectionChip 的结构(rootRef/外点关闭/useRpc/useToast);
|
||||||
|
- 注入条目组件 `DataBasisCard.jsx`(折叠/展开 markdown 展示);
|
||||||
|
- 服务端新只读端点(`one-divine-lot/strategies/basis-summary`)生成摘要(产品逻辑 §4 规格);
|
||||||
|
- 挂载位置:D-4(对话 accessory vs 会话头部)拍板后确定 slot 与可见性策略。
|
||||||
|
|
||||||
|
## 7. D 问题(AI 建议项,与产品逻辑设计共享,待老师拍板)
|
||||||
|
|
||||||
|
| # | 问题 | AI 建议 |
|
||||||
|
|---|---|---|
|
||||||
|
| D-1 | 注入条目呈现形态 | 折叠「数据依据」卡片,点开全文(见 §4.2) |
|
||||||
|
| D-2 | 交易摘要聚合粒度 | 按持仓聚合 + 已清仓标注;如需近期逐笔再加近 N 笔 |
|
||||||
|
| D-3 | 依据跨刷新持久化 | 会话级不持久化(重开重选) |
|
||||||
|
| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 迭代 17 产品逻辑设计:策略会话
|
||||||
|
|
||||||
|
> 依据:R-019(已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
|
||||||
|
|
||||||
|
## 0. 一句话逻辑
|
||||||
|
|
||||||
|
> 会话 = 用户选定「讨论策略」→ 系统把该策略**当前数据**(当前持仓 + 全历史关联交易摘要)作为会话依据注入 → 之后人机围绕该数据复盘/分析;依据可随时更换/刷新;只读。
|
||||||
|
|
||||||
|
## 1. 概念与数据域
|
||||||
|
|
||||||
|
| 概念 | 定义 | 数据来源 |
|
||||||
|
|---|---|---|
|
||||||
|
| 策略 | settings 中的策略定义(id/name,做T、网格超市…) | settings.strategies(现有) |
|
||||||
|
| 策略当前持仓(活动行) | 该策略账本中未清仓的 holding(shares>0 活动) | SqliteStore strategy_holdings(R-018 数据域:账本为唯一权威) |
|
||||||
|
| 行情现价/涨跌 | holding code 的实时价格 | QuoteHub 内存快照(读穿透;不可得=—) |
|
||||||
|
| 关联交易记录 | 归属该策略/持仓的委托(含已清仓历史,trade_orders 归属列 + 历史库) | SqliteStore trade_orders/trade_fills(R-009/R-018) |
|
||||||
|
| 数据依据(basis) | 一次注入到会话的"策略当前数据摘要快照" | 服务端按需生成(本设计) |
|
||||||
|
|
||||||
|
**关键语义(沿用 R-018 数据域分界)**:持仓展示 = 账本(活动行);交易 = 归属账本(历史全量);行情 = 内存快照。摘要生成**只读**这些域,不写账本。
|
||||||
|
|
||||||
|
## 2. 会话级状态
|
||||||
|
|
||||||
|
策略会话不需要跨会话持久存储(设计建议,见 D-3);会话内 UI 状态:
|
||||||
|
|
||||||
|
- `basis: none | { strategyId, strategyName, injectedAt }`
|
||||||
|
- `basisState: idle | generating | done | error`
|
||||||
|
- 注入后的摘要内容随会话消息流存在(可折叠"数据依据"条目),不单独存储。
|
||||||
|
|
||||||
|
## 3. 核心流程
|
||||||
|
|
||||||
|
### 3.1 选择策略 → 注入依据(主流程)
|
||||||
|
1. 对话视图(聊天)中,用户在「讨论依据」控件里选一个策略;
|
||||||
|
2. 客户端调服务端 `strategies/:id/basis-summary`(只读)生成摘要文本(规格见 §4);
|
||||||
|
3. 摘要通过**宿主注入通道**进入当前会话上下文(技术通道调研待定,见 §6);同屏在对话流顶部呈现「数据依据」条目;
|
||||||
|
4. toast 反馈:`已载入「做T」数据依据(3 持仓 / 42 笔交易,快照 14:32:05)`;
|
||||||
|
5. 用户开始提问复盘/分析;模型以摘要(及会话中后续补充)为依据回答。
|
||||||
|
|
||||||
|
### 3.2 换策略
|
||||||
|
- 重开选择器选另一策略 → 再次执行 3.1(新摘要作为新的依据条目追加;chip 切换为新策略)。旧依据条目保留在历史中(模型自然以最近/明确引用的为准)。
|
||||||
|
|
||||||
|
### 3.3 刷新依据
|
||||||
|
- 已选状态下 chip 提供「刷新」:重新生成**当前时刻**摘要并注入(盘中数据变化后想拿最新)。
|
||||||
|
- 语义:每次刷新=新快照依据条目(含新快照时间),不覆盖旧条目。
|
||||||
|
|
||||||
|
### 3.4 清除依据
|
||||||
|
- 选择器提供「清除依据」:回到无依据普通会话(chip 复位为「讨论依据:未选择」);已注入的历史条目仍在消息流中,后续对话不再视为策略依据会话。
|
||||||
|
|
||||||
|
### 3.5 边界
|
||||||
|
| 场景 | 行为 |
|
||||||
|
|---|---|
|
||||||
|
| 无可选策略(未配置) | 选择器显示空态「暂无策略,请到 设置→神之一手→策略分组 添加」 |
|
||||||
|
| 会话进行中该策略被删除/改名 | chip 下次加载策略列表时按 id 对齐:找不到=显示「策略已删除」+ 可清除;改名后按 id 正常(显示新名);历史依据条目保留 |
|
||||||
|
| 策略当前持仓为空 / 无行情 | 摘要照常生成:持仓区显示"当前无持仓";现价列显示 —;交易摘要照常(历史) |
|
||||||
|
| 账本只读 | 摘要生成与注入路径无任何写操作;UI 无操作按钮(区别于数据 tab 的手动操作) |
|
||||||
|
| 未选策略的普通聊天 | 与现状完全一致,不受影响(策略会话是可选增强,不锁定会话) |
|
||||||
|
|
||||||
|
## 4. 摘要规格(服务端 `basis-summary` 输出,markdown 文本)
|
||||||
|
|
||||||
|
结构(自上而下):
|
||||||
|
1. **快照头**:`【神之一手数据依据】策略:做T | 快照:2026-09-08 14:32:05 | 数据为只读快照,非实时`
|
||||||
|
2. **当前持仓**(活动行,逐行):code 名称|份额|可用|成本价|现价(— 缺失)|市值|浮动盈亏|持仓天数;尾部合计(N 只 / 总市值 / 总盈亏)。现价/市值缺行情时给—,不阻塞。
|
||||||
|
3. **交易摘要(该策略全历史关联委托,聚合)**:总笔数/买入累计/卖出累计(按归属 holding 聚合):
|
||||||
|
- 每 holding 一段:code 名称|状态(持有中/已清仓 清仓日)|买卖笔数|累计买入(量/额)|累计卖出(量/额)|当前份额;
|
||||||
|
- 已清仓 holding 标注清仓日期与最后动作摘要;
|
||||||
|
- 排序:持有中在前(按成本日/名称),已清仓在后(按清仓日倒序);
|
||||||
|
- **规模上限**:holding 超过 20 行时按最新清仓倒序截断并注明「更多历史见 交易记录 tab」。
|
||||||
|
4. **尾注**:只读提示「以上为策略数据快照;本会话只读,不执行任何交易/账本操作」。
|
||||||
|
|
||||||
|
字段精确取数与空值规则在技术实现方案细化。
|
||||||
|
|
||||||
|
## 5. 非目标(本期明确不做)
|
||||||
|
|
||||||
|
- 宿主工作区/目录注册(R-019 R3);
|
||||||
|
- 账本写/交易执行、任何操作按钮;
|
||||||
|
- 按需取数工具(AI 自行调用业务接口);
|
||||||
|
- 复盘产物落盘 / 复盘管理(含 DB)——未来 T-002 方向;
|
||||||
|
- 通用视图(全部持仓/交易记录)作依据;
|
||||||
|
- 关注列表作为依据源。
|
||||||
|
|
||||||
|
## 6. 技术通道(设计假设,待调研确认)
|
||||||
|
|
||||||
|
注入的可行通道候选:① 宿主 injected-context/上下文节点(可折叠条目、非用户消息)→ 首选;② 会话 send(文本消息形态进会话)→ 兜底(呈现为一条前置依据消息)。**渲染与动作方案以调研结果为准**(本期设计保留两种形态的表达,UI 交互设计按"数据依据条目"统一描述)。
|
||||||
|
|
||||||
|
## 7. D 问题(AI 建议项,待老师拍板)
|
||||||
|
|
||||||
|
| # | 问题 | AI 建议 |
|
||||||
|
|---|---|---|
|
||||||
|
| D-1 | 注入条目的呈现形态 | 对话流顶部「数据依据」条目:默认折叠为一行(图标+策略+快照时间+行数摘要),点开看全文;不伪装成用户消息 |
|
||||||
|
| D-2 | 交易摘要聚合粒度 | 按持仓聚合(每持仓一段汇总 + 已清仓标注),不逐笔铺开(避免注入过长);如老师要"近期逐笔"可加近 N 笔小表 |
|
||||||
|
| D-3 | 依据选择是否跨刷新持久化 | 本期**会话级不持久化**(刷新需重选;历史注入条目仍在消息流),与 R-016 会话级偏好一致、不加存储;复盘管理未来再考虑 |
|
||||||
|
| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# 迭代 17:策略会话(以策略当前数据为依据的会话讨论)
|
||||||
|
|
||||||
|
## 目标(一句话)
|
||||||
|
|
||||||
|
让 AI 会话可"以策略为 Workspace":对话开始前选「讨论策略」,预注入该策略**当前数据摘要**作为会话依据,AI 与老师围绕该策略持仓/交易做复盘与分析讨论;只读、会话中可换/刷新依据。
|
||||||
|
|
||||||
|
## 目标描述
|
||||||
|
|
||||||
|
- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-019,2026-09-08):
|
||||||
|
- **改造现有 DSH 对话窗口**,不引入宿主工作区/目录;
|
||||||
|
- 会话的数据 = ①开始前/过程中注入的策略当前数据摘要 + ②会话过程本身产生的对话;复盘管理(可能带 DB 沉淀)为神之一手未来模块,本期不做;
|
||||||
|
- 注入口径:当前持仓明细 + 该策略全历史关联交易聚合摘要(含已清仓);
|
||||||
|
- 只读(Q4=A);预注入不挂工具(R2-Q1=A);产物不落盘(R2-Q4)。
|
||||||
|
- **本迭代分阶段推进**:先设计(产品逻辑 + UI 交互逻辑,老师 2026-09-08 指令)→ 宿主注入通道调研 → 技术实现方案 → 实现与验收。
|
||||||
|
|
||||||
|
## 讨论过程
|
||||||
|
|
||||||
|
| 日期 | 轮次 | 要点 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-09-08 | 需求讨论 Q1-Q4 | 语义收敛:非物理目录;数据为依据;会话内只读 |
|
||||||
|
| 2026-09-08 | 需求讨论 R2 | 预注入;范围 R2-Q3=A;产物暂不落 |
|
||||||
|
| 2026-09-08 | 需求讨论 R3 | 参考 DSH 工作区模式的"开始前选策略"体验,但**不用宿主工作区** |
|
||||||
|
| 2026-09-08 | 需求定稿 F1/F2 | 首条消息前「讨论策略」选择 + 会话中可换/刷新;注入口径维持 R2-Q3=A |
|
||||||
|
| 2026-09-08 | 老师指令 | 立项迭代:先做产品逻辑、UI 交互逻辑设计 |
|
||||||
|
|
||||||
|
## 本期(设计阶段)交付
|
||||||
|
|
||||||
|
- `产品逻辑设计.md`(数据与行为逻辑、摘要规格、状态与边界)
|
||||||
|
- `UI交互设计.md`(交互流程、控件、视觉与文案、状态机)
|
||||||
|
- D 系列设计问题(AI 建议项)→ 老师拍板 → 设计定稿
|
||||||
|
|
||||||
|
## 后续阶段(设计定稿后)
|
||||||
|
|
||||||
|
- 宿主注入通道技术调研 → 技术实现方案.md → 实现 → 验收标准.md + 验收(是否拆分独立迭代由老师定)。
|
||||||
|
|
||||||
|
## 对老师配合的请求
|
||||||
|
|
||||||
|
1. 拍板 D1-D4(产品/UI 设计中的 AI 建议项,见设计文档末);
|
||||||
|
2. 实现验收需真实会话环境人工目视。
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# 迭代 18 技术实现方案:QMT Bridge MCP 能力内建
|
||||||
|
|
||||||
|
> 依据:R-020(2026-09-08 定稿,Q1-Q6 + 老师澄清:移除三方 dsh-skill-mcp-panel、官方 dsh-mcp-client 可依赖)| PLAN-018
|
||||||
|
> 日期:2026-09-08
|
||||||
|
|
||||||
|
## 目标架构
|
||||||
|
|
||||||
|
神之一手在 apply 内**动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client**(依赖官方库、管理在插件内),把 QMT Bridge 的 MCP 工具注册给模型;MCP 实例生命周期(挂/切/卸)随 QMT 连接配置的增删改与激活切换联动;状态出口 = 设置页 + 会话头部指示灯。REST 直连架构不变(技术约束-003)。
|
||||||
|
|
||||||
|
```
|
||||||
|
神之一手 apply(ctx)
|
||||||
|
├─ settings(QMT 连接配置:list + activeId + defaultId) [已有 R-004]
|
||||||
|
├─ dataSource(REST 直连,baseUrl 随激活热切换) [已有]
|
||||||
|
└─ QmtMcpManager(新增 src/mcp/QmtMcpManager.js)
|
||||||
|
├─ 动态 import(@deepseek-ai/dsh-mcp-client) → ctx.plugin(module, config)
|
||||||
|
│ config = { serverName: QMT_Bridge_MCP, transport: streamable-http,
|
||||||
|
│ url: 激活 baseUrl + /mcp, failOnStartupError:false,
|
||||||
|
│ reconnect: {enabled, 500ms→30s, 10次} } ← 与宿主现受管块同参(Q5)
|
||||||
|
├─ 生命周期:start(启动挂载激活连接)/ resync(url)(激活·编辑地址·删除联动)
|
||||||
|
├─ 状态:probe()(SDK listTools 握手:connected + 工具数 + 延迟 / error)
|
||||||
|
│ getStatus() → { state, serverName, url, mounted, toolCount, error, latencyMs, checkedAt }
|
||||||
|
└─ dispose:卸载 fiber(ctx.effect 释放)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键事实(已实证)
|
||||||
|
|
||||||
|
- `@deepseek-ai/dsh-mcp-client@0.1.1-rc.2` 已加入 dependencies;从插件 lib 可 import,模块导出 {name:mcp-client, inject:[tools], apply, Config},可 ctx.plugin() 动态挂载(mcp_router 同款实证);
|
||||||
|
- serverName 存活实例内必须唯一 → 移三方受管块前插件实例与宿主实例不可并存(实施时序见 PLAN-018 注意事项);
|
||||||
|
- dsh-mcp-client 不暴露连接状态查询 API → 状态用**独立 SDK 探测**(@modelcontextprotocol/sdk Client:initialize + tools/list → 工具数/延迟),与 dsh-mcp-client 自管连接互不干扰;
|
||||||
|
- 宿主现 QMT_Bridge_MCP 受管块 = 三方 dsh-skill-mcp-panel 写入,连接与注册实为官方 dsh-mcp-client。
|
||||||
|
|
||||||
|
## 改动清单
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
|---|---|
|
||||||
|
| package.json | dependencies + @deepseek-ai/dsh-mcp-client@0.1.1-rc.2 + @modelcontextprotocol/sdk@1.30.0(已加) |
|
||||||
|
| src/mcp/QmtMcpManager.js(新增) | MCP 实例管理:动态挂载/卸载 + resync + probe/getStatus + dispose |
|
||||||
|
| src/index.js | 实例化 QmtMcpManager、start、ctx.effect 释放、传入 registerApi runtime |
|
||||||
|
| src/api/index.js | runtime 增 mcpManager(分发 handleQmt/handleMarket) |
|
||||||
|
| src/api/qmt-connections.js | METHODS + mcp-status;activate/update(地址变)/remove 热切换点 + mcpManager.resync(url) |
|
||||||
|
| src/api/market.js | sync-status 增加 mcp 域(读 mcpManager.getStatus) |
|
||||||
|
| src/client/views/SyncIndicators.jsx | 三灯 → 四灯:+「MCP」(绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用;点击刷新) |
|
||||||
|
| src/client/views/SettingsSection.jsx | QMT 连接配置子 tab 顶部 MCP 状态条 + 「检查 MCP」按钮(手动触发 mcp-status refresh) |
|
||||||
|
|
||||||
|
## MCP 状态语义(对齐产品约束-012 指示灯体系)
|
||||||
|
|
||||||
|
| state | 灯色 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| connected | 绿 | 已连接 + N 工具(probe listTools 成功) |
|
||||||
|
| connecting | 黄 | 挂载中 / 重连中 |
|
||||||
|
| error | 红 | 不可达 / 失败(含原因) |
|
||||||
|
| disabled | 灰 | 无激活连接(未启用) |
|
||||||
|
|
||||||
|
## 实现步骤
|
||||||
|
|
||||||
|
1. QmtMcpManager(挂/卸/resync/probe/getStatus/dispose);
|
||||||
|
2. index.js 接线 + registerApi runtime;
|
||||||
|
3. api:mcp-status + sync-status.mcp + 热切换联动;
|
||||||
|
4. 客户端:SyncIndicators 四灯 + 设置页 MCP 状态条;
|
||||||
|
5. 构建(pnpm build)验证;
|
||||||
|
6. 宿主迁移(独立步骤,需老师确认 + editing-cordis-compositions 流程):卸载 dsh-skill-mcp-panel + 移除 cordis.patch.yml 受管块 → 重载 → 验证工具来自神之一手;
|
||||||
|
7. 验收。
|
||||||
|
|
||||||
|
## 风险与注意
|
||||||
|
|
||||||
|
- 同 serverName 冲突:插件实例与宿主受管块不可并存(先插件后移除,或先移除后插件接管,二选一窗口);
|
||||||
|
- ctx.plugin 子 fiber 需要 tools 服务:one-divine-lot inject 列表评估(对齐 mcp_router inject [webServer, tools]);
|
||||||
|
- dsh-mcp-client 初始连接失败不抛(failOnStartupError:false),但**重复 serverName 会抛** → 挂载错误必须 catch,插件其余功能不受影响。
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# 迭代 18:QMT Bridge MCP 能力内建(去三方插件依赖)
|
||||||
|
|
||||||
|
## 目标(一句话)
|
||||||
|
|
||||||
|
神之一手插件在 apply 内动态挂载 DSH 官方 dsh-mcp-client(serverName 固定 QMT_Bridge_MCP,url 自动派生自激活连接 baseUrl+/mcp,挂载/切换/卸载/状态均由神之一手插件内管理),MCP 能力不再依赖三方 dsh-skill-mcp-panel(卸载其插件 + 移除宿主 cordis.patch.yml 受管块)。
|
||||||
|
|
||||||
|
## 目标描述
|
||||||
|
|
||||||
|
- 依据 R-020(2026-09-08 定稿,Q1-Q6)+ PLAN-018;
|
||||||
|
- Q1 只挂激活连接;Q2 移除三方块由神之一手管理;Q3 url 自动派生;Q4 状态出口=设置页卡片检查 + 会话头部 MCP 指示灯;Q5 失败不崩+重连;Q6 不做通用 MCP 管理;
|
||||||
|
- REST 直连架构不变(技术约束-003)。
|
||||||
|
|
||||||
|
## 讨论过程
|
||||||
|
|
||||||
|
| 日期 | 轮次 | 要点 |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-09-08 | 需求讨论 Q1-Q6 | R-020 定稿(见 R-020.md) |
|
||||||
|
| 2026-09-08 | 立项 | 老师:「启动,这个不是一个大需求」→ 快速迭代 |
|
||||||
|
| 2026-09-08 | 指令澄清 | 老师修正:不要的是三方 dsh-skill-mcp-panel;依赖官方 @deepseek-ai/dsh-mcp-client 没问题 → 方案定为官方库动态挂载 + 移除三方面板 |
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 迭代 18 验收标准:QMT Bridge MCP 能力内建
|
||||||
|
|
||||||
|
> 验收方法/验收目标见 PLAN-018「验收标准 / 验收方法」逐条。
|
||||||
|
|
||||||
|
1. 移除宿主 cordis.patch.yml QMT_Bridge_MCP 受管块后,模型仍能用 mcp__QMT_Bridge_MCP__qmt_* 工具(来源=神之一手动态挂载,工具集一致);
|
||||||
|
2. 激活连接切换 → MCP 实例 url 跟随;编辑/删除激活连接 → 同步重挂无残留;
|
||||||
|
3. 连接列表为空 → 不挂实例(mcp-status=disabled),其余功能不受影响;
|
||||||
|
4. QMT 不可达 → 插件不崩、MCP 灯红、恢复后自动重连绿灯;
|
||||||
|
5. 设置页连接卡片 MCP 状态 + 检查按钮;会话头部 MCP 指示灯四灯齐全;
|
||||||
|
6. 现有功能无回归;typecheck + build 通过。
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 需求:R-019 策略会话(会话以策略当前数据为依据做讨论)· 已定稿
|
||||||
|
|
||||||
|
> 登记:2026-09-08 | 来源:老师指令(新需求)| 状态:**已定稿(2026-09-08,F1/F2 老师确认)**
|
||||||
|
> 归属:迭代 17(待立项)| 计划:待出(PLAN-017)| 实现状态:未开始
|
||||||
|
|
||||||
|
## 需求描述(老师原始诉求,原话)
|
||||||
|
|
||||||
|
> **新需求,让 AI 会话可以以策略为 Workspace。**
|
||||||
|
> 三轮讨论后收敛为:**让 AI 会话以某个策略的当前数据为讨论依据**(复盘 / 分析),改造现有对话窗口,会话开始前确认用哪个策略。
|
||||||
|
|
||||||
|
## 讨论记录(2026-09-08)
|
||||||
|
|
||||||
|
### 第一轮(Q1-Q4,老师拍板)——语义收敛
|
||||||
|
|
||||||
|
| # | 问题 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| Q1 | 需求对象与形态 | 改造现有 DSH 对话窗口:会话以某个策略的持仓、交易记录为依据做复盘/分析 |
|
||||||
|
| Q2 | 「以策略为 Workspace」含义 | **不是物理工作目录/文件为主要依据**;是让策略数据作为会话的聊天依据 |
|
||||||
|
| Q3 | 入口与触发 | 在现有会话窗口(非独立新页面) |
|
||||||
|
| Q4 | 会话内能力边界 | **A 只读**:读该策略业务数据,不开放账本写 |
|
||||||
|
|
||||||
|
### 第二轮(R2-Q1..Q4,老师拍板)——机制与范围
|
||||||
|
|
||||||
|
| # | 问题 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| R2-Q1 | 聊天依据如何生效 | **A 预注入**:选定策略即开始聊,把该策略数据依据注入会话上下文(不挂按需取数工具) |
|
||||||
|
| R2-Q2 | 选择粒度 | C 单选策略/通用视图混选——后被 R3 收敛(本期仅策略) |
|
||||||
|
| R2-Q3 | 时间覆盖 | **A**:当前持仓 + 该策略全部历史交易(含已清仓) |
|
||||||
|
| R2-Q4 | 复盘产物 | **暂不落盘**(留会话历史);保存沉淀属 T-002/未来复盘管理 |
|
||||||
|
|
||||||
|
### 第三轮(R3,老师定调)——不用 DSH 原生工作区
|
||||||
|
|
||||||
|
**老师原话(2026-09-08)**:目前不需要 dsh web 原本定义的工作区。会话的数据 = ① 会话注入的数据 + ② 会话过程中产生的数据;以后神之一手会加「复盘管理」,可能用数据库存沉淀;**目前会话只是以策略当前数据来做会话讨论**。
|
||||||
|
|
||||||
|
- 不做 DSH 宿主工作区(不注册目录 / 不动工作区分组与侧栏);
|
||||||
|
- 「会话开始前选策略」= 仿工作区"先选再开始"的体验,但只做会话级策略依据选择;
|
||||||
|
- 会话过程中产生的数据 = 会话历史本身;复盘管理(可能带 DB)为未来模块,本期不做。
|
||||||
|
|
||||||
|
### 第四轮(F1-F2,2026-09-08 老师确认)——定稿收口
|
||||||
|
|
||||||
|
| # | 问题 | 结论(老师拍板) |
|
||||||
|
|---|---|---|
|
||||||
|
| F1 | 入口形态(无宿主工作区下"会话开始前选策略") | **采纳 AI 建议**:对话视图首条消息前显示「讨论策略」选择区(仿 DSH workspace chip Choose 体验:显示「依据:做T ▾」),选策略即注入数据摘要开始聊;会话中可点 chip 换策略 / 点「刷新」注入最新数据 |
|
||||||
|
| F2 | 「策略当前数据」口径 | **采纳 AI 建议(R2-Q3=A 维持)**:注入 = 当前持仓明细 + 该策略全历史关联交易**聚合摘要**(按持仓/时间汇总买卖与结果,含已清仓;明细过长不进注入) |
|
||||||
|
|
||||||
|
## 定稿语义
|
||||||
|
|
||||||
|
- **对象**:现有 DSH 对话窗口(挂神之一手 tab 的会话),对话视图首条消息前出现「讨论策略」选择;
|
||||||
|
- **机制**:选策略 → 服务端取该策略当前数据 → 预注入会话上下文(摘要形态,含快照时间)→ 开始聊;换策略 / 点刷新 = 再次注入该策略最新摘要(模型以最近一次注入为依据);
|
||||||
|
- **数据范围**:当前持仓明细 + 全历史关联交易聚合摘要(含已清仓),只读;
|
||||||
|
- **能力**:会话内 AI 只读(无账本写 / 交易执行入口);产物不落盘。
|
||||||
|
|
||||||
|
## 定稿边界
|
||||||
|
|
||||||
|
**做**:对话开始前的策略选择 UI(F1 形态);策略当前数据摘要的生成与注入;会话中换策略/刷新的再注入。
|
||||||
|
|
||||||
|
**不做**:DSH 宿主工作区/目录(R3 明确不需要);账本写与交易执行(Q4=A);按需取数工具(R2-Q1=A);复盘产物落盘与复盘管理/DB(R2-Q4,未来 T-002/复盘管理另立);通用数据视图(全部持仓/交易记录)作为依据(R2-Q2 收敛,本期仅策略)。
|
||||||
|
|
||||||
|
## 计划阶段待办(定稿后第一件事:技术可行性调研)
|
||||||
|
|
||||||
|
1. **宿主注入通道调研**:DSH 会话如何把外部数据作为上下文注入给模型(injected-context 节点 / 会话 send / 其他公开通道),确认可行通道与形态(决定"摘要以消息形态进会话"还是"上下文节点形态");
|
||||||
|
2. 入口 UI 落点核实:对话视图首条消息前的输入区 accessory / workspace chip 同构位置可由插件挂载的 slot(及 composer 可见性管理,与现有"神之一手 tab 隐藏 composer" CSS 的交互);
|
||||||
|
3. settings 是否需记录策略-会话选择偏好(会话级,不持久化 or 持久化待定);
|
||||||
|
4. 数据摘要服务:服务端生成"策略当前数据摘要"的组成与规模边界(当前持仓字段集 / 交易聚合规则 / 上限裁剪)。
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- 终极目标:目标-002(策略)/ 目标-006(交易复盘)/ 目标-007(人机合一·只读);目标-001(复用宿主会话能力)
|
||||||
|
- 需求池:T-002 复盘记录工具(起草)——复盘管理/落库的未来归属
|
||||||
|
- 设计约束影响(计划阶段核对):技术约束-009(会话头部 slot 机制参照)/ 技术约束-014、产品约束-009(会话结构语义)、UI约束-001(chip 形态参照);如需新约束条目于计划阶段补充
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
**(计划/迭代立项后写入迭代验收标准.md)**
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 需求:R-020 QMT Bridge MCP 能力内建(神之一手自注册 MCP,去三方插件依赖)· 已定稿
|
||||||
|
|
||||||
|
> 登记:2026-09-08 | 来源:老师指令(2026-09-08 新需求)| 状态:**已定稿(2026-09-08,Q1-Q6 老师逐项确认)**
|
||||||
|
> 归属:待立项(拟迭代 18)| 计划:待出 | 实现状态:未开始
|
||||||
|
|
||||||
|
## 需求描述(老师原始诉求)
|
||||||
|
|
||||||
|
> **老师原话(2026-09-08)**:当前我们给 DSH 配置了一个 MCP,用于连接 QMT bridge,这个配置是在另一个插件中配置的(三方插件 dsh-skill-mcp-panel 在 profile cordis.patch.yml 受管块里插入 QMT_Bridge_MCP 实例——管理 UI + 写宿主 patch;真正连接的是 DSH 官方 dsh-mcp-client)。我希望可以在神之一手插件的配置 tab 中,添加了 QMTBridge 的配置后,可以**自动检查 MCP 连接信息**,这样我们**不需要依赖三方插件来加入 MCP 的能力**。
|
||||||
|
>
|
||||||
|
> **2026-09-08 老师澄清(指令修正)**:不要的是**第三方 dsh-skill-mcp-panel**(设置菜单「技能 / MCP」+ 它写的宿主 patch 受管块);**依赖 DSH 官方 @deepseek-ai/dsh-mcp-client 是允许的**。实现 = 神之一手 apply 内动态挂载官方 dsh-mcp-client(参照 mcp_router 实证),**移除三方 dsh-skill-mcp-panel 及其受管块**。
|
||||||
|
|
||||||
|
## 定稿语义
|
||||||
|
|
||||||
|
- **MCP 能力内建**:神之一手在自己的 apply 内按 QMT 连接配置动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client(插件内管理:挂载/切换/卸载/状态都由神之一手控制),QMT Bridge 的 MCP 工具由神之一手自己接入;不再依赖三方 dsh-skill-mcp-panel 的面板与宿主 patch 受管块;
|
||||||
|
- **REST 直连不变**(技术约束-003):插件 UI/tab 数据仍走 REST,MCP 是插件自带的 Agent 侧能力;
|
||||||
|
- **MCP url 自动派生**:激活连接 baseUrl + /mcp,零新增配置字段。
|
||||||
|
|
||||||
|
## 讨论记录(2026-09-08,第一轮,Q1-Q6 老师逐项拍板 → 定稿)
|
||||||
|
|
||||||
|
| # | 问题 | 结论(老师拍板) |
|
||||||
|
|---|---|---|
|
||||||
|
| Q1 | MCP 客户端挂载粒度 | **只使用当前激活的那个连接**:仅激活连接挂 1 个 dsh-mcp-client 实例(serverName 固定 QMT_Bridge_MCP,切换激活即热切换 MCP url,工具名 mcp__QMT_Bridge_MCP__* 保持稳定);不逐连接各挂实例 |
|
||||||
|
| Q2 | 现有三方条目处置 | **现在的移除(2026-09-08 老师澄清:移除对象 = 第三方 dsh-skill-mcp-panel)**:① 卸载/停用三方 dsh-skill-mcp-panel(其「技能 / MCP」设置菜单与宿主 patch 管理职责一并移除);② 删除 cordis.patch.yml 中其受管块(QMT_Bridge_MCP 不再由三方写入);**QMT 的 MCP 连接通过神之一手插件自己管理**——插件 apply 内动态挂载**官方 @deepseek-ai/dsh-mcp-client**(依赖官方库、管理在我们插件内),含连接 CRUD/激活切换的全生命周期联动 |
|
||||||
|
| Q3 | MCP url 与字段 | **自动来**:默认派生 baseUrl + /mcp,不需要每连接 mcpUrl 覆盖字段 |
|
||||||
|
| Q4 | 「自动检查 MCP 连接信息」口径 | **两处状态出口**:① 设置页「QMT 连接配置」连接卡片可检查(手动触发);② **对话窗口顶端状态指示灯区新增一个 MCP 状态**(与现有持仓/盘口同步指示灯并列) |
|
||||||
|
| Q5 | 失败与重连语义 | **采纳建议**:对齐 failOnStartupError:false + reconnect(初始连不上不拒绝插件激活;连接丢失自动指数退避重连;期间保留断开状态不崩插件) |
|
||||||
|
| Q6 | 范围确认 | **不做通用 MCP 管理**,只做 QMT 的 MCP 内建;通用任意 MCP 服务器管理面板不做(该域属 mcp-router / dsh-skill-mcp-panel,另议) |
|
||||||
|
|
||||||
|
## 定稿边界
|
||||||
|
|
||||||
|
**做**:
|
||||||
|
1. 神之一手 apply 内动态挂载/卸载 dsh-mcp-client(唯一实例,serverName=QMT_Bridge_MCP,url=激活连接 baseUrl+/mcp);
|
||||||
|
2. 生命周期联动:启动(激活默认/上次连接)→ 挂载;添加/编辑/删除/激活连接 → MCP 挂/切/卸(对齐现有 qmt-connections API 热切换编排);激活连接被删自动切默认并重挂;
|
||||||
|
3. MCP 状态出口 ×2:设置页连接卡片 MCP 检查/状态;对话窗口顶端状态指示灯区新增 MCP 状态(三态风格对齐产品约束-012 指示灯体系);
|
||||||
|
4. 移除三方 dsh-skill-mcp-panel:卸载其插件 + 移除宿主 cordis.patch.yml 受管块(Q2;属宿主配置变更,按 editing-cordis-compositions 技能流程操作 + 老师确认);
|
||||||
|
5. 失败/重连语义按 Q5(failOnStartupError:false + reconnect)。
|
||||||
|
|
||||||
|
**不做(本期)**:通用 MCP 服务器管理面板(Q6,含自建「任意 MCP 管理 UI」——Q6 明确不做);插件 UI/tab 数据源改走 MCP(技术约束-003 REST 直连不变);多 serverName 并存;自研 MCP 协议客户端(老师定:依赖官方 dsh-mcp-client 即可,不重造轮子)。
|
||||||
|
|
||||||
|
## 计划阶段待办(定稿后第一件事:技术可行性调研)
|
||||||
|
|
||||||
|
1. **依赖解析实证**:one-divine-lot 运行时能否 import(@deepseek-ai/dsh-mcp-client)(宿主 CLI 依赖树 / profile node_modules 解析路径核实,对照 mcp_router 的 peerDependencies 声明与日志实证;确认后在 package.json 补 peer 依赖);
|
||||||
|
2. **动态挂载生命周期设计**:add/update/remove/activate 连接 → MCP 实例 挂/切/卸 编排(对齐 qmt-connections API 现有热切换逻辑,MCP url 随激活 baseUrl);
|
||||||
|
3. **MCP 状态查询与展示**:服务端 MCP 状态端点(已连接/断开 + 工具数 + 错误/重连信息)+ 设置页卡片检查 + 会话头部指示灯新增 MCP 状态(对齐产品约束-012/UI约束的指示灯体系);
|
||||||
|
4. **宿主受管块迁移**:cordis.patch.yml 移除 QMT_Bridge_MCP 块的精确操作步骤(保留块外内容逐字节不动);
|
||||||
|
5. 前端连接卡片表单是否需提示「MCP 将自动随激活连接启用」(Q4 落点 UI 文案)。
|
||||||
|
|
||||||
|
## 关联
|
||||||
|
|
||||||
|
- 终极目标:目标-001(DSH 插件形态·复用宿主 MCP 能力)、目标-008(真实交易系统接入)
|
||||||
|
- 既有需求:R-004(QMT 连接配置,本需求数据基础:settings.qmtConnections + 激活/默认语义 + 热切换编排);R-015(会话头部指示灯体系,MCP 状态灯并入同一区)
|
||||||
|
- 设计约束:技术约束-003(REST 直连不变)、技术约束-008(连接配置存储/热切换);定稿后需补新约束条目(技术约束:「插件内建 MCP 客户端注册规范」;产品约束:「MCP 状态出口(设置卡片 + 会话头部指示灯)」)
|
||||||
|
- 参考实现:mcp_router(ds-plugins/mcp_router)——动态 ctx.plugin(dsh-mcp-client) 实证
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
**(立项后写入迭代验收标准.md)**
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
| R-017 | 委托归属候选纳入近期清仓持仓 | 交易记录 tab 归属候选只回当前持仓——清仓后当日委托失去下拉锚点无法补关联(R-016 关联观察立项,2026-09-07 大连热电实际发生:盘中归属后被清为未关联,下拉已无做T候选)。**2026-09-07 定稿(老师指令确认方向)**:候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记,选项文本「已清仓」后缀),下拉 value 改 holdingId 键控(同策略多轮不撞值),排序当前在前/清仓按 closed_at DESC;当日数据修复经 set-attribution 通道完成(非固化功能)。**2026-09-08 R-018 取代标注:7 天清仓候选退役(R-018 T2 拍板)**——归属候选改策略级 attribution-targets(迭代 16 实施),本需求「清仓后补关联」痛点根因(幽灵清仓抢先归档)已由数据域分界消除;holdingId 键控/占位安全逻辑保留。详见 R-017.md | 老师指令(2026-09-07) | P1 | 已定稿 | 2026-09-07 | 15-归属候选纳入清仓持仓 | 已实现(被 R-018 语义取代) |
|
| R-017 | 委托归属候选纳入近期清仓持仓 | 交易记录 tab 归属候选只回当前持仓——清仓后当日委托失去下拉锚点无法补关联(R-016 关联观察立项,2026-09-07 大连热电实际发生:盘中归属后被清为未关联,下拉已无做T候选)。**2026-09-07 定稿(老师指令确认方向)**:候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记,选项文本「已清仓」后缀),下拉 value 改 holdingId 键控(同策略多轮不撞值),排序当前在前/清仓按 closed_at DESC;当日数据修复经 set-attribution 通道完成(非固化功能)。**2026-09-08 R-018 取代标注:7 天清仓候选退役(R-018 T2 拍板)**——归属候选改策略级 attribution-targets(迭代 16 实施),本需求「清仓后补关联」痛点根因(幽灵清仓抢先归档)已由数据域分界消除;holdingId 键控/占位安全逻辑保留。详见 R-017.md | 老师指令(2026-09-07) | P1 | 已定稿 | 2026-09-07 | 15-归属候选纳入清仓持仓 | 已实现(被 R-018 语义取代) |
|
||||||
| R-018 | 交易关联驱动持仓份额动态调整(归属 = 账本写操作) | 从交易记录出发给委托关联策略/持仓时自动同步调整该策略下持仓份额(关联即账本写操作):策略级候选 + 统一「份额分配器」(一笔委托拆 0..N 段 × 策略×量,余额默认全量可改小,允许部分关联)+ 买入加仓/建新仓(不复活旧行)、卖出减仓/归零清仓、仓不足自动截断续分、无仓关联失败;撤段逆操作(撤建仓买入=持仓作废第三态不产生假清仓历史、撤加仓=减回归零作废、撤卖出=恢复活动仓+份额加回,粒度=段、改归属原子);**数据域分界(老师拍板):幽灵清仓等同步机制只作用于「全部持仓」对账单域,不再写 strategy_holdings 账本**——账本生命周期只由交易关联 + 手动份额操作驱动,账本转历史唯一途径=卖出单关联 closeHolding;R-017 7 天清仓候选退役(holdingId 键控/占位保留);漏关联软提示(账本有活动行+QMT 无此 code)。影响既有定稿:技术约束-017/产品约束-011/R-014/迭代12 幽灵清仓条款修订。详见 R-018.md | 老师指令(2026-09-08 新优化需求讨论) | P1 | 已定稿 | 2026-09-08 | 16-交易关联驱动持仓份额动态调整 | **已实现(已归档至 已完成/R-018.md,2026-09-08 验收通过)** |
|
| R-018 | 交易关联驱动持仓份额动态调整(归属 = 账本写操作) | 从交易记录出发给委托关联策略/持仓时自动同步调整该策略下持仓份额(关联即账本写操作):策略级候选 + 统一「份额分配器」(一笔委托拆 0..N 段 × 策略×量,余额默认全量可改小,允许部分关联)+ 买入加仓/建新仓(不复活旧行)、卖出减仓/归零清仓、仓不足自动截断续分、无仓关联失败;撤段逆操作(撤建仓买入=持仓作废第三态不产生假清仓历史、撤加仓=减回归零作废、撤卖出=恢复活动仓+份额加回,粒度=段、改归属原子);**数据域分界(老师拍板):幽灵清仓等同步机制只作用于「全部持仓」对账单域,不再写 strategy_holdings 账本**——账本生命周期只由交易关联 + 手动份额操作驱动,账本转历史唯一途径=卖出单关联 closeHolding;R-017 7 天清仓候选退役(holdingId 键控/占位保留);漏关联软提示(账本有活动行+QMT 无此 code)。影响既有定稿:技术约束-017/产品约束-011/R-014/迭代12 幽灵清仓条款修订。详见 R-018.md | 老师指令(2026-09-08 新优化需求讨论) | P1 | 已定稿 | 2026-09-08 | 16-交易关联驱动持仓份额动态调整 | **已实现(已归档至 已完成/R-018.md,2026-09-08 验收通过)** |
|
||||||
|
|
||||||
|
| R-019 | 策略会话:会话以策略当前数据为依据做复盘/分析讨论 | 让 AI 会话以策略为 Workspace(老师原话)→ **已定稿(2026-09-08,F1/F2 拍板)**:改造现有 DSH 对话窗口,对话视图首条消息前提供「讨论策略」选择(仿 DSH workspace chip Choose 体验:依据:做T ▾),选策略即把该策略**当前数据摘要**预注入会话上下文开始聊,会话中可换策略/刷新再注入;**不用 DSH 宿主工作区**(R3);注入 = 当前持仓明细 + 全历史关联交易聚合摘要(含已清仓,F2);只读不开放账本写(Q4=A);产物不落盘(R2-Q4,复盘管理/落库归未来 T-002)。宿主注入通道等技术调研列计划阶段待办。详见 R-019.md | 老师指令(2026-09-08) | P1 | 已定稿 | 2026-09-08 | 17-策略会话 | 已立项(迭代 17 设计阶段) |
|
||||||
|
| R-020 | QMT Bridge MCP 能力内建(神之一手自注册 MCP,去三方插件依赖) | 当前 DSH 的 QMT Bridge MCP 由三方插件 dsh-skill-mcp-panel 写入 profile cordis.patch.yml 受管块。**2026-09-08 定稿(Q1-Q6 老师逐项拍板)**:神之一手 apply 内动态挂载 **DSH 官方 dsh-mcp-client**(**只挂当前激活连接**,serverName 固定 QMT_Bridge_MCP,url 自动派生 baseUrl+/mcp;挂载/切换/卸载/状态由神之一手插件内管理),MCP 能力内建;**移除对象 = 三方 dsh-skill-mcp-panel**(2026-09-08 老师澄清:卸载其插件 + 移除其宿主 cordis.patch.yml 受管块,官方 dsh-mcp-client 可依赖);MCP 生命周期随连接 CRUD/激活切换联动;状态出口 = 设置页连接卡片检查 + **会话头部指示灯区新增 MCP 状态灯**;失败重连对齐 failOnStartupError:false+reconnect;**不做通用 MCP 管理**。REST 直连不变(技术约束-003)。详见 R-020.md | 老师指令(2026-09-08) | P1 | 已定稿 | 2026-09-08 | -(拟迭代 18) | 未开始 |
|
||||||
|
|
||||||
## 渐进明细规划素材
|
## 渐进明细规划素材
|
||||||
|
|
||||||
> 2026-08-27 老师确立「渐进明细」:前期目标笼统,先实现周边工具,做中学逐步明确计划。以下周边工具候选为规划素材,**状态为「起草」级**(未定稿,不进入计划/迭代范围),待讨论与实践中逐步明确并转入正式需求。
|
> 2026-08-27 老师确立「渐进明细」:前期目标笼统,先实现周边工具,做中学逐步明确计划。以下周边工具候选为规划素材,**状态为「起草」级**(未定稿,不进入计划/迭代范围),待讨论与实践中逐步明确并转入正式需求。
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -33,7 +33,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@deepseek-ai/schemastery": "^3.18.1"
|
"@deepseek-ai/dsh-mcp-client": "0.1.1-rc.2",
|
||||||
|
"@deepseek-ai/schemastery": "^3.18.1",
|
||||||
|
"@modelcontextprotocol/sdk": "1.30.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@deepseek-ai/cordis": "^4.0.1",
|
"@deepseek-ai/cordis": "^4.0.1",
|
||||||
|
|||||||
Generated
+797
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -60,8 +60,8 @@ const HANDLERS = [
|
|||||||
* @param {import('@deepseek-ai/cordis').Context} ctx
|
* @param {import('@deepseek-ai/cordis').Context} ctx
|
||||||
* @param {object} runtime { manager, settings, dataSource, marketCache }
|
* @param {object} runtime { manager, settings, dataSource, marketCache }
|
||||||
*/
|
*/
|
||||||
export function registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution }) {
|
export function registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution, mcpManager }) {
|
||||||
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution };
|
const runtime = { ctx, manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution, mcpManager };
|
||||||
|
|
||||||
ctx.effect(() => ctx.webServer.register({
|
ctx.effect(() => ctx.webServer.register({
|
||||||
kind: 'prefix',
|
kind: 'prefix',
|
||||||
|
|||||||
+2
-1
@@ -42,7 +42,7 @@ function projectQuote(hub, code, snap) {
|
|||||||
* @param {object} args
|
* @param {object} args
|
||||||
* @param {object} runtime { settings, dataSource, marketHub, marketFeed(=QuoteSync), qmtHealthMonitor, positionSync }
|
* @param {object} runtime { settings, dataSource, marketHub, marketFeed(=QuoteSync), qmtHealthMonitor, positionSync }
|
||||||
*/
|
*/
|
||||||
export async function handleMarket(method, args, { settings, dataSource, marketHub, marketFeed: marketSync, qmtHealthMonitor, positionSync }) {
|
export async function handleMarket(method, args, { settings, dataSource, marketHub, marketFeed: marketSync, qmtHealthMonitor, positionSync, mcpManager }) {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'market-snapshot': {
|
case 'market-snapshot': {
|
||||||
const codes = Array.isArray(args.codes) ? args.codes : [];
|
const codes = Array.isArray(args.codes) ? args.codes : [];
|
||||||
@@ -86,6 +86,7 @@ export async function handleMarket(method, args, { settings, dataSource, marketH
|
|||||||
? { ...marketSync.getStatus(), state: state(marketSync.getStatus()) }
|
? { ...marketSync.getStatus(), state: state(marketSync.getStatus()) }
|
||||||
: null,
|
: null,
|
||||||
qmt: qmtHealthMonitor ? qmtHealthMonitor.getStatus() : null,
|
qmt: qmtHealthMonitor ? qmtHealthMonitor.getStatus() : null,
|
||||||
|
mcp: mcpManager ? mcpManager.getStatus() : null, // R-020/迭代18:QMT MCP 状态(会话头部指示灯)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'sync-now': {
|
case 'sync-now': {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const QMT_METHODS = new Set([
|
|||||||
'qmt-connections/set-default',
|
'qmt-connections/set-default',
|
||||||
'qmt-connections/test',
|
'qmt-connections/test',
|
||||||
'qmt-connections/active-host',
|
'qmt-connections/active-host',
|
||||||
|
'qmt-connections/mcp-status', // R-020/迭代18:QMT MCP 状态(probe / 缓存)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** 服务端代理测试连接:GET {baseUrl}/health(浏览器跨域规避,Q3 手动测试) */
|
/** 服务端代理测试连接:GET {baseUrl}/health(浏览器跨域规避,Q3 手动测试) */
|
||||||
@@ -63,7 +64,7 @@ async function testQmtConnection(ctx, args) {
|
|||||||
* @param {object} args
|
* @param {object} args
|
||||||
* @param {object} runtime { manager, settings, dataSource, marketCache }
|
* @param {object} runtime { manager, settings, dataSource, marketCache }
|
||||||
*/
|
*/
|
||||||
export async function handleQmt(method, args, { ctx, settings, dataSource, marketFeed, qmtHealthMonitor }) {
|
export async function handleQmt(method, args, { ctx, settings, dataSource, marketFeed, qmtHealthMonitor, mcpManager }) {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'qmt-connections': {
|
case 'qmt-connections': {
|
||||||
const state = getQmtConnections(settings);
|
const state = getQmtConnections(settings);
|
||||||
@@ -80,6 +81,8 @@ export async function handleQmt(method, args, { ctx, settings, dataSource, marke
|
|||||||
marketFeed?.setBaseUrl(value.baseUrl);
|
marketFeed?.setBaseUrl(value.baseUrl);
|
||||||
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
||||||
ctx?.logger?.info('[one-divine-lot] 激活配置地址已变更,数据源热切换 → ' + value.baseUrl);
|
ctx?.logger?.info('[one-divine-lot] 激活配置地址已变更,数据源热切换 → ' + value.baseUrl);
|
||||||
|
// R-020/迭代18:MCP 目标地址随激活地址变更重挂
|
||||||
|
mcpManager?.resync(value.baseUrl).catch(() => {});
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -94,6 +97,8 @@ export async function handleQmt(method, args, { ctx, settings, dataSource, marke
|
|||||||
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
||||||
ctx?.logger?.info('[one-divine-lot] 激活配置已删除,自动切换 → ' + active.name + ' (' + active.baseUrl + ')');
|
ctx?.logger?.info('[one-divine-lot] 激活配置已删除,自动切换 → ' + active.name + ' (' + active.baseUrl + ')');
|
||||||
}
|
}
|
||||||
|
// R-020/迭代18:激活已变(或列表清空)→ MCP 跟随新激活 / 卸载
|
||||||
|
mcpManager?.resync(active?.baseUrl).catch(() => {});
|
||||||
return { ...afterState, active, removedWasActive: result.removedWasActive, removedWasDefault: result.removedWasDefault };
|
return { ...afterState, active, removedWasActive: result.removedWasActive, removedWasDefault: result.removedWasDefault };
|
||||||
}
|
}
|
||||||
case 'qmt-connections/activate': {
|
case 'qmt-connections/activate': {
|
||||||
@@ -105,6 +110,8 @@ export async function handleQmt(method, args, { ctx, settings, dataSource, marke
|
|||||||
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
qmtHealthMonitor?.probe(); // 切换后立即补探健康
|
||||||
ctx?.logger?.info('[one-divine-lot] QMT 连接激活: ' + active.name + ' (' + active.baseUrl + ')');
|
ctx?.logger?.info('[one-divine-lot] QMT 连接激活: ' + active.name + ' (' + active.baseUrl + ')');
|
||||||
}
|
}
|
||||||
|
// R-020/迭代18:激活切换 → MCP 实例跟随(卸旧挂新,url=新激活 baseUrl+/mcp)
|
||||||
|
mcpManager?.resync(active?.baseUrl).catch(() => {});
|
||||||
return { ...state, active };
|
return { ...state, active };
|
||||||
}
|
}
|
||||||
case 'qmt-connections/set-default':
|
case 'qmt-connections/set-default':
|
||||||
@@ -132,6 +139,12 @@ export async function handleQmt(method, args, { ctx, settings, dataSource, marke
|
|||||||
} catch { /* baseUrl 非法时返回空 */ }
|
} catch { /* baseUrl 非法时返回空 */ }
|
||||||
return { name: active.name, baseUrl, host, wsUrl, activeId: state.activeId ?? '' };
|
return { name: active.name, baseUrl, host, wsUrl, activeId: state.activeId ?? '' };
|
||||||
}
|
}
|
||||||
|
case 'qmt-connections/mcp-status': {
|
||||||
|
// R-020/迭代18:MCP 状态(缓存秒回;refresh=true 即时探测)
|
||||||
|
if (!mcpManager) return { state: 'disabled', error: 'mcpManager 不可用', checkedAt: 0 };
|
||||||
|
if (args.refresh) return await mcpManager.probe();
|
||||||
|
return mcpManager.getStatus();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw Object.assign(new Error('unknown qmt method: ' + method), { code: 'not-found' });
|
throw Object.assign(new Error('unknown qmt method: ' + method), { code: 'not-found' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -520,6 +520,8 @@ function QmtConnectionsSettings() {
|
|||||||
const [editingId, setEditingId] = useState(null);
|
const [editingId, setEditingId] = useState(null);
|
||||||
const [formName, setFormName] = useState('');
|
const [formName, setFormName] = useState('');
|
||||||
const [formUrl, setFormUrl] = useState('');
|
const [formUrl, setFormUrl] = useState('');
|
||||||
|
const [mcpStatus, setMcpStatus] = useState(null); // R-020/迭代18:MCP 状态
|
||||||
|
const [mcpChecking, setMcpChecking] = useState(false);
|
||||||
const call = useRpc();
|
const call = useRpc();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -533,7 +535,14 @@ function QmtConnectionsSettings() {
|
|||||||
}
|
}
|
||||||
}, [call]);
|
}, [call]);
|
||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
const loadMcp = useCallback(async (refresh) => {
|
||||||
|
try {
|
||||||
|
const res = await call('one-divine-lot/qmt-connections/mcp-status', { args: { refresh: !!refresh } });
|
||||||
|
if (res && res.ok) setMcpStatus(res.value);
|
||||||
|
} catch { /* 静默 */ }
|
||||||
|
}, [call]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); loadMcp(false); }, [load, loadMcp]);
|
||||||
|
|
||||||
const openAdd = () => { setEditingId(null); setFormName(''); setFormUrl(''); setFormOpen(true); setTestResult(null); };
|
const openAdd = () => { setEditingId(null); setFormName(''); setFormUrl(''); setFormOpen(true); setTestResult(null); };
|
||||||
const openEdit = (c) => { setEditingId(c.id); setFormName(c.name); setFormUrl(c.baseUrl); setFormOpen(true); setTestResult(null); };
|
const openEdit = (c) => { setEditingId(c.id); setFormName(c.name); setFormUrl(c.baseUrl); setFormOpen(true); setTestResult(null); };
|
||||||
@@ -576,6 +585,7 @@ function QmtConnectionsSettings() {
|
|||||||
const res = await call('one-divine-lot/qmt-connections/activate', { args: { id: c.id } });
|
const res = await call('one-divine-lot/qmt-connections/activate', { args: { id: c.id } });
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
setState(res.value);
|
setState(res.value);
|
||||||
|
loadMcp(false); // R-020/迭代18:激活切换后刷新 MCP 状态
|
||||||
toast.success('已激活:' + c.name + '(立即生效)');
|
toast.success('已激活:' + c.name + '(立即生效)');
|
||||||
} else {
|
} else {
|
||||||
toast.error((res && res.error && res.error.message) || '激活失败');
|
toast.error((res && res.error && res.error.message) || '激活失败');
|
||||||
@@ -679,6 +689,41 @@ function QmtConnectionsSettings() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* R-020/迭代18:MCP 连接状态条(随激活连接;绿=已连接+N工具 / 黄=连接中 / 红=断开 / 灰=未启用) */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, minWidth: 0, fontSize: 12 }}>
|
||||||
|
<span style={{ flex: 'none', fontWeight: 600 }}>MCP 连接</span>
|
||||||
|
<span style={{
|
||||||
|
flex: 'none', width: 10, height: 10, borderRadius: '50%',
|
||||||
|
background: mcpStatus?.state === 'connected'
|
||||||
|
? 'var(--dsw-alias-state-success-primary, #2e7d32)'
|
||||||
|
: mcpStatus?.state === 'connecting'
|
||||||
|
? 'var(--dsw-alias-state-warn-primary, #f9a825)'
|
||||||
|
: mcpStatus?.state === 'error'
|
||||||
|
? 'var(--dsw-alias-state-error-primary, #c62828)'
|
||||||
|
: 'var(--dsw-alias-label-tertiary, #bbb)',
|
||||||
|
}} />
|
||||||
|
<EllipsisText
|
||||||
|
text={mcpStatus?.state === 'connected'
|
||||||
|
? '已连接' + (mcpStatus.toolCount != null ? '(' + mcpStatus.toolCount + ' 个工具)' : '') + ' · ' + (mcpStatus.url || '')
|
||||||
|
: mcpStatus?.state === 'connecting' ? '连接中…'
|
||||||
|
: mcpStatus?.state === 'error' ? '断开 · ' + String(mcpStatus.error || '').slice(0, 80)
|
||||||
|
: mcpStatus ? '未启用(无激活连接)' : '检测中…'}
|
||||||
|
maxWidth={520}
|
||||||
|
title={mcpStatus?.error ? String(mcpStatus.error) : (mcpStatus?.url || '')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
if (mcpChecking) return;
|
||||||
|
setMcpChecking(true);
|
||||||
|
try { await loadMcp(true); } finally { setMcpChecking(false); }
|
||||||
|
}}
|
||||||
|
disabled={mcpChecking}
|
||||||
|
style={{ flex: 'none', padding: '2px 10px', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2, #ccc)', borderRadius: 4, background: 'var(--dsw-alias-bg-layer-1, #fff)', fontSize: 12 }}
|
||||||
|
>
|
||||||
|
{mcpChecking ? '检测中…' : '检测 MCP'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && <div style={{ color: 'var(--dsw-alias-state-error-primary, #c62828)', marginBottom: 12 }}><EllipsisText text={error} maxWidth={560} /></div>}
|
{error && <div style={{ color: 'var(--dsw-alias-state-error-primary, #c62828)', marginBottom: 12 }}><EllipsisText text={error} maxWidth={560} /></div>}
|
||||||
{!state && !error && <div style={{ color: 'var(--dsw-alias-label-secondary, #666)' }}>加载中...</div>}
|
{!state && !error && <div style={{ color: 'var(--dsw-alias-label-secondary, #666)' }}>加载中...</div>}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* SyncIndicators —— 数据同步指示灯组(R-015 / 迭代 13;2026-09-03 老师调整:三灯带全称标签)
|
* SyncIndicators —— 数据同步指示灯组(R-015 / 迭代 13;2026-09-03 老师调整:三灯带全称标签)
|
||||||
*
|
*
|
||||||
* 位置:会话头部(QmtConnectionChip 内,QMT 下拉框右侧)。
|
* 位置:会话头部(QmtConnectionChip 内,QMT 下拉框右侧)。
|
||||||
* 形态:QMT连接(灯)|持仓数据(灯)|行情数据(灯)——统一圆点 + 全称标签。
|
* 形态:QMT连接(灯)|持仓数据(灯)|行情数据(灯)|MCP(灯,R-020/迭代18)——统一圆点 + 全称标签。
|
||||||
*
|
*
|
||||||
* 状态(产品约束-012):
|
* 状态(产品约束-012):
|
||||||
* - QMT 连接:绿=健康 / 红=异常 / 灰=未知(qmt-health 缓存)
|
* - QMT 连接:绿=健康 / 红=异常 / 灰=未知(qmt-health 缓存)
|
||||||
@@ -87,6 +87,19 @@ export function SyncIndicators() {
|
|||||||
}
|
}
|
||||||
}, [call, healthRefreshing, load]);
|
}, [call, healthRefreshing, load]);
|
||||||
|
|
||||||
|
// R-020/迭代18:MCP 状态即时刷新(点灯触发)
|
||||||
|
const [mcpRefreshing, setMcpRefreshing] = useState(false);
|
||||||
|
const refreshMcp = useCallback(async () => {
|
||||||
|
if (mcpRefreshing) return;
|
||||||
|
setMcpRefreshing(true);
|
||||||
|
try {
|
||||||
|
await call('one-divine-lot/qmt-connections/mcp-status', { args: { refresh: true } });
|
||||||
|
} catch { /* 静默 */ } finally {
|
||||||
|
setMcpRefreshing(false);
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
}, [call, mcpRefreshing, load]);
|
||||||
|
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
|
|
||||||
// QMT 连接灯:绿=健康 / 红=异常 / 灰=未知
|
// QMT 连接灯:绿=健康 / 红=异常 / 灰=未知
|
||||||
@@ -115,6 +128,27 @@ export function SyncIndicators() {
|
|||||||
return lines.filter(Boolean).join(' | ');
|
return lines.filter(Boolean).join(' | ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MCP 灯(R-020/迭代18):绿=已连接(connected) / 黄=连接中(connecting) / 红=断开(error) / 灰=未启用(disabled/缺失)
|
||||||
|
const mcp = status.mcp;
|
||||||
|
const mcpColor = mcp?.state === 'connected'
|
||||||
|
? 'var(--dsw-alias-state-success-primary, #2e7d32)'
|
||||||
|
: mcp?.state === 'connecting'
|
||||||
|
? 'var(--dsw-alias-state-warn-primary, #f9a825)'
|
||||||
|
: mcp?.state === 'error'
|
||||||
|
? 'var(--dsw-alias-state-error-primary, #d32f2f)'
|
||||||
|
: 'var(--dsw-alias-label-tertiary, #bbb)';
|
||||||
|
const mcpTitleLines = [
|
||||||
|
'MCP:' + ({
|
||||||
|
connected: '已连接' + (mcp?.toolCount != null ? '(' + mcp.toolCount + ' 个工具)' : ''),
|
||||||
|
connecting: '连接中',
|
||||||
|
error: '断开' + (mcp?.error ? ':' + String(mcp.error).slice(0, 60) : ''),
|
||||||
|
disabled: '未启用(无激活连接)',
|
||||||
|
}[mcp?.state] ?? '未知'),
|
||||||
|
mcp?.url ? mcp.url : null,
|
||||||
|
mcp?.checkedAt ? '检测于 ' + new Date(mcp.checkedAt).toLocaleTimeString() : null,
|
||||||
|
'点击重新检测',
|
||||||
|
].filter(Boolean).join(' | ');
|
||||||
|
|
||||||
const sep = <span style={{ color: 'var(--dsw-alias-border-l2, #ddd)', fontSize: 12, flex: 'none' }}>|</span>;
|
const sep = <span style={{ color: 'var(--dsw-alias-border-l2, #ddd)', fontSize: 12, flex: 'none' }}>|</span>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -138,6 +172,8 @@ export function SyncIndicators() {
|
|||||||
onClick={() => syncNow('quote')}
|
onClick={() => syncNow('quote')}
|
||||||
dimmed={syncingDomain === 'quote'}
|
dimmed={syncingDomain === 'quote'}
|
||||||
/>
|
/>
|
||||||
|
{sep}
|
||||||
|
<IndicatorDot label="MCP" color={mcpColor} glow={mcp?.state === 'connected'} title={mcpTitleLines} onClick={refreshMcp} dimmed={mcpRefreshing} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -24,6 +24,7 @@ import { QuoteSync } from './market/QuoteSync.js';
|
|||||||
import { QmtHealthMonitor } from './data-source/QmtHealthMonitor.js';
|
import { QmtHealthMonitor } from './data-source/QmtHealthMonitor.js';
|
||||||
import { TradeSync } from './trades/TradeSync.js';
|
import { TradeSync } from './trades/TradeSync.js';
|
||||||
import { AttributionService } from './trades/AttributionService.js';
|
import { AttributionService } from './trades/AttributionService.js';
|
||||||
|
import { QmtMcpManager } from './mcp/QmtMcpManager.js'; // R-020/迭代18:QMT Bridge MCP 能力内建
|
||||||
|
|
||||||
const name = 'one-divine-lot';
|
const name = 'one-divine-lot';
|
||||||
|
|
||||||
@@ -90,9 +91,12 @@ async function apply(ctx, config) {
|
|||||||
|
|
||||||
// R-018 迭代 16:归属服务(关联 = 账本写操作;经 SqliteStore 段表/生命周期同步接口)
|
// R-018 迭代 16:归属服务(关联 = 账本写操作;经 SqliteStore 段表/生命周期同步接口)
|
||||||
const attribution = new AttributionService({ store: storage.sqlite });
|
const attribution = new AttributionService({ store: storage.sqlite });
|
||||||
|
// R-020/迭代18:QMT Bridge MCP 客户端管理(动态挂载官方 dsh-mcp-client;随激活连接挂载/切换)
|
||||||
|
const mcpManager = new QmtMcpManager({ ctx, runtime: { settings, dataSource }, logger });
|
||||||
|
mcpManager.start(); // 启动即按激活连接挂载 + 探测(内部 catch 不阻塞)
|
||||||
|
|
||||||
// S6: 服务端 HTTP API(R-004:注入 dataSource 以编排激活热切换;R-009:注入 storage 供 trades/history;R-018:注入 attribution)
|
// S6: 服务端 HTTP API(R-004:注入 dataSource 以编排激活热切换;R-009:注入 storage 供 trades/history;R-018:注入 attribution)
|
||||||
registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution });
|
registerApi(ctx, { manager, settings, dataSource, marketHub, marketFeed, qmtHealthMonitor, storage, tradeSync, positionSync, attribution, mcpManager });
|
||||||
|
|
||||||
// 启动时可用性检查(日志,不阻塞)
|
// 启动时可用性检查(日志,不阻塞)
|
||||||
dataSource.isAvailable().then((ok) => {
|
dataSource.isAvailable().then((ok) => {
|
||||||
@@ -108,6 +112,7 @@ async function apply(ctx, config) {
|
|||||||
qmtHealthMonitor.stop();
|
qmtHealthMonitor.stop();
|
||||||
positionSync.stop(); // 停止持仓快照定时同步(2026-09-02;内存快照保留供收尾读取)
|
positionSync.stop(); // 停止持仓快照定时同步(2026-09-02;内存快照保留供收尾读取)
|
||||||
tradeSync.stop(); // 停止交易记录定时同步(迭代 07)
|
tradeSync.stop(); // 停止交易记录定时同步(迭代 07)
|
||||||
|
mcpManager.dispose(); // R-020/迭代18:卸载 QMT MCP 实例
|
||||||
storage.close(); // 关闭 SQLite 连接(迭代 06)
|
storage.close(); // 关闭 SQLite 连接(迭代 06)
|
||||||
logger.info('[one-divine-lot] 插件已释放');
|
logger.info('[one-divine-lot] 插件已释放');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* QmtMcpManager —— QMT Bridge MCP 客户端管理(R-020 / 迭代 18)
|
||||||
|
*
|
||||||
|
* 职责:在神之一手插件 apply 内动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client,
|
||||||
|
* 把 QMT Bridge /mcp 的工具以 mcp__QMT_Bridge_MCP__* 注册给模型;管理 MCP 实例的
|
||||||
|
* 挂载 / 切换 / 卸载(随 QMT 连接配置激活联动),并提供连接状态(SDK 独立探测)。
|
||||||
|
*
|
||||||
|
* 设计约束(R-020 Q1-Q6):
|
||||||
|
* - Q1 只挂当前激活连接:唯一实例,serverName 固定 QMT_Bridge_MCP;
|
||||||
|
* - Q3 MCP url 自动派生:激活连接 baseUrl + /mcp;
|
||||||
|
* - Q5 失败不崩:failOnStartupError:false + reconnect(对齐宿主受管块同参);
|
||||||
|
* - Q4 状态出口:mcp-status 端点 + sync-status.mcp 域(设置页 + 会话头部指示灯)。
|
||||||
|
*
|
||||||
|
* 实现参照:mcp_router(ctx.plugin 动态挂载 dsh-mcp-client 实证);
|
||||||
|
* 挂载错误必须 catch(同 serverName 重复 / 依赖缺失),不影响插件其余功能。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
|
import { resolveActiveBaseUrl } from '../api/common.js';
|
||||||
|
|
||||||
|
const SERVER_NAME = 'QMT_Bridge_MCP';
|
||||||
|
const PROBE_TIMEOUT_MS = 8000;
|
||||||
|
const MOUNT_CONFIG_BASE = {
|
||||||
|
serverName: SERVER_NAME,
|
||||||
|
transport: 'streamable-http',
|
||||||
|
headers: {},
|
||||||
|
toolCallTimeoutMs: 60000,
|
||||||
|
failOnStartupError: false,
|
||||||
|
reconnect: { enabled: true, initialDelayMs: 500, maxDelayMs: 30000, maxAttempts: 10 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export class QmtMcpManager {
|
||||||
|
/**
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {object} opts.ctx Cordis 宿主 ctx
|
||||||
|
* @param {object} opts.runtime { settings, dataSource } —— 解析激活 baseUrl
|
||||||
|
* @param {object} [opts.logger]
|
||||||
|
*/
|
||||||
|
constructor({ ctx, runtime, logger } = {}) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
this.runtime = runtime;
|
||||||
|
this.logger = logger ?? console;
|
||||||
|
/** @type {Promise<object>|null} dsh-mcp-client 模块(惰性加载) */
|
||||||
|
this._modulePromise = null;
|
||||||
|
/** @type {object|null} 当前挂载的 fiber(ctx.plugin 返回值,含 dispose) */
|
||||||
|
this._fiber = null;
|
||||||
|
/** @type {string} 当前已挂载的 mcp url('' = 未挂载) */
|
||||||
|
this._currentUrl = '';
|
||||||
|
/** 状态缓存:{ state, serverName, url, mounted, toolCount, error, latencyMs, checkedAt } */
|
||||||
|
this.status = this._disabledStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
_disabledStatus() {
|
||||||
|
return {
|
||||||
|
state: 'disabled', serverName: SERVER_NAME, url: '', mounted: false,
|
||||||
|
toolCount: 0, error: null, latencyMs: 0, checkedAt: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前激活连接对应的 mcp url(baseUrl + /mcp);无激活连接返回 '' */
|
||||||
|
_currentMcpUrl() {
|
||||||
|
const base = resolveActiveBaseUrl(this.runtime.settings, this.runtime.dataSource);
|
||||||
|
return base ? String(base).replace(/\/+$/, '') + '/mcp' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 惰性加载 dsh-mcp-client 模块(失败不抛,返回 null) */
|
||||||
|
async _loadModule() {
|
||||||
|
if (!this._modulePromise) {
|
||||||
|
this._modulePromise = import('@deepseek-ai/dsh-mcp-client')
|
||||||
|
.catch((err) => {
|
||||||
|
this.logger.warn?.('[one-divine-lot] 无法加载 @deepseek-ai/dsh-mcp-client: ' + (err?.message ?? err));
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._modulePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 启动:解析当前激活 baseUrl → 挂载 + 探测(非阻塞,内部 catch) */
|
||||||
|
async start() {
|
||||||
|
try {
|
||||||
|
const url = this._currentMcpUrl();
|
||||||
|
if (url) {
|
||||||
|
await this._mount(url);
|
||||||
|
} else {
|
||||||
|
this.status = this._disabledStatus();
|
||||||
|
}
|
||||||
|
await this.probe();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn?.('[one-divine-lot] QMT MCP 启动异常: ' + (err?.message ?? err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂载 dsh-mcp-client 到指定 mcp url(先卸旧再挂新;同址幂等)。
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
async _mount(url) {
|
||||||
|
const cleanUrl = String(url ?? '').trim().replace(/\/+$/, '');
|
||||||
|
if (!cleanUrl) {
|
||||||
|
await this.unmount();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._currentUrl === cleanUrl && this._fiber) return; // 已挂同址
|
||||||
|
|
||||||
|
this._unmountFiber();
|
||||||
|
const mod = await this._loadModule();
|
||||||
|
if (!mod) {
|
||||||
|
this._currentUrl = cleanUrl;
|
||||||
|
this.status = {
|
||||||
|
state: 'error', serverName: SERVER_NAME, url: cleanUrl, mounted: false,
|
||||||
|
toolCount: 0, error: '@deepseek-ai/dsh-mcp-client 依赖不可用', latencyMs: 0, checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = { ...MOUNT_CONFIG_BASE, url: cleanUrl };
|
||||||
|
try {
|
||||||
|
const fiber = this.ctx.plugin(mod, config);
|
||||||
|
this._fiber = fiber;
|
||||||
|
this._currentUrl = cleanUrl;
|
||||||
|
this.status = {
|
||||||
|
...this._disabledStatus(), state: 'connecting', url: cleanUrl, mounted: true, checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
// 激活结果(连接 + 工具同步)非致命:成功/失败都记录,不抛给插件主体
|
||||||
|
fiber.then(
|
||||||
|
() => this.logger.info?.('[one-divine-lot] QMT MCP 已挂载: ' + SERVER_NAME + ' (' + cleanUrl + ')'),
|
||||||
|
(err) => {
|
||||||
|
this.logger.warn?.('[one-divine-lot] QMT MCP 挂载异常: ' + (err?.message ?? err));
|
||||||
|
// 挂载失败(如同名 serverName 冲突 / 连接失败)→ 状态标 error,不崩插件
|
||||||
|
this.status = {
|
||||||
|
...this.status, state: 'error',
|
||||||
|
error: (err?.message ?? String(err)).slice(0, 300), checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn?.('[one-divine-lot] QMT MCP 挂载抛出: ' + (err?.message ?? err));
|
||||||
|
this._fiber = null;
|
||||||
|
this._currentUrl = cleanUrl;
|
||||||
|
this.status = {
|
||||||
|
...this._disabledStatus(), state: 'error', url: cleanUrl, mounted: false,
|
||||||
|
error: (err?.message ?? String(err)).slice(0, 300), checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卸载当前 fiber(内部 dispose,失败静默) */
|
||||||
|
_unmountFiber() {
|
||||||
|
const fiber = this._fiber;
|
||||||
|
this._fiber = null;
|
||||||
|
if (fiber) {
|
||||||
|
try { fiber.dispose?.(); } catch { /* already disposed */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 显式卸载(置 disabled 状态) */
|
||||||
|
async unmount() {
|
||||||
|
this._unmountFiber();
|
||||||
|
this._currentUrl = '';
|
||||||
|
this.status = this._disabledStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接配置热切换联动(activate / update 地址 / remove 激活后调用)。
|
||||||
|
* baseUrl 变 → 卸旧挂新;不变 → 仅刷新探测。
|
||||||
|
* @param {string} baseUrl 新的激活连接 baseUrl(可能为空 = 无激活)
|
||||||
|
*/
|
||||||
|
async resync(baseUrl) {
|
||||||
|
try {
|
||||||
|
const next = baseUrl ? String(baseUrl).replace(/\/+$/, '') + '/mcp' : '';
|
||||||
|
if (!next) {
|
||||||
|
await this.unmount();
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
if (next === this._currentUrl && this._fiber) {
|
||||||
|
await this.probe();
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
await this._mount(next);
|
||||||
|
await this.probe();
|
||||||
|
return this.status;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn?.('[one-divine-lot] QMT MCP resync 异常: ' + (err?.message ?? err));
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 独立探测:SDK Client 连 {url} → initialize + tools/list → 工具数/延迟。
|
||||||
|
* 与 dsh-mcp-client 自管连接互不干扰(只读检查,用完即关)。
|
||||||
|
* @param {string} [url] 缺省用当前激活派生 url
|
||||||
|
*/
|
||||||
|
async probe(url) {
|
||||||
|
const target = String(url ?? this._currentMcpUrl() ?? '').trim().replace(/\/+$/, '');
|
||||||
|
if (!target) {
|
||||||
|
this.status = this._disabledStatus();
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
const startedAt = Date.now();
|
||||||
|
let client = null;
|
||||||
|
try {
|
||||||
|
client = new Client({ name: 'one-divine-lot-mcp-probe', version: '0.1.0' }, { capabilities: {} });
|
||||||
|
const transport = new StreamableHTTPClientTransport(new URL(target), {
|
||||||
|
requestInit: { headers: {} },
|
||||||
|
});
|
||||||
|
await Promise.race([
|
||||||
|
client.connect(transport),
|
||||||
|
new Promise((_, rej) => setTimeout(() => rej(new Error('探测超时 (' + PROBE_TIMEOUT_MS + 'ms)')), PROBE_TIMEOUT_MS)),
|
||||||
|
]);
|
||||||
|
const list = await Promise.race([
|
||||||
|
client.listTools(),
|
||||||
|
new Promise((_, rej) => setTimeout(() => rej(new Error('探测超时 (' + PROBE_TIMEOUT_MS + 'ms)')), PROBE_TIMEOUT_MS)),
|
||||||
|
]);
|
||||||
|
const tools = Array.isArray(list?.tools) ? list.tools : [];
|
||||||
|
this.status = {
|
||||||
|
state: 'connected', serverName: SERVER_NAME, url: target,
|
||||||
|
mounted: !!this._fiber && this._currentUrl === target,
|
||||||
|
toolCount: tools.length, error: null,
|
||||||
|
latencyMs: Date.now() - startedAt, checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
try { await client.close(); } catch { /* ignore */ }
|
||||||
|
} catch (err) {
|
||||||
|
this.status = {
|
||||||
|
state: 'error', serverName: SERVER_NAME, url: target,
|
||||||
|
mounted: !!this._fiber, toolCount: 0,
|
||||||
|
error: (err?.message ?? String(err)).slice(0, 300),
|
||||||
|
latencyMs: Date.now() - startedAt, checkedAt: Date.now(),
|
||||||
|
};
|
||||||
|
try { await client?.close(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读取缓存状态(前端秒回) */
|
||||||
|
getStatus() {
|
||||||
|
return { ...this.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 释放(插件 dispose 时调用) */
|
||||||
|
async dispose() {
|
||||||
|
await this.unmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QmtMcpManager;
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown';
|
|||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
entry: ['src/index.js', 'src/api/index.js', 'src/settings.js', 'src/data-source/*.js', 'src/storage/*.js', 'src/position/*.js', 'src/market/*.js', 'src/trades/*.js'],
|
entry: ['src/index.js', 'src/api/index.js', 'src/settings.js', 'src/data-source/*.js', 'src/storage/*.js', 'src/position/*.js', 'src/market/*.js', 'src/trades/*.js', 'src/mcp/*.js'], // R-020/迭代18: mcp 域
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
outDir: 'lib',
|
outDir: 'lib',
|
||||||
clean: true,
|
clean: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user