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 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` | 公告(含 Markdown 下载) |
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
- # 不传 --security 默认返回全市场,不传 --start-date 默认往前一年,不传 --end-date 默认最新
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.name("gangtise").description("Gangtise OpenAPI CLI").version(CLI_VERSION);
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 result = await client.call("insight.summary.download", undefined, { summaryId: options.summaryId });
111
- const title = options.output ? undefined : await resolveTitle(client, result, "insight.summary.list", "summaryId", options.summaryId);
112
- await saveDownloadResult(result, `summary-${options.summaryId}`, options.output ?? title);
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
- const result = await client.call("insight.research.download", undefined, { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
141
- const title = options.output ? undefined : await resolveTitle(client, result, "insight.research.list", "reportId", options.reportId);
142
- await saveDownloadResult(result, `research-${options.reportId}`, options.output ?? title);
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
- const result = await client.call("insight.foreign-report.download", undefined, { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
158
- const title = options.output ? undefined : await resolveTitle(client, result, "insight.foreign-report.list", "reportId", options.reportId);
159
- await saveDownloadResult(result, `foreign-report-${options.reportId}`, options.output ?? title);
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
- const result = await client.call("insight.announcement.download", undefined, { announcementId: options.announcementId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
173
- const title = options.output ? undefined : await resolveTitle(client, result, "insight.announcement.list", "announcementId", options.announcementId);
174
- await saveDownloadResult(result, `announcement-${options.announcementId}`, options.output ?? title);
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.call("quote.day-kline", buildQuoteKlineBody(options)), parseOutputFormat(options.format), options.output);
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.call("quote.day-kline-hk", buildQuoteKlineBody(options)), parseOutputFormat(options.format), options.output);
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.call("quote.index-day-kline", buildQuoteKlineBody(options)), parseOutputFormat(options.format), options.output);
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("income-statement").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) => {
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("fundamental.cash-flow-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);
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 saveDownloadResult(await client.call("ai.knowledge-resource.download", undefined, { resourceType: parseNumberOption(options.resourceType, "--resource-type", { integer: true, min: 0 }), sourceId: options.sourceId }), `resource-${options.sourceId}`, options.output);
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
- const result = await client.call("vault.drive.download", undefined, { fileId: options.fileId });
379
- const title = options.output ? undefined : await resolveTitle(client, result, "vault.drive.list", "fileId", options.fileId);
380
- await saveDownloadResult(result, `file-${options.fileId}`, options.output ?? title);
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
- const result = await client.call("vault.record.download", undefined, { recordId: options.recordId, contentType: options.contentType });
389
- const title = options.output ? undefined : await resolveTitle(client, result, "vault.record.list", "recordId", options.recordId);
390
- await saveDownloadResult(result, `record-${options.recordId}`, options.output ?? title);
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
- const result = await client.call("vault.my-conference.download", undefined, { conferenceId: options.conferenceId, contentType: options.contentType });
399
- const title = options.output ? undefined : await resolveTitle(client, result, "vault.my-conference.list", "conferenceId", options.conferenceId);
400
- await saveDownloadResult(result, `conference-${options.conferenceId}`, options.output ?? title);
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
- const data = await client.call(endpointKey, body, options.query);
424
- if (data && typeof data === "object" && "data" in data && data.data instanceof Uint8Array) {
425
- await saveDownloadResult(data, "download.bin", options.output);
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 = 12;
4
- export const POLL_DELAY_MS = 15_000;
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
- process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: content not ready, retrying in 15s...\n`);
27
- await new Promise(resolve => setTimeout(resolve, POLL_DELAY_MS));
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;
@@ -10,10 +10,7 @@ export async function readTokenCache(filePath) {
10
10
  }
11
11
  return null;
12
12
  }
13
- catch (error) {
14
- if (error.code === "ENOENT") {
15
- return null;
16
- }
13
+ catch {
17
14
  return null;
18
15
  }
19
16
  }