diff --git a/docs/02-计划/计划-策略会话数据依据.md b/docs/02-计划/计划-策略会话数据依据.md
index d6687f3..3a4390e 100644
--- a/docs/02-计划/计划-策略会话数据依据.md
+++ b/docs/02-计划/计划-策略会话数据依据.md
@@ -1,6 +1,6 @@
# 计划:策略会话——会话以策略当前数据为依据做讨论(阶段航点)
-> 来源:R-019(**已定稿**,2026-09-08)| 状态:执行中(设计阶段先行)| 所属迭代:17-策略会话
+> 来源:R-021(合并原 R-019+R-021,2026-09-08 已定稿)| 状态:执行中(设计阶段先行)| 所属迭代:17-策略会话
> 粒度:阶段航点(覆盖设计 → 宿主调研 → 实现 → 验收全过程,随进展拆分小任务)
## 目标
@@ -20,4 +20,4 @@
## 边界
-见 R-019 定稿边界:不用宿主工作区;不开放账本写;不做按需取数工具;复盘产物不落盘;本期不做通用视图(全部持仓/交易记录)作依据。
+见 R-021 定稿边界(合并原 R-019+R-021):不用宿主工作区;不开放账本写;不做按需取数工具;复盘产物不落盘;本期不做通用视图(全部持仓/交易记录)作依据。
\ No newline at end of file
diff --git a/docs/04-迭代记录/17-策略会话/UI交互设计.md b/docs/04-迭代记录/17-策略会话/UI交互设计.md
index ce0b678..4f4bf20 100644
--- a/docs/04-迭代记录/17-策略会话/UI交互设计.md
+++ b/docs/04-迭代记录/17-策略会话/UI交互设计.md
@@ -1,6 +1,6 @@
# 迭代 17 UI 交互设计:策略会话(讨论依据控件)
-> 依据:R-019(已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
+> 依据:R-021(合并原 R-019+R-021,已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
> 视觉基线:UI约束-004(--dsw-* token)/ UI约束-001(头部 chip 形态)/ 现有 QmtConnectionChip 交互模式;复用 Toast、外点关闭、RPC(useRpc) 机制。
## 1. 交互总览(一句话)
@@ -73,4 +73,4 @@
| D-1 | 注入条目呈现形态 | 折叠「数据依据」卡片,点开全文(见 §4.2) |
| D-2 | 交易摘要聚合粒度 | 按持仓聚合 + 已清仓标注;如需近期逐笔再加近 N 笔 |
| D-3 | 依据跨刷新持久化 | 会话级不持久化(重开重选) |
-| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
+| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
\ No newline at end of file
diff --git a/docs/04-迭代记录/17-策略会话/产品逻辑设计.md b/docs/04-迭代记录/17-策略会话/产品逻辑设计.md
index b170607..8d4b097 100644
--- a/docs/04-迭代记录/17-策略会话/产品逻辑设计.md
+++ b/docs/04-迭代记录/17-策略会话/产品逻辑设计.md
@@ -1,6 +1,6 @@
# 迭代 17 产品逻辑设计:策略会话
-> 依据:R-019(已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
+> 依据:R-021(合并原 R-019+R-021,已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
## 0. 一句话逻辑
@@ -70,7 +70,7 @@
## 5. 非目标(本期明确不做)
-- 宿主工作区/目录注册(R-019 R3);
+- 宿主工作区/目录注册(原 R-019 R3,已并入 R-021);
- 账本写/交易执行、任何操作按钮;
- 按需取数工具(AI 自行调用业务接口);
- 复盘产物落盘 / 复盘管理(含 DB)——未来 T-002 方向;
@@ -81,6 +81,46 @@
注入的可行通道候选:① 宿主 injected-context/上下文节点(可折叠条目、非用户消息)→ 首选;② 会话 send(文本消息形态进会话)→ 兜底(呈现为一条前置依据消息)。**渲染与动作方案以调研结果为准**(本期设计保留两种形态的表达,UI 交互设计按"数据依据条目"统一描述)。
+## 0.5 产品逻辑总图(2026-09-08 老师确认:画像 / 模型 / 主线 / 分期草案)
+
+### 目标交易者画像(Q-画像,2026-09-08 确认)
+- **混合交易模式的多策略分仓管理**:同一账户下并存多种交易风格的策略分组(当前:手动做T + 程序化网格超市;未来:情绪流交易风格的管理逻辑);
+- 即"策略 = 分仓 + 交易风格/规则的容器";系统须能承载 手动主观 + 自动化规则 + 未来情绪流 三类玩法(彼此数据/规则/复盘口径不同)。
+
+### 交易员工作模型(知识库归纳,作为产品逻辑基准)
+- **日循环**:盘前计划 → 盘中执行/监控 → 盘后记账核对 → 复盘总结 → 明日计划;
+- **策略循环**:规则制定 → 按规则执行 → 规则校验(复盘)→ 规则修订;
+- **复盘闭环**:重建事实 → 归因 → 对照规则 → 提炼改进;**复盘原料 = 决策时的依据记录**(知识库共识:没有决策留痕的复盘只是猜)。
+
+### AI 分工定位(人机合一,只读)
+- **盘中 = AI 参谋台**(状态聚合 / 规则触发提醒 / 轻问答 / 决策留痕 / 人扣扳机执行),快、准、不打断;
+- **盘后 = AI 复盘主持人**(自动重建事实线 → 引导人过结论 → 结论留痕),人不被 AI 下"对错"裁决;
+- 决策依据记录(盘中一键留痕:理由 + 当时快照)是产品逻辑主线之一(2026-09-08 老师确认)——复盘有据的前提。
+
+### 能力地图(现状 → 缺口)
+- 已具备"事实记录层":对账单快照 / 策略账本(唯一权威)/ 交易记录+归属 / 行情快照 / 策略 configSchema(可承载规则参数);
+- 缺口(按承接排序):①复盘事实线入口+可讨论会话(R-021 合并需求,本迭代)②决策依据留痕(盘中)③规则触发提醒 ④盘后自动日结/统计 ⑤复盘管理库(决策日志+复盘记录结构化存储检索)。
+
+### 分期草案(2026-09-08 老师认可分步实现;**各 Phase 具体内容待讨论细化**)
+| 阶段 | 主题 | 交付设想(草案) |
+|---|---|---|
+| Phase 0(迭代 17) | 复盘会话闭环 | R-021 策略会话复盘(原 R-019+R-021 合并;策略数据依据 + 粒度复盘上下文,事实线 → 会话讨论 → 结论在会话) |
+| Phase 1 | 盘中辅助(雏形) | 决策依据一键留痕(理由+快照落库)+ 规则触发/偏离提醒雏形(configSchema × 行情)+ 会话内盘中快查 |
+| Phase 2 | 盘后日结与统计 | 自动日结(当日各策略事实线+盈亏+异常)→ 一键入会话复盘;周期统计(轮次收益/胜率/策略健康) |
+| Phase 3 | 复盘管理库 | 决策日志+复盘结论结构化沉淀/检索/关联(DB),对接目标-006;情绪流风格管理逻辑另立项 |
+
+## 0.6 Phase 0 范围确认(2026-09-08 老师确认:就这样)
+
+- **Phase 0 = 复盘会话闭环,范围 = R-021 单一合并需求(原 R-019 + R-021,2026-09-08 老师指令合并、逻辑整合入 R-021)**;
+- **复盘单元建模(A 拍板)**:引入"轮次 round"概念(holding 之下的交易周期聚合层):
+ - 网格超市:一次买入触发 + 对应卖出配对 = 一轮(每格触发记一轮);
+ - 手动做T:一次开仓 → 清仓 = 一轮;
+ - 支持人工合并/拆分修正(先人工可调);
+ - round 承接"复盘对象"的最小粒度,委托/段为 round 内明细;
+- **行情口径(C 拍板)**:本期事实线 = 交易记录 + 持仓史 + 现价对照(无历史 K 线/分时);历史行情背景 Phase 3 复盘库时再评估;
+- **决策日志预留(拍板)**:Phase 0 设计为"决策依据记录"留数据模型位置(时刻/理由/类别/当时快照/关联 round 或 holding),Phase 1 实现,不返工;
+- **风格差异**:复盘口径按策略类型可扩展(手动/网格,未来情绪流预留策略类型维度)。
+
## 7. D 问题(AI 建议项,待老师拍板)
| # | 问题 | AI 建议 |
@@ -88,4 +128,4 @@
| D-1 | 注入条目的呈现形态 | 对话流顶部「数据依据」条目:默认折叠为一行(图标+策略+快照时间+行数摘要),点开看全文;不伪装成用户消息 |
| D-2 | 交易摘要聚合粒度 | 按持仓聚合(每持仓一段汇总 + 已清仓标注),不逐笔铺开(避免注入过长);如老师要"近期逐笔"可加近 N 笔小表 |
| D-3 | 依据选择是否跨刷新持久化 | 本期**会话级不持久化**(刷新需重选;历史注入条目仍在消息流),与 R-016 会话级偏好一致、不加存储;复盘管理未来再考虑 |
-| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
+| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
\ No newline at end of file
diff --git a/docs/04-迭代记录/17-策略会话/迭代目标.md b/docs/04-迭代记录/17-策略会话/迭代目标.md
index fd3ddb6..7da3f80 100644
--- a/docs/04-迭代记录/17-策略会话/迭代目标.md
+++ b/docs/04-迭代记录/17-策略会话/迭代目标.md
@@ -6,7 +6,7 @@
## 目标描述
-- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-019,2026-09-08):
+- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-019,2026-09-08;同日老师指令 R-019 并入 R-021 合并为单一需求):
- **改造现有 DSH 对话窗口**,不引入宿主工作区/目录;
- 会话的数据 = ①开始前/过程中注入的策略当前数据摘要 + ②会话过程本身产生的对话;复盘管理(可能带 DB 沉淀)为神之一手未来模块,本期不做;
- 注入口径:当前持仓明细 + 该策略全历史关联交易聚合摘要(含已清仓);
@@ -36,4 +36,4 @@
## 对老师配合的请求
1. 拍板 D1-D4(产品/UI 设计中的 AI 建议项,见设计文档末);
-2. 实现验收需真实会话环境人工目视。
+2. 实现验收需真实会话环境人工目视。
\ No newline at end of file
diff --git a/docs/04-迭代记录/19-指示灯状态自适应探测/技术实现方案.md b/docs/04-迭代记录/19-指示灯状态自适应探测/技术实现方案.md
new file mode 100644
index 0000000..956a834
--- /dev/null
+++ b/docs/04-迭代记录/19-指示灯状态自适应探测/技术实现方案.md
@@ -0,0 +1,45 @@
+# 技术实现方案:19-指示灯状态自适应探测
+
+> 迭代编号:19 | 依据:R-022 + R-023 | 涉及约束:产品约束-012(指示灯体系)、技术约束-016、技术约束-020/021(本迭代新增)
+
+## 方案总览
+
+两只连接类状态缓存统一改「**结果驱动 setTimeout 自适应链**」:每次探测完成(成功/失败/异常)后按结果排下一轮。
+
+| 维度 | QmtHealthMonitor(R-022) | QmtMcpManager(R-023) |
+|---|---|---|
+| 健康/已连接 | intervalMs 默认 5 分钟(稳态,原设计) | intervalMs 默认 5 分钟 |
+| 异常/未知/未连接 | retryMs 默认 10 秒快速重试 | retryMs 默认 10 秒快速重试 |
+| 探测目标 | 激活 baseUrl + /health(GET) | 激活 baseUrl + /mcp(SDK Client 握手,只读) |
+| 停止调度 | stop()(mounted 守卫) | 无激活连接 / dispose() / unmount() |
+| 并发防护 | _inFlight 去重 | _probing 去重 |
+| 前端/API | 零改动(读缓存秒回) | 零改动(getStatus 读缓存秒回) |
+
+## 实现步骤
+
+### R-022 QmtHealthMonitor(src/data-source/QmtHealthMonitor.js)
+
+1. 常量 HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs }(测试用短间隔,对齐 QuoteSync 先例);
+2. 固定 setInterval(5min) → _nextDelay()/_scheduleNext()(setTimeout 链,探测完成重排);
+3. resolveActiveBaseUrl 移入 try:解析失败也落 healthy:false 缓存走快速重试
+ (原实现解析在 try 外,抛错场景灯会灰且不自动重试——顺带修复的隐性缺陷);
+4. start() 立即探测不变;stop() 用 clearTimeout + mounted 守卫。
+
+### R-023 QmtMcpManager(src/mcp/QmtMcpManager.js)
+
+1. 常量 PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs };
+2. probe() 结果写缓存后 _scheduleNext();target 为空早退置 disabled 并取消定时器;
+3. unmount() 取消定时器;dispose() 置 _disposed + 取消 + 卸载;_probing 防并发;
+4. 探测本体(SDK Client connect/initialize/tools/list)与挂载/重连逻辑零改动。
+
+### 回归
+
+- scripts/test-health-monitor.mjs(10 项:启动即探测/异常快速重试自动恢复/健康稳态不空转/stop 停止调度…)
+- scripts/test-mcp-status.mjs(12 项:无激活连接不调度/异常快速重试自动恢复/已连接稳态不空转/dispose 停止调度/节奏选择;
+ SDK 握手不可纯内存 mock,守护调度机制:stub probe 翻转状态 + 触达真实 _scheduleNext)
+
+## 涉及文件
+
+- src/data-source/QmtHealthMonitor.js、src/mcp/QmtMcpManager.js(核心改动)
+- scripts/test-health-monitor.mjs、scripts/test-mcp-status.mjs(新增回归)
+- lib/*(build 再产物,部署用)
diff --git a/docs/04-迭代记录/19-指示灯状态自适应探测/迭代复盘.md b/docs/04-迭代记录/19-指示灯状态自适应探测/迭代复盘.md
new file mode 100644
index 0000000..beb8fdf
--- /dev/null
+++ b/docs/04-迭代记录/19-指示灯状态自适应探测/迭代复盘.md
@@ -0,0 +1,45 @@
+# 迭代复盘:19-指示灯状态自适应探测
+
+## 结论
+
+达成(R-022 + R-023 全范围):会话头部两只「连接类」指示灯(QMT连接 / MCP)统一改为
+**结果驱动自适应探测**——健康/已连接维持 5 分钟稳态(原设计不变),异常/未知每 10 秒快速重试;
+QMT Bridge 与其 MCP 服务启动/恢复后两灯 ≤20s 自动回绿,不再等满 5 分钟 / 不再永不恢复。
+前端指示灯与 sync-status / qmt-health / mcp-status 端点零改动(读缓存秒回语义不变)。
+
+## 事实记录
+
+### R-022 QMT 健康灯(src/data-source/QmtHealthMonitor.js)
+
+- 根因(已证实):健康缓存固定 5 分钟探测一次激活 /health;其余三灯有高频自愈回路
+ (持仓 10s / 行情 5s / MCP 客户端重连)。Bridge 在两次探测间启动/恢复时缓存停留在上次失败结果。
+- 变更:setInterval(5min) → setTimeout 自适应链(_nextDelay/_scheduleNext,结果驱动);
+ HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs };resolveActiveBaseUrl 移入 try
+ (解析失败也落 healthy:false 走快速重试,原实现该场景灯灰且不自动重试);_inFlight 防并发保留。
+
+### R-023 MCP 状态灯(src/mcp/QmtMcpManager.js)
+
+- 根因(已证实):状态缓存只在 挂载/切换/手动探测 时刷新,无定时回路;dsh-mcp-client 自带 reconnect
+ 只恢复真实连接(fiber 层),不刷新插件状态缓存。
+- 变更:probe() 结果写缓存后 _scheduleNext();PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs };
+ 无激活连接早退置 disabled 并取消定时器;unmount()/dispose() 停止调度;_probing 防并发;
+ 探测本体与挂载/重连逻辑零改动。
+
+### 验证
+
+- scripts/test-health-monitor.mjs 10/10 绿;scripts/test-mcp-status.mjs 12/12 绿(调度机制守护);
+- pnpm typecheck 0 错;pnpm build 通过(lib 两文件均含自适应逻辑);
+- 存量回归 test-quote-sync / test-position-sync 均 0 退出。
+
+## 待老师确认事项
+
+1. R-022 / R-023 定稿确认(边界:健康/已连接稳态 5 分钟不变 / 异常 10s 重试 / 不新增配置 / API 前端零改动);
+2. 真实环境人工验收(验收标准.md 第 8 条):停/启 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿、
+ 点击仍即时探测、插件重载后缓存秒回。
+
+## 经验沉淀(候选)
+
+- 定时轮询类功能的恢复感知要与故障探测解耦:固定低频定时器做稳态,异常态必须用快速重试把「恢复」
+ 也纳入自愈回路——灯类状态"看起来没生效"的根因通常是恢复路径的响应时延,而不是探测本身失效。
+- 「外层有自管重连」不等于「状态缓存会自愈」:重连只恢复真实连接,展示层缓存若无自己的刷新回路,
+ 必须单独加状态自适应探测(R-023 教训)。
diff --git a/docs/04-迭代记录/19-指示灯状态自适应探测/迭代目标.md b/docs/04-迭代记录/19-指示灯状态自适应探测/迭代目标.md
new file mode 100644
index 0000000..91c35eb
--- /dev/null
+++ b/docs/04-迭代记录/19-指示灯状态自适应探测/迭代目标.md
@@ -0,0 +1,32 @@
+# 迭代目标:19-指示灯状态自适应探测(QMT 健康灯 + MCP 灯)
+
+## 目标
+
+会话头部两只「连接类」指示灯(QMT连接 / MCP)改为**结果驱动自适应探测**:
+健康/已连接时维持 5 分钟稳态探测(低开销),异常/未知时每 10 秒快速重试,
+使 QMT Bridge 与其 MCP 服务启动/恢复后指示灯 ≤20s 自动回绿,不再等满 5 分钟。
+
+## 目标描述
+
+- **背景**:两只灯的数据分别来自 QmtHealthMonitor / QmtMcpManager 的内存状态缓存,刷新机制均为
+ 固定/缺失低频定时回路——QMT 健康缓存固定每 5 分钟探测一次(R-022),MCP 状态缓存只在
+ 挂载/切换/手动探测时刷新、无定时回路(R-023)。持仓(10s)/ 行情(5s)灯靠各自定时同步秒级自愈;
+ 唯独两只连接灯恢复感知滞后,老师实测反馈「QMT连接灯一直没亮」「MCP 灯也不自动恢复」(2026-09-09)。
+- **范围**:只改 QmtHealthMonitor(R-022)与 QmtMcpManager(R-023)的探测调度;前端指示灯组件、
+ sync-status / qmt-health / mcp-status 端点零改动;健康稳态间隔 5 分钟不变;不新增配置项。
+- **验收线**:两灯异常/未知态 ≤10s 重试;健康/已连接态维持 5 分钟不空转;服务恢复后缓存回正常态
+ 沿快速重试被感知;无激活连接与 dispose 后停止调度。对应需求 R-022 + R-023。
+
+## 目标讨论过程
+
+1. 老师报告症状(2026-09-09):Bridge 启动后其他三灯自动点亮,QMT 灯不亮 → 定位为健康缓存固定 5 分钟探测。
+2. AI 提议自适应节奏(健康 5min / 异常 10s),老师确认方向,迭代 19 实施(R-022)。
+3. 老师复查发现 MCP 灯同样不自动恢复(R-023)→ 确认 QmtMcpManager 状态缓存无定时回路,
+ dsh-mcp-client 重连只恢复真实连接、不刷新本类缓存 → 同模式加自适应探测,并入本次迭代。
+4. 决策点见 R-022 / R-023 决策记录。
+
+## 对老师(项目主理人)的配合需求
+
+- 确认 R-022 / R-023 定稿与迭代 19 验收(核实标准见验收标准.md);
+- 真实环境人工验收:停/启 QMT Bridge → QMT 灯 ≤20s 回绿、MCP 灯 ≤20s 回绿;点击灯/「检测 MCP」仍即时探测;
+ 插件重载后灯按缓存秒回并正常。
diff --git a/docs/04-迭代记录/19-指示灯状态自适应探测/验收标准.md b/docs/04-迭代记录/19-指示灯状态自适应探测/验收标准.md
new file mode 100644
index 0000000..d8c1f05
--- /dev/null
+++ b/docs/04-迭代记录/19-指示灯状态自适应探测/验收标准.md
@@ -0,0 +1,29 @@
+# 验收标准:19-指示灯状态自适应探测
+
+## 验收标准线
+
+| # | 标准 | 判定 |
+|---|---|---|
+| 1 | QMT 健康:异常/未知态按 retryMs(默认 10s)快速重试 | test-health-monitor [2]:≥2 次快速重试且缓存 healthy=false |
+| 2 | QMT 健康:Bridge 恢复后 ≤ 一个重试周期缓存回 healthy=true(灯回绿),无需等 5 分钟 | 同脚本 [2] |
+| 3 | QMT 健康:健康态维持稳态间隔(默认 5 分钟)不空转 | 同脚本 [3] |
+| 4 | QMT 健康:stop() 后停止自动调度 | 同脚本 [4] |
+| 5 | MCP:无激活连接置 disabled 且不调度(真实 probe 早退不触 SDK) | test-mcp-status [1] |
+| 6 | MCP:异常态按 retryMs 快速重试;MCP 服务恢复后自动探测到 connected(灯回绿) | 同脚本 [2] |
+| 7 | MCP:已连接态按 intervalMs 稳态不空转;dispose() 后停止调度;_nextDelay 节奏正确 | 同脚本 [3][4][5] |
+| 8 | 启动即探测 + 读缓存秒回语义不变(QMT 健康 / MCP 状态) | 两脚本 [1];sync-status/qmt-health/mcp-status 端点零改动 |
+| 9 | 构建产物(lib/)含两处自适应逻辑;typecheck 0 错;存量不回归 | pnpm typecheck + pnpm build;test-quote-sync / test-position-sync 通过 |
+
+## 验收方法
+
+- 自动化:node scripts/test-health-monitor.mjs(10/10)+ node scripts/test-mcp-status.mjs(12/12)全绿;
+ pnpm typecheck;pnpm build;存量回归脚本 0 退出。
+- 人工(老师真实环境):重启插件后四灯状态秒回;停 QMT Bridge → QMT 灯 ≤10s 转红、MCP 灯 ≤10s 转红、
+ 持仓/行情灯按 30s/15s 阈值转黄;**重新启动 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿**(本次修复核心);
+ 点击 QMT 灯 /「检测 MCP」仍即时探测并刷新悬停详情。
+
+## 验收目标
+
+修复确认:QMT Bridge 及其 MCP 服务启动/恢复后,会话头部 QMT连接灯与 MCP 灯自动回绿(≤20s),
+与其余两盏数据灯恢复节奏一致;健康/已连接稳态无额外探测开销;前端与 API 行为不变。
+验收通过后 R-022 / R-023 定稿并归档(老师确认)。
diff --git a/docs/04-迭代记录/20-策略列设置拖拽排序/技术实现方案.md b/docs/04-迭代记录/20-策略列设置拖拽排序/技术实现方案.md
new file mode 100644
index 0000000..509ff65
--- /dev/null
+++ b/docs/04-迭代记录/20-策略列设置拖拽排序/技术实现方案.md
@@ -0,0 +1,88 @@
+# 技术实现方案:20-策略列设置拖拽排序
+
+## 总体思路
+
+在 ColumnSettingsPopover.jsx 内复用 Tab 设置(SettingsSection.jsx TabSettings)已验证的
+**原生 HTML5 Drag & Drop** 模式:行 draggable + onDragStart/onDragOver/onDrop + 落点高亮 + drop 重排,
+零第三方依赖。持久化沿用现有 strategy-columns/update 整表覆盖 RPC,把「保存」语义从手动按钮改为
+每次变更(drop / 勾选 / ↑↓)立即提交。
+
+## 改动点(单文件:src/client/views/ColumnSettingsPopover.jsx)
+
+### 1. 状态
+
+- 已有 `dragKey`(未被使用,正好用于 DnD):当前被拖行的 column.key;
+- 新增 `overKey`:当前悬停落点的 column.key(用于高亮);
+- `saving` 保持:正在写库时禁止再次拖动/勾选。
+
+### 2. 持久化函数(替换原 save)
+
+- 新增 `persist(nextList, msg)`:提交整表 `{key, visible}` → strategy-columns/update;
+ 成功 Toast 提示 + onSaved() 刷新;失败 Toast 错误 + 本地回滚(还原为服务端最新列配置)。
+- 勾选显隐:切完立即 persist(不再等「保存」按钮);
+- ↑↓ 箭头:移动完立即 persist;
+- drop 重排:重排完立即 persist(对齐 Tab 设置)。
+
+### 3. 拖拽事件(照搬 TabSettings 模式)
+
+```jsx
+
{ setDragKey(c.key); e.dataTransfer.effectAllowed = 'move'; }}
+ onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverKey(c.key); }}
+ onDragLeave={() => { if (overKey === c.key) setOverKey(null); }}
+ onDrop={(e) => { e.preventDefault(); handleDrop(c.key); }}
+ style={{ cursor: saving ? 'default' : 'grab',
+ background: overKey === c.key ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent' }}>
+ ≡
+
+
+ ...
+
+```
+
+### 4. handleDrop(照搬 TabSettings 逻辑,key 定位)
+
+```js
+const handleDrop = (targetKey) => {
+ if (!dragKey || dragKey === targetKey) { setDragKey(null); setOverKey(null); return; }
+ const from = list.findIndex((c) => c.key === dragKey);
+ const to = list.findIndex((c) => c.key === targetKey);
+ if (from < 0 || to < 0) { setDragKey(null); setOverKey(null); return; }
+ const next = list.slice();
+ const [moved] = next.splice(from, 1);
+ next.splice(to, 0, moved);
+ setDragKey(null); setOverKey(null);
+ persist(next, `「${moved.label}」已移动`, next);
+};
+```
+
+### 5. UI 文案
+
+- 标题下提示改为:「勾选显示列,按住 ≡ 拖动调整顺序;代码/名称/操作固定」;
+- 底部按钮区:去掉「保存」,保留「关闭」;
+- 每行最前加 ≡ 手柄(配色沿用 Tab 设置 `#999`/label-tertiary)。
+
+## 边界与不做
+
+- 服务端、API、表格渲染零改动(strategy-columns/update 已是整表覆盖语义);
+- 固定列不参与排序,不进列表(现状维持);
+- 不做方案 C(表格表头直接拖列头),不引入第三方 DnD 库;
+- `saving` 期间禁用拖拽与勾选,防并发写。
+
+## 回归
+
+- scripts/test-r013-columns.mjs(列配置归一化/读写的服务端逻辑,未改动应全绿);
+- npm run typecheck + build(tsdown 产物,弹层 JSX 变更需重编 web bundle);
+- 页面人工验收:拖拽重排 / 落点高亮 / drop 即存 / ↑↓ 微调 / 刷新保持。
+## 实现纪要(2026-09-10,老师验收反馈后修订)
+
+> 老师反馈:初版按原方案做的是「整行高亮」(overKey → 目标行淡蓝底),拖动时无法判断会插入目标列的**前面还是后面**,
+> 观感不符合习惯。定稿修订为 **间隙高亮线**:
+
+- 落点判定:拖动悬停时读鼠标在目标行内的 **Y 坐标**(getBoundingClientRect),上半 → 插入该行**之前**(线画行上缘),
+ 下半 → 插入该行**之后**(线画行下缘);两行之间显示 3px 主色圆角高亮线(absolute 定位在行自身 padding 内,left/right 6,
+ 无负偏移无裁剪风险),指示精确插入位置。
+- drop 判定:drop 事件内直接按坐标重算 before(不依赖 state 闭包,避免滞后);无操作原地(insertAt 等于原槽位)
+ 跳过写库;移除后目标下标左移(to > from 时 to-1)。
+- 状态:overKey(整行)→ overPos { idx, half }(间隙线锚点行 + 上下缘);保留 onDragEnd 清理防拖出弹层残留。
+- 边界维持:固定列不参与、服务端零改动、↑↓ 兜底保留、立即持久化不变。
diff --git a/docs/04-迭代记录/20-策略列设置拖拽排序/迭代复盘.md b/docs/04-迭代记录/20-策略列设置拖拽排序/迭代复盘.md
new file mode 100644
index 0000000..e235d3f
--- /dev/null
+++ b/docs/04-迭代记录/20-策略列设置拖拽排序/迭代复盘.md
@@ -0,0 +1,47 @@
+# 迭代复盘:20-策略列设置拖拽排序
+
+## 结论
+
+达成(R-024):策略 tab「列设置」弹层的列排序从 ↑↓ 箭头点按升级为**鼠标选中后拖动排序**,
+复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式;老师 2026-09-10 页面确认交互直观,验收通过。
+
+## 事实记录
+
+### src/client/views/ColumnSettingsPopover.jsx(单文件改动,服务端零改动)
+
+- 排序交互升级:每行新增 ≡ 拖拽手柄(整行 draggable),拖到目标位置松手即重排;↑↓ 箭头保留作兜底微调。
+- **落点立即持久化**:勾选显隐 / 拖拽 / ↑↓ 任一操作立即调 strategy-columns/update 整表提交;弹层去掉「保存」
+ 按钮只留「关闭」,Toast 反馈;saving 期间禁用拖拽与勾选防并发写;失败回滚到服务端最新配置。
+- **落点指示修订(老师验收反馈)**:初版为整行高亮(overKey → 目标行淡蓝底),老师反馈无法判断插入目标列的
+ 前/后;改为**行间间隙高亮线**——按鼠标在目标行内的 Y 坐标取上半/下半,决定线画在该行上缘(插其前)/下缘(插其后),
+ 明确指示精确插入位置;drop 事件内按坐标重算(不依赖 state 闭包),拖回原位置不重复写库,onDragEnd 清理残留。
+- 修复:间隙线渲染曾误将 style 对象直接作 React child(`{gapLineStyle(i)}`),改为条件渲染 ``。
+
+### 验证
+
+- pnpm typecheck 0 错;pnpm build 通过(client bundle 185KB 含间隙线逻辑,grep 确认编译产物);
+- test-r013-columns.mjs 回归:11 过 / 5 败 —— **与本次改动无关**(git stash 基线验证同样 11/5);
+ 失败根因:脚本仍按 R-013 时期期望「6 个基础列」,而 COLUMN_META 已有 10 列(R-015 新增涨停/跌停/今开/最高 4 个默认隐藏列),
+ 测试期望未随迭代 13 更新的存量债务,不在本迭代范围,列为遗留事项。
+
+### 文档
+
+- docs/05-需求池/R-024.md:定稿 + 变更记录(整行高亮 → 间隙高亮线);
+- docs/03-设计约束/UI交互约束.md:新增 UI约束-007(列表/弹层排序交互规范:间隙高亮线标准,Tab 设置是否统一待老师评估);
+- docs/04-迭代记录/20-策略列设置拖拽排序/:迭代目标 + 技术实现方案(含实现纪要)+ 验收标准(10 项)。
+
+## 遗留事项(待老师决策)
+
+1. **test-r013-columns.mjs 期望过期**:COLUMN_META 现 10 列(R-015 新增 4 行情列默认隐藏),脚本期望仍为 6 列,
+ 默认列数相关 5 项断言失败。建议后续小迭代同步更新脚本期望(或重构为从 COLUMN_META 推导期望),本次未动。
+2. **Tab 设置整行高亮是否统一为间隙高亮线**:UI约束-007 列设置弹层已用间隙线,Tab 设置(UI约束-003)仍为整行高亮;
+ 老师反馈列设置「直观多了」——若 Tab 设置同样已存在「无法判断插入前/后」的观感问题,可同模式调整(独立小任务,本次未动)。
+
+## 经验沉淀(候选)
+
+- 排序类拖拽的落点指示,**间隙线(两行之间)比整行高亮更符合用户观感**:整行高亮只能表达「落在哪一行」,
+ 无法表达「插入该行前还是后」;主流列表/表格拖拽(如 Notion、表格列排序)均用间隙指示。
+- 服务端整表覆盖语义(updateStrategyColumns 整表提交)天然适配前端即时持久化,无需逐条增量接口;
+ 「保存按钮」在每次变更即写库后失去存在意义(本迭代据此移除)。
+- 回归脚本的期望值要随数据模型演进同步更新(test-r013-columns 教训:R-015 扩展 COLUMN_META 后脚本未跟随,
+ 留下 5 项静默失败,直到本迭代回归才暴露)。
\ No newline at end of file
diff --git a/docs/04-迭代记录/20-策略列设置拖拽排序/迭代目标.md b/docs/04-迭代记录/20-策略列设置拖拽排序/迭代目标.md
new file mode 100644
index 0000000..2f83abd
--- /dev/null
+++ b/docs/04-迭代记录/20-策略列设置拖拽排序/迭代目标.md
@@ -0,0 +1,30 @@
+# 迭代目标:20-策略列设置拖拽排序
+
+## 目标
+
+策略 tab「列设置」弹层(ColumnSettingsPopover)的列排序交互从 **↑↓ 箭头点按** 升级为
+**鼠标选中后拖动排序**(原生 HTML5 DnD),并对齐 Tab 设置的即时持久化体验。
+
+## 目标描述
+
+- **背景**:策略 tab 的可配置列(基础数据列 + 自定义字段列)在「列设置」弹层中管理,排序目前依赖
+ ↑↓ 箭头(R-013 迭代 11 产物,当时刻意简化,代码注释自述「用箭头替代 DnD」)。老师反馈希望
+ 改成鼠标选中后拖动排序(2026-09-10)。
+- **结论**:复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式(draggable 行 + 落点高亮 + drop 重排),
+ 零第三方依赖;落点立即持久化(strategy-columns/update 整表覆盖),弹层去掉「保存」按钮只留「关闭」。
+- **范围**:仅改 src/client/views/ColumnSettingsPopover.jsx 单文件;服务端/API/表格渲染零改动;
+ 固定列(代码/名称/操作/展开箭头)不参与排序,维持现状。
+- **验收线**:拖拽可重排列顺序且落点高亮可见;drop 后立即写库(无需点保存);↑↓ 箭头仍可微调;
+ 勾选显隐立即生效;刷新后列顺序保持。对应需求 R-024(已定稿)。
+
+## 目标讨论过程
+
+1. 老师反馈(2026-09-10):策略 tab 列设计可否做成鼠标选中后拖动排序。
+2. AI 调研:Tab 设置(R-011)已实现同款原生 HTML5 DnD(SettingsSection.jsx),可零依赖复用;
+ 列设置弹层存在未使用的 dragKey 状态(原计划 DnD 的残留)。
+3. 登记 R-024(讨论中)→ 方案草案 A/B/C → 老师确认方案 A(弹层内拖拽);
+ 保存时机 / 箭头去留按 AI 推荐默认(立即持久化 + ↑↓ 保留兜底)。
+
+## 对老师(项目主理人)的配合需求
+
+- 无(纯前端交互优化,服务端零改动,验收走页面人工确认)。
\ No newline at end of file
diff --git a/docs/04-迭代记录/20-策略列设置拖拽排序/验收标准.md b/docs/04-迭代记录/20-策略列设置拖拽排序/验收标准.md
new file mode 100644
index 0000000..00cb0c3
--- /dev/null
+++ b/docs/04-迭代记录/20-策略列设置拖拽排序/验收标准.md
@@ -0,0 +1,32 @@
+# 验收标准:20-策略列设置拖拽排序
+
+## 验收线(在哪里验收)
+
+DSH Web GUI → 神之一手 → 任一策略 tab → 工具条「列设置」按钮 → 弹层。
+页面侧人工验收;服务端逻辑零改动,跑 scripts/test-r013-columns.mjs 回归确认无破坏。
+
+## 验收方法(逐项操作)
+
+| # | 操作 | 预期 |
+|---|---|---|
+| 1 | 打开「列设置」弹层,鼠标按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标列之前还是之后,非整行高亮 |
+| 2 | drop 后直接关闭弹层(不点任何保存) | 策略表格表头列顺序已按新顺序展示(立即持久化生效) |
+| 3 | 刷新页面 / 重新打开弹层 | 列顺序保持拖拽后的结果(写库成功) |
+| 4 | 点 ↑ / ↓ 箭头 | 仍可逐格微调,且立即持久化(关闭弹层后表格列顺序同步变化) |
+| 5 | 勾选/取消勾选某一列 | 显隐立即生效(关闭弹层后,表格对应列隐藏/出现) |
+| 6 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该目标列**之前**;下半区 = 插入该目标列**之后**(以落点线位置为准) |
+| 7 | 拖动期间把行拖回原位置或原地松手 | 无异常,列顺序不变(不重复写库) |
+| 8 | 固定列(代码/名称/操作) | 弹层列表不含固定列,不参与拖拽,恒显示(维持现状) |
+| 9 | 自定义字段列(某策略配了 configSchema) | 与基础列一样可拖拽排序、可勾选显隐 |
+| 10 | 保存中(快速连续拖动) | 无并发写异常;saving 期间拖拽/勾选禁用,Toast 提示保存结果 |
+
+## 验收目标
+
+- 功能正交:拖拽排序 / ↑↓ 微调 / 显隐勾选 三者并存且各自立即持久化;
+- 零回归:scripts/test-r013-columns.mjs 全绿、typecheck + build 通过;
+- 视觉一致性:≡ 手柄沿用 Tab 设置(UI约束-003)同套样式;落点为**间隙高亮线**(主色 token,UI约束-004 无硬编码色值);
+- 服务端零改动确认:git diff 无 src/api / src/settings.js 变更。
+
+## 判定
+
+- 老师页面人工确认 10 项全过 + 服务端回归全绿 ⇒ 迭代 20 验收通过,R-024 归档至 已完成/ 并更新索引实现状态。
\ No newline at end of file
diff --git a/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/技术实现方案.md b/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/技术实现方案.md
new file mode 100644
index 0000000..87f5887
--- /dev/null
+++ b/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/技术实现方案.md
@@ -0,0 +1,84 @@
+# 技术实现方案:21-Tab设置排序间隙高亮统一
+
+## 总体思路
+
+把 SettingsSection.jsx 的 TabSettings 组件落点指示从「整行高亮(overId → 行背景 color-mix 12%)」
+改为「行间间隙高亮线(overPos { idx, half } → 目标行上缘/下缘 3px 主色线)」,与 ColumnSettingsPopover.jsx
+(R-024)逐点对齐;其余交互(≡ 手柄、开关、立即持久化、徽标、saving 禁用)不动。
+
+## 改动点(单文件:src/client/views/SettingsSection.jsx,仅 TabSettings 组件)
+
+### 1. 状态
+
+- `overId`(整行高亮锚点)→ `overPos { idx, half }`(间隙线锚点行下标 + 上/下缘);
+- `dragId` 保留。
+
+### 2. 悬停判定(与 R-024 全同)
+
+```jsx
+const handleOver = (e, index) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ const rect = e.currentTarget.getBoundingClientRect();
+ const half = e.clientY - rect.top < rect.height / 2 ? 'top' : 'bottom';
+ setOverPos({ idx: index, half });
+};
+```
+
+### 3. drop(事件内按坐标重算,不依赖 state 闭包)
+
+```jsx
+const handleDrop = async (e, index) => {
+ e.preventDefault();
+ if (!tabs || !dragId) { setDragId(null); setOverPos(null); return; }
+ const rect = e.currentTarget.getBoundingClientRect();
+ const before = e.clientY - rect.top < rect.height / 2;
+ let to = before ? index : index + 1;
+ const from = tabs.findIndex((x) => x.id === dragId);
+ if (from < 0 || to < 0 || to > tabs.length) { setDragId(null); setOverPos(null); return; }
+ if (to === from || to === from + 1) { setDragId(null); setOverPos(null); return; } // 原地跳过写库
+ const next = tabs.slice();
+ const [moved] = next.splice(from, 1);
+ const insertAt = to > from ? to - 1 : to;
+ next.splice(insertAt, 0, moved);
+ const ordered = next.map((x, i) => ({ ...x, order: i }));
+ setDragId(null);
+ setOverPos(null);
+ await persist(ordered, '「' + moved.name + '」已移动');
+};
+```
+
+### 4. 行渲染(tr 上挂间隙线样式)
+
+tr 为表格行,直接内嵌 div 会破坏表格结构(tr 只允许 td/th)——**间隙线以 td 为定位锚点绘制**:
+首选在每个 td 内各画一段 3px 高亮线(结构合规、视觉连续),实现纪要记录最终选型。
+
+```jsx
+
{ setDragId(t.id); setOverPos(null); e.dataTransfer.effectAllowed = 'move'; }}
+ onDragOver={(e) => handleOver(e, i)}
+ onDrop={(e) => handleDrop(e, i)}
+ onDragEnd={() => { setDragId(null); setOverPos(null); }}
+ style={{ cursor: saving ? 'default' : 'grab' }}
+>
+ | {gapLine(i, 'td1')} ≡ |
+ ...
+
+```
+
+间隙线样式(与 R-024 全同):absolute、left/right 内缩 6、height 3、borderRadius 2、
+背景 primary token;top:0(插前)/ bottom:0(插后)。
+
+## 边界与不做
+
+- 服务端/API/表格列结构/持久化逻辑零改动;
+- 不动「策略分组」子 tab(该处无排序交互);
+- 不引入第三方 DnD 库。
+
+## 回归
+
+- pnpm typecheck + build(SettingsSection 变更需重编 web bundle);
+- 页面人工验收:Tab 设置拖动 → 间隙线位置正确(上=前/下=后)、整行高亮消失、drop 即存、
+ 拖回原位不重复写库、刷新页面后顺序保持。
diff --git a/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/迭代目标.md b/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/迭代目标.md
new file mode 100644
index 0000000..8a8b7db
--- /dev/null
+++ b/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/迭代目标.md
@@ -0,0 +1,26 @@
+# 迭代目标:21-Tab设置排序间隙高亮统一
+
+## 目标
+
+设置页「Tab 设置」的分组 tab 拖拽排序落点指示,从**整行高亮**统一为**行间间隙高亮线**,
+与策略 tab 列设置弹层(R-024/UI约束-007)完全一致——明确指示插入目标行的前/后。
+
+## 目标描述
+
+- **背景**:迭代 20 复盘遗留第 2 项——R-024 将列设置弹层落点改为间隙高亮线后老师确认「直观多了」,
+ 但设置页 Tab 设置(R-011/UI约束-003)仍是整行高亮(overId → 目标行淡蓝底),无法判断插入前/后,
+ 两处观感不一致。2026-09-10 老师指令:Tab 设置也统一为间隙高亮线形式。
+- **范围**:仅改 src/client/views/SettingsSection.jsx 的 TabSettings 组件落点指示;表格结构、≡ 手柄、
+ 显隐开关、立即持久化(tabs/update)、徽标、saving 禁用全部维持不变;服务端/API 零改动。
+- **验收线**:拖动时目标行上缘/下缘显示主色间隙线(上半区=插前、下半区=插后);整行高亮消失;
+ 拖回原位不重复写库;drop 后立即持久化。对应需求 R-025(已定稿)。
+
+## 目标讨论过程
+
+1. 迭代 20 复盘(2026-09-10)列遗留第 2 项:Tab 设置整行高亮是否统一为间隙线,留待老师评估。
+2. 老师指令(2026-09-10):设置页分组 tab 排序也改上面(列设置弹层)的拖动高亮形式 → 登记 R-025 已定稿,
+ 迭代 21 实施。
+
+## 对老师(项目主理人)的配合需求
+
+- 无(纯前端交互统一,服务端零改动,验收走页面人工确认)。
\ No newline at end of file
diff --git a/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/验收标准.md b/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/验收标准.md
new file mode 100644
index 0000000..4d8ec36
--- /dev/null
+++ b/docs/04-迭代记录/21-Tab设置排序间隙高亮统一/验收标准.md
@@ -0,0 +1,27 @@
+# 验收标准:21-Tab设置排序间隙高亮统一
+
+## 验收线(在哪里验收)
+
+DSH Web GUI → 设置页 → 「Tab 设置」子 tab → 拖动任一标签行排序。
+页面侧人工验收;服务端逻辑零改动。
+
+## 验收方法(逐项操作)
+
+| # | 操作 | 预期 |
+|---|---|---|
+| 1 | 打开「Tab 设置」,按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标 tab 之前还是之后;原整行淡蓝高亮消失 |
+| 2 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该 tab **之前**;下半区 = 插入该 tab **之后**(以落点线位置为准) |
+| 3 | drop 后落点立即生效 | Toast「已移动」,刷新页面后 tab 顺序保持(tabs/update 已写库) |
+| 4 | 拖动期间把行拖回原位置或原地松手 | 无异常,顺序不变,且不触发重复写库 |
+| 5 | 既有交互回归 | ≡ 手柄、显示/隐藏开关、内置/策略徽标、saving 期间拖拽禁用——全部维持原行为 |
+| 6 | 策略分组子 tab | 无排序交互,不受影响(仍为 新增/重命名/删除) |
+
+## 验收目标
+
+- 功能对准:Tab 设置落点与列设置弹层(R-024)**同一套间隙线交互**,观感一致;
+- 零回归:既有 tabs 持久化(整表 order + visible)、显隐开关、策略改名联动 name join 不受影响;
+- typecheck + build 通过;无新增硬编码色值(UI约束-004)。
+
+## 判定
+
+- 老师页面人工确认 6 项全过 + build 通过 ⇒ 迭代 21 验收通过,R-025 归档至 已完成/ 并更新索引实现状态。
diff --git a/docs/05-需求池/R-021.md b/docs/05-需求池/R-021.md
new file mode 100644
index 0000000..8f0f689
--- /dev/null
+++ b/docs/05-需求池/R-021.md
@@ -0,0 +1,60 @@
+# 需求:R-021 神之一手数据加入会话(对象目录 + 添加机制;复盘为第一用例)· 已定稿(部分暂定)
+
+> 登记:2026-09-08 | 来源:老师指令(原 R-019 + R-021 合并演进)| 状态:**已定稿(暂定项见文末)**
+> 归属:迭代 17-策略会话(Phase 0)| 计划:PLAN-017 | 实现状态:已立项(设计阶段)
+
+## 需求本质(2026-09-08 再定调,老师)
+
+> **总体是:会话(agent)可以获取"神之一手"的数据;复盘只是 agent 获取到对应数据后的一种用法。本需求要确定两件事:① 数据怎么添加到会话 ② 哪些数据可以加。**
+> (策略会话、粒度复盘、未来的盘中问答/分析 都是同一能力的消费方式——主干 = 「神之一手数据 → 会话」能力。)
+
+## 一、数据怎么添加到会话(机制)——暂定 2026-09-08
+
+| 维度 | 暂定结论(AI 建议,老师"暂定") | 开放注记 |
+|---|---|---|
+| 触发动因 | **A:人挑对象加入**——在数据展示处/会话内选一个"数据对象",生成它的数据上下文进会话 | B(agent 按需取数工具)不采纳,维持 R-019"预注入不挂工具"拍板;日后盘中问答需要实时取数时再独立讨论 |
+| 注入形态 | 每个对象 = 一条只读「数据上下文」(折叠条目/引用),含快照时间;同对象重复加 = 新快照追加(不覆盖旧),模型以最近为准 | 通道形态待宿主技术调研 |
+| 数据时效 | 注入"加入那一刻"的只读快照;要最新 = 重新添加 | — |
+
+## 二、哪些数据可以加(对象目录)——候选目录,首批范围暂定
+
+### 神之一手数据对象目录(候选全量)
+
+账户:账户资金/资产、全部持仓(对账单快照)| 策略:策略(分组下持仓与交易)| 持仓:当前持仓行、历史(已清仓)持仓| 轮次:某一轮交易周期(round,待建模)| 交易:单笔委托/成交| 关系视图:持仓 ↔ 交易| 行情:单 code 现价(随行,不单独成对象)| 汇总:策略汇总/未分配/同步健康 | 未来:决策日志/统计/复盘记录(Phase 1/3)
+
+### 首批范围(复盘闭环所需,暂定草案,待老师细调)
+| # | 对象 | 加入后 agent 拿到的数据上下文(草案) | 为什么复盘需要 |
+|---|---|---|---|
+| P1 | 策略 | 策略定义 + 当前持仓 + 关联交易聚合摘要 | 整策略复盘/会话依据 |
+| P2 | 持仓(当前/历史) | 持仓完整档案:基本信息+自定义字段+建仓至今买卖链+现价盈亏+(历史=清仓信息) | 复盘一个持仓 |
+| P3 | 持仓↔交易关系 | 该持仓全部关联委托/成交(按 holding 聚合) | 追溯买卖过程 |
+| P4 | 单笔委托 | 时间/方向/量/价/费用/归属 + 该持仓上下文 | 复盘某次动作 |
+| P5 | 轮次 round | 一轮建→平 全动作+结果(需本轮引入 round 建模) | 复盘最小单元(网格/做T 一轮) |
+| P6 | 行情随行 | 对象行内现价/涨跌等(不单独成对象;历史 K 线不在本期数据域) | 复盘需当前价对照 |
+
+**候选后补**:账户资产 / 全部持仓对账单 / 策略汇总 / 未分配 / 同步健康(供 Phase 1 盘中辅助与 Phase 2 日结)。
+
+## 三、用例视角(复盘仅是用法之一)
+
+- 复盘 = 挑对象(P1-P6 任意组合,通常 P2/P3/P4/P5)→ 数据上下文进会话 → agent 基于事实线引导复盘 → 结论留会话;
+- 其他用例(未来消费方式):盘中状态问答、策略分析、计划讨论——同一"对象目录 + 添加机制"。
+
+## 定稿边界(合并两轮结论)
+
+**做**:数据对象目录 + 添加机制(人挑对象 → 只读数据上下文注入会话);首批对象数据上下文生成;折叠条目呈现;换/刷新/叠加;round 建模(若 P5 首批保留)。
+
+**不做**:DSH 宿主工作区;账本写与交易执行;agent 按需取数工具(暂定);复盘结论落库/统计/决策日志实现(Phase 1/2/3,预留模型位);历史 K 线注入;通用数据视图(全部持仓/交易记录)作为对象目录外的会话依据特例——(对象目录已含"全部持仓",是否启用由首批范围定)。
+
+## 待定/开放项
+1. 首批对象范围与 P1-P6 取舍(老师可增删,含是否本期引入 round=P5);
+2. 宿主注入通道形态(技术调研);
+3. 各对象"数据上下文"的精确字段规格(服务端实现细节,技术方案阶段);
+4. 会话侧呈现细节(UI 交互设计延后,老师先前指令)。
+
+## 关联
+- 迭代 17-策略会话(Phase 0)| R-010(持仓↔交易关系载体)| 终极目标-002/006/007
+- 未来:Phase 1 决策留痕/盘中辅助、Phase 2 日结统计、Phase 3 复盘库(产品逻辑设计 §0.5)
+- 归档:R-019 已并入本需求(已完成/R-019.md 为讨论史存档)
+
+## 验收
+**(技术方案定稿后补入 04-迭代记录/17-策略会话/验收标准.md)**
diff --git a/docs/05-需求池/R-022.md b/docs/05-需求池/R-022.md
new file mode 100644
index 0000000..62cab85
--- /dev/null
+++ b/docs/05-需求池/R-022.md
@@ -0,0 +1,37 @@
+# 需求:R-022 QMT 连接健康灯自适应探测(Bridge 启动/恢复后快速回绿)· 讨论中
+
+> 登记:2026-09-09 | 来源:老师反馈(会话头部 QMT 指示灯)| 状态:**讨论中(AI 提议定稿,待老师确认)**
+> 归属:迭代 19-指示灯状态自适应探测(R-022:QMT 健康灯)| 实现状态:已实现(待验收)
+
+## 症状(老师反馈,2026-09-09)
+
+启动 QMT Bridge 后,会话头部四个指示灯中「持仓数据 / 行情数据 / MCP」几秒内自动点亮,
+唯独「QMT连接」灯一直不亮(停留在红/灰态)——「QMT 连接状态自动检查好像没有生效」。
+
+## 根因
+
+QmtHealthMonitor 只在**插件启动时 + 每 5 分钟**探测一次激活 Bridge /health(2026-09-01 迭代 04 定的
+「服务端 5 分钟定时探测 + 缓存」机制),结果缓存内存;前端 10s 轮询 sync-status 只读缓存、不触发探测。
+其余三灯靠高频自愈回路:PositionSync 10s / QuoteSync 5s 定时拉 Bridge、MCP 客户端自带失败重连,
+Bridge 恢复后秒级回绿。唯独健康缓存要等满 5 分钟下一次探测——Bridge 在两次探测间启动/恢复时,
+灯长期停留在上一次失败结果,观感即「自动检查没生效」(反向同理:Bridge 中途宕机,灯最多 5 分钟不转红)。
+
+## 方案(AI 提议,老师拍板确认)
+
+**自适应探测节奏(owner = QmtHealthMonitor,单点改动)**:
+
+- 健康 → 每 5 分钟探测一次(**稳态,维持原设计低开销**);
+- 异常/未知(含从未探测成功)→ **每 10 秒快速重试**(与前端 sync-status 10s 轮询对齐,
+ 与持仓/行情 5-10s 恢复节奏一致)——Bridge 启动后灯 ≤20s 自动回绿。
+
+**边界(不做)**:前端读缓存秒回不变;API/前端零改动;健康稳态间隔不放宽不放窄(5 分钟不变);
+不新增配置项(间隔为常量,测试经构造参数注入);不做 read-path 触发探测(定时器已覆盖,避免耦合)。
+
+## 决策记录
+
+| 项 | 结论 | 讨论 |
+|---|---|---|
+| 异常重试间隔 | 10s | 与前端 10s 轮询对齐;5s 会翻倍失败探测开销(每次失败探测最长挂 5s 超时) |
+| 健康稳态间隔 | 5 分钟维持 | 原设计为省探测开销;异常态才快速重试,健康态不空转 |
+| 防并发/调度 | setTimeout 链 + _inFlight 去重 | 沿用原防并发语义;手动探测/热切换共用同一节奏重排 |
+| baseUrl 解析异常 | 落 healthy:false 缓存走快速重试 | 原实现解析在 try 外,异常会丢缓存导致灯灰且不重试 |
diff --git a/docs/05-需求池/R-023.md b/docs/05-需求池/R-023.md
new file mode 100644
index 0000000..214bb4e
--- /dev/null
+++ b/docs/05-需求池/R-023.md
@@ -0,0 +1,36 @@
+# 需求:R-023 MCP 状态灯自适应探测(MCP 服务恢复后自动回绿)· 讨论中
+
+> 登记:2026-09-09 | 来源:老师反馈(R-022 之后实测 MCP 灯同样不自动恢复)| 状态:**讨论中(AI 提议定稿,待老师确认)**
+> 归属:迭代 19-指示灯状态自适应探测 | 实现状态:已实现(待验收)
+
+## 症状(老师反馈,2026-09-09)
+
+QMT 健康灯修复后复查发现:会话头部 MCP 指示灯也存在同样的「状态不自动恢复」——
+MCP 服务(QMT Bridge /mcp)启动/恢复后,MCP 灯停留在上次失败态,不会自动回绿。
+
+## 根因
+
+QmtMcpManager 的**状态缓存只在 挂载 / 配置切换 / 手动探测(点击「检测 MCP」)时刷新**,无任何定时回路;
+sync-status 端点每 10s 读的只是这份缓存。dsh-mcp-client 自带的 reconnect 只恢复**真实连接**
+(fiber 层),并不刷新插件自己的状态缓存——所以「连接恢复了、灯还是红的」。
+
+## 方案(AI 提议,与 R-022 同模式,owner = QmtMcpManager)
+
+**结果驱动自适应探测**(对齐 QmtHealthMonitor):
+
+- connected → 每 5 分钟探测一次(稳态,低开销);
+- 非 connected(error/异常)→ **每 10 秒快速重试**——MCP 服务恢复后灯 ≤20s 自动回绿;
+- 无激活连接(url 为空)不探测不调度;dispose() 后停止调度;
+- 探测仍走 SDK 独立只读握手(Client connect → initialize + tools/list),与 dsh-mcp-client 自管连接互不干扰。
+
+**边界(不做)**:不接管/不替代 dsh-mcp-client 的重连(那是真实连接层,保持现状 Q5);
+不新增配置项(间隔为常量,测试经构造参数注入);前端/API 零改动(getStatus 读缓存语义不变)。
+
+## 决策记录
+
+| 项 | 结论 | 讨论 |
+|---|---|---|
+| 非连接态重试间隔 | 10s | 与前端 sync-status 10s 轮询、R-022 QMT 灯一致;MCP 探测最坏挂 8s 超时,快失败场景成本可忽略 |
+| 已连接稳态间隔 | 5 分钟 | 与 R-022 一致;MCP 握手比重(initialize+tools/list),健康态不空转 |
+| 调度停止条件 | 无激活连接 / dispose / unmount | 无 url 时探测早退置 disabled 并取消定时器,避免空转 |
+| 并发防护 | _probing 去重(同 QmtHealthMonitor._inFlight) | 定时器与手动/热切换探测可能撞车 |
diff --git a/docs/05-需求池/R-025.md b/docs/05-需求池/R-025.md
new file mode 100644
index 0000000..f7cece5
--- /dev/null
+++ b/docs/05-需求池/R-025.md
@@ -0,0 +1,40 @@
+# 需求:R-025 设置页 Tab 设置排序落点统一为间隙高亮线 · 已定稿
+
+> 登记:2026-09-10 | 来源:老师指令(迭代 20 复盘遗留第 2 项确认)| 状态:**已定稿(2026-09-10 老师指令)**
+> 归属:迭代 21-Tab设置排序间隙高亮统一 | 实现状态:未开始
+
+## 诉求(老师,2026-09-10)
+
+> 神之一手设置页的分组 tab 排序(Tab 设置),也改一下上面(列设置弹层)的拖动高亮显示形式。
+
+## 背景
+
+- R-024/迭代 20 列设置弹层拖拽落点已从整行高亮改为**行间间隙高亮线**(UI约束-007),老师确认「直观多了」;
+- 复盘遗留第 2 项:设置页「Tab 设置」的排序仍是整行高亮(overId → 行淡蓝底,UI约束-003),存在同样的
+ 「无法判断插入前/后」观感问题;老师指令统一为间隙线形式。
+
+## 定稿结论(2026-09-10 老师指令)
+
+设置页「Tab 设置」排序落点从整行高亮改为**行间间隙高亮线**(复用 UI约束-007 / R-024 已验证模式):
+
+- 拖动悬停时按鼠标在目标行内的 Y 坐标(上半/下半)判定,间隙线画在目标行上缘(插其前)/下缘(插其后);
+- drop 事件内按坐标重算插入位置(不依赖 state 闭包);拖回原位置(to===from 或 to===from+1)跳过写库;
+- 保留 onDragEnd 清理残留;整行高亮背景(color-mix 12%)移除;
+- 其余维持不变:≡ 手柄、显隐开关、落点立即持久化(tabs/update)、「刷新页面后生效」提示、内置/策略徽标、
+ saving 期间禁用拖拽。
+
+## 决策记录
+
+| 项 | 结论 | 讨论 |
+|---|---|---|
+| 范围 | 仅 Tab 设置(SettingsSection.jsx TabSettings)落点指示 | 老师指令;不动表格结构、不动持久化、不动开关 |
+| 落点形式 | 行间间隙高亮线(上缘=前/下缘=后) | 与 R-024 弹层全同,UI约束-007 已定义 |
+| 判定 | 目标行内 Y 坐标上下半区 | 与 R-024 全同 |
+| 无操作 | to===from 或 to===from+1 跳过写库 | 复用 R-024 数学式 |
+| 服务端/API | 零改动 | tabs/update 整表语义不变 |
+| 约束更新 | UI约束-003 变更:落点从整行高亮改间隙线;UI约束-007 补「Tab 设置已统一」 | 2026-09-10 老师指令 |
+
+## 关联
+
+- 上游:UI约束-007(排序交互间隙线标准)、R-024(同款模式实证)、迭代 20 复盘遗留第 2 项;
+- 迭代 20 复盘遗留第 1 项(test-r013-columns 期望过期)不在本需求范围。
\ No newline at end of file
diff --git a/docs/05-需求池/R-014.md b/docs/05-需求池/已完成/R-014.md
similarity index 85%
rename from docs/05-需求池/R-014.md
rename to docs/05-需求池/已完成/R-014.md
index 592c3b4..6989d5a 100644
--- a/docs/05-需求池/R-014.md
+++ b/docs/05-需求池/已完成/R-014.md
@@ -3,6 +3,12 @@
> 登记:2026-09-02 | 来源:架构梳理讨论 + 老师指令 | 状态:**已定稿(老师逐项拍板 4 问)**
> 归属:迭代 12 | 计划:PLAN-013
+
+> **归档头(2026-09-08)** | 需求状态:已完成 | 归档日期:2026-09-08 | 实现迭代:12-持仓内存快照(持仓内存快照)
+> 讨论记录索引:R-014.md 正文;实现细节见 04-迭代记录/12-持仓内存快照/
+> 归档路径:05-需求池/已完成/R-014.md
+> 迭代状态标记:已实现(迭代 12,回归 34/34 + typecheck + build 通过)→ 已归档(2026-09-08 老师归档指令确认验收)
+
## 需求描述
原实盘持仓数据在**每次请求时穿透 QMT**(`PositionManager.getAllPositions()` 实时拉 `/trade/positions`),带来两个问题:
@@ -29,4 +35,4 @@
## 验收
-见 `docs/04-迭代记录/12-持仓内存快照/验收标准.md`。
+见 `docs/04-迭代记录/12-持仓内存快照/验收标准.md`。
\ No newline at end of file
diff --git a/docs/05-需求池/R-015.md b/docs/05-需求池/已完成/R-015.md
similarity index 85%
rename from docs/05-需求池/R-015.md
rename to docs/05-需求池/已完成/R-015.md
index 5710b2a..204b683 100644
--- a/docs/05-需求池/R-015.md
+++ b/docs/05-需求池/已完成/R-015.md
@@ -3,6 +3,12 @@
> 登记:2026-09-02 | 来源:架构演进讨论(老师逐项拍板)| 状态:**已定稿**
> 归属:迭代 13 | 计划:PLAN-014
+
+> **归档头(2026-09-08)** | 需求状态:已完成 | 归档日期:2026-09-08 | 实现迭代:13-盘口内存快照(盘口数据内存化+指示灯)
+> 讨论记录索引:R-015.md 正文;实现细节见 04-迭代记录/13-盘口内存快照/
+> 归档路径:05-需求池/已完成/R-015.md
+> 迭代状态标记:已实现(迭代 13,test-quote-sync 全绿 + 存量回归通过)→ 已归档(2026-09-08 老师归档指令确认验收)
+
## 需求描述
持仓数据已完成「内存快照统一管理」(R-014/迭代 12)。本轮把**盘口(市场行情)数据**收敛为同一模式:
@@ -25,4 +31,4 @@
## 验收
-见 `docs/04-迭代记录/13-盘口内存快照/验收标准.md`。
+见 `docs/04-迭代记录/13-盘口内存快照/验收标准.md`。
\ No newline at end of file
diff --git a/docs/05-需求池/R-016.md b/docs/05-需求池/已完成/R-016.md
similarity index 93%
rename from docs/05-需求池/R-016.md
rename to docs/05-需求池/已完成/R-016.md
index 8aaae32..7d6d56b 100644
--- a/docs/05-需求池/R-016.md
+++ b/docs/05-需求池/已完成/R-016.md
@@ -3,6 +3,12 @@
> 登记:2026-09-07 | 来源:老师指令(优化功能需求讨论)| 状态:**已定稿(2026-09-07,Q1-Q7 老师拍板)**
> 归属:迭代 14 | 计划:PLAN-015 | 实现状态:**已实现(2026-09-07,待老师人工验收)**
+
+> **归档头(2026-09-08)** | 需求状态:已完成 | 归档日期:2026-09-08 | 实现迭代:14-策略tab历史持仓展示(策略 tab 历史持仓展示)
+> 讨论记录索引:R-016.md 正文;实现细节见 04-迭代记录/14-策略tab历史持仓展示/
+> 归档路径:05-需求池/已完成/R-016.md
+> 迭代状态标记:已实现(迭代 14)→ 已归档(2026-09-08 老师归档指令确认验收)
+
## 需求描述(老师原始诉求)
**两个策略 tab**(做T / 网格超市)**title 旁边**添加以下功能:
@@ -60,4 +66,4 @@
## 验收
-见 `docs/04-迭代记录/14-策略tab历史持仓展示/验收标准.md`。
+见 `docs/04-迭代记录/14-策略tab历史持仓展示/验收标准.md`。
\ No newline at end of file
diff --git a/docs/05-需求池/R-017.md b/docs/05-需求池/已完成/R-017.md
similarity index 83%
rename from docs/05-需求池/R-017.md
rename to docs/05-需求池/已完成/R-017.md
index ffca701..4b36429 100644
--- a/docs/05-需求池/R-017.md
+++ b/docs/05-需求池/已完成/R-017.md
@@ -4,6 +4,12 @@
> 归属:迭代 15 | 计划:PLAN-016 | 实现状态:**已实现(2026-09-07,待老师人工验收)**
> 前置:R-016「关联观察」所列问题的正式立项(清仓后当日委托无法通过下拉关联到对应 holding,2026-09-07 大连热电实际发生——盘中手动归属后又被清为未关联,下拉已无手动做T候选)。
+
+> **归档头(2026-09-08)** | 需求状态:已完成 | 归档日期:2026-09-08 | 实现迭代:15-归属候选纳入清仓持仓(归属候选纳入近期清仓持仓)
+> 讨论记录索引:R-017.md 正文;实现细节见 04-迭代记录/15-归属候选纳入清仓持仓/
+> 归档路径:05-需求池/已完成/R-017.md
+> 迭代状态标记:已实现(迭代 15)后被 R-018 语义取代退役 → 已归档(2026-09-08)
+
## 需求描述
交易记录 tab 的归属候选(orders/attribution-candidates)只回**当前持仓**——清仓后(幽灵清仓最快约 30s 转历史)当日委托在下拉中失去对应策略/持仓锚点,无法补关联。本轮:**候选 = 该 code 的当前持仓 + 近 7 天清仓的持仓**,清仓行带标记供 UI 区分展示。
@@ -27,4 +33,4 @@
## 验收
-见 `docs/04-迭代记录/15-归属候选纳入清仓持仓/验收标准.md`。
+见 `docs/04-迭代记录/15-归属候选纳入清仓持仓/验收标准.md`。
\ No newline at end of file
diff --git a/docs/05-需求池/R-019.md b/docs/05-需求池/已完成/R-019.md
similarity index 92%
rename from docs/05-需求池/R-019.md
rename to docs/05-需求池/已完成/R-019.md
index 3ef2112..b096f62 100644
--- a/docs/05-需求池/R-019.md
+++ b/docs/05-需求池/已完成/R-019.md
@@ -3,6 +3,11 @@
> 登记:2026-09-08 | 来源:老师指令(新需求)| 状态:**已定稿(2026-09-08,F1/F2 老师确认)**
> 归属:迭代 17(待立项)| 计划:待出(PLAN-017)| 实现状态:未开始
+
+> **归档头(2026-09-08)** | 需求状态:已完成(并入合并需求)| 归档日期:2026-09-08
+> 合并承接:**R-021(策略会话复盘)**——老师 2026-09-08 指令将 R-019 与 R-021 合并、逻辑整合入 R-021;本文件为 R-019 线讨论史存档(Q1-Q4/R2-Q1..Q4/R3/F1-F2)
+> 归档路径:05-需求池/已完成/R-019.md
+
## 需求描述(老师原始诉求,原话)
> **新需求,让 AI 会话可以以策略为 Workspace。**
@@ -71,4 +76,4 @@
## 验收
-**(计划/迭代立项后写入迭代验收标准.md)**
+**(计划/迭代立项后写入迭代验收标准.md)**
\ No newline at end of file
diff --git a/docs/05-需求池/已完成/R-024.md b/docs/05-需求池/已完成/R-024.md
new file mode 100644
index 0000000..f1d3059
--- /dev/null
+++ b/docs/05-需求池/已完成/R-024.md
@@ -0,0 +1,46 @@
+# 需求:R-024 策略 tab 列设置改为拖拽排序 · 已完成
+
+> 归档日期:2026-09-10 | 需求状态:已完成 | 实现迭代:[迭代 20-策略列设置拖拽排序](../04-迭代记录/20-策略列设置拖拽排序/迭代复盘.md)
+> 讨论记录索引:本文档(定稿 + 变更记录);登记:2026-09-10 | 来源:老师反馈(策略 tab 列设计细节优化)
+> 实现状态:**已实现(迭代 20 验收通过,2026-09-10 老师页面确认)**
+
+## 诉求(老师,2026-09-10)
+
+> 细节优化,策略 tab 下的列设计,可否做成鼠标选中后、**拖动排序**的形式?
+
+## 现状
+
+策略 tab「列设置」弹层(ColumnSettingsPopover,R-013 迭代 11 实现)管理可配置列(基础数据列 + 自定义字段列):
+- 显隐:复选框勾选;
+- 排序:**↑↓ 箭头按钮**(连续点按移动),保存按钮提交整份列配置(one-divine-lot/strategy-columns/update);
+- 代码注释原话:「同 Tab 设置简化版:用箭头替代 DnD,零依赖稳妥」——当时刻意简化。
+
+## 定稿结论(2026-09-10 老师确认方案 A)
+
+**方案 A:列设置弹层内拖拽排序(原生 HTML5 DnD,复用 R-011 Tab 设置已验证模式)**
+
+- 弹层每行加 **≡ 拖拽手柄**(整行 draggable),按住拖动到目标行松手即重排;落点行高亮提示,视觉沿
+ Tab 设置(UI约束-003)同一套:color-mix(primary 12%) 淡底。
+- **落点立即持久化**(对齐 Tab 设置体验):每次 drop 调一次 one-divine-lot/strategy-columns/update
+ 整表提交(含不可见列保位置);点按手势(勾选显隐 / ↑↓ 微调)同步立即持久化 → **弹层去掉「保存」按钮,
+ 只留「关闭」**,Toast 反馈保存结果。
+- **↑↓ 箭头保留作兜底**(精准微调场景,与拖拽并存)。
+- 固定列(代码/名称/操作/展开箭头)不参与排序,恒显示(维持现状)。
+- 边界:不改服务端 API(strategy-columns/update 已是整表覆盖语义,天然兼容);核心改动在
+ ColumnSettingsPopover.jsx 单文件;显隐勾选交互不变。
+
+## 决策记录
+
+| 项 | 结论 | 讨论 |
+|---|---|---|
+| 拖拽范围 | 方案 A:弹层内拖拽(≡ 手柄) | 老师 2026-09-10 确认;方案 C(表头直拖)改动大、需兼容百分比排序点击,暂不做 |
+| 持久化时机 | 落点立即持久化(每次变更即写库) | 对齐 Tab 设置(UI约束-003)体验;strategy-columns/update 为整表覆盖语义,天然支持 |
+| ↑↓ 箭头 | 保留作兜底 | 与拖拽并存,兼顾精准微调;不增加额外依赖 |
+| 保存按钮 | 移除(改「关闭」) | 所有变更即时持久化后按钮失去意义,避免「忘了点保存」 |
+| 服务端 | 零改动 | strategy-columns/update 已是整表覆盖 |
+
+## 变更记录(追加式)
+
+| 日期 | 变更 | 原因 |
+|---|---|---|
+| 2026-09-10 | 拖拽落点提示从**整行高亮**改为**行间(两列之间)间隙高亮线**,指示精确插入位置(前/后) | 老师反馈:整行高亮只能看出落在哪一行,无法判断会插入目标列的前面还是后面;间隙高亮是主流列表拖拽交互(Tab 设置的整行高亮是否同样调整待老师评估,见 UI约束-007) |
\ No newline at end of file
diff --git a/scripts/test-health-monitor.mjs b/scripts/test-health-monitor.mjs
new file mode 100644
index 0000000..186e76c
--- /dev/null
+++ b/scripts/test-health-monitor.mjs
@@ -0,0 +1,126 @@
+/**
+ * QmtHealthMonitor 自适应探测 回归测试(R-022 / 迭代 19,2026-09-09)
+ *
+ * 运行:node scripts/test-health-monitor.mjs
+ * 隔离:纯内存 mock(mock fetch + mock settings scope),用短间隔参数测真实 setTimeout 调度。
+ * 背景:原实现固定 5 分钟探测,QMT Bridge 恢复后指示灯要等满 5 分钟(老师反馈「自动检查没生效」);
+ * 本测试守护「异常/未知 → 快速重试,健康 → 稳态间隔」的自适应节奏。
+ */
+
+let passed = 0;
+let failed = 0;
+function ok(cond, name) {
+ if (cond) { passed++; console.log(' ✓ ' + name); }
+ else { failed++; console.error(' ✗ ' + name); }
+}
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+const { QmtHealthMonitor } = await import('../src/data-source/QmtHealthMonitor.js');
+
+/** 可编程 settings scope(resolveActiveBaseUrl 依赖 scope.get() → qmtConnections) */
+function mockSettings(baseUrl) {
+ return {
+ get: () => ({
+ qmtConnections: {
+ list: [{ id: 'c1', name: 'bridge', baseUrl, order: 1 }],
+ activeId: 'c1',
+ defaultId: '',
+ },
+ }),
+ };
+}
+
+const BRIDGE_URL = 'http://bridge.test:8610';
+
+/** 可编程 fetch mock:bridgeUp=false 时抛连接错误 */
+function mockFetch() {
+ const calls = [];
+ let up = true;
+ globalThis.fetch = async (url) => {
+ calls.push({ url: String(url), t: Date.now() });
+ if (!up) throw new Error('fetch failed: ECONNREFUSED bridge down');
+ return { ok: true, status: 200 };
+ };
+ return {
+ calls,
+ set up(v) { up = v; },
+ get up() { return up; },
+ };
+}
+
+const RETRY_MS = 40;
+const INTERVAL_MS = 300;
+
+console.log('[1] 启动即探测 + 缓存读取(健康)');
+{
+ const fetch = mockFetch();
+ const monitor = new QmtHealthMonitor({
+ runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
+ intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ await monitor.probe(); // 等价 start() 的立即探测
+ ok(fetch.calls.length === 1, '启动探测发起 1 次');
+ ok(fetch.calls[0].url === BRIDGE_URL + '/health', '探测地址 = 激活 baseUrl + /health');
+ const st = monitor.getStatus();
+ ok(st.healthy === true && st.fromCache === true, '缓存 healthy=true 且 fromCache=true');
+ await monitor.stop();
+}
+
+console.log('[2] 异常 → 快速重试自动恢复(核心回归:Bridge 启动后灯 ≤ 重试间隔回绿)');
+{
+ const fetch = mockFetch();
+ fetch.up = false;
+ const monitor = new QmtHealthMonitor({
+ runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
+ intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ monitor.start();
+ await sleep(RETRY_MS * 3); // 快速重试窗口内应已多次探测
+ ok(fetch.calls.length >= 2, '异常态多次快速重试(' + fetch.calls.length + ' 次 ≥ 2)');
+ ok(monitor.getStatus().healthy === false, '异常缓存 healthy=false');
+
+ fetch.up = true; // 模拟 QMT Bridge 启动
+ const before = fetch.calls.length;
+ await sleep(RETRY_MS * 4); // 等下一个快速重试命中
+ const st = monitor.getStatus();
+ ok(fetch.calls.length > before, '恢复后被自动重探测到');
+ ok(st.healthy === true, '恢复后缓存 healthy=true(灯将回绿,未等 5 分钟稳态间隔)');
+ await monitor.stop();
+}
+
+console.log('[3] 健康稳态:回落到长间隔,不空转');
+{
+ const fetch = mockFetch();
+ const monitor = new QmtHealthMonitor({
+ runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
+ intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ monitor.start();
+ await sleep(RETRY_MS * 4); // 小于稳态间隔:健康态不应按重试节奏空转
+ ok(fetch.calls.length === 1, '健康态在稳态间隔内只探测 1 次(无快速空转)');
+ await sleep(INTERVAL_MS + RETRY_MS * 2); // 跨过稳态间隔
+ ok(fetch.calls.length === 2, '跨过稳态间隔后又探测 1 次(共 2 次)');
+ await monitor.stop();
+}
+
+console.log('[4] stop() 后停止自动调度');
+{
+ const fetch = mockFetch();
+ fetch.up = false;
+ const monitor = new QmtHealthMonitor({
+ runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
+ intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ monitor.start();
+ await sleep(RETRY_MS * 2);
+ const before = fetch.calls.length;
+ monitor.stop();
+ fetch.up = true;
+ await sleep(RETRY_MS * 4);
+ ok(fetch.calls.length === before, 'stop 后无自动探测');
+}
+
+console.log('----');
+console.log(passed + ' passed, ' + failed + ' failed');
+if (failed > 0) process.exit(1);
diff --git a/scripts/test-mcp-status.mjs b/scripts/test-mcp-status.mjs
new file mode 100644
index 0000000..195406b
--- /dev/null
+++ b/scripts/test-mcp-status.mjs
@@ -0,0 +1,149 @@
+/**
+ * QmtMcpManager 状态自适应探测 回归测试(R-023 / 迭代 19,2026-09-09)
+ *
+ * 运行:node scripts/test-mcp-status.mjs
+ * 说明:SDK 握手(initialize/tools.list)依赖真实网络端点,无法纯内存 mock;
+ * 本测试守护的是【调度机制】——probe 结果驱动 setTimeout 自适应链:
+ * connected → intervalMs 稳态;非 connected → retryMs 快速重试;
+ * 无激活连接不调度;dispose 停止调度。
+ * 用 stub probe(记录调用 + 翻转状态 + 触达真实 _scheduleNext)验证自动恢复数据流;
+ * 无激活连接场景用真实 probe(早退路径不触 SDK)。
+ */
+
+let passed = 0;
+let failed = 0;
+function ok(cond, name) {
+ if (cond) { passed++; console.log(' ✓ ' + name); }
+ else { failed++; console.error(' ✗ ' + name); }
+}
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+const { QmtMcpManager } = await import('../src/mcp/QmtMcpManager.js');
+
+const RETRY_MS = 50;
+const INTERVAL_MS = 350;
+const BRIDGE_URL = 'http://bridge.test:8610';
+
+/** settings scope:baseUrl 传 '' = 无激活连接 */
+function mockSettings(baseUrl) {
+ return {
+ get: () => ({
+ qmtConnections: {
+ list: baseUrl ? [{ id: 'c1', name: 'bridge', baseUrl, order: 1 }] : [],
+ activeId: baseUrl ? 'c1' : '',
+ defaultId: '',
+ },
+ }),
+ };
+}
+
+const quietLogger = { info() {}, warn() {}, debug() {} };
+
+console.log('[1] 无激活连接:置 disabled 且不调度(真实 probe 早退,不触 SDK)');
+{
+ const mgr = new QmtMcpManager({
+ ctx: {}, runtime: { settings: mockSettings(''), dataSource: {} },
+ logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ let probeCalls = 0;
+ const orig = mgr.probe.bind(mgr);
+ mgr.probe = async (u) => { probeCalls++; return orig(u); };
+ await mgr.start();
+ ok(mgr.getStatus().state === 'disabled', '无激活连接 → 状态 disabled');
+ await sleep(RETRY_MS * 4);
+ ok(probeCalls === 1, '无激活连接无定时探测(仅 start 一次)');
+ await mgr.dispose();
+}
+
+console.log('[2] 异常 → 快速重试自动恢复(核心回归:MCP 服务恢复后灯 ≤ 重试间隔回绿)');
+{
+ const mgr = new QmtMcpManager({
+ ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
+ logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ let bridgeUp = false;
+ let probeCalls = 0;
+ mgr.probe = async () => {
+ probeCalls++;
+ mgr.status = bridgeUp
+ ? { state: 'connected', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 9, error: null, latencyMs: 3, checkedAt: Date.now() }
+ : { state: 'error', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 0, error: 'ECONNREFUSED', latencyMs: 3, checkedAt: Date.now() };
+ mgr._scheduleNext(); // 等价真实 probe 尾部
+ return mgr.status;
+ };
+ await mgr.probe(); // 启动首次探测(Bridge 未启动)
+ ok(mgr.getStatus().state === 'error', 'Bridge 未启动 → 状态 error');
+ await sleep(RETRY_MS * 3);
+ ok(probeCalls >= 3, '异常态按 retryMs 快速重试(' + probeCalls + ' 次 ≥ 3)');
+
+ bridgeUp = true; // 模拟 MCP 服务启动
+ await sleep(RETRY_MS * 3);
+ ok(mgr.getStatus().state === 'connected', '恢复后被自动探测到 → connected(灯将回绿,未等 5 分钟稳态间隔)');
+ await mgr.dispose();
+}
+
+console.log('[3] 已连接稳态:回落到长间隔,不空转');
+{
+ const mgr = new QmtMcpManager({
+ ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
+ logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ let probeCalls = 0;
+ mgr.probe = async () => {
+ probeCalls++;
+ mgr.status = { state: 'connected', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 9, error: null, latencyMs: 3, checkedAt: Date.now() };
+ mgr._scheduleNext();
+ return mgr.status;
+ };
+ await mgr.probe();
+ ok(probeCalls === 1 && mgr.getStatus().state === 'connected', '首探测 connected');
+ await sleep(RETRY_MS * 3); // 小于稳态间隔、大于重试节奏
+ ok(probeCalls === 1, '已连接态在稳态间隔内不按重试节奏空转');
+ await sleep(INTERVAL_MS + RETRY_MS * 2); // 跨过稳态间隔
+ ok(probeCalls === 2, '跨过稳态间隔后又探测 1 次');
+ await mgr.dispose();
+}
+
+console.log('[4] dispose() 停止自动调度');
+{
+ const mgr = new QmtMcpManager({
+ ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
+ logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ let bridgeUp = false;
+ let probeCalls = 0;
+ mgr.probe = async () => {
+ probeCalls++;
+ mgr.status = bridgeUp
+ ? { state: 'connected', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 9, error: null, latencyMs: 3, checkedAt: Date.now() }
+ : { state: 'error', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 0, error: 'ECONNREFUSED', latencyMs: 3, checkedAt: Date.now() };
+ mgr._scheduleNext();
+ return mgr.status;
+ };
+ await mgr.probe();
+ await sleep(RETRY_MS * 2);
+ const before = probeCalls;
+ await mgr.dispose();
+ bridgeUp = true;
+ await sleep(RETRY_MS * 4);
+ ok(probeCalls === before, 'dispose 后无自动探测');
+}
+
+console.log('[5] _nextDelay 节奏选择');
+{
+ const mgr = new QmtMcpManager({
+ ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
+ logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
+ });
+ mgr.status = { state: 'connected' };
+ ok(mgr._nextDelay() === INTERVAL_MS, 'connected → intervalMs');
+ mgr.status = { state: 'error' };
+ ok(mgr._nextDelay() === RETRY_MS, 'error → retryMs');
+ mgr.status = mgr._disabledStatus();
+ ok(mgr._nextDelay() === RETRY_MS, 'disabled → retryMs(无连接时 probe 早退不调度)');
+}
+
+console.log('----');
+console.log(passed + ' passed, ' + failed + ' failed');
+if (failed > 0) process.exit(1);
diff --git a/src/data-source/QmtHealthMonitor.js b/src/data-source/QmtHealthMonitor.js
index 19c44ce..58048fb 100644
--- a/src/data-source/QmtHealthMonitor.js
+++ b/src/data-source/QmtHealthMonitor.js
@@ -1,11 +1,17 @@
/**
- * QmtHealthMonitor —— QMT 连接健康检查(2026-09-01 老师定)
+ * QmtHealthMonitor —— QMT 连接健康检查(2026-09-01 老师定;2026-09-09 R-022 自适应探测修正)
*
* 机制(服务端定时 + 缓存):
- * - DSH 服务端每 5 分钟探测一次激活 QMT Bridge /health,结果缓存在内存;
+ * - DSH 服务端定时探测激活 QMT Bridge /health,结果缓存在内存;
* - 前端打开页面时读缓存(秒回,不等实时探测);
* - 激活配置切换时立即补探一次(热切换联动);
- * - 前端点击/悬停可触发即时探测(on-demand),刷新缓存。
+ * - 前端点击/悬停可触发即时探测(on-demand),刷新缓存并重排下轮节奏。
+ *
+ * 自适应探测节奏(R-022 / 迭代 19,2026-09-09):
+ * - 健康 → 每 5 分钟探测一次(稳态,低开销,维持原设计);
+ * - 异常/未知 → 每 10 秒快速重试——与持仓 10s / 行情 5s 指示灯恢复节奏对齐:
+ * QMT Bridge 启动/恢复后灯 ≤20s 自动回绿(原实现要等满 5 分钟,灯长期停留在失败态,
+ * 被老师反馈为「自动检查没有生效」)。
*
* 职责(单一):维护 QMT 健康状态缓存。
*/
@@ -13,36 +19,43 @@
import { resolveActiveBaseUrl } from '../api/common.js';
const HEALTH_TIMEOUT_MS = 5000;
-const HEALTH_INTERVAL_MS = 5 * 60 * 1000; // 5 分钟定时探测
+const HEALTH_INTERVAL_MS = 5 * 60 * 1000; // 健康稳态:5 分钟定时探测
+const HEALTH_RETRY_MS = 10 * 1000; // 异常/未知:10 秒快速重试(恢复感知)
export class QmtHealthMonitor {
/**
* @param {object} opts
* @param {object} opts.runtime { settings, dataSource } —— 解析激活 baseUrl
* @param {object} [opts.logger]
+ * @param {number} [opts.intervalMs] 健康稳态探测间隔(默认 5 分钟;测试可调小)
+ * @param {number} [opts.retryMs] 异常/未知重试间隔(默认 10 秒;测试可调小)
*/
- constructor({ runtime, logger } = {}) {
+ constructor({ runtime, logger, intervalMs = HEALTH_INTERVAL_MS, retryMs = HEALTH_RETRY_MS } = {}) {
this.runtime = runtime;
this.logger = logger;
+ this.intervalMs = intervalMs;
+ this.retryMs = retryMs;
this.cache = null; // { baseUrl, healthy, latencyMs, httpStatus, healthError, checkedAt }
- this.timer = null;
+ this.timer = null; // setTimeout 句柄(自适应调度)
this.mounted = true;
this._inFlight = null; // 防止并发探测
}
- /** 启动:立即探测一次 + 每 5 分钟定时 */
+ /** 启动:立即探测一次,之后按结果自适应调度(健康 5 分钟 / 异常 10 秒) */
start() {
this.mounted = true;
- this.probe(); // 启动即探测
- this.timer = setInterval(() => this.probe(), HEALTH_INTERVAL_MS);
- this.logger?.info?.('[one-divine-lot] QmtHealthMonitor 已启动(5 分钟定时探测)');
+ this.probe().catch(() => {}); // 启动即探测;完成后 _scheduleNext() 排下轮
+ this.logger?.info?.(
+ '[one-divine-lot] QmtHealthMonitor 已启动(自适应探测:健康 ' + Math.round(this.intervalMs / 1000) +
+ 's / 异常 ' + Math.round(this.retryMs / 1000) + 's 重试)'
+ );
}
/** 停止(插件释放时) */
stop() {
this.mounted = false;
if (this.timer) {
- clearInterval(this.timer);
+ clearTimeout(this.timer);
this.timer = null;
}
}
@@ -54,20 +67,41 @@ export class QmtHealthMonitor {
: { healthy: null, baseUrl: '', latencyMs: 0, healthError: null, checkedAt: 0, fromCache: false };
}
- /** 立即探测(启动/定时/前端点击/配置切换共用),结果写缓存 */
+ /** 下轮探测延迟:健康 → 稳态间隔;异常/未知(含从未探测成功)→ 快速重试 */
+ _nextDelay() {
+ return this.cache?.healthy === true ? this.intervalMs : this.retryMs;
+ }
+
+ /** 自适应调度下一轮探测(每次探测完成后调用;手动探测/热切换共用同一节奏) */
+ _scheduleNext() {
+ if (!this.mounted) return;
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = null;
+ }
+ this.timer = setTimeout(() => {
+ this.timer = null;
+ this.probe().catch(() => {});
+ }, this._nextDelay());
+ }
+
+ /**
+ * 立即探测(启动/定时/前端点击/配置切换共用),结果写缓存并重排下轮节奏。
+ * 任意失败(含 baseUrl 解析异常)都落 healthy:false 缓存——异常态走快速重试。
+ */
async probe() {
if (!this.mounted) return this.cache;
// 防并发:同一时刻只探测一次
if (this._inFlight) return this._inFlight;
- const baseUrl = resolveActiveBaseUrl(this.runtime.settings, this.runtime.dataSource);
- const startedAt = Date.now();
this._inFlight = (async () => {
- let result;
+ let baseUrl;
try {
+ baseUrl = resolveActiveBaseUrl(this.runtime.settings, this.runtime.dataSource);
+ const startedAt = Date.now();
const res = await fetch(baseUrl + '/health', {
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
});
- result = {
+ return {
baseUrl,
healthy: res.ok,
latencyMs: Date.now() - startedAt,
@@ -76,22 +110,23 @@ export class QmtHealthMonitor {
checkedAt: Date.now(),
};
} catch (e) {
- result = {
- baseUrl,
+ return {
+ baseUrl: baseUrl ?? '',
healthy: false,
- latencyMs: Date.now() - startedAt,
+ latencyMs: 0,
httpStatus: null,
healthError: e?.message ?? String(e),
checkedAt: Date.now(),
};
}
- this.cache = result;
- return result;
})();
try {
- return await this._inFlight;
+ const result = await this._inFlight;
+ this.cache = result;
+ return result;
} finally {
this._inFlight = null;
+ this._scheduleNext();
}
}
}
diff --git a/src/mcp/QmtMcpManager.js b/src/mcp/QmtMcpManager.js
index 42d0bc4..bacf6b7 100644
--- a/src/mcp/QmtMcpManager.js
+++ b/src/mcp/QmtMcpManager.js
@@ -1,5 +1,5 @@
/**
- * QmtMcpManager —— QMT Bridge MCP 客户端管理(R-020 / 迭代 18)
+ * QmtMcpManager —— QMT Bridge MCP 客户端管理(R-020 / 迭代 18;2026-09-09 R-023/迭代 19 加状态自适应探测)
*
* 职责:在神之一手插件 apply 内动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client,
* 把 QMT Bridge /mcp 的工具以 mcp__QMT_Bridge_MCP__* 注册给模型;管理 MCP 实例的
@@ -11,6 +11,15 @@
* - Q5 失败不崩:failOnStartupError:false + reconnect(对齐宿主受管块同参);
* - Q4 状态出口:mcp-status 端点 + sync-status.mcp 域(设置页 + 会话头部指示灯)。
*
+ * 状态自适应探测(R-023 / 迭代 19,2026-09-09):
+ * - 原实现状态缓存只在 挂载/切换/手动探测 时刷新,无定时回路:QMT Bridge 启动/恢复后
+ * MCP 灯停留在上次失败态(与 QMT 健康灯同症状,老师反馈「MCP 也没自动恢复」);
+ * dsh-mcp-client 自带 reconnect 只恢复真实连接,不刷新本类状态缓存。
+ * - 现按结果驱动自适应探测(与 QmtHealthMonitor 同模式):connected → intervalMs
+ * (默认 5 分钟,稳态低开销);非 connected → retryMs(默认 10 秒)快速重试,与前端
+ * sync-status 10s 轮询对齐,MCP 服务恢复后灯 ≤20s 自动回绿;无激活连接(url 为空)
+ * 不探测不调度;dispose 后停止调度。
+ *
* 实现参照:mcp_router(ctx.plugin 动态挂载 dsh-mcp-client 实证);
* 挂载错误必须 catch(同 serverName 重复 / 依赖缺失),不影响插件其余功能。
*/
@@ -21,6 +30,8 @@ import { resolveActiveBaseUrl } from '../api/common.js';
const SERVER_NAME = 'QMT_Bridge_MCP';
const PROBE_TIMEOUT_MS = 8000;
+const PROBE_INTERVAL_MS = 5 * 60 * 1000; // 已连接稳态:5 分钟探测一次
+const PROBE_RETRY_MS = 10 * 1000; // 未连接/异常:10 秒快速重试
const MOUNT_CONFIG_BASE = {
serverName: SERVER_NAME,
transport: 'streamable-http',
@@ -36,11 +47,15 @@ export class QmtMcpManager {
* @param {object} opts.ctx Cordis 宿主 ctx
* @param {object} opts.runtime { settings, dataSource } —— 解析激活 baseUrl
* @param {object} [opts.logger]
+ * @param {number} [opts.intervalMs] 已连接稳态探测间隔(默认 5 分钟;测试可调小)
+ * @param {number} [opts.retryMs] 异常/未连接重试间隔(默认 10 秒;测试可调小)
*/
- constructor({ ctx, runtime, logger } = {}) {
+ constructor({ ctx, runtime, logger, intervalMs = PROBE_INTERVAL_MS, retryMs = PROBE_RETRY_MS } = {}) {
this.ctx = ctx;
this.runtime = runtime;
this.logger = logger ?? console;
+ this.intervalMs = intervalMs;
+ this.retryMs = retryMs;
/** @type {Promise