{"id":49262272,"url":"https://github.com/simplerjiang/baostock.net","last_synced_at":"2026-04-29T12:00:46.905Z","repository":{"id":353287959,"uuid":"1218761602","full_name":"simplerjiang/baostock.NET","owner":"simplerjiang","description":"python baostock库的.Net版本，纯原生.Net，并提供nuget下载","archived":false,"fork":false,"pushed_at":"2026-04-27T09:37:25.000Z","size":702,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-27T10:02:52.750Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simplerjiang.png","metadata":{"files":{"readme":"README.UserAgentTest.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-23T07:33:46.000Z","updated_at":"2026-04-27T09:37:29.000Z","dependencies_parsed_at":null,"dependency_job_id":"8535537d-39bd-404b-8e7c-1c107a592071","html_url":"https://github.com/simplerjiang/baostock.NET","commit_stats":null,"previous_names":["simplerjiang/baostock.net"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/simplerjiang/baostock.NET","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplerjiang%2Fbaostock.NET","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplerjiang%2Fbaostock.NET/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplerjiang%2Fbaostock.NET/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplerjiang%2Fbaostock.NET/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simplerjiang","download_url":"https://codeload.github.com/simplerjiang/baostock.NET/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simplerjiang%2Fbaostock.NET/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32377599,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T09:24:15.638Z","status":"ssl_error","status_checked_at":"2026-04-28T09:24:15.071Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-25T08:01:42.661Z","updated_at":"2026-04-29T12:00:46.856Z","avatar_url":"https://github.com/simplerjiang.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Baostock.NET 用户验收测试手册\n\n\u003e 版本：v1.3.1（含 I6/I7/I8 交易所覆盖硬性用例）\n\u003e 最近更新：2026-04-24\n\n## 适用范围\n\n本手册供 **User Representative Agent / 真实交易员** 在 v1.2.0 已交付能力上做人工复测。\n覆盖：\n\n- 28 个 baostock TCP query API（`/api/baostock/*`）\n- 3 个多源 hedge HTTP API（`/api/multi/*`）\n- TestUI 前端（API 调用 Tab + 压测面板）\n- `POST /api/loadtest/run` 进程内压测器\n\n不涉及：单元测试 / 集成测试（已由 CI 跑过 267/0/2）。本手册聚焦**端到端浏览器实操**与**对外可观察行为**。\n\n---\n\n## 启动准备\n\n### 1) 释放占用端口（5050）\n\n```powershell\nGet-Process dotnet -ErrorAction SilentlyContinue | Stop-Process -Force\n```\n\n\u003e 注意：会强杀所有 `dotnet` 进程。如本机有其它 .NET 服务正在跑，请改用按 PID kill。\n\n### 2) 启动 TestUI\n\n```powershell\ncd C:\\Users\\kong\\baostock.Net\ndotnet run --no-build -c Release --project src/Baostock.NET.TestUI\n```\n\n启动成功标志：\n\n```\nNow listening on: http://localhost:5050\nApplication started. Press Ctrl+C to shut down.\n```\n\n浏览器打开 \u003chttp://localhost:5050\u003e。顶部应显示 “未登录” 状态条 + 两个 Tab（API 调用 / 压测面板）。\n\n### 3) 健康检查\n\n```powershell\nInvoke-WebRequest -UseBasicParsing http://localhost:5050/api/session/status\n```\n\n期望：HTTP 200，body `{\"isLoggedIn\":false,...}`。\n\n---\n\n## 已知约束（先看完再测，避免踩坑）\n\n### A. baostock TCP 端点不可并发压测\n\n`BaostockClient` 是**单例 + 单条共享 TCP 长连接**，协议非线程安全。\n`concurrency \u003e 1` 会击毙会话，需重启进程才能恢复。\n\n- 后端 `/api/loadtest/run` 已对 `targetPath = /api/baostock/*` 做硬拦截：\n  - `concurrency \u003e 1` → HTTP 400\n  - `concurrency = 1` 但 `totalRequests \u003e 200` 或 `durationSeconds \u003e 30s` → HTTP 400\n- 前端压测面板选中 TCP 端点时，concurrency 输入框被锁到 max=1，会显示 `[TCP] concurrency locked to 1` 提示。\n- v1.3.0 计划：`BaostockClient` TCP 自愈（B1，Sprint 3 P0）。\n\n**本手册的压测剧本只对 `/api/multi/*` 端点跑高并发。**\n\n### B. 北交所（BJ）数据可能陈旧\n\n- BJ K 线公网双源（EastMoney `secid=116.{c}` / Tencent `fqkline`）当前都不可用。\n  `GetHistoryKLineAsync(\"BJ430047\", ...)` 会抛 `AllSourcesFailedException`。\n- BJ 实时只能拿到 Tencent 的盘前价（北交所流动性差，Sina 经常返回 all-zero）。\n- 验收时若拿到的 BJ 实时 `timestamp` 早于今天 09:00，请记为 **“BJ 数据陈旧”已知问题**，不要标 blocker。\n\n### C. 登录态 vs 实际 socket 状态可能脱节\n\nv1.2.0-preview3 已知缺陷：socket 死后 `/api/session/login` 仍返回 `ok=true`，但实际 baostock query 会抛 `IOException`。\n处理：遇到 `IOException` 风暴时，**Stop-Process 释放后重启服务**。\n\n\u003e **v1.2.0-preview5 起**：`BaostockClient` 已内置 TCP 自愈（socket 半死自动重连 + relogin 一次）。`/api/session/status` 新增 `isSocketConnected` 字段可直观看到底层连接健康。一般不再需要重启服务；若仍连续抛 `reconnect_failed` / `relogin_failed`，说明上游 baostock 服务器不可达，才需人工介入。\n\n\u003e **logout 后 `isSocketConnected=true` 是正常的**：logout 只告诉 baostock “这个会话结束”，不会主动拆掉 TCP 连接（避免下次重连延迟）。所以 `{isLoggedIn:false, isSocketConnected:true}` 是**预期状态**。要真正断开 TCP，`Dispose` 整个 `BaostockClient` 实例即可。\n\n### D. PowerShell 直接调用 baostock 端点须显式指定日期范围\n\n**PowerShell 直接调用 baostock 端点不带 `startDate`/`endDate` 时，后端会回退到 baostock 协议默认值**（如 `startDate=2015-01-01`），导致返回数据量爆炸（实测 `/api/baostock/metadata/trade-dates` 不传日期返回 **4132 行** 而非预期 15 行）。建议每次 PowerShell 调用都显式指定日期范围（例如 `-Body '{\"startDate\":\"2026-03-25\",\"endDate\":\"2026-04-24\"}'`）。UI 已用动态日期默认值（今天前 30 天~今天）避免此问题。\n\n---\n\n## 测试模块\n\n\u003e 每个模块的步骤都假定 “已 Login 成功”（用户名 `anonymous`，密码 `123456`，匿名权限够覆盖本手册全部场景）。\n\n### 模块 A — 早盘日历 + 茅台基础信息\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| A1 | 选 `metadata / QueryTradeDatesAsync`，使用默认 startDate（今天-30 天）/ endDate（今天），点 Send | `ok=true`，`rowCount \u003e 0`，data 含 30 条左右记录，`is_trading_day` 字段有 0/1 | 若 `rowCount=0` → 检查日期是否落在节假日全段；若 `ok=false IOException` → socket 死，重启服务 |\n| A2 | 选 `metadata / QueryStockBasicAsync`，code=`SH600519`，Send | `ok=true rowCount=1`，data[0].code_name 含 “贵州茅台” | 若返回 0 行 → 报告为 blocker（基础接口失效） |\n\n### 模块 B — 历史 K 线（多源）\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| B1 | 选 `multi / GetHistoryKLineAsync`，code=`SH600519`，frequency=`Day`，startDate=今天-60 天，endDate=今天，adjust=`PreAdjust`，Send | `ok=true`，`rowCount` ≈ 40，`source` 显示 `EastMoney` 或 `Tencent`，最后一条 close 价合理（约 1300~1700） | 若 `source=Tencent` 但全部行 `turnoverRate=null` → 正常（Tencent 不返回换手率）；若 EM/Tencent 都失败 → blocker |\n| B2 | 同上但 code=`BJ430047` | **预期失败** `ok=false errorType=AllSourcesFailedException` | 若反而成功 → 报 BJ K 线源已恢复（更新已知问题清单） |\n\n### 模块 C — 实时报价\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| C1 | 选 `multi / GetRealtimeQuoteAsync`，code=`SH600519`，Send | `ok=true rowCount=1`，data.source 多为 `Sina`，`lastPrice` \u003e 0 | source 全部走 EM 源 → 报 hedge 退化（minor） |\n| C2 | 选 `multi / GetRealtimeQuotesAsync`，codes 改为 `[\"SH600519\",\"SZ000001\",\"BJ430047\"]`，Send | `ok=true rowCount=3`，前 2 条 source=Sina/Tencent，BJ 那条 source 多为 Tencent | BJ 拿不到 → 检查 timestamp，若早于今天则记 “BJ 数据陈旧” |\n| C3 | 选 `multi / GetRealtimeQuotesAsync`，codes 字段**清空**为 `[]`，Send | `ok=false`，`error` 包含 `codes is required and must be non-empty`（ArgumentException 实际返回附带 ` (Parameter 'codes')` 后缀，使用 Contains 语义校验；**不允许回退到默认股票**） | 若返回 ok=true 数据 → blocker（产品级错误：用户拿到不是自己请求的数据） |\n\n### 模块 D — 财务报表（季频）\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| D1 | 选 `evaluation / QueryProfitDataAsync`，code=`SH600519`，year=`2024`，quarter=`1`，Send | `ok=true rowCount=1`，roeAvg \u003e 0.05 | rowCount=0 → baostock 该季度数据未发布；rowCount\u003e1 → 异常 |\n| D2 | 默认 year/quarter（应为 当前年/上一完整季度），Send | `ok=true` 或 `rowCount=0`（若财报未发布）；不应返回 2023/Q4 这种过时默认值 | 若默认值仍是 2023 → metadata 动态化失败，blocker |\n\n### 模块 E — 压测基线（仅 multi）\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| E1 | 切到 “压测面板”，Target 选 `multi / GetRealtimeQuoteAsync`，Mode=`count`，TotalRequests=`30`，Concurrency=`3`，Warmup=`2`，开始压测 | `ok=true`，QPS ≥ 5，错误率 ≤ 10%，p95 \u003c 2000ms | 错误率 \u003e 30% → 数据源不稳定（minor，记入报告） |\n| E2 | 切到 TCP 端点（如 `metadata / QueryTradeDatesAsync`） | UI 应自动锁 concurrency=1，TotalRequests 默认改 50，duration max=30s，banner 出现 `⚠️ TCP 端点：concurrency 锁定 1, total≤200, duration≤30s` | 锁定失效 → minor（后端会兜底拦截） |\n\n### 模块 F — 边界拒绝（必须全部 400/拒绝）\n\n| # | 操作 | 期望 |\n|---|---|---|\n| F1 | 直接 curl `POST /api/loadtest/run`，body `{\"targetPath\":\"/api/baostock/metadata/trade-dates\",\"concurrency\":2,\"mode\":\"count\",\"totalRequests\":10}` | HTTP 400 + `error` 含 `concurrency \u003e 1 ... non-thread-safe` |\n| F2 | 直接 curl，body `{\"targetPath\":\"/api/baostock/metadata/trade-dates\",\"concurrency\":1,\"mode\":\"count\",\"totalRequests\":300}` | HTTP 400 + `error` 含 `heavy load (\u003e200 requests or \u003e30s duration)` |\n| F3 | 同时双开两个压测（连续两次 POST `/api/loadtest/run`，第二个不等第一个完成） | 第二个返回 HTTP 409 `another load test is running` |\n| F4 | curl `POST /api/loadtest/run` body `{\"targetPath\":\"...\",\"concurrency\":200,...}` | HTTP 400 `concurrency must be 1..100` |\n\n### 模块 H — 财报三表（v1.3.0 新增，HTTP 多源对冲）\n\n\u003e 硬规则：**至少 2 轮 UR 验证**（第二轮建议换一只银行 / 证券股，例如 `SZ000001` / `SH601398` / `SH600030`，触发 `CompanyType` 自动嗅探）。\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| H1 | 左侧 sidebar 切到 `financial` 分组，选 `QueryFullBalanceSheetAsync`，code=`SH600519`，其余默认（`reportDates` 留空、`dateType=ByReport`、`reportKind=Cumulative`、`companyType=Auto`），点 **Send** | `ok=true`，`rowCount ≥ 4`（近几年 4~8 份报告），每条含 `reportDate` / `totalAssets` / `totalLiabilities` / `totalEquity` 非 null，`source` 为 `EastMoney` 或 `Sina` | 超时或返回 0 行 → 直接记 bug，**不 retry**；若仅 `source=Sina` 全覆盖 → 记 hedge 退化（minor） |\n| H2 | 选 `QueryFullIncomeStatementAsync`，code=`SH600519`，`reportDates`=`2024-12-31,2024-09-30`（逗号分隔），Send | `ok=true rowCount=2`，两条分别对应年报 / 三季报，`totalOperateIncome` / `netProfit` / `parentNetProfit` 非 null；`reportTitle` 含 \"年报\" / \"三季报\" 字样 | reportDates 未生效（返回 4+ 条）→ 记 bug |\n| H3 | 选 `QueryFullCashFlowAsync`，code=`SZ000001`（平安银行，银行类）—— **触发公司类型差异**，Send | `ok=true rowCount ≥ 1`；允许大量字段为 null（银行现金流量表结构与一般工商业差异大），但 `rawFields` 字典不为空，`netcashOperate` 通常有值 | `rawFields` 也为空 → 记 bug（解析失败） |\n| H4 | 性能观察：任一上述端点 Send，**首次** ≤ 10s（含对冲 + 500ms hedge 间隔 + 冷启动），**后续同 code** ≤ 3s | 首次 timeout（\u003e 10s 但仍 ok=true）仅记 minor | 持续 timeout 或 `errorType=AllSourcesFailedException` → blocker |\n\n### 模块 I — 巨潮公告 + PDF 下载（v1.3.0 新增，单源）\n\n\u003e 硬规则：**至少 2 轮 UR 验证**（第二轮换 `SZ000001` + `category=SemiAnnualReport`，验证 `column` 参数按交易所切换正确）。\n\n| # | 操作 | 期望 | 失败分流 |\n|---|---|---|---|\n| I1 | 左侧 sidebar 切到 `cninfo` 分组，选 `QueryAnnouncementsAsync`，code=`SH600519`，`category=AnnualReport`，`startDate=2024-01-01`，`endDate` 留空 / 今天，`pageNum=1`，`pageSize=30`，Send | `ok=true rowCount ≥ 1`，`data[]` 每条含 `announcementId` / `title`（含 \"年报\"）/ `publishDate` / `adjunctUrl`（以 `finalpage/` 之类路径开头）/ `fullPdfUrl`（以 `http://static.cninfo.com.cn/` 开头） | 列表为空（rowCount=0）→ 记 bug（贵州茅台近年必有年报） |\n| I2 | 前端自动在 I1 的结果旁渲染每一行的 **下载链接**（指向 `/api/cninfo/pdf-download?adjunctUrl=...`）。点击第一条的下载链接 | 浏览器开始下载 PDF，文件名以 `.pdf` 结尾 | 下载链接没渲染 → 记 minor（前端 bug，后端可用 curl 直连）；点击后 HTTP 502 `cninfo pdf download failed` → 检查网络到 `static.cninfo.com.cn`，连续 3 次失败记 blocker |\n| I3 | 下载完成后用 PDF reader（Edge / Acrobat）打开文件 | 文件大小 **\u003e 100KB**（年报一般 1MB+），正文可阅读，无乱码 | 文件大小 \u003c 10KB 或打不开 → blocker（极可能下到 HTML 错误页） |\n| I4 | 分类筛选验证：改 `category=SemiAnnualReport`，其余同 I1 | 返回的 `title` 全部含 \"半年度报告\" 或 \"半年报\" | 出现其他类型 → 记 bug（分类参数没生效） |\n| I5 | 失败分流演练：code=`SZ000001`，`category=QuarterlyReport`，`startDate=2024-01-01`，Send | `ok=true`，rowCount ≥ 1；若为 0 → 按预期行为处理（某些公司季报不一定全披露），不算 bug | — |\n\n### I6 · 创业板覆盖（硬性）\n\n**目的**：防 Bug-N-03 回归（v1.2 曾发生创业板 announcements 静默返回 0 行）。\n\n- 代码：`SZ300750`（宁德时代）\n- category：`AnnualReport`\n- 时间段：`2024-01-01 ~ 2025-12-31`\n- 期望：`rowCount ≥ 1`，至少 1 条 title 含“年度报告”\n- 失败分流：rowCount=0 → **直接 Blocker**，不要 retry，不要“以为该公司没发年报”\n\n### I7 · 科创板覆盖（硬性）\n\n- 代码：`SH688981`（中芯国际）\n- category：`AnnualReport`\n- 时间段：`2024-01-01 ~ 2025-12-31`\n- 期望：`rowCount ≥ 1`，至少 1 条 title 含“中芯国际”\n- 失败分流：同 I6\n\n### I8 · 北交所覆盖（硬性）\n\n- 代码：`BJ430047`（诺思兰德）或任一北交所活跃公司\n- category：`All`（北交所公司公告少，分类限制会频繁 0 行）\n- 时间段：`2025-01-01 ~ 2025-12-31`\n- 期望：`rowCount ≥ 1`（北交所公司总有定期公告）\n- 失败分流：rowCount=0 → 换另一家北交所活跃公司（如 `BJ838924` / `BJ835174`），若仍 0 → Blocker\n\n### Part G — 健康态快速自检（启动后 smoke test）\n\n五步 curl 序列，3 秒完成，用于每次启动服务后快速确认健康：\n\n```powershell\n# 1. 未登录状态\nInvoke-WebRequest -UseBasicParsing http://localhost:5050/api/session/status\n# 期望：{isLoggedIn:false, isSocketConnected:false}\n\n# 2. Login\n$login = Invoke-WebRequest -UseBasicParsing -Method Post -ContentType 'application/json' -Body '{}' http://localhost:5050/api/session/login\n# 期望：ok=true\n\n# 3. baostock query（会触发 TCP 连接）\n$tradeDates = Invoke-WebRequest -UseBasicParsing -Method Post -ContentType 'application/json' -Body '{\"startDate\":\"2026-01-01\",\"endDate\":\"2026-01-10\"}' http://localhost:5050/api/baostock/metadata/trade-dates\n# 期望：ok=true, rowCount\u003e0\n\n# 4. 状态确认（现在两者都应 true）\nInvoke-WebRequest -UseBasicParsing http://localhost:5050/api/session/status\n# 期望：{isLoggedIn:true, isSocketConnected:true}\n\n# 5. Logout（TCP 不拆，仅登出会话）\nInvoke-WebRequest -UseBasicParsing -Method Post -ContentType 'application/json' -Body '{}' http://localhost:5050/api/session/logout\n# 期望：ok=true；后续 /api/session/status 应 {isLoggedIn:false, isSocketConnected:true}（TCP 保留）\n```\n\n---\n\n## 失败分流（Triage）\n\n| 现象 | 分级 | 处理 |\n|---|---|---|\n| `IOException` 风暴 + 后续所有 baostock 请求都失败 | **blocker** | Stop-Process + 重启，记录前置压测/操作 |\n| `multi/*` 全部 source=Sina 且其它源都缺 | **major** | 标 “hedge 退化未触发”，提 issue 附时间窗 |\n| `multi/*` 某源 5xx / parse error 单次出现 | **minor** | 记数，不阻塞 |\n| BJ 数据 timestamp 早于今天 09:00 | **minor (已知)** | 记 “BJ 数据陈旧已知问题” |\n| metadata 默认日期是 2024-01-01 等过时值 | **blocker** | N1 修复回退，立即报 |\n| `/api/multi/realtime-quotes` 空 codes 返回 ok=true | **blocker** | M1 修复回退，立即报 |\n| 前端 TCP 端点未自动锁 concurrency | **minor** | M3 前端兜底失效，但后端 B2 仍会拦 |\n| 后端 baostock concurrency=2 未拦截 | **blocker** | B2 修复回退 |\n\n---\n\n## 改进建议提交格式\n\nUR 验收完后请用此结构产出报告（建议直接以 `docs/UR-Acceptance-{date}.md` 提交 PR）：\n\n```\n## 摘要\n- 通过 / 全部模块数：X/6\n- 阻塞问题数：N\n- 高优问题数：N\n- 低优问题数：N\n\n## Blocker（阻塞）\n### B-001 简短标题\n- 复现步骤：1. ... 2. ...\n- 期望：...\n- 实际：...\n- 截图/响应片段：...\n\n## Major（高优）\n（同上结构，编号 M-xxx）\n\n## Minor（低优）\n（同上结构，编号 N-xxx）\n\n## 数据样本\n- 测试时间窗：YYYY-MM-DD HH:MM~HH:MM\n- 命中股票：SH600519 / SZ000001 / BJ430047\n- baostock socket 是否曾死：是 / 否\n```\n\n---\n\n## 历史已知问题列表（v1.2.0-preview3 验收发现）\n\n| ID | 描述 | 状态 |\n|---|---|---|\n| **B1** | `BaostockClient` TCP socket 死后无法自愈，登录态与实际 socket 状态脱节 | ✅ v1.2.0-preview5（Sprint 3 P0）已修 |\n| B2 | `/api/loadtest/run` 对 baostock TCP 端点未拦截 concurrency\u003e1 / heavy load | ✅ Sprint 2.5 批 3 已修 |\n| M1 | `/api/multi/realtime-quotes` 空 codes 静默回退到默认股票 | ✅ Sprint 2.5 批 3 已修 |\n| M2 | `/api/meta/endpoints` metadata 缺少 `protocol` 字段 | ✅ Sprint 2.5 批 3 已修 |\n| M3 | 前端无 protocol 徽章 / 警告 banner / TCP concurrency 锁 | ✅ Sprint 2.5 批 3 已修 |\n| N1 | metadata 默认日期硬编码 2024-xx-xx，跨年/跨季后陈旧 | ✅ Sprint 2.5 批 3 已修（每次 GET /api/meta/endpoints 重算） |\n| N2..N7 | 其它低优体验问题 | 列入 v1.2.1 |\n\n---\n\n## v1.3.3 修复回归（必跑）\n\n\u003e 修复 v1.3.2 三项契约缺陷：(1) 3 个 internal endpoint meta 谎报 POST；(2) `multi/*` 三端点 `sources` 字段全空；(3) `pdf-download` Range 不合规。\n\n### V3-1 · meta 自洽（method=GET）\n\n```powershell\n$meta = (Invoke-WebRequest -UseBasicParsing -Uri http://localhost:5050/api/meta/endpoints).Content | ConvertFrom-Json\n$meta | Where-Object { $_.path -in '/api/session/status','/api/meta/endpoints','/api/loadtest/list-targets' } | Select-Object path, method\n```\n\n期望：3 行全部 `method = GET`。任何一行为 POST → blocker。\n\n### V3-2 · multi/* sources 字段非空\n\n```powershell\n$body = '{\"code\":\"SH600519\"}'\n(Invoke-WebRequest -UseBasicParsing -Method POST -ContentType 'application/json' -Body $body `\n    -Uri http://localhost:5050/api/multi/realtime-quote).Content | ConvertFrom-Json | Select-Object ok, sources\n\n$body2 = '{\"codes\":\"SH600519,SZ000001\"}'\n(Invoke-WebRequest -UseBasicParsing -Method POST -ContentType 'application/json' -Body $body2 `\n    -Uri http://localhost:5050/api/multi/realtime-quotes).Content | ConvertFrom-Json | Select-Object ok, sources\n\n$body3 = '{\"code\":\"SH600519\",\"startDate\":\"2024-12-01\",\"endDate\":\"2024-12-31\"}'\n(Invoke-WebRequest -UseBasicParsing -Method POST -ContentType 'application/json' -Body $body3 `\n    -Uri http://localhost:5050/api/multi/history-k-line).Content | ConvertFrom-Json | Select-Object ok, sources\n```\n\n期望：三次响应 `sources` 都不为空数组（至少 1 个源名，如 `Sina` / `Tencent` / `EastMoney`）。任一为空 → blocker。\n\n### V3-3 · pdf-download Range RFC 7233 合规\n\n\u003e 任选一份真实 cninfo PDF（参考 Part I 拿 adjunctUrl）。\n\n```powershell\n$adjunct = 'finalpage/2024-12-28/1222168020.PDF'   # 替换为真实存在的\n$url = \"http://localhost:5050/api/cninfo/pdf-download?adjunctUrl=$adjunct\"\n\n# (a) 完整下载，拿 total + Accept-Ranges\n$full = Invoke-WebRequest -UseBasicParsing -Uri $url\n$total = $full.Content.Length\n\"FULL: status=$($full.StatusCode), bytes=$total, Accept-Ranges=$($full.Headers['Accept-Ranges'])\"\n# 期望：200 + Accept-Ranges = bytes\n\n# (b) bytes=0-999 → 206 + 1000 字节 + Content-Range: bytes 0-999/$total\n$r1 = Invoke-WebRequest -UseBasicParsing -Uri $url -Headers @{ Range = 'bytes=0-999' }\n\"0-999: status=$($r1.StatusCode), bytes=$($r1.Content.Length), Content-Range=$($r1.Headers['Content-Range'])\"\n\n# (c) bytes=100-199 → 206 + 100 字节 + Content-Range: bytes 100-199/$total\n$r2 = Invoke-WebRequest -UseBasicParsing -Uri $url -Headers @{ Range = 'bytes=100-199' }\n\"100-199: status=$($r2.StatusCode), bytes=$($r2.Content.Length), Content-Range=$($r2.Headers['Content-Range'])\"\n\n# (d) bytes=1000- → 206 + (total-1000) 字节 + Content-Range: bytes 1000-(total-1)/$total\n$r3 = Invoke-WebRequest -UseBasicParsing -Uri $url -Headers @{ Range = 'bytes=1000-' }\n\"1000-: status=$($r3.StatusCode), bytes=$($r3.Content.Length), Content-Range=$($r3.Headers['Content-Range'])\"\n\n# (e) bytes=-100 → 416 Range Not Satisfiable\ntry {\n    $r4 = Invoke-WebRequest -UseBasicParsing -Uri $url -Headers @{ Range = 'bytes=-100' }\n    \"-100: status=$($r4.StatusCode) (期望 416 但成功了 → blocker)\"\n} catch {\n    \"-100: 期望异常 → $($_.Exception.Response.StatusCode)\"\n}\n\n# (f) bytes=0-999,2000-2999 (multi-range) → 416\ntry {\n    $r5 = Invoke-WebRequest -UseBasicParsing -Uri $url -Headers @{ Range = 'bytes=0-999,2000-2999' }\n    \"multi: status=$($r5.StatusCode) (期望 416 但成功了 → blocker)\"\n} catch {\n    \"multi: 期望异常 → $($_.Exception.Response.StatusCode)\"\n}\n```\n\n期望逐项：\n- (a) 200，`Accept-Ranges: bytes`\n- (b) 206，body=1000 字节，`Content-Range: bytes 0-999/\u003ctotal 或 *\u003e`\n- (c) 206，body=100 字节，`Content-Range: bytes 100-199/\u003ctotal 或 *\u003e`\n- (d) 206，body=`total-1000` 字节，`Content-Range: bytes 1000-\u003ctotal-1\u003e/\u003ctotal 或 *\u003e`\n- (e) 416\n- (f) 416\n\n任一不符 → blocker。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplerjiang%2Fbaostock.net","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimplerjiang%2Fbaostock.net","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimplerjiang%2Fbaostock.net/lists"}