gangtise-openapi-cli 0.31.0 → 0.33.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 +22 -11
- package/dist/src/cli.js +79 -62
- package/dist/src/core/args.js +0 -22
- package/dist/src/core/client.js +55 -8
- package/dist/src/core/commandBodies.js +11 -6
- package/dist/src/core/endpoints.js +18 -0
- package/dist/src/core/errors.js +7 -2
- package/dist/src/core/indicatorMatrix.js +31 -22
- package/dist/src/core/output.js +8 -0
- package/dist/src/core/printer.js +13 -11
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +45 -22
- package/gangtise-openapi/references/commands/indicator.md +135 -44
- package/gangtise-openapi/references/commands/insight.md +59 -5
- package/gangtise-openapi/references/commands/reference-and-lookup.md +22 -7
- package/gangtise-openapi/references/commands/vault.md +3 -3
- package/gangtise-openapi/references/examples.md +29 -3
- package/gangtise-openapi/references/lookup-ids.md +7 -5
- package/gangtise-openapi/references/response-schema.md +4 -2
- package/package.json +1 -1
package/dist/src/core/client.js
CHANGED
|
@@ -186,10 +186,26 @@ export class GangtiseClient {
|
|
|
186
186
|
});
|
|
187
187
|
if (!this.isPaginatedListResponse(firstPage)) {
|
|
188
188
|
// Shape drift (e.g. total arriving as a string) silently degrades fetch-all
|
|
189
|
-
// to a single page with no partial marker
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
189
|
+
// to a single page with no partial marker. This is NOT hypothetical: passing
|
|
190
|
+
// industryList to insight foreign-opinion / independent-opinion makes the
|
|
191
|
+
// server answer 200 + data:null, which then renders as one phantom row
|
|
192
|
+
// ({"value":null} in jsonl, a blank row in table/markdown). Verbose-only was
|
|
193
|
+
// the wrong bar — a caller who never sets GANGTISE_VERBOSE reads that row as
|
|
194
|
+
// real data, so warn on stderr unconditionally.
|
|
195
|
+
process.stderr.write(`[gangtise] warning: ${endpoint.key} is marked paginated but the first page has an unexpected shape (no numeric total + list); returning it as-is\n`);
|
|
196
|
+
// A warning only helps a human. Scripts read the exit code, and without one they
|
|
197
|
+
// cannot tell "this filter legitimately matched nothing" from "this filter is
|
|
198
|
+
// broken". Reuse exit 3 (result is not what was asked for) rather than inventing a
|
|
199
|
+
// fourth code — fetch-all silently degraded to one page either way.
|
|
200
|
+
//
|
|
201
|
+
// Applies to EVERY malformed shape, not just `null`: a string `total` truncates the
|
|
202
|
+
// result to page 1, which looks complete and is therefore worse than an obviously
|
|
203
|
+
// empty payload. All 24 paginated endpoints are genuine {total, list} lists (the
|
|
204
|
+
// odd-shaped ones like reference.constant-list are not marked paginated), so there
|
|
205
|
+
// is no legitimate response that lands here. Endpoints where `null` IS a valid
|
|
206
|
+
// answer (ai.one-pager for a security with no generated content) are unpaginated
|
|
207
|
+
// and never reach this branch.
|
|
208
|
+
process.exitCode = 3;
|
|
193
209
|
return firstPage;
|
|
194
210
|
}
|
|
195
211
|
const total = firstPage.total;
|
|
@@ -295,11 +311,42 @@ export class GangtiseClient {
|
|
|
295
311
|
if (truncatedByPageCap) {
|
|
296
312
|
process.stderr.write(`[gangtise] warning: hit the ${MAX_PAGES}-page safety cap; fetched ${collected.length} of ${total} rows. Narrow the query (e.g. a shorter date range) or pass --size to fetch a bounded subset.\n`);
|
|
297
313
|
}
|
|
314
|
+
const short = collected.length < target;
|
|
315
|
+
// `total` is not always the real row count. Three opinion endpoints report a fixed
|
|
316
|
+
// 10000 while `from=30000` still returns real rows with monotonically older publish
|
|
317
|
+
// times (the shape of Elasticsearch's default track_total_hits). A fetch-all then
|
|
318
|
+
// stops exactly at the cap with collected === total, so every completeness check
|
|
319
|
+
// below passes and the truncated export looks complete — the worst failure mode we
|
|
320
|
+
// have, because `opinion` bills 30 credits per row.
|
|
321
|
+
//
|
|
322
|
+
// Probe one row past the claimed end rather than hardcoding any number: the server
|
|
323
|
+
// can change the cap, and evidence survives that where a constant would not. Only on
|
|
324
|
+
// a genuine fetch-all that otherwise looked complete — when `total` is honest the
|
|
325
|
+
// probe comes back empty (and bills nothing, since these endpoints charge per row).
|
|
326
|
+
let totalCapped = false;
|
|
327
|
+
if (requestedSize === undefined && total > 0 && !short && !totalDrift && !truncatedByPageCap && failedPages.length === 0) {
|
|
328
|
+
try {
|
|
329
|
+
const probe = await this.requestJson(endpoint, { ...initialBody, from: total, size: 1 });
|
|
330
|
+
if (this.isPaginatedListResponse(probe) && probe.list.length > 0)
|
|
331
|
+
totalCapped = true;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// A failed probe must never fail an otherwise-complete export: we simply do not
|
|
335
|
+
// learn whether the total was capped, which is the pre-existing behaviour.
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (totalCapped) {
|
|
339
|
+
process.stderr.write(`[gangtise] warning: ${endpoint.key} reported total=${total} but rows exist past that offset — 'total' is a server-side cap, not the real count. This export is TRUNCATED at ${collected.length} rows. Narrow the query (date range / filters) and fetch in slices.\n`);
|
|
340
|
+
}
|
|
298
341
|
const out = {
|
|
299
342
|
...firstPage,
|
|
300
343
|
total,
|
|
301
344
|
list: requestedSize === undefined ? collected : collected.slice(0, requestedSize),
|
|
302
345
|
};
|
|
346
|
+
if (totalCapped) {
|
|
347
|
+
out.partial = true;
|
|
348
|
+
out.totalCapped = true;
|
|
349
|
+
}
|
|
303
350
|
// Unified completeness backstop. Whatever the cause — a failed/shape-broken page,
|
|
304
351
|
// a short later page (server page cap < maxPageSize), the MAX_PAGES cap, or `total`
|
|
305
352
|
// drifting mid-fetch — the result is partial. A short row count or a total drift each
|
|
@@ -309,7 +356,6 @@ export class GangtiseClient {
|
|
|
309
356
|
// must agree. printData maps partial → exit 3 so a script can't read a truncated export
|
|
310
357
|
// as complete. The cap and drift branches above already warned on stderr; failedPages
|
|
311
358
|
// warns below.
|
|
312
|
-
const short = collected.length < target;
|
|
313
359
|
if (short || totalDrift || failedPages.length > 0)
|
|
314
360
|
out.partial = true;
|
|
315
361
|
if (failedPages.length > 0) {
|
|
@@ -427,8 +473,9 @@ export class GangtiseClient {
|
|
|
427
473
|
},
|
|
428
474
|
}).catch((error) => {
|
|
429
475
|
// Until 2026-08-01 EDE answered a no-data query with 999999, so this hint
|
|
430
|
-
// led with "多为查询无数据". No-data now comes back as
|
|
431
|
-
//
|
|
476
|
+
// led with "多为查询无数据". No-data now comes back as a null CELL with its
|
|
477
|
+
// row and column intact (re-probed 2026-08-08), which leaves 999999 meaning
|
|
478
|
+
// an actual server fault — but the parameter checklist
|
|
432
479
|
// is still the right first move, because a wrong param name or date axis
|
|
433
480
|
// silently yields an EMPTY TABLE rather than this error. Only the data
|
|
434
481
|
// endpoints take a date/security/params; indicator.search shares the
|
|
@@ -436,7 +483,7 @@ export class GangtiseClient {
|
|
|
436
483
|
// instead of nonsensical date/scope/param guidance.
|
|
437
484
|
const isIndicatorFetch = endpoint.key === 'indicator.cross-section' || endpoint.key === 'indicator.time-series' || endpoint.key === 'indicator.screener';
|
|
438
485
|
if (isIndicatorFetch && error instanceof ApiError && error.code === '999999') {
|
|
439
|
-
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名是静默失效)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required
|
|
486
|
+
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名是静默失效)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——有效 code 无数据现在返回保留行列的 null 单元格,空表则多半是整个轴的 code 都没被识别。');
|
|
440
487
|
}
|
|
441
488
|
throw error;
|
|
442
489
|
});
|
|
@@ -36,12 +36,17 @@ export function buildStockPoolStocksBody(options) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Parameter keys that REPLACE an indicator's `tradeDate`. The 2026-08-01 EDE
|
|
38
38
|
* revision dropped the root-level `date`, so `--date` now fans out into each
|
|
39
|
-
* indicator's `tradeDate`.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
39
|
+
* indicator's `tradeDate`. A caller-supplied `reportDate` suppresses that
|
|
40
|
+
* injection for its indicator, so the two date fields are never sent together.
|
|
41
|
+
*
|
|
42
|
+
* This used to be load-bearing in a stronger sense: a report-period indicator
|
|
43
|
+
* answered a `tradeDate` with an EMPTY result rather than an error (probed
|
|
44
|
+
* 2026-08-01 on `is_op_rev_mom`). Re-probed 2026-08-08 — the server now resolves
|
|
45
|
+
* a `tradeDate` to the enclosing report period, and `is_op_rev_mom` returns the
|
|
46
|
+
* same 33.4903 either way. Kept regardless: sending the caller's own date field
|
|
47
|
+
* rather than a second one alongside it is the honest request, and it survives a
|
|
48
|
+
* rollback. Injecting into an indicator that takes no date at all is harmless —
|
|
49
|
+
* `pty_op_scope` still answers normally with a stray `tradeDate` attached.
|
|
45
50
|
*
|
|
46
51
|
* `sDate` is deliberately NOT here. It is an interval START, not a substitute:
|
|
47
52
|
* `qte_vol_intvl` declares `tradeDate` required (the interval END) and `sDate`
|
|
@@ -49,6 +49,24 @@ const ENDPOINT_DEFS = {
|
|
|
49
49
|
// 50/篇 — same price tier as the AI Agent calls; billing probed non-idempotent.
|
|
50
50
|
retry: "no-replay",
|
|
51
51
|
},
|
|
52
|
+
"insight.pamirs-summary.list": {
|
|
53
|
+
method: "POST",
|
|
54
|
+
path: "/application/open-insight/pamirs-summary/getList",
|
|
55
|
+
kind: "json",
|
|
56
|
+
description: "List Pamirs expert summaries (requires the expert-summary database)",
|
|
57
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
58
|
+
},
|
|
59
|
+
"insight.pamirs-summary.download": {
|
|
60
|
+
method: "GET",
|
|
61
|
+
path: "/application/open-insight/pamirs-summary/download/file",
|
|
62
|
+
kind: "download",
|
|
63
|
+
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
|
+
retry: "no-replay",
|
|
69
|
+
},
|
|
52
70
|
"insight.roadshow.list": {
|
|
53
71
|
method: "POST",
|
|
54
72
|
path: "/application/open-insight/schedule/roadshow/getList",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -40,7 +40,7 @@ const ERROR_HINTS = {
|
|
|
40
40
|
"999001": "检查 GANGTISE_TOKEN 或 AK/SK 是否已 export。",
|
|
41
41
|
"999002": "有 AK/SK 时 CLI 会自动重新登录重试一次,否则请重新登录。",
|
|
42
42
|
"999003": "定制接口需联系客户经理开通。",
|
|
43
|
-
"999004": "
|
|
43
|
+
"999004": "整库/整接口未开通(如帕米尔专家纪要需单独购买),或该条记录本账号不可见——list 撞上多为前者,先确认该数据库是否已开通。",
|
|
44
44
|
"999005": "联系客户经理充值,或缩小查询范围降低消耗。",
|
|
45
45
|
"999006": "触发限流,稍后再试或联系客户经理提额;429 所有端点都退避重试,5xx 仅普通端点重试(贵档 no-replay 端点的 5xx 不重放,但其 429 仍重试)。",
|
|
46
46
|
// CLI 自身发不出这三类请求(method / Content-Type 都由 endpoint 定义固定),
|
|
@@ -72,7 +72,9 @@ const ERROR_HINTS = {
|
|
|
72
72
|
// --report-date 的用户越导越错。
|
|
73
73
|
"110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
|
|
74
74
|
"110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
|
|
75
|
-
|
|
75
|
+
// 超出的是账号数据权限的时间下界,不是单次窗口太宽——`--fiscal-year 2015` 这种
|
|
76
|
+
// 整个区间都在界外的请求,怎么缩窗口都还是这个码(实测 2026-08-08)。
|
|
77
|
+
"110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
|
|
76
78
|
"120001": "用 `gangtise reference securities-search` 确认代码与后缀(如 600519.SH / 00700.HK)。",
|
|
77
79
|
"130001": "未找到数据——先核对查询条件;EDE 指标端点此码也可能是未开通该指标权限,仍失败联系客户经理。",
|
|
78
80
|
"130002": "确认下载 ID 有效且本账号可见;下载类还需检查 --file-type 取值是否合法(非法 file-type 也归此码)。",
|
|
@@ -91,6 +93,9 @@ const ERROR_HINTS = {
|
|
|
91
93
|
"210001": "换一篇,或改用 list 取正文摘要。",
|
|
92
94
|
"220001": "改用 list 取正文摘要。",
|
|
93
95
|
"230001": "只有自己上传的文件可下载。",
|
|
96
|
+
// 2026-08-07 新增码。私域模块,`vault wechat-*` 就在这个模块下且明确要求
|
|
97
|
+
// 「已绑定并激活群消息助理」,所以本 CLI 够得着,不能当作不可达而不给提示。
|
|
98
|
+
"230002": "微信账号未绑定——群消息类接口要求先在 Gangtise 端绑定并激活群消息助理、且助理已入群。",
|
|
94
99
|
"240001": "换更早的 --period(如 2025q3 → 2025interim)。",
|
|
95
100
|
"240002": "改述后重新提交。",
|
|
96
101
|
"240003": "对照命令 --help 检查取值。",
|
|
@@ -116,19 +116,27 @@ function indicatorHeaders(indicators, keyBy, reserved) {
|
|
|
116
116
|
}
|
|
117
117
|
const CROSS_SECTION_COLUMNS = ["security", "name"];
|
|
118
118
|
/** A response carrying neither securities nor indicators: the query as a whole
|
|
119
|
-
* resolved to nothing. This is NOT a dropped axis
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
119
|
+
* resolved to nothing. This is NOT a dropped axis, so it must not be reported as
|
|
120
|
+
* partial — calling every requested code "omitted" would be false metadata, the
|
|
121
|
+
* diff against the request is total by construction.
|
|
122
|
+
*
|
|
123
|
+
* What it MEANS changed on 2026-08-07: a genuine no-data answer (non-trading
|
|
124
|
+
* date, uncovered market, future date) now comes back as a null cell with its
|
|
125
|
+
* row and column intact, so an all-empty matrix no longer means "no data". It
|
|
126
|
+
* now means nothing in the request RESOLVED — every security code or every
|
|
127
|
+
* indicator code was unrecognised — or a parameter name is wrong. The caller's
|
|
128
|
+
* stderr note says exactly that.
|
|
123
129
|
*
|
|
124
130
|
* The check covers every STRUCTURAL array, not just the two axis lists, because
|
|
125
131
|
* accepting anything looser would let a malformed payload — `values: null`, a
|
|
126
132
|
* missing `values`, or dates with no matrix — pass as "legitimately empty" and
|
|
127
133
|
* exit 0, which is precisely the protocol regression this release's shape guards
|
|
128
134
|
* exist to catch. Required empty: `securityCodeList`, `indicatorList`, `values`;
|
|
129
|
-
* plus `dates` when present. Probed 2026-08-02
|
|
130
|
-
*
|
|
131
|
-
* all), hence "absent or
|
|
135
|
+
* plus `dates` when present. Probed 2026-08-02 (back when this shape still meant
|
|
136
|
+
* "no data"): a time-series all-empty answer is five empty arrays, a
|
|
137
|
+
* cross-section one is four (it carries no `dates` key at all), hence "absent or
|
|
138
|
+
* empty" rather than a flat count. The STRUCTURE is unchanged; only its meaning
|
|
139
|
+
* moved — since 2026-08-07 it signals an unresolved axis, not missing data.
|
|
132
140
|
*
|
|
133
141
|
* `securityNameList` is deliberately NOT checked: it holds display labels, not
|
|
134
142
|
* structure, so a `null` there cannot misalign anything — and rejecting it would
|
|
@@ -186,8 +194,9 @@ export function checkScreenerBindings(data, requested, expression) {
|
|
|
186
194
|
}
|
|
187
195
|
// Securities came back, so the filter ran. A column vanishing is never "it got
|
|
188
196
|
// filtered out" — filtering removes securities (rows), never indicators
|
|
189
|
-
// (columns)
|
|
190
|
-
//
|
|
197
|
+
// (columns). Since 2026-08-07 a covered-but-empty indicator keeps a null
|
|
198
|
+
// column, so an absent column means the server did not resolve that indicator
|
|
199
|
+
// code at all. What that implies depends on how the expression combines terms:
|
|
191
200
|
//
|
|
192
201
|
// The answer follows the expression's BOOLEAN STRUCTURE, not the mere presence
|
|
193
202
|
// of a `||` — see screenerExpressionIsEvaluable. If no branch survives the
|
|
@@ -203,19 +212,19 @@ export function checkScreenerBindings(data, requested, expression) {
|
|
|
203
212
|
// correctness intact.
|
|
204
213
|
return missing;
|
|
205
214
|
}
|
|
206
|
-
/** What the caller asked for that the response does not contain.
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* security
|
|
214
|
-
*
|
|
215
|
+
/** What the caller asked for that the response does not contain.
|
|
216
|
+
*
|
|
217
|
+
* The server used to drop any axis it had no DATA for, which made this a
|
|
218
|
+
* coverage check. Re-probed 2026-08-08: a coverage gap is now padded with `null`
|
|
219
|
+
* and keeps its row and column, down to the 1×1 case (`finc_pb_mrq` × 09992.HK,
|
|
220
|
+
* `mgn_bal` × 00700.HK — null, present, alone in the request). What still
|
|
221
|
+
* disappears is a code the server cannot RESOLVE: an unknown indicator code, or
|
|
222
|
+
* a security code with the wrong market suffix (`AAPL.US` vanishes, `AAPL.O`
|
|
223
|
+
* returns).
|
|
215
224
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* than requested with exit code 0.
|
|
225
|
+
* So the remaining shape is the dangerous one: a misspelled code is invisible
|
|
226
|
+
* otherwise — `--key-by code` batch mapping finds no key at all rather than a
|
|
227
|
+
* null, and the pull quietly returns fewer rows than requested with exit code 0.
|
|
219
228
|
*
|
|
220
229
|
* Universe entries with no `.` are skipped — those are sector IDs, which the
|
|
221
230
|
* server expands into constituents, so their absence from the response is
|
|
@@ -408,7 +417,7 @@ export function flattenTimeSeries(data, keyBy = "name", requestedUniverse) {
|
|
|
408
417
|
// no other guard notices.
|
|
409
418
|
// Without this, dates-with-no-series still passes the later shape check (0 rows
|
|
410
419
|
// for 0 series) and emits one row per date carrying nothing but the date. A
|
|
411
|
-
//
|
|
420
|
+
// response that resolved to nothing empties `dates` too (probed 2026-08-02), so dates
|
|
412
421
|
// alongside a missing axis is always a broken response.
|
|
413
422
|
if (dates.length > 0 && (securityCode.length === 0 || indicators.length === 0)) {
|
|
414
423
|
throw new ApiError(`Indicator matrix shape mismatch: the time-series response carries ${dates.length} dates but ${securityCode.length} securities and ${indicators.length} indicators — the data could not be attributed`, undefined, undefined, data);
|
package/dist/src/core/output.js
CHANGED
|
@@ -51,6 +51,14 @@ function rowsFromList(list) {
|
|
|
51
51
|
return list.map((item, index) => ({ index, value: item }));
|
|
52
52
|
}
|
|
53
53
|
function toRows(value) {
|
|
54
|
+
// A null/undefined payload means "no data", not "one row whose only column is
|
|
55
|
+
// null". Without this guard it fell through to [{ value }] and jsonl emitted
|
|
56
|
+
// {"value":null} — a phantom record that makes `wc -l` report 1. Real case:
|
|
57
|
+
// insight foreign-opinion --industry answers 200 + data:null.
|
|
58
|
+
// Scalars (0, "", false) are left alone; only the two empty values short-circuit.
|
|
59
|
+
if (value === null || value === undefined) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
54
62
|
if (Array.isArray(value)) {
|
|
55
63
|
return rowsFromList(value);
|
|
56
64
|
}
|
package/dist/src/core/printer.js
CHANGED
|
@@ -28,16 +28,12 @@ export async function printData(data, format, output, cache) {
|
|
|
28
28
|
const listLen = Array.isArray(meta.list) ? meta.list.length : 0;
|
|
29
29
|
process.stderr.write(`Total: ${meta.total}, showing: ${listLen}\n`);
|
|
30
30
|
}
|
|
31
|
-
// Incomplete
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
// only the screener's duplicate-indicator defect, where every binding of a
|
|
38
|
-
// repeated code is resolved from the EARLIEST date among them, so the one
|
|
39
|
-
// surviving number need not belong to the variable it is labelled with.
|
|
40
|
-
if (meta.partial === true || meta.unreliable === true) {
|
|
31
|
+
// Incomplete results exit with code 3: the table/csv/jsonl renderers only
|
|
32
|
+
// emit the rows, so without a distinct exit code a script or AI consumer
|
|
33
|
+
// cannot tell such an export from a clean one. `partial` means rows are
|
|
34
|
+
// missing — failed pages/shards, a row cap, or an EDE response that dropped
|
|
35
|
+
// whole securities/indicators because it did not recognise those codes.
|
|
36
|
+
if (meta.partial === true) {
|
|
41
37
|
process.exitCode = 3;
|
|
42
38
|
}
|
|
43
39
|
}
|
|
@@ -57,5 +53,11 @@ export async function printData(data, format, output, cache) {
|
|
|
57
53
|
return;
|
|
58
54
|
}
|
|
59
55
|
warnIfLargeInMemory(items, format);
|
|
60
|
-
|
|
56
|
+
const rendered = renderOutput(normalized, format);
|
|
57
|
+
// An empty render means "no rows at all". Appending the newline anyway would put
|
|
58
|
+
// one blank line on stdout — `wc -l` reports 1 and `while read` yields one empty
|
|
59
|
+
// record, which is exactly the phantom row the null-payload guard in toRows()
|
|
60
|
+
// removes. Table/markdown render "(empty)" so they are unaffected.
|
|
61
|
+
if (rendered)
|
|
62
|
+
process.stdout.write(`${rendered}\n`);
|
|
61
63
|
}
|
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.33.0";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gangtise-openapi
|
|
3
|
-
version: "0.
|
|
3
|
+
version: "0.33.0"
|
|
4
4
|
description: |-
|
|
5
5
|
通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
|
|
6
6
|
|
|
@@ -30,13 +30,14 @@ description: |-
|
|
|
30
30
|
6.1. **日 K 仅历史**:`day-kline` / `day-kline-hk` / `day-kline-us` **不返回盘中实时数据**。当日数据入库时间:A 股 ~15:30 / 港股 ~16:30 / 美股 ~07:00(北京时间)。需要盘中快照请走 `quote realtime`。
|
|
31
31
|
6.2. **多标的日 K 不自动分片**:只有 `--security all` 才按日切片提额;显式传多个 `--security` 时走单请求(默认 `--limit 6000` / 上限 10000)。**v0.23.0 起:返回行数撞上 `--limit` 时结果会标 `partial`、退出码 3、stderr 警告**(不再静默截断;`--limit` 超 10000 本地直接报错)。仍建议先估 标的数 × 交易日数,接近/超 6000 → 逐只分开拉、或显式 `--limit 10000` 并按日期区间分批。
|
|
32
32
|
7. **CLI 已内置自动化,不要手动复刻**:
|
|
33
|
-
- 翻页 → 首页拿 total
|
|
33
|
+
- 翻页 → 首页拿 total 后剩余页并发拉取;🔴 **全量拉取结束会多探一行验证 `total` 是不是服务端封顶**(`opinion` / `foreign-opinion` / `independent-opinion` 的 `total` 恒为 10000 但实际远不止)——探到就标 `partial` + `totalCapped` + 退出 3,**这时导出的是截断结果,要缩小时间范围分片拉**
|
|
34
34
|
- K 线 `--security all` 跨日期 → 自动按日切片并合并
|
|
35
35
|
- 5xx / `429` / 网络错误 / `999999` → 自动指数退避重试(🔴 贵档端点例外:仅连接失败 / 429 / token 自愈重试,5xx/超时不重放防重复扣分,v0.26.0;`indicator` 端点对 `999999` 不重试,v0.27.0)
|
|
36
36
|
- Token 失效(`0000001008` / `999002`,含已废弃的 `8000014`/`8000015`)→ 自动重新登录并重试一次;凭证错 `999011` → **不重试**(AK/SK 不对不会自己好),查环境变量
|
|
37
37
|
8. **参数命名差异**:Insight/Quote/Vault 用 `--security`,Fundamental/AI 用 `--security-code`(例外:`ai stock-summary` 用 `--security`,`ai security-clue` 用 `--gts-code`)。
|
|
38
38
|
9. **调试**:`--verbose` 或 `GANGTISE_VERBOSE=1` 打印每个请求的耗时/字节数到 stderr。
|
|
39
39
|
10. **`--field` 字段名必须核对,不确定就别传**(返回全量最稳):`quote realtime` / `fundamental main-business` / `valuation-analysis` 遇到不存在的字段名时,上游只丢**值**、字段名照请求**回显**,按位置拍平会把值贴到错误的字段上(实测 realtime 传 `close`——它没有这个字段——换手率 28.5573 被贴成 `close`,茅台真实价 1297.41)。v0.28.3 起 CLI 检测到长度不匹配直接报错(退出码 1):**带 `--field` 的命令看到这个报错,先去 `references/fields.md` 核对字段名**(没有 `--field` 的命令如 `alternative edb-data` 报此错则是上游响应结构异常,报障时若报错末尾附了 `(trace …)` 就一并带上)。另:realtime **无 `close`**(用 `latestPrice`)、**无市值**(总市值走 `indicator cross-section --indicator qte_mkt_cptl`,2026-08-03 起 A/港/美股均有数)。
|
|
40
|
+
11. 🔴 **EDE 取不到数时给的是占位值,而 `0` 和 `null` 都可能**(`indicator` 截面 / 时序 / 选股):多数指标填 `null`,但个别——最常用的是 `is_dnrpnp` 扣非归母净利润——填 **`0`**,且这是**指标属性、与日期对不对无关**(日期落在报告期末时,覆盖不到的证券如美股同样返 `0`,行还在、无告警、退出码 0)。**`null` 通常被聚合跳过,`0` 会照常穿过比较与比率计算**:`screener` 的 `F1 > 0` 可能筛出空集(读起来像「没有盈利的股票」),时序整列求均值可能差几十倍。所以:报告期类指标的日期一律落**报告期末**;拿 `is_dnrpnp` 这类做筛选或聚合前先单查确认不是占位;**别对时序整列直接求均值 / 求和**。详见 `references/commands/indicator.md`。
|
|
40
41
|
|
|
41
42
|
## 工作流(3 步)
|
|
42
43
|
|
|
@@ -65,7 +66,7 @@ description: |-
|
|
|
65
66
|
|
|
66
67
|
### 积分计费速查
|
|
67
68
|
|
|
68
|
-
"免费"=0
|
|
69
|
+
"免费"=0 积分;**只列单价**,数据范围见下一节。
|
|
69
70
|
|
|
70
71
|
- **免费**:所有 `quote` 行情、`fundamental` 报表/主营/估值/股东(**盈利预测除外**)、`reference`/`constant` 查询(含 `official-account-search`)、`alternative edb-search`、`vault`(record/wechat/股票池/drive/AI云盘)、`insight report-image list`
|
|
71
72
|
- **0.1/条 list**:research / foreign-report / official-account / announcement(A/港/美) / summary / qa / performance-calendar 的 list、`vault my-conference-list`;`insight report-image download` 0.1/张
|
|
@@ -74,8 +75,23 @@ description: |-
|
|
|
74
75
|
- **按页**:`tool file-parse` 0.8/页,**提交(`--file`)时按实际页数一次性扣**,取结果(`file-parse-check`)免费——50 页 PDF = 40 积分,别重复提交同一文件
|
|
75
76
|
- 🔴 **按次贵**:`ai knowledge-batch` 10、`management-discuss-*` 10;AI Agent(`one-pager` / `investment-logic` / `peer-comparison` / `research-outline` / `earnings-review` / `viewpoint-debate` / `theme-tracking`)**50/次**;`ai hot-topic` 50/篇
|
|
76
77
|
- 🔴 **极贵**:`alternative concept-info` / `concept-securities` **500/次**
|
|
77
|
-
- ⚠️ **同参数重复调用不免费**:按次计费无缓存命中豁免(2026-07-11 实测 `one-pager` 重复调用每次扣分,即使秒回缓存内容)——生成类结果拿到后自行留存复用,别为"刷新"重调;CLI 已对上述 🔴 贵档端点关闭 5xx
|
|
78
|
+
- ⚠️ **同参数重复调用不免费**:按次计费无缓存命中豁免(2026-07-11 实测 `one-pager` 重复调用每次扣分,即使秒回缓存内容)——生成类结果拿到后自行留存复用,别为"刷新"重调;CLI 已对上述 🔴 贵档端点关闭 5xx/超时自动重放,50/篇 的 `summary` / `foreign-report` / `my-conference` download、单价未公布但保守同档处理的 `pamirs-summary` download、以及 `tool file-parse` 的提交,同样不重放(共 18 个端点),正是为防重复扣分
|
|
78
79
|
- **按单元格**:`indicator cross-section` / `time-series` / `screener`(A股 0.05 / 港股 0.1 / 美股 0.2 积分每 100 单元格;screener 按**筛选前**范围计费,见 `indicator.md`);`ai knowledge-resource-download` 按下游资源计费
|
|
80
|
+
- **单价未公布**:`insight pamirs-summary list` / `download`——spec 只写了「需购买专家纪要数据库」这个准入门槛,没给单次价格。**别据此假定免费**;大批量拉取前先小量试,或向平台确认
|
|
81
|
+
|
|
82
|
+
### 数据范围(能查多久)
|
|
83
|
+
|
|
84
|
+
2026-08-07 官方放宽后的**正式账号**口径(下表是官方公布值;本机验证账号另开了 10 年定制权限,所以实测范围比这宽,**别拿本仓库的实测数字当平台口径**):
|
|
85
|
+
|
|
86
|
+
| 命令组 | 可回溯 |
|
|
87
|
+
|--------|--------|
|
|
88
|
+
| `quote` 行情 / `fundamental` 财报、主营、估值、股东 / `indicator`(EDE) | 前溯 **5 年**(原 3 年) |
|
|
89
|
+
| `ai security-clue` 投研线索 | 前溯 **1 个月**(原 7 天) |
|
|
90
|
+
| 主题 / 热点 / QA / 日程(路演·调研·策略会·论坛)/ 纪要 / 观点 / 研报 / 公众号 | 前溯 **3 个月**(原 1 个月) |
|
|
91
|
+
| 管理层讨论 / A·港·美股公告 / `alternative edb-*` 行业指标 | 前溯 **3 年**(原 1 年) |
|
|
92
|
+
| `insight pamirs-summary` 帕米尔纪要 | **不限**(但需单独购买专家纪要库) |
|
|
93
|
+
|
|
94
|
+
⚠️ **这是官方口径,不是硬边界**:实际范围按账号等级而定,**同一账号下按接口还可能不同**(实测 `indicator screener` 仍卡 today−3 年,而同模块的 `cross-section`/`time-series` 已放宽)。超范围查询返回 `110003`,**不是空结果**——拿到 `110003` 就是撞了权限边界,缩窗口对「整段都在界外」的查询无效,要把日期移进范围或联系客户经理。
|
|
79
95
|
|
|
80
96
|
### 下载规则(`--file-type` / `--content-type`)
|
|
81
97
|
|
|
@@ -85,6 +101,7 @@ description: |-
|
|
|
85
101
|
| `insight foreign-report download` | `--file-type` | `1` PDF / `2` MD / `3` 中译 PDF / `4` 中译 MD |
|
|
86
102
|
| `insight announcement download` | `--file-type` | `1` PDF / `2` Markdown |
|
|
87
103
|
| `insight summary download` | `--file-type`(可选) | `1` 原始(默认)/ `2` HTML(仅会议平台来源) |
|
|
104
|
+
| `insight pamirs-summary download` | `--file-type`(可选) | `1` 原始(默认)/ `2` HTML(仅此两种) |
|
|
88
105
|
| `insight independent-opinion download` | `--file-type` **必选** | `1` 原文 HTML / `2` 翻译 HTML |
|
|
89
106
|
| `insight announcement-hk download` | `--file-type` | `1` 原始(默认)/ `2` Markdown |
|
|
90
107
|
| `insight announcement-us download` | `--file-type` | `1` 原始 PDF(默认)/ `2` Markdown |
|
|
@@ -104,6 +121,7 @@ description: |-
|
|
|
104
121
|
| 外资机构观点 / 外资券商观点 | `insight foreign-opinion list` |
|
|
105
122
|
| 外资独立观点 / 独立分析师观点 | `insight independent-opinion list` |
|
|
106
123
|
| 纪要 / 会议纪要(外部) | `insight summary list` |
|
|
124
|
+
| 帕米尔纪要 / 帕米尔专家纪要 / Pamirs | `insight pamirs-summary list`(专家纪要库,需单独购买;筛选项比 `summary` 少,无 `--source`/`--institution`/`--participant-role`) |
|
|
107
125
|
| 路演 / 调研 / 策略会 / 论坛 | `insight roadshow / site-visit / strategy / forum list` |
|
|
108
126
|
| 财报日历 / 业绩预告 / 业绩快报 / 财报披露排期 | `insight performance-calendar list`(**用 `--start-date`/`--end-date`,不是 `--start-time`**;全表 >12 万条,CLI 强制要求日期范围 / `--security` / `--size` 三者至少其一;只给 `--security` 时另有 1000 行隐式上限,撞上限且 total 还有剩余=筛选可能没生效(标 `partial`、退出码 3),改用日期范围重查。下载原文 `performance-calendar download --performance-report-id`,仅 `hasAttachment: true` 可下) |
|
|
109
127
|
| A 股公告 / 公告 | `insight announcement list` |
|
|
@@ -161,19 +179,21 @@ description: |-
|
|
|
161
179
|
| PDF 转 Markdown / 解析文件 / 提取 PDF 正文 | `tool file-parse --file <x.pdf> --wait`(异步,0.8 积分/页,提交时扣;取结果 `tool file-parse-check --task-id`。**平台自有研报/公告优先用各 download 的 `--file-type 2` 直出 Markdown**,别花解析费) |
|
|
162
180
|
|
|
163
181
|
**易混淆消歧**:
|
|
164
|
-
- "纪要" → 外部信息走 `insight summary
|
|
182
|
+
- "纪要" → 外部信息走 `insight summary`;明确点名"帕米尔 / Pamirs"才走 `insight pamirs-summary`(另一个库,不是 `summary` 的子集);公司内部录音/会议走 `vault my-conference`
|
|
165
183
|
- "搜索 X" → 数据维度精确(按行业/券商)走对应 `insight ... list`;跨类型语义搜索走 `ai knowledge-batch`
|
|
166
184
|
- 港股代码用在 `insight foreign-opinion --security` 还是 `quote day-kline-hk --security`?前者要"境外"格式(`UBER.N`),后者要 `.HK`
|
|
167
185
|
- "成分股" → 题材深度(分组/重点标记/纳入理由)走 `alternative concept-securities`;板块(行业/概念分类树,纯代码名单)走 `reference sector-constituents`
|
|
168
186
|
- **证券基本面 / 指标先按任务形态路由,不是搜到 EDE 就一律走 EDE**:
|
|
169
187
|
- 单证券先优先对应 `fundamental` 专用命令(财务、估值、盈利预测、股东、主营或完整三大报表,多数免费 / 低价)。其中 `valuation-analysis` / `earning-forecast` 实测仅支持 A 股,港 / 美股的**估值历史分位**与**盈利预测**当前无可用口径。但**估值指标本身别照抄旧结论**:2026-08-03 实测 `finc_pe_ttm` 港股已有数、`qte_mkt_cptl`/`shr_tot` 港美股都已有数。⚠️ **凡「仅 A 股」「无数据」这类否定结论都只是某时点抽查**,服务端在持续补数据;负面结论过期不会报错、只会让你白白拒掉一个能跑的查询。**一律以当次 `scopeList` + 抽查为准**
|
|
170
|
-
- 多证券批量取一组**已实现**财务 / 估值指标 → 优先 `indicator search` 后用 EDE 一次拉取,替代逐只循环;单日或同一报告期横向比较用 `cross-section`,区间走势用 `time-series`(后者不能多指标 × 多证券同时)。**批量按 code 回填加 `--key-by code`**(列头用 `indicatorCode
|
|
188
|
+
- 多证券批量取一组**已实现**财务 / 估值指标 → 优先 `indicator search` 后用 EDE 一次拉取,替代逐只循环;单日或同一报告期横向比较用 `cross-section`,区间走势用 `time-series`(后者不能多指标 × 多证券同时)。**批量按 code 回填加 `--key-by code`**(列头用 `indicatorCode`,防同名指标碰撞)。⚠️ **两个轴的顺序规则不同**:`indicatorList` = 请求顺序,但 **`securityCodeList` 是按代码升序重排的**(请求 `000858,600519,000001` → 回 `000001,000858,600519`)——**行绝不能按请求下标对位**,一律按 `security` 字段取值
|
|
189
|
+
- ⚠️ **估值指标的历史序列:两个接口的财报口径切换时点不同**——`indicator time-series`(EDE)按**正式财报披露日**切,`fundamental valuation-analysis` 按**业绩快报**口径切、通常更早,所以同一天取到的估值指标可能不一样(已验证 `finc_pe_ttm`/`peTtm`;`finc_pb_mrq` 等非 TTM 口径同样在报告期节点变化,规则未单独验证)。做估值分位 / 回测时**两个接口都拉一遍交叉核**,尤其业绩大幅变动的标的(抽查中出现过个别标的在 `valuation-analysis` 侧长期未更新)。分叉时用「总市值 ÷ PE 反推隐含净利润」对照利润表滚动 TTM,即可判出哪侧是陈值。**判断某财报在某日是否已公开,查 `insight announcement list` 的公告披露日**,不要用 `fundamental income-statement` 返回的 `announcementDate`(实测该字段会把季报披露日填成年报日)。
|
|
171
190
|
- 始终排除 EDE:A股盈利预测 / 一致预期(含预测 EPS)→ `fundamental earning-forecast`;A股估值历史分位 → `fundamental valuation-analysis`;开高低收 / 成交量等行情与 K 线 → `quote`;单证券完整报表 → 对应三大报表命令。**例外:总市值只有 EDE 有**——`quote realtime` / `day-kline` 都不返回市值,走 `indicator cross-section --indicator qte_mkt_cptl`(2026-08-03 起 A/港/美股均有数,默认单位「元」,用 `--scale` 缩放)。EDE 搜到的基本 / 稀释 EPS 是已实现值,**不能冒充预测 EPS**;港 / 美股缺少上述专用能力时应如实说明不支持,不能用别的语义代替
|
|
172
191
|
- EDE 取数前必须用 `search --format json` 同时核对:`indicatorName` + `description` 语义准确、`scopeList` 覆盖全部目标市场 / 证券类型、`parameterList` 必填参数与枚举可满足;`scopeList` 缺失 / `null` / 空或任一项不符,都视为无法证明覆盖并回退专用接口。专用接口也不覆盖目标市场时,说明当前不可用,不要硬调。`scopeList` 按指标各不相同,不能因 EDE 服务支持 A / 港 / 美股就假定某个指标三市场都覆盖
|
|
173
192
|
- `indicator search` 免费,`cross-section` / `time-series` 按单元格计费;除多证券批量的效率收益外,仍优先免费 / 低价的 `quote` 或 `fundamental`
|
|
174
193
|
- 行业 / 宏观指标(空调销量、社融等,无证券维度)走 `alternative edb-*`(EDB),不要与证券级 EDE 混用
|
|
175
|
-
- EDE
|
|
176
|
-
- **EDE 指标参数名一律以 `indicator search --format json` 的 `parameterList`
|
|
194
|
+
- **EDE「没数据」和「代码写错」是两回事**(2026-08-08 实测):①代码有效但无数据 / 无覆盖 / 未来日期 → **行列都保留**(含单指标 × 单证券的 1×1)并给一个占位单元格,**退出码 0**。🔴 **占位值不统一、由指标决定**:多数是 `null`,但个别(最常用的是 `is_dnrpnp` 扣非归母净利润)填 **`0`**,而 `0` 会照常穿过比较与聚合——`screener` 的 `F1 > 0` 因此可能筛出空集、整列求均值可能差几十倍。**别把 `0` 当真值**,详见 `references/commands/indicator.md`;②**指标 code 认不出来** → 该指标整列消失;③**证券 code 认不出来**(拼错,或美股用了 `.US` 而非 `.O`/`.N`)→ 该证券整行消失——②③ CLI 标 `partial: true` + `omittedIndicators`/`omittedSecurities` 并**退出码 3**(**`screener` 例外见下**),`--key-by code` 回填时 key 会直接不存在;④请求里没有任何可解析的 code → **空表**,退出码 0 + stderr 提示。**所以退出码 3 现在读作「你有 code 写错了」,不是「这批数据不完整」**——⚠️ 但它**需要同批有对照物**:整个轴都写错(比如只查一个指标而它拼错了)拿到的是空表 + 退出码 0,所以**空表的第一嫌疑是拼写和证券后缀,不是没数据**;反过来,真实的覆盖缺口也留在表里(就是那个占位单元格),多数情况不用再靠「和已知有数的标的一起查」做对照——⚠️ **但只有 `null` 那一档真的一眼可见**,填 `0` 的指标(`is_dnrpnp`)无覆盖时与真值无法区分,对照法仍然必要。拿到空表先查证券后缀和 code 拼写,再查参数名。日期语义按指标分两类:财务报表指标=报告期末(可为非交易日)、`finc_pe_ttm` / `finc_pb_mrq` 等日频估值=最新交易日(⚠️ `finc_pb_mrq` 是日频的,用报告期末日期会取到几个月前的陈值);混合取数按各自有效日期分次 `cross-section` 再按证券合并,别塞进同一个 `--date`。详见 `references/commands/indicator.md`
|
|
195
|
+
- **EDE 指标参数名一律以 `indicator search --format json` 的 `parameterList` 为准**,不要凭记忆或照抄示例:**参数名写错不报错、按默认值取数**——如复权参数是 `adjustType`,写成 `adjustmentType` 会返回不复权价,看着正常实则口径不对。`--security` 支持板块 ID(`reference sector-search` 的 10 位 `sectorId`;`--indicator` 只收指标编码)。✅ 根级 `--scale` 污染不声明 `scale` 的指标(`qte_close` 被 `--scale 8` 缩成 `0`)**已于 2026-08-08 修复**,价格与金额可以混查了
|
|
196
|
+
- **2026-08-07 新增指标**:融资融券 21 个 `mgn_*`(`mgn_bal` 两融余额 / `mgn_fin_*` 融资 / `mgn_sl_*` 融券 / `mgn_flag` 是否标的,**仅 A 股有数**);行业分类 `scr_indu`(一个指标覆盖申万/中信/恒生/GICS 四套,**必填** `industryType` 1-4 + `industryLevel` 0-4,A/港/美股均支持,返回字符串)
|
|
177
197
|
- "业绩点评"双义消歧:**检索已有**(研报/纪要里的业绩点评内容)走 `insight ... list --llm-tag earningsReview`(0.1/条);**AI 现生成**一份走 `ai earnings-review`(异步、50/次)。不确定问一句
|
|
178
198
|
|
|
179
199
|
## 公司名 → 证券代码
|
|
@@ -245,7 +265,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
245
265
|
|
|
246
266
|
日期参数**按参数名判断、不按命令组**(命令组会误导——AI 里既有 `--start-time` 又有 `--date`/`--report-date`;Insight 里 `performance-calendar` 是唯一用 `--start-date`/`--end-date` 的 list):名字带 `-date` 的(`--start-date`/`--end-date`/`--date`/`--report-date`)一律 `YYYY-MM-DD`,覆盖 Quote/Fundamental、`insight performance-calendar`、AI 的 `theme-tracking`(`--date`)/`hot-topic`/`management-discuss-*`(`--report-date`)、Alternative `edb-data`、Indicator `cross-section`(`--date`)/`time-series`;名字带 `-time` 的(`--start-time`/`--end-time`)用 `YYYY-MM-DD[ HH:mm[:ss]]`(秒可省、空格或 `T` 分隔)或 10/13 位时间戳,覆盖 Insight/Vault 各 list、`quote minute-kline`、`ai security-clue`、`ai knowledge-batch`。其中 **A 股公告(`insight announcement list`)与 `knowledge-batch` 会把输入转成 13 位毫秒**(10 位秒自动 ×1000),其余 `-time` 命令(含 `announcement-hk`/`announcement-us`)原样透传字符串;CLI 输入统一接受 10/13 位纯数字或 `YYYY-MM-DD[ HH:mm[:ss]]`(同上:秒可省、空格或 `T` 分隔)。
|
|
247
267
|
|
|
248
|
-
支持时间倒序的命令加 `--rank-type 2`:opinion / summary / research / foreign-report / announcement / announcement-hk / announcement-us / foreign-opinion / independent-opinion / official-account
|
|
268
|
+
支持时间倒序的命令加 `--rank-type 2`:opinion / summary / pamirs-summary / research / foreign-report / announcement / announcement-hk / announcement-us / foreign-opinion / independent-opinion / official-account(⚠️ **只实测过 `pamirs-summary` / `summary` / `research` 三个,另外 8 个未验证、别外推**。在这三个上:`--rank-type 2` 是严格时间倒序;`1`=综合排序只在带 `--keyword` 时才有差异,且强弱不同——`pamirs-summary --search-type 2` 是真正的相关度重排,`summary`/`research` 的 `1` 仍是时间倒序、只改变同一时间戳内几条的先后。未验证的端点按后者的保守预期对待。详见 `insight.md` 开头)。其他 list 命令按 API 默认排序。
|
|
249
269
|
|
|
250
270
|
## 异常处理
|
|
251
271
|
|
|
@@ -264,7 +284,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
264
284
|
| 错误码 | 含义 | CLI 行为 | Agent 是否介入 |
|
|
265
285
|
|--------|------|---------|--------------|
|
|
266
286
|
| ✅ `100003` | 参数值非法——**最宽的兜底码**:类型错、`limit` 越界都归这里。**msg 通常已指明字段**(如「请求体字段类型不匹配: size 期望类型 Integer」「limit 最小为 1,最大为 10000」),先读 msg 再猜 | — | 按 msg 指的字段改;msg 没指明才对照 `--help` 查枚举拼写,**不要重试同命令** |
|
|
267
|
-
| ✅ `999999` | 系统错误。**`indicator`(EDE)2026-08-01
|
|
287
|
+
| ✅ `999999` | 系统错误。**`indicator`(EDE)2026-08-01 前用此码表示查询无数据,之后改为返回数据体**,所以这个码基本只剩真故障。⚠️ 现在「无数据」是保留行列的占位单元格(多为 `null`,个别指标是 `0`,见 `indicator.md`),**不是空表**;空表另有含义(整轴 code 未识别) | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | 确认参数无误仍报此码即服务端故障 |
|
|
268
288
|
| ✅ `410110` | **异步生成中**(HTTP 400,旧码未切)。新码 `140001`,CLI 两码都认 | 轮询视为 pending | 继续等 |
|
|
269
289
|
| ✅ `410111` | **异步生成失败**(HTTP 400,旧码未切)。新码 `140002`,CLI 两码都认 | 终态 | **不重试**,换参数 |
|
|
270
290
|
| ✅ `130002` | 资源不存在——**下载类的兜底码**:`reportId` 不存在 / 非数字 / `fileType` 非法**全归这里**(`130003`/`130004`/`130005` 实测均未启用) | — | 确认 ID 有效且本账号可见;换 `--file-type` 或换一篇验证 |
|
|
@@ -272,21 +292,23 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
272
292
|
| ✅ `100001` | 缺必填参数——**msg 带字段名**(「缺少必填参数: reportId」) | — | 按 msg 指的字段补上 |
|
|
273
293
|
| ✅ `110001` / `110002` | 日期格式错(msg 带字段名)/ 起晚于止。**哪个格式报错、哪个被静默误读是端点相关的**(实测 `fundamental` 对 `2020/01/01` 报 110001,`insight research list` 对 `30/06/2025` 却宽松解析返回数据)——别按命令组预判 | — | 按参数名:`--*-date` 用 `YYYY-MM-DD`、`--*-time` 用 `YYYY-MM-DD HH:mm:ss`;`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒 |
|
|
274
294
|
| ✅ `120001` | 证券代码无效——msg 带原因(「非有效A股」)。**只有 Fundamental 系报**,Quote 系静默返回空 | — | `reference securities-search` 确认代码与后缀(`600519.SH` / `00700.HK`) |
|
|
295
|
+
| ✅ `110003` | **超出账号数据权限的时间范围**。范围按账号等级而定、**不是平台常量**,且**同一账号下按接口还可能不一样**——本机 10 年定制权限账号上实测 `indicator screener` 仍卡 today−3 年,而同模块的 `cross-section`/`time-series` 已放宽(正式账号是否同样如此未验证) | — | 把日期移进权限范围内;整个区间都早于下界时缩短窗口无用(`--fiscal-year 2015` 无论怎么缩都报错)。`screener` 撞界改用 `cross-section` 拉数再本地筛,否则联系客户经理开通 |
|
|
275
296
|
| ✅ `100006` | 查询/下载数量超限——**取代旧 `430007`**;实测 `fund-flow` 全市场不传日期即此码 | — | 缩短日期范围或调小 `--size`/`--limit`;全市场场景应已自动分片 |
|
|
276
297
|
| ✅ `240001` | 财报期未披露或超出查询期(`earnings-review` 提交阶段就报,**不扣积分**) | — | 换更早的 `--period`(`2025q3` → `2025interim`) |
|
|
277
298
|
| ✅ `250001` | 不支持的数据源——**取代旧 `433007`** | — | 检查 `resourceType + sourceId` 组合 |
|
|
278
299
|
| ✅ `999011` | 开发账号凭证无效——**取代旧 `8000014`/`8000015`,已合并,不再区分 AK 错还是 SK 错** | 登录即失败,**不重试** | 检查 `GANGTISE_ACCESS_KEY`/`GANGTISE_SECRET_KEY` 是否写反或未 export |
|
|
279
300
|
| ✅ `999010` | 接口地址不存在 | — | `raw call` 的 key 可能已下线,用 `gangtise raw list` 核对 |
|
|
301
|
+
| ✅ `999004` | 无资源权限。**整库未开通与单条记录不可见都走这个码**——`insight pamirs-summary list`(专家纪要库需单独购买)未开通时即报此码 | — | `list` 撞上多为整库未开通,先确认该数据库是否已购买;`download` 撞上再考虑换一条本账号可见的记录 |
|
|
280
302
|
| ✅ `0000001008` | Token 服务端失效(他处登录挤掉)——**旧码未切,token 自愈依赖它** | **强制重新登录并重试一次** | 无 AK/SK 时无法自愈,提示重新登录 |
|
|
281
303
|
| ✅ `0000001007` | 请求未携带 Bearer token | — | 检查 `GANGTISE_TOKEN` / AK/SK 是否已 export |
|
|
282
|
-
| ✅ `900002` | **请求方法不正确**(msg「请求类型有误」,HTTP 405
|
|
304
|
+
| ✅ `900002` | **请求方法不正确**(msg「请求类型有误」,HTTP 405) | — | `raw call` 时确认该 endpoint 是 GET 还是 POST |
|
|
283
305
|
| ✅ `140002` | 终态参数错:AI 异步生成失败,或 `indicator` 的指标必填参数缺失 / 枚举越界 / 表达式语法错(实测 2026-08-02) | **不重试**(终态码) | 按 msg 改参数重提;EDE 的参数名与枚举读 `indicator search --format json` 的 `parameterList` |
|
|
284
306
|
|
|
285
307
|
**⚠️ 实测发现的坑(都是"不报错"型,最难发现)**
|
|
286
|
-
- 🔴 **日期只写 `YYYY-MM-DD`、时间只写 `YYYY-MM-DD HH:mm:ss`(或 10/13 位时间戳);CLI v0.28.0 起 date 与 datetime 两类、含所有 insight/vault 透传参数都本地拦截**。服务端对「年在后」格式**日月顺序随分隔符翻转**且静默误解析(HTTP 200、不回显实际用的日期):`07/01/2026`(斜杠)读成 **2026-01-07**、`07-01-2026`(横杠)读成 **2026-07-01**,差半年。实测 `insight research list --start-time`:`07/01/2026`
|
|
308
|
+
- 🔴 **日期只写 `YYYY-MM-DD`、时间只写 `YYYY-MM-DD HH:mm:ss`(或 10/13 位时间戳);CLI v0.28.0 起 date 与 datetime 两类、含所有 insight/vault 透传参数都本地拦截**。服务端对「年在后」格式**日月顺序随分隔符翻转**且静默误解析(HTTP 200、不回显实际用的日期):`07/01/2026`(斜杠)读成 **2026-01-07**、`07-01-2026`(横杠)读成 **2026-07-01**,差半年。实测 `insight research list --start-time`:`07/01/2026` 与 `07-01-2026` 的命中数相差数倍,分别与标准写法 `2026-01-07` / `2026-07-01` 逐位吻合;`quote day-kline`/`kline-hk`/`kline-us`/`index`、`fundamental balance-sheet` 同理。v0.28.0 前透传命令(research/summary/announcement-hk/us/vault/minute-kline 等)**静默放行**,且同值在本地转时间戳的 `announcement`(A 股)与透传的 hk/us 之间还会差半年、都 exit 0。现在全部在发请求前报 `ValidationError`,**但绕过 CLI 直连接口务必自己保证格式**
|
|
287
309
|
- **财报接口的日期按「报告期末」过滤**,不是公告日:`fundamental balance-sheet` 等的 `--start-date`/`--end-date` 匹配的是 `endDate` 字段(如 `20200630`),响应里的 `announcementDate`(如 `20200729`)只是公告日。**查某期财报要传季度末日期**(`2020-06-30` / `2020-03-31` / `2020-09-30` / `2020-12-31`);传 `2020-07-01` 这类非报告期日期会返回 0 行,属正常行为,不是没数据
|
|
288
310
|
- 🔴 **Quote 系对非法证券代码不报错**,静默返回 `total:0` 空列表——无法区分"代码写错"和"该票该区间真无数据"。**空结果先回头核对代码后缀**。Fundamental 系会正常报 `120001`
|
|
289
|
-
- **枚举值拼错、分页参数越界服务端不报错**——静默忽略该条件返回全量/正常结果。所以 `100004`/`100005`
|
|
311
|
+
- **枚举值拼错、分页参数越界服务端不报错**——静默忽略该条件返回全量/正常结果。所以 `100004`/`100005` 实测触发不到。最坏一例:非法 `--search-type` 会**连带吞掉 `--keyword`**(`summary list --keyword 茅台 --search-type 99` 拿到的是**未经筛选的全库**,与不传 `--keyword` 的结果逐位相同,而不是搜索结果)。**v0.32.0 起 CLI 本地拦截**:全部 `--search-type` / `--rank-type`(19 处)、全部 `--file-type`(9 处下载,`foreign-report` 为 1–4、其余 1–2)、`pamirs-summary` 的 `--category` / `--market`、`--top` 上限、以及 `reference securities-search` / `institution-search` / `official-account-search` 的 `--category`。**仍未覆盖**:`insight research/summary --category`、`--market`、`--source`、`--llm-tag` 等仍是自由字符串,拼错不报错也不生效——这些要自己核对。**拼错的筛选条件会伪装成"结果正常",枚举拼写要自己保证**
|
|
290
312
|
- **`viewpoint-debate` 传敏感内容不会被提前拦截**——实测不返回 `240002`,而是照常受理、扣满 50 积分、生成阶段才以 `410111` 失败。**提交前自己把关措辞**
|
|
291
313
|
- **`ai one-pager` 的非法 `mode` 被静默忽略**,照常生成并扣 50 积分
|
|
292
314
|
|
|
@@ -296,13 +318,13 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
296
318
|
|--------|------|---------|
|
|
297
319
|
| `999001` / `999002` | 缺 token / token 无效 | 实际返回旧码 `0000001007` / `0000001008` |
|
|
298
320
|
| `999007` / `999008` / `999009` | 方法/媒体类型/请求体不支持 | 实际返回 `900002` / `999999` / `100003` |
|
|
299
|
-
| `999003` / `
|
|
321
|
+
| `999003` / `999005` / `999006` | 无接口权限 / 积分不足 / 限流 | 未构造出(需特定账号状态) |
|
|
300
322
|
| `999012`–`999016` | 账号禁用/过期、租户失效、无长期 token、IP 不合规 | 未构造出 |
|
|
301
|
-
| `100002` / `100004` / `100005` | 类型错 / 分页非法 / 枚举非法 | 类型错归 `100003
|
|
302
|
-
| `110003` | 超出时间范围限制 | 未触发(1900 年至今的范围仍正常返回) |
|
|
323
|
+
| `100002` / `100004` / `100005` | 类型错 / 分页非法 / 枚举非法 | 类型错归 `100003`;后两者不报错,该条件按未传处理 |
|
|
303
324
|
| `130003` / `130004` / `130005` | 无文件可下 / ID 非数字 / 文件类型不支持 | 全部归 `130002` |
|
|
304
325
|
| `140001` / `140002` | 结果生成中 / 处理失败 | 异步端点仍用 `410110` / `410111` |
|
|
305
326
|
| `210001` / `220001` / `230001` | 研报/观点/分享文件不支持下载 | 未构造出 |
|
|
327
|
+
| `230002` | 微信账号未绑定(私域,2026-08-07 新增)。⚠️ **`vault wechat-*` 就在该模块下、够得着**:要求先绑定并激活群消息助理且助理已入群 | 未构造出(本账号已绑定) |
|
|
306
328
|
| `240002` / `240003` | 敏感词 / 模式不支持 | 敏感词走 `410111`;`one-pager` 的非法 `mode` 被静默忽略 |
|
|
307
329
|
| `903301` / `10011401` | 今日调用上限 / 白名单未开通 | 历史遗留,**均未实测触发**。不臆断对应新码——`10011401` 按语义更接近 `999003`(未开通接口权限)而非 `999016`(IP 限制),别据此去查 IP |
|
|
308
330
|
|
|
@@ -315,11 +337,12 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
315
337
|
|
|
316
338
|
**其他场景**:
|
|
317
339
|
- CLI 未安装 → `npm install -g gangtise-openapi-cli`
|
|
318
|
-
- **退出码 3 =
|
|
319
|
-
-
|
|
320
|
-
-
|
|
321
|
-
- **`screener` 的缺列按表达式的布尔结构判**:把缺列的变量当作「无法求值」,看整个表达式**是否还有一条能成立的分支**——`A && B` 要两边都可求值,`A || B` 只要一边。**一条分支都不剩 → 退出码 1、不输出任何结果**(返回的行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过);**还有分支可求值,或缺的只是未参与表达式的辅助变量 → `partial` + 退出码 3**。所以 `F1 || F2` 缺 F1 是降级(行可能靠 F2 正当命中),缺两个则是致命;`F1 && (F2 || F3)` 缺 F1 仍是致命(F1
|
|
322
|
-
- 拿到 3
|
|
340
|
+
- **退出码 3 = `partial: true`,结果少了数据**(保留已取到的部分;stderr 有 warning,`--format json` 才看得见标记,table/csv/jsonl 只有数据行、看不出问题):
|
|
341
|
+
- 触发场景:翻页/K线分片有页失败、服务端返回行数与 `total` 矛盾(提前短页)、或 **EDE 有 code 没被服务端认出来**。附带定位字段:页失败 `failedPages`;分片失败 `failedShards`、分片撞行数上限 `truncatedShards`(均带日期区间可缩窗补拉);EDE 为 `omittedIndicators` / `omittedSecurities`
|
|
342
|
+
- **EDE 的这两个字段现在读作「这些 code 服务端不认识」**——先查拼写和证券后缀(美股 `.O`/`.N`,不是 `.US`),而不是查 `scopeList` 覆盖:真的没覆盖会返占位单元格(多数 `null`、个别指标 `0`)、退出码 0
|
|
343
|
+
- **`screener` 的缺列按表达式的布尔结构判**:把缺列的变量当作「无法求值」,看整个表达式**是否还有一条能成立的分支**——`A && B` 要两边都可求值,`A || B` 只要一边。**一条分支都不剩 → 退出码 1、不输出任何结果**(返回的行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过);**还有分支可求值,或缺的只是未参与表达式的辅助变量 → `partial` + 退出码 3**。所以 `F1 || F2` 缺 F1 是降级(行可能靠 F2 正当命中),缺两个则是致命;`F1 && (F2 || F3)` 缺 F1 仍是致命(F1 是必选合取项)。⚠️ **前提是服务端返回了命中行**——零命中时没有行需要质疑,一律是「nothing matched」+ **退出码 0**,哪怕变量整个求不了值(实测 `F1:拼错的码` 单绑就是空集 + 退出 0)。**空集别当成「条件成立但无标的符合」**,先核对指标码
|
|
344
|
+
- 拿到 3 就必须告知用户缺了哪段,不能当成功静默继续
|
|
345
|
+
- (历史:条件选股把同一指标绑到多个变量曾触发 `unreliable: true` + `duplicatedIndicators`,因服务端把所有绑定按最早日期取数。2026-08-08 服务端已修复,该标记**已从 CLI 移除**,这类查询现在正常可用)
|
|
323
346
|
- 空结果(list 为空数组) → 建议扩大时间范围、换关键词、去掉部分筛选
|
|
324
347
|
- 模糊公司名匹配多只("平安" → 中国平安 / 平安银行 / ...) → 列出让用户选
|
|
325
348
|
- 下载文件路径冲突 → 询问覆盖
|
|
@@ -337,7 +360,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
337
360
|
**`list` 全空但参数看着对**
|
|
338
361
|
1. 时间窗太窄 → 扩到 30 天试
|
|
339
362
|
2. `--security` 后缀拼错(如 `300750` 漏了 `.SZ`)
|
|
340
|
-
3. 行业 ID 用错体系:`--industry`(用 `citicIndustry` 码 `1008001xx`)/ `--research-area
|
|
363
|
+
3. 行业 ID 用错体系:`--industry`(用 `citicIndustry` 码 `1008001xx`)/ `--research-area`(行业同样用 `citicIndustry` 码 `1008001xx`,方向才用 `gangtiseIndustry` 码 `122000xxx`——`gangtiseIndustry` 里**只有 6 条方向码、没有行业**)/ `--gts-code`(申万 `821xxx.SWI`)。申万数字码 `104xx0000` 用于 `--research-area` 时多数端点返 0,详见 `references/commands/reference-and-lookup.md`
|
|
341
364
|
4. `--rating` / `--category` 等枚举值拼错(参考对应命令的 references 文件)
|
|
342
365
|
|
|
343
366
|
**`999011` 凭证无效**(旧码 `8000014`/`8000015`;服务端已合并为一个码,不再指明是 AK 错还是 SK 错,**登录直接失败、CLI 不重试**)
|