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
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)) {
|
|
@@ -90,71 +82,39 @@ export function parseChoiceList(values, optionName, allowed) {
|
|
|
90
82
|
}
|
|
91
83
|
return maybeArray(values);
|
|
92
84
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
*/
|
|
85
|
+
const YEAR_FIRST_DATE = /^(\d{4})([-/]?)(\d{2})\2(\d{2})$/;
|
|
86
|
+
function normalizeYearFirstDate(value) {
|
|
87
|
+
const parts = YEAR_FIRST_DATE.exec(value);
|
|
88
|
+
return parts ? `${parts[1]}-${parts[3]}-${parts[4]}` : undefined;
|
|
89
|
+
}
|
|
121
90
|
export function parseDateOption(value, optionName) {
|
|
122
|
-
|
|
123
|
-
|
|
91
|
+
const normalized = normalizeYearFirstDate(value);
|
|
92
|
+
if (normalized === undefined) {
|
|
93
|
+
throw new ValidationError(`Invalid ${optionName}: expected YYYY-MM-DD (YYYY/MM/DD and YYYYMMDD also accepted), got "${value}" — year-last layouts are refused because the API reads them month-first (US convention): "01-07-2026" means 7 January, not 1 July`);
|
|
124
94
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// would turn a valid year 0050 into 1950 and report it as a non-existent date.
|
|
128
|
-
const [year, month, day] = value.split("-").map(Number);
|
|
129
|
-
const parsed = new Date(`${value}T00:00:00Z`);
|
|
95
|
+
const [year, month, day] = normalized.split("-").map(Number);
|
|
96
|
+
const parsed = new Date(`${normalized}T00:00:00Z`);
|
|
130
97
|
if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
|
|
131
98
|
throw new ValidationError(`Invalid ${optionName}: "${value}" is not a real calendar date`);
|
|
132
99
|
}
|
|
133
|
-
return
|
|
100
|
+
return normalized;
|
|
134
101
|
}
|
|
135
|
-
/** Commander argParser factory — `.option("--start-date <date>", desc, dateArg("--start-date"))`. */
|
|
136
102
|
export function dateArg(optionName) {
|
|
137
103
|
return (value) => parseDateOption(value, optionName);
|
|
138
104
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
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. */
|
|
105
|
+
const LOCAL_DATETIME = /^(\d{4})([-/]?)(\d{2})\2(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
|
|
106
|
+
function normalizeDatetime(value) {
|
|
107
|
+
const parts = LOCAL_DATETIME.exec(value);
|
|
108
|
+
if (!parts)
|
|
109
|
+
return value;
|
|
110
|
+
const [, y, sep, mo, d] = parts;
|
|
111
|
+
return sep === "" ? `${y}-${mo}-${d}${value.slice(8)}` : `${y}-${mo}-${d}${value.slice(10)}`;
|
|
112
|
+
}
|
|
153
113
|
function datetimeFieldsValid(value) {
|
|
154
114
|
const parts = LOCAL_DATETIME.exec(value);
|
|
155
115
|
if (!parts)
|
|
156
116
|
return false;
|
|
157
|
-
const [, y, mo, d, hh = "0", mi = "0", ss = "0"] = parts;
|
|
117
|
+
const [, y, , mo, d, hh = "0", mi = "0", ss = "0"] = parts;
|
|
158
118
|
const year = Number(y), month = Number(mo), day = Number(d);
|
|
159
119
|
if (month < 1 || month > 12)
|
|
160
120
|
return false;
|
|
@@ -164,12 +124,6 @@ function datetimeFieldsValid(value) {
|
|
|
164
124
|
return false;
|
|
165
125
|
return Number(hh) <= 23 && Number(mi) <= 59 && Number(ss) <= 59;
|
|
166
126
|
}
|
|
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
127
|
function epochMillis(value) {
|
|
174
128
|
if (/^\d{13}$/.test(value))
|
|
175
129
|
return Number(value);
|
|
@@ -185,17 +139,10 @@ export function toTimestamp13(value) {
|
|
|
185
139
|
return ts;
|
|
186
140
|
if (!datetimeFieldsValid(value))
|
|
187
141
|
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
142
|
const parts = LOCAL_DATETIME.exec(value);
|
|
192
|
-
const [, y, mo, d, hh = "0", mi = "0", ss = "0"] = parts;
|
|
143
|
+
const [, y, , mo, d, hh = "0", mi = "0", ss = "0"] = parts;
|
|
193
144
|
const year = Number(y);
|
|
194
145
|
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
146
|
const faithful = dt.getFullYear() === year && dt.getMonth() === Number(mo) - 1
|
|
200
147
|
&& dt.getDate() === Number(d) && dt.getHours() === Number(hh)
|
|
201
148
|
&& dt.getMinutes() === Number(mi) && dt.getSeconds() === Number(ss);
|
|
@@ -206,53 +153,29 @@ export function parseTimestamp13(value, optionName) {
|
|
|
206
153
|
return undefined;
|
|
207
154
|
const parsed = toTimestamp13(value);
|
|
208
155
|
if (parsed === undefined) {
|
|
209
|
-
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
|
|
156
|
+
throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator; YYYY/MM/DD and YYYYMMDD also accepted), got "${value}" — year-last layouts are refused because the API reads them month-first (US convention): "01-07-2026" means 7 January, not 1 July`);
|
|
210
157
|
}
|
|
211
158
|
return parsed;
|
|
212
159
|
}
|
|
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
160
|
export function parseDatetimeOption(value, optionName) {
|
|
231
|
-
if (epochMillis(value)
|
|
232
|
-
|
|
161
|
+
if (epochMillis(value) !== undefined)
|
|
162
|
+
return value;
|
|
163
|
+
if (!datetimeFieldsValid(value)) {
|
|
164
|
+
throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator; YYYY/MM/DD and YYYYMMDD also accepted), got "${value}" — year-last layouts are refused because the API reads them month-first (US convention): "01-07-2026" means 7 January, not 1 July`);
|
|
233
165
|
}
|
|
234
|
-
return value;
|
|
166
|
+
return normalizeDatetime(value);
|
|
235
167
|
}
|
|
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
168
|
export function datetimeArg(optionName) {
|
|
239
169
|
return (value) => parseDatetimeOption(value, optionName);
|
|
240
170
|
}
|
|
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
171
|
export function localDateString(d) {
|
|
246
172
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
247
173
|
const day = String(d.getDate()).padStart(2, "0");
|
|
248
174
|
return `${d.getFullYear()}-${month}-${day}`;
|
|
249
175
|
}
|
|
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
176
|
function parseParamSpecs(specs, option, syntax) {
|
|
255
177
|
const groups = new Map();
|
|
178
|
+
const noQueryDate = new Set();
|
|
256
179
|
for (const spec of specs) {
|
|
257
180
|
const colon = spec.indexOf(":");
|
|
258
181
|
const rest = colon === -1 ? "" : spec.slice(colon + 1);
|
|
@@ -260,6 +183,14 @@ function parseParamSpecs(specs, option, syntax) {
|
|
|
260
183
|
const lhs = colon === -1 ? "" : spec.slice(0, colon).trim();
|
|
261
184
|
const paramKey = eq === -1 ? "" : rest.slice(0, eq).trim();
|
|
262
185
|
const paramValue = eq === -1 ? "" : rest.slice(eq + 1).trim();
|
|
186
|
+
if (rest.trim() === "") {
|
|
187
|
+
if (!lhs)
|
|
188
|
+
throw new ValidationError(`Invalid ${option}: expected "${syntax}", got "${spec}"`);
|
|
189
|
+
noQueryDate.add(lhs);
|
|
190
|
+
if (!groups.has(lhs))
|
|
191
|
+
groups.set(lhs, []);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
263
194
|
if (!lhs || !paramKey) {
|
|
264
195
|
throw new ValidationError(`Invalid ${option}: expected "${syntax}", got "${spec}"`);
|
|
265
196
|
}
|
|
@@ -270,27 +201,19 @@ function parseParamSpecs(specs, option, syntax) {
|
|
|
270
201
|
}
|
|
271
202
|
params.push({ paramKey, paramValue });
|
|
272
203
|
}
|
|
273
|
-
return groups;
|
|
204
|
+
return { groups, noQueryDate };
|
|
274
205
|
}
|
|
275
|
-
// Parse repeatable `--indicator-param "code:key=value"` specs into the nested
|
|
276
|
-
// indicatorParamList the EDE cross-section / time-series endpoints expect.
|
|
277
206
|
export function parseIndicatorParams(specs) {
|
|
278
207
|
if (specs.length === 0)
|
|
279
208
|
return undefined;
|
|
280
|
-
|
|
209
|
+
const { groups, noQueryDate } = parseParamSpecs(specs, "--indicator-param", "code:key=value");
|
|
210
|
+
return [...groups].map(([indicatorCode, parameters]) => (noQueryDate.has(indicatorCode)
|
|
211
|
+
? { indicatorCode, parameters, noQueryDate: true }
|
|
212
|
+
: { indicatorCode, parameters }));
|
|
281
213
|
}
|
|
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
214
|
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
215
|
const SCREENER_FIELD_REF = /\bF[1-9][0-9]*\b/g;
|
|
290
216
|
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
217
|
function tokenizeScreenerExpression(src) {
|
|
295
218
|
const tokens = [];
|
|
296
219
|
let atom = "";
|
|
@@ -325,20 +248,6 @@ function tokenizeScreenerExpression(src) {
|
|
|
325
248
|
flush();
|
|
326
249
|
return tokens;
|
|
327
250
|
}
|
|
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
251
|
export function screenerExpressionIsEvaluable(expression, present) {
|
|
343
252
|
const tokens = tokenizeScreenerExpression(expression ?? "");
|
|
344
253
|
let pos = 0;
|
|
@@ -351,13 +260,10 @@ export function screenerExpressionIsEvaluable(expression, present) {
|
|
|
351
260
|
return value;
|
|
352
261
|
}
|
|
353
262
|
const atom = tokens[pos++] ?? "";
|
|
354
|
-
// A term naming no variable (a literal comparison) is always evaluable.
|
|
355
263
|
return (atom.replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? []).every((ref) => present.has(ref));
|
|
356
264
|
};
|
|
357
265
|
const and = () => {
|
|
358
266
|
let value = unit();
|
|
359
|
-
// Evaluate both sides before combining: short-circuiting would leave the
|
|
360
|
-
// parser mid-expression.
|
|
361
267
|
while (tokens[pos] === "&&") {
|
|
362
268
|
pos += 1;
|
|
363
269
|
const right = unit();
|
|
@@ -376,18 +282,11 @@ export function screenerExpressionIsEvaluable(expression, present) {
|
|
|
376
282
|
};
|
|
377
283
|
return or();
|
|
378
284
|
}
|
|
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
285
|
export function screenerExpressionFields(expression) {
|
|
383
286
|
return (expression ?? "").replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? [];
|
|
384
287
|
}
|
|
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
288
|
export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
390
|
-
const params = parseParamSpecs(paramSpecs, "--indicator-param", "F1:key=value");
|
|
289
|
+
const { groups: params, noQueryDate } = parseParamSpecs(paramSpecs, "--indicator-param", "F1:key=value");
|
|
391
290
|
const indicators = new Map();
|
|
392
291
|
for (const spec of bindings) {
|
|
393
292
|
const colon = spec.indexOf(":");
|
|
@@ -402,18 +301,15 @@ export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
|
402
301
|
if (indicators.has(field)) {
|
|
403
302
|
throw new ValidationError(`Duplicate --indicator variable "${field}": each variable must bind exactly one indicator`);
|
|
404
303
|
}
|
|
405
|
-
indicators.set(field,
|
|
304
|
+
indicators.set(field, noQueryDate.has(field)
|
|
305
|
+
? { field, indicatorCode, parameters: params.get(field) ?? [], noQueryDate: true }
|
|
306
|
+
: { field, indicatorCode, parameters: params.get(field) ?? [] });
|
|
406
307
|
}
|
|
407
|
-
// A param for an unbound variable is silently dropped by the server, so the
|
|
408
|
-
// query would run with a filter the caller believes is applied but is not.
|
|
409
308
|
for (const field of params.keys()) {
|
|
410
309
|
if (!indicators.has(field)) {
|
|
411
310
|
throw new ValidationError(`--indicator-param references "${field}", which no --indicator binds`);
|
|
412
311
|
}
|
|
413
312
|
}
|
|
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
313
|
for (const ref of screenerExpressionFields(expression)) {
|
|
418
314
|
if (!indicators.has(ref)) {
|
|
419
315
|
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
|
}
|