gangtise-openapi-cli 0.11.0 → 0.12.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 +98 -5
- package/dist/src/cli.js +181 -68
- package/dist/src/core/asyncContent.js +13 -4
- package/dist/src/core/auth.js +1 -4
- package/dist/src/core/client.js +217 -129
- package/dist/src/core/download.js +4 -0
- package/dist/src/core/endpoints.js +138 -89
- 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,49 @@
|
|
|
2
2
|
|
|
3
3
|
一个可直接调用 Gangtise OpenAPI 获取金融数据的命令行工具,同时提供Agent Skill。
|
|
4
4
|
|
|
5
|
+
## Changelog
|
|
6
|
+
|
|
7
|
+
### v0.12.0 — 2026-05-10
|
|
8
|
+
|
|
9
|
+
**性能 / 架构**
|
|
10
|
+
- 翻页并行化:自动翻页接口拉到首页 `total` 后,剩余页通过 `Promise.all` 并发请求(默认并发 5,`GANGTISE_PAGE_CONCURRENCY` 可调)
|
|
11
|
+
- 共享 `undici.Agent`:所有请求复用连接池(keep-alive 60s,max 16 连接),避免重复 TLS 握手
|
|
12
|
+
- 流式下载:`--output` 指定时二进制响应直接 `pipeline` 到磁盘,不再走内存 `Uint8Array`
|
|
13
|
+
- 流式输出:`--format jsonl/csv --output xxx` 且 ≥1000 行时逐行写盘
|
|
14
|
+
- Token 内存缓存:Token 在进程内不再每次读盘
|
|
15
|
+
- 自动重试:5xx / `ECONNRESET` / `ETIMEDOUT` / `999999` 自动指数退避重试 2 次
|
|
16
|
+
- Token 自愈:8000014/8000015 自动重新登录并重试一次
|
|
17
|
+
- 异步轮询退避:`earnings-review` / `viewpoint-debate` 轮询从固定 15s 改为 5→8→13→20→30s 指数退避
|
|
18
|
+
- K线自动分片:`quote day-kline --security all` 等全市场查询自动按日期切分并发执行
|
|
19
|
+
- 标题缓存:原"读全文→改→写全文"改为内存快照 + 原子写入(temp+rename)
|
|
20
|
+
|
|
21
|
+
**调试 / 可观测性**
|
|
22
|
+
- 新增 `--verbose` / `GANGTISE_VERBOSE=1`:打印每个请求的耗时、状态码、响应字节数到 stderr
|
|
23
|
+
|
|
24
|
+
### v0.11.1 — 2026-05-10
|
|
25
|
+
|
|
26
|
+
**新增接口**
|
|
27
|
+
- `insight announcement-hk list/download` — 查询/下载港股公告
|
|
28
|
+
- `insight foreign-opinion list` — 查询外资机构观点(外资券商)
|
|
29
|
+
- `insight independent-opinion list/download` — 查询/下载外资独立分析师观点
|
|
30
|
+
- `reference securities-search` — GTS Code 搜索(按名称/代码/拼音多维度匹配证券)
|
|
31
|
+
|
|
32
|
+
**接口变更**
|
|
33
|
+
- `insight summary download` 新增可选 `--file-type`(`1`=原始内容 / `2`=HTML),仅影响来源为会议平台的纪要
|
|
34
|
+
- `insight announcement list/download` 名称调整为"查询A股公告列表/下载A股公告文件"(路径不变)
|
|
35
|
+
- `insight opinion list` 名称调整为"查询内资机构观点列表"(路径不变)
|
|
36
|
+
|
|
37
|
+
### v0.11.0 — 2026-04-17
|
|
38
|
+
|
|
39
|
+
- 新增 `ai viewpoint-debate` / `viewpoint-debate-check` — 观点PK(异步)
|
|
40
|
+
- 新增 `ai management-discuss-announcement` / `management-discuss-earnings-call` — 管理层讨论
|
|
41
|
+
|
|
42
|
+
### v0.10.9 — 2026-04-10
|
|
43
|
+
|
|
44
|
+
- 修复信封检测、版本更新检查、端点去重
|
|
45
|
+
- 新增 `quote index-day-kline` 指数日K线
|
|
46
|
+
- 新增 `vault wechat-message-list` / `wechat-chatroom-list` 群消息
|
|
47
|
+
|
|
5
48
|
## 首次安装
|
|
6
49
|
|
|
7
50
|
```bash
|
|
@@ -46,9 +89,14 @@ export GANGTISE_ACCESS_KEY="your-ak"
|
|
|
46
89
|
export GANGTISE_SECRET_KEY="your-sk"
|
|
47
90
|
export GANGTISE_BASE_URL="https://open.gangtise.com"
|
|
48
91
|
export GANGTISE_TOKEN="Bearer xxx"
|
|
92
|
+
|
|
93
|
+
# 性能/调试可选项
|
|
94
|
+
export GANGTISE_PAGE_CONCURRENCY=5 # 翻页并发数(默认 5)
|
|
95
|
+
export GANGTISE_VERBOSE=1 # 打印每个请求的耗时与字节数
|
|
96
|
+
export GANGTISE_TIMEOUT_MS=30000 # 请求超时(默认 30s)
|
|
49
97
|
```
|
|
50
98
|
|
|
51
|
-
如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。
|
|
99
|
+
如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。Token 失效(8000014/8000015)时会自动重新登录并重试一次。
|
|
52
100
|
|
|
53
101
|
|
|
54
102
|
## AI Agent Skill
|
|
@@ -102,15 +150,19 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
102
150
|
|------|--------|------|
|
|
103
151
|
| **Auth** | `login` / `status` | 认证登录、状态查询 |
|
|
104
152
|
| **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` |
|
|
153
|
+
| **Insight** | `opinion list` | 内资机构观点 |
|
|
154
|
+
| | `summary list` / `download` | 纪要(含下载,支持 `--file-type` 选原始/HTML) |
|
|
107
155
|
| | `roadshow list` | 路演 |
|
|
108
156
|
| | `site-visit list` | 调研 |
|
|
109
157
|
| | `strategy list` | 策略 |
|
|
110
158
|
| | `forum list` | 论坛 |
|
|
111
159
|
| | `research list` / `download` | 研报(含 Markdown 下载) |
|
|
112
160
|
| | `foreign-report list` / `download` | 外资研报(含中文翻译下载) |
|
|
113
|
-
| | `announcement list` / `download` |
|
|
161
|
+
| | `announcement list` / `download` | A股公告(含 Markdown 下载) |
|
|
162
|
+
| | `announcement-hk list` / `download` | 港股公告(含下载) |
|
|
163
|
+
| | `foreign-opinion list` | 外资机构观点 |
|
|
164
|
+
| | `independent-opinion list` / `download` | 外资独立分析师观点(含原文/翻译HTML下载) |
|
|
165
|
+
| **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
|
|
114
166
|
| **Quote** | `day-kline` / `day-kline-hk` | A股/港股日K线 |
|
|
115
167
|
| | `index-day-kline` | 沪深京指数日K线 |
|
|
116
168
|
| | `minute-kline` | A股分钟K线 |
|
|
@@ -148,6 +200,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
148
200
|
- `gangtise fundamental ...`
|
|
149
201
|
- `gangtise ai ...`
|
|
150
202
|
- `gangtise vault ...`
|
|
203
|
+
- `gangtise reference ...`
|
|
151
204
|
- `gangtise raw call ...`
|
|
152
205
|
|
|
153
206
|
## 推荐工作流
|
|
@@ -173,6 +226,18 @@ gangtise quote day-kline --security 600519.SH --start-date 2025-03-01 --end-date
|
|
|
173
226
|
gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
174
227
|
```
|
|
175
228
|
|
|
229
|
+
## 性能特性
|
|
230
|
+
|
|
231
|
+
- **并发翻页**:自动翻页接口的首页拿到 `total` 后,剩余页用 `Promise.all` 并发拉取(默认并发数 5,可通过 `GANGTISE_PAGE_CONCURRENCY` 调整)。20 页查询从串行 ~10s 降到 ~2s。
|
|
232
|
+
- **HTTP keep-alive**:所有请求复用同一个 `undici.Agent`(连接池 16),避免重复 TLS 握手。
|
|
233
|
+
- **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
|
|
234
|
+
- **流式输出**:`jsonl`/`csv` 格式且 `--output` 指定时,超过 1000 行自动切换为逐行写盘,避免一次性构建百 MB 字符串。
|
|
235
|
+
- **自动重试**:5xx / `ECONNRESET` / `ETIMEDOUT` / `999999` 系统错误自动指数退避重试 2 次。
|
|
236
|
+
- **Token 自愈**:调用返回 8000014/8000015 时自动强制刷新 Token 并重试一次。
|
|
237
|
+
- **K线自动分片**:`quote day-kline --security all` 等全市场查询自动按日期切分(A股 2 天/片、HK 3 天/片、指数 30 天/片),并发执行后合并结果。
|
|
238
|
+
- **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
|
|
239
|
+
- **`--verbose`**:打印每个请求的方法、路径、状态码、耗时和响应大小到 stderr,方便定位慢查询。
|
|
240
|
+
|
|
176
241
|
## 自动翻页
|
|
177
242
|
|
|
178
243
|
以下列表接口会自动翻页:
|
|
@@ -185,6 +250,9 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
185
250
|
- `insight research list`
|
|
186
251
|
- `insight foreign-report list`
|
|
187
252
|
- `insight announcement list`
|
|
253
|
+
- `insight announcement-hk list`
|
|
254
|
+
- `insight foreign-opinion list`
|
|
255
|
+
- `insight independent-opinion list`
|
|
188
256
|
- `ai security-clue`
|
|
189
257
|
- `vault drive-list`
|
|
190
258
|
- `vault record-list`
|
|
@@ -249,13 +317,38 @@ gangtise insight announcement download --announcement-id 123456 --file-type 2
|
|
|
249
317
|
gangtise insight research download --report-id 12345 --output ./report.pdf
|
|
250
318
|
|
|
251
319
|
gangtise insight roadshow list --institution C100000017
|
|
320
|
+
|
|
321
|
+
# 港股公告
|
|
322
|
+
gangtise insight announcement-hk list --security 01913.HK --rank-type 2 --size 20 --format json
|
|
323
|
+
gangtise insight announcement-hk download --announcement-id ANN2026040200012345
|
|
324
|
+
|
|
325
|
+
# 外资机构观点
|
|
326
|
+
gangtise insight foreign-opinion list --keyword "自动驾驶" --region us --rank-type 2 --format json
|
|
327
|
+
gangtise insight foreign-opinion list --security APP.O --rating buy --format json
|
|
328
|
+
|
|
329
|
+
# 外资独立观点
|
|
330
|
+
gangtise insight independent-opinion list --keyword "肿瘤" --industry 104370000 --format json
|
|
331
|
+
gangtise insight independent-opinion download --independent-opinion-id 207051900018372 --file-type 2
|
|
332
|
+
|
|
333
|
+
# 纪要下载(会议平台来源可选 HTML 格式)
|
|
334
|
+
gangtise insight summary download --summary-id 4906813 --file-type 2
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Reference
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# GTS Code 搜索:按公司名/代码/拼音查证券代码
|
|
341
|
+
gangtise reference securities-search --keyword "贵州茅台" --category stock
|
|
342
|
+
gangtise reference securities-search --keyword "600519" --category stock
|
|
343
|
+
gangtise reference securities-search --keyword gzmt --top 5
|
|
344
|
+
gangtise reference securities-search --keyword "银行" --category stock --category index
|
|
252
345
|
```
|
|
253
346
|
|
|
254
347
|
### Quote
|
|
255
348
|
|
|
256
349
|
```bash
|
|
257
350
|
gangtise quote day-kline --security 600519.SH --start-date 2026-03-01 --end-date 2026-03-31
|
|
258
|
-
#
|
|
351
|
+
# 查最近/最新 K 线建议显式传 --start-date/--end-date;只传 --limit 会截取查询窗口开头,不等于最近N条
|
|
259
352
|
gangtise quote day-kline --format json
|
|
260
353
|
# 全市场查询(--security all)
|
|
261
354
|
gangtise quote day-kline --security all --start-date 2026-04-01 --end-date 2026-04-01 --limit 100 --format json
|
package/dist/src/cli.js
CHANGED
|
@@ -4,8 +4,10 @@ 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";
|
|
10
|
+
import { ENDPOINTS } from "./core/endpoints.js";
|
|
9
11
|
import { ApiError, ConfigError } from "./core/errors.js";
|
|
10
12
|
import { normalizeRows } from "./core/normalize.js";
|
|
11
13
|
import { parseOutputFormat } from "./core/output.js";
|
|
@@ -15,6 +17,21 @@ async function createClient() {
|
|
|
15
17
|
const { GangtiseClient } = await import("./core/client.js");
|
|
16
18
|
return new GangtiseClient(loadConfig());
|
|
17
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
|
+
}
|
|
18
35
|
function addTimeFilters(command) {
|
|
19
36
|
return command
|
|
20
37
|
.option("--from <number>", "Starting offset", "0")
|
|
@@ -23,9 +40,18 @@ function addTimeFilters(command) {
|
|
|
23
40
|
.option("--end-time <datetime>", "End time")
|
|
24
41
|
.option("--keyword <keyword>", "Keyword");
|
|
25
42
|
}
|
|
43
|
+
import { setVerbose } from "./core/transport.js";
|
|
26
44
|
import { CLI_VERSION } from "./version.js";
|
|
27
45
|
const program = new Command();
|
|
28
|
-
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
|
+
});
|
|
29
55
|
program
|
|
30
56
|
.command("auth")
|
|
31
57
|
.description("Authentication commands")
|
|
@@ -87,6 +113,9 @@ const forum = new Command("forum");
|
|
|
87
113
|
const research = new Command("research");
|
|
88
114
|
const foreignReport = new Command("foreign-report");
|
|
89
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");
|
|
90
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) => {
|
|
91
120
|
const client = await createClient();
|
|
92
121
|
await printData(await client.call("insight.opinion.list", {
|
|
@@ -105,11 +134,16 @@ addTimeFilters(summary.command("list").option("--search-type <number>", "Search
|
|
|
105
134
|
categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
|
|
106
135
|
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.summary.list", idField: "summaryId" });
|
|
107
136
|
});
|
|
108
|
-
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) => {
|
|
109
138
|
const client = await createClient();
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
});
|
|
113
147
|
});
|
|
114
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) => {
|
|
115
149
|
const client = await createClient();
|
|
@@ -137,9 +171,11 @@ addTimeFilters(research.command("list").option("--search-type <number>", "Search
|
|
|
137
171
|
});
|
|
138
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) => {
|
|
139
173
|
const client = await createClient();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
});
|
|
143
179
|
});
|
|
144
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) => {
|
|
145
181
|
const client = await createClient();
|
|
@@ -154,9 +190,11 @@ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "S
|
|
|
154
190
|
});
|
|
155
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) => {
|
|
156
192
|
const client = await createClient();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
});
|
|
160
198
|
});
|
|
161
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) => {
|
|
162
200
|
const client = await createClient();
|
|
@@ -169,9 +207,60 @@ addTimeFilters(announcement.command("list").option("--search-type <number>", "Se
|
|
|
169
207
|
});
|
|
170
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) => {
|
|
171
209
|
const client = await createClient();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
});
|
|
175
264
|
});
|
|
176
265
|
insight.addCommand(opinion);
|
|
177
266
|
insight.addCommand(summary);
|
|
@@ -182,19 +271,22 @@ insight.addCommand(forum);
|
|
|
182
271
|
insight.addCommand(research);
|
|
183
272
|
insight.addCommand(foreignReport);
|
|
184
273
|
insight.addCommand(announcement);
|
|
274
|
+
insight.addCommand(announcementHk);
|
|
275
|
+
insight.addCommand(foreignOpinion);
|
|
276
|
+
insight.addCommand(independentOpinion);
|
|
185
277
|
program.addCommand(insight);
|
|
186
278
|
const quote = new Command("quote").description("Quote APIs");
|
|
187
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) => {
|
|
188
280
|
const client = await createClient();
|
|
189
|
-
await printData(await client
|
|
281
|
+
await printData(await callKlineWithSharding(client, "quote.day-kline", buildQuoteKlineBody(options), { shardDays: 2 }), parseOutputFormat(options.format), options.output);
|
|
190
282
|
});
|
|
191
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) => {
|
|
192
284
|
const client = await createClient();
|
|
193
|
-
await printData(await client
|
|
285
|
+
await printData(await callKlineWithSharding(client, "quote.day-kline-hk", buildQuoteKlineBody(options), { shardDays: 3 }), parseOutputFormat(options.format), options.output);
|
|
194
286
|
});
|
|
195
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) => {
|
|
196
288
|
const client = await createClient();
|
|
197
|
-
await printData(await client
|
|
289
|
+
await printData(await callKlineWithSharding(client, "quote.index-day-kline", buildQuoteKlineBody(options), { shardDays: 30 }), parseOutputFormat(options.format), options.output);
|
|
198
290
|
});
|
|
199
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) => {
|
|
200
292
|
const client = await createClient();
|
|
@@ -202,26 +294,15 @@ quote.command("minute-kline").option("--security <code>", "Security code (A-shar
|
|
|
202
294
|
});
|
|
203
295
|
program.addCommand(quote);
|
|
204
296
|
const fundamental = new Command("fundamental").description("Fundamental APIs");
|
|
205
|
-
fundamental.command(
|
|
206
|
-
const client = await createClient();
|
|
207
|
-
await printData(await client.call("fundamental.income-statement", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
208
|
-
});
|
|
209
|
-
fundamental.command("income-statement-quarterly").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/q2/q3/q4/latest", collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
210
|
-
const client = await createClient();
|
|
211
|
-
await printData(await client.call("fundamental.income-statement-quarterly", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
212
|
-
});
|
|
213
|
-
fundamental.command("balance-sheet").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period", collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
214
|
-
const client = await createClient();
|
|
215
|
-
await printData(await client.call("fundamental.balance-sheet", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
216
|
-
});
|
|
217
|
-
fundamental.command("cash-flow").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period", collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
218
|
-
const client = await createClient();
|
|
219
|
-
await printData(await client.call("fundamental.cash-flow", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
220
|
-
});
|
|
221
|
-
fundamental.command("cash-flow-quarterly").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/q2/q3/q4/latest", collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
297
|
+
const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
|
|
222
298
|
const client = await createClient();
|
|
223
|
-
await printData(await client.call(
|
|
299
|
+
await printData(await client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
224
300
|
});
|
|
301
|
+
addFinancialReport("income-statement", "fundamental.income-statement");
|
|
302
|
+
addFinancialReport("income-statement-quarterly", "fundamental.income-statement-quarterly", "Period: q1/q2/q3/q4/latest");
|
|
303
|
+
addFinancialReport("balance-sheet", "fundamental.balance-sheet");
|
|
304
|
+
addFinancialReport("cash-flow", "fundamental.cash-flow");
|
|
305
|
+
addFinancialReport("cash-flow-quarterly", "fundamental.cash-flow-quarterly", "Period: q1/q2/q3/q4/latest");
|
|
225
306
|
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) => {
|
|
226
307
|
const client = await createClient();
|
|
227
308
|
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);
|
|
@@ -264,7 +345,10 @@ ai.command("knowledge-batch").requiredOption("--query <text>", "Query", collectL
|
|
|
264
345
|
});
|
|
265
346
|
ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action(async (options) => {
|
|
266
347
|
const client = await createClient();
|
|
267
|
-
await
|
|
348
|
+
await runDownload(client, "ai.knowledge-resource.download", { resourceType: parseNumberOption(options.resourceType, "--resource-type", { integer: true, min: 0 }), sourceId: options.sourceId }, {
|
|
349
|
+
output: options.output,
|
|
350
|
+
fallbackName: `resource-${options.sourceId}`,
|
|
351
|
+
});
|
|
268
352
|
});
|
|
269
353
|
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) => {
|
|
270
354
|
const client = await createClient();
|
|
@@ -368,6 +452,16 @@ ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId fr
|
|
|
368
452
|
const client = await createClient();
|
|
369
453
|
await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output);
|
|
370
454
|
});
|
|
455
|
+
const reference = new Command("reference").description("Reference data APIs");
|
|
456
|
+
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) => {
|
|
457
|
+
const client = await createClient();
|
|
458
|
+
await printData(await client.call("reference.securities-search", {
|
|
459
|
+
keyword: options.keyword,
|
|
460
|
+
category: options.category.length ? options.category : undefined,
|
|
461
|
+
top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
|
|
462
|
+
}), parseOutputFormat(options.format), options.output);
|
|
463
|
+
});
|
|
464
|
+
program.addCommand(reference);
|
|
371
465
|
const vault = new Command("vault").description("Vault APIs");
|
|
372
466
|
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) => {
|
|
373
467
|
const client = await createClient();
|
|
@@ -375,9 +469,11 @@ vault.command("drive-list").option("--from <number>", "Starting offset", "0").op
|
|
|
375
469
|
});
|
|
376
470
|
vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action(async (options) => {
|
|
377
471
|
const client = await createClient();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
472
|
+
await runDownload(client, "vault.drive.download", { fileId: options.fileId }, {
|
|
473
|
+
output: options.output,
|
|
474
|
+
fallbackName: `file-${options.fileId}`,
|
|
475
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "vault.drive.list", "fileId", options.fileId),
|
|
476
|
+
});
|
|
381
477
|
});
|
|
382
478
|
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) => {
|
|
383
479
|
const client = await createClient();
|
|
@@ -385,9 +481,11 @@ vault.command("record-list").option("--from <number>", "Starting offset", "0").o
|
|
|
385
481
|
});
|
|
386
482
|
vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action(async (options) => {
|
|
387
483
|
const client = await createClient();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
484
|
+
await runDownload(client, "vault.record.download", { recordId: options.recordId, contentType: options.contentType }, {
|
|
485
|
+
output: options.output,
|
|
486
|
+
fallbackName: `record-${options.recordId}`,
|
|
487
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "vault.record.list", "recordId", options.recordId),
|
|
488
|
+
});
|
|
391
489
|
});
|
|
392
490
|
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) => {
|
|
393
491
|
const client = await createClient();
|
|
@@ -395,9 +493,11 @@ vault.command("my-conference-list").option("--from <number>", "Starting offset",
|
|
|
395
493
|
});
|
|
396
494
|
vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action(async (options) => {
|
|
397
495
|
const client = await createClient();
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
496
|
+
await runDownload(client, "vault.my-conference.download", { conferenceId: options.conferenceId, contentType: options.contentType }, {
|
|
497
|
+
output: options.output,
|
|
498
|
+
fallbackName: `conference-${options.conferenceId}`,
|
|
499
|
+
resolveOutputPath: (result) => resolveTitle(client, result, "vault.my-conference.list", "conferenceId", options.conferenceId),
|
|
500
|
+
});
|
|
401
501
|
});
|
|
402
502
|
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) => {
|
|
403
503
|
const client = await createClient();
|
|
@@ -410,6 +510,10 @@ vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset
|
|
|
410
510
|
program.addCommand(vault);
|
|
411
511
|
program.addCommand(ai);
|
|
412
512
|
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) => {
|
|
513
|
+
const endpoint = ENDPOINTS[endpointKey];
|
|
514
|
+
if (!endpoint) {
|
|
515
|
+
throw new ConfigError(`Unknown endpoint key: ${endpointKey}`);
|
|
516
|
+
}
|
|
413
517
|
const client = await createClient();
|
|
414
518
|
let body;
|
|
415
519
|
if (options.body) {
|
|
@@ -420,14 +524,43 @@ program.command("raw").description("Raw API calls").addCommand(new Command("call
|
|
|
420
524
|
throw new ConfigError(`Invalid JSON in --body: ${options.body}`);
|
|
421
525
|
}
|
|
422
526
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
527
|
+
if (endpoint.kind === "download") {
|
|
528
|
+
await runDownload(client, endpointKey, options.query, {
|
|
529
|
+
output: options.output,
|
|
530
|
+
fallbackName: "download.bin",
|
|
531
|
+
});
|
|
426
532
|
return;
|
|
427
533
|
}
|
|
534
|
+
const data = await client.call(endpointKey, body, options.query);
|
|
428
535
|
await printData(data, parseOutputFormat(options.format), options.output);
|
|
429
536
|
}));
|
|
537
|
+
async function checkForUpdate(timeoutMs = 2000) {
|
|
538
|
+
const https = await import("node:https");
|
|
539
|
+
await new Promise((resolve) => {
|
|
540
|
+
const req = https.get("https://registry.npmjs.org/gangtise-openapi-cli/latest", (res) => {
|
|
541
|
+
let body = "";
|
|
542
|
+
res.on("data", (chunk) => { body += chunk; });
|
|
543
|
+
res.on("end", () => {
|
|
544
|
+
try {
|
|
545
|
+
const latest = JSON.parse(body).version;
|
|
546
|
+
if (latest && latest !== CLI_VERSION) {
|
|
547
|
+
process.stderr.write(`Update available: ${CLI_VERSION} → ${latest}\nRun: npm update -g gangtise-openapi-cli\n`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch { /* ignore */ }
|
|
551
|
+
resolve();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
req.on("error", () => resolve());
|
|
555
|
+
req.setTimeout(timeoutMs, () => { req.destroy(); resolve(); });
|
|
556
|
+
});
|
|
557
|
+
}
|
|
430
558
|
async function main() {
|
|
559
|
+
if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
560
|
+
process.stdout.write(`${CLI_VERSION}\n`);
|
|
561
|
+
await checkForUpdate();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
431
564
|
try {
|
|
432
565
|
await program.parseAsync(process.argv);
|
|
433
566
|
}
|
|
@@ -448,23 +581,3 @@ async function main() {
|
|
|
448
581
|
}
|
|
449
582
|
}
|
|
450
583
|
void main();
|
|
451
|
-
// Background update check on --version
|
|
452
|
-
if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
453
|
-
import("node:https").then((https) => {
|
|
454
|
-
const req = https.get("https://registry.npmjs.org/gangtise-openapi-cli/latest", (res) => {
|
|
455
|
-
let body = "";
|
|
456
|
-
res.on("data", (chunk) => { body += chunk; });
|
|
457
|
-
res.on("end", () => {
|
|
458
|
-
try {
|
|
459
|
-
const latest = JSON.parse(body).version;
|
|
460
|
-
if (latest && latest !== CLI_VERSION) {
|
|
461
|
-
process.stderr.write(`\nUpdate available: ${CLI_VERSION} → ${latest}\nRun: npm update -g gangtise-openapi-cli\n`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
catch { /* ignore */ }
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
req.on("error", () => { });
|
|
468
|
-
req.setTimeout(3000, () => { req.destroy(); });
|
|
469
|
-
}).catch(() => { });
|
|
470
|
-
}
|
|
@@ -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;
|