gangtise-openapi-cli 0.28.2 → 0.29.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 +55 -2
- package/dist/src/cli.js +115 -9
- package/dist/src/core/asyncContent.js +5 -3
- package/dist/src/core/client.js +39 -12
- package/dist/src/core/endpoints.js +34 -0
- package/dist/src/core/fileParse.js +87 -0
- package/dist/src/core/normalize.js +32 -8
- package/dist/src/core/transport.js +20 -0
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +13 -7
- package/gangtise-openapi/references/commands/fundamental.md +5 -0
- package/gangtise-openapi/references/commands/indicator.md +13 -2
- package/gangtise-openapi/references/commands/insight.md +19 -0
- package/gangtise-openapi/references/commands/quote.md +4 -0
- package/gangtise-openapi/references/commands/tool.md +40 -0
- package/gangtise-openapi/references/commands/vault.md +3 -1
- package/gangtise-openapi/references/examples.md +35 -1
- package/gangtise-openapi/references/fields.md +3 -0
- package/gangtise-openapi/references/response-schema.md +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
## Changelog
|
|
6
6
|
|
|
7
|
+
### v0.29.0 — 2026-07-25
|
|
8
|
+
|
|
9
|
+
对齐服务端 2026-07-24 更新:新增财报日历(列表 + 原文下载)与 PDF 解析工具,群消息补 `quoteMsg` 引用字段。
|
|
10
|
+
|
|
11
|
+
**新增**
|
|
12
|
+
- `insight performance-calendar list` — 财报日历:业绩预告 / 业绩快报 / 业绩公告三类事件,按 `--start-date`/`--end-date`(过滤 `publishDate`)、`--market`、`--security`、`--category` 筛选,自动翻页(单页上限 50)。**它是唯一按 `--*-date` 过滤的 insight list**(其余用 `--start-time`);`--market` / `--category` 拼错本地直接报错,不会静默返全量。**全表 >12 万条(0.1/条),裸跑会被本地拦下**——必须给一个约束:日期范围、`--security` 或显式 `--size`;`--security` 作为唯一约束时另加 1000 行隐式上限(**仅当 `total` 显示还有未取行时**才标 `partial` + 退出码 3——恰好取满 1000 行且 total 也是 1000 属完整结果,不会误报)
|
|
13
|
+
- `insight performance-calendar download --performance-report-id <id>` — 下载业绩报告原文 PDF(A股 10 积分 / 港美股 20 积分);仅 `hasAttachment: true` 的记录可下。省略 `--output` 时沿用标题命名
|
|
14
|
+
- `gangtise tool file-parse --file <x.pdf>` — PDF 解析(异步):上传拿 `taskId`,`--wait` 阻塞轮询(≈316s 预算)直接落盘结果 ZIP(内含 `file.md` + `images/`);不带 `--wait` 时用 `gangtise tool file-parse-check --task-id <id>` 取结果。**0.8 积分/页,提交时一次性扣费**,取结果免费。本地先校验 PDF 后缀 / 非空 / ≤100MB 再上传;提交端点标 `no-replay`(超时不重放,防重复扣分),单请求超时下限 300s
|
|
15
|
+
|
|
16
|
+
**变更**
|
|
17
|
+
- 群消息 `vault wechat-message-list` 返回新增 `quoteMsg`(`quoteMsgId` / `quoteContent` / `quoteUrl`,无引用时为 `null`)。**同时修正随包 skill 里写错的字段名**:正文字段是 `content`、链接是 `url`(旧文档写作 `msgContent` / `contentUrl`,实测 2026-07-25 不存在)
|
|
18
|
+
- `raw call` 现在允许对 POST 型 download 端点传 `--body`(file-parse 取结果用),GET 型仍只收 `--query`;upload 型端点(`tool.file-parse.submit`)明确报错并指向 `tool file-parse`
|
|
19
|
+
- 新增 `EndpointDefinition.bigIntFields`:解析响应前把指定字段的裸数字重新加引号,防止雪花 ID 被 `JSON.parse` 丢位(`1782345678901234567` → `…4700`)。file-parse 的 `taskId` 实测返回字符串、当前不受影响,属前向防护——ID 丢位=已扣费的解析任务取不回结果
|
|
20
|
+
- `--verbose` 的下载日志改用端点真实 method(此前一律打印 `GET`)
|
|
21
|
+
|
|
22
|
+
### v0.28.3 — 2026-07-24
|
|
23
|
+
|
|
24
|
+
🔴 **数据完整性修复**:`--field` 传错字段名会导致**静默错列**(值贴到错误的字段上)。
|
|
25
|
+
|
|
26
|
+
**修复**
|
|
27
|
+
- 列式响应(`{fieldList, list}`)拍平时校验字段数与行长度,不匹配直接报错(`ValidationError`,退出码 1),不再输出错位数据。上游对不存在的字段名有两套处理:`day-kline` / `minute-kline` / `fund-flow` 名值同丢、三大报表补 `null`(长度相等,安全);但 **`quote realtime` / `fundamental main-business` / `valuation-analysis` 只丢值、字段名照请求回显**——实测 `quote realtime --field securityCode --field close --field turnoverRate`(realtime 根本没有 `close`)把换手率 `28.5573` 拍成了 `close`,茅台真实价 1297.41。不报错、数字看着合理、却完全是另一个指标。`alternative edb-data` 的同款拍平一并纳入校验
|
|
28
|
+
|
|
29
|
+
**文档(随包 skill)**
|
|
30
|
+
- SKILL.md 必备规则加第 10 条:`--field` 不确定就别传;`quote realtime` **无 `close`**(用 `latestPrice`)、**无市值**(总市值走 `indicator cross-section --indicator qte_mkt_cptl`,仅 A 股)
|
|
31
|
+
- `quote realtime` 补全 16 个实测字段(此前 `fields.md` 漏 `turnoverRate` / `volumeRatio`);`valuation-analysis` 标注只有 7 个字段、无 `securityCode`(误传会拿到一列重复的 `tradeDate`,长度相等拦不住)
|
|
32
|
+
- **推翻 07-23 关于 EDE `reportType` 的结论**(复测 2026-07-24):旧文档写「枚举不可信、`value=2/4` 直接 `999999`、指定口径请改用 `fundamental --report-type`」。实测是 label 与 value **错位但映射稳定**:`1`=合并(默认)、`2`=合并(调整)、`3`=母公司、`4`=母公司(调整),四值与三大报表逐一对得上;`2`/`4` 为空只是该报告期尚无调整表。**EDE 可以指定口径——母公司传 `3`,合并省略即可**
|
|
33
|
+
- 修正 `response-schema.md` 里会**反向诱导传错字段**的陈旧记录:`main-business` 行原写 `endDate` / `breakdownName` / `revenue`(实测均不存在),改为真实的 `periodName` / `periodEndDate` / `categoryName` + `opRevenue` / `grossProfit` 等 15 个字段
|
|
34
|
+
- 标注上游 meta 字段错位(实测边界):**A 股累计口径的 `balance-sheet` / `cash-flow`** 的 `companyType` 与 `currency` 值互换(`companyType=人民币`、`currency=银行`),A 股 `income-statement`、港股/美股三表均正确;A 股 `*-quarterly` 单季表另有 `companyType` 返回未映射数字码(`102119999`)的问题——读这两列按值判断语义,科目数字不受影响
|
|
35
|
+
|
|
7
36
|
### v0.28.2 — 2026-07-24
|
|
8
37
|
|
|
9
38
|
EDE 指标批量取数优化(基于对上游 990 个指标的实测)。
|
|
@@ -31,6 +60,7 @@ Agent Skill 文档取数路由对齐(对齐 gangtise-mcp 0.1.46):多证券
|
|
|
31
60
|
**实测校正(2026-07-23)**
|
|
32
61
|
- `scope` 字段更正为 `scopeList[].market/.securityType`(服务端已返回实际覆盖),覆盖按指标而异:`finc_pe_ttm`/`finc_pb_mrq` 仅 A 股、`is_op_rev` A 股+港股,均不含美股;`valuation-analysis`/`earning-forecast` 仅 A 股
|
|
33
62
|
- `finc_pb_mrq`(市净率 MRQ) 只在报告期末打值(交易日取 `null`),非日频;EDE 财务指标 `reportType` 枚举 label 与实测取数不符(`value=2/4` 直接 `999999`),要指定报表口径改用 `fundamental` 三大报表 `--report-type`
|
|
63
|
+
- ⚠️ **本条 `reportType` 结论已被 v0.28.3 复测推翻,勿据此路由**:实际是 `1`=合并 / `2`=合并(调整) / `3`=母公司 / `4`=母公司(调整),EDE 可直接指定口径,见上方 v0.28.3 条目(`finc_pb_mrq` 部分仍成立)
|
|
34
64
|
|
|
35
65
|
### v0.28.0 — 2026-07-21
|
|
36
66
|
|
|
@@ -290,6 +320,7 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
290
320
|
| | `site-visit list` | 调研 |
|
|
291
321
|
| | `strategy list` | 策略 |
|
|
292
322
|
| | `forum list` | 论坛 |
|
|
323
|
+
| | `performance-calendar list` / `download` | 财报日历(业绩预告/快报/公告,含原文 PDF 下载) |
|
|
293
324
|
| | `research list` / `download` | 研报(含 Markdown 下载) |
|
|
294
325
|
| | `foreign-report list` / `download` | 外资研报(含中文翻译下载) |
|
|
295
326
|
| | `announcement list` / `download` | A股公告(含 Markdown 下载) |
|
|
@@ -348,7 +379,8 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
348
379
|
| | `edb-data` | 行业指标时序数据(批量拉取,最多10个指标) |
|
|
349
380
|
| | `concept-info` | 题材指数基本信息(投资逻辑/行业空间/竞争格局/催化事件) |
|
|
350
381
|
| | `concept-securities` | 题材指数成分股(题材深度F8,按分组,标记重点个股) |
|
|
351
|
-
| **
|
|
382
|
+
| **Tool** | `file-parse` / `file-parse-check` | PDF 解析为 Markdown + 图片(异步,返回 ZIP) |
|
|
383
|
+
| **Raw** | `call` | 原始接口调用(可访问任意 JSON / download endpoint;upload 型如 `tool.file-parse.submit` 需走 `tool file-parse`,raw 带不了文件) |
|
|
352
384
|
|
|
353
385
|
## 命令概览
|
|
354
386
|
|
|
@@ -362,6 +394,7 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
|
|
|
362
394
|
- `gangtise indicator ...`
|
|
363
395
|
- `gangtise alternative ...`
|
|
364
396
|
- `gangtise reference ...`
|
|
397
|
+
- `gangtise tool ...`
|
|
365
398
|
- `gangtise raw call ...` / `gangtise raw list`
|
|
366
399
|
|
|
367
400
|
## 推荐工作流
|
|
@@ -410,6 +443,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
410
443
|
- `insight site-visit list`
|
|
411
444
|
- `insight strategy list`
|
|
412
445
|
- `insight forum list`
|
|
446
|
+
- `insight performance-calendar list`
|
|
413
447
|
- `insight research list`
|
|
414
448
|
- `insight foreign-report list`
|
|
415
449
|
- `insight announcement list`
|
|
@@ -438,7 +472,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
438
472
|
|
|
439
473
|
## 智能文件命名
|
|
440
474
|
|
|
441
|
-
下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`announcement-hk download`、`announcement-us download`、`official-account download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
|
|
475
|
+
下载命令(`summary download`、`research download`、`foreign-report download`、`announcement download`、`announcement-hk download`、`announcement-us download`、`official-account download`、`performance-calendar download`、`vault drive-download`、`vault record-download`、`vault my-conference-download`)省略 `--output` 时,自动使用真实标题作为文件名:
|
|
442
476
|
|
|
443
477
|
1. **缓存优先** — 如果之前执行过对应的 `list` 命令,标题已缓存在 `~/.config/gangtise/title-cache.json`,直接使用,无额外 API 调用
|
|
444
478
|
2. **API 回查** — 缓存未命中时,自动查询最近 200 条记录匹配标题
|
|
@@ -515,6 +549,12 @@ gangtise insight report-image download --chunk-id image_10_384655917758685184_8
|
|
|
515
549
|
|
|
516
550
|
# 纪要下载(会议平台来源可选 HTML 格式)
|
|
517
551
|
gangtise insight summary download --summary-id 4906813 --file-type 2
|
|
552
|
+
|
|
553
|
+
# 财报日历:注意用 --start-date/--end-date(按 publishDate 过滤),不是 --start-time
|
|
554
|
+
gangtise insight performance-calendar list --start-date 2026-07-01 --end-date 2026-07-25 \
|
|
555
|
+
--market aShares --category performanceForecast --size 20 --format json
|
|
556
|
+
# 下载业绩报告原文(仅 hasAttachment: true 的记录;A股 10 积分 / 港美股 20 积分)
|
|
557
|
+
gangtise insight performance-calendar download --performance-report-id 33753017 --output ./业绩预告.pdf
|
|
518
558
|
```
|
|
519
559
|
|
|
520
560
|
### Reference
|
|
@@ -749,6 +789,19 @@ gangtise alternative concept-info --concept-id 121000130 --format json
|
|
|
749
789
|
gangtise alternative concept-securities --concept-id 121000130 --format json
|
|
750
790
|
```
|
|
751
791
|
|
|
792
|
+
### Tool(PDF 解析)
|
|
793
|
+
|
|
794
|
+
```bash
|
|
795
|
+
# 一步到位:上传 → 阻塞等待 → 结果 ZIP 落盘(含 file.md + images/)
|
|
796
|
+
gangtise tool file-parse --file ./研报.pdf --wait --output ./研报.zip
|
|
797
|
+
|
|
798
|
+
# 分两步:先提交拿 taskId(此时按页扣费 0.8/页),约 3 分钟后取结果(免费)
|
|
799
|
+
gangtise tool file-parse --file ./研报.pdf
|
|
800
|
+
gangtise tool file-parse-check --task-id 829081108954501120 --output ./研报.zip
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
限制:单文件 ≤100MB、≤500 页,同一用户最多 10 个并发任务;未就绪时 `file-parse-check` 输出 `{"status":"pending"}`(退出码 0),重试即可,不会重复扣费。
|
|
804
|
+
|
|
752
805
|
### Raw
|
|
753
806
|
|
|
754
807
|
```bash
|
package/dist/src/cli.js
CHANGED
|
@@ -10,7 +10,8 @@ import { loadConfig } from "./core/config.js";
|
|
|
10
10
|
import { resolveTitle, saveDownloadResult, uniquePath } from "./core/download.js";
|
|
11
11
|
import { ENDPOINTS, listEndpoints } from "./core/endpoints.js";
|
|
12
12
|
import { ApiError, ConfigError, ValidationError } from "./core/errors.js";
|
|
13
|
-
import {
|
|
13
|
+
import { fetchFileParseResult, pollFileParseResult, submitFileParse } from "./core/fileParse.js";
|
|
14
|
+
import { normalizeRows, zipFieldRow } from "./core/normalize.js";
|
|
14
15
|
import { parseOutputFormat } from "./core/output.js";
|
|
15
16
|
import { printData } from "./core/printer.js";
|
|
16
17
|
// --- Lazy-loaded modules (deferred to action handlers) ---
|
|
@@ -69,11 +70,11 @@ function flagIfLimitTruncated(data, cap, label) {
|
|
|
69
70
|
*/
|
|
70
71
|
async function runDownload(client, endpointKey, query, options) {
|
|
71
72
|
if (options.output) {
|
|
72
|
-
const result = await client.call(endpointKey,
|
|
73
|
+
const result = await client.call(endpointKey, options.body, query, { streamTo: options.output });
|
|
73
74
|
await saveDownloadResult(result, options.fallbackName, options.output);
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
|
-
const result = await client.call(endpointKey,
|
|
77
|
+
const result = await client.call(endpointKey, options.body, query);
|
|
77
78
|
const resolved = options.resolveOutputPath ? await options.resolveOutputPath(result) : undefined;
|
|
78
79
|
// Title-derived names are auto-generated too — dedupe them like the fallback names.
|
|
79
80
|
await saveDownloadResult(result, options.fallbackName, resolved ? await uniquePath(resolved) : undefined);
|
|
@@ -161,6 +162,7 @@ const roadshow = new Command("roadshow");
|
|
|
161
162
|
const siteVisit = new Command("site-visit");
|
|
162
163
|
const strategy = new Command("strategy");
|
|
163
164
|
const forum = new Command("forum");
|
|
165
|
+
const performanceCalendar = new Command("performance-calendar");
|
|
164
166
|
const research = new Command("research");
|
|
165
167
|
const foreignReport = new Command("foreign-report");
|
|
166
168
|
const announcement = new Command("announcement");
|
|
@@ -235,6 +237,79 @@ addScheduleList(siteVisit, "insight.site-visit.list", {
|
|
|
235
237
|
});
|
|
236
238
|
addScheduleList(strategy, "insight.strategy.list", { institution: true, location: true });
|
|
237
239
|
addScheduleList(forum, "insight.forum.list", { researchArea: true, location: true });
|
|
240
|
+
// Earnings calendar: the only insight list filtered by DATE (--start-date/--end-date
|
|
241
|
+
// on publishDate), not by the --start-time datetime every sibling uses — so it does
|
|
242
|
+
// not go through addTimeFilters. It also takes no --keyword / --rank-type / --search-type.
|
|
243
|
+
/** Row ceiling applied when `--security` is the only thing bounding a
|
|
244
|
+
* performance-calendar fetch. Far above any single company's calendar (a whole
|
|
245
|
+
* A-share history is dozens of rows), far below the 50k the auto-pagination
|
|
246
|
+
* would otherwise pull if the server ever stopped honoring securityList. */
|
|
247
|
+
const SECURITY_ONLY_ROW_CAP = 1000;
|
|
248
|
+
/** Warn + mark partial when a `--security`-only fetch lands on the cap with rows
|
|
249
|
+
* still unfetched: that is the signature of a filter that did not narrow anything,
|
|
250
|
+
* and the rows on screen are then a truncated slice of the whole calendar rather
|
|
251
|
+
* than a company's. `total` decides it — a result that happens to be exactly `cap`
|
|
252
|
+
* rows long IS complete (from + rows covers total) and must stay exit 0, or every
|
|
253
|
+
* automated caller reads a full answer as truncated. */
|
|
254
|
+
function flagIfImplicitCapHit(data, cap, from) {
|
|
255
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
256
|
+
return;
|
|
257
|
+
const rec = data;
|
|
258
|
+
if (!Array.isArray(rec.list) || rec.list.length < cap)
|
|
259
|
+
return;
|
|
260
|
+
const total = typeof rec.total === "number" ? rec.total : undefined;
|
|
261
|
+
if (total !== undefined && from + rec.list.length >= total)
|
|
262
|
+
return;
|
|
263
|
+
rec.partial = true;
|
|
264
|
+
process.stderr.write(`[gangtise] warning: --security was the only bound, so the fetch was capped at ${cap} rows and more remain (total=${String(rec.total)}) — the filter may not have narrowed anything. Re-run with --start-date/--end-date or an explicit --size.\n`);
|
|
265
|
+
}
|
|
266
|
+
const PERFORMANCE_MARKETS = ["aShares", "hkStocks", "usChinaConcept", "usStocks"];
|
|
267
|
+
const PERFORMANCE_CATEGORIES = ["performanceForecast", "performanceExpress", "performanceAnnouncement"];
|
|
268
|
+
performanceCalendar.command("list").description("Earnings calendar (业绩预告 / 快报 / 公告)")
|
|
269
|
+
.option("--from <number>", "Starting offset", "0")
|
|
270
|
+
.option("--size <number>", "Total rows to return; omit to fetch all")
|
|
271
|
+
.option("--start-date <date>", "Start date (yyyy-MM-dd), filters publishDate", dateArg("--start-date"))
|
|
272
|
+
.option("--end-date <date>", "End date (yyyy-MM-dd), filters publishDate", dateArg("--end-date"))
|
|
273
|
+
.option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, [])
|
|
274
|
+
.option("--market <name>", `Market: ${PERFORMANCE_MARKETS.join("/")}`, collectList, [])
|
|
275
|
+
.option("--category <name>", `Event type: ${PERFORMANCE_CATEGORIES.join("/")}`, collectList, [])
|
|
276
|
+
.option("--format <format>", "Output format", "table").option("--output <path>", "Output path")
|
|
277
|
+
.action((options) => {
|
|
278
|
+
// Enum typos first: a misspelled --category is the likelier mistake, and its
|
|
279
|
+
// message is the more useful one when both checks would fire.
|
|
280
|
+
const marketList = parseChoiceList(options.market, "--market", PERFORMANCE_MARKETS);
|
|
281
|
+
const categoryList = parseChoiceList(options.category, "--category", PERFORMANCE_CATEGORIES);
|
|
282
|
+
// Unfiltered, this endpoint holds >120k rows (probed 2026-07-25: 126683, it
|
|
283
|
+
// also carries FUTURE scheduled events) and an omitted --size means "fetch
|
|
284
|
+
// everything" — 50k rows at the 1000-page cap, ~5000 credits at 0.1/row.
|
|
285
|
+
// Require a bound: a full date range, an explicit --size, or a security filter.
|
|
286
|
+
const explicitlyBounded = Boolean(options.size) || Boolean(options.startDate && options.endDate);
|
|
287
|
+
if (!explicitlyBounded && !options.security.length) {
|
|
288
|
+
throw new ValidationError("insight performance-calendar list without a bound would auto-paginate the whole calendar (>120k rows at 0.1 credits each): pass --start-date and --end-date, or --security, or an explicit --size");
|
|
289
|
+
}
|
|
290
|
+
// --security is only a real bound while the server honors securityList. It does
|
|
291
|
+
// today (probed 2026-07-25: an unknown or malformed code returns total 0, it is
|
|
292
|
+
// not silently ignored like a bad enum) — but a five-figure credit bill must not
|
|
293
|
+
// rest on that staying true. When --security is the ONLY bound, cap the fetch:
|
|
294
|
+
// one company's whole calendar is dozens of rows, so the cap is invisible in
|
|
295
|
+
// normal use and turns a filter regression into a truncated result (partial +
|
|
296
|
+
// exit 3) instead of a 5000-credit pull.
|
|
297
|
+
const implicitCap = explicitlyBounded ? undefined : SECURITY_ONLY_ROW_CAP;
|
|
298
|
+
const from = parseFrom(options.from);
|
|
299
|
+
return emit(options, async (client) => {
|
|
300
|
+
const data = await client.call("insight.performance-calendar.list", {
|
|
301
|
+
from, size: parseSize(options.size) ?? implicitCap,
|
|
302
|
+
startDate: options.startDate, endDate: options.endDate,
|
|
303
|
+
marketList,
|
|
304
|
+
securityList: maybeArray(options.security),
|
|
305
|
+
categoryList,
|
|
306
|
+
});
|
|
307
|
+
if (implicitCap)
|
|
308
|
+
flagIfImplicitCapHit(data, implicitCap, from);
|
|
309
|
+
return data;
|
|
310
|
+
}, { endpointKey: "insight.performance-calendar.list", idField: "performanceReportId" });
|
|
311
|
+
});
|
|
312
|
+
addDownloadCommand(performanceCalendar, { endpointKey: "insight.performance-calendar.download", idOption: "--performance-report-id", idField: "performanceReportId", fallbackPrefix: "performance-calendar", titleListEndpoint: "insight.performance-calendar.list" });
|
|
238
313
|
addTimeFilters(research.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--broker <id>", "Broker ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--source <type>", "Source type", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.research.list", {
|
|
239
314
|
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
|
|
240
315
|
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
@@ -331,6 +406,7 @@ insight.addCommand(roadshow);
|
|
|
331
406
|
insight.addCommand(siteVisit);
|
|
332
407
|
insight.addCommand(strategy);
|
|
333
408
|
insight.addCommand(forum);
|
|
409
|
+
insight.addCommand(performanceCalendar);
|
|
334
410
|
insight.addCommand(research);
|
|
335
411
|
insight.addCommand(foreignReport);
|
|
336
412
|
insight.addCommand(announcement);
|
|
@@ -612,10 +688,8 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
|
|
|
612
688
|
});
|
|
613
689
|
let data = raw;
|
|
614
690
|
if (raw && Array.isArray(raw.fieldList) && Array.isArray(raw.dataList)) {
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
return acc;
|
|
618
|
-
}, {}));
|
|
691
|
+
const fields = raw.fieldList;
|
|
692
|
+
const list = raw.dataList.map((row) => zipFieldRow(fields, row, raw));
|
|
619
693
|
data = { list, total: list.length };
|
|
620
694
|
}
|
|
621
695
|
await printData(data, format, options.output);
|
|
@@ -643,6 +717,35 @@ indicator.command("time-series").option("--indicator <code>", "Indicator code, e
|
|
|
643
717
|
await printData(flattenTimeSeries(unwrapIndicatorData(raw), options.keyBy), format, options.output);
|
|
644
718
|
}));
|
|
645
719
|
program.addCommand(indicator);
|
|
720
|
+
const tool = new Command("tool").description("Research tool APIs: PDF parsing");
|
|
721
|
+
tool.command("file-parse").description("Parse a PDF into Markdown + images (async; 0.8 credits/page, billed at submit)")
|
|
722
|
+
.requiredOption("--file <path>", "PDF to upload (max 100MB / 500 pages)")
|
|
723
|
+
.option("--wait", "Wait for the parse to finish and save the result ZIP (blocking, up to ~5 min)")
|
|
724
|
+
.option("--output <path>", "Where to save the result ZIP (used with --wait)")
|
|
725
|
+
.action((options) => withClient(async (client) => {
|
|
726
|
+
const taskId = await submitFileParse(client, options.file);
|
|
727
|
+
if (!options.wait) {
|
|
728
|
+
process.stderr.write(`File parse task submitted. taskId: ${taskId}\n`);
|
|
729
|
+
process.stdout.write(`${JSON.stringify({ taskId, status: "pending", hint: `Run 'gangtise tool file-parse-check --task-id ${taskId}' in ~3 minutes to download the result ZIP` })}\n`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
process.stderr.write(`Got taskId: ${taskId}, waiting for the parse to finish...\n`);
|
|
733
|
+
if (await pollFileParseResult(client, taskId, options.output) !== "ok") {
|
|
734
|
+
// Fetching the result is free and the task keeps running server-side —
|
|
735
|
+
// re-checking later costs nothing, resubmitting re-bills the whole file.
|
|
736
|
+
process.stderr.write(`Parse result not available after ${POLL_MAX_ATTEMPTS} attempts. Try again later with: gangtise tool file-parse-check --task-id ${taskId}\n`);
|
|
737
|
+
process.exitCode = 1;
|
|
738
|
+
}
|
|
739
|
+
}));
|
|
740
|
+
tool.command("file-parse-check").description("Download a finished file-parse result ZIP by taskId (free)")
|
|
741
|
+
.requiredOption("--task-id <id>", "taskId from 'tool file-parse'")
|
|
742
|
+
.option("--output <path>", "Where to save the result ZIP")
|
|
743
|
+
.action((options) => withClient(async (client) => {
|
|
744
|
+
if (await fetchFileParseResult(client, options.taskId, options.output) === "pending") {
|
|
745
|
+
process.stdout.write(`${JSON.stringify({ taskId: options.taskId, status: "pending", hint: "Parse not finished yet, retry in ~1 minute" })}\n`);
|
|
746
|
+
}
|
|
747
|
+
}));
|
|
748
|
+
program.addCommand(tool);
|
|
646
749
|
program.command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
647
750
|
const endpoint = ENDPOINTS[endpointKey];
|
|
648
751
|
if (!endpoint) {
|
|
@@ -662,12 +765,15 @@ program.command("raw").description("Raw API calls").addCommand(new Command("call
|
|
|
662
765
|
// Fail loudly on arguments the endpoint kind can't use — they used to be
|
|
663
766
|
// silently dropped, leaving the user to puzzle over server-side errors.
|
|
664
767
|
if (endpoint.kind === "download") {
|
|
665
|
-
|
|
666
|
-
|
|
768
|
+
// POST download endpoints (file-parse result) take their parameters as a JSON
|
|
769
|
+
// body; GET ones take --query and can't carry a body at all.
|
|
770
|
+
if (body !== undefined && endpoint.method !== "POST") {
|
|
771
|
+
throw new ValidationError(`--body is not supported for GET download endpoints (use --query key=value); ${endpointKey} is kind=download`);
|
|
667
772
|
}
|
|
668
773
|
await runDownload(client, endpointKey, options.query, {
|
|
669
774
|
output: options.output,
|
|
670
775
|
fallbackName: "download.bin",
|
|
776
|
+
body,
|
|
671
777
|
});
|
|
672
778
|
return;
|
|
673
779
|
}
|
|
@@ -5,7 +5,7 @@ import { isTransientError } from "./transport.js";
|
|
|
5
5
|
export const POLL_MAX_ATTEMPTS = 14;
|
|
6
6
|
const POLL_INITIAL_DELAY_MS = 5_000;
|
|
7
7
|
const POLL_MAX_DELAY_MS = 30_000;
|
|
8
|
-
function
|
|
8
|
+
export function nextPollDelayMs(attempt) {
|
|
9
9
|
// 5s, 8s, 13s, 20s, 30s, 30s, ...
|
|
10
10
|
const grown = POLL_INITIAL_DELAY_MS * 1.6 ** (attempt - 1);
|
|
11
11
|
return Math.min(POLL_MAX_DELAY_MS, Math.round(grown));
|
|
@@ -19,7 +19,9 @@ function nextDelayMs(attempt) {
|
|
|
19
19
|
// recognize the pending code aborts the poll on a job already billed 50 credits.
|
|
20
20
|
const PENDING_CODES = new Set(["410110", "140001"]);
|
|
21
21
|
const FAILED_CODES = new Set(["410111", "140002"]);
|
|
22
|
-
|
|
22
|
+
/** Shared by the AI content endpoints and `tool file-parse` — both report
|
|
23
|
+
* "still generating" with the same codes. */
|
|
24
|
+
export function isAsyncPending(error) {
|
|
23
25
|
return error instanceof ApiError && error.code !== undefined && PENDING_CODES.has(error.code);
|
|
24
26
|
}
|
|
25
27
|
function isAsyncFailed(error) {
|
|
@@ -66,7 +68,7 @@ export async function pollAsyncContent(client, getContentEndpoint, dataId, forma
|
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
if (attempt < POLL_MAX_ATTEMPTS) {
|
|
69
|
-
const delay =
|
|
71
|
+
const delay = nextPollDelayMs(attempt);
|
|
70
72
|
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: content not ready, retrying in ${Math.round(delay / 1000)}s...\n`);
|
|
71
73
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
72
74
|
}
|
package/dist/src/core/client.js
CHANGED
|
@@ -2,12 +2,12 @@ import { createWriteStream } from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
|
-
import { request } from "undici";
|
|
5
|
+
import { FormData, request } from "undici";
|
|
6
6
|
import { isTokenCacheValid, normalizeToken, readTokenCache, requireAccessCredentials, writeTokenCache } from "./auth.js";
|
|
7
7
|
import { ApiError, attachEnvelopeTraceId, ValidationError } from "./errors.js";
|
|
8
8
|
import { ENDPOINTS, resolveTimeoutMs } from "./endpoints.js";
|
|
9
9
|
import { getLookupData } from "./lookupData/index.js";
|
|
10
|
-
import { decodeResponseBody, getDispatcher, isVerbose, logTiming, markRetryable, PAGE_CONCURRENCY, parseRetryAfterMs, runWithConcurrency, withRetry } from "./transport.js";
|
|
10
|
+
import { decodeResponseBody, getDispatcher, isVerbose, logTiming, markRetryable, PAGE_CONCURRENCY, parseRetryAfterMs, quoteBigIntFields, runWithConcurrency, withRetry } from "./transport.js";
|
|
11
11
|
// Auth errors that warrant a forced re-login + one replay: the token was rejected
|
|
12
12
|
// server-side while still looking valid by local expiry, so only a forced refresh
|
|
13
13
|
// recovers it. 0000001008 is the legacy code (probed 2026-07-20: still what the
|
|
@@ -343,12 +343,17 @@ export class GangtiseClient {
|
|
|
343
343
|
const authState = { retried: false };
|
|
344
344
|
const timeoutMs = resolveTimeoutMs(this.config.timeoutMs, endpoint);
|
|
345
345
|
return withRetry(async () => {
|
|
346
|
+
// An upload endpoint hands undici the FormData itself: undici derives the
|
|
347
|
+
// multipart content-type (boundary included) from the body, so setting one
|
|
348
|
+
// here would corrupt the request.
|
|
349
|
+
const isUpload = endpoint.kind === 'upload';
|
|
346
350
|
const headers = {
|
|
347
|
-
'content-type': 'application/json',
|
|
348
351
|
// undici does not auto-decompress; decodeResponseBody gunzips below. Server
|
|
349
352
|
// gzip cuts JSON payloads ~3-10x (measured 3.6x on constant-list).
|
|
350
353
|
'accept-encoding': 'gzip',
|
|
351
354
|
};
|
|
355
|
+
if (!isUpload)
|
|
356
|
+
headers['content-type'] = 'application/json';
|
|
352
357
|
// Keep the header we actually sent: the self-heal check compares it against
|
|
353
358
|
// the current memoCache token to tell "stale token" from "fresh token died".
|
|
354
359
|
let usedAuthorization;
|
|
@@ -360,7 +365,7 @@ export class GangtiseClient {
|
|
|
360
365
|
const response = await request(url, {
|
|
361
366
|
method: endpoint.method,
|
|
362
367
|
headers,
|
|
363
|
-
body: endpoint.method === 'GET' ? undefined : JSON.stringify(body ?? {}),
|
|
368
|
+
body: endpoint.method === 'GET' ? undefined : (isUpload ? body : JSON.stringify(body ?? {})),
|
|
364
369
|
headersTimeout: timeoutMs,
|
|
365
370
|
bodyTimeout: timeoutMs,
|
|
366
371
|
dispatcher,
|
|
@@ -392,7 +397,7 @@ export class GangtiseClient {
|
|
|
392
397
|
const retryAfterMs = parseRetryAfterMs(response.headers['retry-after'], Date.now());
|
|
393
398
|
let parsed;
|
|
394
399
|
try {
|
|
395
|
-
parsed = JSON.parse(text);
|
|
400
|
+
parsed = JSON.parse(quoteBigIntFields(text, endpoint.bigIntFields));
|
|
396
401
|
}
|
|
397
402
|
catch {
|
|
398
403
|
const message = response.statusCode >= 400
|
|
@@ -434,7 +439,22 @@ export class GangtiseClient {
|
|
|
434
439
|
throw error;
|
|
435
440
|
});
|
|
436
441
|
}
|
|
437
|
-
|
|
442
|
+
/** POST a file as multipart/form-data under the field name `file`. Reuses
|
|
443
|
+
* requestJson for auth / retry / envelope handling — only the body differs. */
|
|
444
|
+
async uploadFile(endpointKey, file) {
|
|
445
|
+
const endpoint = ENDPOINTS[endpointKey];
|
|
446
|
+
if (!endpoint || endpoint.kind !== 'upload') {
|
|
447
|
+
throw new ApiError(`Not an upload endpoint: ${endpointKey}`);
|
|
448
|
+
}
|
|
449
|
+
const form = new FormData();
|
|
450
|
+
// Cast: TS 5.7+ types Uint8Array as Uint8Array<ArrayBufferLike>, which BlobPart
|
|
451
|
+
// (ArrayBufferView<ArrayBuffer>) rejects; a Node Buffer is always ArrayBuffer-backed.
|
|
452
|
+
form.append('file', new Blob([file.data], { type: file.contentType ?? 'application/octet-stream' }), file.filename);
|
|
453
|
+
return this.requestJson(endpoint, form);
|
|
454
|
+
}
|
|
455
|
+
/** `body` is only sent for POST download endpoints (the file-parse result
|
|
456
|
+
* endpoint takes `{taskId}` as JSON and answers with the ZIP bytes). */
|
|
457
|
+
async download(endpoint, query, options, body) {
|
|
438
458
|
const dispatcher = getDispatcher();
|
|
439
459
|
const url = this.buildUrl(endpoint.path);
|
|
440
460
|
Object.entries(query).forEach(([key, value]) => {
|
|
@@ -446,9 +466,13 @@ export class GangtiseClient {
|
|
|
446
466
|
const startedAt = Date.now();
|
|
447
467
|
let currentUrl = url;
|
|
448
468
|
let auth = authorization;
|
|
469
|
+
const isPost = endpoint.method === 'POST';
|
|
449
470
|
let response = await request(currentUrl, {
|
|
450
471
|
method: endpoint.method,
|
|
451
|
-
headers:
|
|
472
|
+
headers: isPost
|
|
473
|
+
? { Authorization: authorization, 'content-type': 'application/json' }
|
|
474
|
+
: { Authorization: authorization },
|
|
475
|
+
body: isPost ? JSON.stringify(body ?? {}) : undefined,
|
|
452
476
|
headersTimeout: this.config.timeoutMs,
|
|
453
477
|
bodyTimeout: this.config.timeoutMs,
|
|
454
478
|
dispatcher,
|
|
@@ -488,7 +512,7 @@ export class GangtiseClient {
|
|
|
488
512
|
const retryAfterMs = parseRetryAfterMs(response.headers['retry-after'], Date.now());
|
|
489
513
|
if (contentType?.includes('application/json')) {
|
|
490
514
|
const text = await response.body.text();
|
|
491
|
-
logTiming(
|
|
515
|
+
logTiming(`${endpoint.method} ${endpoint.path} (json)`, Date.now() - startedAt, `${response.statusCode}, ${text.length}B`);
|
|
492
516
|
let parsed;
|
|
493
517
|
try {
|
|
494
518
|
parsed = JSON.parse(text);
|
|
@@ -517,7 +541,7 @@ export class GangtiseClient {
|
|
|
517
541
|
}
|
|
518
542
|
if (contentType?.includes('text/plain') || contentType?.includes('text/html')) {
|
|
519
543
|
const text = await response.body.text();
|
|
520
|
-
logTiming(
|
|
544
|
+
logTiming(`${endpoint.method} ${endpoint.path} (text)`, Date.now() - startedAt, `${response.statusCode}, ${text.length}B`);
|
|
521
545
|
if (response.statusCode >= 400) {
|
|
522
546
|
throw new ApiError('Download failed', undefined, response.statusCode, text, retryAfterMs);
|
|
523
547
|
}
|
|
@@ -560,11 +584,11 @@ export class GangtiseClient {
|
|
|
560
584
|
await fs.unlink(partPath).catch(() => { });
|
|
561
585
|
throw error;
|
|
562
586
|
}
|
|
563
|
-
logTiming(
|
|
587
|
+
logTiming(`${endpoint.method} ${endpoint.path} (stream)`, Date.now() - startedAt, `${response.statusCode}`);
|
|
564
588
|
return { contentType, filename, savedPath: options.streamTo };
|
|
565
589
|
}
|
|
566
590
|
const buffer = await response.body.arrayBuffer();
|
|
567
|
-
logTiming(
|
|
591
|
+
logTiming(`${endpoint.method} ${endpoint.path} (binary)`, Date.now() - startedAt, `${response.statusCode}, ${buffer.byteLength}B`);
|
|
568
592
|
return {
|
|
569
593
|
data: new Uint8Array(buffer),
|
|
570
594
|
contentType,
|
|
@@ -587,8 +611,11 @@ export class GangtiseClient {
|
|
|
587
611
|
if (!endpoint) {
|
|
588
612
|
throw new ApiError(`Unknown endpoint key: ${endpointKey}`);
|
|
589
613
|
}
|
|
614
|
+
if (endpoint.kind === 'upload') {
|
|
615
|
+
throw new ValidationError(`${endpointKey} takes a file upload — use 'gangtise tool file-parse --file <path>' ('raw call' cannot send files)`);
|
|
616
|
+
}
|
|
590
617
|
if (endpoint.kind === 'download') {
|
|
591
|
-
return this.download(endpoint, query ?? {}, options);
|
|
618
|
+
return this.download(endpoint, query ?? {}, options, body);
|
|
592
619
|
}
|
|
593
620
|
if (endpoint.kind === 'json' && endpoint.pagination?.enabled) {
|
|
594
621
|
return this.requestPaginated(endpoint, body);
|
|
@@ -77,6 +77,19 @@ const ENDPOINT_DEFS = {
|
|
|
77
77
|
description: "List forums",
|
|
78
78
|
pagination: { enabled: true, maxPageSize: 50 },
|
|
79
79
|
},
|
|
80
|
+
"insight.performance-calendar.list": {
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: "/application/open-insight/schedule/performance-calendar/getList",
|
|
83
|
+
kind: "json",
|
|
84
|
+
description: "List earnings calendar events (forecast / express / announcement)",
|
|
85
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
86
|
+
},
|
|
87
|
+
"insight.performance-calendar.download": {
|
|
88
|
+
method: "GET",
|
|
89
|
+
path: "/application/open-insight/schedule/performance-calendar/download/file",
|
|
90
|
+
kind: "download",
|
|
91
|
+
description: "Download an earnings report file (A-share 10 credits, HK/US 20)",
|
|
92
|
+
},
|
|
80
93
|
"insight.research.list": {
|
|
81
94
|
method: "POST",
|
|
82
95
|
path: "/application/open-insight/broker-report/getList",
|
|
@@ -619,6 +632,27 @@ const ENDPOINT_DEFS = {
|
|
|
619
632
|
description: "Get time-series data (multi-indicator x single-security OR single-indicator x multi-security)",
|
|
620
633
|
retry: "no-999999",
|
|
621
634
|
},
|
|
635
|
+
// ─── tool (open-tool: async file parsing) ───
|
|
636
|
+
"tool.file-parse.submit": {
|
|
637
|
+
method: "POST",
|
|
638
|
+
path: "/application/open-tool/file-parse/submit",
|
|
639
|
+
kind: "upload",
|
|
640
|
+
description: "Submit a PDF for parsing (multipart upload), returns taskId",
|
|
641
|
+
// Billed per page (0.8/页) at submit time, and the upload itself can take
|
|
642
|
+
// minutes on a 100MB file — never replay it, and don't let the default 30s
|
|
643
|
+
// headers timeout kill an in-flight upload.
|
|
644
|
+
timeoutMs: 300_000,
|
|
645
|
+
retry: "no-replay",
|
|
646
|
+
// Probed 2026-07-25: taskId comes back as a string today. Guard anyway — if it
|
|
647
|
+
// ever arrives as a bare number, rounding would strand a paid parse job.
|
|
648
|
+
bigIntFields: ["taskId"],
|
|
649
|
+
},
|
|
650
|
+
"tool.file-parse.result": {
|
|
651
|
+
method: "POST",
|
|
652
|
+
path: "/application/open-tool/file-parse/result",
|
|
653
|
+
kind: "download",
|
|
654
|
+
description: "Fetch a file-parse result ZIP by taskId (140001 = still generating)",
|
|
655
|
+
},
|
|
622
656
|
};
|
|
623
657
|
export const ENDPOINTS = Object.fromEntries(Object.entries(ENDPOINT_DEFS).map(([key, def]) => [key, { key, ...def }]));
|
|
624
658
|
/** Flat catalog of every registered endpoint, for `raw list` discoverability
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isAsyncPending, nextPollDelayMs, POLL_MAX_ATTEMPTS } from "./asyncContent.js";
|
|
4
|
+
import { saveDownloadResult } from "./download.js";
|
|
5
|
+
import { ApiError, ValidationError } from "./errors.js";
|
|
6
|
+
import { isTransientError } from "./transport.js";
|
|
7
|
+
/** Server-side upload cap (spec: ≤100MB, ≤500 pages). Checked locally so a
|
|
8
|
+
* doomed 100MB+ upload fails instantly instead of after a long transfer. */
|
|
9
|
+
export const FILE_PARSE_MAX_BYTES = 100 * 1024 * 1024;
|
|
10
|
+
/** Upload a PDF and return its taskId. Billed at submit time (0.8 credits/page),
|
|
11
|
+
* so every reason to reject a file is checked before the request goes out. */
|
|
12
|
+
export async function submitFileParse(client, filePath) {
|
|
13
|
+
const resolved = path.resolve(filePath);
|
|
14
|
+
let stat;
|
|
15
|
+
try {
|
|
16
|
+
stat = await fs.stat(resolved);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new ValidationError(`File not found: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
if (!stat.isFile())
|
|
22
|
+
throw new ValidationError(`Not a file: ${filePath}`);
|
|
23
|
+
if (stat.size === 0)
|
|
24
|
+
throw new ValidationError(`File is empty: ${filePath}`);
|
|
25
|
+
if (stat.size > FILE_PARSE_MAX_BYTES) {
|
|
26
|
+
throw new ValidationError(`File is ${(stat.size / 1024 / 1024).toFixed(1)}MB — the parse API accepts at most 100MB`);
|
|
27
|
+
}
|
|
28
|
+
if (path.extname(resolved).toLowerCase() !== ".pdf") {
|
|
29
|
+
throw new ValidationError(`Only PDF files are supported: ${filePath}`);
|
|
30
|
+
}
|
|
31
|
+
// Buffered, not streamed: undici's FormData needs a Blob, so a 100MB file peaks
|
|
32
|
+
// at ~2× its size in memory. Acceptable against the API's own 100MB ceiling —
|
|
33
|
+
// switch to a hand-rolled streaming multipart body if that ceiling ever rises.
|
|
34
|
+
const data = await fs.readFile(resolved);
|
|
35
|
+
const result = await client.uploadFile("tool.file-parse.submit", {
|
|
36
|
+
filename: path.basename(resolved),
|
|
37
|
+
data,
|
|
38
|
+
contentType: "application/pdf",
|
|
39
|
+
});
|
|
40
|
+
const taskId = result?.taskId;
|
|
41
|
+
if (taskId === undefined || taskId === null || taskId === "") {
|
|
42
|
+
throw new ApiError("File parse task was accepted but the response carried no taskId", undefined, undefined, result);
|
|
43
|
+
}
|
|
44
|
+
return String(taskId);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One result fetch. "pending" = the server answered with the generating code
|
|
48
|
+
* (140001 / legacy 410110); "ok" = the ZIP was written and its path printed.
|
|
49
|
+
* Free to call — only the submit step is billed.
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchFileParseResult(client, taskId, output) {
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await client.call("tool.file-parse.result", { taskId }, undefined, output ? { streamTo: output } : undefined);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (isAsyncPending(error))
|
|
58
|
+
return "pending";
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
await saveDownloadResult(result, `file-parse-${taskId}`, output);
|
|
62
|
+
return "ok";
|
|
63
|
+
}
|
|
64
|
+
/** Poll until the ZIP is ready. Same backoff budget as the AI async endpoints
|
|
65
|
+
* (≈316s), which covers the documented ~3 min parse time. */
|
|
66
|
+
export async function pollFileParseResult(client, taskId, output) {
|
|
67
|
+
for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
if (await fetchFileParseResult(client, taskId, output) === "ok")
|
|
70
|
+
return "ok";
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// The task is already paid for: a blip must not void the wait. Transient
|
|
74
|
+
// errors consume the attempt, anything else (bad taskId, no permission) aborts.
|
|
75
|
+
if (!isTransientError(error))
|
|
76
|
+
throw error;
|
|
77
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
78
|
+
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: transient error (${msg.slice(0, 80)}), continuing to wait...\n`);
|
|
79
|
+
}
|
|
80
|
+
if (attempt < POLL_MAX_ATTEMPTS) {
|
|
81
|
+
const delay = nextPollDelayMs(attempt);
|
|
82
|
+
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: parse result not ready, retrying in ${Math.round(delay / 1000)}s...\n`);
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return "timeout";
|
|
87
|
+
}
|
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
import { ENVELOPE_TRACE_ID, ValidationError } from "./errors.js";
|
|
2
|
+
/** 信封 traceId 由 `client.ts` 挂在每个成功响应的 payload 上(非枚举 symbol)。
|
|
3
|
+
* 结构异常报障没这个 id 服务端查不了,所以拍平失败时要一并带出。 */
|
|
4
|
+
function traceSuffix(source) {
|
|
5
|
+
if (!source || typeof source !== "object")
|
|
6
|
+
return "";
|
|
7
|
+
const traceId = source[ENVELOPE_TRACE_ID];
|
|
8
|
+
return typeof traceId === "string" ? `(trace ${traceId})` : "";
|
|
9
|
+
}
|
|
10
|
+
/** 按位置把列式响应的一行值拍平成对象。
|
|
11
|
+
*
|
|
12
|
+
* 上游对「fieldList 里有该接口不存在的字段名」有两套处理,实测 2026-07-24:
|
|
13
|
+
* day-kline / minute-kline / fund-flow 把名和值一起丢、三大报表补 null——长度仍相等,安全;
|
|
14
|
+
* 但 realtime / main-business / valuation-analysis 是**值只按有效字段返回、字段名却按请求
|
|
15
|
+
* 原样回显**。长度一旦不等,按位置拍平就会把值贴到错误的字段上:realtime 传
|
|
16
|
+
* ["securityCode","close","turnoverRate"](realtime 根本没有 close)只回 2 个值,
|
|
17
|
+
* 换手率 28.5573 被贴成 close,读起来就是「茅台收盘价 28.56」(真实价 1297.41)。
|
|
18
|
+
* 不报错、数字看着还合理、却完全是另一个指标——静默错列必须变成显式失败。
|
|
19
|
+
*
|
|
20
|
+
* 文案不能一口咬定「字段名传错」:`alternative edb-data` 走同一个拍平却根本没有
|
|
21
|
+
* `--field`(只有 `--indicator-id`),那里长度不等只可能是上游响应结构变了。 */
|
|
22
|
+
export function zipFieldRow(fields, row, source) {
|
|
23
|
+
if (row.length !== fields.length) {
|
|
24
|
+
throw new ValidationError(`响应字段数与 fieldList 不匹配(fieldList ${fields.length} 项、该行返回 ${row.length} 个值)——按位置拍平会把值贴到错误的字段上,已拒绝输出。带 --field 的命令多为传了该接口不存在的字段名(上游只返回有效字段的值、字段名却按请求回显):核对 --field 取值(如 quote realtime 没有 close,最新价是 latestPrice),不确定就不传 --field(返回全量字段最稳)。没有 --field 的命令(如 alternative edb-data)出现此错,是上游响应结构异常,请报障${traceSuffix(source)}。`);
|
|
25
|
+
}
|
|
26
|
+
return fields.reduce((acc, field, index) => {
|
|
27
|
+
acc[String(field)] = row[index];
|
|
28
|
+
return acc;
|
|
29
|
+
}, {});
|
|
30
|
+
}
|
|
1
31
|
export function normalizeRows(value) {
|
|
2
32
|
if (!value || typeof value !== "object") {
|
|
3
33
|
return value;
|
|
@@ -7,14 +37,8 @@ export function normalizeRows(value) {
|
|
|
7
37
|
}
|
|
8
38
|
const record = value;
|
|
9
39
|
if (Array.isArray(record.fieldList) && Array.isArray(record.list)) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
return row;
|
|
13
|
-
return record.fieldList.reduce((acc, field, index) => {
|
|
14
|
-
acc[String(field)] = row[index];
|
|
15
|
-
return acc;
|
|
16
|
-
}, {});
|
|
17
|
-
});
|
|
40
|
+
const fields = record.fieldList;
|
|
41
|
+
const normalizedList = record.list.map((row) => (Array.isArray(row) ? zipFieldRow(fields, row, record) : row));
|
|
18
42
|
const { fieldList, list, ...meta } = record;
|
|
19
43
|
const hasMeta = Object.keys(meta).length > 0;
|
|
20
44
|
return hasMeta ? { ...meta, list: normalizedList } : normalizedList;
|
|
@@ -9,6 +9,26 @@ export function decodeResponseBody(buf, contentEncoding) {
|
|
|
9
9
|
const b = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
10
10
|
return enc === "gzip" ? gunzipSync(b).toString("utf8") : b.toString("utf8");
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Re-quote the *unquoted numeric* values of the named JSON fields so ids past
|
|
14
|
+
* 2^53 survive JSON.parse: `{"taskId":1782345678901234567}` would otherwise come
|
|
15
|
+
* back as 1782345678901234700 and address a task that does not exist.
|
|
16
|
+
*
|
|
17
|
+
* Probed 2026-07-25: file-parse's submit returns `taskId` as a STRING (matching
|
|
18
|
+
* the spec), so this is a forward guard, not a live fix — but the failure mode it
|
|
19
|
+
* covers is silent and expensive (the parse job is already billed, and a rounded
|
|
20
|
+
* id can never fetch its result). Values already in quotes are left alone: the
|
|
21
|
+
* pattern only matches a bare number right after the key.
|
|
22
|
+
*/
|
|
23
|
+
export function quoteBigIntFields(text, fields) {
|
|
24
|
+
if (!fields?.length)
|
|
25
|
+
return text;
|
|
26
|
+
let out = text;
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
out = out.replace(new RegExp(`("${field}"\\s*:\\s*)(-?\\d+)`, "g"), '$1"$2"');
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
12
32
|
/** A Retry-After delay we'll honor even if it exceeds maxDelay — but never past this
|
|
13
33
|
* ceiling, so a hostile/misconfigured header can't hang the CLI for minutes. */
|
|
14
34
|
const RETRY_AFTER_CEILING_MS = 60_000;
|
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.29.0";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gangtise-openapi
|
|
3
|
-
version: "0.
|
|
3
|
+
version: "0.29.0"
|
|
4
4
|
description: |-
|
|
5
5
|
通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
|
|
6
6
|
|
|
7
7
|
**触发词**:调接口 / CLI / openapi / 导出 / 下载研报 / 批量查 / 拉数据 / 跑一下 / 钢尼斯 / gtIC(Gangtise 语音误识别)
|
|
8
8
|
|
|
9
|
-
**适用**:原始数据导出、批量 jsonl/csv、下载 PDF/MD、行情 K 线、财务报表、估值指标、证券级数据指标(EDE
|
|
9
|
+
**适用**:原始数据导出、批量 jsonl/csv、下载 PDF/MD、行情 K 线、财务报表、估值指标、证券级数据指标(EDE 截面/时序)、财报日历(业绩预告/快报/公告)、PDF 解析为 Markdown、AI 能力(一页通/投资逻辑/同业对比/个股看点·投研总结/投研线索/业绩点评/观点PK·多空辩论/主题跟踪/热点话题/管理层讨论/调研提纲/知识库搜索)、云盘文件管理(Vault)
|
|
10
10
|
|
|
11
11
|
**不适用**:不脱离 OpenAPI 自行撰写研报、编造投研结论或做自由问答——观点总结、多空 PK 等 AI 产物本 skill 只经由 Gangtise 平台 AI 接口获取,不自行生成
|
|
12
12
|
|
|
@@ -36,6 +36,7 @@ description: |-
|
|
|
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
|
+
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`,仅 A 股)。
|
|
39
40
|
|
|
40
41
|
## 工作流(3 步)
|
|
41
42
|
|
|
@@ -67,9 +68,10 @@ description: |-
|
|
|
67
68
|
"免费"=0 积分;**只列单价**,数据范围(可查多久)随账号等级不同、不在此列。
|
|
68
69
|
|
|
69
70
|
- **免费**:所有 `quote` 行情、`fundamental` 报表/主营/估值/股东(**盈利预测除外**)、`reference`/`constant` 查询(含 `official-account-search`)、`alternative edb-search`、`vault`(record/wechat/股票池/drive/AI云盘)、`insight report-image list`
|
|
70
|
-
- **0.1/条 list**:research / foreign-report / official-account / announcement(A/港/美) / summary / qa 的 list、`vault my-conference-list`;`insight report-image download` 0.1/张
|
|
71
|
+
- **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/张
|
|
71
72
|
- **按条(观点/含详情类 list)**:independent-opinion list 与 `ai security-clue` 5;roadshow/site-visit/strategy/forum list 20;opinion / foreign-opinion list 30;`fundamental earning-forecast` 0.5;`ai stock-summary` 3(无看点的证券不返回也不扣);`alternative edb-data` 30
|
|
72
|
-
- **各 download(/篇)**:announcement / official-account / research 10;announcement-hk / announcement-us 20;independent-opinion 30;summary / foreign-report / my-conference 50
|
|
73
|
+
- **各 download(/篇)**:announcement / official-account / research 10;announcement-hk / announcement-us 20;independent-opinion 30;summary / foreign-report / my-conference 50;`performance-calendar download` A 股 10 / 港美股 20
|
|
74
|
+
- **按页**:`tool file-parse` 0.8/页,**提交(`--file`)时按实际页数一次性扣**,取结果(`file-parse-check`)免费——50 页 PDF = 40 积分,别重复提交同一文件
|
|
73
75
|
- 🔴 **按次贵**:`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/篇
|
|
74
76
|
- 🔴 **极贵**:`alternative concept-info` / `concept-securities` **500/次**
|
|
75
77
|
- ⚠️ **同参数重复调用不免费**:按次计费无缓存命中豁免(2026-07-11 实测 `one-pager` 重复调用每次扣分,即使秒回缓存内容)——生成类结果拿到后自行留存复用,别为"刷新"重调;CLI 已对上述 🔴 贵档端点关闭 5xx/超时自动重放(v0.26.0),50/篇 的 `summary` / `foreign-report` / `my-conference` download 同样不重放(v0.27.0),正是为防重复扣分
|
|
@@ -103,6 +105,7 @@ description: |-
|
|
|
103
105
|
| 外资独立观点 / 独立分析师观点 | `insight independent-opinion list` |
|
|
104
106
|
| 纪要 / 会议纪要(外部) | `insight summary list` |
|
|
105
107
|
| 路演 / 调研 / 策略会 / 论坛 | `insight roadshow / site-visit / strategy / forum list` |
|
|
108
|
+
| 财报日历 / 业绩预告 / 业绩快报 / 财报披露排期 | `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` 可下) |
|
|
106
109
|
| A 股公告 / 公告 | `insight announcement list` |
|
|
107
110
|
| 港股公告 / HK 公告 | `insight announcement-hk list` |
|
|
108
111
|
| 美股公告 / US 公告 | `insight announcement-us list` |
|
|
@@ -154,6 +157,7 @@ description: |-
|
|
|
154
157
|
| 题材 ID 搜索 | `reference concept-search` |
|
|
155
158
|
| 板块 ID 搜索 | `reference sector-search` |
|
|
156
159
|
| 板块成分股 | `reference sector-constituents`(前置:`reference sector-search` 拿 `sector-id`) |
|
|
160
|
+
| PDF 转 Markdown / 解析文件 / 提取 PDF 正文 | `tool file-parse --file <x.pdf> --wait`(异步,0.8 积分/页,提交时扣;取结果 `tool file-parse-check --task-id`。**平台自有研报/公告优先用各 download 的 `--file-type 2` 直出 Markdown**,别花解析费) |
|
|
157
161
|
|
|
158
162
|
**易混淆消歧**:
|
|
159
163
|
- "纪要" → 外部信息走 `insight summary`;公司内部录音/会议走 `vault my-conference`
|
|
@@ -163,7 +167,7 @@ description: |-
|
|
|
163
167
|
- **证券基本面 / 指标先按任务形态路由,不是搜到 EDE 就一律走 EDE**:
|
|
164
168
|
- 单证券先优先对应 `fundamental` 专用命令(财务、估值、盈利预测、股东、主营或完整三大报表,多数免费 / 低价)。其中 `valuation-analysis` / `earning-forecast` 实测仅支持 A 股;港 / 美股的估值历史分位、盈利预测、以及 PE/PB 等核心估值(EDE 也仅 A 股)当前 CLI 均无可用接口,如实说明不支持、勿用别的语义顶替
|
|
165
169
|
- 多证券批量取一组**已实现**财务 / 估值指标 → 优先 `indicator search` 后用 EDE 一次拉取,替代逐只循环;单日或同一报告期横向比较用 `cross-section`,区间走势用 `time-series`(后者不能多指标 × 多证券同时)。**批量按 code 回填加 `--key-by code`**(列头用 `indicatorCode`,防同名指标碰撞 + 服务端重排列序导致的错位)
|
|
166
|
-
- 始终排除 EDE:A股盈利预测 / 一致预期(含预测 EPS)→ `fundamental earning-forecast`;A股估值历史分位 → `fundamental valuation-analysis`;开高低收 / 成交量等行情与 K 线 → `quote`;单证券完整报表 →
|
|
170
|
+
- 始终排除 EDE:A股盈利预测 / 一致预期(含预测 EPS)→ `fundamental earning-forecast`;A股估值历史分位 → `fundamental valuation-analysis`;开高低收 / 成交量等行情与 K 线 → `quote`;单证券完整报表 → 对应三大报表命令。**例外:总市值只有 EDE 有**——`quote realtime` / `day-kline` 都不返回市值,走 `indicator cross-section --indicator qte_mkt_cptl`(仅 A 股,默认单位「元」,用 `--scale` 缩放)。EDE 搜到的基本 / 稀释 EPS 是已实现值,**不能冒充预测 EPS**;港 / 美股缺少上述专用能力时应如实说明不支持,不能用别的语义代替
|
|
167
171
|
- EDE 取数前必须用 `search --format json` 同时核对:`indicatorName` + `description` 语义准确、`scopeList` 覆盖全部目标市场 / 证券类型、`parameterList` 必填参数与枚举可满足;`scopeList` 缺失 / `null` / 空或任一项不符,都视为无法证明覆盖并回退专用接口。专用接口也不覆盖目标市场时,说明当前不可用,不要硬调。`scopeList` 按指标各不相同,不能因 EDE 服务支持 A / 港 / 美股就假定某个指标三市场都覆盖
|
|
168
172
|
- `indicator search` 免费,`cross-section` / `time-series` 按单元格计费;除多证券批量的效率收益外,仍优先免费 / 低价的 `quote` 或 `fundamental`
|
|
169
173
|
- 行业 / 宏观指标(空调销量、社融等,无证券维度)走 `alternative edb-*`(EDB),不要与证券级 EDE 混用
|
|
@@ -203,6 +207,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
203
207
|
| **AI 内容** | one-pager / investment-logic / peer-comparison / research-outline | `{content: "markdown文本"}` | 取 `content` 直接呈现 |
|
|
204
208
|
| **K 线** | quote * | `{list: [{tradeDate, ...}]}` | 按 tradeDate 排序,取需要的尾部 |
|
|
205
209
|
| **异步(含 *-check)** | earnings-review / viewpoint-debate / earnings-review-check / viewpoint-debate-check | 提交 `{dataId, status, hint}`;check 成功 `{date, content}` / pending `{status:"pending"}` 或抛 `140001`(旧 `410110`) | 见下方"异步任务流程" |
|
|
210
|
+
| **异步文件** | tool file-parse / file-parse-check | 提交 `{taskId, status, hint}`;就绪后 stdout = ZIP 路径,未就绪 `{status:"pending"}` | 解压取 `file.md`;重取用 `file-parse-check`(免费),别重跑 `file-parse`(按页重扣) |
|
|
206
211
|
|
|
207
212
|
完整字段对照见 `references/response-schema.md`。
|
|
208
213
|
|
|
@@ -236,7 +241,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
236
241
|
| 最新一期 / 最新报告期(财报) | — | — | 省略 `--fiscal-year`,传 `--period latest`(默认) |
|
|
237
242
|
| 最新观点 / 今日观点 | 1 天范围 + `--rank-type 2` | — | — |
|
|
238
243
|
|
|
239
|
-
日期参数**按参数名判断、不按命令组**(命令组会误导——AI 里既有 `--start-time` 又有 `--date`/`--report-date
|
|
244
|
+
日期参数**按参数名判断、不按命令组**(命令组会误导——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` 分隔)。
|
|
240
245
|
|
|
241
246
|
支持时间倒序的命令加 `--rank-type 2`:opinion / summary / research / foreign-report / announcement / announcement-hk / announcement-us / foreign-opinion / independent-opinion / official-account。其他 list 命令按 API 默认排序。
|
|
242
247
|
|
|
@@ -361,13 +366,14 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
|
|
|
361
366
|
|
|
362
367
|
按需 Read 对应文件:
|
|
363
368
|
|
|
364
|
-
- 内资观点 / 纪要 / 路演 / 调研 / 策略 / 论坛 / 研报 / 外资研报 / A 股公告 / 港股公告 / 美股公告 / 外资观点 / 独立观点 / 公众号(official-account)/ 投资者问答(qa)/ 研报图表(report-image)→ `references/commands/insight.md`
|
|
369
|
+
- 内资观点 / 纪要 / 路演 / 调研 / 策略 / 论坛 / 财报日历(performance-calendar)/ 研报 / 外资研报 / A 股公告 / 港股公告 / 美股公告 / 外资观点 / 独立观点 / 公众号(official-account)/ 投资者问答(qa)/ 研报图表(report-image)→ `references/commands/insight.md`
|
|
365
370
|
- 行情命令(A 股 / 港股 / 美股日 K / 指数日 K / 分钟 K / 实时行情 / 资金流向 fund-flow) → `references/commands/quote.md`
|
|
366
371
|
- 三大报表(A 股 / 港股 / 美股)/ 主营 / 估值 / 盈利预测 / 股东 → `references/commands/fundamental.md`
|
|
367
372
|
- knowledge-batch / security-clue / 个股看点(stock-summary)/ AI agent / 异步任务 / 主题跟踪 / 热点 / 管理层讨论 → `references/commands/ai.md`
|
|
368
373
|
- drive / record / my-conference / wechat / 股票池 → `references/commands/vault.md`
|
|
369
374
|
- 行业指标数据库(EDB)/ 题材指数画像与成分股(concept-info / concept-securities)→ `references/commands/alternative.md`
|
|
370
375
|
- 数据指标(EDE:search / cross-section / time-series,证券级指标截面与时序)→ `references/commands/indicator.md`
|
|
376
|
+
- PDF 解析(file-parse:上传 PDF → Markdown + 图片 ZIP)→ `references/commands/tool.md`
|
|
371
377
|
- securities-search / chiefs-search(首席 ID)/ institution-search(机构 ID)/ official-account-search(公众号 ID)/ 常量查询(constant-category / constant-list)/ 题材 ID(concept-search)/ 板块(sector-search / sector-constituents)/ lookup 本地表 / 行业别名 / raw call → `references/commands/reference-and-lookup.md`
|
|
372
378
|
|
|
373
379
|
跑通流程对照 → `references/examples.md`
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
通用:所有命令都需 `--security-code`(如 `600519.SH`,注意是 `--security-code` 不是 `--security`)。`--field` 可重复,可用字段见 `references/fields.md`;A / 港 / 美股三大报表命令都在省略 `--field` 时返回完整报表,指定后只保留基础字段与所选科目。
|
|
4
4
|
|
|
5
|
+
**`--field` 字段名必须核对**(v0.28.3 起传错直接报错):三大报表遇到不存在的字段名会补 `null`(安全),但 `main-business` / `valuation-analysis` 是**只丢值、字段名照请求回显**,按位置拍平会把值贴到错误的字段上(同 `quote realtime`,详见 `references/commands/quote.md`)。CLI 现在长度不匹配就直接失败(退出码 1),不输出错位数据。不确定字段名就别传 `--field`。
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## A股三大报表(累计) `income-statement` / `balance-sheet` / `cash-flow`
|
|
@@ -15,6 +17,7 @@ gangtise fundamental <income-statement|balance-sheet|cash-flow> --security-code
|
|
|
15
17
|
- `--fiscal-year` 可重复:`--fiscal-year 2023 --fiscal-year 2024`
|
|
16
18
|
- `--start-date`/`--end-date` 有值时覆盖 `--fiscal-year`
|
|
17
19
|
- **固定返回字段**(无需 `--field` 指定):`securityCode` `companyName` `category` `announcementDate` `endDate` `fiscalYear` `period` `reportType` `companyType` `currency` `unit`
|
|
20
|
+
- ⚠️ **本节两个命令的 `companyType` / `currency` 值是反的**(实测 2026-07-24,服务端字段名映射问题,与公司类型无关):A 股累计口径的 `balance-sheet` / `cash-flow` 返回 `companyType=人民币`、`currency=一般企业`(工行则是 `currency=银行`);**`income-statement` 是对的**。另:A 股 `*-quarterly` 单季表的 `companyType` 返回未映射的数字码(如 `102119999`)、`currency` 正确;**港股 / 美股三表实测均正常**(`companyType=一般企业`、`currency=人民币`/`美元`)。读这两列时按**值**判断语义,别按列名——科目数字本身不受影响
|
|
18
21
|
|
|
19
22
|
**常用字段速查:**
|
|
20
23
|
- 利润表:`totalOpRev` 营收 | `netProfit` 净利润 | `netProfitAttrParent` 归母 | `basicEPS` EPS | `rdExp` 研发
|
|
@@ -75,6 +78,7 @@ gangtise fundamental main-business --security-code <code> [--breakdown <type>] [
|
|
|
75
78
|
- `--period`:`interim` 中报 | `annual` 年报(可重复)
|
|
76
79
|
- 默认时间窗:`endDate` 当前日期、`startDate` 三年前
|
|
77
80
|
- **不支持 `--fiscal-year`**(误传触发 `100001`/`100003`,旧 `900001`);按年份筛选用 `--start-date`/`--end-date`
|
|
81
|
+
- `--field` 只认 `references/fields.md` 「主营业务」小节的字段(`opRevenue` / `grossProfit` 等);`securityCode`、`itemName` 这类**不是**该接口字段,传了会报「响应字段数与 fieldList 不匹配」(前 3 列 `periodName` / `periodEndDate` / `categoryName` 恒定返回)
|
|
78
82
|
|
|
79
83
|
## A股估值分析 `fundamental valuation-analysis`
|
|
80
84
|
|
|
@@ -86,6 +90,7 @@ gangtise fundamental valuation-analysis --security-code <code> --indicator <name
|
|
|
86
90
|
- `--indicator`(**必选**):`peTtm` 滚动PE | `pbMrq` PB | `peg` PEG | `psTtm` 滚动PS | `pcfTtm` 滚动PCF | `em` 企业倍数
|
|
87
91
|
- `--limit` 默认 2000,省略 `--start-date` 时自动查近一年
|
|
88
92
|
- `--skip-null`:丢弃 `value`/`percentileRank` 为 null 的行(最新交易日可能未入库)
|
|
93
|
+
- **返回字段只有 7 个**(实测 2026-07-24):`tradeDate` `value` `percentileRank` `average` `median` `upper1Std` `lower1Std`。**没有 `securityCode`**——误传会拿到一列重复的 `tradeDate`(长度相等,CLI 拦不住),传其他不存在的字段名则直接报错。**建议不传 `--field`**,证券代码本来就是你自己传进去的
|
|
89
94
|
|
|
90
95
|
## A股盈利预测 `fundamental earning-forecast`
|
|
91
96
|
|
|
@@ -59,7 +59,7 @@ gangtise indicator cross-section \
|
|
|
59
59
|
--security 600519.SH --security 000858.SZ --security 300750.SZ \
|
|
60
60
|
--date 2025-12-31 --format table
|
|
61
61
|
# 列:date / security / name / 营业收入(利润表,累计) / 基本每股收益(利润表,累计)
|
|
62
|
-
# 省略 reportType
|
|
62
|
+
# 省略 reportType 即取合并口径(茅台2025=1688亿)。⚠️ 服务端 label 与实际取数错位:实际 1=合并 2=合并(调整) 3=母公司 4=母公司(调整),按 label 传会取反(详见下方「reportType 口径」)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## 指标时间序列 `indicator time-series`
|
|
@@ -138,7 +138,18 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
|
|
|
138
138
|
- **名称反查 code 要核对,别取首条**:存在同显示名的兄弟指标——单季 `cf_finc_exp_qtr` 与累计 `cf_finc_exp` 都叫「财务费用」,`bs_fmt`/`cf_fmt`/`is_fmt` 都叫「报表格式」。`search` 按名称模糊匹配,目标 code 高概率在 top1 但不绝对,要看 `indicatorCode` 确认。
|
|
139
139
|
- **批量查询做失败拆分**:某指标**缺必填参数**或入参错误时会整批报错(单元格级无数据按 `null` 返回;整个查询无数据的 `999999` 例外见上),逐指标单查能定位是哪个指标缺参/不可查。
|
|
140
140
|
- **市值量纲(实测 2026-07)**:`qte_mkt_cptl`(总市值)**仅 A 股**——港股/美股返 `null`(换 `currency` 也没用,是 scope 外 ≠ 无数据);**默认返原始「元」**(茅台 ≈ `1.5e12`,即 1.5 万亿),别误当天文数字。用 `scale` 数字码缩放(`0`元 / `3`千 / `4`万 / `6`百万 / `8`亿 / `9`十亿——`scale=8` → `15038` 亿元)、`currency` 换币种(`dft`本币 / `cny` / `hkd` / `usd` …)。**跨证券比市值前先统一 `scale`+`currency`**。
|
|
141
|
-
- **EDE 财务指标的 `reportType
|
|
141
|
+
- **EDE 财务指标的 `reportType`:label 与 value 错位,但映射本身稳定可用(复测 2026-07-24,修正 07-23 的结论)**。服务端 label 标 `1`母公司/`2`合并/`3`母公司调整/`4`合并调整,**实际取数是**:
|
|
142
|
+
|
|
143
|
+
| value | 实际口径 | 说明 |
|
|
144
|
+
|-------|---------|------|
|
|
145
|
+
| `1`(默认,省略即此值) | **合并报表** | 绝大多数场景要的就是这个 |
|
|
146
|
+
| `2` | **合并报表(调整)** | 该报告期无重述数时为空 |
|
|
147
|
+
| `3` | **母公司报表** | |
|
|
148
|
+
| `4` | **母公司报表(调整)** | 该报告期无重述数时为空 |
|
|
149
|
+
|
|
150
|
+
实测对照(中信证券 `600030.SH` FY2024 营业收入,与 `fundamental income-statement --report-type` 逐一相等):`1`→637.9亿(合并)、`2`→581.2亿(合并调整)、`3`→321.9亿(母公司)、`4`→321.9亿(母公司调整);中国神华 `601088.SH` 同期 `1`→3383.75亿、`2`→3397.88亿,两值不同,可排除「2 就是合并原值」。**按 label 传会取反**(想要母公司却传 `1` 拿到合并)——认 value 不认 label:合并省略即可,母公司传 `3`。
|
|
151
|
+
|
|
152
|
+
`2`/`4` 返回空值**不是枚举失效**,是该报告期尚无调整表(如最新年报 FY2025:`1`/`3` 有数、`2`/`4` 无值,与 `fundamental --report-type consolidatedRestated` 同期无数据一致);查询内所有单元格都无值时才会升级成 `999999`。港股默认口径同样是合并(腾讯 FY2024 营收 6602.57亿)。
|
|
142
153
|
|
|
143
154
|
## 通用说明
|
|
144
155
|
|
|
@@ -56,6 +56,25 @@ gangtise insight forum list [--research-area <id>] [--location <id>]
|
|
|
56
56
|
- `--market`:路演 `aShares`|`hkStocks`|`usChinaConcept`|`usStocks`;调研 `aShares`|`hkStocks`|`usChinaConcept`(无 usStocks)
|
|
57
57
|
- `--research-area`(路演/调研/论坛):用 `gangtiseIndustry` 码(行业 `1008001xx` + 方向 `122000xxx`,见 `reference-and-lookup.md`)。**strategy 无 `--research-area`,只按 `--institution`/`--location` 筛**
|
|
58
58
|
|
|
59
|
+
## 财报日历 `insight performance-calendar list/download`
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gangtise insight performance-calendar list [--start-date <date>] [--end-date <date>] [--market <name>] [--security <code>] [--category <name>] [--from <n>] [--size <n>]
|
|
63
|
+
gangtise insight performance-calendar download --performance-report-id <id> [--output <path>]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- ⚠️ **本命令用 `--start-date` / `--end-date`(`yyyy-MM-dd`),不是其余 insight list 的 `--start-time`**;过滤的是 `publishDate`(财报事件发布日)。也**没有** `--keyword` / `--rank-type` / `--search-type`
|
|
67
|
+
- `--category`:`performanceForecast` 业绩预告 | `performanceExpress` 业绩快报 | `performanceAnnouncement` 业绩公告(可重复)
|
|
68
|
+
- `--market`:`aShares` | `hkStocks` | `usChinaConcept` | `usStocks`(可重复)
|
|
69
|
+
- `--market` / `--category` 拼错 CLI 本地直接报错(不是静默返全量)——这两个参数的枚举值不必猜
|
|
70
|
+
- `--security`:证券代码,如 `000001.SZ`(可重复)
|
|
71
|
+
- 自动翻页(`{total,list}`,单页上限 50)。**不加任何筛选时 total 十万量级**(实测 2026-07-25 为 126683,含未来已排期的财报日程)——CLI 因此要求至少一个约束:`--start-date` + `--end-date`、或 `--security`、或显式 `--size`,裸跑直接报 `ValidationError`(不发请求、不扣分)
|
|
72
|
+
- 只给 `--security`(不给日期/`--size`)时,CLI 额外套一个 **1000 行隐式上限**:单只证券的整段日历只有几十条,正常查询感知不到;万一服务端哪天不再按 `securityList` 过滤,结果会在 1000 行截断并标 `partial`(stderr 警告 + 退出码 3),而不是闷头翻完全表。判据是 `total`:只有「取满 1000 行且 total 显示还有更多」才告警——恰好 1000 行且 total=1000 是完整结果,退出码仍是 0。看到告警说明筛选**可能**没生效,改用日期范围重查
|
|
73
|
+
- 返回字段:`performanceReportId`(下载用)/ `securityCodeList[]`(A+H 同时上市会有多个代码)/ `securityName` / `category` / `publishDate` / `title` / `hasAttachment`
|
|
74
|
+
- 实测 `publishDate` 返回的是 `yyyy-MM-dd 00:00:00`(文档写 `yyyy-MM-dd`),取日期请截前 10 位
|
|
75
|
+
- download:**只有 `hasAttachment: true` 的记录能下**(先 list 确认);省略 `--output` 用真实标题命名(走 title-cache,未命中会回查 list 接口,那次回查按 0.1/条 计费——批量下载建议显式 `--output`)
|
|
76
|
+
- **积分**:list 0.1/条;download A 股 10/篇、港美股 20/篇
|
|
77
|
+
|
|
59
78
|
## 研报 `insight research list/download`
|
|
60
79
|
|
|
61
80
|
```bash
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
通用:`--field` 可重复(`--field open --field close`),可用字段见 `references/fields.md`。
|
|
4
4
|
|
|
5
|
+
**`--field` 字段名必须核对**(v0.28.3 起传错直接报错):上游对不存在的字段名有两套处理——day-kline / minute-kline / fund-flow 把字段名和值一起丢掉(安全);但 `quote realtime`(及 `fundamental main-business` / `valuation-analysis`)**只丢值、字段名照请求回显**,按位置拍平就把值贴到了错误的字段上。实测 `quote realtime --field securityCode --field close --field turnoverRate`:realtime 根本没有 `close`,返回的 2 个值被拍成 `close = 28.5573`(那是换手率,茅台真实价 1297.41)——不报错、数字看着合理、却完全是另一个指标。CLI 现在长度不匹配就直接失败(退出码 1),不输出错位数据。**不确定字段名就别传 `--field`**(返回全量最稳)。
|
|
6
|
+
|
|
5
7
|
**关键规则**:查"最近"K线必须显式 `--start-date`/`--end-date` 拉范围,再从 `tradeDate` 取尾部最近 N 条;不要只用 `--limit N`(会截取查询窗口开头)。
|
|
6
8
|
|
|
7
9
|
**自动分片**(v0.12.0;v0.14.2 修正分片粒度并注入 limit=10000):`--security all` 跨日期范围 CLI 会自动按日切片并并发执行(A 股 1 天/片,美股 1 天/片,HK 2 天/片,指数 30 天/片),合并结果返回。CLI 在 `--security all` 路径会自动把 `limit` 抬到 10000(API 上限),避免默认 6000 行截断。无需手动按季度分批。
|
|
@@ -50,6 +52,8 @@ gangtise quote realtime [--security <code>] [--field <name>]
|
|
|
50
52
|
- **全市场关键字**:`--security aShares` 全部 A 股 / `--security hkStocks` 全部港股 / `--security usStocks` 全部美股;建议配合 `--field` 精简返回字段
|
|
51
53
|
- 返回**最新时刻**的行情快照(最新价/开高低/涨跌/成交量额/振幅)
|
|
52
54
|
- 非交易时间返回最近一个交易日的收盘快照;停牌证券返回停牌前最后一个有效快照
|
|
55
|
+
- **实测全量字段(16 个,2026-07-24)**:`securityCode` `exchange` `tradeDate` `tradeTime` `open` `high` `low` `latestPrice` `preClose` `change` `pctChange` `volume` `amount` `turnoverRate` `amplitude` `volumeRatio`
|
|
56
|
+
- **没有 `close`**——收盘价语义用 `latestPrice`(非交易时间即为收盘价),或改用 `quote day-kline` 的 `close`;**也没有市值**,总市值走 `indicator cross-section --indicator qte_mkt_cptl`(仅 A 股)
|
|
53
57
|
- 字段速查:见 `references/fields.md` 中的"实时行情"小节
|
|
54
58
|
|
|
55
59
|
## A股资金流向 `quote fund-flow`
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Tool 命令详细参数(投研工具)
|
|
2
|
+
|
|
3
|
+
## PDF 解析 `tool file-parse` / `file-parse-check`
|
|
4
|
+
|
|
5
|
+
把 PDF(研报、公告、合同等)解析成 Markdown 正文 + 提取的图片,结果打包成 ZIP。异步两步:提交拿 `taskId` → 取结果 ZIP。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 一步到位(推荐):提交后阻塞轮询,就绪即落盘
|
|
9
|
+
gangtise tool file-parse --file <x.pdf> --wait [--output <out.zip>]
|
|
10
|
+
|
|
11
|
+
# 两步:先提交,稍后取结果
|
|
12
|
+
gangtise tool file-parse --file <x.pdf> # 输出 {"taskId": "...", "status": "pending"}
|
|
13
|
+
gangtise tool file-parse-check --task-id <id> [--output <out.zip>]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- `--file`(**必填**):待解析的 PDF。CLI 在上传前本地校验:文件存在、非空、后缀 `.pdf`、≤100MB——不合格直接报 `ValidationError`,不发请求也不扣分
|
|
17
|
+
- `--wait`:阻塞轮询到结果就绪(预算 ≈316s,覆盖官方「单文件约 3 分钟」)。**外层工具/命令超时要设到 ≥360s**,否则外层先超时(任务仍在服务端跑,之后 `file-parse-check` 照样能取)
|
|
18
|
+
- `--output`:结果 ZIP 落盘路径。省略时用服务端返回的文件名(实测形如 `<taskId>.md.zip`),无则 `file-parse-<taskId>.zip`
|
|
19
|
+
- 服务端限制:单文件 ≤100MB、≤500 页,同一用户最多 10 个并发任务
|
|
20
|
+
- **积分**:**提交时按实际页数一次性扣费,0.8 积分/页**(50 页 = 40 积分);取结果免费。提交端点标 `no-replay`——超时/5xx 不自动重放,避免同一文件重复扣费。**重跑 `file-parse` 会重新扣费,取结果请一律用 `file-parse-check --task-id`**
|
|
21
|
+
- 未就绪时 `file-parse-check` 输出 `{"taskId": "...", "status": "pending"}` 且退出码 0(服务端返 `140001` RESULT_GENERATING,HTTP 409,实测 2026-07-25),隔 ~1 分钟再取即可
|
|
22
|
+
|
|
23
|
+
ZIP 内容:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
├── file.md # Markdown 正文(保留标题层级与阅读顺序)
|
|
27
|
+
└── images/ # 从 PDF 提取的图片(JPG,文件名由服务端生成;无图时为空目录)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
呈现建议:解压后读 `file.md`;正文很长时先 `wc -l` / `head` 采样,不要整篇灌进上下文。
|
|
31
|
+
|
|
32
|
+
## 常见搭配
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# 下载研报原文 PDF → 解析成 Markdown
|
|
36
|
+
gangtise insight research download --report-id 1234567 --output ./r.pdf
|
|
37
|
+
gangtise tool file-parse --file ./r.pdf --wait --output ./r.zip
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
⚠️ 研报/公告类接口本身多数支持 `--file-type 2`(Markdown)直出,**先看能不能直接下 Markdown**(只算下载积分),别为已有 Markdown 的文件再花 0.8/页 走解析。`file-parse` 的价值在于外部来源的 PDF(自己的资料、非平台文件)。
|
|
@@ -53,7 +53,9 @@ gangtise vault wechat-message-list [--keyword <text>] [--security <code>] [--wec
|
|
|
53
53
|
- `--wechat-group-id`:先用 `vault wechat-chatroom-list` 查;可重复
|
|
54
54
|
- `--category`:`text` | `image` | `documents` | `url`(可重复)
|
|
55
55
|
- `--tag`:`roadShow` | `research` | `strategyMeeting` | `meetingSummary` | `industryComment` | `companyComment` | `earningsReview`(可重复)
|
|
56
|
-
-
|
|
56
|
+
- 返回字段(实测 2026-07-25):`msgId` / **`content`**(正文)/ **`url`**(链接)/ `msgTime` / `wechatGroupId` / `wechatGroupName` / `speakerName` / `category` / `tagList[]{tagCode, tagName}` / `securityList[]{securityCode, securityName}` / `quoteMsg{quoteMsgId, quoteContent, quoteUrl}`。旧版本文档写作 `msgContent` / `contentUrl` 是错的,取值请用 `content` / `url`
|
|
57
|
+
- `quoteMsg`(2026-07-24 新增):被引用的消息,无引用时为 `null`;`quoteContent` / `quoteUrl` 也可能为空。做上下文还原时用它把「回复」接回原消息
|
|
58
|
+
- 未打标签/未关联证券的消息,`tagList` / `securityList` 返回 `null`(不是空数组)
|
|
57
59
|
|
|
58
60
|
## 群 ID 查询 `vault wechat-chatroom-list`
|
|
59
61
|
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
- scopeList:覆盖全部三只 A 股;缺失/null/空也视为不通过
|
|
242
242
|
- parameterList:补 required 参数并核对枚举
|
|
243
243
|
任一不符 → 回退相应专用接口。
|
|
244
|
-
3. 三类指标日期语义不同 → 拆三次截面,均加 `--key-by code`(列头用 indicatorCode,跨三张表按 code 稳定合并、免受同名/服务端重排干扰;省略 reportType 即取合并口径,⚠️ 该枚举 label
|
|
244
|
+
3. 三类指标日期语义不同 → 拆三次截面,均加 `--key-by code`(列头用 indicatorCode,跨三张表按 code 稳定合并、免受同名/服务端重排干扰;省略 reportType 即取合并口径,⚠️ 该枚举 label 与实际取数错位——实际 1=合并 2=合并(调整) 3=母公司 4=母公司(调整),要母公司口径传 3、别按 label 传 1):
|
|
245
245
|
a) 财务(营收/EPS)用报告期末 2025-12-31:
|
|
246
246
|
gangtise indicator cross-section \
|
|
247
247
|
--indicator is_op_rev --indicator is_eps_bas \
|
|
@@ -294,3 +294,37 @@
|
|
|
294
294
|
--start-time "2026-06-06 00:00:00" --end-time "2026-07-06 23:59:59" --format json
|
|
295
295
|
4. 只有要「全量枚举」券商/机构表时才用本地 lookup broker-org/meeting-org list(institution-search 是搜索、非全量)
|
|
296
296
|
```
|
|
297
|
+
|
|
298
|
+
## 例 18:财报日历(本周谁发业绩预告)
|
|
299
|
+
|
|
300
|
+
**用户**:"这周 A 股有哪些业绩预告"
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
1. 路由 → insight performance-calendar list(不是 announcement——预告/快报/公告事件走财报日历)
|
|
304
|
+
2. ⚠️ 本命令用 --start-date/--end-date(yyyy-MM-dd,过滤 publishDate),不是其余 insight list 的 --start-time
|
|
305
|
+
"这周" → 本周一至今天
|
|
306
|
+
3. Pre-flight:不加筛选 total 十万量级(含未来排期)→ 必须带日期范围;0.1 积分/条,先 --size 探量
|
|
307
|
+
4. gangtise insight performance-calendar list \
|
|
308
|
+
--start-date 2026-07-20 --end-date 2026-07-25 \
|
|
309
|
+
--market aShares --category performanceForecast \
|
|
310
|
+
--size 50 --format json
|
|
311
|
+
→ list[].securityName / title / publishDate / performanceReportId / hasAttachment
|
|
312
|
+
5. 用户要原文时:只有 hasAttachment: true 能下(A股 10 积分/篇)
|
|
313
|
+
gangtise insight performance-calendar download --performance-report-id 33753017 --output ./预告.pdf
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## 例 19:外部 PDF 转 Markdown(异步文件解析)
|
|
317
|
+
|
|
318
|
+
**用户**:"把这份 PDF 转成 Markdown 我要读正文"
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
1. 先判断来源:平台自有研报/公告 → 直接 download --file-type 2 出 Markdown(只花下载积分),不要走解析
|
|
322
|
+
外部 PDF(用户自己的文件)→ tool file-parse
|
|
323
|
+
2. Pre-flight:🔴 按页计费 0.8/页、提交即扣——先看页数估积分(50 页 = 40 积分)告知用户
|
|
324
|
+
本地限制 CLI 会先校验:.pdf 后缀 / 非空 / ≤100MB
|
|
325
|
+
3. gangtise tool file-parse --file ./x.pdf --wait --output ./x.zip
|
|
326
|
+
(--wait 内部轮询 ≈316s;外层工具超时设 ≥360s。不带 --wait 则拿 taskId,稍后
|
|
327
|
+
gangtise tool file-parse-check --task-id <id> --output ./x.zip——取结果免费)
|
|
328
|
+
4. unzip 后读 file.md(图片在 images/);正文长先 wc -l / head 采样再呈现
|
|
329
|
+
5. 超时不要重跑 file-parse(会重复扣费),用 file-parse-check 拿同一个 taskId 的结果
|
|
330
|
+
```
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
| `low` | 最低价 | `change` | 涨跌额 |
|
|
31
31
|
| `pctChange` | 涨跌幅(%) | `volume` | 成交量(股,当日累计) |
|
|
32
32
|
| `amount` | 成交总额(当日累计) | `amplitude` | 振幅(%) |
|
|
33
|
+
| `turnoverRate` | 换手率(%) | `volumeRatio` | 量比 |
|
|
34
|
+
|
|
35
|
+
**以上 16 个就是全部**(实测 2026-07-24):**无 `close`**(用 `latestPrice`)、**无市值**(走 `indicator cross-section --indicator qte_mkt_cptl`)。realtime 传了不存在的字段名不会被忽略——上游只丢值、字段名照回显,CLI 检测到长度不匹配会直接报错(v0.28.3 起)。
|
|
33
36
|
|
|
34
37
|
### 指数日K线(沪深京 `quote index-day-kline`)
|
|
35
38
|
|
|
@@ -38,6 +38,9 @@ CLI 自动处理 envelope:`{code, msg, data}` 信封会按 `code === "000000"`
|
|
|
38
38
|
| insight official-account list | `{list, total}` | `list[].articleId` / `list[].accountName` / `list[].title` / `list[].publishTime` / `list[].articleCategory` / `list[].summary` / `list[].industryList[].industryName` / `list[].conceptList[].conceptName` / `list[].securityList[].securityCode` |
|
|
39
39
|
| insight official-account download | 文件路径(stdout) | — |
|
|
40
40
|
| insight qa list | `{list, total}` | `list[].source`(conference/interactive/survey)/ `list[].publishTime` / `list[].question` / `list[].answer` / `list[].member` / `list[].securityCode` / `list[].questionCategory[]` / `list[].answerImportant`(1/0) |
|
|
41
|
+
| insight performance-calendar list | `{list, total}` | `list[].performanceReportId`(下载用)/ `list[].securityCodeList[]`(A+H 可能多个)/ `list[].securityName` / `list[].category`(performanceForecast/performanceExpress/performanceAnnouncement)/ `list[].publishDate`(实测带 ` 00:00:00` 后缀)/ `list[].title` / `list[].hasAttachment`(`false` 则无法下载) |
|
|
42
|
+
| insight performance-calendar download | 文件路径(stdout,PDF) | — |
|
|
43
|
+
| tool file-parse | `{taskId, status:"pending", hint}`(提交);`--wait` 或 `file-parse-check` 就绪后 = 文件路径(stdout,ZIP) | ZIP 内 `file.md` + `images/`;未就绪时 check 输出 `{taskId, status:"pending"}`(退出码 0) |
|
|
41
44
|
| insight report-image list | `[{...}]`(扁平数组,无 `total`) | `[].chunkId`(下载用 `--chunk-id`)/ `[].title` / `[].sourceId` / `[].broker` / `[].category` / `[].page` / `[].totalPages` / `[].imageCaption[]` / `[].imageFootnote[]` / `[].pageContent`(该页 OCR/描述) |
|
|
42
45
|
| insight report-image download | 文件路径(stdout,JPEG) | — |
|
|
43
46
|
| reference securities-search | `{returnedCount, list}` | `list[].gtsCode` / `list[].gtsName` / `list[].category` / `list[].matchScore` / `list[].matchType` |
|
|
@@ -51,11 +54,11 @@ CLI 自动处理 envelope:`{code, msg, data}` 信封会按 `code === "000000"`
|
|
|
51
54
|
| reference sector-constituents | `{total, list}` | `list[].gtsCode` / `list[].gtsName`;total=0 说明 sectorId 不对(先 sector-search 确认) |
|
|
52
55
|
| quote day-kline / day-kline-hk / day-kline-us / index-day-kline | `{fieldList, list}` 或规范化后 `{list: [{...}]}` | `tradeDate` / `securityCode` / `open` / `close` / `pctChange` / `volume`;index 另含 `securityName`(指数名称,v0.15.0 起) |
|
|
53
56
|
| quote minute-kline | `{list: [{...}]}` | `tradeTime` / `open` / `close` / `volume` |
|
|
54
|
-
| quote realtime | `{fieldList, list, total}` 或规范化后 `{list: [{...}]}` | `securityCode` / `exchange` / `tradeDate` / `tradeTime` / `latestPrice` / `pctChange` / `volume` / `amount` / `amplitude` |
|
|
57
|
+
| quote realtime | `{fieldList, list, total}` 或规范化后 `{list: [{...}]}` | `securityCode` / `exchange` / `tradeDate` / `tradeTime` / `open` / `high` / `low` / `latestPrice` / `preClose` / `change` / `pctChange` / `volume` / `amount` / `turnoverRate` / `amplitude` / `volumeRatio`(共 16 个,**无 `close`、无市值**) |
|
|
55
58
|
| quote fund-flow | `{fieldList, list, total}` 列式 → 规范化后 `{list: [{...}], total}` 宽表 | `securityCode` / `tradeDate` + 请求的字段(`mainNetInflow` / `largeInflow` / `xlargeOutflow` / …) |
|
|
56
|
-
| fundamental income-statement / balance-sheet / cash-flow(含 quarterly / -hk / -us) | `{total, list: [{...}]}` | `fiscalYear` / `period` / `endDate` / `companyName` / `companyType
|
|
57
|
-
| fundamental main-business | `{list: [{...}]}` | `
|
|
58
|
-
| fundamental valuation-analysis(仅 A 股) | `{list: [{...}]}` | `tradeDate` / `value` / `percentileRank` |
|
|
59
|
+
| fundamental income-statement / balance-sheet / cash-flow(含 quarterly / -hk / -us) | `{total, list: [{...}]}` | `fiscalYear` / `period` / `endDate` / `companyName` / `companyType` / `currency` / `unit` + 各 `--field` 字段;港股/美股另含 `timeCovered`(不规则跨度)。⚠️ **A 股累计口径的 `balance-sheet` / `cash-flow` 两个命令**,`companyType` 与 `currency` 的值是互换的(实测 2026-07-24:`companyType=人民币`、`currency=银行`/`一般企业`;A 股 `income-statement` 正确,港股/美股三表均正确);A 股 `*-quarterly` 单季表则是 `companyType` 返回未映射的数字码(如 `102119999`)、`currency` 正确。读这两列按**值**判断语义,别按列名;科目数字不受影响 |
|
|
60
|
+
| fundamental main-business | `{fieldList, list}` 列式 → 规范化后 `{list: [{...}]}` | 前 3 列恒定:`periodName` / `periodEndDate` / `categoryName`(分项名,随 `--breakdown` 变)+ `opRevenue` / `opRevenueYoy` / `opRevenueRatio` / `opCost` / `opCostYoy` / `opCostRatio` / `grossProfit` / `grossProfitYoy` / `grossProfitRatio` / `grossMargin` / `grossMarginYoy` / `grossMarginRatio`(共 15 个,实测 2026-07-24)。`--field` 只能从后 12 个里选 |
|
|
61
|
+
| fundamental valuation-analysis(仅 A 股) | `{fieldList, list}` 列式 → 规范化后 `{list: [{...}]}` | `tradeDate` / `value` / `percentileRank` / `average` / `median` / `upper1Std` / `lower1Std`(共 7 个,实测 2026-07-24);**无 `securityCode`**——误传会拿到一列重复的 `tradeDate` |
|
|
59
62
|
| fundamental earning-forecast(仅 A 股) | `{securityCode, securityName, updateList: [...]}` | `updateList[].date` / `updateList[].fieldList[].forecastYear` + 各 consensus 指标 |
|
|
60
63
|
| fundamental top-holders | `{holderType, list: [{...}]}` | `reportPeriod` / `rank` / `shareholderName` / `holdingNum` / `holdingPct` / `chgNum` / `chgPct` |
|
|
61
64
|
| ai knowledge-batch | `{list: [{...}]}` | `list[].resourceType` / `list[].sourceId` / `list[].title` / `list[].summary` |
|
|
@@ -73,7 +76,7 @@ CLI 自动处理 envelope:`{code, msg, data}` 信封会按 `code === "000000"`
|
|
|
73
76
|
| vault record-download | 文件路径(stdout) | — |
|
|
74
77
|
| vault my-conference-list | `{list, total}` | `list[].conferenceId` / `list[].title` / `list[].category` / `list[].institution.institutionName` / `list[].publishTime` |
|
|
75
78
|
| vault my-conference-download | 文件路径(stdout) | — |
|
|
76
|
-
| vault wechat-message-list | `{list, total}` | `list[].msgId` / `list[].
|
|
79
|
+
| vault wechat-message-list | `{list, total}` | `list[].msgId` / `list[].content`(正文)/ `list[].url` / `list[].msgTime` / `list[].wechatGroupName` / `list[].speakerName` / `list[].category` / `list[].tagList[].tagCode` / `list[].securityList[].securityCode` / `list[].quoteMsg.quoteContent`(引用消息,无引用为 `null`)。**不是 `msgContent`/`contentUrl`**(旧文档笔误,实测 2026-07-25) |
|
|
77
80
|
| vault wechat-chatroom-list | `{list, total}` | `list[].chatroomName` / `list[].chatroomId` |
|
|
78
81
|
| alternative edb-search | `{list: [...]}` 指标列表 | `indicatorId` / `indicatorName` / `dataSource` / `frequency` / `unit` |
|
|
79
82
|
| alternative edb-data | 列表,每行 `{date, <indicatorId>: value, ...}` 宽表 | `date` + 每个 `--indicator-id` 一列(该日指标值) |
|