gangtise-openapi-cli 0.11.1 → 0.13.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 +123 -7
- package/dist/src/cli.js +181 -31
- package/dist/src/core/asyncContent.js +13 -4
- package/dist/src/core/client.js +208 -125
- package/dist/src/core/commandBodies.js +1 -0
- package/dist/src/core/download.js +4 -0
- package/dist/src/core/endpoints.js +104 -8
- package/dist/src/core/output.js +64 -0
- package/dist/src/core/printer.js +7 -3
- package/dist/src/core/quoteSharding.js +81 -0
- package/dist/src/core/titleCache.js +58 -10
- package/dist/src/core/transport.js +91 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
一个可直接调用 Gangtise OpenAPI 获取金融数据的命令行工具,同时提供Agent Skill。
|
|
4
4
|
|
|
5
|
+
## Changelog
|
|
6
|
+
|
|
7
|
+
### v0.13.0 — 2026-05-15
|
|
8
|
+
|
|
9
|
+
**新增接口**
|
|
10
|
+
- `fundamental income-statement-hk / balance-sheet-hk / cash-flow-hk` — 港股三大报表(中国会计准则)
|
|
11
|
+
- `alternative edb-search` — 行业指标列表搜索(按关键词匹配指标名称,返回 indicatorId 等元信息)
|
|
12
|
+
- `alternative edb-data` — 行业指标时序数据(批量按 indicatorId 拉取时间序列,最多 10 个指标)
|
|
13
|
+
- `vault stock-pool-list` — 查询用户自选股股票池列表(poolId / poolName)
|
|
14
|
+
- `vault stock-pool-stocks` — 查询股票池证券明细(支持 `--pool-id all` 全量查询)
|
|
15
|
+
|
|
16
|
+
**接口变更**
|
|
17
|
+
- `fundamental income-statement / balance-sheet / cash-flow / income-statement-quarterly / cash-flow-quarterly` 名称调整为 A股报表(路径不变)
|
|
18
|
+
- `ai management-discuss-announcement` `--dimension` 新增 `all` 选项,返回报告中完整的管理层讨论内容(内容可能较长)
|
|
19
|
+
- `vault wechat-message-list` 新增 `--security <code>` 参数(按证券代码过滤),返回结果增加 `securityList` 字段
|
|
20
|
+
|
|
21
|
+
### v0.12.0 — 2026-05-10
|
|
22
|
+
|
|
23
|
+
**性能 / 架构**
|
|
24
|
+
- 翻页并行化:自动翻页接口拉到首页 `total` 后,剩余页通过 `Promise.all` 并发请求(默认并发 5,`GANGTISE_PAGE_CONCURRENCY` 可调)
|
|
25
|
+
- 共享 `undici.Agent`:所有请求复用连接池(keep-alive 60s,max 16 连接),避免重复 TLS 握手
|
|
26
|
+
- 流式下载:`--output` 指定时二进制响应直接 `pipeline` 到磁盘,不再走内存 `Uint8Array`
|
|
27
|
+
- 流式输出:`--format jsonl/csv --output xxx` 且 ≥1000 行时逐行写盘
|
|
28
|
+
- Token 内存缓存:Token 在进程内不再每次读盘
|
|
29
|
+
- 自动重试:5xx / `ECONNRESET` / `ETIMEDOUT` / `999999` 自动指数退避重试 2 次
|
|
30
|
+
- Token 自愈:8000014/8000015 自动重新登录并重试一次
|
|
31
|
+
- 异步轮询退避:`earnings-review` / `viewpoint-debate` 轮询从固定 15s 改为 5→8→13→20→30s 指数退避
|
|
32
|
+
- K线自动分片:`quote day-kline --security all` 等全市场查询自动按日期切分并发执行
|
|
33
|
+
- 标题缓存:原"读全文→改→写全文"改为内存快照 + 原子写入(temp+rename)
|
|
34
|
+
|
|
35
|
+
**调试 / 可观测性**
|
|
36
|
+
- 新增 `--verbose` / `GANGTISE_VERBOSE=1`:打印每个请求的耗时、状态码、响应字节数到 stderr
|
|
37
|
+
|
|
38
|
+
### v0.11.1 — 2026-05-10
|
|
39
|
+
|
|
40
|
+
**新增接口**
|
|
41
|
+
- `insight announcement-hk list/download` — 查询/下载港股公告
|
|
42
|
+
- `insight foreign-opinion list` — 查询外资机构观点(外资券商)
|
|
43
|
+
- `insight independent-opinion list/download` — 查询/下载外资独立分析师观点
|
|
44
|
+
- `reference securities-search` — GTS Code 搜索(按名称/代码/拼音多维度匹配证券)
|
|
45
|
+
|
|
46
|
+
**接口变更**
|
|
47
|
+
- `insight summary download` 新增可选 `--file-type`(`1`=原始内容 / `2`=HTML),仅影响来源为会议平台的纪要
|
|
48
|
+
- `insight announcement list/download` 名称调整为"查询A股公告列表/下载A股公告文件"(路径不变)
|
|
49
|
+
- `insight opinion list` 名称调整为"查询内资机构观点列表"(路径不变)
|
|
50
|
+
|
|
51
|
+
### v0.11.0 — 2026-04-17
|
|
52
|
+
|
|
53
|
+
- 新增 `ai viewpoint-debate` / `viewpoint-debate-check` — 观点PK(异步)
|
|
54
|
+
- 新增 `ai management-discuss-announcement` / `management-discuss-earnings-call` — 管理层讨论
|
|
55
|
+
|
|
56
|
+
### v0.10.9 — 2026-04-10
|
|
57
|
+
|
|
58
|
+
- 修复信封检测、版本更新检查、端点去重
|
|
59
|
+
- 新增 `quote index-day-kline` 指数日K线
|
|
60
|
+
- 新增 `vault wechat-message-list` / `wechat-chatroom-list` 群消息
|
|
61
|
+
|
|
5
62
|
## 首次安装
|
|
6
63
|
|
|
7
64
|
```bash
|
|
@@ -46,9 +103,14 @@ export GANGTISE_ACCESS_KEY="your-ak"
|
|
|
46
103
|
export GANGTISE_SECRET_KEY="your-sk"
|
|
47
104
|
export GANGTISE_BASE_URL="https://open.gangtise.com"
|
|
48
105
|
export GANGTISE_TOKEN="Bearer xxx"
|
|
106
|
+
|
|
107
|
+
# 性能/调试可选项
|
|
108
|
+
export GANGTISE_PAGE_CONCURRENCY=5 # 翻页并发数(默认 5)
|
|
109
|
+
export GANGTISE_VERBOSE=1 # 打印每个请求的耗时与字节数
|
|
110
|
+
export GANGTISE_TIMEOUT_MS=30000 # 请求超时(默认 30s)
|
|
49
111
|
```
|
|
50
112
|
|
|
51
|
-
如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。
|
|
113
|
+
如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。Token 失效(8000014/8000015)时会自动重新登录并重试一次。
|
|
52
114
|
|
|
53
115
|
|
|
54
116
|
## AI Agent Skill
|
|
@@ -63,10 +125,19 @@ Skill 目录结构:
|
|
|
63
125
|
|
|
64
126
|
```
|
|
65
127
|
gangtise-openapi/
|
|
66
|
-
├── SKILL.md
|
|
128
|
+
├── SKILL.md # 主 skill 文件(必备规则、速查表、按需引用 references)
|
|
67
129
|
└── references/
|
|
68
|
-
├──
|
|
69
|
-
|
|
130
|
+
├── commands/ # 按命令组拆分的详细参数文档(agent 按需 Read)
|
|
131
|
+
│ ├── ai.md # AI 能力命令(one-pager / earnings-review / viewpoint-debate 等)
|
|
132
|
+
│ ├── fundamental.md # 财务数据命令(三大报表 / 估值 / 盈利预测 / 股东)
|
|
133
|
+
│ ├── insight.md # 投研内容命令(研报 / 观点 / 纪要 / 公告 / 外资)
|
|
134
|
+
│ ├── quote.md # 行情命令(A股/港股/指数 K 线)
|
|
135
|
+
│ ├── reference-and-lookup.md # GTS Code 搜索与枚举速查
|
|
136
|
+
│ └── vault.md # 云盘/录音/会议/群消息
|
|
137
|
+
├── examples.md # 典型场景的端到端示例
|
|
138
|
+
├── fields.md # K线/财务字段中英文对照速查表
|
|
139
|
+
├── lookup-ids.md # 常用 ID 速查表(行业/券商/机构/公告分类等)
|
|
140
|
+
└── response-schema.md # 各接口响应字段说明
|
|
70
141
|
```
|
|
71
142
|
|
|
72
143
|
安装:
|
|
@@ -102,15 +173,19 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
102
173
|
|------|--------|------|
|
|
103
174
|
| **Auth** | `login` / `status` | 认证登录、状态查询 |
|
|
104
175
|
| **Lookup** | `research-area list` / `broker-org list` / `meeting-org list` / `industry list` / `industry-code list` / `region list` / `announcement-category list` / `theme-id list` | 枚举速查(内置,无需额外文档) |
|
|
105
|
-
| **Insight** | `opinion list` |
|
|
106
|
-
| | `summary list` / `download` |
|
|
176
|
+
| **Insight** | `opinion list` | 内资机构观点 |
|
|
177
|
+
| | `summary list` / `download` | 纪要(含下载,支持 `--file-type` 选原始/HTML) |
|
|
107
178
|
| | `roadshow list` | 路演 |
|
|
108
179
|
| | `site-visit list` | 调研 |
|
|
109
180
|
| | `strategy list` | 策略 |
|
|
110
181
|
| | `forum list` | 论坛 |
|
|
111
182
|
| | `research list` / `download` | 研报(含 Markdown 下载) |
|
|
112
183
|
| | `foreign-report list` / `download` | 外资研报(含中文翻译下载) |
|
|
113
|
-
| | `announcement list` / `download` |
|
|
184
|
+
| | `announcement list` / `download` | A股公告(含 Markdown 下载) |
|
|
185
|
+
| | `announcement-hk list` / `download` | 港股公告(含下载) |
|
|
186
|
+
| | `foreign-opinion list` | 外资机构观点 |
|
|
187
|
+
| | `independent-opinion list` / `download` | 外资独立分析师观点(含原文/翻译HTML下载) |
|
|
188
|
+
| **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
|
|
114
189
|
| **Quote** | `day-kline` / `day-kline-hk` | A股/港股日K线 |
|
|
115
190
|
| | `index-day-kline` | 沪深京指数日K线 |
|
|
116
191
|
| | `minute-kline` | A股分钟K线 |
|
|
@@ -148,6 +223,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
148
223
|
- `gangtise fundamental ...`
|
|
149
224
|
- `gangtise ai ...`
|
|
150
225
|
- `gangtise vault ...`
|
|
226
|
+
- `gangtise reference ...`
|
|
151
227
|
- `gangtise raw call ...`
|
|
152
228
|
|
|
153
229
|
## 推荐工作流
|
|
@@ -173,6 +249,18 @@ gangtise quote day-kline --security 600519.SH --start-date 2025-03-01 --end-date
|
|
|
173
249
|
gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
174
250
|
```
|
|
175
251
|
|
|
252
|
+
## 性能特性
|
|
253
|
+
|
|
254
|
+
- **并发翻页**:自动翻页接口的首页拿到 `total` 后,剩余页用 `Promise.all` 并发拉取(默认并发数 5,可通过 `GANGTISE_PAGE_CONCURRENCY` 调整)。20 页查询从串行 ~10s 降到 ~2s。
|
|
255
|
+
- **HTTP keep-alive**:所有请求复用同一个 `undici.Agent`(连接池 16),避免重复 TLS 握手。
|
|
256
|
+
- **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
|
|
257
|
+
- **流式输出**:`jsonl`/`csv` 格式且 `--output` 指定时,超过 1000 行自动切换为逐行写盘,避免一次性构建百 MB 字符串。
|
|
258
|
+
- **自动重试**:5xx / `ECONNRESET` / `ETIMEDOUT` / `999999` 系统错误自动指数退避重试 2 次。
|
|
259
|
+
- **Token 自愈**:调用返回 8000014/8000015 时自动强制刷新 Token 并重试一次。
|
|
260
|
+
- **K线自动分片**:`quote day-kline --security all` 等全市场查询自动按日期切分(A股 2 天/片、HK 3 天/片、指数 30 天/片),并发执行后合并结果。
|
|
261
|
+
- **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
|
|
262
|
+
- **`--verbose`**:打印每个请求的方法、路径、状态码、耗时和响应大小到 stderr,方便定位慢查询。
|
|
263
|
+
|
|
176
264
|
## 自动翻页
|
|
177
265
|
|
|
178
266
|
以下列表接口会自动翻页:
|
|
@@ -185,6 +273,9 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
185
273
|
- `insight research list`
|
|
186
274
|
- `insight foreign-report list`
|
|
187
275
|
- `insight announcement list`
|
|
276
|
+
- `insight announcement-hk list`
|
|
277
|
+
- `insight foreign-opinion list`
|
|
278
|
+
- `insight independent-opinion list`
|
|
188
279
|
- `ai security-clue`
|
|
189
280
|
- `vault drive-list`
|
|
190
281
|
- `vault record-list`
|
|
@@ -249,6 +340,31 @@ gangtise insight announcement download --announcement-id 123456 --file-type 2
|
|
|
249
340
|
gangtise insight research download --report-id 12345 --output ./report.pdf
|
|
250
341
|
|
|
251
342
|
gangtise insight roadshow list --institution C100000017
|
|
343
|
+
|
|
344
|
+
# 港股公告
|
|
345
|
+
gangtise insight announcement-hk list --security 01913.HK --rank-type 2 --size 20 --format json
|
|
346
|
+
gangtise insight announcement-hk download --announcement-id ANN2026040200012345
|
|
347
|
+
|
|
348
|
+
# 外资机构观点
|
|
349
|
+
gangtise insight foreign-opinion list --keyword "自动驾驶" --region us --rank-type 2 --format json
|
|
350
|
+
gangtise insight foreign-opinion list --security APP.O --rating buy --format json
|
|
351
|
+
|
|
352
|
+
# 外资独立观点
|
|
353
|
+
gangtise insight independent-opinion list --keyword "肿瘤" --industry 104370000 --format json
|
|
354
|
+
gangtise insight independent-opinion download --independent-opinion-id 207051900018372 --file-type 2
|
|
355
|
+
|
|
356
|
+
# 纪要下载(会议平台来源可选 HTML 格式)
|
|
357
|
+
gangtise insight summary download --summary-id 4906813 --file-type 2
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Reference
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
# GTS Code 搜索:按公司名/代码/拼音查证券代码
|
|
364
|
+
gangtise reference securities-search --keyword "贵州茅台" --category stock
|
|
365
|
+
gangtise reference securities-search --keyword "600519" --category stock
|
|
366
|
+
gangtise reference securities-search --keyword gzmt --top 5
|
|
367
|
+
gangtise reference securities-search --keyword "银行" --category stock --category index
|
|
252
368
|
```
|
|
253
369
|
|
|
254
370
|
### Quote
|
package/dist/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { checkAsyncContent, pollAsyncContent, POLL_MAX_ATTEMPTS } from "./core/a
|
|
|
4
4
|
import { readTokenCache } from "./core/auth.js";
|
|
5
5
|
import { collectKeyValue, collectList, collectNumberList, maybeArray, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize, parseTimestamp13 } from "./core/args.js";
|
|
6
6
|
import { buildQuoteKlineBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
|
|
7
|
+
import { callKlineWithSharding } from "./core/quoteSharding.js";
|
|
7
8
|
import { loadConfig } from "./core/config.js";
|
|
8
9
|
import { resolveTitle, saveDownloadResult } from "./core/download.js";
|
|
9
10
|
import { ENDPOINTS } from "./core/endpoints.js";
|
|
@@ -16,6 +17,21 @@ async function createClient() {
|
|
|
16
17
|
const { GangtiseClient } = await import("./core/client.js");
|
|
17
18
|
return new GangtiseClient(loadConfig());
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Run a download. If `output` is set we already know the destination, so the
|
|
22
|
+
* client streams the body straight to disk (no in-memory Uint8Array copy);
|
|
23
|
+
* otherwise we buffer and let the caller resolve a friendly title.
|
|
24
|
+
*/
|
|
25
|
+
async function runDownload(client, endpointKey, query, options) {
|
|
26
|
+
if (options.output) {
|
|
27
|
+
const result = await client.call(endpointKey, undefined, query, { streamTo: options.output });
|
|
28
|
+
await saveDownloadResult(result, options.fallbackName, options.output);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const result = await client.call(endpointKey, undefined, query);
|
|
32
|
+
const resolved = options.resolveOutputPath ? await options.resolveOutputPath(result) : undefined;
|
|
33
|
+
await saveDownloadResult(result, options.fallbackName, resolved);
|
|
34
|
+
}
|
|
19
35
|
function addTimeFilters(command) {
|
|
20
36
|
return command
|
|
21
37
|
.option("--from <number>", "Starting offset", "0")
|
|
@@ -24,9 +40,18 @@ function addTimeFilters(command) {
|
|
|
24
40
|
.option("--end-time <datetime>", "End time")
|
|
25
41
|
.option("--keyword <keyword>", "Keyword");
|
|
26
42
|
}
|
|
43
|
+
import { setVerbose } from "./core/transport.js";
|
|
27
44
|
import { CLI_VERSION } from "./version.js";
|
|
28
45
|
const program = new Command();
|
|
29
|
-
program
|
|
46
|
+
program
|
|
47
|
+
.name("gangtise")
|
|
48
|
+
.description("Gangtise OpenAPI CLI")
|
|
49
|
+
.version(CLI_VERSION)
|
|
50
|
+
.option("--verbose", "Print per-request timings to stderr (also: GANGTISE_VERBOSE=1)")
|
|
51
|
+
.hook("preAction", (thisCommand) => {
|
|
52
|
+
if (thisCommand.opts().verbose)
|
|
53
|
+
setVerbose(true);
|
|
54
|
+
});
|
|
30
55
|
program
|
|
31
56
|
.command("auth")
|
|
32
57
|
.description("Authentication commands")
|
|
@@ -88,6 +113,9 @@ const forum = new Command("forum");
|
|
|
88
113
|
const research = new Command("research");
|
|
89
114
|
const foreignReport = new Command("foreign-report");
|
|
90
115
|
const announcement = new Command("announcement");
|
|
116
|
+
const announcementHk = new Command("announcement-hk");
|
|
117
|
+
const foreignOpinion = new Command("foreign-opinion");
|
|
118
|
+
const independentOpinion = new Command("independent-opinion");
|
|
91
119
|
addTimeFilters(opinion.command("list").option("--rank-type <number>", "Rank type", "1").option("--research-area <id>", "Research area ID", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
92
120
|
const client = await createClient();
|
|
93
121
|
await printData(await client.call("insight.opinion.list", {
|
|
@@ -106,11 +134,16 @@ addTimeFilters(summary.command("list").option("--search-type <number>", "Search
|
|
|
106
134
|
categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
|
|
107
135
|
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.summary.list", idField: "summaryId" });
|
|
108
136
|
});
|
|
109
|
-
summary.command("download").requiredOption("--summary-id <id>").option("--output <path>").action(async (options) => {
|
|
137
|
+
summary.command("download").requiredOption("--summary-id <id>").option("--file-type <number>", "File type: 1=original(default) 2=HTML; only affects meeting platform summaries").option("--output <path>").action(async (options) => {
|
|
110
138
|
const client = await createClient();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
const qp = { summaryId: options.summaryId };
|
|
140
|
+
if (options.fileType)
|
|
141
|
+
qp.fileType = parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 });
|
|
142
|
+
await runDownload(client, "insight.summary.download", qp, {
|
|
143
|
+
output: options.output,
|
|
144
|
+
fallbackName: `summary-${options.summaryId}`,
|
|
145
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "insight.summary.list", "summaryId", options.summaryId),
|
|
146
|
+
});
|
|
114
147
|
});
|
|
115
148
|
const addScheduleList = (command, endpointKey) => addTimeFilters(command.command("list").option("--research-area <id>", "Research area", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--broker-type <name>", "Broker type", collectList, []).option("--object <type>", "Object type: company/industry", collectList, []).option("--permission <number>", "Permission", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
116
149
|
const client = await createClient();
|
|
@@ -138,9 +171,11 @@ addTimeFilters(research.command("list").option("--search-type <number>", "Search
|
|
|
138
171
|
});
|
|
139
172
|
research.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action(async (options) => {
|
|
140
173
|
const client = await createClient();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
174
|
+
await runDownload(client, "insight.research.download", { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
|
|
175
|
+
output: options.output,
|
|
176
|
+
fallbackName: `research-${options.reportId}`,
|
|
177
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "insight.research.list", "reportId", options.reportId),
|
|
178
|
+
});
|
|
144
179
|
});
|
|
145
180
|
addTimeFilters(foreignReport.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("--security <code>", "Security code", collectList, []).option("--region <id>", "Region ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", 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("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
146
181
|
const client = await createClient();
|
|
@@ -155,9 +190,11 @@ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "S
|
|
|
155
190
|
});
|
|
156
191
|
foreignReport.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", "1").option("--output <path>").action(async (options) => {
|
|
157
192
|
const client = await createClient();
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
193
|
+
await runDownload(client, "insight.foreign-report.download", { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
|
|
194
|
+
output: options.output,
|
|
195
|
+
fallbackName: `foreign-report-${options.reportId}`,
|
|
196
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "insight.foreign-report.list", "reportId", options.reportId),
|
|
197
|
+
});
|
|
161
198
|
});
|
|
162
199
|
addTimeFilters(announcement.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("--security <code>", "Security code", collectList, []).option("--announcement-type <type>", "Announcement type", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
163
200
|
const client = await createClient();
|
|
@@ -170,9 +207,60 @@ addTimeFilters(announcement.command("list").option("--search-type <number>", "Se
|
|
|
170
207
|
});
|
|
171
208
|
announcement.command("download").requiredOption("--announcement-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action(async (options) => {
|
|
172
209
|
const client = await createClient();
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
210
|
+
await runDownload(client, "insight.announcement.download", { announcementId: options.announcementId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
|
|
211
|
+
output: options.output,
|
|
212
|
+
fallbackName: `announcement-${options.announcementId}`,
|
|
213
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "insight.announcement.list", "announcementId", options.announcementId),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
addTimeFilters(announcementHk.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("--security <code>", "Security code (e.g. 01913.HK)", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
217
|
+
const client = await createClient();
|
|
218
|
+
await printData(await client.call("insight.announcement-hk.list", {
|
|
219
|
+
from: parseFrom(options.from), size: parseSize(options.size),
|
|
220
|
+
startTime: options.startTime, endTime: options.endTime,
|
|
221
|
+
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
|
|
222
|
+
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
223
|
+
keyword: options.keyword,
|
|
224
|
+
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
225
|
+
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.announcement-hk.list", idField: "announcementId" });
|
|
226
|
+
});
|
|
227
|
+
announcementHk.command("download").requiredOption("--announcement-id <id>").option("--output <path>").action(async (options) => {
|
|
228
|
+
const client = await createClient();
|
|
229
|
+
await runDownload(client, "insight.announcement-hk.download", { announcementId: options.announcementId }, {
|
|
230
|
+
output: options.output,
|
|
231
|
+
fallbackName: `announcement-hk-${options.announcementId}`,
|
|
232
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "insight.announcement-hk.list", "announcementId", options.announcementId),
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
addTimeFilters(foreignOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
236
|
+
const client = await createClient();
|
|
237
|
+
await printData(await client.call("insight.foreign-opinion.list", {
|
|
238
|
+
from: parseFrom(options.from), size: parseSize(options.size),
|
|
239
|
+
startTime: options.startTime, endTime: options.endTime,
|
|
240
|
+
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
241
|
+
keyword: options.keyword,
|
|
242
|
+
regionList: maybeArray(options.region), industryList: maybeArray(options.industry),
|
|
243
|
+
securityList: maybeArray(options.security), brokerList: maybeArray(options.broker),
|
|
244
|
+
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
245
|
+
}), parseOutputFormat(options.format), options.output);
|
|
246
|
+
});
|
|
247
|
+
addTimeFilters(independentOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
|
|
248
|
+
const client = await createClient();
|
|
249
|
+
await printData(await client.call("insight.independent-opinion.list", {
|
|
250
|
+
from: parseFrom(options.from), size: parseSize(options.size),
|
|
251
|
+
startTime: options.startTime, endTime: options.endTime,
|
|
252
|
+
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
253
|
+
keyword: options.keyword,
|
|
254
|
+
industryList: maybeArray(options.industry), securityList: maybeArray(options.security),
|
|
255
|
+
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
256
|
+
}), parseOutputFormat(options.format), options.output);
|
|
257
|
+
});
|
|
258
|
+
independentOpinion.command("download").requiredOption("--independent-opinion-id <id>").requiredOption("--file-type <number>", "File type: 1=original HTML 2=CN-translated HTML").option("--output <path>").action(async (options) => {
|
|
259
|
+
const client = await createClient();
|
|
260
|
+
await runDownload(client, "insight.independent-opinion.download", { independentOpinionId: options.independentOpinionId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
|
|
261
|
+
output: options.output,
|
|
262
|
+
fallbackName: `independent-opinion-${options.independentOpinionId}`,
|
|
263
|
+
});
|
|
176
264
|
});
|
|
177
265
|
insight.addCommand(opinion);
|
|
178
266
|
insight.addCommand(summary);
|
|
@@ -183,19 +271,22 @@ insight.addCommand(forum);
|
|
|
183
271
|
insight.addCommand(research);
|
|
184
272
|
insight.addCommand(foreignReport);
|
|
185
273
|
insight.addCommand(announcement);
|
|
274
|
+
insight.addCommand(announcementHk);
|
|
275
|
+
insight.addCommand(foreignOpinion);
|
|
276
|
+
insight.addCommand(independentOpinion);
|
|
186
277
|
program.addCommand(insight);
|
|
187
278
|
const quote = new Command("quote").description("Quote APIs");
|
|
188
279
|
quote.command("day-kline").option("--security <code>", "Security code (A-share: .SH/.SZ/.BJ, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
189
280
|
const client = await createClient();
|
|
190
|
-
await printData(await client
|
|
281
|
+
await printData(await callKlineWithSharding(client, "quote.day-kline", buildQuoteKlineBody(options), { shardDays: 2 }), parseOutputFormat(options.format), options.output);
|
|
191
282
|
});
|
|
192
283
|
quote.command("day-kline-hk").option("--security <code>", "Security code (HK stock: .HK, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
193
284
|
const client = await createClient();
|
|
194
|
-
await printData(await client
|
|
285
|
+
await printData(await callKlineWithSharding(client, "quote.day-kline-hk", buildQuoteKlineBody(options), { shardDays: 3 }), parseOutputFormat(options.format), options.output);
|
|
195
286
|
});
|
|
196
287
|
quote.command("index-day-kline").option("--security <code>", "Index code (.SH/.SZ/.BJ, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
197
288
|
const client = await createClient();
|
|
198
|
-
await printData(await client
|
|
289
|
+
await printData(await callKlineWithSharding(client, "quote.index-day-kline", buildQuoteKlineBody(options), { shardDays: 30 }), parseOutputFormat(options.format), options.output);
|
|
199
290
|
});
|
|
200
291
|
quote.command("minute-kline").option("--security <code>", "Security code (A-share only: .SH/.SZ/.BJ)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)").option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)").option("--limit <number>", "Max rows per request (default: 5000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
201
292
|
const client = await createClient();
|
|
@@ -212,6 +303,9 @@ addFinancialReport("income-statement-quarterly", "fundamental.income-statement-q
|
|
|
212
303
|
addFinancialReport("balance-sheet", "fundamental.balance-sheet");
|
|
213
304
|
addFinancialReport("cash-flow", "fundamental.cash-flow");
|
|
214
305
|
addFinancialReport("cash-flow-quarterly", "fundamental.cash-flow-quarterly", "Period: q1/q2/q3/q4/latest");
|
|
306
|
+
addFinancialReport("income-statement-hk", "fundamental.income-statement-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
|
|
307
|
+
addFinancialReport("balance-sheet-hk", "fundamental.balance-sheet-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
|
|
308
|
+
addFinancialReport("cash-flow-hk", "fundamental.cash-flow-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
|
|
215
309
|
fundamental.command("main-business").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").addOption(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product")).option("--period <type>", "Period: interim/annual", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
216
310
|
const client = await createClient();
|
|
217
311
|
await printData(await client.call("fundamental.main-business", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, breakdown: options.breakdown, periodList: maybeArray(options.period), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
@@ -254,7 +348,10 @@ ai.command("knowledge-batch").requiredOption("--query <text>", "Query", collectL
|
|
|
254
348
|
});
|
|
255
349
|
ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action(async (options) => {
|
|
256
350
|
const client = await createClient();
|
|
257
|
-
await
|
|
351
|
+
await runDownload(client, "ai.knowledge-resource.download", { resourceType: parseNumberOption(options.resourceType, "--resource-type", { integer: true, min: 0 }), sourceId: options.sourceId }, {
|
|
352
|
+
output: options.output,
|
|
353
|
+
fallbackName: `resource-${options.sourceId}`,
|
|
354
|
+
});
|
|
258
355
|
});
|
|
259
356
|
ai.command("security-clue").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").requiredOption("--start-time <datetime>").requiredOption("--end-time <datetime>").addOption(new Option("--query-mode <mode>").choices(["bySecurity", "byIndustry"]).makeOptionMandatory()).option("--gts-code <code>", "GTS code", collectList, []).option("--source <name>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
260
357
|
const client = await createClient();
|
|
@@ -318,7 +415,7 @@ ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option
|
|
|
318
415
|
withCloseReading: options.withCloseReading === false ? undefined : true,
|
|
319
416
|
}), parseOutputFormat(options.format), options.output);
|
|
320
417
|
});
|
|
321
|
-
ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
|
|
418
|
+
ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension: businessOperation/financialPerformance/developmentAndRisk/all").choices(["businessOperation", "financialPerformance", "developmentAndRisk", "all"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
|
|
322
419
|
const client = await createClient();
|
|
323
420
|
await printData(await client.call("ai.management-discuss-announcement", {
|
|
324
421
|
reportDate: options.reportDate,
|
|
@@ -358,6 +455,16 @@ ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId fr
|
|
|
358
455
|
const client = await createClient();
|
|
359
456
|
await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output);
|
|
360
457
|
});
|
|
458
|
+
const reference = new Command("reference").description("Reference data APIs");
|
|
459
|
+
reference.command("securities-search").requiredOption("--keyword <text>", "Search keyword (name/code/pinyin/English)").option("--category <type>", "Category: stock/dr/index/fund", collectList, []).option("--top <number>", "Max results (default: 10, max: 10)", "10").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
460
|
+
const client = await createClient();
|
|
461
|
+
await printData(await client.call("reference.securities-search", {
|
|
462
|
+
keyword: options.keyword,
|
|
463
|
+
category: options.category.length ? options.category : undefined,
|
|
464
|
+
top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
|
|
465
|
+
}), parseOutputFormat(options.format), options.output);
|
|
466
|
+
});
|
|
467
|
+
program.addCommand(reference);
|
|
361
468
|
const vault = new Command("vault").description("Vault APIs");
|
|
362
469
|
vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
363
470
|
const client = await createClient();
|
|
@@ -365,9 +472,11 @@ vault.command("drive-list").option("--from <number>", "Starting offset", "0").op
|
|
|
365
472
|
});
|
|
366
473
|
vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action(async (options) => {
|
|
367
474
|
const client = await createClient();
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
475
|
+
await runDownload(client, "vault.drive.download", { fileId: options.fileId }, {
|
|
476
|
+
output: options.output,
|
|
477
|
+
fallbackName: `file-${options.fileId}`,
|
|
478
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "vault.drive.list", "fileId", options.fileId),
|
|
479
|
+
});
|
|
371
480
|
});
|
|
372
481
|
vault.command("record-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", collectList, []).option("--space-type <number>", "Space type: 1=my records / 2=tenant records", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
373
482
|
const client = await createClient();
|
|
@@ -375,9 +484,11 @@ vault.command("record-list").option("--from <number>", "Starting offset", "0").o
|
|
|
375
484
|
});
|
|
376
485
|
vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action(async (options) => {
|
|
377
486
|
const client = await createClient();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
487
|
+
await runDownload(client, "vault.record.download", { recordId: options.recordId, contentType: options.contentType }, {
|
|
488
|
+
output: options.output,
|
|
489
|
+
fallbackName: `record-${options.recordId}`,
|
|
490
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "vault.record.list", "recordId", options.recordId),
|
|
491
|
+
});
|
|
381
492
|
});
|
|
382
493
|
vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--research-area <id>", "Research area ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
383
494
|
const client = await createClient();
|
|
@@ -385,11 +496,13 @@ vault.command("my-conference-list").option("--from <number>", "Starting offset",
|
|
|
385
496
|
});
|
|
386
497
|
vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action(async (options) => {
|
|
387
498
|
const client = await createClient();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
499
|
+
await runDownload(client, "vault.my-conference.download", { conferenceId: options.conferenceId, contentType: options.contentType }, {
|
|
500
|
+
output: options.output,
|
|
501
|
+
fallbackName: `conference-${options.conferenceId}`,
|
|
502
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "vault.my-conference.list", "conferenceId", options.conferenceId),
|
|
503
|
+
});
|
|
391
504
|
});
|
|
392
|
-
vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
505
|
+
vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
393
506
|
const client = await createClient();
|
|
394
507
|
await printData(await client.call("vault.wechat-message.list", buildWechatMessageListBody(options)), parseOutputFormat(options.format), options.output);
|
|
395
508
|
});
|
|
@@ -397,8 +510,42 @@ vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset
|
|
|
397
510
|
const client = await createClient();
|
|
398
511
|
await printData(await client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options)), parseOutputFormat(options.format), options.output);
|
|
399
512
|
});
|
|
513
|
+
vault.command("stock-pool-list").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
514
|
+
const client = await createClient();
|
|
515
|
+
await printData(await client.call("vault.stock-pool.list", {}), parseOutputFormat(options.format), options.output);
|
|
516
|
+
});
|
|
517
|
+
vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; use 'all' for all pools", collectList, ["all"]).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
518
|
+
const client = await createClient();
|
|
519
|
+
await printData(await client.call("vault.stock-pool.stocks", { poolIdList: options.poolId }), parseOutputFormat(options.format), options.output);
|
|
520
|
+
});
|
|
400
521
|
program.addCommand(vault);
|
|
401
522
|
program.addCommand(ai);
|
|
523
|
+
const alternative = new Command("alternative").description("Alternative data APIs");
|
|
524
|
+
alternative.command("edb-search").requiredOption("--keyword <text>", "Search keyword (e.g. '空调')").option("--limit <number>", "Max results (default: 100, max: 200)", "100").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
525
|
+
const client = await createClient();
|
|
526
|
+
await printData(await client.call("alternative.edb-search", {
|
|
527
|
+
keyword: options.keyword,
|
|
528
|
+
limit: parseNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
|
|
529
|
+
}), parseOutputFormat(options.format), options.output);
|
|
530
|
+
});
|
|
531
|
+
alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (repeat, max 10)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)").requiredOption("--end-date <date>", "End date (yyyy-MM-dd)").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
532
|
+
const client = await createClient();
|
|
533
|
+
const raw = await client.call("alternative.edb-data", {
|
|
534
|
+
indicatorIdList: options.indicatorId,
|
|
535
|
+
startDate: options.startDate,
|
|
536
|
+
endDate: options.endDate,
|
|
537
|
+
});
|
|
538
|
+
let data = raw;
|
|
539
|
+
if (raw && Array.isArray(raw.fieldList) && Array.isArray(raw.dataList)) {
|
|
540
|
+
const list = raw.dataList.map((row) => raw.fieldList.reduce((acc, field, i) => {
|
|
541
|
+
acc[field] = row[i];
|
|
542
|
+
return acc;
|
|
543
|
+
}, {}));
|
|
544
|
+
data = { list, total: list.length };
|
|
545
|
+
}
|
|
546
|
+
await printData(data, parseOutputFormat(options.format), options.output);
|
|
547
|
+
});
|
|
548
|
+
program.addCommand(alternative);
|
|
402
549
|
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) => {
|
|
403
550
|
const endpoint = ENDPOINTS[endpointKey];
|
|
404
551
|
if (!endpoint) {
|
|
@@ -414,11 +561,14 @@ program.command("raw").description("Raw API calls").addCommand(new Command("call
|
|
|
414
561
|
throw new ConfigError(`Invalid JSON in --body: ${options.body}`);
|
|
415
562
|
}
|
|
416
563
|
}
|
|
417
|
-
const data = await client.call(endpointKey, body, options.query);
|
|
418
564
|
if (endpoint.kind === "download") {
|
|
419
|
-
await
|
|
565
|
+
await runDownload(client, endpointKey, options.query, {
|
|
566
|
+
output: options.output,
|
|
567
|
+
fallbackName: "download.bin",
|
|
568
|
+
});
|
|
420
569
|
return;
|
|
421
570
|
}
|
|
571
|
+
const data = await client.call(endpointKey, body, options.query);
|
|
422
572
|
await printData(data, parseOutputFormat(options.format), options.output);
|
|
423
573
|
}));
|
|
424
574
|
async function checkForUpdate(timeoutMs = 2000) {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { ApiError } from "./errors.js";
|
|
2
2
|
import { printData } from "./printer.js";
|
|
3
|
-
export const POLL_MAX_ATTEMPTS =
|
|
4
|
-
|
|
3
|
+
export const POLL_MAX_ATTEMPTS = 14;
|
|
4
|
+
const POLL_INITIAL_DELAY_MS = 5_000;
|
|
5
|
+
const POLL_MAX_DELAY_MS = 30_000;
|
|
6
|
+
/** Total wait time stays close to the previous 12*15s=180s budget. */
|
|
7
|
+
export const POLL_DELAY_MS = POLL_INITIAL_DELAY_MS;
|
|
8
|
+
function nextDelayMs(attempt) {
|
|
9
|
+
// 5s, 8s, 13s, 20s, 30s, 30s, ...
|
|
10
|
+
const grown = POLL_INITIAL_DELAY_MS * 1.6 ** (attempt - 1);
|
|
11
|
+
return Math.min(POLL_MAX_DELAY_MS, Math.round(grown));
|
|
12
|
+
}
|
|
5
13
|
function isAsyncPending(error) {
|
|
6
14
|
return error instanceof ApiError && error.code === "410110";
|
|
7
15
|
}
|
|
@@ -23,8 +31,9 @@ export async function pollAsyncContent(client, getContentEndpoint, dataId, forma
|
|
|
23
31
|
throw error;
|
|
24
32
|
}
|
|
25
33
|
if (attempt < POLL_MAX_ATTEMPTS) {
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
const delay = nextDelayMs(attempt);
|
|
35
|
+
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: content not ready, retrying in ${Math.round(delay / 1000)}s...\n`);
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
28
37
|
}
|
|
29
38
|
}
|
|
30
39
|
return false;
|