@@ -0,0 +1,482 @@
# 迭代说明:04-WS盘中价格实时更新
> 本文件为本次迭代的**修改记录**(追加式,只记事实):每完成一处修改,追加一条记录(改了什么 / 为什么 / 涉及文件)。
> 创建:2026-08-31
## 修改记录
### 2026-08-31 · 建立迭代 04 文档骨架
- **做了什么**:需求 T-005 草稿转正为 R-005( docs/05-需求池/R-005.md);创建计划 PLAN-004( docs/02-计划/计划-WS盘中价格实时更新.md);创建迭代 04 子目录(docs/04-迭代记录/04-WS盘中价格实时更新/)下的迭代目标 / 技术实现方案 / 验收标准 三件套 + 本说明。
- **为什么**:本轮迭代按 divine-lot-dev 流程先落文档(需求定稿 → 计划 → 迭代范围),后续修改全部可回溯。
- **涉及文件**: docs/05-需求池/R-005.md、docs/02-计划/计划-WS盘中价格实时更新.md、docs/04-迭代记录/04-WS盘中价格实时更新/(4 个 md)。
### 2026-08-31 · 服务端:新增 active-host / unsubscribe-ws 端点
- **做了什么**: src/api.js 新增两个端点:
- `qmt-connections/active-host` :返回激活 QMT 连接配置的 `{ name, baseUrl, host, wsUrl, activeId }` ( host 含端口,wsUrl = ws://host/ws),供前端据此直连 QMT Bridge WebSocket;连接列表为空时回退数据源当前 baseUrl(cordis 兜底);
- `qmt-connections/unsubscribe-ws` :服务端代理 `POST {baseUrl}/data/unsubscribe` (浏览器侧 WS 直连可行,REST 退订统一走服务端避免跨域)。
- **为什么**:前端需要激活配置的 host 才能建 WS(技术约束-010);退订走服务端代理更干净。
- **涉及文件**: src/api.js。
### 2026-08-31 · 客户端:MarketDataProvider( WS 行情客户端)
- **做了什么**:新增 src/client/market/MarketDataProvider.jsx:
- 直连 `ws://<激活配置host>:8610/ws` (浏览器原生 WebSocket);
- 连接前 REST 订阅 `POST /data/subscribe { type:'whole', codes:['SH','SZ'] }` 拿 sub_id;
- 维护 code → { lastPrice, lastClose, stime, open, high, low } 价格 Map( useReducer);
- 事件驱动:WS 推送 `{ type:'whole', data: { code: snap } }` → dispatch update → 所有订阅表格重渲染现价;
- 连接后拉一次 `GET /data/tick?codes=SH,SZ` 快照补初值(WS 只推增量,无 prime);
- 断线指数退避重连(1s→2s→4s…上限 15s) + 连接状态(connecting/live/reconnecting/disconnected);
- 每 5s 轮询 active-host,激活配置 host 变化时自动断开旧 WS 并重建(复用 R-004 多配置热切换)。
- **为什么**:实现「前端直连 + 全量订阅 + 事件驱动」的核心(R-005 定稿)。
- **涉及文件**: src/client/market/MarketDataProvider.jsx(新增)。
### 2026-08-31 · 客户端:PriceCell 现价单元格 + 行情状态条
- **做了什么**:新增 src/client/views/PriceCell.jsx:
- `PriceCell` :显示 lastPrice(≥100 用 2 位小数,否则 3 位),红涨绿跌(对比 lastClose,A股习惯:涨红 #d32f2f / 跌绿 #2e7d32 / 平灰);价格变化时 1.2s 金色高亮闪动(CSS keyframes);
- `MarketStatusBar` :表格顶部行情连接状态提示(连接中/实时/重连中/未连接,圆点颜色区分)。
- **为什么**:产品约束-007(现价列 + 涨跌色 + 变化高亮)。
- **涉及文件**: src/client/views/PriceCell.jsx(新增)。
### 2026-08-31 · 客户端:3 个监控表格接入现价列
- **做了什么**:
- src/client/index.js: withConnection 内包一层 MarketDataProvider, 3 个表格共享同一行情实例;
- src/client/views/AllPositionsTab.jsx:表头/表体新增「现价」列(名称后),标题下加 MarketStatusBar;
- src/client/views/StrategyTab.jsx:表头/表体新增「现价」列(名称后),顶部加 MarketStatusBar(手动做T / 网格超市共用)。
- **为什么**:3 表格展示关注股票盘中现价(R-005 目标)。
- **涉及文件**: src/client/index.js、src/client/views/AllPositionsTab.jsx、src/client/views/StrategyTab.jsx。
### 2026-08-31 · 构建验证
- **做了什么**: `pnpm typecheck` ( 0 错误)+ `pnpm build` 通过(客户端 bundle 82.54kB / 17.96kB gzip,服务端 14 文件,wrap 完成)。
- **端到端链路验证**:Node 模拟前端流程(订阅 whole → WS 连接 → 26 批推送 → 关注 code 过滤 → 提取现价)成功:603028.SH → lastPrice=7.18( lastClose=7.1,涨,UI 红色)。
- **为什么**:构建与链路验证通过,可进入宿主安装验收。
- **涉及文件**:lib/(构建产物)。
### 2026-08-31 · 问题分析记录:订阅走服务端代理,WS 推送问题归属另一工作空间
**现象 ** (老师观察):全部持仓页「行情实时 IP」短暂出现后变「行情重连中」;持仓列表现价始终无数据。
**分析过程(基于实验证据,非猜测) ** :
1. **CORS 实证 ** : QMT Bridge REST( /health、/data/subscribe、/data/tick)响应**不含 Access-Control-Allow-Origin 头**, OPTIONS 预检返回 404 → 浏览器页面(origin=127.0.0.1:3080)直接 fetch QMT Bridge REST 会被**跨域拦截**。这与项目历史一致:REST 一直走 DSH 服务端代理(qmt-connections/test 注释「浏览器跨域规避」)。
2. **架构结论 ** : **订阅是 RESTful 接口(POST /data/subscribe),必须走 DSH 服务端代理转发**,服务端无跨域问题;前端直连 REST 订阅是错误的架构。
3. **WS 行为实验 ** ( Node 直连 GEMWIN):
- 不订阅直接连 WS → 收到 25 条/5秒 推送(首个连接);
- REST 订阅成功(sub_id 有效)→ 连 WS → 0 条;
- 全新进程不订阅直连 → 稳定收到 21 条/5秒。
- 结论:**QMT Bridge 的 WS 推送语义与 OpenAPI 文档描述不符**(订阅反而可能干扰推送),该问题**归属 QMT Bridge 工作空间**(老师负责,另行解决)。
4. **本工作空间策略 ** :订阅与快照(REST)一律走 DSH 服务端代理;数据显示先以 **tick 快照轮询 ** ( GET /data/tick,稳定可用)为兜底数据源 + WS 推送为增强通道,确保持仓列表实盘价格**先显示出来**。
**涉及文件 ** :本记录(docs/04-迭代记录/04-WS盘中价格实时更新/迭代说明.md)。
### 2026-08-31 · 服务端:新增 subscribe-ws / tick-snapshot 代理端点
- **做了什么**: src/api.js 新增两个端点(均走 DSH 服务端代理,无跨域):
- `qmt-connections/subscribe-ws` :代理 `POST {激活baseUrl}/data/subscribe` ( body { type, codes }),返回 { ok, sub_id };
- `qmt-connections/tick-snapshot` :代理 `GET {激活baseUrl}/data/tick?codes=...` ,返回 { ok, data: { code: snapshot } }。
- **为什么**:订阅/快照是 RESTful 接口,浏览器直连 QMT Bridge 会跨域(无 CORS 头、OPTIONS 404),统一走服务端代理(与 qmt-connections/test 同模式,项目既有架构约定)。
- **涉及文件**: src/api.js。
### 2026-08-31 · 客户端:MarketDataProvider 重构(服务端代理 + 快照轮询兜底)
- **做了什么**:重写 src/client/market/MarketDataProvider.jsx:
- 订阅 / 快照 / 退订全部改走服务端代理端点(/odl/api/qmt-connections/subscribe-ws、tick-snapshot、unsubscribe-ws),**移除前端直连 QMT Bridge REST**;
- **双通道价格源**:① tick 快照轮询(3s 间隔,走服务端代理,稳定兜底,确保现价一定显示);② WS 推送(增强通道,收到即实时覆盖);
- WS 连接保持前端直连(WebSocket 不受 CORS 限制),断线重连、激活配置切换联动保留。
- **为什么**:实证确认 QMT Bridge REST 无 CORS 头(浏览器直连被拦截);「先实现订阅后的数据显示」——快照轮询保证现价先显示出来,WS 作为增强。
- **涉及文件**: src/client/market/MarketDataProvider.jsx。
### 2026-08-31 · 模拟验证
- **做了什么**:Node 模拟新版前端流程(tick 快照轮询走服务端代理)→ 一次返回全部关注 code( 600719.SH/601117.SH/603028.SH/001330.SZ/002065.SZ)的 lastPrice + lastClose,现价提取完整(如 600719.SH → 7.17,昨收 7.22 → 跌,UI 绿色)。
- **为什么**:验证服务端代理 + 快照轮询链路可用,前端渲染 PriceCell 即可显示红涨绿跌。
- **涉及文件**:无(验证记录)。
### 2026-08-31 · 端到端验收(宿主链路)
- **做了什么**:服务端新端点经宿主验证生效(subscribe-ws 订阅成功拿 sub_id; tick-snapshot 返回真实持仓现价);前端 bundle 确认加载新代码(rev=ae40cadd056a,与本地 lib 一致)。
- **端到端链路验证**(Node 模拟前端调宿主 /odl/api):
- 激活配置 GEMWIN → 订阅 ok( sub_id)→ 13 条持仓 → tick 快照 5/5 命中现价 → 红涨绿跌判定正确(600719.SH 跌→绿、603028.SH 涨→红)→ 退订 ok。
- **验证中发现**: /odl/api/positions 端点返回**数组**( value 直接是数组),非 summary 的 {positions:[...]} 结构(测试脚本字段误用,已修正;非产品问题)。
- **待老师确认**:浏览器刷新后 3 个表格现价列显示与红涨绿跌效果。
- **涉及文件**:无(验证记录)。
### 2026-08-31 · 修复:快照用真实持仓代码(根因定位)
- **现象**:刷新后订阅成功(sub_id 日志出现),但快照日志与现价均无。
- **定位(实证,非猜测)**:给前端加调试日志(订阅/快照/prices 三处)→ 只见订阅成功,无快照日志 → 审查 fetchSnapshot 传参为 'SH,SZ'(交易所代码)→ 宿主实测:tick-snapshot 传 'SH,SZ' 返回 **data 为空 ** {},传真实股票代码(600719.SH)才返回数据。
- **根因**: QMT Bridge 的 /data/tick 需要**真实股票代码**,前端快照却传了交易所代码 'SH,SZ',导致 data 为空、现价无数据。
- **修复**: MarketDataProvider 新增 fetchWatchCodes()(从 /odl/api/positions 获取持仓 code 集合),快照改用真实持仓代码。
- **验证**:Node 模拟修复后流程 → 13 只持仓全部取到现价(002347.SZ → 6.25 昨收 6.14 红涨;600719.SH → 7.17 昨收 7.22 绿跌)。
- **涉及文件**: src/client/market/MarketDataProvider.jsx。
### 2026-08-31 · 架构升级:服务端中转 + 行情缓存(老师定)
- **老师决策**:DSH 服务端做「一次 WS 订阅 + QMT 桥整合」,维护一份实时行情缓存(内存 Map);页面统一 5s 轮询读 DSH 接口(按 code 查询),不做前端直连。
- **实现**:
- 新增 src/market-cache.js: MarketCache 类(服务端连 ws://<激活host>:8610/ws 收全市场推送 → 写缓存 Map;断线指数退避重连;setBaseUrl 热切换;getByCodes 按 code 查询);
- src/index.js:创建 MarketCache 并启动,插件释放时 stop;激活热切换同步 setBaseUrl;
- src/api.js:新增 `market-snapshot` 端点(按 codes 查询缓存);激活/更新/删除连接时同步 marketCache;
- src/client/market/MarketDataProvider.jsx: **移除前端直连 WS**,改为 5s 轮询 /odl/api/market-snapshot(先取持仓 code,再按 code 查询)。
- **WS 推送稳定性实验**( Node 直连 GEMWIN, 2026-08-31):
- 不订阅直连 10s → 0 条;REST 订阅后连 10s → 0 条;退订后再连 10s → 8 条;
- 结论:**QMT Bridge WS 推送是会话级/有状态行为**,仅首个连接或特定订阅窗口会推送——该问题归属 QMT Bridge 工作空间(老师负责,另行解决)。
- **缓存兜底**: MarketCache 以 WS 推送更新缓存为主,**辅以 REST tick 快照补数据**(REST 稳定可用,13 只持仓全部可取),确保缓存有值、前端轮询能拿到现价。
- **涉及文件**: src/market-cache.js(新增)、src/index.js、src/api.js、src/client/market/MarketDataProvider.jsx。
### 2026-08-31 · 缓存兜底:market-snapshot 按需补 + REST 定时刷新
- **做了什么**:
- src/market-cache.js: MarketCache 加 REST tick 快照兜底(每 5s 对缓存已有 code 用 /data/tick 刷新);
- src/api.js: market-snapshot 端点加「miss 按需补」——查询的 code 不在缓存时,现场用 REST /data/tick 拉取补入缓存并返回(**不依赖 WS 推送**,REST 稳定可用)。
- **为什么**: 2026-08-31 实验证实 QMT Bridge WS 推送是会话级/有状态行为(仅首个连接或特定订阅窗口推送),不能作为唯一数据源;REST tick 稳定可取(13 只持仓全取到),作为兜底确保前端轮询必有数据。
- **验证**: Node 模拟 market-snapshot(缓存空→按需补→5/5 命中→缓存 5 条→再次查询全命中)。
- **涉及文件**: src/market-cache.js、src/api.js。
### 2026-08-31 · 结构调整:src/api.js 按领域拆分
- **做了什么**(老师要求,审视结构后清理):
- 原 src/api.js(377 行,聚合 5 类端点)拆分为:
- src/api/index.js:路由入口(合并各领域方法表 + 405/404/500 公共处理 + 领域分发);
- src/api/common.js:公共工具(writeJson / readJsonBody / resolveActiveBaseUrl);
- src/api/positions.js:持仓域(positions);
- src/api/strategies.js:策略/份额/tabs 域(14 端点);
- src/api/qmt-connections.js: QMT 连接域(11 端点,含热切换编排 setBaseUrl + marketCache 同步);
- src/api/market.js:行情域(market-snapshot,含 miss 按需补)。
- 更新 src/index.js import( ./api.js → ./api/index.js)与 tsdown.config.ts 入口。
- **验证**: typecheck 0 错误;构建通过(lib/api/index.js + api chunk 正常);27 个端点方法表核对完整(positions 1 + strategies 14 + qmt 11 + market 1);构建产物模块加载正常(registerApi 正确导出)。
- **涉及文件**: src/api/*( 6 个新文件)、src/index.js、tsdown.config.ts;删除 src/api.js。
### 2026-08-31 · 拆分验收(宿主重启后全端点回归)
- **做了什么**:宿主重启后对拆分后的 API 做全端点回归测试。
- **结果**:
- 27 端点全部可用(15 项首轮通过;3 项「失败」为测试参数错误——shares=0 触发业务校验、无未分配份额触发 no-unallocated,属正确业务行为);
- 合法参数重测 add-shares / remove-shares / move-all-shares 全部 OK( 600719.SH 100 股往返,清理干净);
- 核心端点:market-snapshot 13/13、active-host GEMWIN、QMT CRUD、subscribe-ws/unsubscribe-ws、tick-snapshot( lastPrice=7.15)全部通过。
- **结论**:api.js 按领域拆分无功能回归,验收通过。
- **涉及文件**:无(验证记录)。
### 2026-08-31 · 结构调整:创建 src/component/ 目录 + market-cache 拆分为 Hub/Feed
- **老师决策**:在 src/ 下统一创建 component/ 目录收纳业务模块(便于统一位置管理);market-cache 拆成多个模块各一个文件。
- **实现**:
- 新建 src/component/ 目录:
- MarketDataHub.js:行情缓存服务(缓存 Map + getByCodes 查询 + **miss 自动补拉收敛在内部 ** + ingest 写入入口);
- MarketFeed.js:行情数据获取(WS 订阅连接 + REST 定时刷新兜底 + 断线重连 + setBaseUrl 热切换),写入 Hub;
- src/index.js:装配改为 marketHub + marketFeed(替代 marketCache);
- src/api/market.js: **移除 miss 补拉代码**,只调 hub.getByCodes( HTTP 编排纯净);
- src/api/qmt-connections.js:热切换 marketCache?.setBaseUrl → marketFeed?.setBaseUrl;
- src/api/index.js: runtime 命名 marketCache → marketHub/marketFeed;
- 删除 src/market-cache.js。
- **职责划分**(解决「职责混乱」):
- MarketDataHub = 缓存 + 查询(单一职责,查询保证有值);
- MarketFeed = 取数(WS + REST 刷新),不对外查询;
- api/market.js = 纯 HTTP 编排,不碰缓存内部。
- **验证**: typecheck 0 错误;构建通过(chunk 打包正常,无动态导入残留);Node 模拟 hub.getByCodes 三场景(空缓存 miss 补拉 / 命中缓存 / 重复全命中)全部通过。
- **涉及文件**: src/component/MarketDataHub.js(新增)、src/component/MarketFeed.js(新增)、src/index.js、src/api/market.js、src/api/qmt-connections.js、src/api/index.js;删除 src/market-cache.js。
### 2026-08-31 · 拆分验收(宿主重启后全端点回归)
- **结果**:宿主重启后全端点回归全部通过:
- market-snapshot 13/13( miss 补拉正常,600719.SH → 7.18);二次查询命中缓存 13 条;
- summary 13 只;qmt-connections / active-host( GEMWIN) / activate 热切换 / subscribe-ws+unsubscribe-ws / tick-snapshot( 7.18) / strategies( 2 个)全部 OK。
- **结论**: MarketDataHub + MarketFeed 拆分无功能回归,component/ 目录结构落地,验收通过。
- **涉及文件**:无(验证记录)。
### 2026-08-31 · 优化:移除冗余端点 + 前端去重 + 模块迁入 component/
* * #1 移除冗余端点**(死代码清理):
- 移除 src/api/qmt-connections.js 的 subscribe-ws / unsubscribe-ws / tick-snapshot 三端点(架构演进后无人调用:前端无引用、MarketFeed 直接 new WebSocket 不依赖它们)。
- 涉及文件:src/api/qmt-connections.js。
* * #2 前端去重**(消除重复 positions 调用):
- MarketDataProvider 不再自拉 positions 获取关注 code,改为暴露 registerCodes(codes),由表格组件渲染后上报(AllPositionsTab 上报 summary 持仓 code、StrategyTab 上报策略持仓 code);Provider 只轮询 market-snapshot。
- 收益:减少一次网络往返;Provider 与表格数据天然同步(同一份 code 来源)。
- 涉及文件:src/client/market/MarketDataProvider.jsx、src/client/views/AllPositionsTab.jsx、src/client/views/StrategyTab.jsx。
* * #3 业务模块迁入 src/component/**(结构治理延续):
- 迁移:storage.js → component/AllocationStorage.js; position/manager.js → component/PositionManager.js; data-source/qmt-bridge-rest.js → component/QmtBridgeRestDataSource.js; data-source/types.js → component/data-source-types.js。
- 删除空的 src/position/、src/data-source/ 目录;更新 index.js import、tsdown 入口(src/component/*.js)、JSDoc 类型引用。
- 收益:lib/component/ 每个模块独立编译文件,结构统一(component = 业务模块,api = 路由,client = 前端)。
- 涉及文件:迁移 4 文件 + src/index.js + tsdown.config.ts + 2 处 JSDoc。
**验证 ** : typecheck 0 错误;构建通过(lib/component/ 6 个模块独立文件);构建产物模块加载全部正常。
### 2026-08-31 · 优化验收(宿主重启后)
- **全端点回归通过**: positions 13 条、market-snapshot 13/13、summary 13 只、strategy-positions 12 只、qmt-connections/active-host GEMWIN、已移除端点 tick-snapshot 返回 404(确认移除生效)、test 可达 82ms、二次 market-snapshot 命中缓存。
- **备注**:回归中出现一次 positions/summary fetch failed,排查为 QMT Bridge 瞬时网络抖动(Tailscale relay 切换,ping 通 + health ok 后恢复),非代码问题。
- **结论**: #1 移除冗余端点 + #2 前端去重 + #3 模块迁入 component/ 三项优化全部完成,无功能回归,验收通过。
- **涉及文件**:无(验证记录)。
### 2026-08-31 · 数据误删事故分析与防再发机制
**事故 ** :大连热电(600719.SH)与万顺新材(300057.SZ)的份额分配数据丢失。
**根因(非业务代码 bug,是测试脚本误操作) ** :
- 我在回归测试脚本(regression.mjs / retest-shares.mjs)中在**真实生产数据**上执行了写操作:
- regression.mjs 的 remove-all-shares(清空某策略全部份额);
- retest-shares.mjs 对 600719.SH 的 move-all-shares + remove-shares 清理。
- 这些操作本应只读或使用临时数据,误在真实 allocations.json 上执行,导致 600719 的 grid/manual-t 份额、300057 的 manual-t 份额被清空。
**恢复 ** :按老师提供的原值恢复(600719 = grid 800 + manual-t 1000; 300057 = grid 1000 + manual-t 1000),已确认 13 只全部正确。
**防再发机制(老师定:测试用独立数据目录) ** :
- AllocationStorage 新增测试模式:环境变量 ODL_TEST_DATA_DIR 或构造参数 dataDir 指向独立目录时,存储隔离,不影响真实 ~/.dsh/one-divine-lot/allocations.json;
- 已验证:临时目录实例执行 set/removeStrategyShares 写操作,真实数据不变;
- 测试规范:回归/测试脚本必须用独立数据目录,**禁止在真实数据上执行写操作**(只读端点可直连,写操作一律走测试实例)。
**涉及文件 ** : src/component/AllocationStorage.js(测试模式支持)。
### 2026-08-31 · R-006:策略数据 JSON data store schema 标准化
**需求定稿 ** (讨论确认):
- 范围:策略数据集(份额分配)标准化;策略定义仍留 DSH settings(设置页交互不变);
- 形态:store.schema.json(类 JSON Schema 描述 dataset) + store.json(数据,策略为中心);
- dataset 格式:[{code, shares}] 数组(可扩展);
- 旧 allocations.json 启动时自动迁移(保留 .bak 备份);
- 删除策略时 API 层联动删 store dataset(同现状联清份额模式)。
**实现 ** :
- 新增 src/component/DataStore.js: STORE_SCHEMA 定义 + 读写 store.json(原子写)+ 迁移 + dataset CRUD( getDataset/setDataset/removeDataset/getAllDatasets);
- 重写 src/component/PositionManager.js:存储改接 DataStore(策略为中心),对外方法签名不变(addToStrategy/removeFromStrategy/getSummary 等);
- src/index.js: DataStore 替代 AllocationStorage 装配。
**验证 ** (测试隔离目录):
- 迁移:allocations.json( code 为中心)→ store.json(策略为中心 strategies[].dataset=[{code,shares}]),旧文件备份 .bak ✅;
- schema 生成 ✅;dataset CRUD ✅;原子写多次 save 完整 ✅。
**涉及文件 ** : src/component/DataStore.js(新增)、src/component/PositionManager.js(重写)、src/index.js。
### 2026-08-31 · R-006 验收(宿主重启后)
- **迁移验证**:宿主重启自动迁移真实 allocations.json → store.json(策略为中心),13 只持仓份额完整转入(grid 13 只 + manual-t 2 只),旧文件备份 allocations.json.bak ✅
- **数据完整性**:大连热电 grid 800 + manual-t 1000(分配 1800);万顺新材 grid 1000 + manual-t 1000(分配 2000)✅
- **端点回归**: summary 13 只、strategy-positions(grid) 13 只、strategies 列表正常;add-shares 超限正确拒绝(业务校验);strategies/remove 联动清 dataset 正常;market-snapshot 行情正常 ✅
- **真实数据未污染**:测试用临时策略(add/remove 往返 + 删除联动),600719 份额不变 ✅
- **结论**: R-006 JSON data store schema 标准化完成,数据迁移零丢失,无功能回归,验收通过。
- **涉及文件**:无(验证记录)。
### 2026-08-31 · 修复:行情状态误显示「无激活配置」
- **现象**:点击持仓 tab 能加载持仓数据,但状态条显示「行情未连接(无激活配置)」。
- **根因**: MarketDataProvider 主循环在 watchCodes 为空(表格尚未上报 code)时设置 DISCONNECTED 状态;DISCONNECTED 文案「无激活配置」是前端直连时代遗留——R-006 后行情走服务端中转,前端对激活配置应**无感**。
- **修复**:
- Provider 主循环:watchCodes 为空时保持 IDLE(等待表格上报),不再设 DISCONNECTED;用 statusRef 同步最新状态避免闭包过期;
- PriceCell 状态文案:disconnected 改为「行情未连接」(去掉「无激活配置」);
- IDLE 时不显示状态条。
- **涉及文件**: src/client/market/MarketDataProvider.jsx、src/client/views/PriceCell.jsx。
### 2026-08-31 · 行情持久化(store.market.json,解决首屏无价格)
- **问题**:行情缓存仅内存(MarketDataHub.cache Map),宿主重启后为空;持仓 tab 首次刷新时价格 miss,若 REST 补拉慢/失败则显示「—」。
- **根因**:行情未持久化——重启后无上次价格,需每次实盘订阅/拉取。
- **方案(老师确认:DataStore 就是为数据快速展现存在)**:
- DataStore 新增行情持久化:store.market.json( code → snapshot,原子写);
- MarketDataHub 接入:启动 warmup 读盘 → 内存缓存首屏即有价;ingest 增量更新后防抖写盘(3s);查询命中顺序:内存 → 磁盘 → REST 补拉;
- index.js: dataStore 注入 Hub + 启动预热。
- **验证**(测试隔离目录):实盘 ingest → 写盘 2 条;模拟重启 warmup 加载 2 条;首次查询 2/2 命中(600719 lastPrice=7.16)——重启后首屏即有价格 ✅。
- **涉及文件**: src/component/DataStore.js、src/component/MarketDataHub.js、src/index.js。
### 2026-08-31 · 修复:启动报错 storage 未初始化(TDZ)
- **现象**:宿主重启后插件加载失败,报 `Cannot access 'storage' before initialization` 。
- **根因**: index.js 装配时序错误——marketHub 创建时引用 storage( dataStore: storage),但 storage 在其后才创建(const 暂时性死区 TDZ)。
- **修复**:将 storage 创建移到 marketHub 之前(55 行),顺序改为 storage → marketHub → marketFeed。
- **验证**:装配顺序正确,构建通过。
- **涉及文件**: src/index.js。
### 2026-09-01 · 行情持久化验收(宿主重启后)
- **启动修复**: storage 时序问题(TDZ)修复后宿主正常启动。
- **持久化验证**:
- store.market.json 已生成(13 条行情快照,600719 大连热电 lastPrice=7.16 等完整字段);
- market-snapshot 查询 13/13 命中(内存→磁盘→REST 三级);
- 即使 QMT 断网,磁盘缓存也能提供上次价格(首屏有价,不依赖实盘订阅)。
- **结论**:行情持久化闭环完成,验收通过。
- **涉及文件**:无(验证记录)。
### 2026-09-01 · 修复:持仓 tab 价格要等很久才出现(时序)
- **现象**:打开全部持仓,价格要等很久(约 5s)才刷新出来。
- **根因**: MarketDataProvider 主循环是 5s 定时器;页面加载时 poll() 立即跑一次但 watchCodes 为空(表格未上报)跳过;表格加载后 registerCodes 上报 code,但**不触发立即拉取**,只能等下一次定时器(最多 5s)才有价格。
- **修复**: registerCodes 上报新 code 后**立即触发一次 pollMarket**(通过 pollMarketRef 引用),首屏价格快速显示;定时器继续 5s 增量更新。
- **验证**:构建通过。服务端 market-snapshot 本就 13/13 有价(已实测),修复后前端上报即拉取,不再等定时器。
- **涉及文件**: src/client/market/MarketDataProvider.jsx。
### 2026-09-01 · MarketFeed 启动主动拉取持仓盘口(prime)
- **问题**(老师指出):前端打开页面只有持仓数据、价格要等行情刷新才出现;但行情刷新是服务端职责,前端应直接拿到最新最终价格。
- **根因查证**:插件启动时 MarketFeed 只做了 ①warmup 读磁盘(可能旧/空)②连 WS(被动等推送,QMT 推送会话级不可靠)③定时刷新(**空缓存时跳过**)——**没有任何「启动主动拉一次最新盘口」的逻辑**。
- **修复**: MarketFeed.start() 新增 _primePositions():启动即从 dataSource 拿持仓 code → REST /data/tick → 写入缓存;setBaseUrl 热切换时也主动拉一次(新地址盘口)。
- **验证**: Node 模拟启动 → prime 拉取 13 条盘口,缓存 13 条,查询 600719 → 7.16 ✅。
- **效果**:服务端启动即有最新盘口价,前端任何时候打开页面都能从缓存拿到价格(不依赖前端轮询触发)。
- **涉及文件**: src/component/MarketFeed.js。
### 2026-09-01 · 需求:移除行情状态条 + 订阅状态灯 + QMT 健康检查
**需求 1:移除「行情实时」状态条 **
- 前端表格彻底移除 MarketStatusBar( PriceCell 删除该组件;AllPositionsTab / StrategyTab 移除引用与 status/wsInfo 解构)。
- 原则:行情刷新是服务端职责,前端只拿结果,对行情状态彻底无感。
- 涉及文件:src/client/views/PriceCell.jsx、AllPositionsTab.jsx、StrategyTab.jsx。
**需求 3: QMT 连接健康检查 **
- 新增 market-status 端点(src/api/market.js):探测激活 QMT Bridge /health 可达性(5s 超时)+ 返回订阅状态 + 缓存大小。
- MarketFeed 新增 getStatus()(WS 连接态、最近推送时间、推送计数)。
**需求 2:订阅状态灯(QMT 切换 chip) **
- QmtConnectionChip 文字前加圆点状态灯:
- 绿:订阅存在 + 数据正常接收 + QMT 健康;
- 黄:订阅存在但数据停滞(最近 30s 无推送);
- 红:QMT 连接不健康;
- 灰:无订阅。
- 每 5s 轮询 market-status;下拉菜单显示订阅明细(推送次数)+ QMT 健康(延迟)。
- 涉及文件:src/client/views/QmtConnectionChip.jsx。
**验证 ** : typecheck 0 错误;构建通过;本地模拟 market-status( QMT 健康 162ms、订阅 active+dataHealthy、状态灯绿)逻辑正确。
### 2026-09-01 · 修正:market-status 数据健康判断(WS 不可靠时结合缓存)
- **问题**:宿主重启后 market-status 返回 wsConnected=true 但 tickCount=0、lastTickAt=0 → dataHealthy=false → 状态灯黄(停滞)。但缓存实际有 13 条(REST prime/轮询兜底)。
- **根因**:数据健康只以「WS 收到推送」为标准,而 QMT WS 推送是会话级/有状态(连接后可能 0 推送)。
- **修正**: dataHealthy = WS 推送新鲜(30s 内)**或** 缓存有数据(cacheSize > 0)——REST 兜底保证缓存更新时数据仍是好的。
- **涉及文件**: src/api/market.js。
### 2026-09-01 · 状态灯不稳定分析(黄/灰波动)
- **实测**(连续 5 次 market-status):qmt.healthy=true、wsConnected=true( WS 稳定)、tickCount=0、cache=13、dataHealthy=false → 灯黄。
- **根因**:
1. 服务端 dataHealthy 修正(wsFresh || cacheSize) **未生效**——宿主加载的是上一轮构建前的旧代码(构建在重启后完成);
2. 前端首次加载 marketStatus=null 显示灰,轮询成功后变黄;「一会儿又变灰」可能与前端某次请求失败/时序有关(已加调试日志待确认)。
- **修复**:
- 服务端:dataHealthy = WS 推送新鲜 || 缓存有数据(WS 不可靠时缓存兜底);
- 前端:loadStatus 失败保留上次状态(不误降灰);加调试日志定位。
- **待办**:重启宿主让服务端修正生效,观察状态灯应为绿(订阅+缓存正常)。
### 2026-09-01 · 数据存储设计文档(设计约束)
- **做了什么**:整理 DataStore 设计为独立设计约束文档 docs/03-设计约束/数据存储设计.md:
- 设计目标(数据快速展现、标准化描述、统一 JSON 操作);
- 总体架构(store.schema.json / store.json / store.market.json 三层);
- 数据文件结构与 Schema(策略为中心份额分配 + 行情快照缓存);
- 数据流(启动读盘 → 实盘更新 → 防抖写盘 → 三级查询命中);
- 关键设计决策表(JSON 存储、策略定义留 settings、dataset 数组、行情持久化、原子写、测试隔离);
- 迁移(allocations.json 自动迁移 + 备份)。
- **同步更新**:技术方案约束.md —— 技术约束-010 更新为「服务端中转 + 缓存」(原前端直连已废弃);新增技术约束-012(数据存储设计引用)。
- **涉及文件**:docs/03-设计约束/数据存储设计.md(新增)、docs/03-设计约束/技术方案约束.md。
### 2026-09-01 · 重做:移除全盘订阅状态标记,独立 QMT health 检查
**移除 ** (全盘订阅状态暂缓,老师重开筛选做):
- market-status 端点的 subscriptions 部分(订阅状态);
- MarketFeed.getStatus()(订阅状态);
- QmtConnectionChip 的订阅状态灯 + market-status 轮询。
**保留并重做 ** ( QMT health 检查,需求 3):
- 服务端:market-status → 精简为 qmt-health 端点(仅 QMT /health 可达性 + 延迟);
- 前端:QmtConnectionChip 下拉框**右侧**新增独立 QmtHealthIndicator 控件(小圆点:绿灯=健康 / 红灯=异常;悬停显示延迟/错误;每 5s 轮询 qmt-health)。
**涉及文件 ** : src/api/market.js、src/component/MarketFeed.js、src/client/views/QmtConnectionChip.jsx。
### 2026-09-01 · QMT 健康检查改为服务端定时 + 缓存机制
**机制(老师定) ** :
- 服务端 QmtHealthMonitor 每 **5 分钟**探测一次激活 QMT /health,结果缓存内存;
- 前端打开页面读缓存(**秒回**,不等实时探测);
- 前端点击圆点触发即时探测(refresh=true)刷新缓存;
- 激活配置切换时立即补探(热切换联动)。
**实现 ** :
- 新增 src/component/QmtHealthMonitor.js:定时探测(5min) + 缓存 + getStatus(读缓存)+ probe(即时探测,防并发);
- src/api/market.js: qmt-health 默认读缓存;refresh=true 触发即时探测;
- src/index.js:创建并启动 QmtHealthMonitor,注入 registerApi,释放时 stop;
- src/api/qmt-connections.js:激活/更新/删除连接后 qmtHealthMonitor.probe() 联动;
- 前端 QmtHealthIndicator:打开读缓存、30s 低频同步、点击触发即时探测(悬停显示检测时间/延迟/错误)。
**验证 ** : typecheck 0 错误;构建通过;QmtHealthMonitor 模块加载正常。
### 2026-09-01 · QMT 健康检查验收(宿主重启后)
- **验证**:
- 读缓存(默认):healthy=true、fromCache=true,耗时 23ms(秒回);
- 即时探测(refresh=true):healthy=true、latencyMs=745ms;
- 再读缓存:fromCache=true(命中探测结果),耗时 3ms。
- **结论**:服务端定时探测(5 分钟)+ 缓存机制生效;前端读缓存秒回,点击触发即时探测;验收通过。
- **涉及文件**:无(验证记录)。
### 2026-09-01 · 修复:store.market.json 膨胀(38MB,全市场行情入库)
- **问题**: store.market.json 膨胀到 38MB( 50910 只股票行情)。
- **根因**: MarketFeed WS 推送是全市场(whole),MarketDataHub.ingest 不过滤,把全市场 5 万+ code 写进缓存并持久化。
- **修复**: MarketDataHub 增加 watchCodes 关注集合(前端查询 code + 持仓 code 自动加入);ingest 只保留关注集合内 code;写盘也只写关注集合(防抖)。MarketFeed prime 把持仓 code 加入 watch。
- **清理**:删除膨胀的 store.market.json(修复后自动重建,只含持仓行情)。
- **验证**: Node 模拟 WS 推 100 只 → 缓存只 3 只持仓 → store.market.json 只 3 条 ✅。
- **涉及文件**: src/component/MarketDataHub.js、src/component/MarketFeed.js。
### 2026-09-01 · 修复 v2: warmup 认可全市场导致 watchCodes 过滤失效
- **问题**:重启后 store.market.json 又膨胀到 38MB( 50910 键)——watchCodes 过滤未生效。
- **根因**: MarketDataHub.warmup 从磁盘加载旧数据时,把**所有 code 都加入 watchCodes**( this.watch(code))——磁盘历史认可了全市场,后续 WS 推送全部保留。
- **修复**: warmup 只加载数据到内存 cache, **不加入 watchCodes**( watchCodes 只由「持仓 + 前端查询」驱动)。
- **清理**:再次删除膨胀文件(重建后只含持仓 13 条)。
- **涉及文件**: src/component/MarketDataHub.js。
### 2026-09-01 · 修复 v3: setMarketQuotes 整体替换(清除旧键残留)
- **问题**: v2 后 store.market.json 仍含旧全量数据(100 条旧 + 新)。
- **根因**: DataStore.setMarketQuotes 是**增量合并**( this.market[code]=snap),旧键永不清除——磁盘旧数据(如 50910 条)残留,每次合并越积越多。
- **修复**: setMarketQuotes 改为**整体替换**( this.market = next),传入完整集合,清除不在其中的旧键。
- **验证**:磁盘预置 100 旧 → warmup → 持仓查询 → WS 推送 → 写盘只 2 条(600719/300057),旧 100 条清除 ✅。
- **清理**:删除真实膨胀文件(重启重建只含持仓)。
- **涉及文件**: src/component/DataStore.js。
### 2026-09-01 · 膨胀修复验收(宿主重启后)
- **结果**: store.market.json 从 38MB( 50910 键)恢复为 **11KB( 13 键 = 13 只持仓) ** ,无多余键。
- **结论**: watchCodes 过滤 + warmup 不认可全市场 + setMarketQuotes 整体替换 三层修复全部生效,膨胀 bug 彻底解决,验收通过。
- **涉及文件**:无(验证记录)。
### 2026-09-01 · 决策:全市场缓存方案搁置(保持 watchCodes)
- **讨论**:老师考虑「全市场缓存」(前端任意 A股实盘查询秒回),vs 当前 watchCodes(持仓 + 前端查询自动累积)。
- **实测发现**: QMT whole 推送 26,751 条中,**标准 A股仅 2,775 只(10.4%) **,其余 23,976 条为期权/衍生品/其他品种代码段(23x/10x/19x/16x 等)——A股真实约 5,400 只,推送全市场含大量非股票。
- **结论(老师定)**:全市场缓存方案**搁置**,保持当前 watchCodes 方案(持仓 + 前端查询过的 code 自动累积,内存稳定 ~13 条);
- **记录**:若未来启用全市场缓存,需只缓存标准 A股代码(正则过滤 60/688/000/001/002/003/300/301),并先解决 WS 推送稳定性(归属 QMT Bridge 工作空间)。