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 +109 -32
- package/dist/src/cli.js +35 -30
- package/dist/src/core/args.js +152 -19
- package/dist/src/core/asyncContent.js +43 -7
- package/dist/src/core/client.js +38 -12
- package/dist/src/core/download.js +19 -5
- package/dist/src/core/endpoints.js +7 -0
- package/dist/src/core/errors.js +121 -20
- package/dist/src/core/indicatorMatrix.js +8 -2
- package/dist/src/core/output.js +29 -7
- package/dist/src/core/quoteSharding.js +9 -5
- package/dist/src/core/transport.js +34 -3
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +369 -0
- package/gangtise-openapi/references/commands/ai.md +149 -0
- package/gangtise-openapi/references/commands/alternative.md +102 -0
- package/gangtise-openapi/references/commands/fundamental.md +108 -0
- package/gangtise-openapi/references/commands/indicator.md +136 -0
- package/gangtise-openapi/references/commands/insight.md +188 -0
- package/gangtise-openapi/references/commands/quote.md +93 -0
- package/gangtise-openapi/references/commands/reference-and-lookup.md +229 -0
- package/gangtise-openapi/references/commands/vault.md +79 -0
- package/gangtise-openapi/references/examples.md +291 -0
- package/gangtise-openapi/references/fields.md +166 -0
- package/gangtise-openapi/references/lookup-ids.md +264 -0
- package/gangtise-openapi/references/response-schema.md +85 -0
- package/package.json +3 -2
package/dist/src/core/errors.js
CHANGED
|
@@ -10,26 +10,111 @@ 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
|
+
export const ENVELOPE_TRACE_ID = Symbol("gangtise.envelopeTraceId");
|
|
21
|
+
export function attachEnvelopeTraceId(payload, traceId) {
|
|
22
|
+
if (payload && typeof payload === "object" && (typeof traceId === "string" || typeof traceId === "number")) {
|
|
23
|
+
Object.defineProperty(payload, ENVELOPE_TRACE_ID, { value: String(traceId), enumerable: false, configurable: true });
|
|
24
|
+
}
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
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. */
|
|
13
36
|
const ERROR_HINTS = {
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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
|
+
"999001": "检查 GANGTISE_TOKEN 或 AK/SK 是否已 export。",
|
|
41
|
+
"999002": "有 AK/SK 时 CLI 会自动重新登录重试一次,否则请重新登录。",
|
|
42
|
+
"999003": "定制接口需联系客户经理开通。",
|
|
43
|
+
"999004": "换一条本账号可见的记录重试。",
|
|
44
|
+
"999005": "联系客户经理充值,或缩小查询范围降低消耗。",
|
|
45
|
+
"999006": "触发限流,稍后再试或联系客户经理提额;429 所有端点都退避重试,5xx 仅普通端点重试(贵档 no-replay 端点的 5xx 不重放,但其 429 仍重试)。",
|
|
46
|
+
// CLI 自身发不出这三类请求(method / Content-Type 都由 endpoint 定义固定),
|
|
47
|
+
// 只有 raw call 打错端点或服务端行为变化才可能撞上。实测 2026-07-20 分别落到
|
|
48
|
+
// 900002 / 999999 / 100003,此处为服务端接上新码后的预置。
|
|
49
|
+
"999007": "请求方法不支持——`raw call` 时确认该 endpoint 是 GET 还是 POST。",
|
|
50
|
+
"999008": "Content-Type 不支持,该接口只收 application/json。",
|
|
51
|
+
"999009": "请求体无法解析,检查 JSON 是否合法。",
|
|
52
|
+
"999010": "`raw call` 传的 endpoint key 可能已下线,用 `gangtise raw list` 核对。",
|
|
53
|
+
"999011": "检查 GANGTISE_ACCESS_KEY / GANGTISE_SECRET_KEY 是否写反或未 export。",
|
|
54
|
+
"999012": "联系客户经理。",
|
|
55
|
+
"999013": "联系客户经理续期。",
|
|
56
|
+
"999014": "联系客户经理。",
|
|
57
|
+
"999015": "联系客户经理开通长期 token。",
|
|
58
|
+
"999016": "联系客户经理登记当前出口 IP。",
|
|
59
|
+
"999999": "请稍后重试;持续失败请带上面的 trace 报障。",
|
|
60
|
+
// ── 业务通用 1xxxxx ──
|
|
61
|
+
"100001": "对照命令 --help 检查必填项。",
|
|
62
|
+
"100002": "检查数值/字符串参数是否传反。",
|
|
63
|
+
// 实测两种形态都有:类型/范围错的 msg 带字段(「请求体字段类型不匹配: size 期望类型
|
|
64
|
+
// Integer」「limit 最小为 1,最大为 10000」),枚举错的 msg 只有笼统的「参数值非法」。
|
|
65
|
+
// 条件句让两种形态都读得通——v0.25.0 的旧文案断言"服务端不会指明",与前一种直接打架。
|
|
66
|
+
"100003": "msg 已指明字段名或取值范围时直接按 msg 改;msg 只说「参数值非法」时多为枚举参数拼写错误(如 --source / --question-category / --answer-important),对照命令 --help 列出的合法值检查。",
|
|
67
|
+
"100004": "检查 --size / --from 是否为非负数且未超单页上限。",
|
|
68
|
+
"100005": "对照命令 --help 列出的合法取值检查。",
|
|
69
|
+
"100006": "缩短日期范围或调小 --size / --limit。",
|
|
70
|
+
// 按参数名判断,不要按命令组:AI 下 management-discuss 的 --report-date 是 date,
|
|
71
|
+
// 而同属 AI 的 knowledge-batch 收时间戳或 datetime——旧文案笼统写"AI 用 datetime"会把
|
|
72
|
+
// --report-date 的用户越导越错。
|
|
73
|
+
"110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
|
|
74
|
+
"110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
|
|
75
|
+
"110003": "缩短查询窗口后重试。",
|
|
76
|
+
"120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
|
|
77
|
+
"130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
|
|
78
|
+
"130002": "确认下载 ID 有效且本账号可见;下载类还需检查 --file-type 取值是否合法(非法 file-type 也归此码)。",
|
|
79
|
+
"130003": "该条记录可能未附带文件。",
|
|
80
|
+
// 下载类命令各有各的 ID 参数(--report-id / --announcement-id / --chunk-id /
|
|
81
|
+
// --summary-id / --conference-id / --record-id / --file-id / --article-id /
|
|
82
|
+
// --independent-opinion-id);--data-id 是异步 *-check 用的,不产生此码。
|
|
83
|
+
"130004": "下载 ID 需为数字,检查该命令的 --*-id 参数是否传对。",
|
|
84
|
+
"130005": "对照命令 --help 检查 --file-type / --content-type 取值。",
|
|
85
|
+
"140001": "稍后用对应 *-check 命令查询。",
|
|
86
|
+
"140002": "异步生成失败(终态)——换参数重新提交,重试同一 dataId 不会变。",
|
|
87
|
+
// ── 接口专有 2xxxxx ──
|
|
88
|
+
"210001": "换一篇,或改用 list 取正文摘要。",
|
|
89
|
+
"220001": "改用 list 取正文摘要。",
|
|
90
|
+
"230001": "只有自己上传的文件可下载。",
|
|
91
|
+
"240001": "换更早的 --period(如 2025q3 → 2025interim)。",
|
|
92
|
+
"240002": "改述后重新提交。",
|
|
93
|
+
"240003": "对照命令 --help 检查取值。",
|
|
94
|
+
"250001": "检查 resourceType 与 sourceId 组合(两者都来自 knowledge-batch 返回)。",
|
|
95
|
+
// ── 旧码(2026-07-20 实测仍在线,或历史遗留) ──
|
|
96
|
+
"0000001007": "请求未携带 Bearer token,检查 GANGTISE_TOKEN 或 AK/SK 是否已 export。",
|
|
20
97
|
"0000001008": "Token 已失效(多为他处登录挤掉本会话);有 AK/SK 时 CLI 会自动重新登录重试一次,否则请重新登录。",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"410004": "
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
98
|
+
"900001": "对照命令 --help 检查必填项。",
|
|
99
|
+
"900002": "请求方法不正确(服务端 msg 为「请求类型有误」)——`raw call` 时检查该 endpoint 是 GET 还是 POST。",
|
|
100
|
+
"903301": "次日再试,或联系客户经理提额。",
|
|
101
|
+
// EDE 专有旧码,未被 2026-07-17 重排收编但仍是 indicator 取数的主要报错
|
|
102
|
+
// (references/commands/indicator.md 把这两个列为首要排查项)。
|
|
103
|
+
"410001": "补齐 --indicator / --security;`time-series` 不支持「多指标 × 多证券」,改用 `indicator cross-section`。",
|
|
104
|
+
"410106": "读 `indicator search --format json` 的 parameterList,用 --indicator-param 补上 required:true 的参数(如 periodNum / startDate / fiscalYear)。",
|
|
105
|
+
"410004": "换证券或日期确认该条件下本应有数据;仍失败多为未开通该指标,联系客户经理。",
|
|
106
|
+
"410110": "稍后用对应 *-check 命令查询。",
|
|
107
|
+
"410111": "终态,换参数后重新提交,重试同一请求不会变。",
|
|
108
|
+
"430004": "确认 reportId 有效,或更换 --file-type 重试(官方未文档化错误码)。",
|
|
109
|
+
"430007": "缩短日期范围或调小 --limit。",
|
|
110
|
+
"433007": "检查 resourceType 与 sourceId 组合(两者都来自 knowledge-batch 返回)。",
|
|
111
|
+
"8000014": "检查 GANGTISE_ACCESS_KEY 是否正确、是否与 SECRET_KEY 写反。",
|
|
112
|
+
"8000015": "检查 GANGTISE_SECRET_KEY 是否正确、是否与 ACCESS_KEY 写反。",
|
|
113
|
+
"8000016": "联系客户经理核查账号状态。",
|
|
114
|
+
"8000018": "联系客户经理续期。",
|
|
115
|
+
"999995": "联系客户经理充值,或缩小查询范围降低消耗。",
|
|
116
|
+
"999997": "联系客户经理开通。",
|
|
117
|
+
"10011401": "联系客户经理开通白名单。",
|
|
33
118
|
};
|
|
34
119
|
export class ApiError extends CliError {
|
|
35
120
|
code;
|
|
@@ -40,12 +125,28 @@ export class ApiError extends CliError {
|
|
|
40
125
|
constructor(message, code, statusCode, details,
|
|
41
126
|
/** Server-specified Retry-After (ms), set on 429 responses so the transport
|
|
42
127
|
* backoff can honor it instead of the default exponential schedule. */
|
|
43
|
-
retryAfterMs
|
|
128
|
+
retryAfterMs,
|
|
129
|
+
/** Context-specific hint that beats the generic per-code table — e.g. EDE's
|
|
130
|
+
* 999999 means "no data", not the table's "系统错误,请稍后重试". */
|
|
131
|
+
hintOverride) {
|
|
44
132
|
super(message);
|
|
45
133
|
this.code = code;
|
|
46
134
|
this.statusCode = statusCode;
|
|
47
135
|
this.details = details;
|
|
48
136
|
this.retryAfterMs = retryAfterMs;
|
|
49
|
-
this.hint = code ? ERROR_HINTS[code] : undefined;
|
|
137
|
+
this.hint = hintOverride ?? (code ? ERROR_HINTS[code] : undefined);
|
|
138
|
+
}
|
|
139
|
+
/** Server-side correlation id from the 2026-07-17 envelope
|
|
140
|
+
* (`{code, errorType, msg, status, data, traceId}`). Read off `details` rather
|
|
141
|
+
* than threading a 7th positional constructor arg through every call site.
|
|
142
|
+
* Worth surfacing: it is the only handle Gangtise support can trace a 999999 by. */
|
|
143
|
+
get traceId() {
|
|
144
|
+
if (!this.details || typeof this.details !== "object")
|
|
145
|
+
return undefined;
|
|
146
|
+
// Fall back to the outer envelope's id for double-wrapped (EDE) responses,
|
|
147
|
+
// whose inner failure envelope carries no traceId of its own.
|
|
148
|
+
const details = this.details;
|
|
149
|
+
const value = details.traceId ?? details[ENVELOPE_TRACE_ID];
|
|
150
|
+
return typeof value === "string" || typeof value === "number" ? String(value) : undefined;
|
|
50
151
|
}
|
|
51
152
|
}
|
|
@@ -16,7 +16,11 @@ export function unwrapIndicatorData(raw) {
|
|
|
16
16
|
// (status/msg/data) so a non-envelope object that merely carries a `code`
|
|
17
17
|
// field can't be misread as a failure.
|
|
18
18
|
if (!ok && ("status" in record || "msg" in record || "data" in record)) {
|
|
19
|
-
|
|
19
|
+
// Pass `record` as details: the inner envelope carries no traceId of its
|
|
20
|
+
// own, but unwrapEnvelope attached the outer one to this object and
|
|
21
|
+
// ApiError.traceId falls back to it. Without it these failures — the EDE
|
|
22
|
+
// 999999 / 130001 that most need reporting — reach the user trace-less.
|
|
23
|
+
throw new ApiError(typeof record.msg === "string" && record.msg ? record.msg : "Indicator API request failed", code, undefined, record);
|
|
20
24
|
}
|
|
21
25
|
if (ok && "data" in record)
|
|
22
26
|
return record.data;
|
|
@@ -34,7 +38,9 @@ function rowOf(values, index) {
|
|
|
34
38
|
// Build one column header per series. Prefer the human-readable name; on a
|
|
35
39
|
// duplicate name append the code so a column is never silently overwritten.
|
|
36
40
|
function buildHeaders(names, codes, count) {
|
|
37
|
-
|
|
41
|
+
// Pre-seed the metadata column names: an indicator literally named "date" /
|
|
42
|
+
// "security" / "name" must get a suffixed header, not overwrite the metadata.
|
|
43
|
+
const used = new Set(["date", "security", "name"]);
|
|
38
44
|
const headers = [];
|
|
39
45
|
for (let i = 0; i < count; i++) {
|
|
40
46
|
const base = String(names?.[i] ?? codes?.[i] ?? `col${i}`);
|
package/dist/src/core/output.js
CHANGED
|
@@ -9,10 +9,11 @@ export function parseOutputFormat(value) {
|
|
|
9
9
|
throw new ConfigError(`Unsupported format: ${format}`);
|
|
10
10
|
}
|
|
11
11
|
/** Cell text for terminal-facing formats (table/markdown): newlines collapsed for
|
|
12
|
-
* alignment, remaining C0/DEL control chars stripped so server data can't inject
|
|
13
|
-
* terminal escape sequences
|
|
12
|
+
* alignment, remaining C0/DEL/C1 control chars stripped so server data can't inject
|
|
13
|
+
* terminal escape sequences into the user's terminal (U+009B is a one-byte CSI
|
|
14
|
+
* that 8-bit-control terminals treat exactly like ESC[). */
|
|
14
15
|
function sanitizeCell(value) {
|
|
15
|
-
return value.replace(/[\r\n]+/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
16
|
+
return value.replace(/[\r\n]+/g, " ").replace(/[\u0000-\u001f\u007f\u0080-\u009f]/g, "");
|
|
16
17
|
}
|
|
17
18
|
/** Terminal display width: CJK/fullwidth chars and emoji occupy 2 columns — padEnd
|
|
18
19
|
* counts UTF-16 code units and misaligns every table containing Chinese text or emoji
|
|
@@ -62,6 +63,24 @@ function toRows(value) {
|
|
|
62
63
|
}
|
|
63
64
|
return [{ value }];
|
|
64
65
|
}
|
|
66
|
+
// One huge cell (a 50KB brief/chat message) would otherwise force every row of
|
|
67
|
+
// that column to be padded to the same width — rows × width of pure spaces.
|
|
68
|
+
const MAX_CELL_DISPLAY_WIDTH = 120;
|
|
69
|
+
/** Truncate a cell to the display-width cap, ellipsis included in the budget. */
|
|
70
|
+
function clampCell(value) {
|
|
71
|
+
if (displayWidth(value) <= MAX_CELL_DISPLAY_WIDTH)
|
|
72
|
+
return value;
|
|
73
|
+
let out = "";
|
|
74
|
+
let width = 0;
|
|
75
|
+
for (const ch of value) {
|
|
76
|
+
const w = displayWidth(ch);
|
|
77
|
+
if (width + w > MAX_CELL_DISPLAY_WIDTH - 1)
|
|
78
|
+
break;
|
|
79
|
+
out += ch;
|
|
80
|
+
width += w;
|
|
81
|
+
}
|
|
82
|
+
return out + "…";
|
|
83
|
+
}
|
|
65
84
|
function renderTable(rows) {
|
|
66
85
|
if (rows.length === 0) {
|
|
67
86
|
return "(empty)";
|
|
@@ -73,8 +92,8 @@ function renderTable(rows) {
|
|
|
73
92
|
// spreading a per-row array overflows the call stack on large results (table is
|
|
74
93
|
// the default format, e.g. `quote day-kline --security all`). Widths and padding
|
|
75
94
|
// use displayWidth so CJK cells stay aligned.
|
|
76
|
-
const headerCells = columns.map(sanitizeCell);
|
|
77
|
-
const matrix = rows.map((row) => columns.map((column) => sanitizeCell(formatScalar(row[column]))));
|
|
95
|
+
const headerCells = columns.map((column) => clampCell(sanitizeCell(column)));
|
|
96
|
+
const matrix = rows.map((row) => columns.map((column) => clampCell(sanitizeCell(formatScalar(row[column])))));
|
|
78
97
|
const widths = columns.map((_, c) => matrix.reduce((max, cells) => Math.max(max, displayWidth(cells[c])), displayWidth(headerCells[c])));
|
|
79
98
|
const renderLine = (values) => values.map((value, index) => value + " ".repeat(Math.max(0, widths[index] - displayWidth(value)))).join(" ");
|
|
80
99
|
const header = renderLine(headerCells);
|
|
@@ -89,9 +108,12 @@ function renderMarkdown(rows) {
|
|
|
89
108
|
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
90
109
|
// Column names come from server data (e.g. EDE indicator display names) — escape
|
|
91
110
|
// them like cell values or a name containing | / , breaks the whole table.
|
|
92
|
-
|
|
111
|
+
// Backslash must go first: escaping only "|" turns a literal `\|` into `\\|`,
|
|
112
|
+
// which GFM reads as an escaped backslash + BARE pipe → an extra column.
|
|
113
|
+
const mdEscape = (value) => value.replaceAll("\\", "\\\\").replaceAll("|", "\\|");
|
|
114
|
+
const header = `| ${columns.map((column) => mdEscape(sanitizeCell(column))).join(" | ")} |`;
|
|
93
115
|
const divider = `| ${columns.map(() => "---").join(" | ")} |`;
|
|
94
|
-
const body = rows.map((row) => `| ${columns.map((column) => sanitizeCell(formatScalar(row[column]))
|
|
116
|
+
const body = rows.map((row) => `| ${columns.map((column) => mdEscape(sanitizeCell(formatScalar(row[column])))).join(" | ")} |`);
|
|
95
117
|
return [header, divider, ...body].join("\n");
|
|
96
118
|
}
|
|
97
119
|
function renderCsv(rows) {
|
|
@@ -153,8 +153,11 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
153
153
|
let fieldList;
|
|
154
154
|
let header = null;
|
|
155
155
|
const merged = [];
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// Record WHICH windows maxed out, not just how many: a script/agent consumer
|
|
157
|
+
// needs the concrete date ranges to re-pull narrower windows (mirrors failedShards).
|
|
158
|
+
const truncatedShards = [];
|
|
159
|
+
for (let i = 0; i < results.length; i++) {
|
|
160
|
+
const r = results[i];
|
|
158
161
|
if (!(r && typeof r === "object"))
|
|
159
162
|
continue;
|
|
160
163
|
const rec = r;
|
|
@@ -163,7 +166,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
163
166
|
if (!fieldList && Array.isArray(rec.fieldList))
|
|
164
167
|
fieldList = rec.fieldList;
|
|
165
168
|
if (isTruncated(rec))
|
|
166
|
-
truncatedShards
|
|
169
|
+
truncatedShards.push(shards[i]);
|
|
167
170
|
// Append one-by-one rather than push(...list): a future higher row cap could
|
|
168
171
|
// make a single shard's list large enough to overflow the stack via spread.
|
|
169
172
|
if (Array.isArray(rec.list))
|
|
@@ -187,9 +190,10 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
187
190
|
out.failedShards = failedShards;
|
|
188
191
|
process.stderr.write(`[gangtise] warning: ${failedShards.length}/${shards.length} shards failed; results are partial (see failedShards)\n`);
|
|
189
192
|
}
|
|
190
|
-
if (truncatedShards > 0) {
|
|
193
|
+
if (truncatedShards.length > 0) {
|
|
191
194
|
out.partial = true;
|
|
192
|
-
|
|
195
|
+
out.truncatedShards = truncatedShards;
|
|
196
|
+
process.stderr.write(`[gangtise] warning: ${truncatedShards.length}/${shards.length} shard(s) hit the ${perShardLimit}-row limit and were likely truncated; results are partial (see truncatedShards) — ${truncationHint}.\n`);
|
|
193
197
|
}
|
|
194
198
|
return out;
|
|
195
199
|
}
|
|
@@ -62,11 +62,34 @@ export async function runWithConcurrency(items, concurrency, fn) {
|
|
|
62
62
|
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
63
63
|
return results;
|
|
64
64
|
}
|
|
65
|
+
/** Parse GANGTISE_PAGE_CONCURRENCY defensively. runWithConcurrency clamps to
|
|
66
|
+
* ≥1 worker, so a negative/zero/NaN value silently degrades to SERIAL fetching
|
|
67
|
+
* (slow, confusing); an absurd value fans out up to items.length workers at
|
|
68
|
+
* once and can 429-storm the server. Fall back to the default and cap at 32. */
|
|
69
|
+
export function resolvePageConcurrency(raw, fallback = 5, max = 32) {
|
|
70
|
+
const parsed = Math.floor(Number(raw));
|
|
71
|
+
if (!Number.isFinite(parsed) || parsed < 1)
|
|
72
|
+
return fallback;
|
|
73
|
+
return Math.min(parsed, max);
|
|
74
|
+
}
|
|
65
75
|
/** Fan-out width for pagination and kline shards — one env knob tunes both. */
|
|
66
|
-
export const PAGE_CONCURRENCY =
|
|
76
|
+
export const PAGE_CONCURRENCY = resolvePageConcurrency(process.env.GANGTISE_PAGE_CONCURRENCY);
|
|
67
77
|
const RETRYABLE_HTTP_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
68
78
|
const RETRYABLE_NETWORK_CODES = new Set(["ECONNREFUSED", "ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
|
|
69
79
|
const RETRYABLE_API_CODES = new Set(["999999"]);
|
|
80
|
+
// Never replayed on any HTTP status:
|
|
81
|
+
// - 999011 CREDENTIAL_INVALID: bad AK/SK, only from auth.login (useAuth=false, so it
|
|
82
|
+
// never consults AUTH_RETRY_CODES, and it declares no retry policy) — a 5xx would
|
|
83
|
+
// otherwise be retried twice by the status rule.
|
|
84
|
+
// - 140002 PROCESSING_FAILED: the async *-check endpoints (get-content) declare no
|
|
85
|
+
// retry policy, so a 140002@500 would be retried 2× by the default policy BEFORE
|
|
86
|
+
// asyncContent's FAILED_CODES gets to call it terminal — that guard sits above
|
|
87
|
+
// client.call's withRetry and cannot see the retries. 140002 means "generation
|
|
88
|
+
// failed" (terminal by definition) and only those async endpoints can return it,
|
|
89
|
+
// so a blanket rule is safe and skips the wasted retries. Server still emits the
|
|
90
|
+
// legacy 410111 today (410111@400 isn't retryable anyway); this is a forward guard
|
|
91
|
+
// for the documented 140002@500.
|
|
92
|
+
const TERMINAL_API_CODES = new Set(["999011", "140002"]);
|
|
70
93
|
// Connect-phase / DNS failures: the request provably never reached the server, so a
|
|
71
94
|
// replay cannot double-execute (or double-bill) anything even under "no-replay".
|
|
72
95
|
const NO_REPLAY_NETWORK_CODES = new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
@@ -75,13 +98,15 @@ function isRetryableError(error, policy) {
|
|
|
75
98
|
return true;
|
|
76
99
|
}
|
|
77
100
|
if (error instanceof ApiError) {
|
|
101
|
+
if (error.code && TERMINAL_API_CODES.has(error.code))
|
|
102
|
+
return false;
|
|
78
103
|
if (error.statusCode === 429)
|
|
79
104
|
return true;
|
|
80
105
|
if (policy === "no-replay")
|
|
81
106
|
return false;
|
|
82
|
-
if (error.statusCode != null && RETRYABLE_HTTP_STATUS.has(error.statusCode))
|
|
83
|
-
return true;
|
|
84
107
|
if (error.code && RETRYABLE_API_CODES.has(error.code))
|
|
108
|
+
return policy !== "no-999999";
|
|
109
|
+
if (error.statusCode != null && RETRYABLE_HTTP_STATUS.has(error.statusCode))
|
|
85
110
|
return true;
|
|
86
111
|
return false;
|
|
87
112
|
}
|
|
@@ -100,6 +125,12 @@ function isRetryableError(error, policy) {
|
|
|
100
125
|
export function markRetryable(error) {
|
|
101
126
|
return Object.assign(error, { __retryable: true });
|
|
102
127
|
}
|
|
128
|
+
/** Errors worth waiting out (anything the default policy would retry): transient
|
|
129
|
+
* 5xx / network / timeout / 429 / 999999. Used by async polling to survive a
|
|
130
|
+
* blip without abandoning a multi-minute wait. */
|
|
131
|
+
export function isTransientError(error) {
|
|
132
|
+
return isRetryableError(error, "default");
|
|
133
|
+
}
|
|
103
134
|
export async function withRetry(fn, options = {}) {
|
|
104
135
|
const retries = options.retries ?? 2;
|
|
105
136
|
const baseDelay = options.baseDelayMs ?? 400;
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated — DO NOT EDIT
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.28.0";
|