gangtise-openapi-cli 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,73 @@
4
4
 
5
5
  ## Changelog
6
6
 
7
+ ### v0.28.0 — 2026-07-21
8
+
9
+ 对齐服务端 2026-07-17 更新(内资研报下载调价 + 41 个公开错误码重排)。**41 个码逐个打了线上探针**,结论是迁移按「错误处理层」而非按业务模块进行、文档并不等于现状:同一个接口内,参数校验层与路由层已发新码,方法路由层和 token 过滤器仍发旧码,异步生成状态也仍是旧码。CLI 对两代都识别。
10
+
11
+ **错误码体系**
12
+ - `errors.ts` 错误码表按新三层结构(`999xxx` 服务统一层 / `1xxxxx` 业务通用层 / `2xxxxx` 接口专有层)重写,覆盖 41 个公开码 + 实测仍在线的旧码
13
+ - 异步轮询同时识别 `410110`/`140001`(生成中)与 `410111`/`140002`(终态失败)。实测服务端**仍在用旧码**(HTTP 400、无 `errorType`),新码为预置——服务端切换那天 `--wait` 不会在首次轮询就抛错中止,把已扣的 50 积分作废
14
+ - `140002`(异步 PROCESSING_FAILED,`410111` 的新码)纳入 transport 终态码集合、任何 HTTP 状态都不重试——异步 `*-check`(get-content)端点无 retry 声明、走默认策略,`140002@500` 会被白重试 2 次才轮到 `asyncContent` 的 `FAILED_CODES` 识别(后者在 `client.call` 的 `withRetry` 之上、拦不到重试);`140002` 语义即「生成失败·终态」、只有那些异步端点会返回它,故全局终态化既安全又省掉白重试。实测服务端仍用 `410111`,此为预置
15
+ - token 自愈补上 `999002`(`0000001008` 的新码);`999011`(AK/SK 不匹配)加入**终态码集合**,任何 HTTP 状态下都不重试——凭证错不会自己好。注意它只来自 `auth.login`,而 login 走 `useAuth=false` 压根不经过自愈码表,所以「不列进自愈表」并拦不住 `auth.login` 在 5xx 上按默认策略重放两次,必须落在终态码上
16
+ - 修正 `900002` 的错误释义:实测服务端用它表示「请求方法不正确」(HTTP 405),旧文档写作「请求缺少 uid」,据此排查会走错方向
17
+ - 错误提示改为只给下一步动作,不再复述服务端 msg(此前输出形如 `资源不存在 资源不存在,确认 ID 有效`)——新旧两代都过了一遍:留用的 `903301` / `8000016` / `8000018` / `999995` / `999997` / `900001` / `130001` / `410004` / `410110` / `410111` 原本是逐字重复 msg
18
+ - 补上 `410001` / `410106` 两个 EDE 专有旧码的提示——它们没被 2026-07-17 重排收编,却是 `indicator` 取数最常见的两个报错(漏传 `--indicator`/`--security`、漏传 `periodNum` 等必填参数),`indicator.md` 早已把它们列为首要排查项
19
+ - `110002`(日期区间非法)提示改为同时覆盖 `--start-date/--end-date` 与 `--start-time/--end-time`——此前只提 date 参数,而 `insight` 系 list 按 `--start-time` 排序,旧提示指向的是命令根本没有的参数
20
+ - `999006`(限流)提示不再断言「CLI 已退避重试」——仅限流以 429 返回时才全局按 `Retry-After` 退避重试;5xx 形态只有默认策略端点会重试、贵档 no-replay 端点不会,200 错误信封则不重试
21
+ - `130001` 提示改为先给通用「未找到数据/核对查询条件」再限定 EDE 指标权限(此前把通用 DATA_NOT_FOUND 一律导向「未开通指标」);`130002` 提示补上「非法 `--file-type` 也归此码」(下载类兜底)
22
+
23
+ **行为变更(本地校验)**
24
+ - 所有 date 参数(`--start-date`/`--end-date`/`--date`/`--report-date`,覆盖 Quote/Fundamental、AI `theme-tracking`/`hot-topic`/`management-discuss-*`、Alternative `edb-data`、Indicator)只接受 `YYYY-MM-DD`,其余格式在发请求前报 `ValidationError`——**服务端额外接受的两种「年在后」格式日月顺序相反**:实测 `07/01/2026`(斜杠)读成 `2026-01-07`、`07-01-2026`(横杠)读成 `2026-07-01`,同样三个数字差半年且都返回 HTTP 200,响应里不回显服务端实际采用的日期(用 `25/12` 与 `12/25` 的互补接受结果交叉验证)。CLI 无从判断用户想要哪个读法,故只转发无歧义写法。`20260701` / `2026/07/01` 这类服务端同样能正确处理的写法也一并拒掉——统一成一种入参形态,好过按端点逐一探针维护白名单;报错文案说明该用哪种写法,不再断言输入本身有歧义
25
+ - **datetime 参数(`--start-time`/`--end-time`)本地拦截覆盖全部透传命令**(insight research/summary/announcement-hk/us、vault 各 list、`quote minute-kline`、`ai security-clue` 等原样透传的 18 处,外加转时间戳的 A 股 `announcement` / `knowledge-batch`)。**服务端对透传的年在后格式静默误解析、并不报 `110001`**:实测 `insight research list` 对 `07/01/2026` 返回 1562 条(=`2026-01-07`)、`07-01-2026` 返回 210 条(=`2026-07-01`),差半年、都 HTTP 200、响应不回显实际日期。新增 `parseDatetimeOption` 做**时区无关**的字段校验(算术闰年、不构造本地 `Date`,故 DST 缺口时刻等对服务端合法的字符串不被客户端时区误伤)后**原样透传**
26
+ - 本地时间校验只认 `YYYY-MM-DD`、`YYYY-MM-DD HH:mm[:ss]`(空格或 `T` 分隔、秒可省)或 10/13 位时间戳;此前 `parseTimestamp13` 用 `new Date()` 兜底还能吞 `.SSS` 毫秒尾、`+08:00` 时区尾、以及 `Infinity`/`1e309`/非整数(数字分支只查 `NaN`,这类会序列化成 null 静默取消过滤),现一律拒绝(时间戳分支改用严格 `^\d{10}$`/`^\d{13}$` 位数正则——这也是科学计数法 / 16 进制 / 空白 / 非标准位数被拒、且 13 位 `1000000000000` 不再落进秒分支的原因)
27
+
28
+ **修复**
29
+ - EDE 内层信封的报错(`indicator` 取数失败的 `999999` / `130001` 等)此前**永远拿不到 traceId**:实测 `traceId` 只挂在外层信封上,而外层在解包时即被丢弃,内层抛错又没传 details。现在外层 id 以不可枚举属性随 payload 带下去(不进 JSON/CSV 输出),`ApiError.traceId` 兜底读它——这类错误恰恰最需要报障,此前与 README「报错行会带 trace」的表述对不上
30
+ - HTTP 200 包裹的错误信封(Gangtise 也用这种形态)此前会丢掉服务端的 `Retry-After`:主 JSON 路径与下载 JSON 路径两处 `unwrapEnvelope` 都补上(此前只有 4xx/5xx 的 `throwHttpError` 保留),限流响应的退避窗口不再被丢弃
31
+ - `toTimestamp13` 的日历校验补年份与时间 round-trip:`0050-06-15` 曾被 `Date(50,…)` 构造器映射成 1950、DST 缺失时刻(如 America/New_York 的 `02:30`)曾被静默移到 `03:30`——均改为拒绝
32
+ - 异步终态失败(`410111`/`140002`)的报错行补上 code / msg / `traceId` 并提示重提会再次计费——此前只打印一句 "Content generation failed",把本版新增的 trace 信息吞掉了,与 README「报错行会带 trace」的表述矛盾
33
+
34
+ **可观测性**
35
+ - 响应信封新增的 `traceId` 透出到 CLI 报错行:`API error (130002) [trace 830965044897325056]: 资源不存在 确认 ID 有效…`——这是 Gangtise 侧唯一能回溯一次失败的抓手,报障时请带上
36
+
37
+ **计费**
38
+ - `insight research download`(内资研报)**20 → 10 积分/篇**,SKILL.md 积分速查表与 `insight.md` 同步
39
+
40
+ **文档(实测结论沉淀)**
41
+ - SKILL.md 异常处理表重写为「实测确认在用」与「文档列出但未触发」两组,标注每个码的实测状态与兜底关系(`100003` 是参数类兜底、`130002` 是下载类兜底,`130003`/`130004`/`130005` 均未启用)
42
+ - 记录两个实测坑:**枚举值拼错与分页越界服务端不报错**(静默忽略该筛选条件,拼错会伪装成"结果正常");**`viewpoint-debate` 敏感内容不被提前拦截**,扣满 50 积分后才以 `410111` 失败
43
+ - 纠正 SKILL 异常表 `110001`/`110002` 行的日期分类:此前按命令组(「Quote/Fundamental 用 date、Insight/AI 用 datetime」「110001 只有 Insight 系报」)与 AI `management-discuss --report-date`(date 型)及实测都冲突——实测 `fundamental` 对 `2020/01/01` 报 110001、`insight research list` 对 `30/06/2025` 反而宽松解析返回数据,改为按参数名分类、不按命令组预判
44
+ - 新增判别法:新码信封 `code` 是 JSON 数字且带 `errorType`,旧码是字符串且没有——但它判断的是**单条错误路径**切没切,不是整个接口(成功响应也没有 `errorType`,别拿它当判据)
45
+ - README 常见错误表同步重写;Troubleshooting 的 `8000014/8000015` → `999011`、`430007` → `100006`
46
+
47
+ ### v0.27.0 — 2026-07-11
48
+
49
+ **EDE 指标(体验修复)**
50
+ - `indicator` 三端点对 `999999` 不再自动重试——实测服务端用 `999999` + HTTP 500 表示「查询无数据」(节假日 / 未来日期 / 未覆盖标的),此前每次空查询白烧 3 个请求 + ~4 秒;错误提示改为指向检查查询条件而非「稍后重试」
51
+
52
+ **资金与下载加固(承接 v0.26.0)**
53
+ - 下载路径同样接入重试策略:50/篇 的 `summary` / `foreign-report` / `my-conference` download 改为 no-replay(与 AI Agent 同价档;下载中断重试可能重复计费),10-30/篇 的下载维持默认重试
54
+ - 签名 URL 下载增加整体硬截止(10× 单请求超时)——headers/body 超时是空闲型,慢滴速传输可无限续命;最终 rename 失败时清理 `.part`
55
+ - `GANGTISE_PAGE_CONCURRENCY` 防御性解析:非法/非正数回退默认 5、上限 32——负值此前被底层钳制成**单 worker 串行**(静默变慢),过大值可能造成过度并发触发限流
56
+ - `--version` 更新提示改为数值分段版本比较(不处理预发布号;本项目只发 x.y.z)——刚发版的 registry 滞后窗口不再把旧版本提示成"可更新"
57
+
58
+ **体验与正确性小修**
59
+ - `--wait` 异步轮询容忍瞬态错误:5xx/网络抖动只消耗一次尝试并继续等待,不再作废整段等待(积分不足等终态错误仍立即中止)
60
+ - table 输出单元格显示宽度上限 120(超长截断加 `…`)——一个超长字段不再把整列所有行 pad 成同宽(行数 × 宽度的空格放大)
61
+ - markdown 输出先转义反斜杠再转义竖线,字面 `\|` 单元格不再错位列;table/markdown 过滤 C1 控制符(U+009B 单字节 CSI 注入面)
62
+ - 自动文件名按码点截断,emoji 不再被截成 `�`;EDE 矩阵中与 `date`/`security`/`name` 同名的指标列自动加后缀,不再覆盖元数据列
63
+ - 全市场分片截断时输出 `truncatedShards`(具体日期区间,与 `failedShards` 对称),脚本/AI 消费者可定向缩窗补拉
64
+ - 分页端点首页形状漂移(如 `total` 变字符串)时 `--verbose` 下告警,不再完全静默退化单页
65
+
66
+ **Skill 分发**
67
+ - `gangtise-openapi/` 目录纳入 npm 包;README 安装命令改为从 `$(npm root -g)` 复制——此前的相对路径命令对 npm 用户不可执行
68
+
69
+ **防漂移门禁(工程,不影响 CLI 行为)**
70
+ - 新增 README↔ENDPOINTS 一致性测试:「自动翻页」清单与注册表 pagination 标记双向比对(此类手抄清单漂移已发生两次);insight/reference 子命令的 `--help` 覆盖改为从端点注册表派生,新命令漏接线直接测试失败
71
+ - `npm run prepare` 前置断言 README/CHANGELOG 含当前版本条目(写盘前检查,失败零残留);`npm run typecheck` 纳入 tests/(tsconfig.test.json)
72
+ - CI:`npm pack` 装包冒烟(`--help` + skill 文件存在校验)、测试矩阵 Node 下限改精确 20.18.1、CI typecheck;publish 的 `workflow_dispatch` 必须指向 `v*` tag(关闭无护栏发布通道)
73
+
7
74
  ### v0.26.0 — 2026-07-11
8
75
 
9
76
  **资金安全(重要)**
@@ -68,14 +135,6 @@
68
135
  - `reference institution-search` — 机构 ID 搜索,输入机构名/简称返回 `institutionId` 及适用接口参数(`usageScopes`);`--keyword`(必填)、`--category`(`domesticBroker`/`foreignInstitution`/`leadInstitution`/`opinionInstitution`/`foreignOpinionInstitution`,可重复)、`--top`(默认 10,上限 10);免费。覆盖既有 `--broker`/`--institution` 全部机构类(research/foreign-report/opinion/foreign-opinion/summary/roadshow/site-visit/strategy/my-conference)
69
136
  - `vault my-conference-list` 新增 `--source` — 按录制来源筛选(`1`=企微会议助理 `2`=会议服务微信群,可重复;不传返回全部)
70
137
 
71
- ### v0.22.1 — 2026-07-03
72
-
73
- **修复**
74
- - 错误码 `410004` 提示改为中性措辞「数据未找到或无指标权限,请检查查询条件与指标权限」——此前只说"数据未找到",与 `indicator` 内层信封的"无权限"消息拼接后自相矛盾
75
-
76
- **文档 / Skill**(随 `/sync-skill` 分发,不影响 CLI 行为)
77
- - gangtise-openapi Agent Skill 经 fable5 审计 + 多轮 review 优化:官方积分计费速查表 + 高积分 pre-flight 闸门、AI 同步生成命令 `GANGTISE_TIMEOUT_MS=120000` 超时前置、大结果集 `--output` 落盘、异步 `--wait` 主路径、行业码口径收敛到单一权威、市值量纲实测(`qte_mkt_cptl` 仅 A 股 / 默认原始「元」/ `scale`+`currency`)等文档补全与消歧
78
-
79
138
  > 更早版本及完整更新历史见 [CHANGELOG.md](CHANGELOG.md)。
80
139
 
81
140
  ## 首次安装
@@ -96,6 +155,8 @@ gangtise --help
96
155
  npm update -g gangtise-openapi-cli
97
156
  ```
98
157
 
158
+ > 更新后若使用 Agent Skill:包内 skill 已随包更新,但复制到 `~/.claude/skills/` 等目录的副本是快照,**需重新执行下方「安装」段的复制命令**才能让 AI 助手拿到新版 skill。
159
+
99
160
  本地开发:
100
161
 
101
162
  ```bash
@@ -116,13 +177,13 @@ export GANGTISE_BASE_URL="https://openapi.gangtise.com"
116
177
  export GANGTISE_TOKEN="Bearer xxx"
117
178
 
118
179
  # 性能/调试可选项
119
- export GANGTISE_PAGE_CONCURRENCY=5 # 翻页并发数(默认 5
180
+ export GANGTISE_PAGE_CONCURRENCY=5 # 翻页/分片并发数(默认 5,上限 32;非法值回退默认)
120
181
  export GANGTISE_VERBOSE=1 # 打印每个请求的耗时与字节数
121
182
  export GANGTISE_TIMEOUT_MS=30000 # 请求超时(默认 30s)
122
183
  export GANGTISE_TOKEN_CACHE_PATH=... # 覆盖 token 缓存路径(默认 ~/.config/gangtise/token.json)
123
184
  ```
124
185
 
125
- 如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。Token 失效(8000014/8000015/0000001008)时会自动重新登录并重试一次。
186
+ 如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。Token 失效(`0000001008` / `999002`)时会自动重新登录并重试一次;凭证本身错(`999011`)不重试,直接报错让你查环境变量。
126
187
 
127
188
 
128
189
  ## AI Agent Skill
@@ -155,22 +216,26 @@ gangtise-openapi/
155
216
  └── response-schema.md # 各接口响应字段说明
156
217
  ```
157
218
 
158
- 安装:
219
+ 安装(skill 目录随 npm 包分发,`npm install -g` 之后即可从全局安装位置复制):
159
220
 
160
221
  ```bash
222
+ SKILL_SRC="$(npm root -g)/gangtise-openapi-cli/gangtise-openapi"
223
+
161
224
  # Claude Code
162
- cp -r gangtise-openapi ~/.claude/skills/gangtise-openapi
225
+ cp -r "$SKILL_SRC" ~/.claude/skills/gangtise-openapi
163
226
 
164
227
  # Codex
165
- cp -r gangtise-openapi ~/.codex/skills/gangtise-openapi
228
+ cp -r "$SKILL_SRC" ~/.codex/skills/gangtise-openapi
166
229
 
167
230
  # OpenClaw
168
- cp -r gangtise-openapi ~/.openclaw/skills/gangtise-openapi
231
+ cp -r "$SKILL_SRC" ~/.openclaw/skills/gangtise-openapi
169
232
 
170
233
  # Hermes
171
- cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
234
+ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
172
235
  ```
173
236
 
237
+ > 从仓库 clone 开发时,把 `$SKILL_SRC` 换成仓库内的 `gangtise-openapi` 目录即可。
238
+
174
239
  > **版本更新**:每次 CLI 发版时,`gangtise-openapi/SKILL.md` 的 `version` 字段会自动同步。更新 CLI 后,请将项目中的 `gangtise-openapi/` 目录重新复制到对应的 skills 目录覆盖更新:
175
240
  >
176
241
  > ```bash
@@ -302,8 +367,8 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
302
367
  - **HTTP keep-alive**:所有请求复用同一个 `undici.Agent`(连接池 16),避免重复 TLS 握手。
303
368
  - **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
304
369
  - **流式输出**:`jsonl`/`csv` 格式且 `--output` 指定时,超过 1000 行自动切换为逐行写盘,避免一次性构建百 MB 字符串。
305
- - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**贵档生成类端点例外**(one-pager 等 13 个):5xx/超时不重放——按次计费不幂等,重放即重复扣分;仅连接失败、429 与 token 自愈重试。
306
- - **Token 自愈**:调用返回 8000014/8000015 时自动强制刷新 Token 并重试一次。
370
+ - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**贵档端点例外**(one-pager 等 13 个生成/提交类 + 50/篇 的 summary/foreign-report/my-conference 下载,共 16 个):5xx/超时不重放——按次计费不幂等,重放即重复扣分;仅连接失败、429 与 token 自愈重试。**`indicator`(EDE)端点对 `999999` 不重试**——该码实为「查询无数据」(实测),重试纯浪费。**终态码 `999011`(凭证无效)/ `140002`(异步生成失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,异步生成失败是终态。
371
+ - **Token 自愈**:调用返回 `0000001008` / `999002` 时自动强制刷新 Token 并重试一次。
307
372
  - **K线/资金流向自动分片**:`quote day-kline --security all`、`quote fund-flow --security aShares` 等全市场查询自动按日期切分(A股 K线/资金流向 1 天/片、美股 1 天/片、HK 2 天/片、指数 30 天/片),并发执行后合并结果;按日分片自动跳过周六日。分片时如果用户未传 `--limit`,自动注入 `limit: 10000`(API 上限)避免默认 6000 截断。
308
373
  - **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
309
374
  - **`--verbose`**:打印每个请求的方法、路径、状态码、耗时和响应大小到 stderr,方便定位慢查询。
@@ -687,7 +752,8 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
687
752
  - `--from`:非负整数
688
753
  - `--size` / `--limit` / `--top`:正整数
689
754
  - `--file-type` / `--resource-type` 以及数值型列表参数:有限数字
690
- - 公告 `--start-time` / `--end-time`:可解析的时间字符串或 Unix 时间戳
755
+ - 所有 date 参数(`--start-date`/`--end-date`/`--date`/`--report-date`,含 Quote/Fundamental/AI/Alternative/Indicator):`YYYY-MM-DD`(严格——年在后等歧义格式在发请求前拒绝)
756
+ - 所有 `--start-time` / `--end-time`(Insight/Vault/AI 透传、`quote minute-kline`,以及 A 股公告 / `knowledge-batch` 两个转换端点):`YYYY-MM-DD[ HH:mm[:ss]]`(时间部分秒可省、空格或 `T` 分隔)或 10/13 位 Unix 时间戳(严格校验——年在后等歧义格式在发请求前拒绝)
691
757
 
692
758
  校验失败会输出 `ValidationError: Invalid ...` 并以非 0 状态退出。
693
759
 
@@ -697,20 +763,31 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
697
763
  |-----------|------|
698
764
  | `ValidationError` | 本地参数校验失败,检查 `--size` / `--limit` / `--from` / `--file-type` 等数值参数 |
699
765
  | `API error (HTTP 4xx/5xx)` | HTTP 层失败;CLI 会把 4xx/5xx 响应视为错误,即使响应体不是标准 `{code,msg,data}` 信封 |
700
- | `8000014` | `GANGTISE_ACCESS_KEY` 错误 |
701
- | `8000015` | `GANGTISE_SECRET_KEY` 错误 |
702
- | `8000016` | 开发账号状态异常 |
703
- | `8000018` | 开发账号已到期 |
704
- | `900001` | 请求参数为空或缺少必填项 |
705
- | `900002` | 请求缺少 uid |
706
- | `903301` | 今日调用次数已达上限 |
707
- | `999995` | 积分不足 |
708
- | `999997` | 未开通接口权限 |
709
- | `999999` | Gangtise 系统错误,请稍后重试 |
710
- | `433007` | 不支持该数据源(`knowledge-resource-download` 需正确的 `resourceType + sourceId` 组合) |
711
- | `430007` | 行情查询超出限制(数据量过大,请缩短日期范围或减少 `--limit`) |
712
- | `410110` | 异步任务生成中(非终态,需继续轮询) |
713
- | `410111` | 异步任务生成失败(终态,不可重试) |
766
+ | `999011` | 开发账号凭证无效(AK/SK 不匹配)——**取代旧 `8000014`/`8000015`**,不再区分是 AK 错还是 SK 错 |
767
+ | `999002` / `0000001008` | Token 无效或已过期(有 AK/SK 时 CLI 自动重登重试一次) |
768
+ | `999001` / `0000001007` | 请求未携带 token |
769
+ | `999003` | 未开通接口权限(定制接口需联系客户经理) |
770
+ | `999005` | 积分不足 |
771
+ | `999006` | 调用超出上限(HTTP 429,CLI 按 Retry-After 退避重试) |
772
+ | `999010` | 接口地址不存在(`raw call` 的 key 可能已下线,用 `raw list` 核对) |
773
+ | `999012` / `999013` / `999014` | 账号禁用 / 已过期 / 租户失效 |
774
+ | `999016` | 调用方 IP 不在允许范围 |
775
+ | `999999` | Gangtise 系统错误,请稍后重试(`indicator` 端点此码多为查询无数据) |
776
+ | `100003` | 参数值非法——**最宽的兜底码**;msg 通常已指明字段(如「limit 最小为 1,最大为 10000」),先读 msg |
777
+ | `100001` | 缺必填参数(msg 带字段名,如「缺少必填参数: reportId」) |
778
+ | `100006` | 查询/下载数量超限——**取代旧 `430007`** |
779
+ | `110001` / `110002` | 日期格式错误 / 日期区间非法(起晚于止) |
780
+ | `120001` | 证券代码无效(用 `reference securities-search` 确认代码与后缀) |
781
+ | `130001` | 数据未找到或无指标权限——**取代旧 `410004`** |
782
+ | `130002` | 资源不存在——**下载类的兜底码**,`--report-id` 不存在 / 非数字 / `--file-type` 非法都归这里(取代旧 `430004`) |
783
+ | `410110` / `410111` | 异步任务生成中(继续轮询)/ 生成失败(终态)——**实测服务端仍发这两个旧码**;新码 `140001`/`140002`,CLI 两代都认 |
784
+ | `240001` | 财报期未披露或超出查询期(`earnings-review` 提交阶段即报,不扣积分) |
785
+ | `250001` | 不支持该数据源(`knowledge-resource-download` 需正确的 `resourceType + sourceId` 组合)——**取代旧 `433007`** |
786
+ | `900002` | 请求方法不正确(服务端 msg 为「请求类型有误」;旧文档写作"缺少 uid"是错的) |
787
+
788
+ > **关于这次错误码重排**:服务端 2026-07-17 重排了 41 个公开码(三层:`999xxx` 服务统一层 / `1xxxxx` 业务通用层 / `2xxxxx` 接口专有层),信封新增 `errorType` 和 `traceId`。2026-07-20 逐码实测发现**迁移是按「错误处理层」而非按业务模块进行的**:同一个接口内,参数校验层与路由层已发新码,方法路由层、token 过滤器、以及异步生成状态仍发旧码。新码信封 `code` 是 JSON 数字且带 `errorType`,旧码是字符串且没有——但这判断的是单条错误路径,不是整个接口;CLI 对两代都能识别。报错行会带 `[trace <id>]`,**报障时请带上它**。
789
+ >
790
+ > 其余码(`999003`–`999006`、`999012`–`999016`、`100002`、`100004`、`100005`、`110003`、`130003`–`130005`、`210001`、`220001`、`230001`、`240002`、`240003`)在实测中未触发到,多被上面的兜底码接管,CLI 仍内置了对应提示。两个实测坑:**枚举值拼错和分页越界服务端不报错**(静默忽略该筛选条件);**`viewpoint-debate` 的敏感内容不会被提前拦截**,会扣满 50 积分再以 `410111` 失败。
714
791
 
715
792
  ---
716
793
 
package/dist/src/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Command, Option } from "commander";
3
3
  import { checkAsyncContent, pollAsyncContent, POLL_MAX_ATTEMPTS } from "./core/asyncContent.js";
4
4
  import { readTokenCache, redactTokenCache } from "./core/auth.js";
5
- import { collectKeyValue, collectList, collectNumberList, localDateString, maybeArray, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize, parseTimestamp13 } from "./core/args.js";
5
+ import { collectKeyValue, collectList, collectNumberList, dateArg, datetimeArg, isVersionNewer, localDateString, maybeArray, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize, parseTimestamp13 } from "./core/args.js";
6
6
  import { buildIndicatorCrossSectionBody, buildIndicatorTimeSeriesBody, buildQuoteKlineBody, buildStockPoolStocksBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
7
7
  import { flattenCrossSection, flattenTimeSeries, unwrapIndicatorData } from "./core/indicatorMatrix.js";
8
8
  import { callKlineWithSharding, isAllMarket, isFullMarket } from "./core/quoteSharding.js";
@@ -111,8 +111,8 @@ function addTimeFilters(command) {
111
111
  return command
112
112
  .option("--from <number>", "Starting offset", "0")
113
113
  .option("--size <number>", "Total rows to return; omit to fetch all")
114
- .option("--start-time <datetime>", "Start time")
115
- .option("--end-time <datetime>", "End time")
114
+ .option("--start-time <datetime>", "Start time", datetimeArg("--start-time"))
115
+ .option("--end-time <datetime>", "End time", datetimeArg("--end-time"))
116
116
  .option("--keyword <keyword>", "Keyword");
117
117
  }
118
118
  import { setVerbose } from "./core/transport.js";
@@ -314,13 +314,13 @@ addTimeFilters(officialAccount.command("list").option("--search-type <number>",
314
314
  addDownloadCommand(officialAccount, { endpointKey: "insight.official-account.download", idOption: "--article-id", idField: "articleId", fallbackPrefix: "official-account", fileType: { description: "File type: 1=txt(default) 2=HTML", default: "1" }, titleListEndpoint: "insight.official-account.list" });
315
315
  // QA request keys are BARE (source/questionCategory/answerImportant), not the *List
316
316
  // convention — the body below mirrors the spec exactly. Datetimes pass through as strings.
317
- qa.command("list").requiredOption("--security-code <code>", "Security code, e.g. 601012.SH").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all (max page 500)").option("--start-time <datetime>", "Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)").option("--end-time <datetime>", "End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)").option("--source <type>", "Source: conference/interactive/survey (repeat)", collectList, []).option("--question-category <name>", "Question category (repeat): productAndBusiness/capacityAndProjects/ordersAndCustomers/financialData/materialEvents/capitalOperations/shareholdersAndDividends/corporateGovernance/marketAndValuation/macroAndIndustry/risksAndOthers", collectList, []).option("--answer-important <flag>", "Answer involves key info: 1=yes 0=no (repeat; omit for all)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("insight.qa.list", {
317
+ qa.command("list").requiredOption("--security-code <code>", "Security code, e.g. 601012.SH").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all (max page 500)").option("--start-time <datetime>", "Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time")).option("--end-time <datetime>", "End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time")).option("--source <type>", "Source: conference/interactive/survey (repeat)", collectList, []).option("--question-category <name>", "Question category (repeat): productAndBusiness/capacityAndProjects/ordersAndCustomers/financialData/materialEvents/capitalOperations/shareholdersAndDividends/corporateGovernance/marketAndValuation/macroAndIndustry/risksAndOthers", collectList, []).option("--answer-important <flag>", "Answer involves key info: 1=yes 0=no (repeat; omit for all)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("insight.qa.list", {
318
318
  from: parseFrom(options.from), size: parseSize(options.size),
319
319
  securityCode: options.securityCode, startTime: options.startTime, endTime: options.endTime,
320
320
  source: maybeArray(options.source), questionCategory: maybeArray(options.questionCategory),
321
321
  answerImportant: options.answerImportant.length ? options.answerImportant : undefined,
322
322
  })));
323
- reportImage.command("list").requiredOption("--keyword <text>", "Search keyword, e.g. 'AI' '新能源汽车'").option("--top <number>", "Max results (default: 10, max: 20)", "10").option("--source-id <id>", "Report source ID, to filter to one report (from a report list or knowledge base)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss; yyyy-MM-dd auto-completed)").option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss; yyyy-MM-dd auto-completed)").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("insight.report-image.list", {
323
+ reportImage.command("list").requiredOption("--keyword <text>", "Search keyword, e.g. 'AI' '新能源汽车'").option("--top <number>", "Max results (default: 10, max: 20)", "10").option("--source-id <id>", "Report source ID, to filter to one report (from a report list or knowledge base)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss; yyyy-MM-dd auto-completed)", datetimeArg("--start-time")).option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss; yyyy-MM-dd auto-completed)", datetimeArg("--end-time")).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("insight.report-image.list", {
324
324
  keyword: options.keyword, top: parseNumberOption(options.top, "--top", { integer: true, min: 1, max: 20 }),
325
325
  sourceId: options.sourceId, startTime: options.startTime, endTime: options.endTime,
326
326
  })));
@@ -345,8 +345,8 @@ program.addCommand(insight);
345
345
  const quote = new Command("quote").description("Quote APIs");
346
346
  const addKlineCommand = (name, endpointKey, securityHelp, shardDays) => quote.command(name)
347
347
  .option("--security <code>", securityHelp, collectList, [])
348
- .option("--start-date <date>", "Start date (default: 1 year before end-date)")
349
- .option("--end-date <date>", "End date (default: latest)")
348
+ .option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
349
+ .option("--end-date <date>", "End date (default: latest)", dateArg("--end-date"))
350
350
  .option("--limit <number>", "Max rows per request (default: 6000, max: 10000)")
351
351
  .option("--field <field>", "Field", collectList, [])
352
352
  .option("--format <format>", "Output format", "table")
@@ -373,7 +373,7 @@ addKlineCommand("day-kline", "quote.day-kline", "Security code (A-share: .SH/.SZ
373
373
  addKlineCommand("day-kline-hk", "quote.day-kline-hk", "Security code (HK stock: .HK, or 'all' for full market)", 2);
374
374
  addKlineCommand("day-kline-us", "quote.day-kline-us", "Security code (US stock: e.g. AAPL.O, or 'all' for full market)", 1);
375
375
  addKlineCommand("index-day-kline", "quote.index-day-kline", "Index code (.SH/.SZ/.BJ, or 'all' for full market)", 30);
376
- quote.command("minute-kline").option("--security <code>", "Security code (A-share only: .SH/.SZ/.BJ)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)").option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
376
+ quote.command("minute-kline").option("--security <code>", "Security code (A-share only: .SH/.SZ/.BJ)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time")).option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time")).option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
377
377
  const format = parseOutputFormat(options.format);
378
378
  const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 10000 }) ?? DEFAULT_QUOTE_LIMIT;
379
379
  const data = await client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit, fieldList: maybeArray(options.field) });
@@ -381,7 +381,7 @@ quote.command("minute-kline").option("--security <code>", "Security code (A-shar
381
381
  await printData(data, format, options.output);
382
382
  }));
383
383
  quote.command("realtime").description("Realtime quote snapshot (A-share / HK / US)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK / AAPL.O), or market keyword: aShares / hkStocks / usStocks", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList: maybeArray(options.field) })));
384
- quote.command("fund-flow").description("A-share daily fund flow (SH/SZ/BJ)").option("--security <code>", "Security code (e.g. 600519.SH / 872931.BJ), or 'aShares' for full A-share market — auto-sharded by day (repeat)", collectList, []).option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)").option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000; single-security cap — aShares auto-shards by day)").option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
384
+ quote.command("fund-flow").description("A-share daily fund flow (SH/SZ/BJ)").option("--security <code>", "Security code (e.g. 600519.SH / 872931.BJ), or 'aShares' for full A-share market — auto-sharded by day (repeat)", collectList, []).option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)", dateArg("--start-date")).option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)", dateArg("--end-date")).option("--limit <number>", "Max rows per request (default: 6000, max: 10000; single-security cap — aShares auto-shards by day)").option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
385
385
  const format = parseOutputFormat(options.format);
386
386
  const body = {
387
387
  securityList: maybeArray(options.security),
@@ -410,7 +410,7 @@ quote.command("fund-flow").description("A-share daily fund flow (SH/SZ/BJ)").opt
410
410
  }));
411
411
  program.addCommand(quote);
412
412
  const fundamental = new Command("fundamental").description("Fundamental APIs");
413
- const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) })));
413
+ const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) })));
414
414
  addFinancialReport("income-statement", "fundamental.income-statement");
415
415
  addFinancialReport("income-statement-quarterly", "fundamental.income-statement-quarterly", "Period: q1/q2/q3/q4/latest");
416
416
  addFinancialReport("balance-sheet", "fundamental.balance-sheet");
@@ -422,8 +422,8 @@ addFinancialReport("cash-flow-hk", "fundamental.cash-flow-hk", "Period: q1/h1/q3
422
422
  addFinancialReport("income-statement-us", "fundamental.income-statement-us", "Period: q1/h1/q3/nsd/annual/latest");
423
423
  addFinancialReport("balance-sheet-us", "fundamental.balance-sheet-us", "Period: q1/h1/q3/nsd/annual/latest");
424
424
  addFinancialReport("cash-flow-us", "fundamental.cash-flow-us", "Period: q1/h1/q3/nsd/annual/latest");
425
- fundamental.command("main-business").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").addOption(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product")).option("--period <type>", "Period: interim/annual", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.main-business", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, breakdown: options.breakdown, periodList: maybeArray(options.period), fieldList: maybeArray(options.field) })));
426
- fundamental.command("valuation-analysis").requiredOption("--security-code <code>").addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--limit <number>").option("--field <field>", "Field", collectList, []).option("--skip-null", "Drop rows where value or percentileRank is null").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
425
+ fundamental.command("main-business").requiredOption("--security-code <code>").option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).addOption(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product")).option("--period <type>", "Period: interim/annual", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.main-business", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, breakdown: options.breakdown, periodList: maybeArray(options.period), fieldList: maybeArray(options.field) })));
426
+ fundamental.command("valuation-analysis").requiredOption("--security-code <code>").addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory()).option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--limit <number>").option("--field <field>", "Field", collectList, []).option("--skip-null", "Drop rows where value or percentileRank is null").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
427
427
  const format = parseOutputFormat(options.format);
428
428
  let data = await client.call("fundamental.valuation-analysis", { securityCode: options.securityCode, indicator: options.indicator, startDate: options.startDate, endDate: options.endDate, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) });
429
429
  if (options.skipNull) {
@@ -443,8 +443,8 @@ fundamental.command("valuation-analysis").requiredOption("--security-code <code>
443
443
  }
444
444
  await printData(data, format, options.output);
445
445
  }));
446
- fundamental.command("top-holders").requiredOption("--security-code <code>").addOption(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/interim/q3/annual/latest", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.top-holders", { securityCode: options.securityCode, holderType: options.holderType, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined })));
447
- fundamental.command("earning-forecast").requiredOption("--security-code <code>").option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: today)").option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => {
446
+ fundamental.command("top-holders").requiredOption("--security-code <code>").addOption(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory()).option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/interim/q3/annual/latest", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.top-holders", { securityCode: options.securityCode, holderType: options.holderType, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined })));
447
+ fundamental.command("earning-forecast").requiredOption("--security-code <code>").option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date")).option("--end-date <date>", "End date (default: today)", dateArg("--end-date")).option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => {
448
448
  const endDate = options.endDate ?? localDateString(new Date());
449
449
  // Anchor the default window to endDate (as the help text promises), not to today —
450
450
  // a historical --end-date without --start-date should mean "the year before it".
@@ -453,10 +453,10 @@ fundamental.command("earning-forecast").requiredOption("--security-code <code>")
453
453
  }));
454
454
  program.addCommand(fundamental);
455
455
  const ai = new Command("ai").description("AI APIs");
456
- ai.command("knowledge-batch").option("--query <text>", "Query", collectList, []).option("--top <number>", "Max results (default: 10, max: 20)", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <ms>").option("--end-time <ms>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => {
456
+ ai.command("knowledge-batch").option("--query <text>", "Query", collectList, []).option("--top <number>", "Max results (default: 10, max: 20)", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <datetime>", "13/10-digit epoch or YYYY-MM-DD[ HH:mm[:ss]] (space or T)").option("--end-time <datetime>", "13/10-digit epoch or YYYY-MM-DD[ HH:mm[:ss]] (space or T)").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => {
457
457
  if (!options.query.length)
458
458
  throw new ValidationError("--query is required: pass at least one --query");
459
- return emit(options, (client) => client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1, max: 20 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseOptionalNumberOption(options.startTime, "--start-time", { integer: true, min: 0 }), endTime: parseOptionalNumberOption(options.endTime, "--end-time", { integer: true, min: 0 }) }));
459
+ return emit(options, (client) => client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1, max: 20 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time") }));
460
460
  });
461
461
  ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
462
462
  await runDownload(client, "ai.knowledge-resource.download", { resourceType: parseNumberOption(options.resourceType, "--resource-type", { integer: true, min: 0 }), sourceId: options.sourceId }, {
@@ -464,7 +464,7 @@ ai.command("knowledge-resource-download").requiredOption("--resource-type <numbe
464
464
  fallbackName: `resource-${options.sourceId}`,
465
465
  });
466
466
  }));
467
- ai.command("security-clue").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").requiredOption("--start-time <datetime>").requiredOption("--end-time <datetime>").addOption(new Option("--query-mode <mode>").choices(["bySecurity", "byIndustry"]).makeOptionMandatory()).option("--gts-code <code>", "GTS code", collectList, []).option("--source <name>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.security-clue.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, queryMode: options.queryMode, gtsCodeList: maybeArray(options.gtsCode), source: maybeArray(options.source) })));
467
+ ai.command("security-clue").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").requiredOption("--start-time <datetime>", "Start time", datetimeArg("--start-time")).requiredOption("--end-time <datetime>", "End time", datetimeArg("--end-time")).addOption(new Option("--query-mode <mode>").choices(["bySecurity", "byIndustry"]).makeOptionMandatory()).option("--gts-code <code>", "GTS code", collectList, []).option("--source <name>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.security-clue.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, queryMode: options.queryMode, gtsCodeList: maybeArray(options.gtsCode), source: maybeArray(options.source) })));
468
468
  ai.command("one-pager").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.one-pager", { securityCode: options.securityCode })));
469
469
  ai.command("investment-logic").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.investment-logic", { securityCode: options.securityCode })));
470
470
  ai.command("peer-comparison").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.peer-comparison", { securityCode: options.securityCode })));
@@ -494,12 +494,12 @@ ai.command("earnings-review").requiredOption("--security-code <code>").requiredO
494
494
  }
495
495
  }));
496
496
  ai.command("earnings-review-check").requiredOption("--data-id <id>", "dataId from earnings-review").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient((client) => checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseOutputFormat(options.format), options.output)));
497
- ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use 'reference concept-search')").requiredOption("--date <date>", "Date (yyyy-MM-dd)").option("--type <name>", "Report type: morning/night", collectList, []).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
497
+ ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use 'reference concept-search')").requiredOption("--date <date>", "Date (yyyy-MM-dd)", dateArg("--date")).option("--type <name>", "Report type: morning/night", collectList, []).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
498
498
  const typeList = options.type.length ? options.type : undefined;
499
499
  return client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList });
500
500
  }));
501
501
  ai.command("research-outline").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.research-outline", { securityCode: options.securityCode })));
502
- ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--category <name>", "Report type: morningBriefing/noonBriefing/afternoonFlash/eveningBriefing", collectList, []).option("--with-related-securities", "Include related securities info").option("--no-with-related-securities", "Exclude related securities info").option("--with-close-reading", "Include close reading content").option("--no-with-close-reading", "Exclude close reading content").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
502
+ ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).option("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--category <name>", "Report type: morningBriefing/noonBriefing/afternoonFlash/eveningBriefing", collectList, []).option("--with-related-securities", "Include related securities info").option("--no-with-related-securities", "Exclude related securities info").option("--with-close-reading", "Include close reading content").option("--no-with-close-reading", "Exclude close reading content").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
503
503
  const ALL_CATEGORIES = ["morningBriefing", "noonBriefing", "afternoonFlash", "eveningBriefing"];
504
504
  return client.call("ai.hot-topic", {
505
505
  from: parseFrom(options.from),
@@ -511,12 +511,12 @@ ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option
511
511
  withCloseReading: options.withCloseReading !== false,
512
512
  });
513
513
  }));
514
- ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension: businessOperation/financialPerformance/developmentAndRisk/all").choices(["businessOperation", "financialPerformance", "developmentAndRisk", "all"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-announcement", {
514
+ ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)", dateArg("--report-date")).requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension: businessOperation/financialPerformance/developmentAndRisk/all").choices(["businessOperation", "financialPerformance", "developmentAndRisk", "all"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-announcement", {
515
515
  reportDate: options.reportDate,
516
516
  securityCode: options.securityCode,
517
517
  discussionDimension: options.dimension,
518
518
  })));
519
- ai.command("management-discuss-earnings-call").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-earnings-call", {
519
+ ai.command("management-discuss-earnings-call").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)", dateArg("--report-date")).requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-earnings-call", {
520
520
  reportDate: options.reportDate,
521
521
  securityCode: options.securityCode,
522
522
  discussionDimension: options.dimension,
@@ -586,13 +586,13 @@ reference.command("official-account-search").requiredOption("--keyword <text>",
586
586
  })));
587
587
  program.addCommand(reference);
588
588
  const vault = new Command("vault").description("Vault APIs");
589
- vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.drive.list", idField: "fileId" }));
589
+ vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.drive.list", idField: "fileId" }));
590
590
  addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
591
- vault.command("record-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", collectList, []).option("--space-type <number>", "Space type: 1=my records / 2=tenant records", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.record.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.record.list", idField: "recordId" }));
591
+ vault.command("record-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", collectList, []).option("--space-type <number>", "Space type: 1=my records / 2=tenant records", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.record.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.record.list", idField: "recordId" }));
592
592
  addDownloadCommand(vault, { endpointKey: "vault.record.download", name: "record-download", idOption: "--record-id", idField: "recordId", fallbackPrefix: "record", contentTypeDescription: "Content type: original/asr/summary", titleListEndpoint: "vault.record.list" });
593
- vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--research-area <id>", "Research area ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--source <number>", "Recording source: 1=企微会议助理 2=会议服务微信群 (repeat)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category), sourceList: options.source.length ? options.source : undefined }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
593
+ vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--research-area <id>", "Research area ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--source <number>", "Recording source: 1=企微会议助理 2=会议服务微信群 (repeat)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category), sourceList: options.source.length ? options.source : undefined }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
594
594
  addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "my-conference-download", idOption: "--conference-id", idField: "conferenceId", fallbackPrefix: "conference", contentTypeDescription: "Content type: asr/summary", titleListEndpoint: "vault.my-conference.list" });
595
- vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
595
+ vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
596
596
  vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
597
597
  vault.command("stock-pool-list").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.list", {})));
598
598
  vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; omit (or 'all') for all pools", collectList).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.stocks", buildStockPoolStocksBody(options))));
@@ -603,7 +603,7 @@ alternative.command("edb-search").requiredOption("--keyword <text>", "Search key
603
603
  keyword: options.keyword,
604
604
  limit: parseNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 200 }),
605
605
  })));
606
- alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (repeat, max 10)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)").requiredOption("--end-date <date>", "End date (yyyy-MM-dd)").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
606
+ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (repeat, max 10)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).requiredOption("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
607
607
  const format = parseOutputFormat(options.format);
608
608
  const raw = await client.call("alternative.edb-data", {
609
609
  indicatorIdList: options.indicatorId,
@@ -632,12 +632,12 @@ indicator.command("search").requiredOption("--keyword <text>", "Search keyword,
632
632
  });
633
633
  await printData(unwrapIndicatorData(raw), format, options.output);
634
634
  }));
635
- indicator.command("cross-section").option("--indicator <code>", "Indicator code, e.g. qte_close (repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH (repeat for multiple)", collectList, []).requiredOption("--date <date>", "Data date (yyyy-MM-dd)").option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)").option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)").option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustmentType=2 for 前复权 (repeat)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
635
+ indicator.command("cross-section").option("--indicator <code>", "Indicator code, e.g. qte_close (repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH (repeat for multiple)", collectList, []).requiredOption("--date <date>", "Data date (yyyy-MM-dd)", dateArg("--date")).option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)").option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)").option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustmentType=2 for 前复权 (repeat)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
636
636
  const format = parseOutputFormat(options.format);
637
637
  const raw = await client.call("indicator.cross-section", buildIndicatorCrossSectionBody(options));
638
638
  await printData(flattenCrossSection(unwrapIndicatorData(raw)), format, options.output);
639
639
  }));
640
- indicator.command("time-series").option("--indicator <code>", "Indicator code, e.g. qte_close (repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH (repeat for multiple)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)").requiredOption("--end-date <date>", "End date (yyyy-MM-dd)").option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday (default TD)").option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)").option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)").option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustmentType=2 for 前复权 (repeat)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
640
+ indicator.command("time-series").option("--indicator <code>", "Indicator code, e.g. qte_close (repeat for multiple)", collectList, []).option("--security <code>", "Security code, e.g. 600519.SH (repeat for multiple)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)", dateArg("--start-date")).requiredOption("--end-date <date>", "End date (yyyy-MM-dd)", dateArg("--end-date")).option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday (default TD)").option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)").option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)").option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustmentType=2 for 前复权 (repeat)", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
641
641
  const format = parseOutputFormat(options.format);
642
642
  const raw = await client.call("indicator.time-series", buildIndicatorTimeSeriesBody(options));
643
643
  await printData(flattenTimeSeries(unwrapIndicatorData(raw)), format, options.output);
@@ -681,7 +681,9 @@ async function checkForUpdate(timeoutMs = 2000) {
681
681
  try {
682
682
  const response = await fetch("https://registry.npmjs.org/gangtise-openapi-cli/latest", { signal: AbortSignal.timeout(timeoutMs) });
683
683
  const latest = (await response.json()).version;
684
- if (latest && latest !== CLI_VERSION) {
684
+ // Ordered compare, not inequality: during the just-published window the
685
+ // registry still serves the PREVIOUS version — don't suggest a "downgrade".
686
+ if (latest && isVersionNewer(latest, CLI_VERSION)) {
685
687
  process.stderr.write(`Update available: ${CLI_VERSION} → ${latest}\nRun: npm update -g gangtise-openapi-cli\n`);
686
688
  }
687
689
  }
@@ -709,7 +711,10 @@ async function main() {
709
711
  catch (error) {
710
712
  if (error instanceof ApiError) {
711
713
  const hint = error.hint ? ` ${error.hint}` : "";
712
- process.stderr.write(`API error${error.code ? ` (${error.code})` : ""}: ${error.message}${hint}\n`);
714
+ // traceId is what Gangtise support needs to look a failure up; without it a
715
+ // 999999 report is unactionable on their side.
716
+ const trace = error.traceId ? ` [trace ${error.traceId}]` : "";
717
+ process.stderr.write(`API error${error.code ? ` (${error.code})` : ""}${trace}: ${error.message}${hint}\n`);
713
718
  process.exitCode = 1;
714
719
  return;
715
720
  }