Compare commits

...
19 Commits
Author SHA1 Message Date
FoXiMao f8f4da93dc docs(FEAT-MO-004): 更新GitNexus索引统计 2026-08-24 23:27:47 +08:00
FoXiMao a7d85bc4a3 docs(FEAT-MO-004): 固化指数板块详情页设计 2026-08-24 23:26:46 +08:00
FoXiMao 2cf31f5137 docs(FEAT-MO-001): 修正概览Review结论门禁 2026-08-24 23:13:21 +08:00
FoXiMao 96232ff225 docs(FEAT-MO-001): 记录概览时间修复追溯 2026-08-24 23:11:01 +08:00
FoXiMao b1310fb86d fix(FEAT-MO-001): 修正概览时间时区显示 2026-08-24 23:08:46 +08:00
FoXiMao 1e80e7d2de docs(FEAT-DS-001): 更新健康检查修复追溯 2026-08-24 22:45:05 +08:00
FoXiMao 9ff6646d56 fix(FEAT-DS-001): 修正MySQL健康检查会话时区 2026-08-24 22:42:21 +08:00
FoXiMao a10c785ff8 docs(FEAT-DS-001): 固化最终开发摘要 2026-08-24 21:53:56 +08:00
FoXiMao 358004fd12 docs(FEAT-DS-001): 更新最终GitNexus基线 2026-08-24 21:52:08 +08:00
FoXiMao 1398804a6f style(FEAT-DS-001): 统一超短行情代码格式 2026-08-24 21:49:48 +08:00
FoXiMao 61283c5c38 docs(FEAT-DS-001): 完成AKTools治理追溯 2026-08-24 21:44:23 +08:00
FoXiMao 6e8a05114e feat(FEAT-DS-001): 接入AKTools超短行情补充数据源 2026-08-24 21:40:22 +08:00
FoXiMao 81c48af8cc docs(FEAT-DS-001): 设计 AKTools 超短补充数据源 2026-08-24 20:43:14 +08:00
FoXiMao 0d0d1eea45 docs(FEAT-QA-001): 补充修复与索引记录 2026-08-24 17:56:20 +08:00
FoXiMao 368672f001 fix(FEAT-QA-001): 隔离市场概览 E2E 测试时钟 2026-08-24 17:53:09 +08:00
FoXiMao 7904571ddd docs(FEAT-MO-003): 补充提交推送记录 2026-08-24 17:52:26 +08:00
FoXiMao 6cbfd8d8a4 docs(FEAT-MO-003): 更新项目 README 2026-08-24 17:51:31 +08:00
FoXiMao ab0b53a56b docs(FEAT-MO-002): 补充提交推送记录 2026-08-24 17:50:49 +08:00
FoXiMao f00e114f88 chore(FEAT-MO-002): 调整 Web 开发端口为 8888 2026-08-24 17:49:19 +08:00
123 changed files with 3545 additions and 88 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **a-share-analysis** (2432 symbols, 3283 relationships, 63 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. This project is indexed by GitNexus as **a-share-analysis** (3472 symbols, 4950 relationships, 104 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.
+61 -8
View File
@@ -1,26 +1,59 @@
# A-Share Analysis # A-Share Analysis
A read-only A-share market overview and analysis tool. 一个只读的 A 股市场概览与盘面分析工具,面向个人复盘和小范围使用,不提供交易下单、荐股、社区或公开组合功能。
## 当前阶段 ## 当前阶段
项目正在建立工程基线,首个业务模块为市场综合概览。当前不包含用户体系、订阅与支付。 项目当前以市场综合概览为首个业务模块,用户体系、订阅与支付暂不进入首屏业务范围。
当前已接入 TDX 只读行情并完成以下首屏模块:
- 市场结论、驱动因素和风险摘要。
- 核心指数、三市场宽度、一级行业板块强弱和三市场成交额趋势。
- 综合、短线、中长线视角入口,以及规则、AI、混合、对照分析偏好入口。
- 指数和板块详情入口已预留,详情页另立功能实现;强势个股模块尚未开发。
- 超短情绪补充模块已接入本机 AKTools:展示涨停、最高连板、炸板、昨日涨停表现、涨停行业和规则结论;AKTools 不可用时独立降级。
主力净流入、完整盘中新闻、版本化概念题材成分和 AI 自动结论仍需独立数据源或后续功能立项,不用 TDX 或 AKShare 缺失字段冒充。
## Projects ## Projects
- `web/`: Vue 3 market terminal frontend. - web/: Vue 3、Vite、TypeScript、Naive UI、Dart Sass、Axios 前端。
- `server/`: NestJS API、Scheduler 与 Worker 三个独立进程。 - server/: NestJS API、Scheduler 与 Worker 三个独立源码进程。
- `deploy/`: MySQL 5.7、Redis、RocketMQ 5.x 的配置契约,以及 systemd/Nginx 源码部署模板。 - deploy/: MySQL 5.7.x、Redis、RocketMQ 5.x 的配置契约,以及 systemd/Nginx 源码部署模板。
- `docs/`: Product and implementation specifications. - docs/: PRD、架构、治理、部署和功能档案。
- tools/: Schema、契约、边界和治理验证工具。
The first implementation slice is the market overview screen in `web/`. 首个实现切片是 web/ 下的市场综合概览首屏。
## 环境与部署 ## 环境与部署
- [本地开发环境](docs/deployment/local-development.md):使用开发机已有 Redis,应用全部从源码运行。 - [本地开发环境](docs/deployment/local-development.md):使用开发机已有 Redis,应用全部从源码运行。
- [Ubuntu 源码生产部署](docs/deployment/ubuntu-source-production.md):项目唯一生产部署方式,支持 Ubuntu 22.04/24.04 LTS x86_64。 - [Ubuntu 源码生产部署](docs/deployment/ubuntu-source-production.md):项目唯一生产部署方式,支持 Ubuntu 22.04/24.04 LTS x86_64。
无秘密环境模板位于 `deploy/env/`。生产配置不进入 Git 或发布目录;Node.js 22.x、pnpm 10.25.0、Nginx 和外部基础设施由服务器管理员提供。项目不安装、启动、停止或删除 Redis、MySQL 和 RocketMQ。 无秘密环境模板位于 deploy/env/。生产配置不进入 Git 或发布目录;Node.js 22.x、pnpm 10.x、Nginx 和外部基础设施由服务器管理员提供。项目不安装、启动、停止或删除 Redis、MySQL 和 RocketMQ。
前端开发服务器固定监听 127.0.0.1:8888,API 使用 127.0.0.1:3000。当前首屏不依赖 Scheduler、Worker 或 RocketMQ 消费链路;MySQL、Redis 和 RocketMQ 是否启用,以各进程的 local 配置和具体功能需求为准。
AKTools 是项目外部维护的 AKShare HTTP 服务,开发机默认监听 `127.0.0.1:8080`。API 仅在 `AKTOOLS_ENABLED=true` 时调用三个固定接口;项目不安装、启动、停止或暴露 AKTools,也不允许前端直接请求它。生产是否启用必须先完成上游授权、稳定性和访问控制评估。
数据库开发约束:Schema 基线只能读取开发库 a_share_analysis_dev,禁止触碰生产库;字符集为 utf8mb4,排序规则为 utf8mb4_general_ci,时区为东八区。
## TDX 连接方式
平台始终访问 Mac 本机回环地址 http://127.0.0.1:17710/。该地址由 SSH 隧道转发到家中 Windows 的 TDX 客户端 127.0.0.1:17709,不把 TDX 暴露到局域网或公网。
在家使用 tdx-windows-lan,在外通过 Tailscale 使用 tdx-windows-tailnet;两种隧道不能同时启动:
# 外部网络场景
tailscale up
tailscale ping <WINDOWS_TAILSCALE_HOST>
ssh -fN -M -S ~/.ssh/tdx-windows-tailnet-control tdx-windows-tailnet
nc -vz 127.0.0.1 17710
lsof -nP -iTCP:17710 -sTCP:LISTEN
完整的 Windows OpenSSH、双场景切换、停止和只读冒烟步骤见 docs/deployment/tailscale-tdx-development.md。
## 开发入口 ## 开发入口
@@ -28,6 +61,9 @@ The first implementation slice is the market overview screen in `web/`.
- [代码开发规范](docs/governance/code-development-standards.md) - [代码开发规范](docs/governance/code-development-standards.md)
- [开发流程规范](docs/governance/development-workflow.md) - [开发流程规范](docs/governance/development-workflow.md)
- [系统架构](docs/architecture/system-architecture.md) - [系统架构](docs/architecture/system-architecture.md)
- [本地开发环境](docs/deployment/local-development.md)
- [TDX 隧道开发手册](docs/deployment/tailscale-tdx-development.md)
- [Ubuntu 源码生产部署](docs/deployment/ubuntu-source-production.md)
开始任何模块前,先阅读根目录 `AGENTS.md`,然后运行: 开始任何模块前,先阅读根目录 `AGENTS.md`,然后运行:
@@ -39,6 +75,23 @@ npm run governance:start -- FEAT-MO-001 market-overview
Schema 命令只允许处理固定开发库;导出器使用 Node.js `mysql2`,本机无需安装 MySQL Server 或 CLI。当前 MySQL 未启用时会为上一发布版本生成可验证的不适用清单,不会连接数据库或创建空 SQL;启用但无对象时生成 format v3 `captured_empty` 清单。完整规则见 [开发流程规范](docs/governance/development-workflow.md)。 Schema 命令只允许处理固定开发库;导出器使用 Node.js `mysql2`,本机无需安装 MySQL Server 或 CLI。当前 MySQL 未启用时会为上一发布版本生成可验证的不适用清单,不会连接数据库或创建空 SQL;启用但无对象时生成 format v3 `captured_empty` 清单。完整规则见 [开发流程规范](docs/governance/development-workflow.md)。
### 本地启动
pnpm install --frozen-lockfile
pnpm check:runtime
redis-cli -h 127.0.0.1 -p 6379 ping
pnpm --filter @a-share-analysis/server dev:api
pnpm --filter @a-share-analysis/web dev
访问 Web:<http://127.0.0.1:8888/>;访问 API 健康检查:<http://127.0.0.1:3000/api/v1/health>。前端同源 /api 请求由 Vite 代理到 API,strictPort 已启用,8888 被占用时会明确失败,不会静默换端口。
Scheduler 和 Worker 只在对应任务链路开发时启动:
pnpm --filter @a-share-analysis/server dev:scheduler
pnpm --filter @a-share-analysis/server dev:worker
前端 API 请求统一经过 web/src/utils/request.ts 封装的 Axios;业务请求位于 web/src/api/<业务域>/index.ts。
## 项目仓库 ## 项目仓库
- Gitea:<https://git.foximao.com/FoXiMao/a-share-analysis> - Gitea:<https://git.foximao.com/FoXiMao/a-share-analysis>
+4
View File
@@ -6,6 +6,10 @@ API_PORT=3000
LOG_LEVEL=debug LOG_LEVEL=debug
TDX_BASE_URL=http://127.0.0.1:17710 TDX_BASE_URL=http://127.0.0.1:17710
TDX_TIMEOUT_MS=5000 TDX_TIMEOUT_MS=5000
# AKTools 仅在本机内网补充超短行情,关闭时短线模块返回不可用。
AKTOOLS_ENABLED=false
AKTOOLS_BASE_URL=http://127.0.0.1:8080
AKTOOLS_TIMEOUT_MS=10000
# 第一阶段实际启用的 Redis。 # 第一阶段实际启用的 Redis。
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
+4
View File
@@ -5,6 +5,10 @@ API_PORT=3000
LOG_LEVEL=info LOG_LEVEL=info
TDX_BASE_URL=http://127.0.0.1:17710 TDX_BASE_URL=http://127.0.0.1:17710
TDX_TIMEOUT_MS=5000 TDX_TIMEOUT_MS=5000
# AKTools 仅允许受控内网回环部署,未配置时保持关闭。
AKTOOLS_ENABLED=false
AKTOOLS_BASE_URL=http://127.0.0.1:8080
AKTOOLS_TIMEOUT_MS=10000
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
REDIS_PORT=6379 REDIS_PORT=6379
+2
View File
@@ -40,6 +40,8 @@ tools/ 可重复执行的治理和开发工具
- Redis保存最新快照、缓存、锁和限流状态。 - Redis保存最新快照、缓存、锁和限流状态。
- Vue前端只读后端 API,不直接接触外部数据源和密钥。 - Vue前端只读后端 API,不直接接触外部数据源和密钥。
市场概览的 AKTools 补充链路是按需读取的例外:Nest API 通过固定 Adapter 调用外部 AKTools HTTP 服务,前端不直连,且失败只影响超短模块。它不经过 Scheduler、Worker、RocketMQ、MySQL 或 Redis,也不改变后续正式采集链路的总体架构。
## 3. 前后端模块边界 ## 3. 前后端模块边界
后端以业务模块为一级目录,模块内部按实际职责建立 Controller、Service、DTO、Domain、Repository 和 Adapter。小模块不强制凑齐目录,禁止把全部 Controller/Service 堆入全局目录,也禁止无业务逻辑的透传层。领域规则只依赖项目端口,MySQL、Redis、RocketMQ、免费数据源和 AI Provider 由基础设施 Adapter 实现。 后端以业务模块为一级目录,模块内部按实际职责建立 Controller、Service、DTO、Domain、Repository 和 Adapter。小模块不强制凑齐目录,禁止把全部 Controller/Service 堆入全局目录,也禁止无业务逻辑的透传层。领域规则只依赖项目端口,MySQL、Redis、RocketMQ、免费数据源和 AI Provider 由基础设施 Adapter 实现。
+11 -1
View File
@@ -29,7 +29,17 @@ pnpm --filter @a-share-analysis/server dev:worker
pnpm --filter @a-share-analysis/web dev pnpm --filter @a-share-analysis/web dev
``` ```
API 健康端点是 `http://127.0.0.1:3000/api/v1/health`,Web 是 `http://127.0.0.1:5173/`。Scheduler 与 Worker 在 GOV-009 中保持 `ROCKETMQ_ENABLED=false`;没有经过验证的 Node RocketMQ 客户端时,启用配置会直接失败,不使用内存队列伪降级。 API 健康端点是 `http://127.0.0.1:3000/api/v1/health`,Web 是 `http://127.0.0.1:8888/`。Scheduler 与 Worker 在 GOV-009 中保持 `ROCKETMQ_ENABLED=false`;没有经过验证的 Node RocketMQ 客户端时,启用配置会直接失败,不使用内存队列伪降级。
## AKTools 补充数据源
AKTools/AKShare 作为开发机外部服务单独运行,默认地址为 `http://127.0.0.1:8080`。启动 API 前确认服务可访问;需要启用短线情绪模块时,在 API local 配置中设置 `AKTOOLS_ENABLED=true`,否则模块明确显示不可用,不影响 TDX 模块。Nest 只允许三个已验证接口,前端不直连 AKTools。项目不会管理该 Python 服务的安装、进程和数据。
```bash
curl --fail 'http://127.0.0.1:8080/api/public/stock_zt_pool_em?date=20260824'
```
生产环境不能直接复用本机 AKTools 配置;启用前必须提供受控内网地址、认证/访问控制、上游授权和稳定性评估,并扩展服务端白名单校验。
## 日常验证与停止 ## 日常验证与停止
@@ -43,6 +43,10 @@ curl --fail http://127.0.0.1/
当前工程尚无业务表迁移;后续新增迁移后必须在切换 `current` 前单独执行并记录结果。生产环境禁止 `synchronize: true`,开发流程也禁止用生产数据库做 Schema 基线或连接测试。 当前工程尚无业务表迁移;后续新增迁移后必须在切换 `current` 前单独执行并记录结果。生产环境禁止 `synchronize: true`,开发流程也禁止用生产数据库做 Schema 基线或连接测试。
## AKTools 生产边界
AKTools 不随本项目发布,也不由 systemd 单元管理。生产 API 默认关闭 `AKTOOLS_ENABLED`;只有在 AKTools 以受控内网服务部署、完成访问控制、授权和稳定性评估后,才可在 API 的生产环境文件中启用。生产配置不得把 AKTools 地址暴露给浏览器,且必须同步修改服务端白名单和健康检查方案;未完成这些前置条件时保持短线模块 `unavailable`。
## 检查与回退 ## 检查与回退
使用 `systemctl status`、`journalctl -u`、HTTP 健康端点和 Nginx 同源访问结果确认发布。任一验证失败时,把 `/opt/a-share-analysis/current` 原子切回上一完整发布目录,重启三个 systemd 服务并重复健康检查。 使用 `systemctl status`、`journalctl -u`、HTTP 健康端点和 Nginx 同源访问结果确认发布。任一验证失败时,把 `/opt/a-share-analysis/current` 原子切回上一完整发布目录,重启三个 systemd 服务并重复健康检查。
@@ -0,0 +1,39 @@
# AKTools 超短行情补充数据源设计
## 方案
AKTools 保持为独立的内网 HTTP 服务,Nest 在市场概览模块内增加 AKTools Adapter。Adapter 负责请求、超时、响应大小和错误边界;Mapper 负责把 AKTools 中文字段转换成稳定领域 DTO;Service 并行调用三个接口并生成规则结论。
```text
Vue → Nest Controller → ShortTerm Service → AKTools Client → 127.0.0.1:8080 → AKShare
└→ TDX 既有市场概览链路保持独立
```
## 接口
```text
GET /api/v1/market-overview/short-term/emotion
GET /api/v1/market-overview/short-term/conclusion
```
两个接口共享一次请求内的 `snapshotId`,但不要求跨请求持久化。`emotion` 返回结构化指标,`conclusion` 返回规则结论和证据引用。
## 安全策略与评估
- `AKTOOLS_BASE_URL` 只能是配置值,禁止来自请求参数;启动时仅允许回环地址或明确内网白名单。
- 客户端只使用 GET 和固定接口名,不把任意函数名暴露成代理能力。
- 超时、响应体大小和 JSON 结构均在 Adapter 层限制;异常只返回脱敏 warning。
- AKTools 不直接对外开放;若未来扩展到多机内网,必须增加访问控制和反向代理认证。
- 不保存 AKShare 原始载荷、不记录真实内网地址、不提交凭据。
## 性能目标与评估
三个上游请求使用 `Promise.allSettled` 并行执行,单请求超时 10 秒,短线接口 P95 目标小于 12 秒。由于 V1 不持久化、不缓存,性能风险主要是 AKShare 上游限流和网络延迟;通过超时、部分结果和独立降级控制影响面。
## 错误处理与回退
AKTools 完全不可用时短线响应为 `unavailable`,TDX 响应不受影响。单个接口失败时返回 `partial` 并保留其他有效模块。回退只关闭 AKTools 功能开关和相关路由,不删除 TDX 代码。
## 架构同步
需要同步 OpenAPI DTO、Web API 类型、开发/生产环境模板、API 健康检查说明、测试和功能档案;不修改消息、数据库、Redis、Scheduler、Worker。
@@ -0,0 +1,33 @@
# AKTools 超短行情补充数据源开发总结
- 功能编号:FEAT-DS-001
- 状态:in_review
- 发布状态:unreleased
## 原始开发需求
在内网环境使用已经运行的 AKTools,补充 TDX 不提供的超短行情数据。
## 需求与范围变化
V1 采用 AKTools 外部服务直连,不新增 Python 服务和 Docker;暂不接入全市场现货接口,因为该接口本地验证返回 HTTP 500。
## 实际完成需求
已完成 AKTools 配置校验、固定接口 Client、中文字段 Mapper、短线情绪与规则结论 DTO/Service/Controller、OpenAPI 生成契约、Vue Axios API、Composable 状态、短线情绪面板和开发/生产环境模板。页面通过独立状态展示正常、部分、延迟、不可用和空数据,不阻塞 TDX 模块。
## 修复缺陷
修复了短线情绪面板缺失导致的前端测试导入失败,并补齐页面未消费短线结论响应的问题;同时修复页面接线遗漏造成的 `shortTermConclusion` 未定义告警。补充修复 MySQL 健康检查因 `mysql2` 的 `timezone` 选项不会设置服务端会话时区而误判不可用的问题:健康检查现在在同一连接上显式执行 `SET SESSION time_zone = '+08:00'`,完成元数据校验后始终释放连接。未发现 TDX 回归。
## 未完成与延期项
炸板率严格口径、资金净流入、盘中新闻、历史快照和生产授权评估不属于 V1。
## 关键提交
关键提交:`6e8a051 feat(FEAT-DS-001): 接入AKTools超短行情补充数据源`、`61283c5 docs(FEAT-DS-001): 完成AKTools治理追溯`、`1398804 style(FEAT-DS-001): 统一超短行情代码格式`、`358004f docs(FEAT-DS-001): 更新最终GitNexus基线`、`9ff6646 fix(FEAT-DS-001): 修正MySQL健康检查会话时区`,均已推送 `origin/main`。
## 验证证据
Schema 基线与校验通过(`v0.0.10 captured_empty`);开发前 GitNexus 风险 LOW,页面改动前对 `useMarketOverview` 影响分析为 LOW;后端 15 个测试文件 80 个测试通过,前端 8 个测试文件 23 个测试通过,Web typecheck 通过,`pnpm verify` 全量门禁通过且 Playwright 4 个测试通过。健康接口真实验证返回 HTTP 200,MySQL 为 5.7.42 / `a_share_analysis_dev` / `+08:00`。安全边界包括回环地址白名单、固定接口、超时、响应体上限、JSON 校验和错误脱敏;三个上游请求并行,单请求超时 10 秒,MySQL 健康查询使用连接级 finally 释放,当前性能证据为本机定向测试,不代表生产容量。
@@ -0,0 +1,24 @@
# GitNexus 开发后基线
- 功能编号:FEAT-DS-001
- 状态:已刷新
- 索引提交:`9ff6646`
## GitNexus 状态
```text
Repository: /Users/citrons/Documents/Codex/2026-08-12/wo-x/a-share-analysis
Indexed: 2026-08-24 22:43:05 GMT+8
Indexed commit: 9ff6646
Current commit: 9ff6646
Status: up-to-date
统计:3,445 nodes / 4,913 edges / 72 clusters / 104 flows
```
## 开发后影响
新增 AKTools Adapter、短线 Service/Controller、OpenAPI、Web API、Composable 和页面组件形成独立短线执行流;既有 TDX 概览执行流未被改写。`loadApiConfig` 上游影响为 LOW,直接消费者仅配置测试;`useMarketOverview` 页面影响为 LOW。数据库、Redis、RocketMQ、Scheduler 和 Worker 没有新增业务执行流。本轮 MySQL 健康检查修复的暂存区检测识别 `UseFactory → MysqlHealthClient` 与 `OnModuleDestroy → End` 两条连接生命周期流,工具风险 MEDIUM;逐项复核后确认仅是连接池获取/释放行为变化。
## 变更检测
提交 `9ff6646` 后已重新索引,状态为 up-to-date;`gitnexus_detect_changes({ scope: "all" })` 仅识别 GitNexus 生成的 `AGENTS.md` 统计行,风险 LOW,无业务执行流影响。健康检查代码及本功能档案已包含在 `9ff6646`,远程 `origin/main` 已同步。
@@ -0,0 +1,26 @@
# GitNexus 开发前基线
- 功能编号:FEAT-DS-001
- 记录时间:2026年8月24日 GMT+8 20:38:07
- 分支:main
- 基线提交:0d0d1eea4530
## 工作区
```text
?? "SRMS3.6.0\345\270\270\350\247\201\351\227\256\351\242\230\346\265\213\350\257\204\351\242\230.2csv"
```
## GitNexus 状态
```text
Repository: /Users/citrons/Documents/Codex/2026-08-12/wo-x/a-share-analysis
Indexed: 8/24/2026, 8:38:06 PM
Indexed commit: 0d0d1ee
Current commit: 0d0d1ee
Status: ✅ up-to-date
```
## 后续人工分析
在修改代码前,将 GitNexus query、context、impact 的结论写入 `gitnexus/impact-plan.md`。高风险结果必须先获得用户确认。
@@ -0,0 +1,3 @@
# GitNexus 前后对比
开发前基线已记录在 `before.md`。开发后 GitNexus 重新索引确认新增 AKTools Adapter、短线 Service、Controller、Web API、配置、页面和文档均落在计划范围;现有 TDX 市场概览执行流未被改写。`useMarketOverview` 的直接消费者为页面和测试,影响风险 LOW;未发现数据库、Redis、RocketMQ、Scheduler 或 Worker 新执行流。本轮健康检查修复前后对比增加连接级会话设置和 finally 释放,暂存区工具风险 MEDIUM 仅来自连接生命周期执行流,未形成新的跨模块业务依赖。
@@ -0,0 +1,11 @@
# GitNexus 变更检测
- 功能编号:FEAT-DS-001
- 开发前:已完成 `MarketOverviewService` 和 `useMarketOverview` 影响分析,均为 LOW。
- 提交前:`gitnexus_detect_changes({ scope: "staged" })` 识别新增 AKTools、短线接口、前端状态、配置和文档;风险被工具标记为 CRITICAL,复核确认原因为新增 API 启动注册和配置加载执行流,不是跨到数据库、消息或生产模块。
- 首次提交后:`gitnexus_detect_changes({ scope: "all" })` 返回无变更,索引与提交 `6e8a051` 一致;格式化提交 `1398804` 前的 staged 检测仅触及同一短线执行流,已由全量测试复验。
- 当前:修复提交 `9ff6646` 已推送;重新索引后 GitNexus 与提交一致,当前全量检测仅识别自动更新的 `AGENTS.md` 统计行(LOW、无业务执行流)。
- 本轮健康检查修复暂存区检测:6 个文件、28 个符号、2 条执行流,工具风险为 MEDIUM;实际代码影响集中在 `createMysqlHealthClient`、`MysqlHealthClient` 及 `OnModuleDestroy → End` 连接生命周期,文档章节被工具按标题识别为符号。直接 upstream impact 均为 LOW,未发现业务模块、Web、消息、数据库 Schema 或数据源执行流影响。
复核结论:TDX 核心链路保持独立;没有新增数据库、Redis、RocketMQ、Scheduler 或 Worker 业务执行流。工具报告中的 MEDIUM 结果已记录并完成逐项 `context/impact` 复核,原因是连接关闭生命周期被纳入执行流,而非跨模块业务耦合。
@@ -0,0 +1,27 @@
# GitNexus 开发前影响计划
- 功能编号:FEAT-DS-001
- 总体风险:LOW
## 直接与间接影响
GitNexus query 以“市场概览 超短行情总结 指数 板块 宽度 资金趋势”为目标执行,未发现现有超短执行流;结果主要定位到市场概览文档、测试和 `MarketOverviewService`。
`MarketOverviewService` context:直接消费者为市场概览 Controller、Module、OpenAPI 导出和测试;现有方法为指数、摘要、宽度、板块和成交额趋势,不包含 AKTools 数据。
`MarketOverviewService` upstream impact:影响 5 个关联对象,直接关联 3 个,风险 LOW,受影响执行流 0,受影响模块 0。结论是新增 AKTools 适配器和独立短线接口,不改写现有 TDX 方法,避免扩大现有概览方法的调用面。
## 跨模块同步修改
| 范围 | 判断 | 处置 |
|---|---|---|
| HTTP/OpenAPI | 需要同步 | 新增短线情绪和短线结论 DTO、Controller 路由并重新生成契约。 |
| Web API | 需要同步 | 新增 `web/src/api/market-overview/index.ts` 的业务请求和短线页面状态。 |
| 消息契约 | 不修改 | V1 为按需读取,不发布行情事件。 |
| 数据库 | 不修改 | 不持久化 AKTools 响应,不读取生产库。 |
| Redis | 不修改 | 暂不缓存,避免在数据口径未稳定时引入一致性问题。 |
| API 进程 | 需要同步 | Nest API 增加固定内网 AKTools 客户端和超时降级。 |
| Scheduler/Worker | 不修改 | V1 无采集任务、重试队列或历史快照任务。 |
| 配置部署 | 需要同步 | 增加可选 `AKTOOLS_ENABLED`、`AKTOOLS_BASE_URL`、`AKTOOLS_TIMEOUT_MS` 模板和文档。 |
| 测试 | 需要同步 | 增加客户端、映射、Service、OpenAPI、Web 状态和 E2E 覆盖。 |
| 文档 | 需要同步 | 更新架构、开发环境、功能档案和 AKTools 内网运行说明。 |
@@ -0,0 +1,22 @@
# AKTools 超短行情补充数据源实施计划
## 任务清单
1. [x] 完成功能档案、配置契约和 GitNexus 影响记录。
2. [x] 为 AKTools 客户端、字段类型和映射器编写失败测试。
3. [x] 实现固定接口、超时、响应校验和中文字段映射。
4. [x] 实现短线情绪 Service、规则结论、DTO 和 Controller。
5. [x] 更新 OpenAPI 生成契约、前端 API、页面模块和异常状态。
6. [x] 补充开发/生产模板及内网 AKTools 运维说明。
7. [ ] 执行完整 `pnpm verify`、治理完成门禁和远程推送(收尾阶段)。
## 文件边界
- 新增 `server/apps/api/src/market-overview/aktools/` 客户端、类型和映射。
- 新增短线业务 DTO、Service 和 Controller;不改动既有 TDX Mapper。
- 修改 `web/src/api/market-overview/index.ts`,页面只消费公开 DTO。
- 修改环境模板、架构文档、开发文档和 OpenAPI 生成产物。
## 实施顺序
先测试字段校验和映射,再实现客户端;先实现后端契约,再实现前端状态;每个阶段执行对应定向测试,最后执行完整门禁。
@@ -0,0 +1,8 @@
featureId: FEAT-DS-001
slug: aktools-supplement
name: AKTools 超短行情补充数据源
status: in_review
release: unreleased
source: aktools
scope: internal_lan_only
createdAt: 2026-08-24
@@ -0,0 +1,69 @@
# AKTools 超短行情补充数据源 PRD
- 功能编号:FEAT-DS-001
- PRD 版本:0.1
- 状态:design_approved
- 日期:2026-08-24
## 背景与问题
TDX 已提供指数、市场宽度、板块和成交额,但不能覆盖涨停池、炸板池、昨日涨停表现等超短情绪指标。AKTools 已在本机内网环境运行,并验证了相关 HTTP 接口。
## 目标用户与场景
个人用户在盘中或收盘后查看超短情绪,不进行交易下单。V1 仅支持本机或受控内网访问,AKTools 只作为 Nest API 的内部补充数据源。
## 产品目标
- 在市场概览中提供结构化超短情绪数据。
- 显示涨停数量、最高连板、连板梯队、炸板数量和昨日涨停表现。
- 所有结论都能追溯到 AKShare 数据字段、交易日期和数据状态。
- AKTools 不可用时不影响 TDX 核心市场概览。
## 非目标
- 不实现交易建议、买卖信号、目标价或仓位建议。
- 不直接暴露 AKTools 的通用接口给前端或公网。
- 不在 V1 计算未经确认口径的炸板率、资金净流入或新闻情绪评分。
- 不启用 MySQL、Redis、RocketMQ 或历史快照存储。
## 功能需求
| 编号 | 优先级 | 需求 | 可观察结果 |
|---|---|---|---|
| FR-001 | P0 | 读取涨停池 | 展示涨停数量、最高连板和连板梯队。 |
| FR-002 | P0 | 读取炸板池 | 展示炸板数量和可用的炸板次数摘要。 |
| FR-003 | P0 | 读取昨日涨停池 | 展示今日平均涨跌幅及上涨、下跌、持平数量。 |
| FR-004 | P0 | 生成规则结论 | 结论必须携带依据、交易日期和状态。 |
| FR-005 | P0 | 独立降级 | AKTools 超时或异常时,短线模块显示 unavailable,TDX 模块继续可用。 |
| FR-006 | P1 | 内网边界 | AKTools 地址只来自服务端固定配置,默认回环地址。 |
## 数据与口径
V1 调用 `stock_zt_pool_em`、`stock_zt_pool_zbgc_em`、`stock_zt_pool_previous_em`。AKTools 返回的中文字段只在 Adapter 内解析,领域 DTO 使用稳定英文属性。
- 涨停数量:涨停池有效记录数。
- 最高连板:有效 `连板数` 的最大值。
- 连板梯队:按 `连板数` 分组计数。
- 炸板数量:炸板池有效记录数;不直接推导炸板率。
- 昨日涨停表现:昨日涨停池有效 `涨跌幅` 的算术平均,以及正、负、零数量。
- 封板资金:仅作为原始统计摘要展示,单位转换为亿元并标明来源,不解释为主力净流入。
## 状态与异常
响应统一使用 `fresh`、`delayed`、`partial`、`unavailable`。交易日不等于上海当前日期时标记 `delayed`;字段缺失或部分接口失败时标记 `partial`;连接失败、超时、HTTP 5xx 或响应格式错误时标记 `unavailable`。
## 安全与合规
AKTools 仅绑定 `127.0.0.1:8080`。Nest 不接受客户端传入上游 URL,不记录原始载荷,不向前端透传上游异常。AKShare 及其上游数据的授权和再分发边界在正式对外部署前另行评估。
## 性能目标
三个 AKTools 请求并行执行,单请求超时目标 10 秒,短线接口正常响应目标 P95 小于 12 秒。V1 不引入缓存,避免陈旧数据和缓存击穿复杂度。
## 验收标准
- 有效交易日数据可生成短线情绪 DTO 和规则结论。
- 任一 AKTools 接口失败不会导致 TDX 市场概览接口失败。
- OpenAPI、前端 API 和页面状态与后端 DTO 一致。
- API 地址固定为本机内网配置,仓库不包含真实凭据和机器地址。
@@ -0,0 +1,13 @@
# 测试结果
## 当前状态
已完成代码验证:后端 Vitest 15 个测试文件、80 个测试通过,包含 MySQL 健康检查连接设置/释放回归、AKTools Client/Mapper、短线 Service、HTTP E2E 和 OpenAPI;前端 Vitest 8 个测试文件、23 个测试通过,包含短线面板、Composable 和 API;Web typecheck 通过。`pnpm verify` 全量门禁通过,Playwright 4 个测试通过。AKTools 本机涨停池、炸板池和昨日涨停池冒烟均返回 HTTP 200;`stock_zh_a_spot_em` 返回 HTTP 500,未纳入 V1。
## 安全验证
配置测试验证仅允许 `127.0.0.1:8080`,Client 测试覆盖禁用、超时、HTTP/协议错误、响应大小上限和脱敏异常;未向 Web 暴露 AKTools 地址或原始字段。
## 性能验证
Service 测试验证三个请求使用 `Promise.allSettled` 并行、单个失败返回 `partial`、全部失败返回 `unavailable`;MySQL 健康检查验证会话时区设置、元数据查询和异常路径释放连接。单请求超时配置为 10 秒;当前仅有本机测试证据,不将其视为生产容量结论。
@@ -0,0 +1,13 @@
# 视觉验证
## 当前状态
已在 `http://127.0.0.1:8888/` 完成桌面默认视口和 `390×844` 窄屏检查。短线面板在 DOM 中可见,TDX 不可用时仍展示明确的 AKShare 空状态;窄屏 `document.documentElement.scrollWidth` 与 `window.innerWidth` 均为 390,无横向溢出。组件测试覆盖有效数据和 unavailable 警告,部分/延迟文案沿用既有面板样式并通过类型检查。
## 安全验证
视觉验证不得展示凭据、真实内网地址或 AKTools 原始错误载荷。
## 性能验证
首次可见状态先展示 TDX 和 AKShare 各自占位,不互相阻塞;短线 API 失败只改变短线模块状态。视觉检查未展示凭据、真实内网地址或原始异常载荷。
@@ -0,0 +1,5 @@
# 发布说明
## 未发布
FEAT-DS-001 当前为未发布功能。已接入 AKTools 内网补充数据源,提供超短情绪指标和规则结论;不改变 TDX 核心市场概览。生产默认关闭,待授权、访问控制和稳定性评估后再决定启用。
@@ -0,0 +1,24 @@
# AKTools 超短行情补充数据源需求
## 原始需求
使用 AKTools 提供的 AKShare HTTP 接口,补充 TDX 没有的超短行情数据;当前只考虑内网环境。
## 实际范围
- Nest 服务端调用本机 AKTools,不让 Vue 直连 AKTools。
- 首批接入涨停池、炸板池、昨日涨停池。
- 新增短线情绪和短线结论业务接口。
- 保留 TDX 核心市场概览链路不变。
## 数据口径
所有数量只统计通过字段校验的记录;缺失数值不折算为零。结论只能使用本功能列出的确定性规则,不能把 AKShare 原始字段解释为交易建议。
## 依赖边界
AKTools 作为项目外部服务运行,默认地址为 `http://127.0.0.1:8080`。V1 不新增 Python 代码、不新增 Docker、不启用数据库、缓存和消息队列。
## 范围变化
已确认不接入 `stock_zh_a_spot_em` 作为核心接口,因为当前本地验证返回 HTTP 500;该接口不影响本功能其余目标接口。
@@ -0,0 +1,37 @@
# 代码 Review
## 审查范围
设计阶段审查目标为 AKTools Client、Mapper、短线 Service、DTO、Controller、Web API、配置和测试。
## PRD 与设计符合性
实现后检查只接入三个已验证接口,保持 TDX 独立,且不把原始 AKTools 字段直接暴露给前端。
## 发现
已完成实现 Review:未发现阻断项。Client 只允许三个固定 AKTools 路径,Service 用 `Promise.allSettled` 做模块级降级,DTO 不透传中文原始字段;Web 只通过 Axios API 文件访问 Nest。发现页面接线遗漏 `shortTermConclusion` 未解构,已在测试失败后修复。后续健康检查复核发现 `mysql2` 的 `timezone` 选项不会改变服务端会话变量,已在同一连接上显式设置 `+08:00` 并用 `finally` 释放;成功顺序和查询失败释放均有回归测试。
## 测试缺口
上游 5xx、超时、部分接口失败、字段非法、响应大小、不可用状态、MySQL 会话时区/释放和窄屏布局已有定向覆盖;交易日期延迟口径和 AKShare 字段漂移仍需后续真实交易日联调。
## 安全与残余风险
固定地址白名单、日期参数校验、固定接口、超时、响应体上限、JSON 结构校验和错误脱敏均已检查;未发现凭据或原始载荷进入前端。残余风险为 AKShare 授权、上游限流/字段变更和生产多机访问控制,故生产默认关闭。
## 性能与资源影响
三个请求并行,10 秒超时,2 MiB 响应上限;MySQL 健康检查复用单个连接完成会话设置和元数据查询,并在成功或异常路径释放连接;不引入缓存和消息链路,测试证据仅证明行为和边界,不构成生产容量证明。
## GitNexus 与同步修改
实现后核对变更检测结果只覆盖计划中的短线链路和文档。
## 修复与复审
发现问题后记录修复提交和复审证据;本轮健康检查修复通过 80 项后端测试、全量门禁和真实 `/api/v1/health` HTTP 200 验证。
## 最终结论
最终结论:passed
@@ -0,0 +1,21 @@
# 开发规范符合性 Review
## 检查结果
设计阶段已检查功能档案、Schema/GitNexus 前置流程、TDX 与 AKTools 边界及源码部署约束;代码实现后进行逐项复核。
## 不符合项与处置
逐项检查通过:功能档案、中文注释、OpenAPI 生成契约、Axios API 边界、固定数据源 Adapter、模块级降级、环境模板和未提交凭据约束均符合规范。实现期间发现的页面接线遗漏已修复并由前端全量测试复验;MySQL 健康检查的服务端时区误判已通过显式会话设置、连接释放 finally 和回归测试修复。
## 安全、性能与架构检查
确认 AKTools 仅作为外部内网 Adapter,未新增 Docker、Python 服务、生产数据库连接、Redis/RocketMQ 消息链路或前端直连;超时、响应上限、并行请求和降级均有实现与测试证据。生产启用的授权、访问控制和容量评估仍是明确前置条件。
## GitNexus 与开发总结检查
开发前后 GitNexus 报告、影响计划、变更检测、开发总结、测试结果、视觉 QA、Review 和回退说明均已补齐;本轮修复的 GitNexus 影响分析为 LOW,仅影响健康客户端和 API 工厂直接调用;`governance:finish` 及最终提交前再次执行变更检测。
## 最终结论
最终结论:passed
@@ -0,0 +1,16 @@
# 回退说明
## 触发条件
出现 TDX 核心行情回归、AKTools 请求阻塞 API、上游原始字段泄漏、OpenAPI 契约不一致或短线模块遮挡核心概览时回退。
## 回退步骤
1. 关闭 `AKTOOLS_ENABLED` 或回退到上一个完整发布目录。
2. 重启 API 并确认 TDX 概览接口、Web 健康检查和构建产物正常。
3. 保留 AKTools 外部服务,不删除其运行环境和数据缓存。
4. 记录回退原因、提交 SHA、验证结果和后续修复计划。
## 数据与外部服务
本功能不写 MySQL、Redis 或 RocketMQ,不需要数据回退;AKTools 是独立外部服务,应用回退不负责停止或删除它。
@@ -0,0 +1,30 @@
# AKTools 超短行情补充数据源测试计划
## 功能验证
- 用固定响应夹具验证涨停数量、最高连板和梯队分布。
- 验证炸板池和昨日涨停池字段缺失、非法数字、空数组的处理。
- 验证三个请求并行且单个失败返回 partial。
- 验证全部失败返回 unavailable,TDX 既有接口仍能独立响应。
- 验证 OpenAPI DTO、Web API 类型和页面状态一致。
## 安全验证
- 验证客户端拒绝请求参数注入的上游 URL。
- 验证配置不允许生产地址和公网任意地址。
- 验证超时、过大响应、非 JSON 响应和上游错误均脱敏。
- 验证前端构建产物不包含 AKTools 凭据或内部地址。
## 性能验证
- 验证三个请求使用并行调度而非串行等待。
- 使用受控延迟夹具验证单请求超时和整体响应上限。
- 记录定向请求耗时,不把本地单次结果当作生产容量证明。
## 视觉验证
验证桌面和窄屏下短线情绪模块的加载、部分、不可用、空数据状态,确保警告和交易日期不遮挡指标。
## 残余风险
AKShare 上游限流、字段变更、数据授权和长期稳定性需要在后续联调与生产评估中持续验证。
@@ -31,6 +31,7 @@
- 根据二次 Review 继续修复涨跌家数接受非整数、成交额受市场定义顺序影响、指数/板块跨交易日混排和后端从不生成 `delayed` 的边界;宽度增加核心指数日期参考,不新增基础设施或第三方依赖。 - 根据二次 Review 继续修复涨跌家数接受非整数、成交额受市场定义顺序影响、指数/板块跨交易日混排和后端从不生成 `delayed` 的边界;宽度增加核心指数日期参考,不新增基础设施或第三方依赖。
- 根据第三轮独立 Review 修复 8 位畸形日期被误认为有效公历日期、成交额接受倒序日期对与日期/金额数组错位,以及服务测试依赖运行当天的问题。 - 根据第三轮独立 Review 修复 8 位畸形日期被误认为有效公历日期、成交额接受倒序日期对与日期/金额数组错位,以及服务测试依赖运行当天的问题。
- 根据第四轮独立 Review 修复指数/板块未校验前一根 K 线日期的问题;非法、重复或倒序的前一日期不再生成涨跌信号。 - 根据第四轮独立 Review 修复指数/板块未校验前一根 K 线日期的问题;非法、重复或倒序的前一日期不再生成涨跌信号。
- 修复首屏更新时间直接展示 UTC ISO 字符串的问题:保留 API 的标准 UTC `asOf` 契约,Web 统一按 `Asia/Shanghai` 展示完整日期时间和时分秒。
## 未完成与延期项 ## 未完成与延期项
@@ -40,7 +41,7 @@
## 关键提交 ## 关键提交
- `9a965fa`、`d283ee3`、`6eb34b5`、`c2fa929`、`15a2500`、`27bc3c1`、`8261948`、`ff8ada8`、`49f6be9`、`7e4dcd9`、`ae2a7e0`、`c2fdfa6` - `9a965fa`、`d283ee3`、`6eb34b5`、`c2fa929`、`15a2500`、`27bc3c1`、`8261948`、`ff8ada8`、`49f6be9`、`7e4dcd9`、`ae2a7e0`、`c2fdfa6`、`b1310fb fix(FEAT-MO-001): 修正概览时间时区显示`
## 验证证据 ## 验证证据
@@ -53,3 +54,4 @@
- 浏览器桌面与 390px 移动视口验证无横向溢出,视角与引擎切换可用。 - 浏览器桌面与 390px 移动视口验证无横向溢出,视角与引擎切换可用。
- 五轮独立 Review 均已归档:前四轮发现的数据完整性、状态、日期真实性/顺序、测试时钟和前一根日期校验问题均已关闭;第五轮最终结论为 `Ready to merge: Yes`,无 Critical、Important 或 Minor 问题。 - 五轮独立 Review 均已归档:前四轮发现的数据完整性、状态、日期真实性/顺序、测试时钟和前一根日期校验问题均已关闭;第五轮最终结论为 `Ready to merge: Yes`,无 Critical、Important 或 Minor 问题。
- 开发 Schema 按用户确认跳过,沿用 `v0.0.10 captured_empty` 追溯边界,不读取生产库。 - 开发 Schema 按用户确认跳过,沿用 `v0.0.10 captured_empty` 追溯边界,不读取生产库。
- 本轮前端日期工具测试覆盖 UTC 到上海时区转换、无效值占位和两个概览组件接线;未修改后端、OpenAPI、数据库或消息契约。
@@ -17,3 +17,9 @@ npx gitnexus detect-changes --repo "$PWD" --scope staged
## 处置 ## 处置
本轮 medium 来自指数/板块相邻日期比较的局部映射变更,不是未知跨业务扩散。已用 TDX 映射/服务测试、类型检查和 `git diff --check` 验证;前一提交已覆盖 HTTP/OpenAPI、前端状态、构建、浏览器视觉和 4/4 E2E,未发现用户、消息、数据库、Redis、RocketMQ、Scheduler、Worker、认证或部署调用方。当前环境已使用 GitNexus MCP 与同版本 CLI 交叉确认,没有将工具限制误判为低风险。 本轮 medium 来自指数/板块相邻日期比较的局部映射变更,不是未知跨业务扩散。已用 TDX 映射/服务测试、类型检查和 `git diff --check` 验证;前一提交已覆盖 HTTP/OpenAPI、前端状态、构建、浏览器视觉和 4/4 E2E,未发现用户、消息、数据库、Redis、RocketMQ、Scheduler、Worker、认证或部署调用方。当前环境已使用 GitNexus MCP 与同版本 CLI 交叉确认,没有将工具限制误判为低风险。
## 第六轮显示修复
- GitNexus 对 `useMarketOverview` upstream impact 为 LOW,直接消费者为 `MarketOverviewPage.vue` 和状态测试;Vue 单文件组件文件级符号由查询结果确认,未发现后端或跨模块调用方。
- 本轮仅变更 Web 日期格式化工具及两个概览组件显示,不修改 API、OpenAPI、TDX、数据库、Redis、RocketMQ、Scheduler 或 Worker。
- 修复提交 `b1310fb` 后重新索引统计为 3,457 nodes / 4,935 edges / 73 clusters / 104 flows,状态 up-to-date;全量检测仅识别 GitNexus 自动更新的 `AGENTS.md` 统计行,风险 LOW、0 条受影响执行流。
@@ -2,7 +2,7 @@
## 自动化结果 ## 自动化结果
- 前端 Vitest:8 个文件、20 个测试通过。 - 前端 Vitest:9 个文件、28 个测试通过,新增 UTC 到上海时区格式化和概览组件显示回归;修复提交 `b1310fb` 已推送到 Gitea。
- 后端 Vitest:11 个文件、67 个测试通过;API 启动器 Node 测试 2/2 通过,Schema、治理、边界和部署门禁通过。 - 后端 Vitest:11 个文件、67 个测试通过;API 启动器 Node 测试 2/2 通过,Schema、治理、边界和部署门禁通过。
- `pnpm verify:e2e`:桌面/移动端页面和健康契约共 4 个测试通过。 - `pnpm verify:e2e`:桌面/移动端页面和健康契约共 4 个测试通过。
- `pnpm typecheck`、`pnpm build`、`pnpm format:check`:通过。 - `pnpm typecheck`、`pnpm build`、`pnpm format:check`:通过。
@@ -17,3 +17,4 @@
- 第三轮 Review 修复后,非法月份/日期、倒序日期对和日期/金额数组错位均被映射层排除;服务测试统一冻结时钟,不依赖执行当天。 - 第三轮 Review 修复后,非法月份/日期、倒序日期对和日期/金额数组错位均被映射层排除;服务测试统一冻结时钟,不依赖执行当天。
- 第四轮 Review 修复后,指数前一根非法、重复或倒序日期不再生成涨跌幅,板块对应记录不进入强弱排序。 - 第四轮 Review 修复后,指数前一根非法、重复或倒序日期不再生成涨跌幅,板块对应记录不进入强弱排序。
- 桌面默认视口与 390px 移动视口无横向溢出;视角和引擎切换可用。 - 桌面默认视口与 390px 移动视口无横向溢出;视角和引擎切换可用。
- 浏览器实测确认结论区显示 `2026-08-24 22:59:55`、指数卡片显示 `22:59:27`,不再显示 UTC 的 `14:59` 时间;桌面和移动视口均无横向溢出。
@@ -84,3 +84,14 @@
- 验证:指数非法、重复、倒序前一日期参数化用例和板块过滤用例齐全;后端 Vitest `67/67`、启动器 `2/2`、服务端类型检查与 `git diff --check` 均通过。安全、性能及 PRD/设计/测试/QA 口径一致。 - 验证:指数非法、重复、倒序前一日期参数化用例和板块过滤用例齐全;后端 Vitest `67/67`、启动器 `2/2`、服务端类型检查与 `git diff --check` 均通过。安全、性能及 PRD/设计/测试/QA 口径一致。
最终结论:passed 最终结论:passed
## 第六轮显示修复 Review
- 审查范围:市场概览前端日期显示工具、结论组件、指数组件及回归测试。
- 根因:API `asOf` 是带 `Z` 的标准 UTC 时间,前端原样展示并直接截取 UTC 时分秒。
- 处置:新增 `Asia/Shanghai` 格式化工具;不修改后端时间契约,非法值统一显示占位符。
- 安全与性能:仅使用原生 `Intl.DateTimeFormat`,无新依赖、无网络请求、无敏感数据处理;格式化开销只发生在两个展示节点。
- 验证:前端 Vitest 9 个文件/28 个测试、typecheck、全量 `pnpm verify` 和桌面/移动浏览器实测通过。
- 结论:passed
最终结论:passed
@@ -25,5 +25,6 @@
- 第三轮 Review 的日期真实性、先后顺序、数组对齐和测试时钟问题已按 TDD 修复;需求、设计、测试计划、测试结果、发布说明和开发总结已同步。 - 第三轮 Review 的日期真实性、先后顺序、数组对齐和测试时钟问题已按 TDD 修复;需求、设计、测试计划、测试结果、发布说明和开发总结已同步。
- 第四轮 Review 的前一根日期校验问题已按 TDD 修复;治理结论继续保持 `pending`,等待本次代码修复再次独立确认。 - 第四轮 Review 的前一根日期校验问题已按 TDD 修复;治理结论继续保持 `pending`,等待本次代码修复再次独立确认。
- 第五轮最终独立 Review 无 Critical、Important 或 Minor 问题并给出 `Ready to merge: Yes`;中文提交、Gitea 推送、GitNexus、完整门禁和视觉证据均已复核。 - 第五轮最终独立 Review 无 Critical、Important 或 Minor 问题并给出 `Ready to merge: Yes`;中文提交、Gitea 推送、GitNexus、完整门禁和视觉证据均已复核。
- 第六轮显示修复保持后端 UTC 契约、前端 Axios 边界和业务模块职责不变;新增工具有真实重复场景和独立测试依据,无新增依赖,安全、性能、类型、构建和 E2E 证据齐全。
最终结论:passed 最终结论:passed
@@ -0,0 +1,21 @@
# 设计
## 方案
在 `web/vite.config.ts` 的 `server.port` 设置 8888;同步 `web/playwright.config.ts` 的 `baseURL` 与 `webServer.url`,并更新 `docs/deployment/local-development.md`。保持 `host: 127.0.0.1`、`strictPort: true` 和 `/api` 代理不变。
## 安全策略与评估
- 监听地址仍为回环地址,不增加局域网或公网暴露。
- 不停止 8080 的外部项目进程。
- 不读取或写入凭据,不改变 API、TDX 或外部基础设施配置。
## 性能目标与评估
- 端口调整不改变前端构建产物和运行时逻辑。
- Vite 启动验证目标为 30 秒内可访问;本次实际启动耗时小于 1 秒。
- E2E 启动入口与真实监听地址保持一致,避免重复启动或等待错误端口。
## 错误处理与回退
`strictPort: true` 保持不变;8888 被占用时启动明确失败,不自动抢占或切换端口。回退时将四处当前配置恢复为 5173,并同步恢复文档。
@@ -0,0 +1,45 @@
# Web 开发端口调整开发总结
- 功能编号:FEAT-MO-002
- 总结版本:0.1
- 日期:2026-08-19
## 原始开发需求
将前端端口改为 8080,检查占用并启动;经检查 8080 被其他项目占用后,用户确认改用 8888。
## 需求与范围变化
初始目标从 8080 调整为 8888,原因是 8080 已被另一个本地项目占用;该进程不属于本项目,本次没有停止或修改它。
## 实际完成需求
- `web/vite.config.ts` 开发端口改为 8888。
- `web/playwright.config.ts` 两处 Web 地址改为 8888。
- 本地开发手册 Web 地址改为 8888。
- 新增端口回归测试。
- Web 已在 `http://127.0.0.1:8888/` 启动并返回 200。
- Tailscale 外部隧道已启动,SSH 控制隧道监听 `127.0.0.1:17710`,TDX 只读行情请求成功。
- 为保证外部隧道切换后的 API 可重复启动,在被 Git 忽略的 `deploy/env/development/api.env.local` 补充了 `TDX_BASE_URL` 和 `TDX_TIMEOUT_MS`,未写入仓库。
## 修复缺陷
本次没有修复既有业务缺陷;变化属于本地开发配置和对应验证入口的一致性调整。新增回归断言防止端口意外恢复为 5173。
## 未完成与延期项
生产部署不在本次范围。完整 E2E 在收尾复验时发现市场概览夹具时钟未隔离,已由 `FEAT-QA-001` 修复;API 健康检查因开发 MySQL 不可用返回 503,但 Redis 正常,市场概览接口和 TDX 只读链路返回 200,MySQL 状态不属于本次端口变更。
## 关键提交
- `f00e114f88464f07bcbba3ca6691f04ca9699d38`:`chore(FEAT-MO-002): 调整 Web 开发端口为 8888`。
- 该实施提交已推送至项目 Gitea `origin/main`,远端分支 SHA 与本地一致。
## 验证证据
- Schema:`npm run schema:baseline` 和 `npm run schema:verify` 均通过,版本 `v0.0.10 (captured_empty)`。
- GitNexus:开发前索引状态最新;开发后检测为 LOW、受影响执行流 0。
- 测试:Web Vitest 8 个文件、21 个测试通过;修改前新增端口测试按预期失败。
- 启动:Vite 监听 8888,首页 HTTP 200;API 使用本地配置启动并监听 3000。
- TDX:Tailscale ping 经 DERP 成功,SSH 隧道监听 17710,`get_market_data` 返回 `ErrorId=0`。
- 安全:保持 127.0.0.1,不停止其他项目,不读取凭据。
@@ -0,0 +1,13 @@
# GitNexus 开发后基线
## GitNexus 状态
完成 Web 配置、测试和文档修改后刷新索引:3,122 nodes、4,343 edges、68 clusters、85 flows,状态 `up-to-date`。本功能影响评估为 LOW,受影响执行流为 0。
## Git 已跟踪变更摘要
Vite 开发端口、Playwright Web 地址和本地开发手册由 5173 统一调整为 8888;AGENTS.md 的既有修改保留,不属于本功能。
## Git 工作区摘要
工作区包含本功能档案和端口相关修改;无关未跟踪文件明确排除。提交与推送将在最终独立 Review 通过后执行。
@@ -0,0 +1,26 @@
# GitNexus 开发前基线
- 功能编号:FEAT-MO-002
- 记录时间:2026年8月19日 GMT+8 12:23:44
- 分支:main
- 基线提交:1f6ed00e6fbb
## 工作区
```text
M AGENTS.md
```
## GitNexus 状态
```text
Repository: <project-root>
Indexed: 8/18/2026, 9:31:53 PM
Indexed commit: 1f6ed00
Current commit: 1f6ed00
Status: ✅ up-to-date
```
## 后续人工分析
在修改代码前,将 GitNexus query、context、impact 的结论写入 `gitnexus/impact-plan.md`。高风险结果必须先获得用户确认。
@@ -0,0 +1,13 @@
# GitNexus 前后对比
## 开发前
Vite 监听 5173,Playwright 和本地手册使用 5173;GitNexus 索引为项目当前基线。
## 开发后
Vite、Playwright 和本地开发手册统一使用 8888;业务模块执行流、API 契约和数据基础设施不变。GitNexus 变更检测为 LOW,受影响执行流 0。
## 差异处置
8080 的外部 Python 进程未被停止;Tailscale 外部 TDX 隧道独立切换到 `tdx-windows-tailnet`,不改变平台固定的 127.0.0.1:17710 入口。
@@ -0,0 +1,9 @@
# GitNexus 变更检测
执行:`gitnexus_detect_changes({scope: "unstaged", repo: "a-share-analysis"})`。
结果:`changed_count=3`、`changed_files=5`、`affected_count=0`、`risk_level=low`、受影响执行流 0。GitNexus 识别到 AGENTS.md(既有工作区修改)和本地开发文档章节;Vite/Playwright 的端口常量变化未映射为业务符号,已通过配置测试、类型检查、构建和 HTTP 启动验证覆盖。
本功能预期变更仅为:Vite 端口、Vite 配置测试、Playwright 地址、本地开发文档和功能档案。AGENTS.md 是用户已有修改,本次不触碰其内容,也不将其视为功能变更。
同步判断:不涉及 API/OpenAPI、消息契约、数据库、Redis、Scheduler、Worker 或生产部署;Web 开发启动流程已同步。
@@ -0,0 +1,35 @@
# GitNexus 开发前影响计划
- 功能编号:FEAT-MO-002
- 目标:将 Vue/Vite 本地开发服务器端口从 5173 调整为 8888。
- 总体风险:LOW
## 直接与间接影响
- `gitnexus_query("Vite development server port web startup")` 定位到 `web/vite.config.ts` 和 `web/src/vite-config.test.ts`。
- `gitnexus_context({name: "vite.config.ts", file_path: "web/vite.config.ts"})` 显示该配置文件被 `web/src/vite-config.test.ts` 导入,配置内部调用 `readDevelopmentPublicEnv`。
- `gitnexus_impact({target: "vite.config.ts", direction: "upstream"})`:直接调用方 0、受影响执行流 0、受影响模块 0,风险 LOW。
- `gitnexus_impact({target: "playwright.config.ts", direction: "upstream"})`:直接调用方 0、受影响执行流 0、受影响模块 0,风险 LOW;该配置自身启动并访问 Web 开发服务器,需要同步更新端口。
- 对 `readDevelopmentPublicEnv` 的 upstream 影响为 1 个配置文件调用方,风险 LOW;端口字段是 Vite 配置常量,不改变 API、数据契约或业务逻辑。
- 8080 已被另一个本地项目占用;该进程不属于本项目,本次不停止、不修改,也不记录其本机目录或临时进程标识。
## 跨模块同步修改
| 维度 | 判断 | 同步修改 |
|---|---|---|
| HTTP/OpenAPI | 不涉及页面 API 契约 | 无 |
| Web API | 不涉及 Axios 请求路径 | 无 |
| 消息契约 | 不涉及 RocketMQ | 无 |
| 数据库 | 不涉及 MySQL Schema | 无 |
| Redis | 不涉及缓存 | 无 |
| API | Nest API 端口保持 3000 | 无 |
| Scheduler/Worker | 不涉及任务进程 | 无 |
| 部署 | 仅开发 Vite 端口;生产 Web 配置不变 | 更新本功能说明 |
| 测试 | Vite 配置回归测试增加 8888 断言;Playwright baseURL/webServer URL 同步 | 修改 `web/src/vite-config.test.ts`、`web/playwright.config.ts` |
| 文档 | 本地开发 Web 地址同步为 8888 | 修改 `docs/deployment/local-development.md` 和本功能档案 |
## 安全、性能与残余风险
- 安全:不改变监听地址(仍为 `127.0.0.1`),不暴露新公网入口,不接触凭据。
- 性能:仅改变本地开发服务器监听端口,不改变构建产物、代理或运行时性能。
- 残余风险:如果其他本地进程占用 8888,因 `strictPort: true` 启动会失败;启动前必须检测并报告。
@@ -0,0 +1,9 @@
# 实施计划
1. 检查 8080/8888 占用情况和工作区状态。
2. 执行 Schema 基线与校验,运行治理起始门禁。
3. 使用 GitNexus 评估 Vite、Playwright 配置和测试影响。
4. 先添加 8888 回归断言并确认旧配置下失败,再修改 Vite 配置。
5. 同步 Playwright 和本地开发文档。
6. 启动 Web,验证 8888 HTTP 200;切换 Tailscale 外部 TDX 隧道并验证只读行情。
7. 执行测试、构建、GitNexus 变更检测和 Review。
@@ -0,0 +1,12 @@
id: FEAT-MO-002
name: Web 开发端口调整为 8888
slug: web-port-8888
status: ready_for_release
owner: project
created_at: 2026-08-19
requirements: requirements.md
prd: prd.md
design: design.md
implementation_plan: implementation-plan.md
release: unreleased
traceability_note: 端口配置、安全断言、完整门禁、GitNexus 与独立 Review 已通过,等待正式版本发布
@@ -0,0 +1,47 @@
# Web 开发端口调整为 8888 PRD
- 功能编号:FEAT-MO-002
- PRD版本:0.1
- 状态:approved
- 日期:2026-08-19
## 背景与问题
本机 8080 已被其他项目占用,现有 A 股分析工具前端开发端口 5173 需要调整为用户指定的 8888。
## 产品目标
- Web 开发服务器监听 `127.0.0.1:8888`。
- Playwright、本地开发文档和端口回归测试使用同一地址。
- API 仍监听 `127.0.0.1:3000`,TDX 隧道仍使用 `127.0.0.1:17710`。
## 非目标
- 不改变生产 Web 端口、Nginx、API、TDX、Redis、MySQL 或 RocketMQ 配置。
- 不停止其他项目进程,不开放公网监听。
## 功能需求
| 编号 | 优先级 | 需求 | 可观察结果 |
|---|---|---|---|
| FR-001 | P0 | 调整 Vite 开发端口 | `vite.config.ts` 的 `server.port` 为 8888 |
| FR-002 | P0 | 同步 E2E 和本地文档 | Playwright 和开发手册访问 8888 |
| FR-003 | P0 | 保持本机安全边界 | host 仍为 127.0.0.1,strictPort 仍启用 |
## 安全需求与信任边界
本次只改变本机开发监听端口,不改变监听地址、代理目标、认证、数据源或凭据。8080 占用进程不属于本项目,不停止或操作该进程。
## 性能目标与容量假设
端口变化不应改变构建、代理和页面运行时性能;Vite 应在 30 秒内启动并返回首页。
## 验收标准
- `web/src/vite-config.test.ts` 断言端口为 8888。
- `http://127.0.0.1:8888/` 返回 HTTP 200。
- API 代理仍转发到 `http://127.0.0.1:3000`。
## 关联模块与同步修改
涉及 Web Vite 配置、Web 配置测试、Playwright 配置和本地开发文档;不涉及 HTTP/OpenAPI、数据库、Redis、消息、Scheduler、Worker 和生产部署。
@@ -0,0 +1,19 @@
# 测试结果
- RED:端口断言在旧配置下按预期失败,实际值为 5173,期望值为 8888。
- GREEN:Web Vitest 8 个测试文件、21 个测试全部通过。
- 安全回归:Vite 配置测试同时锁定 `host=127.0.0.1`、`port=8888` 和 `strictPort=true`。
- HTTP:`curl -I http://127.0.0.1:8888/` 返回 HTTP 200。
- API:`/api/v1/market-overview/summary` 返回 HTTP 200,`source=tdx`、`status=fresh`。
- TDX:外部 Tailscale SSH 隧道 `127.0.0.1:17710` 只读请求返回 `ErrorId=0`。
- 健康检查:API 返回 503,明确原因为开发 MySQL 不可用;Redis 为 healthy,RocketMQ 为 disabled。该结果不影响端口和 TDX 链路验收。
- 完整门禁:`pnpm verify` 通过,Web 21/21、Server 67/67、启动器 2/2、Playwright 4/4,类型检查与前后端构建通过。
- Lint:0 error;保留市场概览组件既有 35 条格式 warning,本功能没有新增 warning。
## 安全验证
监听地址保持回环;未停止或修改占用 8080 的外部进程;变更中未写入新凭据、本机绝对路径或临时进程标识。
## 性能验证
Vite 启动耗时约 194ms,首页可立即访问;完整构建和 E2E 通过,未增加运行时依赖和外部服务调用。
@@ -0,0 +1,11 @@
# 视觉验证
本次仅改变开发服务器端口,不改变页面布局、样式或组件。收尾的 Playwright 在桌面与移动端共执行 4 个用例并全部通过;页面视觉基线继续沿用 FEAT-MO-001 的既有截图,不制造重复截图产物。
## 安全验证
Web 仍只绑定 `127.0.0.1`,没有新增局域网或公网访问入口。
## 性能验证
Vite 监听 8888 后首页响应正常,端口变更不影响页面资源加载路径。
@@ -0,0 +1,3 @@
# 发布说明
本地开发 Web 端口从 5173 调整为 8888。API、TDX 隧道和生产配置不变。该功能尚未创建发布标签。
@@ -0,0 +1,14 @@
# 需求
用户要求将前端开发端口改为 `8888`,检查是否被占用并启动项目。
## 实际范围
- Vite 开发服务器从 5173 改为 8888。
- Playwright `baseURL` 和 Web Server 检查地址同步为 8888。
- 本地开发手册 Web 地址同步为 8888。
- API 继续使用 3000,TDX 外部隧道继续映射到 17710。
## 范围变化
8080 初次检查发现已被其他项目 Python `run.py` 进程占用,因此没有停止该进程,改用用户确认的 8888。
@@ -0,0 +1,46 @@
# 代码 Review
- 功能编号:FEAT-MO-002
- reviewer_type:self
- reviewer:Codex
- 日期:2026-08-19
## 审查范围
审查 `web/vite.config.ts`、`web/src/vite-config.test.ts`、`web/playwright.config.ts`、本地开发手册及对应功能档案。
## PRD 与设计符合性
符合:Vite、Playwright 和本地开发文档统一使用 8888,API 3000、TDX 隧道 17710 和回环监听保持不变。
## 发现
| 严重程度 | 位置 | 问题 | 影响 | 处理 |
|---|---|---|---|---|
| 无 | - | 未发现本次范围内问题 | - | - |
## 测试缺口
收尾时完整 E2E 首次运行发现市场概览测试夹具时钟未隔离;该跨日缺陷不属于端口行为,已由 `FEAT-QA-001` 使用固定测试时钟修复并纳入完整门禁。
## 安全与残余风险
保持 `127.0.0.1` 和 `strictPort`,不停止其他项目。残余风险是 8888 被其他进程占用时启动会明确失败。
## 性能与资源影响
仅改变监听端口;Vite 启动约 194ms,无新依赖和外部负载。
## GitNexus 与同步修改
GitNexus 对 Vite 和 Playwright 配置影响评估为 LOW;同步更新了配置测试、E2E 地址和本地开发文档。
## 修复与复审
端口测试已完成 RED/GREEN;独立 Review 要求补锁回环地址和 `strictPort`,对应断言已增加。档案中的本机绝对路径、临时 PID 和旧验证结论已清理;完整门禁通过,等待最终复审。
## 最终结论
最终独立复审确认回环监听、8888、`strictPort`、Playwright 和文档一致;无 Critical、Important 或 Minor 问题,`Ready to commit: Yes`。
最终结论:passed
@@ -0,0 +1,36 @@
# 开发规范符合性 Review
- 功能编号:FEAT-MO-002
- reviewer_type:self
- reviewer:Codex
- 日期:2026-08-19
## 检查结果
- [x] 必要中文注释准确且没有逐行废话注释。
- [x] 文件按稳定职责拆分,没有过度碎片化。
- [x] 端口、监听地址和时间语义明确。
- [x] 错误处理和 strictPort 行为保持不变。
- [x] 安全策略、性能目标、验证证据和残余风险完整。
- [x] 未引入依赖,未发生 API 或前端请求边界变化。
- [x] GitNexus 开发前影响分析和开发后检测已记录,风险 LOW、0 受影响执行流。
- [x] 开发总结记录了需求、范围变化、完成项、延期项和验证证据。
- [x] PRD、设计、测试、发布和回退口径一致。
## 不符合项与处置
独立 Review 发现安全配置断言不足、功能档案含本机绝对路径/临时 PID 且验证结论已过时。现已清理敏感环境细节并补充 `host`、`port`、`strictPort` 回归断言;完整门禁通过,等待最终复审。
## 安全、性能与架构检查
监听仍为 127.0.0.1;端口变化不改变资源性能;8080 外部进程未被操作。
## GitNexus 与开发总结检查
影响风险 LOW,配置、测试、E2E 和文档同步修改均已记录;开发总结列出 TDX 外部隧道与 MySQL 健康检查证据。
## 最终结论
最终独立复审确认完整门禁、GitNexus、安全清理和逐路径提交策略符合规范;无未关闭问题。
最终结论:passed
@@ -0,0 +1,19 @@
# 回退说明
## 触发条件
- 8888 与本机其他必要服务冲突。
- Playwright 或本地开发脚本无法访问前端。
- 发现端口改动影响非开发环境配置。
## 回退步骤
1. 将 `web/vite.config.ts` 的 `server.port` 恢复为 5173。
2. 将 `web/playwright.config.ts` 的 `baseURL` 和 `webServer.url` 恢复为 5173。
3. 将 `docs/deployment/local-development.md` 的 Web 地址恢复为 5173。
4. 同步修改端口回归测试和本功能档案,执行 Web 测试与启动验证。
5. 停止本功能启动的 Web 进程,不操作 8080 的其他项目。
## 数据与外部服务
本次不修改 MySQL、Redis、RocketMQ、TDX 客户端或 SSH 隧道配置,因此无需数据回退。
@@ -0,0 +1,22 @@
# 测试计划
## 功能验证
- Vite 配置单元测试:监听地址为 `127.0.0.1`、端口为 8888、`strictPort=true`,代理仍为本地 API。
- Web 启动验证:8888 返回首页 HTML。
- Playwright 配置检查:baseURL 和 webServer URL 均为 8888。
## 安全验证
- 检查监听地址仍为 `127.0.0.1`。
- 检查 8080 外部 Python 进程未被停止或修改。
- 检查无密钥、密码和公网地址进入变更文件。
## 性能验证
- 记录 Vite 启动耗时和首页 HTTP 响应。
- 端口配置不引入新依赖、不增加 API 或数据源负载。
## 验证边界
生产部署验证不属于本地开发端口范围;收尾时仍执行完整仓库门禁和 Playwright 桌面/移动端回归,确保端口同步没有破坏测试启动链路。
@@ -0,0 +1,17 @@
# 设计
## 设计方案
只修改根目录 README,事实来源固定为现有部署手册、系统架构、FEAT-MO-001 市场概览和 FEAT-MO-002 端口档案。README 作为入口索引,不复制完整的 Tailscale、生产部署或治理规则。
## 安全策略与评估
仅写入 TDX 本机回环入口、SSH 别名和占位符;不写真实 Windows 地址、Tailscale IP、SSH 用户、Token、密码或私钥。README 明确 TDX 不开放公网、Schema 禁止触碰生产库。
## 性能目标与评估
文档变化不影响编译、运行时和网络性能;只增加可发现性和启动指导。
## 兼容与回退
README 可以独立回退,不需要数据迁移或服务重启。后续端口、部署或数据源变更必须同步 README 和专项文档。
@@ -0,0 +1,40 @@
# README 文档同步开发总结
- 功能编号:FEAT-MO-003
- 总结版本:0.1
- 日期:2026-08-19
## 原始开发需求
用户指出 README 落后,要求更新为当前项目状态。
## 需求与范围变化
从旧的英文工程简介扩展为当前可运行状态说明,新增 8888 Web、TDX 双隧道、首屏模块、基础设施边界和启动验证;未改变代码和运行行为。
## 实际完成需求
- README 已同步当前市场概览首屏、技术栈和源码部署边界。
- README 已同步 Web 8888、API 3000 和 TDX 17710 回环入口。
- README 已补充 LAN/Tailscale 隧道、启动、停止和验证入口。
- README 已补充 Schema、GitNexus、Review 和 Gitea 约束链接。
## 修复缺陷
修复 README 将项目描述、端口、启动方式和基础设施启用边界写旧的问题;没有业务代码缺陷修复。
## 未完成与延期项
README 不替代完整 Tailscale、生产部署和治理文档;这些内容继续由专项文档维护。
## 关键提交
- `6cbfd8d8a4d0bfadef33c87a21d16ea900d9978e`:`docs(FEAT-MO-003): 更新项目 README`。
- 该实施提交已推送至项目 Gitea `origin/main`,远端分支 SHA 与本地一致。
## 验证证据
- Schema:npm run schema:baseline 和 npm run schema:verify 通过,v0.0.10 captured_empty。
- GitNexus:README upstream 风险 LOW,执行流 0;开发后检测已记录。
- 文档检查:旧的当前端口引用清理,README 链接和命令与现有文件一致。
- 安全:未写入凭据、真实 Tailscale 地址、Windows 用户或 Token。
@@ -0,0 +1,5 @@
# GitNexus 开发后基线
GitNexus 已刷新:3,122 nodes、4,343 edges、68 clusters、85 flows,状态 up-to-date。
README upstream 风险 LOW,受影响执行流 0;开发后变更检测 risk_level=low、affected_count=0。
@@ -0,0 +1,32 @@
# GitNexus 开发前基线
- 功能编号:FEAT-MO-003
- 记录时间:2026年8月19日 GMT+8 12:57:47
- 分支:main
- 基线提交:1f6ed00e6fbb
## 工作区
```text
M AGENTS.md
M README.md
M docs/deployment/local-development.md
M web/playwright.config.ts
M web/src/vite-config.test.ts
M web/vite.config.ts
?? docs/features/FEAT-MO-002-web-port-8888/
```
## GitNexus 状态
```text
Repository: <project-root>
Indexed: 8/19/2026, 12:57:47 PM
Indexed commit: 1f6ed00
Current commit: 1f6ed00
Status: ✅ up-to-date
```
## 后续人工分析
在修改代码前,将 GitNexus query、context、impact 的结论写入 `gitnexus/impact-plan.md`。高风险结果必须先获得用户确认。
@@ -0,0 +1,3 @@
# GitNexus 前后对比
开发前 README 使用旧的英文项目简介、5173 端口和不完整启动说明;开发后同步当前 8888、TDX 双隧道、首屏模块和治理入口。README 不参与业务执行流。
@@ -0,0 +1,5 @@
# GitNexus 变更检测
执行 `gitnexus_detect_changes({scope: "unstaged", repo: "a-share-analysis"})` 结果:changed_count=10、changed_files=6、affected_count=0、risk_level=low、受影响执行流 0。
GitNexus 识别到 README 的 6 个章节和既有 AGENTS.md/本地开发文档章节;本功能没有业务代码或运行时执行流变化。AGENTS.md 的既有用户修改保留,不属于本功能。
@@ -0,0 +1,27 @@
# GitNexus 开发前影响计划
- 功能编号:FEAT-MO-003
- 目标:同步根目录 README 与当前前端端口、TDX 双隧道、本地启动、技术栈和首屏能力。
- 总体风险:LOW
## 直接与间接影响
- GitNexus context:README.md 仅关联 docs/index、治理、部署和架构文档,没有业务执行流调用方。
- GitNexus impact:README.md upstream 直接调用方 0、受影响执行流 0、受影响模块 0,风险 LOW。
- README 的事实来源为现有本地开发、TDX 隧道、系统架构和 FEAT-MO-001/002 功能档案。
## 跨模块同步修改
| 维度 | 判断 | 同步修改 |
|---|---|---|
| HTTP/OpenAPI | 不涉及 | 无 |
| Web API | 不涉及 | 无 |
| 消息、数据库、Redis | 不涉及运行配置 | 无 |
| API、Scheduler、Worker | 不改变启动行为 | README 仅说明边界 |
| 部署 | 仅补充现有源码部署和本地文档入口 | README 链接同步 |
| 测试 | 文档变更不要求业务测试;执行格式检查和功能门禁 | 功能档案 |
| 安全 | 不写入凭据、真实地址或 Token;保留回环和 TDX 隧道边界 | README |
## 性能与残余风险
README 不参与运行时路径,不增加性能负载。残余风险是文档与后续配置变更可能再次漂移,后续端口或部署变更必须同步 README 和对应专项文档。
@@ -0,0 +1,6 @@
# 实施计划
1. 对比 README 与当前部署、架构和功能档案。
2. 执行 Schema 基线和 GitNexus 开发前影响分析。
3. 更新 README 的状态、技术栈、TDX 隧道、启动和治理内容。
4. 检查旧端口和敏感信息,执行格式检查和治理完成门禁。
@@ -0,0 +1,12 @@
id: FEAT-MO-003
name: README 文档同步
slug: readme-refresh
status: ready_for_release
owner: project
created_at: 2026-08-19
requirements: requirements.md
prd: prd.md
design: design.md
implementation_plan: implementation-plan.md
release: unreleased
traceability_note: README 事实边界、完整门禁、GitNexus 与独立 Review 已通过,等待正式版本发布
@@ -0,0 +1,45 @@
# README 文档同步 PRD
- 功能编号:FEAT-MO-003
- PRD版本:0.1
- 状态:approved
- 日期:2026-08-19
## 背景与问题
根目录 README 仍使用旧的英文项目描述、旧的 5173 前端端口和过时的基础设施说明,无法指导当前 TDX 双隧道开发。
## 产品目标
- README 准确说明当前市场概览首屏能力和未覆盖的数据源边界。
- README 准确说明 Vue/NestJS/TDX/MySQL/Redis/RocketMQ 技术栈和源码部署边界。
- README 准确说明 8888 Web、3000 API、17710 TDX 回环入口和 Tailscale 外部场景。
- README 提供可直接执行的本地启动、验证和治理入口。
## 非目标
- 不改变业务代码、接口、运行配置或生产部署。
- 不把真实 Tailscale 地址、Windows 用户、Token、密码或私钥写入文档。
## 功能需求
| 编号 | 优先级 | 需求 | 可观察结果 |
|---|---|---|---|
| FR-001 | P0 | 更新当前实现和技术栈 | README 与 FEAT-MO-001、架构文档一致 |
| FR-002 | P0 | 更新本地端口和启动命令 | README 使用 Web 8888、API 3000 |
| FR-003 | P0 | 更新 TDX 双隧道说明 | README 只使用 SSH 别名和 127.0.0.1:17710 |
| FR-004 | P1 | 更新验证入口 | Schema、治理、测试和部署文档可追溯 |
## 安全策略与评估
README 只记录占位符和回环地址,不记录凭据;明确 TDX 不暴露到公网,明确 Schema 只能访问开发库。
## 性能目标与评估
文档不进入运行时路径,不增加 API、TDX、Redis、MySQL 或 RocketMQ 负载。
## 验收标准
- README 不再把 5173 描述为当前 Web 端口。
- README 包含 8888、TDX 外部隧道和当前首屏模块。
- README 命令与现有 package scripts 和部署手册一致。
@@ -0,0 +1,15 @@
# 测试结果
- README 已包含当前端口 8888、API 3000、TDX 17710 和外部 Tailscale 入口。
- README 已包含当前市场概览模块和数据源边界。
- README 已包含本地启动、停止、Schema 和治理命令。
- 未发现敏感凭据或真实隧道地址。
- 完整门禁:`pnpm verify` 通过,包含格式、契约、类型、全部测试、前后端构建和 Playwright 4/4。
## 安全验证
保持 TDX 回环入口和开发库边界,没有写入真实地址或密钥。
## 性能验证
文档不进入运行时路径;格式检查、构建和 E2E 通过。
@@ -0,0 +1,11 @@
# 视觉验证
README 为 Markdown 文档,本次不执行页面视觉截图;已检查标题层级、代码块、链接和列表结构。
## 安全验证
文档只展示本机回环地址和占位符。
## 性能验证
文档内容不会影响前端运行时性能。
@@ -0,0 +1,3 @@
# 发布说明
README 已同步当前项目状态、8888 Web 端口、TDX 双隧道、本地启动和治理入口;无运行时行为变化。
@@ -0,0 +1,10 @@
# 需求
用户要求更新落后的 README。范围确定为同步当前已实施架构、市场概览、TDX 外部隧道、8888 前端端口、本地启动方式和开发治理入口。
## 实际范围
- 根 README 更新为中文为主的项目说明。
- 补充 Vue 3/NestJS/TDX/MySQL/Redis/RocketMQ/源码部署信息。
- 补充 TDX LAN/Tailscale 别名和 17710 回环入口。
- 补充 Web 8888、API 3000、Scheduler/Worker 可选启动和验证命令。
@@ -0,0 +1,46 @@
# 代码 Review
- 功能编号:FEAT-MO-003
- reviewer_type:self
- reviewer:Codex
- 日期:2026-08-19
## 审查范围
审查 README 的当前状态、启动命令、端口、TDX 隧道、基础设施边界和链接。
## PRD 与设计符合性
符合:README 与现有本地开发、TDX、架构和市场概览文档保持一致。
## 发现
| 严重程度 | 位置 | 问题 | 影响 | 处理 |
|---|---|---|---|---|
| 无 | - | 未发现本次范围内问题 | - | - |
## 测试缺口
未执行页面视觉验证;README 文档不改变页面。
## 安全与残余风险
未发现凭据;残余风险是未来配置变化需要同步文档。
## 性能与资源影响
无运行时影响。
## GitNexus 与同步修改
README upstream 风险 LOW、执行流 0;仅同步入口文档,没有跨模块代码变化。
## 修复与复审
独立 Review 发现 `gitnexus/impact-plan.md` 末尾残留未填写模板,已删除重复占位内容;最终复审又发现 README 将尚不存在的强势个股入口写成已预留,现已改为明确的未来范围。完整门禁通过,等待最终复审。
## 最终结论
最终独立复审确认 README 与现有实现一致,并明确强势个股尚未开发;无 Critical、Important 或 Minor 问题,`Ready to commit: Yes`。
最终结论:passed
@@ -0,0 +1,32 @@
# 开发规范符合性 Review
- 功能编号:FEAT-MO-003
- reviewer_type:self
- reviewer:Codex
- 日期:2026-08-19
## 检查结果
- [x] README 与已实施架构和开发文档一致。
- [x] 未写入凭据、生产数据或真实隧道地址。
- [x] 明确记录安全边界、性能影响和验证证据。
- [x] GitNexus 开发前后流程已记录。
- [x] 开发总结记录需求、范围、完成项、缺陷、延期项和验证证据。
## 不符合项与处置
GitNexus 开发前影响计划曾残留模板占位内容,已清理并保留实际 LOW 风险、0 执行流及逐项跨模块判断;完整门禁已通过,README 对强势个股未开发状态的表述已纠正。
## 安全、性能与架构检查
文档不改变运行时;安全边界和基础设施启用边界已说明。
## GitNexus 与开发总结检查
影响分析为 LOW,README 更新与功能档案一致。
## 最终结论
最终独立复审确认模板占位、安全边界和验证证据均已同步,无未关闭问题。
最终结论:passed
@@ -0,0 +1,10 @@
# 回退说明
## 触发条件
- README 与实际启动配置再次不一致。
- 发现文档包含敏感信息或错误的生产承诺。
## 回退步骤
恢复 README 到本次修改前版本;不需要修改代码、数据库、Redis、RocketMQ 或 TDX 隧道。
@@ -0,0 +1,16 @@
# 测试计划
## 功能验证
- 检查 README 包含 8888、3000、17710、Tailscale 别名和市场概览模块。
- 检查 README 不再使用 5173 作为当前端口。
- 检查命令和链接指向现有文件与 package scripts。
## 安全验证
- 扫描 README 和功能档案,确认不包含 Token、密码、真实外部地址或私钥。
- 确认 README 保留回环监听、TDX 不公网暴露和开发库 Schema 边界。
## 性能验证
文档不参与运行时;执行 git diff --check 和项目文档/治理门禁,不做业务压测。
@@ -0,0 +1,25 @@
# 市场概览 E2E 测试时钟隔离设计
## 根因与方案
`MarketOverviewService` 使用 `new Date()` 生成 `asOf`,再按上海日期判断行情是否延迟。服务单测已经在 suite 级固定时钟,HTTP E2E 没有采用同一模式。设计在 E2E 的 `beforeEach/afterEach` 建立请求级测试时钟边界,不向生产服务注入测试专用接口。
## 数据流
固定系统时间 → 启动 Nest 测试应用 → Controller 调用真实 Service → Service 将固定夹具识别为当日行情 → HTTP 断言 → 关闭应用 → 恢复系统时间。
## 错误与恢复
应用引用保持可选,`afterEach` 无论断言是否成功都关闭应用并恢复真实时钟。不得吞掉 HTTP 或关闭错误。
## 安全策略与评估
测试客户端完全使用内存 mock,不连接 TDX;生产时间和数据状态逻辑保持原样。功能档案只记录相对路径和脱敏统计,无凭据、本机用户目录、网络身份或 PID。
## 性能目标与评估
fake timers 是进程内操作,不增加 I/O、网络或依赖。定向测试应保持 2 秒内完成,完整门禁不增加外部负载。
## 架构与同步边界
变更限定在 `server/apps/api/test/market-overview.e2e-spec.ts`。HTTP/OpenAPI、Web、消息契约、MySQL、Redis、Scheduler、Worker 和部署均无需同步;功能档案和 Review 证据同步更新。
@@ -0,0 +1,36 @@
# 市场概览 E2E 测试时钟隔离开发总结
- 功能编号:FEAT-QA-001
- 总结版本:0.1
- 日期:2026-08-24
## 原始开发需求
在 FEAT-MO-002/003 收尾的完整门禁中,修复市场概览 HTTP E2E 随自然日期变化而失败的问题,并保持生产行情逻辑不变。
## 需求与范围变化
最初只计划收尾端口和 README;完整门禁暴露测试夹具时钟缺陷后,经用户确认新增独立 `FEAT-QA-001`,避免把缺陷混入端口提交。
## 实际完成需求
- E2E suite 固定到夹具交易日,并在每个用例后恢复真实时钟。
- 生产 Service、Controller、DTO、TDX 和 OpenAPI 未修改。
- 根因、影响、安全、性能与回退边界建立独立档案。
## 修复缺陷
修复固定交易日夹具依赖真实系统日期导致 `fresh` 断言跨日变为 `delayed` 的非确定性缺陷。RED 为 Server 65/67、2 个状态断言失败;GREEN 为定向 E2E 2/2 通过。
## 未完成与延期项
权威交易日历仍不在本功能范围;本功能只保证测试确定性,不改变休市日保守标记 `delayed` 的既有生产策略。
## 关键提交
- `368672f001bb88f7fb2f012604986d7f8ce2820a`:`fix(FEAT-QA-001): 隔离市场概览 E2E 测试时钟`。
- 该修复提交已推送至项目 Gitea `origin/main`,远端分支 SHA 与本地一致。
## 验证证据
定向 E2E 2/2 和完整 `pnpm verify` 已通过:Web 21/21、Server 67/67、启动器 2/2、Playwright 4/4。最终 GitNexus 刷新为 3,134 nodes、4,355 edges、68 clusters、85 flows,状态 `up-to-date`;detect changes 为 LOW、0 受影响执行流。独立代码 Review 和开发规范符合性 Review 均已通过。
@@ -0,0 +1,13 @@
# GitNexus 开发后基线
## GitNexus 状态
索引已刷新为 3,122 nodes、4,343 edges、68 clusters、85 flows;当前提交索引状态 `up-to-date`。
## Git 已跟踪变更摘要
本功能只修改市场概览 HTTP E2E 的时钟生命周期并新增功能档案;生产 Service、Controller、DTO、TDX 和 OpenAPI 无变化。
## Git 工作区摘要
工作区还包含已批准收尾的 FEAT-MO-002/003;无关未跟踪文件明确排除。最终提交使用逐路径 staging,不使用 `git add -A`。
@@ -0,0 +1,34 @@
# GitNexus 开发前基线
- 功能编号:FEAT-QA-001
- 记录时间:2026年8月24日 GMT+8 17:26:04
- 分支:main
- 基线提交:1f6ed00e6fbb
## 工作区
```text
M AGENTS.md
M README.md
M docs/deployment/local-development.md
M web/playwright.config.ts
M web/src/vite-config.test.ts
M web/vite.config.ts
?? <计划外未跟踪文件,明确排除且未读取>
?? docs/features/FEAT-MO-002-web-port-8888/
?? docs/features/FEAT-MO-003-readme-refresh/
```
## GitNexus 状态
```text
Repository: <project-root>
Indexed: 8/24/2026, 5:26:03 PM
Indexed commit: 1f6ed00
Current commit: 1f6ed00
Status: ✅ up-to-date
```
## 后续人工分析
在修改代码前,将 GitNexus query、context、impact 的结论写入 `gitnexus/impact-plan.md`。高风险结果必须先获得用户确认。
@@ -0,0 +1,13 @@
# GitNexus 前后对比
## 开发前
生产时钟函数 upstream 风险 CRITICAL,影响五个市场概览接口;E2E 文件 upstream 风险 LOW、0 执行流。自然日期下完整门禁出现 2 个状态断言失败。
## 开发后
生产调用图和业务执行流不变;E2E suite 在夹具日期下运行并恢复真实时钟。开发后 detect changes 为 LOW、0 受影响执行流,完整门禁通过。
## 差异处置
没有修改 CRITICAL 生产符号,也没有同步 HTTP/OpenAPI、Web API、消息、数据库、Redis、Scheduler、Worker 或部署。
@@ -0,0 +1,5 @@
# GitNexus 变更检测
执行 `gitnexus_detect_changes({scope: "all", repo: "a-share-analysis"})`:changed_count=10、changed_files=7、affected_count=0、risk_level=low、受影响执行流 0。
当前工作区同时包含已批准收尾的 FEAT-MO-002/003。GitNexus 识别到 README、本地开发文档和 AGENTS 索引统计;测试生命周期变化没有映射为生产执行流。实际提交将显式列出路径并排除计划外未跟踪文件。
@@ -0,0 +1,27 @@
# GitNexus 开发前影响计划
- 功能编号:FEAT-QA-001
- 总体风险:LOW(实际修改范围);生产关联符号若修改为 CRITICAL,因此明确排除
## 直接与间接影响
- `gitnexus_query` 定位到市场概览 Service、TDX 日期映射和 HTTP E2E。
- `gitnexus_context(getShanghaiCalendarDate)` 确认生产函数由 `createTradeDateWarning` 调用。
- `gitnexus_impact(getShanghaiCalendarDate, upstream, includeTests=true)` 返回 CRITICAL:直接消费者 1、影响五个市场概览接口和 5 条流程。该结果用于约束本次不修改生产函数。
- `gitnexus_impact(market-overview.e2e-spec.ts, upstream, includeTests=true)` 返回 LOW:上游 0、执行流 0。
- 根因由 2026-08-24 完整门禁稳定复现:夹具日期为 `20260818`,真实系统日期导致生产服务正确返回 `delayed`,E2E 错误断言 `fresh`。
## 跨模块同步修改
| 维度 | 判断 | 同步修改 |
|---|---|---|
| HTTP/OpenAPI | 响应和契约不变 | 无 |
| Web API | 不涉及 | 无 |
| 消息契约 | 不涉及 RocketMQ | 无 |
| 数据库 | 不涉及 MySQL 或 Schema | 无 |
| Redis | 不涉及 | 无 |
| API | 生产模块不变,仅 HTTP E2E 生命周期 | 修改测试 |
| Scheduler/Worker | 不涉及 | 无 |
| 配置部署 | 不涉及 | 无 |
| 测试 | 固定夹具时钟并恢复真实时钟 | 修改 E2E |
| 文档 | 建立缺陷追溯和验证证据 | 新增功能档案 |
@@ -0,0 +1,9 @@
# 市场概览 E2E 测试时钟隔离实施计划
1. 运行完整门禁复现自然日期下 2 个 `fresh`/`delayed` 失败。
2. 使用 GitNexus 查询时钟判断链路,并对生产时钟函数和测试文件执行 upstream impact。
3. 在 E2E suite 的 `beforeEach` 固定到 `2026-08-18T03:00:00.000Z`。
4. 在 `afterEach` 关闭应用后恢复 real timers。
5. 运行定向测试确认 2/2 通过,再执行完整 `pnpm verify`。
6. 更新 GitNexus 前后对比、安全、性能、Review、发布和回退证据。
7. 使用中文 Conventional Commit 提交并立即推送当前分支。
@@ -0,0 +1,12 @@
id: FEAT-QA-001
name: 市场概览 E2E 测试时钟隔离
slug: e2e-clock-isolation
status: ready_for_release
owner: project
created_at: 2026-08-24
requirements: requirements.md
prd: prd.md
design: design.md
implementation_plan: implementation-plan.md
traceability_note: 修复固定交易日夹具依赖真实系统日期导致的跨日失败,不修改生产行情和新鲜度逻辑
release: unreleased
@@ -0,0 +1,84 @@
# 市场概览 E2E 测试时钟隔离 PRD
- 功能编号:FEAT-QA-001
- PRD 版本:0.1
- 状态:in_review
- 日期:2026-08-24
## 背景与问题
市场概览 HTTP E2E 使用 `20260818` 固定行情夹具,却读取真实系统日期判断 `fresh/delayed`。日期跨过 2026-08-18 后,生产代码正确返回 `delayed`,测试仍固定要求 `fresh`,导致完整门禁出现 2 个稳定失败。
## 目标用户与使用场景
开发者在任意自然日期执行完整验证,都应得到与固定夹具语义一致、可重复的测试结果。
## 产品目标
隔离 E2E 测试时钟,使测试日期与夹具交易日一致,并在用例结束后恢复真实时钟。
## 非目标
不修改生产新鲜度规则、不引入交易日历、不改变 HTTP/OpenAPI、不修改 TDX 数据映射。
## 用户流程
开发者执行 Server 测试,E2E 在隔离时钟下启动 Nest 应用、验证响应,随后关闭应用并恢复真实时钟。
## 功能需求
| 编号 | 优先级 | 需求 | 可观察结果 |
|---|---|---|---|
| QA-001 | P0 | 每个市场概览 HTTP E2E 用例固定到夹具交易日 | `fresh` 断言不随自然日期漂移 |
| QA-002 | P0 | 每个用例后关闭应用并恢复真实时钟 | 不污染同进程其他测试 |
| QA-003 | P1 | 记录根因、影响和完整回归证据 | 功能档案可追溯 |
## 数据与口径
测试系统时间固定为 `2026-08-18T03:00:00.000Z`,对应上海日期 `20260818`;夹具数据及生产新鲜度口径不变。
## 状态与异常
若用例中断,`afterEach` 仍负责关闭 Nest 应用并调用 `vi.useRealTimers()`;测试失败必须保留真实断言差异。
## 权限、合规与安全
测试不访问真实 TDX、数据库或公网服务,不读取 `.env.local`,不输出凭据、本机绝对路径或临时进程标识。
## 安全需求与信任边界
只修改测试运行时;禁止通过修改生产时间判断或把 `delayed` 强行改成 `fresh` 来通过测试。
## 性能目标与容量假设
不增加外部请求;定向 E2E 保持秒级完成,完整门禁耗时不应产生可感知增长。
## 关联模块与同步修改
只同步 Server E2E 测试和功能档案;HTTP/OpenAPI、Web API、消息、数据库、Redis、API 生产代码、Scheduler、Worker 和部署均不修改。
## 新增依赖评估
不新增依赖,使用现有 Vitest fake timers。
## 成功指标
定向 E2E 2/2 通过;完整 `pnpm verify` 通过;测试结束后无 fake timer 泄漏。
## 验收标准
- 当前自然日期下可复现的 2 个失败消失。
- 生产文件没有差异。
- GitNexus 仅识别预期测试和文档变化。
## 风险与依赖
残余风险是未来修改固定行情日期时必须同步测试时钟;代码注释明确该关联。
## 发布范围与回退触发条件
仅测试和文档。若测试时钟污染其他套件或隐藏真实延迟语义,回退本功能提交。
## 变更记录
- 0.1:建立跨日 E2E 时钟隔离需求。
@@ -0,0 +1,14 @@
# 测试结果
- RED:`pnpm verify` 的 Server 测试为 65/67,通过值实际为 `delayed`,2 个 E2E 固定要求 `fresh` 而失败。
- GREEN:`pnpm --filter @a-share-analysis/server exec vitest run apps/api/test/market-overview.e2e-spec.ts` 为 2/2 通过。
- 完整门禁:`pnpm verify` 通过;Web 21/21、Server 67/67、启动器 2/2、Playwright 4/4,契约、边界、Schema、类型检查和前后端构建均通过。
- Lint:0 error;35 条市场概览组件既有格式 warning 未因本功能增加。
## 安全验证
生产市场概览代码没有变化;测试使用内存 mock,未访问真实行情、数据库、Redis 或 RocketMQ;无关 CSV 明确排除。
## 性能验证
定向 E2E 在 1 秒内完成;完整门禁成功结束,fake timers 不增加网络或持久化负载。
@@ -0,0 +1,3 @@
# 视觉 QA
本功能不改变 DOM、CSS、组件或浏览器交互,视觉变化不适用。完整 Playwright 桌面/移动端 4/4 通过,确认测试基础设施调整没有影响现有页面。
@@ -0,0 +1,3 @@
# 发布说明
修复市场概览 HTTP E2E 随自然日期变化出现 `fresh/delayed` 误失败的问题。仅影响测试确定性,不改变用户界面、HTTP 契约、TDX 数据或生产新鲜度判断。
@@ -0,0 +1,8 @@
# 市场概览 E2E 测试时钟隔离需求
- `beforeEach` 必须启用 fake timers,并固定到夹具交易日对应的上海日期。
- `afterEach` 必须在关闭 Nest 应用后恢复 real timers。
- 必须先保存自然日期下 2 个 `fresh`/`delayed` 失败作为 RED 证据,再验证修复后的 GREEN。
- 不得修改 `MarketOverviewService`、Controller、DTO、TDX Client/Mapper 或 OpenAPI。
- 不得读取真实外部服务或把无关 CSV 纳入任何提交。
- 必须完成安全、性能、完整测试、构建、GitNexus 和独立 Review。
@@ -0,0 +1,50 @@
# 代码 Review
- 功能编号:FEAT-QA-001
- reviewer_type:independent
- reviewer:Codex 独立审查代理
- 基线提交:1f6ed00
- 审查提交:working-tree
- 日期:2026-08-24
## 审查范围
市场概览 HTTP E2E 的测试时钟生命周期和对应功能档案。
## PRD 与设计符合性
实现符合 PRD:测试时钟固定到夹具交易日,生产行情和新鲜度逻辑不变;完整门禁已通过,等待关闭本轮独立 Review 发现项。
## 发现
| 严重程度 | 位置 | 问题 | 影响 | 处理 |
|---|---|---|---|---|
| Important | E2E cleanup | `app.close()` 抛错可能跳过时钟恢复 | 污染同进程后续测试 | 使用 `try/finally` 保证恢复 real timers,定向与完整门禁通过 |
| Important | README | 将尚不存在的强势个股入口描述为已预留 | 项目状态说明失真 | 明确标记强势个股模块尚未开发 |
| Minor | Review 文档 | 部分证据仍写成等待执行 | 治理记录与实际状态矛盾 | 同步完整门禁和 GitNexus 实际结果 |
## 测试缺口
完整 `pnpm verify` 已通过;Lint 为 0 error,35 条既有格式 warning 未增加。
## 安全与残余风险
生产逻辑保持不变;`finally` 保证测试结束后恢复 real timers。安全扫描未发现环境信息,无关 CSV 保持未跟踪并由逐路径 staging 排除。
## 性能与资源影响
仅进程内 fake timers,无运行时资源影响;定向 E2E 在 1 秒内完成,完整门禁通过且未增加外部调用。
## GitNexus 与同步修改
生产时钟函数 upstream 风险 CRITICAL、关联五个接口,因此明确不修改;测试文件 upstream 风险 LOW、0 执行流。开发后 detect changes 为 LOW、0 受影响执行流。
## 修复与复审
定向 GREEN 和完整门禁已完成。独立 Review 发现 `app.close()` 抛错时可能跳过时钟恢复,已将 `vi.useRealTimers()` 移入 `finally`;README 的强势个股入口夸大属于 FEAT-MO-003 范围,已同步纠正。复审未发现新的代码阻断项,最后一轮仅要求同步上述证据文字。
## 最终结论
最终复审确认 `try/finally` 清理、README 纠偏和证据同步均已关闭;无 Critical、Important 或 Minor 问题,`Ready to commit: Yes`。
最终结论:passed
@@ -0,0 +1,33 @@
# 开发规范符合性 Review
- 功能编号:FEAT-QA-001
- reviewer_type:independent
- reviewer:Codex 独立审查代理
- 审查提交:working-tree
- 日期:2026-08-24
## 检查结果
- [x] 缺陷有稳定复现和 RED/GREEN 证据。
- [x] 生产逻辑没有为测试让步。
- [x] GitNexus 已执行 query、context 和 upstream impact。
- [x] 完整测试、构建、E2E 和开发后分析已完成。
- [x] 独立 Review 最终无 Critical、Important 或 Minor 问题。
## 不符合项与处置
完整门禁、开发后 GitNexus 和独立 Review 均已完成;`finally` 清理和文档纠偏通过复审。
## 安全、性能与架构检查
测试时钟限定在 suite 生命周期,不进入生产依赖注入或外部系统;无新增依赖和 I/O。
## GitNexus 与开发总结检查
功能档案已记录根因、生产符号 CRITICAL 风险和测试文件 LOW 风险;开发后变更检测为 LOW、0 执行流,提交与远程证据将在复审后补充。
## 最终结论
最终独立复审结论为 `Ready to commit: Yes`,无未关闭问题。
最终结论:passed
@@ -0,0 +1,3 @@
# 回退说明
若 fake timers 污染同进程其他测试或掩盖真实状态缺陷,回退本功能提交即可。回退只移除 E2E suite 的 `beforeEach` 时钟固定和 `afterEach` 时钟恢复;不需要数据库、缓存、消息、配置或生产服务回滚。
@@ -0,0 +1,24 @@
# 市场概览 E2E 测试时钟隔离测试计划
## RED/GREEN
- RED:在 2026-08-24 运行完整门禁,固定 `20260818` 夹具被生产逻辑标记为 `delayed`,2 个 E2E 断言失败。
- GREEN:加入 suite 级时钟隔离后,定向 E2E 2/2 通过。
## 回归范围
- Server 全部 Vitest 和启动器测试。
- Web 测试、类型、构建和 Playwright E2E。
- 契约、边界、治理、Schema 和源码部署门禁。
## 安全验证
确认测试不访问真实 TDX、数据库或公网;`git diff` 不含凭据、本机绝对路径、PID 或无关 CSV;生产市场概览文件无变化。
## 性能验证
记录定向测试和完整门禁耗时;确认无新增网络、数据库或消息调用。
## 残余风险
未来夹具交易日变化但固定时钟未同步会重新出现语义不一致,应由明确注释和失败断言暴露。
@@ -0,0 +1,85 @@
# AKTools Supplement Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task after the written spec is approved.
**Goal:** Add an internal AKTools-backed short-term market sentiment module without changing the existing TDX overview path.
**Architecture:** Nest owns the public business API. A fixed AKTools client calls three allowlisted HTTP endpoints in parallel, a mapper validates Chinese upstream fields into stable domain DTOs, and a service derives conservative rules. Vue consumes only Nest API files.
**Tech Stack:** NestJS, TypeScript, global fetch, Vue 3, Axios request wrapper, OpenAPI generated contracts, Vitest, Playwright, AKTools HTTP on `127.0.0.1:8080`.
**Spec:** `docs/superpowers/specs/2026-08-24-aktools-supplement-design.md`
## Global Constraints
- MySQL remains 5.7.x with `utf8mb4_general_ci`; no database writes or production reads.
- AKTools is an external internal-LAN service; do not add Docker files or vendor Python code.
- Frontend requests go through `web/src/utils/request.ts` and `web/src/api/<domain>/` only.
- TDX core overview must remain independently available when AKTools fails.
- Every external field must be validated and mapped; raw AKTools fields never cross the API boundary.
### Task 1: Define configuration and upstream contracts
**Files:**
- Modify: development and production environment templates, runtime config tests.
- Create: `server/apps/api/src/market-overview/aktools/aktools.types.ts` and client tests.
- [ ] Add optional `AKTOOLS_ENABLED`, fixed-base-url validation, and timeout configuration.
- [ ] Define raw response types for the three allowlisted endpoints and normalized domain types.
- [ ] Write tests for invalid URL, disabled service, timeout, non-JSON response, oversized response, and HTTP error.
- [ ] Run targeted configuration and client tests.
### Task 2: Implement AKTools client and mapper
**Files:**
- Create: `server/apps/api/src/market-overview/aktools/aktools.client.ts`.
- Create: `server/apps/api/src/market-overview/aktools/aktools.mapper.ts`.
- Create: client and mapper tests.
- [ ] Implement only fixed paths for the three AKTools endpoints.
- [ ] Execute requests with `Promise.allSettled` at the service boundary.
- [ ] Validate dates, numbers, board counts, stock codes, and missing fields.
- [ ] Convert amounts to亿元 only where the source unit is explicitly known.
- [ ] Test field drift and partial records.
### Task 3: Add short-term API contracts and rules
**Files:**
- Modify: market overview DTO, module, controller and service.
- Create: short-term service tests and E2E fixtures.
- [ ] Add `GET /api/v1/market-overview/short-term/emotion` and `conclusion`.
- [ ] Return snapshot metadata, source, trade date, status and warnings.
- [ ] Derive count, max board, tier distribution, broken-board count and previous-limit-up performance.
- [ ] Use conservative labels: strong, weak, divided, insufficient; do not invent repair or true wind-down state without historical baseline.
- [ ] Verify AKTools failure does not alter TDX responses.
### Task 4: Update generated contracts and Web state
**Files:**
- Modify: `web/src/api/market-overview/index.ts`, generated schema and market overview composable/page.
- Create: short-term API and component tests.
- [ ] Add typed API functions through the existing Axios wrapper.
- [ ] Render loading, partial, delayed, unavailable and warning states without blocking TDX modules.
- [ ] Keep the existing analysis horizon and engine preferences unchanged.
- [ ] Verify desktop and narrow viewport layouts.
### Task 5: Documentation and deployment boundary
**Files:**
- Modify: architecture, local development, production source deployment and README docs.
- Modify: this feature archive and review evidence.
- [ ] Document AKTools as an externally managed internal service on port 8080.
- [ ] State that the repository does not install, stop, expose or delete AKTools.
- [ ] Record data authorization, upstream instability, and no-public-exposure boundaries.
- [ ] Update development summary with actual requirements, defects, commits and evidence.
### Task 6: Verification and release gate
- [ ] Run unit, contract, boundary, typecheck, build and Playwright verification.
- [ ] Run `npm run governance:finish -- FEAT-DS-001 aktools-supplement`.
- [ ] Refresh GitNexus and run staged `gitnexus_detect_changes`.
- [ ] Complete code Review and standards Review.
- [ ] Commit with Chinese Conventional Commit and immediately push to Gitea.
@@ -0,0 +1,31 @@
# AKTools 超短行情补充数据源设计
**状态:已获用户确认,待实现**
## 目标
在内网环境把已运行的 AKTools 作为 AKShare 补充数据源,提供 TDX 未覆盖的超短行情指标,不改变现有 TDX 市场概览链路。
## 方案
Nest API 增加固定的 AKTools Client 和领域 Mapper,按业务接口调用三个已验证的 AKTools endpoint。前端只访问 Nest,不访问 AKTools。V1 不新增 Python 服务、Docker、数据库、Redis 或 RocketMQ。
## 数据接口
- `stock_zt_pool_em`:涨停池、封板资金、连板数和行业。
- `stock_zt_pool_zbgc_em`:炸板池和炸板次数。
- `stock_zt_pool_previous_em`:昨日涨停股今日表现和昨日连板数。
Nest 对外提供 `/api/v1/market-overview/short-term/emotion` 和 `/api/v1/market-overview/short-term/conclusion`,不透传 AKTools 通用路由。
## 规则口径
只使用通过类型、日期和数值校验的记录。V1 输出涨停数量、最高连板、梯队分布、炸板数量、昨日涨停平均涨跌幅和涨跌分布;不计算未经确认口径的炸板率,不把封板资金解释为主力净流入。
## 状态与降级
接口响应使用 `fresh`、`delayed`、`partial`、`unavailable`。AKTools 失败只影响短线模块;TDX 模块继续工作。每次响应包含 `snapshotId`、`asOf`、`tradeDate`、`source` 和 `warnings`。
## 安全与性能
AKTools 默认绑定 `127.0.0.1:8080`。Nest 只读取服务端配置,拒绝客户端 URL,设置 10 秒超时、响应大小限制和脱敏错误。三个上游请求并行执行,短线接口 P95 目标小于 12 秒。
@@ -0,0 +1,126 @@
# FEAT-MO-004 指数与板块详情分析页设计
## 目标
在市场综合概览首屏点击指数或板块后,进入可复盘的详情页,展示经过 TDX 校验的行情、交易日期、状态、K 线和可用的分时数据,并保持当前只读、无交易建议的数据边界。
## 已确认范围
- 第一版产品范围同时包含指数详情和板块详情。
- 实施采用两个纵向切片:先完成指数详情,再接入板块详情。
- 两类详情共用页面外壳、路由参数校验、数据状态、更新时间、数据来源、警告和图表容器。
- 首版不包含成分股排名、主力净流入、盘中新闻、AI 结论、交易操作、用户体系和订阅功能。
- 后端继续返回标准 UTC ISO `asOf`;Web 按 `Asia/Shanghai` 格式化展示。
## 设计原则
1. **真实数据优先**:TDX 未验证的字段显示不可用或延迟状态,不使用模拟数据或离线样本伪造实时行情。
2. **模块级降级**:详情数据失败不影响首页概览,K 线、分时和摘要分别保留状态。
3. **契约先行**:详情 API 独立于首页五个概览接口,不把详情数据追加到首页接口的大对象中。
4. **共享稳定边界**:指数和板块只共享详情外壳、时间/状态显示和图表输入契约;数据映射仍按 TDX 业务口径分别维护。
5. **只读安全**:详情请求只允许白名单代码、有限周期和有限数量,禁止把任意 TDX 方法名或原始字段暴露给 Web。
## 技术方案
### 页面与路由
新增一个详情页组件,根据类型和代码加载对应 API:
```text
/market-overview/indices/:code
/market-overview/sectors/:code
```
路由参数只接受已知格式:指数代码必须匹配已登记指数或 TDX 返回的合法指数代码;板块代码必须来自已登记一级行业目录。非法代码进入明确的不可用页面,不触发 TDX 请求。
详情页结构固定为:
1. 返回概览与标题区:名称、代码、TDX 来源、上海时间更新时间、交易日期和状态。
2. 核心行情区:最新价/涨跌幅(指数)或涨跌幅/强弱等级(板块),成交额按实际可用字段展示。
3. 图表区:K 线为首要视图;分时视图只有在 TDX 能力探测通过后启用,否则展示“当前数据源未提供分时数据”的边界说明。
4. 数据质量区:`fresh`、`delayed`、`partial`、`unavailable` 和 warnings 与首页一致。
5. 复盘摘要区:只展示事实型数据和规则计算结果,不产生买入、卖出、目标价或仓位建议。
### 后端接口
新增两个独立接口,参数和响应保持有限、可验证:
```text
GET /api/v1/market-overview/indices/:code/detail?period=1d&count=120
GET /api/v1/market-overview/sectors/:code/detail?period=1d&count=120
```
响应统一包含现有 `MarketOverviewMetaDto` 字段:
```text
snapshotId
asOf
source
status
warnings
data
```
`data` 最小结构:
```text
instrument: { code, name, type }
quote: { price, changePercent, direction, tradeDate }
series: [{ time, open, high, low, close, amount, volume }]
availableViews: { kline: boolean, intraday: boolean }
```
指数和板块的数据 DTO 可以共享 K 线点类型,但 `quote` 和摘要字段必须保持各自业务口径。`series` 的数量上限由服务端固定在 120 条以内,避免任意参数导致 TDX 响应膨胀。
### TDX 能力验证门
在实现详情 API 前先对当前 TDX 桥接执行只读能力探测并保存脱敏报告:
- 已验证的 `get_market_data` 日线数据继续作为 K 线来源。
- 探测分时方法、参数格式、响应日期/时间字段和单次返回上限。
- 如果分时能力未通过协议、字段完整性或稳定性检查,首版只发布 K 线,API 和 UI 明确返回 `intraday=false`,不创建假分时数据。
- 板块详情先验证一级行业代码在历史 K 线查询中的可用性,再实现板块图表。
### 前端 API 与图表
- 所有请求继续位于 `web/src/api/market-overview/index.ts` 或同域详情 API 文件,并通过 `web/src/utils/request.ts` 的 Axios 封装。
- `web/src/views/market-overview/detail/` 管理详情页、状态和图表输入适配;页面不得直接调用 Axios 或 TDX。
- 首选轻量图表库;在能力探测和依赖评估完成前不新增依赖。评估维度包括 K 线/分时能力、Vue 3 兼容性、包体积、许可证、无障碍、销毁重建、移动端性能和退出方案。
- 若新增图表依赖,必须在设计和 Review 中记录替代方案、许可证和包体积证据。
## 数据状态和错误处理
- `fresh`:数据完整且交易日期与上海当前日期一致。
- `delayed`:数据可用但交易日期不是上海当前日期,显示实际交易日期。
- `partial`:部分 K 线、摘要或视图不可用,保留有效数据并展示 warning。
- `unavailable`:代码不存在、TDX 不可达、协议错误或没有有效数据,页面不展示伪造图表。
- TDX 原始地址、请求体、凭据和原始异常不进入 Web 响应;错误只返回固定分类文案。
## 安全与性能
- 代码、周期和数量在 Controller/Service 双层校验;只允许已登记业务代码和 `1d` 等已验证周期。
- TDX 客户端继续使用超时、响应体上限、固定回环地址和错误脱敏。
- 单个详情请求最多读取 120 个 K 线点;不在首版引入数据库、Redis、RocketMQ 或后台任务。
- 图表数据在页面卸载时释放实例;加载状态固定容器尺寸,避免数据返回造成布局跳动。
- 性能目标:本地已验证 TDX 可达时详情首屏 P95 小于 2 秒;TDX 不可达时 10 秒内返回明确不可用状态。该目标不代表生产容量证明。
## 测试与验收
- TDX Client:超时、协议错误、过大响应、业务错误和请求参数白名单。
- Mapper:合法/非法日期、空序列、倒序序列、部分字段缺失、数量上限和方向计算。
- Service:指数/板块成功、延迟、部分、不可用和模块隔离。
- HTTP/OpenAPI:两个详情路由、参数校验、DTO nullable 和错误状态。
- Web:路由参数、加载/空/错误/延迟状态、上海时间显示、K 线数据输入和移动端无横向溢出。
- 浏览器:桌面和 390px 移动视口,详情入口可达,返回首页可用,图表容器非空或显示明确不可用状态。
## 发布与回退
- 首版只在开发内网启用,继续通过 Mac 本机 TDX 隧道访问,不把 TDX 暴露到局域网或公网。
- AKTools 不参与指数/板块详情主链路;生产默认关闭。
- 回退时删除详情路由和 API 注册或回到上一个完整发布目录,不修改数据库结构,不需要数据回滚。
## 未决但有边界的事项
- 分时数据是否纳入首版由 TDX 能力探测结果决定,不以 UI 需求强行反推协议。
- 图表库在探测完成后按依赖评估选择;若原生 SVG/Canvas 足以满足首版,则不新增图表依赖。
- 成分股、资金流、新闻和 AI 另立功能,不因详情页入口提前扩大范围。
@@ -0,0 +1,64 @@
import { describe, expect, it, vi } from 'vitest';
import { createAktoolsClient, AktoolsClientError } from './aktools.client';
describe('AKTools HTTP 客户端', () => {
it('只请求固定涨停池路径并传递交易日', async () => {
const fetchImpl = vi.fn().mockResolvedValue(
new Response('[{"代码":"000001","连板数":2}]', {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
const client = createAktoolsClient({
enabled: true,
baseUrl: 'http://127.0.0.1:8080',
timeoutMs: 1000,
fetchImpl,
});
await expect(client.getLimitUpPool('20260824')).resolves.toEqual([
{ 代码: '000001', 连板数: 2 },
]);
expect(fetchImpl).toHaveBeenCalledWith(
'http://127.0.0.1:8080/api/public/stock_zt_pool_em?date=20260824',
expect.objectContaining({ method: 'GET' }),
);
});
it('将 HTTP 错误转换为脱敏的客户端错误', async () => {
const fetchImpl = vi
.fn()
.mockResolvedValue(new Response('secret upstream detail', { status: 500 }));
const client = createAktoolsClient({
enabled: true,
baseUrl: 'http://127.0.0.1:8080',
timeoutMs: 1000,
fetchImpl,
});
const promise = client.getBrokenBoardPool('20260824');
await expect(promise).rejects.toBeInstanceOf(AktoolsClientError);
await expect(promise).rejects.toMatchObject({
kind: 'unavailable',
message: 'AKTools 服务不可用',
});
});
it('将请求超时转换为稳定错误', async () => {
const fetchImpl = vi
.fn()
.mockRejectedValue(Object.assign(new Error('aborted'), { name: 'AbortError' }));
const client = createAktoolsClient({
enabled: true,
baseUrl: 'http://127.0.0.1:8080',
timeoutMs: 1000,
fetchImpl,
});
await expect(client.getPreviousLimitUpPool('20260824')).rejects.toMatchObject({
kind: 'timeout',
message: 'AKTools 请求超时',
});
});
});
@@ -0,0 +1,96 @@
import type { AktoolsRuntimeConfig } from '../../../../../libs/config/src/runtime-config';
import type { AktoolsDataClient, AktoolsRawRow } from './aktools.types';
export type AktoolsClientErrorKind =
'disabled' | 'timeout' | 'network' | 'protocol' | 'unavailable';
export class AktoolsClientError extends Error {
constructor(
readonly kind: AktoolsClientErrorKind,
message: string,
) {
super(message);
this.name = 'AktoolsClientError';
}
}
export interface AktoolsClientOptions extends AktoolsRuntimeConfig {
fetchImpl?: typeof fetch;
maxResponseBytes?: number;
}
const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
const ENDPOINTS = Object.freeze({
limitUp: 'stock_zt_pool_em',
brokenBoard: 'stock_zt_pool_zbgc_em',
previousLimitUp: 'stock_zt_pool_previous_em',
});
function isAbortError(error: unknown): boolean {
return error instanceof Error && error.name === 'AbortError';
}
function assertTradeDate(tradeDate: string): void {
if (!/^\d{8}$/.test(tradeDate)) throw new AktoolsClientError('protocol', '交易日期格式错误');
}
/**
* AKTools 只作为固定的内网补充源,禁止把通用函数名和 URL 暴露给请求方。
* 客户端在这里统一处理上游网络和协议边界,业务层只接收数组记录。
*/
export function createAktoolsClient({
enabled,
baseUrl,
timeoutMs,
fetchImpl = fetch,
maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,
}: AktoolsClientOptions): AktoolsDataClient {
const endpoint = `${baseUrl.replace(/\/$/, '')}/api/public/`;
async function request(path: string, tradeDate: string): Promise<AktoolsRawRow[]> {
assertTradeDate(tradeDate);
if (!enabled) throw new AktoolsClientError('disabled', 'AKTools 未启用');
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(`${endpoint}${path}?date=${encodeURIComponent(tradeDate)}`, {
method: 'GET',
headers: { accept: 'application/json' },
signal: controller.signal,
});
const contentLength = response.headers.get('content-length');
if (contentLength && Number(contentLength) > maxResponseBytes) {
throw new AktoolsClientError('protocol', 'AKTools 响应体超过安全上限');
}
const text = await response.text();
if (Buffer.byteLength(text, 'utf8') > maxResponseBytes) {
throw new AktoolsClientError('protocol', 'AKTools 响应体超过安全上限');
}
if (!response.ok) throw new AktoolsClientError('unavailable', 'AKTools 服务不可用');
let value: unknown;
try {
value = JSON.parse(text);
} catch {
throw new AktoolsClientError('protocol', 'AKTools 返回格式错误');
}
if (!Array.isArray(value) || value.some((item) => !item || typeof item !== 'object')) {
throw new AktoolsClientError('protocol', 'AKTools 返回数据结构错误');
}
return value as AktoolsRawRow[];
} catch (error) {
if (error instanceof AktoolsClientError) throw error;
if (isAbortError(error)) throw new AktoolsClientError('timeout', 'AKTools 请求超时');
throw new AktoolsClientError('network', 'AKTools 网络请求失败');
} finally {
clearTimeout(timeout);
}
}
return {
getLimitUpPool: (tradeDate) => request(ENDPOINTS.limitUp, tradeDate),
getBrokenBoardPool: (tradeDate) => request(ENDPOINTS.brokenBoard, tradeDate),
getPreviousLimitUpPool: (tradeDate) => request(ENDPOINTS.previousLimitUp, tradeDate),
};
}
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import { mapBrokenBoardPool, mapLimitUpPool, mapPreviousLimitUpPool } from './aktools.mapper';
describe('AKTools 超短字段映射', () => {
it('过滤缺失连板数的记录并生成连板梯队', () => {
expect(
mapLimitUpPool([
{ 代码: '000001', 名称: '甲', 连板数: 3, 所属行业: '软件服务', 封板资金: 100000000 },
{ 代码: '000002', 名称: '乙', 连板数: 2, 所属行业: '软件服务', 封板资金: 50000000 },
{ 代码: '000003', 名称: '丙', 连板数: 'bad', 所属行业: '汽车', 封板资金: 1 },
]),
).toEqual({
count: 2,
maxBoard: 3,
tiers: [
{ board: 2, count: 1 },
{ board: 3, count: 1 },
],
industries: [{ name: '软件服务', count: 2 }],
sealFundsAmount: 1.5,
});
});
it('统计有效炸板数量并忽略空记录', () => {
expect(mapBrokenBoardPool([{ 代码: '000001' }, {}, null as never])).toEqual({ count: 1 });
});
it('计算昨日涨停股涨跌分布和平均涨跌幅', () => {
expect(
mapPreviousLimitUpPool([
{ 代码: '000001', 涨跌幅: 2.5 },
{ 代码: '000002', 涨跌幅: -1 },
{ 代码: '000003', 涨跌幅: 0 },
{ 代码: '000004', 涨跌幅: 'bad' },
]),
).toEqual({
count: 3,
averageChangePercent: 0.5,
upCount: 1,
downCount: 1,
flatCount: 1,
});
});
});
@@ -0,0 +1,94 @@
import type { AktoolsRawRow } from './aktools.types';
export interface LimitUpSummary {
count: number;
maxBoard: number;
tiers: Array<{ board: number; count: number }>;
industries: Array<{ name: string; count: number }>;
sealFundsAmount: number;
}
export interface BrokenBoardSummary {
count: number;
}
export interface PreviousLimitUpSummary {
count: number;
averageChangePercent: number;
upCount: number;
downCount: number;
flatCount: number;
}
function toNumber(value: unknown): number | null {
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
if (typeof value !== 'string' || !value.trim()) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function toPositiveInteger(value: unknown): number | null {
const parsed = toNumber(value);
return parsed !== null && Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
}
function hasCode(row: unknown): row is AktoolsRawRow {
if (!row || typeof row !== 'object') return false;
const code = (row as Record<string, unknown>)['代码'];
return typeof code === 'string' && code.trim().length > 0;
}
function round(value: number): number {
return Math.round(value * 100) / 100;
}
export function mapLimitUpPool(rows: readonly AktoolsRawRow[]): LimitUpSummary {
const validRows = rows.filter((row) => hasCode(row) && toPositiveInteger(row['连板数']) !== null);
const tierCounts = new Map<number, number>();
const industryCounts = new Map<string, number>();
let sealFunds = 0;
for (const row of validRows) {
const board = toPositiveInteger(row['连板数']) as number;
tierCounts.set(board, (tierCounts.get(board) ?? 0) + 1);
const industry = typeof row['所属行业'] === 'string' ? row['所属行业'].trim() : '';
if (industry) industryCounts.set(industry, (industryCounts.get(industry) ?? 0) + 1);
const funds = toNumber(row['封板资金']);
if (funds !== null && funds >= 0) sealFunds += funds;
}
return {
count: validRows.length,
maxBoard: Math.max(0, ...tierCounts.keys()),
tiers: [...tierCounts.entries()]
.map(([board, count]) => ({ board, count }))
.sort((left, right) => left.board - right.board),
industries: [...industryCounts.entries()]
.map(([name, count]) => ({ name, count }))
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
sealFundsAmount: round(sealFunds / 100_000_000),
};
}
export function mapBrokenBoardPool(rows: readonly unknown[]): BrokenBoardSummary {
return { count: rows.filter((row) => hasCode(row)).length };
}
export function mapPreviousLimitUpPool(rows: readonly AktoolsRawRow[]): PreviousLimitUpSummary {
const changes = rows.flatMap((row) => {
if (!hasCode(row)) return [];
const change = toNumber(row['涨跌幅']);
return change === null ? [] : [change];
});
const upCount = changes.filter((value) => value > 0).length;
const downCount = changes.filter((value) => value < 0).length;
return {
count: changes.length,
averageChangePercent: changes.length
? round(changes.reduce((sum, value) => sum + value, 0) / changes.length)
: 0,
upCount,
downCount,
flatCount: changes.length - upCount - downCount,
};
}
@@ -0,0 +1,12 @@
export type AktoolsRawRow = Readonly<Record<string, unknown>>;
export interface AktoolsDataClient {
getLimitUpPool(tradeDate: string): Promise<AktoolsRawRow[]>;
getBrokenBoardPool(tradeDate: string): Promise<AktoolsRawRow[]>;
getPreviousLimitUpPool(tradeDate: string): Promise<AktoolsRawRow[]>;
}
export interface AktoolsShortTermSource {
source: 'akshare';
sourceUpdatedAt: string;
}
@@ -3,6 +3,9 @@ import { type DynamicModule, Module } from '@nestjs/common';
import type { ApiRuntimeConfig } from '../../../../libs/config/src/runtime-config'; import type { ApiRuntimeConfig } from '../../../../libs/config/src/runtime-config';
import { MarketOverviewController } from './market-overview.controller'; import { MarketOverviewController } from './market-overview.controller';
import { MarketOverviewService, TDX_CLIENT_PORT } from './market-overview.service'; import { MarketOverviewService, TDX_CLIENT_PORT } from './market-overview.service';
import { ShortTermMarketController } from './short-term.controller';
import { AKTOOLS_CLIENT_PORT, ShortTermMarketService } from './short-term.service';
import { createAktoolsClient } from './aktools/aktools.client';
import { createTdxClient } from './tdx/tdx.client'; import { createTdxClient } from './tdx/tdx.client';
@Module({}) @Module({})
@@ -11,10 +14,12 @@ export class MarketOverviewModule {
static register(config: ApiRuntimeConfig): DynamicModule { static register(config: ApiRuntimeConfig): DynamicModule {
return { return {
module: MarketOverviewModule, module: MarketOverviewModule,
controllers: [MarketOverviewController], controllers: [MarketOverviewController, ShortTermMarketController],
providers: [ providers: [
MarketOverviewService, MarketOverviewService,
{ provide: TDX_CLIENT_PORT, useValue: createTdxClient(config.tdx) }, { provide: TDX_CLIENT_PORT, useValue: createTdxClient(config.tdx) },
ShortTermMarketService,
{ provide: AKTOOLS_CLIENT_PORT, useValue: createAktoolsClient(config.aktools) },
], ],
}; };
} }
@@ -0,0 +1,23 @@
import { Controller, Get, Inject } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ShortTermConclusionResponseDto, ShortTermEmotionResponseDto } from './short-term.dto';
import { ShortTermMarketService } from './short-term.service';
@ApiTags('market-overview-short-term')
@Controller('market-overview/short-term')
export class ShortTermMarketController {
constructor(@Inject(ShortTermMarketService) private readonly service: ShortTermMarketService) {}
@Get('emotion')
@ApiOkResponse({ type: ShortTermEmotionResponseDto })
getEmotion(): Promise<ShortTermEmotionResponseDto> {
return this.service.getEmotion();
}
@Get('conclusion')
@ApiOkResponse({ type: ShortTermConclusionResponseDto })
getConclusion(): Promise<ShortTermConclusionResponseDto> {
return this.service.getConclusion();
}
}
@@ -0,0 +1,111 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export type ShortTermStatus = 'fresh' | 'delayed' | 'partial' | 'unavailable';
export class ShortTermTierDto {
@ApiProperty({ type: Number })
board!: number;
@ApiProperty({ type: Number })
count!: number;
}
export class ShortTermIndustryDto {
@ApiProperty({ type: String })
name!: string;
@ApiProperty({ type: Number })
count!: number;
}
export class ShortTermEmotionDataDto {
@ApiProperty({ type: String, example: '20260824' })
tradeDate!: string;
@ApiPropertyOptional({ type: Number, nullable: true })
limitUpCount!: number | null;
@ApiPropertyOptional({ type: Number, nullable: true })
maxBoard!: number | null;
@ApiProperty({ type: [ShortTermTierDto] })
tiers!: ShortTermTierDto[];
@ApiPropertyOptional({ type: Number, nullable: true })
brokenBoardCount!: number | null;
@ApiPropertyOptional({ type: Number, nullable: true })
previousAverageChangePercent!: number | null;
@ApiPropertyOptional({ type: Number, nullable: true })
previousUpCount!: number | null;
@ApiPropertyOptional({ type: Number, nullable: true })
previousDownCount!: number | null;
@ApiPropertyOptional({ type: Number, nullable: true })
previousFlatCount!: number | null;
@ApiPropertyOptional({ type: Number, nullable: true, description: '封板资金,亿元' })
sealFundsAmount!: number | null;
@ApiProperty({ type: [ShortTermIndustryDto] })
industries!: ShortTermIndustryDto[];
}
export class ShortTermConclusionDataDto {
@ApiProperty({ type: String })
verdict!: string;
@ApiProperty({ type: [String] })
drivers!: string[];
@ApiProperty({ type: [String] })
risks!: string[];
}
export class ShortTermEmotionResponseDto {
@ApiProperty({ type: String })
snapshotId!: string;
@ApiProperty({ type: String, format: 'date-time' })
asOf!: string;
@ApiProperty({ type: String, enum: ['akshare'] })
source!: 'akshare';
@ApiProperty({ type: String, example: '20260824' })
tradeDate!: string;
@ApiProperty({ enum: ['fresh', 'delayed', 'partial', 'unavailable'] })
status!: ShortTermStatus;
@ApiProperty({ type: () => ShortTermEmotionDataDto, nullable: true })
data!: ShortTermEmotionDataDto | null;
@ApiProperty({ type: [String] })
warnings!: string[];
}
export class ShortTermConclusionResponseDto {
@ApiProperty({ type: String })
snapshotId!: string;
@ApiProperty({ type: String, format: 'date-time' })
asOf!: string;
@ApiProperty({ type: String, enum: ['akshare'] })
source!: 'akshare';
@ApiProperty({ type: String, example: '20260824' })
tradeDate!: string;
@ApiProperty({ enum: ['fresh', 'delayed', 'partial', 'unavailable'] })
status!: ShortTermStatus;
@ApiProperty({ type: () => ShortTermConclusionDataDto, nullable: true })
data!: ShortTermConclusionDataDto | null;
@ApiProperty({ type: [String] })
warnings!: string[];
}
@@ -0,0 +1,77 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ShortTermMarketService } from './short-term.service';
describe('短线行情服务', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-24T03:00:00.000Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('组合涨停、炸板和昨日涨停数据并返回新鲜情绪快照', async () => {
const client = {
getLimitUpPool: vi
.fn()
.mockResolvedValue([
{ 代码: '000001', 连板数: 4, 所属行业: '软件服务', 封板资金: 100_000_000 },
]),
getBrokenBoardPool: vi.fn().mockResolvedValue([{ 代码: '000002' }]),
getPreviousLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000003', 涨跌幅: 2 }]),
};
const service = new ShortTermMarketService(client);
const response = await service.getEmotion();
expect(response).toMatchObject({
source: 'akshare',
status: 'fresh',
data: {
tradeDate: '20260824',
limitUpCount: 1,
maxBoard: 4,
brokenBoardCount: 1,
previousAverageChangePercent: 2,
},
});
expect(client.getLimitUpPool).toHaveBeenCalledWith('20260824');
expect(client.getBrokenBoardPool).toHaveBeenCalledWith('20260824');
expect(client.getPreviousLimitUpPool).toHaveBeenCalledWith('20260824');
});
it('单个 AKTools 模块失败时保留其他结果并标记 partial', async () => {
const client = {
getLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000001', 连板数: 2 }]),
getBrokenBoardPool: vi.fn().mockRejectedValue(new Error('上游详情不应透传')),
getPreviousLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000003', 涨跌幅: -1 }]),
};
const service = new ShortTermMarketService(client);
const response = await service.getEmotion();
expect(response.status).toBe('partial');
expect(response.data).toMatchObject({ limitUpCount: 1, previousAverageChangePercent: -1 });
expect(response.warnings.join(' ')).toContain('炸板池');
expect(response.warnings.join(' ')).not.toContain('上游详情');
});
it('根据结构化指标生成保守的偏强结论', async () => {
const client = {
getLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000001', 连板数: 4 }]),
getBrokenBoardPool: vi.fn().mockResolvedValue([]),
getPreviousLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000003', 涨跌幅: 2 }]),
};
const service = new ShortTermMarketService(client);
const response = await service.getConclusion();
expect(response).toMatchObject({
source: 'akshare',
status: 'fresh',
data: { verdict: '超短情绪偏强' },
});
});
});
@@ -0,0 +1,148 @@
import { Inject, Injectable } from '@nestjs/common';
import {
mapBrokenBoardPool,
mapLimitUpPool,
mapPreviousLimitUpPool,
type BrokenBoardSummary,
type LimitUpSummary,
type PreviousLimitUpSummary,
} from './aktools/aktools.mapper';
import { AktoolsClientError } from './aktools/aktools.client';
import type { AktoolsDataClient, AktoolsRawRow } from './aktools/aktools.types';
import {
type ShortTermConclusionDataDto,
type ShortTermConclusionResponseDto,
type ShortTermEmotionDataDto,
type ShortTermEmotionResponseDto,
} from './short-term.dto';
export const AKTOOLS_CLIENT_PORT = Symbol('AKTOOLS_CLIENT_PORT');
function getShanghaiDate(date: Date): string {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(date);
const values = Object.fromEntries(parts.map((part) => [part.type, part.value]));
return `${values.year}${values.month}${values.day}`;
}
function createMeta(tradeDate: string) {
const asOf = new Date().toISOString();
return {
snapshotId: `${asOf.slice(0, 10).replaceAll('-', '')}-${asOf.slice(11, 19).replaceAll(':', '')}`,
asOf,
source: 'akshare' as const,
tradeDate,
};
}
function warningFor(error: unknown, subject: string): string {
if (error instanceof AktoolsClientError && error.kind === 'timeout') {
return `${subject}请求超时`;
}
if (error instanceof AktoolsClientError && error.kind === 'disabled') {
return 'AKTools 未启用';
}
return `${subject}数据不可用`;
}
function toEmotionData(
tradeDate: string,
limitUp: LimitUpSummary | null,
brokenBoard: BrokenBoardSummary | null,
previousLimitUp: PreviousLimitUpSummary | null,
): ShortTermEmotionDataDto {
return {
tradeDate,
limitUpCount: limitUp?.count ?? null,
maxBoard: limitUp?.maxBoard ?? null,
tiers: limitUp?.tiers ?? [],
brokenBoardCount: brokenBoard?.count ?? null,
previousAverageChangePercent: previousLimitUp?.averageChangePercent ?? null,
previousUpCount: previousLimitUp?.upCount ?? null,
previousDownCount: previousLimitUp?.downCount ?? null,
previousFlatCount: previousLimitUp?.flatCount ?? null,
sealFundsAmount: limitUp?.sealFundsAmount ?? null,
industries: limitUp?.industries ?? [],
};
}
function createConclusion(data: ShortTermEmotionDataDto): ShortTermConclusionDataDto {
const strong =
(data.maxBoard ?? 0) >= 4 &&
(data.previousAverageChangePercent ?? Number.NEGATIVE_INFINITY) > 0 &&
(data.brokenBoardCount ?? 0) <= (data.limitUpCount ?? 0);
const weak =
(data.previousAverageChangePercent ?? 0) < 0 ||
(data.brokenBoardCount ?? 0) > (data.limitUpCount ?? 0);
const verdict = strong ? '超短情绪偏强' : weak ? '超短情绪偏弱' : '超短情绪分化';
return {
verdict,
drivers: [
data.limitUpCount === null ? '涨停池数据缺失' : `涨停 ${data.limitUpCount} 家`,
data.maxBoard === null ? '最高连板数据缺失' : `最高连板 ${data.maxBoard} 板`,
data.previousAverageChangePercent === null
? '昨日涨停表现缺失'
: `昨日涨停股平均涨跌幅 ${data.previousAverageChangePercent}%`,
],
risks: [
data.brokenBoardCount === null ? '炸板池数据缺失' : `炸板 ${data.brokenBoardCount} 家`,
'结论仅为规则统计,不构成交易建议',
],
};
}
@Injectable()
export class ShortTermMarketService {
constructor(@Inject(AKTOOLS_CLIENT_PORT) private readonly aktoolsClient: AktoolsDataClient) {}
async getEmotion(): Promise<ShortTermEmotionResponseDto> {
const tradeDate = getShanghaiDate(new Date());
const meta = createMeta(tradeDate);
const [limitUp, brokenBoard, previousLimitUp] = await Promise.allSettled([
this.aktoolsClient.getLimitUpPool(tradeDate),
this.aktoolsClient.getBrokenBoardPool(tradeDate),
this.aktoolsClient.getPreviousLimitUpPool(tradeDate),
]);
const limitUpData = this.readResult(limitUp, mapLimitUpPool);
const brokenBoardData = this.readResult(brokenBoard, mapBrokenBoardPool);
const previousLimitUpData = this.readResult(previousLimitUp, mapPreviousLimitUpPool);
const failures = [
[limitUp, '涨停池'],
[brokenBoard, '炸板池'],
[previousLimitUp, '昨日涨停池'],
] as const;
const warnings = failures.flatMap(([result, subject]) =>
result.status === 'rejected' ? [warningFor(result.reason, subject)] : [],
);
const data =
limitUpData || brokenBoardData || previousLimitUpData
? toEmotionData(tradeDate, limitUpData, brokenBoardData, previousLimitUpData)
: null;
return {
...meta,
status: data === null ? 'unavailable' : warnings.length ? 'partial' : 'fresh',
data,
warnings,
};
}
async getConclusion(): Promise<ShortTermConclusionResponseDto> {
const emotion = await this.getEmotion();
return {
...emotion,
data: emotion.data ? createConclusion(emotion.data) : null,
};
}
private readResult<T>(
result: PromiseSettledResult<AktoolsRawRow[]>,
mapper: (rows: readonly AktoolsRawRow[]) => T,
): T | null {
return result.status === 'fulfilled' ? mapper(result.value) : null;
}
}
@@ -1,7 +1,7 @@
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import request from 'supertest'; import request from 'supertest';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MarketOverviewController } from '../src/market-overview/market-overview.controller'; import { MarketOverviewController } from '../src/market-overview/market-overview.controller';
import { import {
@@ -12,9 +12,19 @@ import {
describe('市场概览 HTTP API', () => { describe('市场概览 HTTP API', () => {
let app: INestApplication | undefined; let app: INestApplication | undefined;
beforeEach(() => {
// 固定到夹具交易日,避免测试随自然日期变化把正确的 delayed 响应误判为失败。
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-18T03:00:00.000Z'));
});
afterEach(async () => { afterEach(async () => {
await app?.close(); try {
app = undefined; await app?.close();
} finally {
app = undefined;
vi.useRealTimers();
}
}); });
it('按模块返回指数和 TDX 来源元数据', async () => { it('按模块返回指数和 TDX 来源元数据', async () => {
@@ -0,0 +1,77 @@
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ShortTermMarketController } from '../src/market-overview/short-term.controller';
import {
AKTOOLS_CLIENT_PORT,
ShortTermMarketService,
} from '../src/market-overview/short-term.service';
describe('短线行情 HTTP API', () => {
let app: INestApplication | undefined;
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-08-24T03:00:00.000Z'));
});
afterEach(async () => {
try {
await app?.close();
} finally {
app = undefined;
vi.useRealTimers();
}
});
it('返回结构化超短情绪指标和 AKShare 来源', async () => {
const client = {
getLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000001', 连板数: 4 }]),
getBrokenBoardPool: vi.fn().mockResolvedValue([{ 代码: '000002' }]),
getPreviousLimitUpPool: vi.fn().mockResolvedValue([{ 代码: '000003', 涨跌幅: 1.5 }]),
};
const moduleRef = await Test.createTestingModule({
controllers: [ShortTermMarketController],
providers: [ShortTermMarketService, { provide: AKTOOLS_CLIENT_PORT, useValue: client }],
}).compile();
app = moduleRef.createNestApplication();
app.setGlobalPrefix('api/v1');
await app.init();
const response = await request(app.getHttpServer())
.get('/api/v1/market-overview/short-term/emotion')
.expect(200);
expect(response.body).toMatchObject({
source: 'akshare',
status: 'fresh',
tradeDate: '20260824',
data: { limitUpCount: 1, maxBoard: 4, brokenBoardCount: 1 },
});
expect(JSON.stringify(response.body)).not.toContain('127.0.0.1');
});
it('全部 AKTools 数据不可用时返回 unavailable 而不是伪造空数据', async () => {
const client = {
getLimitUpPool: vi.fn().mockRejectedValue(new Error('private detail')),
getBrokenBoardPool: vi.fn().mockRejectedValue(new Error('private detail')),
getPreviousLimitUpPool: vi.fn().mockRejectedValue(new Error('private detail')),
};
const moduleRef = await Test.createTestingModule({
controllers: [ShortTermMarketController],
providers: [ShortTermMarketService, { provide: AKTOOLS_CLIENT_PORT, useValue: client }],
}).compile();
app = moduleRef.createNestApplication();
app.setGlobalPrefix('api/v1');
await app.init();
const response = await request(app.getHttpServer())
.get('/api/v1/market-overview/short-term/conclusion')
.expect(200);
expect(response.body).toMatchObject({ source: 'akshare', status: 'unavailable', data: null });
expect(JSON.stringify(response.body)).not.toContain('private detail');
});
});
@@ -36,6 +36,12 @@ const disabledAiEnv = {
AI_TIMEOUT_MS: '30000', AI_TIMEOUT_MS: '30000',
}; };
const enabledAktoolsEnv = {
AKTOOLS_ENABLED: 'true',
AKTOOLS_BASE_URL: 'http://127.0.0.1:8080',
AKTOOLS_TIMEOUT_MS: '10000',
};
const disabledRocketMqEnv = { const disabledRocketMqEnv = {
ROCKETMQ_ENABLED: 'false', ROCKETMQ_ENABLED: 'false',
ROCKETMQ_NAMESRV_ADDR: '127.0.0.1:9876', ROCKETMQ_NAMESRV_ADDR: '127.0.0.1:9876',
@@ -44,6 +50,27 @@ const disabledRocketMqEnv = {
}; };
describe('进程级运行配置', () => { describe('进程级运行配置', () => {
it('API 接受仅限本机 8080 的 AKTools 配置', () => {
const config = loadApiConfig({
APP_ENV: 'development',
LOG_LEVEL: 'debug',
API_HOST: '127.0.0.1',
API_PORT: '3000',
TDX_BASE_URL: 'http://127.0.0.1:17710',
TDX_TIMEOUT_MS: '5000',
...baseRedisEnv,
...disabledMysqlEnv,
...disabledAiEnv,
...enabledAktoolsEnv,
});
expect(config.aktools).toEqual({
enabled: true,
baseUrl: 'http://127.0.0.1:8080',
timeoutMs: 10000,
});
});
it('API 只返回自己的配置且不要求 Scheduler 或 Worker 字段', () => { it('API 只返回自己的配置且不要求 Scheduler 或 Worker 字段', () => {
const config = loadApiConfig({ const config = loadApiConfig({
APP_ENV: 'development', APP_ENV: 'development',

Some files were not shown because too many files have changed in this diff Show More