gangtise-openapi-cli 0.34.1 → 0.35.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.
@@ -1,19 +1,13 @@
1
- /** Effective request timeout: the endpoint's floor, or the config timeout if higher
2
- * (a user who raised GANGTISE_TIMEOUT_MS keeps their value). */
3
1
  export function resolveTimeoutMs(configTimeoutMs, endpoint) {
4
2
  return Math.max(configTimeoutMs, endpoint.timeoutMs ?? 0);
5
3
  }
6
- // Registry entries omit `key`: it is derived from the record key when ENDPOINTS is
7
- // built below, so the two can never drift.
8
4
  const ENDPOINT_DEFS = {
9
- // ─── auth ───
10
5
  "auth.login": {
11
6
  method: "POST",
12
7
  path: "/application/auth/oauth/open/loginV2",
13
8
  kind: "json",
14
9
  description: "Get access token",
15
10
  },
16
- // ─── lookup (served from local data, not HTTP) ───
17
11
  "lookup.broker-orgs.list": {
18
12
  method: "GET",
19
13
  path: "/guide/broker-orgs-local",
@@ -26,7 +20,6 @@ const ENDPOINT_DEFS = {
26
20
  kind: "json",
27
21
  description: "List meeting orgs from local docs",
28
22
  },
29
- // ─── insight ───
30
23
  "insight.opinion.list": {
31
24
  method: "POST",
32
25
  path: "/application/open-insight/chief-opinion/getList",
@@ -46,7 +39,6 @@ const ENDPOINT_DEFS = {
46
39
  path: "/application/open-insight/summary/v2/download/file",
47
40
  kind: "download",
48
41
  description: "Download summary file",
49
- // 50/篇 — same price tier as the AI Agent calls; billing probed non-idempotent.
50
42
  retry: "no-replay",
51
43
  },
52
44
  "insight.pamirs-summary.list": {
@@ -61,10 +53,6 @@ const ENDPOINT_DEFS = {
61
53
  path: "/application/open-insight/pamirs-summary/download/file",
62
54
  kind: "download",
63
55
  description: "Download a Pamirs expert summary file",
64
- // The 2026-08-07 spec states an entitlement (the expert-summary database) but
65
- // no per-call price. Treated as non-idempotent anyway, like its
66
- // `insight.summary.download` sibling: if it does meter, a 5xx replay
67
- // double-bills, and the only cost of being wrong is losing one retry.
68
56
  retry: "no-replay",
69
57
  },
70
58
  "insight.roadshow.list": {
@@ -209,7 +197,6 @@ const ENDPOINT_DEFS = {
209
197
  },
210
198
  "insight.qa.list": {
211
199
  method: "POST",
212
- // The literal '&' is the vendor's path segment (Q&A-data), not a query separator.
213
200
  path: "/application/open-insight/Q&A-data/getList",
214
201
  kind: "json",
215
202
  description: "List investor Q&A (conference/interactive/survey) for a security",
@@ -227,7 +214,6 @@ const ENDPOINT_DEFS = {
227
214
  kind: "download",
228
215
  description: "Download a research report image by chunkId",
229
216
  },
230
- // ─── reference ───
231
217
  "reference.securities-search": {
232
218
  method: "POST",
233
219
  path: "/application/open-reference/securities/search",
@@ -282,7 +268,6 @@ const ENDPOINT_DEFS = {
282
268
  kind: "json",
283
269
  description: "List constituent securities of a sector",
284
270
  },
285
- // ─── quote ───
286
271
  "quote.day-kline": {
287
272
  method: "POST",
288
273
  path: "/application/open-quote/kline/daily",
@@ -325,7 +310,6 @@ const ENDPOINT_DEFS = {
325
310
  kind: "json",
326
311
  description: "Query A-share daily fund flow (SH/SZ/BJ; small/medium/large/xlarge orders + main net inflow)",
327
312
  },
328
- // ─── fundamental ───
329
313
  "fundamental.income-statement": {
330
314
  method: "POST",
331
315
  path: "/application/open-fundamental/financial-report/income-statement/accumulated",
@@ -416,7 +400,6 @@ const ENDPOINT_DEFS = {
416
400
  kind: "json",
417
401
  description: "Query earning forecast (consensus estimates)",
418
402
  },
419
- // ─── ai ───
420
403
  "ai.stock-summary.list": {
421
404
  method: "POST",
422
405
  path: "/application/open-ai/stock-summary/getList",
@@ -533,7 +516,6 @@ const ENDPOINT_DEFS = {
533
516
  kind: "json",
534
517
  description: "Get viewpoint debate content",
535
518
  },
536
- // ─── vault ───
537
519
  "vault.drive.list": {
538
520
  method: "POST",
539
521
  path: "/application/open-vault/drive/getList",
@@ -586,7 +568,6 @@ const ENDPOINT_DEFS = {
586
568
  path: "/application/open-vault/wechatgroupmsg/chatroomId",
587
569
  kind: "json",
588
570
  description: "List WeChat group chatroom IDs",
589
- // Response is `{ total, list }` (server caps size at 50); auto-paginate by total.
590
571
  pagination: { enabled: true, maxPageSize: 50 },
591
572
  },
592
573
  "vault.stock-pool.list": {
@@ -601,7 +582,6 @@ const ENDPOINT_DEFS = {
601
582
  kind: "json",
602
583
  description: "List securities in stock pool(s)",
603
584
  },
604
- // ─── alternative ───
605
585
  "alternative.edb-search": {
606
586
  method: "POST",
607
587
  path: "/application/open-alternative/EDB/search",
@@ -628,7 +608,6 @@ const ENDPOINT_DEFS = {
628
608
  description: "Query concept (theme index) constituent securities, grouped",
629
609
  retry: "no-replay",
630
610
  },
631
- // ─── indicator (EDE: security-level data indicators) ───
632
611
  "indicator.search": {
633
612
  method: "POST",
634
613
  path: "/application/open-indicator/EDE/search",
@@ -650,8 +629,6 @@ const ENDPOINT_DEFS = {
650
629
  description: "Get time-series data (multi-indicator x single-security OR single-indicator x multi-security)",
651
630
  retry: "no-999999",
652
631
  },
653
- // Note the path: the screener sits directly under open-indicator, NOT under
654
- // the EDE/ prefix its three siblings share.
655
632
  "indicator.screener": {
656
633
  method: "POST",
657
634
  path: "/application/open-indicator/screener",
@@ -659,19 +636,13 @@ const ENDPOINT_DEFS = {
659
636
  description: "Screen securities by an expression over indicator values (条件选股)",
660
637
  retry: "no-999999",
661
638
  },
662
- // ─── tool (open-tool: async file parsing) ───
663
639
  "tool.file-parse.submit": {
664
640
  method: "POST",
665
641
  path: "/application/open-tool/file-parse/submit",
666
642
  kind: "upload",
667
643
  description: "Submit a PDF for parsing (multipart upload), returns taskId",
668
- // Billed per page (0.8/页) at submit time, and the upload itself can take
669
- // minutes on a 100MB file — never replay it, and don't let the default 30s
670
- // headers timeout kill an in-flight upload.
671
644
  timeoutMs: 300_000,
672
645
  retry: "no-replay",
673
- // Probed 2026-07-25: taskId comes back as a string today. Guard anyway — if it
674
- // ever arrives as a bare number, rounding would strand a paid parse job.
675
646
  bigIntFields: ["taskId"],
676
647
  },
677
648
  "tool.file-parse.result": {
@@ -682,8 +653,6 @@ const ENDPOINT_DEFS = {
682
653
  },
683
654
  };
684
655
  export const ENDPOINTS = Object.fromEntries(Object.entries(ENDPOINT_DEFS).map(([key, def]) => [key, { key, ...def }]));
685
- /** Flat catalog of every registered endpoint, for `raw list` discoverability
686
- * (so `raw call` doesn't require memorizing endpoint keys). */
687
656
  export function listEndpoints() {
688
657
  return Object.values(ENDPOINTS).map(({ key, method, path, description }) => ({ key, method, path, description }));
689
658
  }
@@ -10,13 +10,6 @@ export class ValidationError extends CliError {
10
10
  }
11
11
  export class DownloadError extends CliError {
12
12
  }
13
- /** Outer-envelope traceId, stashed on the unwrapped payload so it survives
14
- * `unwrapEnvelope` discarding the envelope. Needed by the EDE endpoints alone:
15
- * they double-wrap, and probing 2026-07-20 showed the traceId lives only on the
16
- * OUTER envelope — the inner failure envelope (`{code, status:false, msg}`) has
17
- * none. Without this the one correlation id Gangtise support can trace is gone
18
- * exactly where the inner failure is raised. Non-enumerable so it never reaches
19
- * JSON/CSV output. */
20
13
  export const ENVELOPE_TRACE_ID = Symbol("gangtise.envelopeTraceId");
21
14
  export function attachEnvelopeTraceId(payload, traceId) {
22
15
  if (payload && typeof payload === "object" && (typeof traceId === "string" || typeof traceId === "number")) {
@@ -24,28 +17,13 @@ export function attachEnvelopeTraceId(payload, traceId) {
24
17
  }
25
18
  return payload;
26
19
  }
27
- /** Keyed by the code as a string — `unwrapEnvelope` runs every envelope code
28
- * through `String()` first, which matters because the 2026-07-17 error-code
29
- * overhaul emits the new codes as JSON *numbers* while legacy codes stay strings.
30
- *
31
- * Both generations are listed on purpose. Probed 2026-07-20: the rollout is
32
- * partial — the business layer already answers with the new codes (`999011`,
33
- * `130001`, `130002`, `100003`, `999010`), but the outer token filter still
34
- * emits `0000001007` / `0000001008` / `900002`. Dropping either set would leave
35
- * a live code hintless. */
36
20
  const ERROR_HINTS = {
37
- // The hint is appended after the server's own msg, so it must carry the *action*,
38
- // never restate the diagnosis — "资源不存在 资源不存在,确认 ID 有效" reads as a stutter.
39
- // ── 服务统一层 999xxx ──
40
21
  "999001": "检查 GANGTISE_TOKEN 或 AK/SK 是否已 export。",
41
22
  "999002": "有 AK/SK 时 CLI 会自动重新登录重试一次,否则请重新登录。",
42
23
  "999003": "定制接口需联系客户经理开通。",
43
24
  "999004": "整库/整接口未开通(如帕米尔专家纪要需单独购买),或该条记录本账号不可见——list 撞上多为前者,先确认该数据库是否已开通。",
44
25
  "999005": "联系客户经理充值,或缩小查询范围降低消耗。",
45
26
  "999006": "触发限流,稍后再试或联系客户经理提额;429 所有端点都退避重试,5xx 仅普通端点重试(贵档 no-replay 端点的 5xx 不重放,但其 429 仍重试)。",
46
- // CLI 自身发不出这三类请求(method / Content-Type 都由 endpoint 定义固定),
47
- // 只有 raw call 打错端点或服务端行为变化才可能撞上。实测 2026-07-20 分别落到
48
- // 900002 / 999999 / 100003,此处为服务端接上新码后的预置。
49
27
  "999007": "请求方法不支持——`raw call` 时确认该 endpoint 是 GET 还是 POST。",
50
28
  "999008": "Content-Type 不支持,该接口只收 application/json。",
51
29
  "999009": "请求体无法解析,检查 JSON 是否合法。",
@@ -57,61 +35,36 @@ const ERROR_HINTS = {
57
35
  "999015": "联系客户经理开通长期 token。",
58
36
  "999016": "联系客户经理登记当前出口 IP。",
59
37
  "999999": "请稍后重试;持续失败请带上面的 trace 报障。",
60
- // ── 业务通用 1xxxxx ──
61
38
  "100001": "对照命令 --help 检查必填项。",
62
39
  "100002": "检查数值/字符串参数是否传反。",
63
- // 实测两种形态都有:类型/范围错的 msg 带字段(「请求体字段类型不匹配: size 期望类型
64
- // Integer」「limit 最小为 1,最大为 10000」),枚举错的 msg 只有笼统的「参数值非法」。
65
- // 条件句让两种形态都读得通——v0.25.0 的旧文案断言"服务端不会指明",与前一种直接打架。
66
40
  "100003": "msg 已指明字段名或取值范围时直接按 msg 改;msg 只说「参数值非法」时多为枚举参数拼写错误(如 --source / --question-category / --answer-important),对照命令 --help 列出的合法值检查。",
67
41
  "100004": "检查 --size / --from 是否为非负数且未超单页上限。",
68
42
  "100005": "对照命令 --help 列出的合法取值检查。",
69
- // 2026-08-14 起 list 类端点把单页上限(50)也归到此码,不再只是「一次要太多行」。
70
43
  "100006": "缩短日期范围或调小 --size / --limit(list 类端点单页上限为 50,CLI 自动翻页,直发接口时要自己遵守)。",
71
- // 按参数名判断,不要按命令组:AI 下 management-discuss 的 --report-date 是 date,
72
- // 而同属 AI 的 knowledge-batch 收时间戳或 datetime——旧文案笼统写"AI 用 datetime"会把
73
- // --report-date 的用户越导越错。
74
44
  "110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
75
45
  "110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
76
- // 超出的是账号数据权限的时间下界,不是单次窗口太宽——`--fiscal-year 2015` 这种
77
- // 整个区间都在界外的请求,怎么缩窗口都还是这个码(实测 2026-08-08)。
78
46
  "110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
79
47
  "120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
80
48
  "130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
81
- // 2026-08-14 起下载类把「file-type 非法」拆到 130005、「资源未生成」拆到 130003,
82
- // 但历史上两者都归过此码,所以提示保留对 --file-type 的指引。
83
49
  "130002": "确认下载 ID 有效且本账号可见;下载类还需检查 --file-type 取值是否合法。",
84
50
  "130003": "资源未生成或该条记录未附带文件——换一条有附件的记录(列表里 hasAttachment/hasFile 为 true 的),或稍后再试。",
85
- // 下载类命令各有各的 ID 参数(--report-id / --announcement-id / --chunk-id /
86
- // --summary-id / --conference-id / --record-id / --file-id / --article-id /
87
- // --independent-opinion-id);--data-id 是异步 *-check 用的,不产生此码。
88
51
  "130004": "下载 ID 需为数字,检查该命令的 --*-id 参数是否传对。",
89
52
  "130005": "对照命令 --help 检查 --file-type / --content-type 取值。",
90
53
  "140001": "稍后用对应 *-check 命令查询。",
91
- // 两类来源共用此码:AI 异步生成失败,以及 EDE 的参数/表达式错误(实测 2026-08-02:
92
- // 枚举越界「参数 adjustType 的值 99 不在有效范围内 [1,2,3,4]」、表达式语法错误)。
93
- // 两者都是终态、都不该重试,所以文案只说「换参数重来」而不预设是哪一类。
94
54
  "140002": "终态失败,重试同一请求不会变——按 msg 改参数后重新提交;EDE 的指标参数名/枚举以 indicator search 的 parameterList 为准。",
95
- // ── 接口专有 2xxxxx ──
96
55
  "210001": "换一篇,或改用 list 取正文摘要。",
97
56
  "220001": "改用 list 取正文摘要。",
98
57
  "230001": "只有自己上传的文件可下载。",
99
- // 2026-08-07 新增码。私域模块,`vault wechat-*` 就在这个模块下且明确要求
100
- // 「已绑定并激活群消息助理」,所以本 CLI 够得着,不能当作不可达而不给提示。
101
58
  "230002": "微信账号未绑定——群消息类接口要求先在 Gangtise 端绑定并激活群消息助理、且助理已入群。",
102
59
  "240001": "换更早的 --period(如 2025q3 → 2025interim)。",
103
60
  "240002": "改述后重新提交。",
104
61
  "240003": "对照命令 --help 检查取值。",
105
62
  "250001": "检查 resourceType 与 sourceId 组合(两者都来自 knowledge-batch 返回)。",
106
- // ── 旧码(2026-07-20 实测仍在线,或历史遗留) ──
107
63
  "0000001007": "请求未携带 Bearer token,检查 GANGTISE_TOKEN 或 AK/SK 是否已 export。",
108
64
  "0000001008": "Token 已失效(多为他处登录挤掉本会话);有 AK/SK 时 CLI 会自动重新登录重试一次,否则请重新登录。",
109
65
  "900001": "对照命令 --help 检查必填项。",
110
66
  "900002": "请求方法不正确(服务端 msg 为「请求类型有误」)——`raw call` 时检查该 endpoint 是 GET 还是 POST。",
111
67
  "903301": "次日再试,或联系客户经理提额。",
112
- // EDE 专有旧码。2026-08-02 复测已不再出现——入参/表达式错走 100003,指标必填
113
- // 参数缺失与枚举越界走 140002。保留兜底:错误码是按模块分批迁移的,回滚或未迁完
114
- // 的路径仍可能吐旧码,届时给条能用的提示比 fallthrough 强。
115
68
  "410001": "补齐 --indicator / --security;`time-series` 不支持「多指标 × 多证券」,改用 `indicator cross-section`。",
116
69
  "410106": "读 `indicator search --format json` 的 parameterList,用 --indicator-param 补上 required:true 的参数(如 periodNum / sDate / fiscalYear)。",
117
70
  "410004": "换证券或日期确认该条件下本应有数据;仍失败多为未开通该指标,联系客户经理。",
@@ -128,29 +81,56 @@ const ERROR_HINTS = {
128
81
  "999997": "联系客户经理开通。",
129
82
  "10011401": "联系客户经理开通白名单。",
130
83
  };
131
- /** Hints keyed on the server's message rather than its code. Used where one code
132
- * covers many causes and the message identifies which — `100003` in particular is
133
- * the catch-all for every EDE input error, so its per-code hint can only be generic.
134
- *
135
- * Kept to cases where the server names the problem but not the CLI flag that fixes
136
- * it: the caller is told "缺少必填参数 reportDate" and still has to work out that
137
- * such indicators need `--indicator-param`, not `--date`.
138
- *
139
- * ⚠️ Do NOT restate this as a rule about code prefixes or as a clean two-way split.
140
- * Survey of 170 indicators (2026-08-15) says otherwise: 117 take `reportDate` only,
141
- * 51 take `tradeDate` only, `div_cash_yld` takes BOTH (both required), and
142
- * `div_cash_yr` / `div_cash_paid_ratio` take NEITHER (they want `fiscalYear`).
143
- * Prefixes cut across it too — 7 `finc_*` and 3 `div_*` want `reportDate`, while 8
144
- * `is_*` and 4 `cf_*` want `tradeDate`. The hint therefore points at
145
- * `indicator search` rather than asserting which indicators are affected.
146
- *
147
- * Both codes are listed because the server sends this same sentence under either:
148
- * `is_op_rev` answers 100003, `div_cash_yld` answers 100001 (probed 2026-08-15). */
84
+ function distinctFailureShapes(message) {
85
+ const byCode = new Map();
86
+ for (const clause of message.split(/[;;]/)) {
87
+ const m = /指标\s+(\S+)\s+(不支持参数|缺少必填参数)[::]?\s*(\S+)/.exec(clause);
88
+ if (!m)
89
+ continue;
90
+ const [, code, kind, key] = m;
91
+ let shape = byCode.get(code);
92
+ if (!shape) {
93
+ shape = new Set();
94
+ byCode.set(code, shape);
95
+ }
96
+ shape.add(`${kind}:${key}`);
97
+ }
98
+ if (byCode.size === 0)
99
+ return 1;
100
+ return new Set([...byCode.values()].map((shape) => [...shape].sort().join("|"))).size;
101
+ }
149
102
  const MESSAGE_HINTS = [
150
103
  {
151
104
  codes: ["100001", "100003"],
152
- match: /不支持参数\s*tradeDate|缺少必填参数[::]?\s*reportDate/,
153
- hint: "这个指标要的是 reportDate(报告期),不是 --date 下发的 tradeDate。补 --indicator-param \"<指标code>:reportDate=YYYY-MM-DD\"(screener 用 \"F1:reportDate=...\"),--date 仍要保留。⚠️ 少数指标两个日期都要(如 div_cash_yld),补完 reportDate 若再报缺 tradeDate,就把 tradeDate 也显式传上;另有指标要的是 fiscalYear。**以 `indicator search` 返回的 parameterList 为准,别按 code 前缀推断**。",
105
+ match: /不支持参数\s*(?:tradeDate|reportDate)|缺少必填参数[::]?\s*(?:tradeDate|reportDate)/,
106
+ when: (message) => distinctFailureShapes(message) > 1,
107
+ hint: "🔴 **这条报错涉及多个指标,而且它们失败的方式不同**——同一个改法套不到全部,按 msg 里每个指标各自说的话分别处理:msg 里对某个指标说「不支持参数 X; 缺少必填参数 Y」= 把 X 换成 Y(`--indicator-param \"<该指标code>:Y=...\"`);只说「不支持参数 X」而没说缺什么 = 该指标一个日期都不要,给它加 `--indicator-param \"<该指标code>:\"`(冒号后留空);只说「缺少必填参数 Y」= 补上 Y。**最省事的办法是把这批指标拆成几次单独查**,那样每条报错只对应一个指标,提示也能给准。以 `indicator search --keyword <指标code> --format json` 的 parameterList 为准。",
108
+ },
109
+ {
110
+ codes: ["100001", "100003"],
111
+ match: /缺少必填参数[::]?\s*reportDate/,
112
+ when: (message) => distinctFailureShapes(message) <= 1,
113
+ hint: "报错点名的指标要的是 reportDate(报告期),不是 --date 下发的 tradeDate。给每个被点名的指标各补一条 --indicator-param \"<指标code>:reportDate=YYYY-MM-DD\"(screener 用 \"F1:reportDate=...\"),--date 仍要保留——补上后 CLI 就不再为该指标注入 tradeDate。⚠️ 少数指标两个日期都要(如 div_cash_yld),补完若再报缺 tradeDate,就把 tradeDate 也显式传上。**以 `indicator search` 返回的 parameterList 为准,别按 code 前缀推断**。",
114
+ },
115
+ {
116
+ codes: ["100001", "100003"],
117
+ match: /不支持参数\s*(?:tradeDate|reportDate)/,
118
+ also: /缺少必填参数[::]?\s*tradeDate/,
119
+ when: (message) => distinctFailureShapes(message) <= 1,
120
+ hint: "服务端在同一句里**已经点名了两边**:一个键不该出现,另一个键缺了——把前者换成后者即可,别再往里加键。多半是两个日期键写反了:删掉 --indicator-param 里被拒的那个,改成 msg 里说缺的那个(如 --indicator-param \"<指标code>:tradeDate=YYYY-MM-DD\",screener 用 \"F1:tradeDate=...\")。⚠️ 别在这一步用 --indicator-param \"<指标code>:\"(「不要日期」的写法)——服务端明说了它要一个日期,那样只会撞下一个报错。",
121
+ },
122
+ {
123
+ codes: ["100001", "100003"],
124
+ match: /不支持参数\s*(?:tradeDate|reportDate)/,
125
+ notMatch: /缺少必填参数[::]?\s*(?:reportDate|tradeDate)/,
126
+ when: (message) => distinctFailureShapes(message) <= 1,
127
+ hint: "服务端只说这个键不该出现,**没说该换成哪个**——别照着「改用 reportDate」做,那多半同样被拒。报错点名的指标,其 parameterList 里没有 --date 下发的那个日期键,给每个被点名的指标各加一条 --indicator-param \"<指标code>:\"(冒号后留空)声明「本指标不要查询日期」即可,它与真实参数可以共存。跑 `indicator search --keyword <指标code> --format json` 确认还缺什么:parameterList 为空就只加这一条;还列了 fiscalYear 之类的就连同 --indicator-param \"<指标code>:fiscalYear=2025\" 一起给。⚠️ screener 上这个写法用不了(服务端会静默丢弃该指标并返 0 行),改用 `indicator cross-section` 取回来本地筛。",
128
+ },
129
+ {
130
+ codes: ["100001", "100003"],
131
+ match: /缺少必填参数[::]?\s*tradeDate/,
132
+ when: (message) => distinctFailureShapes(message) <= 1,
133
+ hint: "--date 的 tradeDate 这次没有下发——你为该指标传了 reportDate/tradeDate,或者用了 --indicator-param \"<指标code>:\" 这个「不要日期」的写法,两者都会关掉自动注入。对症改:如果这个指标两个日期都必填(如 div_cash_yld),把 tradeDate 也显式传上 --indicator-param \"<指标code>:tradeDate=YYYY-MM-DD\"(screener 用 \"F1:tradeDate=...\");如果是空冒号那条加错了指标,删掉它即可。",
154
134
  },
155
135
  ];
156
136
  export class ApiError extends CliError {
@@ -159,37 +139,24 @@ export class ApiError extends CliError {
159
139
  details;
160
140
  retryAfterMs;
161
141
  hint;
162
- constructor(message, code, statusCode, details,
163
- /** Server-specified Retry-After (ms), set on 429 responses so the transport
164
- * backoff can honor it instead of the default exponential schedule. */
165
- retryAfterMs,
166
- /** Context-specific hint that beats the generic per-code table — e.g. the
167
- * EDE fetch endpoints replace 999999's generic "系统错误,请稍后重试" with a
168
- * parameter checklist, because a wrong param name or date axis there yields
169
- * an empty TABLE rather than this code. */
170
- hintOverride) {
142
+ constructor(message, code, statusCode, details, retryAfterMs, hintOverride) {
171
143
  super(message);
172
144
  this.code = code;
173
145
  this.statusCode = statusCode;
174
146
  this.details = details;
175
147
  this.retryAfterMs = retryAfterMs;
176
- // Precedence: explicit override > message-specific > per-code. The message rule
177
- // sits in the middle because it identifies a narrower cause than the code alone,
178
- // but a call site that already knows the context still knows better.
179
148
  const byMessage = code
180
- ? MESSAGE_HINTS.find((rule) => rule.codes.includes(code) && rule.match.test(message))?.hint
149
+ ? MESSAGE_HINTS.find((rule) => rule.codes.includes(code)
150
+ && rule.match.test(message)
151
+ && (rule.also?.test(message) ?? true)
152
+ && !rule.notMatch?.test(message)
153
+ && (rule.when?.(message) ?? true))?.hint
181
154
  : undefined;
182
155
  this.hint = hintOverride ?? byMessage ?? (code ? ERROR_HINTS[code] : undefined);
183
156
  }
184
- /** Server-side correlation id from the 2026-07-17 envelope
185
- * (`{code, errorType, msg, status, data, traceId}`). Read off `details` rather
186
- * than threading a 7th positional constructor arg through every call site.
187
- * Worth surfacing: it is the only handle Gangtise support can trace a 999999 by. */
188
157
  get traceId() {
189
158
  if (!this.details || typeof this.details !== "object")
190
159
  return undefined;
191
- // Fall back to the outer envelope's id for double-wrapped (EDE) responses,
192
- // whose inner failure envelope carries no traceId of its own.
193
160
  const details = this.details;
194
161
  const value = details.traceId ?? details[ENVELOPE_TRACE_ID];
195
162
  return typeof value === "string" || typeof value === "number" ? String(value) : undefined;
@@ -4,11 +4,7 @@ import { isAsyncPending, nextPollDelayMs, POLL_MAX_ATTEMPTS } from "./asyncConte
4
4
  import { saveDownloadResult } from "./download.js";
5
5
  import { ApiError, ValidationError } from "./errors.js";
6
6
  import { isTransientError } from "./transport.js";
7
- /** Server-side upload cap (spec: ≤100MB, ≤500 pages). Checked locally so a
8
- * doomed 100MB+ upload fails instantly instead of after a long transfer. */
9
7
  export const FILE_PARSE_MAX_BYTES = 100 * 1024 * 1024;
10
- /** Upload a PDF and return its taskId. Billed at submit time (0.8 credits/page),
11
- * so every reason to reject a file is checked before the request goes out. */
12
8
  export async function submitFileParse(client, filePath) {
13
9
  const resolved = path.resolve(filePath);
14
10
  let stat;
@@ -28,9 +24,6 @@ export async function submitFileParse(client, filePath) {
28
24
  if (path.extname(resolved).toLowerCase() !== ".pdf") {
29
25
  throw new ValidationError(`Only PDF files are supported: ${filePath}`);
30
26
  }
31
- // Buffered, not streamed: undici's FormData needs a Blob, so a 100MB file peaks
32
- // at ~2× its size in memory. Acceptable against the API's own 100MB ceiling —
33
- // switch to a hand-rolled streaming multipart body if that ceiling ever rises.
34
27
  const data = await fs.readFile(resolved);
35
28
  const result = await client.uploadFile("tool.file-parse.submit", {
36
29
  filename: path.basename(resolved),
@@ -43,11 +36,6 @@ export async function submitFileParse(client, filePath) {
43
36
  }
44
37
  return String(taskId);
45
38
  }
46
- /**
47
- * One result fetch. "pending" = the server answered with the generating code
48
- * (140001 / legacy 410110); "ok" = the ZIP was written and its path printed.
49
- * Free to call — only the submit step is billed.
50
- */
51
39
  export async function fetchFileParseResult(client, taskId, output) {
52
40
  let result;
53
41
  try {
@@ -61,8 +49,6 @@ export async function fetchFileParseResult(client, taskId, output) {
61
49
  await saveDownloadResult(result, `file-parse-${taskId}`, output);
62
50
  return "ok";
63
51
  }
64
- /** Poll until the ZIP is ready. Same backoff budget as the AI async endpoints
65
- * (≈316s), which covers the documented ~3 min parse time. */
66
52
  export async function pollFileParseResult(client, taskId, output) {
67
53
  for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
68
54
  try {
@@ -70,8 +56,6 @@ export async function pollFileParseResult(client, taskId, output) {
70
56
  return "ok";
71
57
  }
72
58
  catch (error) {
73
- // The task is already paid for: a blip must not void the wait. Transient
74
- // errors consume the attempt, anything else (bad taskId, no permission) aborts.
75
59
  if (!isTransientError(error))
76
60
  throw error;
77
61
  const msg = error instanceof Error ? error.message : String(error);