gangtise-openapi-cli 0.34.0 → 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.
- package/README.md +15 -5
- package/dist/src/cli.js +4 -268
- package/dist/src/core/args.js +19 -136
- package/dist/src/core/asyncContent.js +0 -22
- package/dist/src/core/auth.js +0 -14
- package/dist/src/core/client.js +5 -152
- package/dist/src/core/commandBodies.js +7 -35
- package/dist/src/core/download.js +0 -39
- package/dist/src/core/endpoints.js +0 -31
- package/dist/src/core/errors.js +61 -63
- 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 +45 -50
- package/dist/src/core/transport.js +0 -45
- package/dist/src/version.js +1 -2
- package/gangtise-openapi/SKILL.md +2 -2
- package/gangtise-openapi/references/commands/indicator.md +24 -5
- package/gangtise-openapi/references/commands/insight.md +26 -5
- package/gangtise-openapi/references/examples.md +2 -1
- package/package.json +1 -1
package/dist/src/core/args.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { ValidationError } from "./errors.js";
|
|
2
2
|
export function splitCsv(value) {
|
|
3
|
-
// Also split on full-width ",": voice-input IMEs produce it, and an unsplit
|
|
4
|
-
// "600519,000858" goes to the API as one bogus code with no local hint.
|
|
5
3
|
return value
|
|
6
4
|
.split(/[,,]/)
|
|
7
5
|
.map((item) => item.trim())
|
|
@@ -62,9 +60,6 @@ export function collectKeyValue(value, previous = {}) {
|
|
|
62
60
|
export function maybeArray(value) {
|
|
63
61
|
return value.length > 0 ? value : undefined;
|
|
64
62
|
}
|
|
65
|
-
/** True when `latest` is strictly newer than `current` (numeric per-segment
|
|
66
|
-
* compare). Plain inequality would nag "update available" during the
|
|
67
|
-
* just-published window while the registry still serves the previous version. */
|
|
68
63
|
export function isVersionNewer(latest, current) {
|
|
69
64
|
const parse = (v) => v.split(".").map(Number);
|
|
70
65
|
const a = parse(latest);
|
|
@@ -79,9 +74,6 @@ export function isVersionNewer(latest, current) {
|
|
|
79
74
|
}
|
|
80
75
|
return false;
|
|
81
76
|
}
|
|
82
|
-
// Whitelist for enum-valued repeatable options. Only used where the server was
|
|
83
|
-
// probed NOT to reject bad values (it silently ignores the filter or returns
|
|
84
|
-
// empty instead) — endpoints that answer 100003 keep server-side validation.
|
|
85
77
|
export function parseChoiceList(values, optionName, allowed) {
|
|
86
78
|
for (const value of values) {
|
|
87
79
|
if (!allowed.includes(value)) {
|
|
@@ -91,40 +83,10 @@ export function parseChoiceList(values, optionName, allowed) {
|
|
|
91
83
|
return maybeArray(values);
|
|
92
84
|
}
|
|
93
85
|
const YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
|
|
94
|
-
/**
|
|
95
|
-
* Strict `YYYY-MM-DD` guard for Quote/Fundamental date options.
|
|
96
|
-
*
|
|
97
|
-
* Beyond the documented shape the server accepts two extra year-last formats
|
|
98
|
-
* whose day/month order is *opposite* to each other (probed 2026-07-20 on
|
|
99
|
-
* quote/kline/daily and fundamental/balance-sheet; other groups sharing these
|
|
100
|
-
* flags were not probed, so the guard is applied uniformly as the safe default):
|
|
101
|
-
*
|
|
102
|
-
* "07/01/2026" -> 2026-01-07 slash reads DD/MM/YYYY
|
|
103
|
-
* "07-01-2026" -> 2026-07-01 hyphen reads MM-DD-YYYY
|
|
104
|
-
*
|
|
105
|
-
* Same three digits, six months apart, both HTTP 200, and nothing in the
|
|
106
|
-
* response echoes which date the server actually used. Confirmed by the
|
|
107
|
-
* complement: "25/12/2026" parses while "12/25/2026" errors, and the hyphen
|
|
108
|
-
* forms behave exactly the other way round. Since the CLI cannot know which
|
|
109
|
-
* reading was meant, it forwards only the unambiguous form.
|
|
110
|
-
*
|
|
111
|
-
* Other unambiguous shapes (`20260701`, `2026/07/01`) are rejected too, even
|
|
112
|
-
* though the server handles them — one accepted form beats a per-shape allowlist
|
|
113
|
-
* that has to be re-probed per endpoint group. The message says which form is
|
|
114
|
-
* wanted rather than claiming the input itself was ambiguous.
|
|
115
|
-
*
|
|
116
|
-
* Datetime options (`--start-time`) are guarded separately: pass-through ones by
|
|
117
|
-
* `parseDatetimeOption` (a timezone-free field check that returns the string as-is),
|
|
118
|
-
* and the two conversion endpoints (A-share announcement / knowledge-batch) by
|
|
119
|
-
* `parseTimestamp13`.
|
|
120
|
-
*/
|
|
121
86
|
export function parseDateOption(value, optionName) {
|
|
122
87
|
if (!YYYY_MM_DD.test(value)) {
|
|
123
|
-
throw new ValidationError(`Invalid ${optionName}: expected YYYY-MM-DD, got "${value}" — only that form is forwarded; some endpoints
|
|
88
|
+
throw new ValidationError(`Invalid ${optionName}: expected YYYY-MM-DD, got "${value}" — only that form is forwarded as-is; on some endpoints other layouts (e.g. "07/01/2026") are read as a different day`);
|
|
124
89
|
}
|
|
125
|
-
// Shape alone lets 2026-02-30 / 2026-13-01 through; round-trip to reject those.
|
|
126
|
-
// Built from the ISO string, not Date.UTC(y,...), whose two-digit-year mapping
|
|
127
|
-
// would turn a valid year 0050 into 1950 and report it as a non-existent date.
|
|
128
90
|
const [year, month, day] = value.split("-").map(Number);
|
|
129
91
|
const parsed = new Date(`${value}T00:00:00Z`);
|
|
130
92
|
if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
|
|
@@ -132,24 +94,10 @@ export function parseDateOption(value, optionName) {
|
|
|
132
94
|
}
|
|
133
95
|
return value;
|
|
134
96
|
}
|
|
135
|
-
/** Commander argParser factory — `.option("--start-date <date>", desc, dateArg("--start-date"))`. */
|
|
136
97
|
export function dateArg(optionName) {
|
|
137
98
|
return (value) => parseDateOption(value, optionName);
|
|
138
99
|
}
|
|
139
|
-
/** `yyyy-MM-dd` with an optional ` HH:mm[:ss]` / `THH:mm[:ss]` tail. Anything else
|
|
140
|
-
* is rejected rather than handed to `new Date()`: V8's fallback parser accepts the
|
|
141
|
-
* same year-last shapes the server does but reads them the OTHER way round —
|
|
142
|
-
* `07/01/2026` is July 1 to V8 and January 7 to the server, `25/12/2026` is invalid
|
|
143
|
-
* to V8 and valid to the server (both probed 2026-07-20). Since `announcement list`
|
|
144
|
-
* converts locally while its HK/US siblings pass the string through, an open
|
|
145
|
-
* fallback made the same flag mean two dates six months apart across sibling
|
|
146
|
-
* commands, silently and with exit 0. */
|
|
147
100
|
const LOCAL_DATETIME = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
148
|
-
/** Field-level datetime validation, timezone-free — a real calendar day plus a
|
|
149
|
-
* valid clock time, judged by arithmetic alone (no Date construction, so no
|
|
150
|
-
* dependence on the client's timezone or DST). This is what the pass-through guard
|
|
151
|
-
* needs: a string the CLI forwards verbatim must be judged on its fields, not on
|
|
152
|
-
* whether the local zone happens to contain that wall-clock instant. */
|
|
153
101
|
function datetimeFieldsValid(value) {
|
|
154
102
|
const parts = LOCAL_DATETIME.exec(value);
|
|
155
103
|
if (!parts)
|
|
@@ -164,12 +112,6 @@ function datetimeFieldsValid(value) {
|
|
|
164
112
|
return false;
|
|
165
113
|
return Number(hh) <= 23 && Number(mi) <= 59 && Number(ss) <= 59;
|
|
166
114
|
}
|
|
167
|
-
/** A 10-digit seconds or 13-digit millis epoch, normalized to millis. Judged by
|
|
168
|
-
* digit count, NOT magnitude: a `> 1e12` test sends a real 13-digit `1000000000000`
|
|
169
|
-
* (which equals 1e12) down the seconds branch, and lets Number() coerce scientific
|
|
170
|
-
* / hex / whitespace-padded / 11–12–14-digit inputs through as a "timestamp" — all
|
|
171
|
-
* of which then convert wrong or get rejected by the server. Exactly 10 or 13
|
|
172
|
-
* digits, nothing else. */
|
|
173
115
|
function epochMillis(value) {
|
|
174
116
|
if (/^\d{13}$/.test(value))
|
|
175
117
|
return Number(value);
|
|
@@ -185,17 +127,10 @@ export function toTimestamp13(value) {
|
|
|
185
127
|
return ts;
|
|
186
128
|
if (!datetimeFieldsValid(value))
|
|
187
129
|
return undefined;
|
|
188
|
-
// `new Date("yyyy-MM-dd")` parses as UTC midnight while `new Date("yyyy-MM-dd
|
|
189
|
-
// HH:mm:ss")` parses as local time — for CST users the two forms would differ by 8
|
|
190
|
-
// hours. Build from local components so both mean the same wall-clock day.
|
|
191
130
|
const parts = LOCAL_DATETIME.exec(value);
|
|
192
131
|
const [, y, mo, d, hh = "0", mi = "0", ss = "0"] = parts;
|
|
193
132
|
const year = Number(y);
|
|
194
133
|
const dt = new Date(year, Number(mo) - 1, Number(d), Number(hh), Number(mi), Number(ss));
|
|
195
|
-
// Full round-trip on every field: rejects the two inputs that cannot convert
|
|
196
|
-
// faithfully — the two-digit-year remap (new Date(50,…) → 1950) and a DST gap (a
|
|
197
|
-
// wall-clock time the local zone skips: 02:30 on a US spring-forward morning, or
|
|
198
|
-
// 02:15 in Lord Howe's 30-minute gap, which only a minute-level check catches).
|
|
199
134
|
const faithful = dt.getFullYear() === year && dt.getMonth() === Number(mo) - 1
|
|
200
135
|
&& dt.getDate() === Number(d) && dt.getHours() === Number(hh)
|
|
201
136
|
&& dt.getMinutes() === Number(mi) && dt.getSeconds() === Number(ss);
|
|
@@ -210,49 +145,23 @@ export function parseTimestamp13(value, optionName) {
|
|
|
210
145
|
}
|
|
211
146
|
return parsed;
|
|
212
147
|
}
|
|
213
|
-
/**
|
|
214
|
-
* Guard for datetime options forwarded to the server AS A STRING (never converted
|
|
215
|
-
* to a timestamp): the pass-through Insight/Vault list endpoints echo the string
|
|
216
|
-
* verbatim, and probing 2026-07-21 showed they misread year-last separators exactly
|
|
217
|
-
* like the date endpoints — `insight research list` read `07/01/2026` as 2026-01-07
|
|
218
|
-
* but `07-01-2026` as 2026-07-01, a half-year apart, both HTTP 200 with nothing in
|
|
219
|
-
* the response flagging it. Accept a finite epoch or a well-formed `YYYY-MM-DD
|
|
220
|
-
* [ HH:mm[:ss]]` and return the ORIGINAL string unchanged.
|
|
221
|
-
*
|
|
222
|
-
* Validated with `datetimeFieldsValid`, NOT `toTimestamp13`: the latter's local Date
|
|
223
|
-
* round-trip would reject a DST-gap string (e.g. `2026-03-08 02:30:00` under
|
|
224
|
-
* America/New_York) that the server accepts — the CLI forwards this string as-is and
|
|
225
|
-
* the server resolves it in its own zone, so the client's timezone must not decide
|
|
226
|
-
* validity. Distinct from `parseTimestamp13`, which DOES convert (A-share
|
|
227
|
-
* announcement / knowledge-batch want epoch millis, where an unrepresentable local
|
|
228
|
-
* instant genuinely cannot convert).
|
|
229
|
-
*/
|
|
230
148
|
export function parseDatetimeOption(value, optionName) {
|
|
231
149
|
if (epochMillis(value) === undefined && !datetimeFieldsValid(value)) {
|
|
232
150
|
throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator), got "${value}" — year-last forms are refused because the API reads their day/month order differently per separator`);
|
|
233
151
|
}
|
|
234
152
|
return value;
|
|
235
153
|
}
|
|
236
|
-
/** Commander argParser factory for pass-through datetime options — same role as
|
|
237
|
-
* `dateArg`, but allows an optional time part and keeps the string as-is. */
|
|
238
154
|
export function datetimeArg(optionName) {
|
|
239
155
|
return (value) => parseDatetimeOption(value, optionName);
|
|
240
156
|
}
|
|
241
|
-
/** Machine-local calendar date as `yyyy-MM-dd`, for CLI "default: today" options.
|
|
242
|
-
* `new Date().toISOString().slice(0,10)` renders the UTC day — for CST users a
|
|
243
|
-
* pre-08:00 "today" resolves to yesterday. Anchoring to local components matches
|
|
244
|
-
* toTimestamp13's local-midnight convention. */
|
|
245
157
|
export function localDateString(d) {
|
|
246
158
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
247
159
|
const day = String(d.getDate()).padStart(2, "0");
|
|
248
160
|
return `${d.getFullYear()}-${month}-${day}`;
|
|
249
161
|
}
|
|
250
|
-
// Parse repeatable `"<lhs>:key=value"` specs into per-lhs parameter lists.
|
|
251
|
-
// The left-hand side is an indicator code for cross-section / time-series and a
|
|
252
|
-
// screener variable (F1, F2…) for the screener, so it stays an opaque string
|
|
253
|
-
// here. Multiple specs for the same lhs accumulate, first-seen order.
|
|
254
162
|
function parseParamSpecs(specs, option, syntax) {
|
|
255
163
|
const groups = new Map();
|
|
164
|
+
const noQueryDate = new Set();
|
|
256
165
|
for (const spec of specs) {
|
|
257
166
|
const colon = spec.indexOf(":");
|
|
258
167
|
const rest = colon === -1 ? "" : spec.slice(colon + 1);
|
|
@@ -260,6 +169,14 @@ function parseParamSpecs(specs, option, syntax) {
|
|
|
260
169
|
const lhs = colon === -1 ? "" : spec.slice(0, colon).trim();
|
|
261
170
|
const paramKey = eq === -1 ? "" : rest.slice(0, eq).trim();
|
|
262
171
|
const paramValue = eq === -1 ? "" : rest.slice(eq + 1).trim();
|
|
172
|
+
if (rest.trim() === "") {
|
|
173
|
+
if (!lhs)
|
|
174
|
+
throw new ValidationError(`Invalid ${option}: expected "${syntax}", got "${spec}"`);
|
|
175
|
+
noQueryDate.add(lhs);
|
|
176
|
+
if (!groups.has(lhs))
|
|
177
|
+
groups.set(lhs, []);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
263
180
|
if (!lhs || !paramKey) {
|
|
264
181
|
throw new ValidationError(`Invalid ${option}: expected "${syntax}", got "${spec}"`);
|
|
265
182
|
}
|
|
@@ -270,27 +187,19 @@ function parseParamSpecs(specs, option, syntax) {
|
|
|
270
187
|
}
|
|
271
188
|
params.push({ paramKey, paramValue });
|
|
272
189
|
}
|
|
273
|
-
return groups;
|
|
190
|
+
return { groups, noQueryDate };
|
|
274
191
|
}
|
|
275
|
-
// Parse repeatable `--indicator-param "code:key=value"` specs into the nested
|
|
276
|
-
// indicatorParamList the EDE cross-section / time-series endpoints expect.
|
|
277
192
|
export function parseIndicatorParams(specs) {
|
|
278
193
|
if (specs.length === 0)
|
|
279
194
|
return undefined;
|
|
280
|
-
|
|
195
|
+
const { groups, noQueryDate } = parseParamSpecs(specs, "--indicator-param", "code:key=value");
|
|
196
|
+
return [...groups].map(([indicatorCode, parameters]) => (noQueryDate.has(indicatorCode)
|
|
197
|
+
? { indicatorCode, parameters, noQueryDate: true }
|
|
198
|
+
: { indicatorCode, parameters }));
|
|
281
199
|
}
|
|
282
|
-
/** Screener variables are `F` + a positive integer; the server rejects anything
|
|
283
|
-
* else, and a typo'd variable would otherwise surface as an opaque expression
|
|
284
|
-
* error rather than pointing at the binding that is wrong. */
|
|
285
200
|
const SCREENER_FIELD = /^F[1-9][0-9]*$/;
|
|
286
|
-
/** Variable references inside a screener expression, e.g. the F1/F2 in
|
|
287
|
-
* `F1 >= 800 && F2 <= 30`. String literals are stripped first, so a
|
|
288
|
-
* `contains 'F2 系列'` operand is not mistaken for a reference. */
|
|
289
201
|
const SCREENER_FIELD_REF = /\bF[1-9][0-9]*\b/g;
|
|
290
202
|
const SCREENER_STRING_LITERAL = /'[^']*'|"[^"]*"/g;
|
|
291
|
-
/** Split an expression into `(`, `)`, `&&`, `||` and the atoms between them.
|
|
292
|
-
* String literals are copied verbatim so a `||` or an `F2` inside one is never
|
|
293
|
-
* mistaken for syntax. */
|
|
294
203
|
function tokenizeScreenerExpression(src) {
|
|
295
204
|
const tokens = [];
|
|
296
205
|
let atom = "";
|
|
@@ -325,20 +234,6 @@ function tokenizeScreenerExpression(src) {
|
|
|
325
234
|
flush();
|
|
326
235
|
return tokens;
|
|
327
236
|
}
|
|
328
|
-
/** Whether the expression still has a path that could have been evaluated, given
|
|
329
|
-
* the variables the response actually returned a column for.
|
|
330
|
-
*
|
|
331
|
-
* The boolean structure decides, not the mere presence of a `||`:
|
|
332
|
-
* - a term is evaluable when every variable it names has a column;
|
|
333
|
-
* - `A && B` needs BOTH sides — a missing mandatory conjunct is an unprovable
|
|
334
|
-
* claim even if the other side is a disjunction;
|
|
335
|
-
* - `A || B` needs only one — a row can legitimately match through one operand
|
|
336
|
-
* while the other is not evaluable at all (probed 2026-08-03: `F1 > 0 || F2
|
|
337
|
-
* > 0` over 09992.HK, where `finc_pe_ttm` has no HK coverage, matches on F2).
|
|
338
|
-
*
|
|
339
|
-
* So `F1 && (F2 || F3)` missing F1, and `F1 || F2` missing both, are both
|
|
340
|
-
* unevaluable and must fail — checking only for a `||` anywhere would let them
|
|
341
|
-
* through. */
|
|
342
237
|
export function screenerExpressionIsEvaluable(expression, present) {
|
|
343
238
|
const tokens = tokenizeScreenerExpression(expression ?? "");
|
|
344
239
|
let pos = 0;
|
|
@@ -351,13 +246,10 @@ export function screenerExpressionIsEvaluable(expression, present) {
|
|
|
351
246
|
return value;
|
|
352
247
|
}
|
|
353
248
|
const atom = tokens[pos++] ?? "";
|
|
354
|
-
// A term naming no variable (a literal comparison) is always evaluable.
|
|
355
249
|
return (atom.replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? []).every((ref) => present.has(ref));
|
|
356
250
|
};
|
|
357
251
|
const and = () => {
|
|
358
252
|
let value = unit();
|
|
359
|
-
// Evaluate both sides before combining: short-circuiting would leave the
|
|
360
|
-
// parser mid-expression.
|
|
361
253
|
while (tokens[pos] === "&&") {
|
|
362
254
|
pos += 1;
|
|
363
255
|
const right = unit();
|
|
@@ -376,18 +268,11 @@ export function screenerExpressionIsEvaluable(expression, present) {
|
|
|
376
268
|
};
|
|
377
269
|
return or();
|
|
378
270
|
}
|
|
379
|
-
/** Variables the expression actually filters on, string literals stripped. These
|
|
380
|
-
* are the bindings whose VALUES the result depends on — a column missing for one
|
|
381
|
-
* of them means the filter cannot be shown to have been applied. */
|
|
382
271
|
export function screenerExpressionFields(expression) {
|
|
383
272
|
return (expression ?? "").replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? [];
|
|
384
273
|
}
|
|
385
|
-
// Parse the screener's `--indicator "F1:code"` bindings and merge each one's
|
|
386
|
-
// `--indicator-param "F1:key=value"` specs. Params key off the variable, not the
|
|
387
|
-
// code, because the same indicator may legitimately appear under two variables
|
|
388
|
-
// with different parameters (e.g. the same price on two dates).
|
|
389
274
|
export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
390
|
-
const params = parseParamSpecs(paramSpecs, "--indicator-param", "F1:key=value");
|
|
275
|
+
const { groups: params, noQueryDate } = parseParamSpecs(paramSpecs, "--indicator-param", "F1:key=value");
|
|
391
276
|
const indicators = new Map();
|
|
392
277
|
for (const spec of bindings) {
|
|
393
278
|
const colon = spec.indexOf(":");
|
|
@@ -404,16 +289,14 @@ export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
|
404
289
|
}
|
|
405
290
|
indicators.set(field, { field, indicatorCode, parameters: params.get(field) ?? [] });
|
|
406
291
|
}
|
|
407
|
-
|
|
408
|
-
|
|
292
|
+
if (noQueryDate.size > 0) {
|
|
293
|
+
throw new ValidationError(`--indicator-param "${[...noQueryDate][0]}:" (the no-date opt-out) is not supported by the screener: an indicator sent with no parameters does not take part in the screen, so it would run without that condition and answer 0 rows with no error. Read the value with 'indicator cross-section --indicator-param "<code>:"' and filter client-side.`);
|
|
294
|
+
}
|
|
409
295
|
for (const field of params.keys()) {
|
|
410
296
|
if (!indicators.has(field)) {
|
|
411
297
|
throw new ValidationError(`--indicator-param references "${field}", which no --indicator binds`);
|
|
412
298
|
}
|
|
413
299
|
}
|
|
414
|
-
// The server does reject this (100003), but only after a round trip — and the
|
|
415
|
-
// symmetric mistake (binding a variable the expression never uses) it accepts
|
|
416
|
-
// silently while still billing the extra column.
|
|
417
300
|
for (const ref of screenerExpressionFields(expression)) {
|
|
418
301
|
if (!indicators.has(ref)) {
|
|
419
302
|
throw new ValidationError(`--expression references "${ref}", which no --indicator binds`);
|
|
@@ -1,44 +1,26 @@
|
|
|
1
1
|
import { ApiError } from "./errors.js";
|
|
2
2
|
import { printData } from "./printer.js";
|
|
3
3
|
import { isTransientError } from "./transport.js";
|
|
4
|
-
// 14 attempts with exponential backoff (5s→30s cap) ≈ 316s total wait budget.
|
|
5
4
|
export const POLL_MAX_ATTEMPTS = 14;
|
|
6
5
|
const POLL_INITIAL_DELAY_MS = 5_000;
|
|
7
6
|
const POLL_MAX_DELAY_MS = 30_000;
|
|
8
7
|
export function nextPollDelayMs(attempt) {
|
|
9
|
-
// 5s, 8s, 13s, 20s, 30s, 30s, ...
|
|
10
8
|
const grown = POLL_INITIAL_DELAY_MS * 1.6 ** (attempt - 1);
|
|
11
9
|
return Math.min(POLL_MAX_DELAY_MS, Math.round(grown));
|
|
12
10
|
}
|
|
13
|
-
// Probed 2026-07-20 against a real viewpoint-debate job: the async endpoints are
|
|
14
|
-
// still entirely on the legacy codes — 410110 "正在生成中" and 410111 "生成失败",
|
|
15
|
-
// both string-typed, both HTTP 400, neither carrying the new `errorType` field.
|
|
16
|
-
// The 2026-07-17 spec renumbers them to 140001 RESULT_GENERATING (409) and
|
|
17
|
-
// 140002 PROCESSING_FAILED (500); those are listed ahead of the switchover
|
|
18
|
-
// because the failure mode is expensive and silent — a `--wait` that does not
|
|
19
|
-
// recognize the pending code aborts the poll on a job already billed 50 credits.
|
|
20
11
|
const PENDING_CODES = new Set(["410110", "140001"]);
|
|
21
12
|
const FAILED_CODES = new Set(["410111", "140002"]);
|
|
22
|
-
/** Shared by the AI content endpoints and `tool file-parse` — both report
|
|
23
|
-
* "still generating" with the same codes. */
|
|
24
13
|
export function isAsyncPending(error) {
|
|
25
14
|
return error instanceof ApiError && error.code !== undefined && PENDING_CODES.has(error.code);
|
|
26
15
|
}
|
|
27
16
|
function isAsyncFailed(error) {
|
|
28
17
|
return error instanceof ApiError && error.code !== undefined && FAILED_CODES.has(error.code);
|
|
29
18
|
}
|
|
30
|
-
/** Terminal failures are swallowed here rather than rethrown, so this line is the
|
|
31
|
-
* user's only record of them — it has to carry what the global handler in cli.ts
|
|
32
|
-
* would have printed (code, msg, traceId). Without the traceId the failure is
|
|
33
|
-
* unreportable to Gangtise support, and README promises every error line has one. */
|
|
34
19
|
function terminalFailureLine(error) {
|
|
35
20
|
const api = error instanceof ApiError ? error : undefined;
|
|
36
21
|
const code = api?.code ? ` ${api.code}` : "";
|
|
37
22
|
const trace = api?.traceId ? ` [trace ${api.traceId}]` : "";
|
|
38
23
|
const detail = api?.message ? `: ${api.message}` : "";
|
|
39
|
-
// Name the *submit* endpoint explicitly: re-running a `*-check` is a free
|
|
40
|
-
// lookup, it is resubmitting the generation job that re-bills 50 credits for a
|
|
41
|
-
// verdict that will not change (probed 2026-07-20 on sensitive content).
|
|
42
24
|
return `Content generation failed (terminal${code})${trace}${detail}\n`
|
|
43
25
|
+ "This dataId is final — re-checking it will not change. Resubmitting the generation task bills again for the same result; change the parameters first.\n";
|
|
44
26
|
}
|
|
@@ -57,10 +39,6 @@ export async function pollAsyncContent(client, getContentEndpoint, dataId, forma
|
|
|
57
39
|
return "failed";
|
|
58
40
|
}
|
|
59
41
|
if (!isAsyncPending(error)) {
|
|
60
|
-
// AI generation windows are exactly when the server is busiest: one 5xx
|
|
61
|
-
// (after the client's own retries) must not void minutes of waiting —
|
|
62
|
-
// the dataId is still valid. Transient errors consume this attempt and
|
|
63
|
-
// polling continues; anything else (no credits, bad params) aborts.
|
|
64
42
|
if (!isTransientError(error))
|
|
65
43
|
throw error;
|
|
66
44
|
const msg = error instanceof Error ? error.message : String(error);
|
package/dist/src/core/auth.js
CHANGED
|
@@ -15,13 +15,6 @@ export async function readTokenCache(filePath) {
|
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Return a display-safe copy of a token cache for `auth status`: any field whose
|
|
20
|
-
* name matches a credential pattern (token / key / secret / password / credential)
|
|
21
|
-
* is replaced with "<redacted>" so the raw bearer token — or any unknown credential
|
|
22
|
-
* field the cache file might carry (apiKey, privateKey, …) — is never printed; all
|
|
23
|
-
* other metadata (expiresAt, userName, productCode, …) is preserved.
|
|
24
|
-
*/
|
|
25
18
|
export function redactTokenCache(cache) {
|
|
26
19
|
if (!cache)
|
|
27
20
|
return null;
|
|
@@ -34,11 +27,6 @@ export function redactTokenCache(cache) {
|
|
|
34
27
|
}
|
|
35
28
|
export async function writeTokenCache(filePath, cache) {
|
|
36
29
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
37
|
-
// Write to a fresh 0600 temp file then rename over the target. Writing the bearer
|
|
38
|
-
// token straight to token.json would (a) keep an existing file's lax perms — the
|
|
39
|
-
// `mode` option only applies on creation, so a follow-up chmod still leaves a brief
|
|
40
|
-
// world-readable window — and (b) risk a truncated file on crash. A temp file is
|
|
41
|
-
// 0600 from the first byte and rename is atomic, carrying the 0600 perms over.
|
|
42
30
|
const tmp = `${filePath}.tmp-${randomUUID()}`;
|
|
43
31
|
await fs.writeFile(tmp, JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
44
32
|
try {
|
|
@@ -57,8 +45,6 @@ export function isTokenCacheValid(cache, bufferSeconds = 300) {
|
|
|
57
45
|
return cache.expiresAt - bufferSeconds > now;
|
|
58
46
|
}
|
|
59
47
|
export function normalizeToken(token) {
|
|
60
|
-
// Case-insensitive prefix check: GANGTISE_TOKEN="bearer xxx" must become
|
|
61
|
-
// "Bearer xxx", not the silently-invalid "Bearer bearer xxx".
|
|
62
48
|
const prefix = /^bearer\s+/i.exec(token);
|
|
63
49
|
return `Bearer ${prefix ? token.slice(prefix[0].length) : token}`;
|
|
64
50
|
}
|