gangtise-openapi-cli 0.34.1 → 0.36.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 +41 -12
- package/dist/src/cli.js +4 -268
- package/dist/src/core/args.js +45 -149
- package/dist/src/core/asyncContent.js +0 -22
- package/dist/src/core/auth.js +0 -14
- package/dist/src/core/client.js +0 -151
- package/dist/src/core/commandBodies.js +10 -38
- package/dist/src/core/download.js +0 -39
- package/dist/src/core/endpoints.js +0 -31
- package/dist/src/core/errors.js +53 -86
- package/dist/src/core/fileParse.js +0 -16
- package/dist/src/core/indicatorMatrix.js +0 -207
- package/dist/src/core/normalize.js +0 -14
- package/dist/src/core/output.js +0 -56
- package/dist/src/core/printer.js +0 -19
- package/dist/src/core/quoteSharding.js +0 -58
- package/dist/src/core/titleCache.js +0 -71
- package/dist/src/core/transport.js +0 -45
- package/dist/src/version.js +1 -2
- package/gangtise-openapi/SKILL.md +10 -7
- package/gangtise-openapi/references/commands/ai.md +3 -1
- package/gangtise-openapi/references/commands/indicator.md +23 -3
- package/gangtise-openapi/references/commands/insight.md +26 -5
- package/gangtise-openapi/references/examples.md +2 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
package/dist/src/core/errors.js
CHANGED
|
@@ -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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
*
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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 上是同一个写法,只是按变量写:--indicator-param \"F1:\"。",
|
|
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)
|
|
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);
|