gangtise-openapi-cli 0.10.9 → 0.11.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
@@ -112,6 +112,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
112
112
  | | `foreign-report list` / `download` | 外资研报(含中文翻译下载) |
113
113
  | | `announcement list` / `download` | 公告(含 Markdown 下载) |
114
114
  | **Quote** | `day-kline` / `day-kline-hk` | A股/港股日K线 |
115
+ | | `index-day-kline` | 沪深京指数日K线 |
115
116
  | | `minute-kline` | A股分钟K线 |
116
117
  | **Fundamental** | `income-statement` / `balance-sheet` / `cash-flow` | 三大财务报表(累计) |
117
118
  | | `income-statement-quarterly` / `cash-flow-quarterly` | 利润表/现金流量表(单季度) |
@@ -135,6 +136,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
135
136
  | **Vault** | `drive-list` / `drive-download` | 云盘文件列表与下载 |
136
137
  | | `record-list` / `record-download` | 录音速记列表与下载 |
137
138
  | | `my-conference-list` / `my-conference-download` | 我的会议列表与下载 |
139
+ | | `wechat-message-list` / `wechat-chatroom-list` | 群消息列表与群ID查询 |
138
140
  | **Raw** | `call` | 原始接口调用(可访问任意 endpoint) |
139
141
 
140
142
  ## 命令概览
@@ -187,12 +189,14 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
187
189
  - `vault drive-list`
188
190
  - `vault record-list`
189
191
  - `vault my-conference-list`
192
+ - `vault wechat-message-list`
190
193
  - `ai hot-topic`
191
194
 
192
195
  规则:
193
196
  - **有时间范围时**(传了 `--start-time/--end-time` 或 `--start-date/--end-date`):**省略 `--size`**,CLI 自动翻页查全
194
197
  - **无时间范围时**(未传时间参数):默认 `--size 200`,防止一次查询数据量过大
195
198
  - 如果显式传了 `--size`,则按指定值翻页,直到达到 `size` 或数据取完
199
+ - `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
196
200
  - 安全上限:自动翻页最多 1000 页,防止异常循环
197
201
  - 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`),同时 stderr 输出 `Total: N, showing: M`
198
202
 
@@ -259,6 +263,8 @@ gangtise quote day-kline --security all --start-date 2026-04-01 --end-date 2026-
259
263
  gangtise quote day-kline-hk --security 00700.HK --start-date 2026-03-01 --end-date 2026-03-31
260
264
  # 港股全市场
261
265
  gangtise quote day-kline-hk --security all --start-date 2026-04-01 --end-date 2026-04-01 --limit 100 --format json
266
+ # 沪深京指数日K线
267
+ gangtise quote index-day-kline --security 000001.SH --security 399001.SZ --start-date 2024-05-01 --end-date 2024-05-20 --field securityCode --field tradeDate --field close --field volume
262
268
  # A股分钟K线
263
269
  gangtise quote minute-kline --security 600519.SH --start-time "2026-04-15 09:30:00" --end-time "2026-04-15 15:00:00" --field open --field close --field volume
264
270
  ```
@@ -340,6 +346,10 @@ gangtise vault record-download --record-id 49412 --content-type summary
340
346
  gangtise vault my-conference-list --keyword AI --category earningsCall --institution C100000027
341
347
  # 我的会议下载(--content-type: asr/summary)
342
348
  gangtise vault my-conference-download --conference-id 43319 --content-type asr
349
+
350
+ # 群消息:先按群名称查群ID,再按群ID查消息
351
+ gangtise vault wechat-chatroom-list --room-name "AI学习群,投研分享群" --size 50
352
+ gangtise vault wechat-message-list --keyword AI应用 --wechat-group-id ueKEGyhdjFGkjyebh --category text --category url --tag roadShow --tag meetingSummary --size 50
343
353
  ```
344
354
 
345
355
  ### Raw
@@ -362,10 +372,23 @@ gangtise raw call insight.opinion.list --body '{"from":0,"size":120}'
362
372
 
363
373
  所有格式均支持 `--output <path>` 输出到文件(自动创建父目录)。
364
374
 
375
+ ## 参数校验
376
+
377
+ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到 API:
378
+
379
+ - `--from`:非负整数
380
+ - `--size` / `--limit` / `--top`:正整数
381
+ - `--file-type` / `--resource-type` 以及数值型列表参数:有限数字
382
+ - 公告 `--start-time` / `--end-time`:可解析的时间字符串或 Unix 时间戳
383
+
384
+ 校验失败会输出 `ValidationError: Invalid ...` 并以非 0 状态退出。
385
+
365
386
  ## 常见错误
366
387
 
367
- | 错误码 | 说明 |
368
- |--------|------|
388
+ | 错误/错误码 | 说明 |
389
+ |-----------|------|
390
+ | `ValidationError` | 本地参数校验失败,检查 `--size` / `--limit` / `--from` / `--file-type` 等数值参数 |
391
+ | `API error (HTTP 4xx/5xx)` | HTTP 层失败;CLI 会把 4xx/5xx 响应视为错误,即使响应体不是标准 `{code,msg,data}` 信封 |
369
392
  | `8000014` | `GANGTISE_ACCESS_KEY` 错误 |
370
393
  | `8000015` | `GANGTISE_SECRET_KEY` 错误 |
371
394
  | `8000016` | 开发账号状态异常 |
package/dist/src/cli.js CHANGED
@@ -1,244 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Option } from "commander";
3
- import { collectKeyValue, collectList, collectNumberList, maybeArray, toTimestamp13 } from "./core/args.js";
3
+ import { checkAsyncContent, pollAsyncContent, POLL_MAX_ATTEMPTS } from "./core/asyncContent.js";
4
+ import { readTokenCache } from "./core/auth.js";
5
+ import { collectKeyValue, collectList, collectNumberList, maybeArray, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize, parseTimestamp13 } from "./core/args.js";
6
+ import { buildQuoteKlineBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
4
7
  import { loadConfig } from "./core/config.js";
5
- import { ApiError, ConfigError, DownloadError } from "./core/errors.js";
8
+ import { resolveTitle, saveDownloadResult } from "./core/download.js";
9
+ import { ApiError, ConfigError } from "./core/errors.js";
10
+ import { normalizeRows } from "./core/normalize.js";
11
+ import { parseOutputFormat } from "./core/output.js";
12
+ import { printData } from "./core/printer.js";
6
13
  // --- Lazy-loaded modules (deferred to action handlers) ---
7
14
  async function createClient() {
8
15
  const { GangtiseClient } = await import("./core/client.js");
9
16
  return new GangtiseClient(loadConfig());
10
17
  }
11
- async function readTokenCache(...args) {
12
- return (await import("./core/auth.js")).readTokenCache(...args);
13
- }
14
- async function normalizeRows(...args) {
15
- return (await import("./core/normalize.js")).normalizeRows(...args);
16
- }
17
- async function renderOutput(...args) {
18
- return (await import("./core/output.js")).renderOutput(...args);
19
- }
20
- async function saveOutputIfNeeded(...args) {
21
- return (await import("./core/output.js")).saveOutputIfNeeded(...args);
22
- }
23
- function parseFormat(value) {
24
- const format = value ?? "table";
25
- if (["table", "json", "jsonl", "csv", "markdown"].includes(format)) {
26
- return format;
27
- }
28
- throw new ConfigError(`Unsupported format: ${format}`);
29
- }
30
- // --- Title cache: list writes, download reads ---
31
- let _titleCachePath;
32
- function getTitleCachePath() {
33
- if (!_titleCachePath) {
34
- const path = require("node:path");
35
- const os = require("node:os");
36
- _titleCachePath = path.join(os.homedir(), ".config", "gangtise", "title-cache.json");
37
- }
38
- return _titleCachePath;
39
- }
40
- const TITLE_LOOKUP_SIZE = 200;
41
- const TITLE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
42
- async function readTitleCache() {
43
- try {
44
- const fs = await import("node:fs/promises");
45
- return JSON.parse(await fs.readFile(getTitleCachePath(), "utf8"));
46
- }
47
- catch {
48
- return {};
49
- }
50
- }
51
- async function writeTitleCache(endpoint, titles) {
52
- const fs = await import("node:fs/promises");
53
- const path = await import("node:path");
54
- const data = await readTitleCache();
55
- data[endpoint] = { titles, ts: Date.now() };
56
- await fs.mkdir(path.dirname(getTitleCachePath()), { recursive: true });
57
- await fs.writeFile(getTitleCachePath(), JSON.stringify(data), "utf8");
58
- }
59
- function lookupTitleCache(data, endpoint, id) {
60
- const entry = data[endpoint];
61
- if (!entry || Date.now() - entry.ts > TITLE_CACHE_TTL_MS)
62
- return undefined;
63
- return entry.titles[id];
64
- }
65
- async function printData(data, format, output, cache) {
66
- const normalized = await normalizeRows(data);
67
- const items = Array.isArray(normalized)
68
- ? normalized
69
- : (normalized && typeof normalized === "object" && Array.isArray(normalized.list))
70
- ? normalized.list
71
- : null;
72
- if (cache && items) {
73
- const titleField = cache.titleField ?? "title";
74
- const titles = {};
75
- for (const row of items) {
76
- if (row && typeof row === "object") {
77
- const r = row;
78
- const id = r[cache.idField];
79
- const title = r[titleField];
80
- if (id != null && typeof title === "string" && title)
81
- titles[String(id)] = title;
82
- }
83
- }
84
- if (Object.keys(titles).length > 0)
85
- writeTitleCache(cache.endpointKey, titles).catch(() => { });
86
- }
87
- if (normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
88
- const meta = normalized;
89
- if (typeof meta.total === "number" && format !== "json") {
90
- const listLen = Array.isArray(meta.list) ? meta.list.length : 0;
91
- process.stderr.write(`Total: ${meta.total}, showing: ${listLen}\n`);
92
- }
93
- }
94
- const content = await renderOutput(normalized, format);
95
- if (output) {
96
- await saveOutputIfNeeded(content, output);
97
- process.stdout.write(`${output}\n`);
98
- return;
99
- }
100
- process.stdout.write(`${content}\n`);
101
- }
102
- const MIME_EXT = {
103
- "application/pdf": ".pdf",
104
- "application/msword": ".doc",
105
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
106
- "application/vnd.ms-excel": ".xls",
107
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
108
- "application/vnd.ms-powerpoint": ".ppt",
109
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
110
- "application/zip": ".zip",
111
- "application/x-rar-compressed": ".rar",
112
- "application/gzip": ".gz",
113
- "application/x-7z-compressed": ".7z",
114
- "application/json": ".json",
115
- "application/xml": ".xml",
116
- "text/plain": ".txt",
117
- "text/html": ".html",
118
- "text/csv": ".csv",
119
- "image/png": ".png",
120
- "image/jpeg": ".jpg",
121
- "image/gif": ".gif",
122
- "image/webp": ".webp",
123
- "image/svg+xml": ".svg",
124
- "audio/mpeg": ".mp3",
125
- "video/mp4": ".mp4",
126
- "application/octet-stream": ".bin",
127
- };
128
- function extFromContentType(contentType) {
129
- if (!contentType)
130
- return "";
131
- const mime = contentType.split(";")[0].trim().toLowerCase();
132
- return MIME_EXT[mime] ?? "";
133
- }
134
- /** Resolve a human-readable filename by looking up the title from cache or list endpoint. */
135
- async function resolveTitle(client, result, listEndpoint, idField, idValue, titleField = "title") {
136
- const { extname } = await import("node:path");
137
- const file = result;
138
- const serverExt = file.filename ? extname(file.filename) : extFromContentType(file.contentType);
139
- function buildFilename(rawTitle) {
140
- let title = rawTitle.replace(/[/\\:*?"<>|]/g, "_").trim();
141
- if (serverExt && !title.toLowerCase().endsWith(serverExt.toLowerCase())) {
142
- title += serverExt;
143
- }
144
- return title;
145
- }
146
- // 1. Check file-based title cache (populated by prior list command)
147
- try {
148
- const cacheData = await readTitleCache();
149
- const cached = lookupTitleCache(cacheData, listEndpoint, idValue);
150
- if (cached)
151
- return buildFilename(cached);
152
- }
153
- catch { /* ignore */ }
154
- // 2. Fallback: query list API (scan recent 200 items)
155
- try {
156
- const resp = await client.call(listEndpoint, { from: 0, size: TITLE_LOOKUP_SIZE });
157
- const items = Array.isArray(resp) ? resp : (resp.list ?? []);
158
- const match = items.find(f => String(f[idField]) === String(idValue));
159
- const rawTitle = match?.[titleField];
160
- if (typeof rawTitle === "string" && rawTitle)
161
- return buildFilename(rawTitle);
162
- }
163
- catch { /* ignore */ }
164
- return undefined;
165
- }
166
- async function saveDownloadResult(result, fallbackName, output) {
167
- if (!(result && typeof result === "object")) {
168
- throw new DownloadError("Unexpected download response");
169
- }
170
- const file = result;
171
- if (file.data instanceof Uint8Array) {
172
- const outputPath = output ?? file.filename ?? (fallbackName + extFromContentType(file.contentType));
173
- await saveOutputIfNeeded(file.data, outputPath);
174
- process.stdout.write(`${outputPath}\n`);
175
- return;
176
- }
177
- if (typeof file.text === 'string') {
178
- const outputPath = output ?? `${fallbackName}.txt`;
179
- await saveOutputIfNeeded(file.text, outputPath);
180
- process.stdout.write(`${outputPath}\n`);
181
- return;
182
- }
183
- if (typeof file.url === 'string') {
184
- if (output) {
185
- await saveOutputIfNeeded(file.url, output);
186
- process.stdout.write(`${output}\n`);
187
- return;
188
- }
189
- process.stdout.write(`${file.url}\n`);
190
- return;
191
- }
192
- throw new DownloadError("Unexpected download response");
193
- }
194
- const POLL_MAX_ATTEMPTS = 12;
195
- const POLL_DELAY_MS = 15_000;
196
- function isAsyncPending(error) {
197
- return error instanceof ApiError && error.code === "410110";
198
- }
199
- async function pollAsyncContent(client, getContentEndpoint, dataId, format, output) {
200
- for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
201
- try {
202
- const result = await client.call(getContentEndpoint, { dataId });
203
- if (result?.content != null) {
204
- await printData(result, format, output);
205
- return true;
206
- }
207
- }
208
- catch (error) {
209
- if (error instanceof ApiError && error.code === "410111") {
210
- process.stderr.write("Content generation failed (terminal). Do not retry.\n");
211
- return false;
212
- }
213
- if (!isAsyncPending(error))
214
- throw error;
215
- }
216
- if (attempt < POLL_MAX_ATTEMPTS) {
217
- process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: content not ready, retrying in 15s...\n`);
218
- await new Promise(resolve => setTimeout(resolve, POLL_DELAY_MS));
219
- }
220
- }
221
- return false;
222
- }
223
- async function checkAsyncContent(client, getContentEndpoint, dataId, format, output) {
224
- try {
225
- const result = await client.call(getContentEndpoint, { dataId });
226
- if (result?.content != null) {
227
- await printData(result, format, output);
228
- return;
229
- }
230
- }
231
- catch (error) {
232
- if (error instanceof ApiError && error.code === "410111") {
233
- process.stderr.write("Content generation failed (terminal). Do not retry.\n");
234
- process.exitCode = 1;
235
- return;
236
- }
237
- if (!isAsyncPending(error))
238
- throw error;
239
- }
240
- process.stdout.write(`${JSON.stringify({ dataId, status: "pending", hint: "Content not ready yet, retry in ~2 minutes" })}\n`);
241
- }
242
18
  function addTimeFilters(command) {
243
19
  return command
244
20
  .option("--from <number>", "Starting offset", "0")
@@ -257,48 +33,48 @@ program
257
33
  .option("--format <format>", "Output format", "json")
258
34
  .action(async (options) => {
259
35
  const client = await createClient();
260
- await printData(await client.login(), parseFormat(options.format));
36
+ await printData(await client.login(), parseOutputFormat(options.format));
261
37
  }))
262
38
  .addCommand(new Command("status")
263
39
  .option("--format <format>", "Output format", "json")
264
40
  .action(async (options) => {
265
41
  const config = loadConfig();
266
42
  const cache = await readTokenCache(config.tokenCachePath);
267
- await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache }, parseFormat(options.format));
43
+ await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache }, parseOutputFormat(options.format));
268
44
  }));
269
45
  const lookup = new Command("lookup").description("Lookup helper APIs");
270
46
  lookup
271
47
  .addCommand(new Command("research-area").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
272
48
  const client = await createClient();
273
- await printData(await client.call("lookup.research-areas.list"), parseFormat(options.format));
49
+ await printData(await client.call("lookup.research-areas.list"), parseOutputFormat(options.format));
274
50
  })))
275
51
  .addCommand(new Command("broker-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
276
52
  const client = await createClient();
277
- await printData(await client.call("lookup.broker-orgs.list"), parseFormat(options.format));
53
+ await printData(await client.call("lookup.broker-orgs.list"), parseOutputFormat(options.format));
278
54
  })))
279
55
  .addCommand(new Command("meeting-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
280
56
  const client = await createClient();
281
- await printData(await client.call("lookup.meeting-orgs.list"), parseFormat(options.format));
57
+ await printData(await client.call("lookup.meeting-orgs.list"), parseOutputFormat(options.format));
282
58
  })))
283
59
  .addCommand(new Command("industry").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
284
60
  const client = await createClient();
285
- await printData(await client.call("lookup.industries.list"), parseFormat(options.format));
61
+ await printData(await client.call("lookup.industries.list"), parseOutputFormat(options.format));
286
62
  })))
287
63
  .addCommand(new Command("region").description("Foreign report region codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
288
64
  const client = await createClient();
289
- await printData(await client.call("lookup.regions.list"), parseFormat(options.format));
65
+ await printData(await client.call("lookup.regions.list"), parseOutputFormat(options.format));
290
66
  })))
291
67
  .addCommand(new Command("announcement-category").description("Announcement category codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
292
68
  const client = await createClient();
293
- await printData(await client.call("lookup.announcement-categories.list"), parseFormat(options.format));
69
+ await printData(await client.call("lookup.announcement-categories.list"), parseOutputFormat(options.format));
294
70
  })))
295
71
  .addCommand(new Command("industry-code").description("Shenwan industry codes for security-clue --gts-code").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
296
72
  const client = await createClient();
297
- await printData(await client.call("lookup.industry-codes.list"), parseFormat(options.format));
73
+ await printData(await client.call("lookup.industry-codes.list"), parseOutputFormat(options.format));
298
74
  })))
299
75
  .addCommand(new Command("theme-id").description("Theme IDs for theme-tracking --theme-id").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
300
76
  const client = await createClient();
301
- await printData(await client.call("lookup.theme-ids.list"), parseFormat(options.format));
77
+ await printData(await client.call("lookup.theme-ids.list"), parseOutputFormat(options.format));
302
78
  })));
303
79
  program.addCommand(lookup);
304
80
  const insight = new Command("insight").description("Insight APIs");
@@ -314,20 +90,20 @@ const announcement = new Command("announcement");
314
90
  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) => {
315
91
  const client = await createClient();
316
92
  await printData(await client.call("insight.opinion.list", {
317
- from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime,
318
- rankType: Number(options.rankType), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
93
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
94
+ rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
319
95
  securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
320
96
  llmTagList: maybeArray(options.llmTag), sourceList: maybeArray(options.source),
321
- }), parseFormat(options.format), options.output);
97
+ }), parseOutputFormat(options.format), options.output);
322
98
  });
323
99
  addTimeFilters(summary.command("list").option("--search-type <number>", "Search type", "1").option("--rank-type <number>", "Rank type", "1").option("--source <number>", "Source type", collectNumberList, []).option("--research-area <id>", "Research area", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
324
100
  const client = await createClient();
325
101
  await printData(await client.call("insight.summary.list", {
326
- from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime,
327
- searchType: Number(options.searchType), rankType: Number(options.rankType), keyword: options.keyword, sourceList: options.source.length ? options.source : undefined,
102
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
103
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, sourceList: options.source.length ? options.source : undefined,
328
104
  researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
329
105
  categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
330
- }), parseFormat(options.format), options.output, { endpointKey: "insight.summary.list", idField: "summaryId" });
106
+ }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.summary.list", idField: "summaryId" });
331
107
  });
332
108
  summary.command("download").requiredOption("--summary-id <id>").option("--output <path>").action(async (options) => {
333
109
  const client = await createClient();
@@ -338,11 +114,11 @@ summary.command("download").requiredOption("--summary-id <id>").option("--output
338
114
  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) => {
339
115
  const client = await createClient();
340
116
  await printData(await client.call(endpointKey, {
341
- from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
117
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
342
118
  researchAreaList: maybeArray(options.researchArea), institutionList: maybeArray(options.institution), securityList: maybeArray(options.security),
343
119
  categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
344
120
  brokerTypeList: maybeArray(options.brokerType), objectList: maybeArray(options.object), permission: options.permission.length ? options.permission : undefined,
345
- }), parseFormat(options.format), options.output);
121
+ }), parseOutputFormat(options.format), options.output);
346
122
  });
347
123
  addScheduleList(roadshow, "insight.roadshow.list");
348
124
  addScheduleList(siteVisit, "insight.site-visit.list");
@@ -351,49 +127,49 @@ addScheduleList(forum, "insight.forum.list");
351
127
  addTimeFilters(research.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--broker <id>", "Broker ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--source <type>", "Source type", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
352
128
  const client = await createClient();
353
129
  await printData(await client.call("insight.research.list", {
354
- from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
355
- searchType: Number(options.searchType), rankType: Number(options.rankType),
130
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
131
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
356
132
  brokerList: maybeArray(options.broker), securityList: maybeArray(options.security), industryList: maybeArray(options.industry),
357
133
  categoryList: maybeArray(options.category), llmTagList: maybeArray(options.llmTag), ratingList: maybeArray(options.rating),
358
- ratingChangeList: maybeArray(options.ratingChange), minReportPages: options.minPages ? Number(options.minPages) : undefined,
359
- maxReportPages: options.maxPages ? Number(options.maxPages) : undefined, sourceList: maybeArray(options.source),
360
- }), parseFormat(options.format), options.output, { endpointKey: "insight.research.list", idField: "reportId" });
134
+ ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
135
+ maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
136
+ }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.research.list", idField: "reportId" });
361
137
  });
362
138
  research.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action(async (options) => {
363
139
  const client = await createClient();
364
- const result = await client.call("insight.research.download", undefined, { reportId: options.reportId, fileType: Number(options.fileType) });
140
+ const result = await client.call("insight.research.download", undefined, { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
365
141
  const title = options.output ? undefined : await resolveTitle(client, result, "insight.research.list", "reportId", options.reportId);
366
142
  await saveDownloadResult(result, `research-${options.reportId}`, options.output ?? title);
367
143
  });
368
144
  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) => {
369
145
  const client = await createClient();
370
146
  await printData(await client.call("insight.foreign-report.list", {
371
- from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
372
- searchType: Number(options.searchType), rankType: Number(options.rankType),
147
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
148
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
373
149
  securityList: maybeArray(options.security), regionList: maybeArray(options.region), categoryList: maybeArray(options.category),
374
150
  industryList: maybeArray(options.industry), brokerList: maybeArray(options.broker), llmTagList: maybeArray(options.llmTag),
375
151
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
376
- minReportPages: options.minPages ? Number(options.minPages) : undefined, maxReportPages: options.maxPages ? Number(options.maxPages) : undefined,
377
- }), parseFormat(options.format), options.output, { endpointKey: "insight.foreign-report.list", idField: "reportId" });
152
+ minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
153
+ }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.foreign-report.list", idField: "reportId" });
378
154
  });
379
155
  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) => {
380
156
  const client = await createClient();
381
- const result = await client.call("insight.foreign-report.download", undefined, { reportId: options.reportId, fileType: Number(options.fileType) });
157
+ const result = await client.call("insight.foreign-report.download", undefined, { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
382
158
  const title = options.output ? undefined : await resolveTitle(client, result, "insight.foreign-report.list", "reportId", options.reportId);
383
159
  await saveDownloadResult(result, `foreign-report-${options.reportId}`, options.output ?? title);
384
160
  });
385
161
  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) => {
386
162
  const client = await createClient();
387
163
  await printData(await client.call("insight.announcement.list", {
388
- from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size),
389
- startTime: toTimestamp13(options.startTime), endTime: toTimestamp13(options.endTime),
390
- searchType: Number(options.searchType), rankType: Number(options.rankType), keyword: options.keyword,
164
+ from: parseFrom(options.from), size: parseSize(options.size),
165
+ startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
166
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
391
167
  securityList: maybeArray(options.security), announcementTypeList: maybeArray(options.announcementType), categoryList: maybeArray(options.category),
392
- }), parseFormat(options.format), options.output, { endpointKey: "insight.announcement.list", idField: "announcementId" });
168
+ }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.announcement.list", idField: "announcementId" });
393
169
  });
394
170
  announcement.command("download").requiredOption("--announcement-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action(async (options) => {
395
171
  const client = await createClient();
396
- const result = await client.call("insight.announcement.download", undefined, { announcementId: options.announcementId, fileType: Number(options.fileType) });
172
+ const result = await client.call("insight.announcement.download", undefined, { announcementId: options.announcementId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
397
173
  const title = options.output ? undefined : await resolveTitle(client, result, "insight.announcement.list", "announcementId", options.announcementId);
398
174
  await saveDownloadResult(result, `announcement-${options.announcementId}`, options.output ?? title);
399
175
  });
@@ -410,45 +186,49 @@ program.addCommand(insight);
410
186
  const quote = new Command("quote").description("Quote APIs");
411
187
  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) => {
412
188
  const client = await createClient();
413
- await printData(await client.call("quote.day-kline", { securityList: maybeArray(options.security), startDate: options.startDate, endDate: options.endDate, limit: options.limit ? Number(options.limit) : undefined, fieldList: maybeArray(options.field) }), parseFormat(options.format), options.output);
189
+ await printData(await client.call("quote.day-kline", buildQuoteKlineBody(options)), parseOutputFormat(options.format), options.output);
414
190
  });
415
191
  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) => {
416
192
  const client = await createClient();
417
- await printData(await client.call("quote.day-kline-hk", { securityList: maybeArray(options.security), startDate: options.startDate, endDate: options.endDate, limit: options.limit ? Number(options.limit) : undefined, fieldList: maybeArray(options.field) }), parseFormat(options.format), options.output);
193
+ await printData(await client.call("quote.day-kline-hk", buildQuoteKlineBody(options)), parseOutputFormat(options.format), options.output);
194
+ });
195
+ 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
+ const client = await createClient();
197
+ await printData(await client.call("quote.index-day-kline", buildQuoteKlineBody(options)), parseOutputFormat(options.format), options.output);
418
198
  });
419
199
  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) => {
420
200
  const client = await createClient();
421
- await printData(await client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit: options.limit ? Number(options.limit) : undefined, fieldList: maybeArray(options.field) }), parseFormat(options.format), options.output);
201
+ await printData(await client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
422
202
  });
423
203
  program.addCommand(quote);
424
204
  const fundamental = new Command("fundamental").description("Fundamental APIs");
425
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) => {
426
206
  const client = await createClient();
427
- 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) }), parseFormat(options.format), options.output);
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);
428
208
  });
429
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) => {
430
210
  const client = await createClient();
431
- 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) }), parseFormat(options.format), options.output);
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);
432
212
  });
433
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) => {
434
214
  const client = await createClient();
435
- 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) }), parseFormat(options.format), options.output);
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);
436
216
  });
437
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) => {
438
218
  const client = await createClient();
439
- 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) }), parseFormat(options.format), options.output);
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);
440
220
  });
441
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) => {
442
222
  const client = await createClient();
443
- 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) }), parseFormat(options.format), options.output);
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);
444
224
  });
445
225
  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) => {
446
226
  const client = await createClient();
447
- 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) }), parseFormat(options.format), options.output);
227
+ 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);
448
228
  });
449
229
  fundamental.command("valuation-analysis").requiredOption("--security-code <code>").addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--limit <number>").option("--field <field>", "Field", collectList, []).option("--skip-null", "Drop rows where value or percentileRank is null").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
450
230
  const client = await createClient();
451
- let data = await client.call("fundamental.valuation-analysis", { securityCode: options.securityCode, indicator: options.indicator, startDate: options.startDate, endDate: options.endDate, limit: options.limit ? Number(options.limit) : undefined, fieldList: maybeArray(options.field) });
231
+ let data = await client.call("fundamental.valuation-analysis", { securityCode: options.securityCode, indicator: options.indicator, startDate: options.startDate, endDate: options.endDate, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) });
452
232
  if (options.skipNull) {
453
233
  const normalized = await normalizeRows(data);
454
234
  if (normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
@@ -464,43 +244,43 @@ fundamental.command("valuation-analysis").requiredOption("--security-code <code>
464
244
  }
465
245
  }
466
246
  }
467
- await printData(data, parseFormat(options.format), options.output);
247
+ await printData(data, parseOutputFormat(options.format), options.output);
468
248
  });
469
249
  fundamental.command("top-holders").requiredOption("--security-code <code>").addOption(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/interim/q3/annual/latest", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
470
250
  const client = await createClient();
471
- await printData(await client.call("fundamental.top-holders", { securityCode: options.securityCode, holderType: options.holderType, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined }), parseFormat(options.format), options.output);
251
+ await printData(await client.call("fundamental.top-holders", { securityCode: options.securityCode, holderType: options.holderType, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined }), parseOutputFormat(options.format), options.output);
472
252
  });
473
253
  fundamental.command("earning-forecast").requiredOption("--security-code <code>").option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: today)").option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
474
254
  const client = await createClient();
475
255
  const endDate = options.endDate ?? new Date().toISOString().slice(0, 10);
476
256
  const startDate = options.startDate ?? new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
477
- await printData(await client.call("fundamental.earning-forecast", { securityCode: options.securityCode, startDate, endDate, consensusList: maybeArray(options.consensus) }), parseFormat(options.format), options.output);
257
+ await printData(await client.call("fundamental.earning-forecast", { securityCode: options.securityCode, startDate, endDate, consensusList: maybeArray(options.consensus) }), parseOutputFormat(options.format), options.output);
478
258
  });
479
259
  program.addCommand(fundamental);
480
260
  const ai = new Command("ai").description("AI APIs");
481
261
  ai.command("knowledge-batch").requiredOption("--query <text>", "Query", collectList, []).option("--top <number>", "Top", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <ms>").option("--end-time <ms>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
482
262
  const client = await createClient();
483
- await printData(await client.call("ai.knowledge-batch", { queries: options.query, top: Number(options.top), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: options.startTime ? Number(options.startTime) : undefined, endTime: options.endTime ? Number(options.endTime) : undefined }), parseFormat(options.format), options.output);
263
+ await printData(await client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseOptionalNumberOption(options.startTime, "--start-time", { integer: true, min: 0 }), endTime: parseOptionalNumberOption(options.endTime, "--end-time", { integer: true, min: 0 }) }), parseOutputFormat(options.format), options.output);
484
264
  });
485
265
  ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action(async (options) => {
486
266
  const client = await createClient();
487
- await saveDownloadResult(await client.call("ai.knowledge-resource.download", undefined, { resourceType: Number(options.resourceType), sourceId: options.sourceId }), `resource-${options.sourceId}`, options.output);
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);
488
268
  });
489
269
  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) => {
490
270
  const client = await createClient();
491
- await printData(await client.call("ai.security-clue.list", { from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, queryMode: options.queryMode, gtsCodeList: maybeArray(options.gtsCode), source: maybeArray(options.source) }), parseFormat(options.format), options.output);
271
+ await printData(await client.call("ai.security-clue.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, queryMode: options.queryMode, gtsCodeList: maybeArray(options.gtsCode), source: maybeArray(options.source) }), parseOutputFormat(options.format), options.output);
492
272
  });
493
273
  ai.command("one-pager").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
494
274
  const client = await createClient();
495
- await printData(await client.call("ai.one-pager", { securityCode: options.securityCode }), parseFormat(options.format), options.output);
275
+ await printData(await client.call("ai.one-pager", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
496
276
  });
497
277
  ai.command("investment-logic").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
498
278
  const client = await createClient();
499
- await printData(await client.call("ai.investment-logic", { securityCode: options.securityCode }), parseFormat(options.format), options.output);
279
+ await printData(await client.call("ai.investment-logic", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
500
280
  });
501
281
  ai.command("peer-comparison").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
502
282
  const client = await createClient();
503
- await printData(await client.call("ai.peer-comparison", { securityCode: options.securityCode }), parseFormat(options.format), options.output);
283
+ await printData(await client.call("ai.peer-comparison", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
504
284
  });
505
285
  ai.command("earnings-review").requiredOption("--security-code <code>").requiredOption("--period <period>", "Report period (e.g. 2025q3, 2025interim, 2025annual)").option("--wait", "Wait for content generation (blocking, up to 3 min)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
506
286
  const client = await createClient();
@@ -517,36 +297,36 @@ ai.command("earnings-review").requiredOption("--security-code <code>").requiredO
517
297
  return;
518
298
  }
519
299
  process.stderr.write(`Got dataId: ${dataId}, waiting for content generation...\n`);
520
- if (!await pollAsyncContent(client, "ai.earnings-review.get-content", dataId, parseFormat(options.format), options.output)) {
300
+ if (!await pollAsyncContent(client, "ai.earnings-review.get-content", dataId, parseOutputFormat(options.format), options.output)) {
521
301
  process.stderr.write(`Content not available after ${POLL_MAX_ATTEMPTS} attempts. Try again later with: gangtise ai earnings-review-check --data-id ${dataId}\n`);
522
302
  process.exitCode = 1;
523
303
  }
524
304
  });
525
305
  ai.command("earnings-review-check").requiredOption("--data-id <id>", "dataId from earnings-review").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
526
306
  const client = await createClient();
527
- await checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseFormat(options.format), options.output);
307
+ await checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseOutputFormat(options.format), options.output);
528
308
  });
529
309
  ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use lookup theme-id list)").requiredOption("--date <date>", "Date (yyyy-MM-dd)").option("--type <name>", "Report type: morning/night", collectList, []).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
530
310
  const client = await createClient();
531
311
  const typeList = options.type.length ? options.type : undefined;
532
- await printData(await client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList }), parseFormat(options.format), options.output);
312
+ await printData(await client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList }), parseOutputFormat(options.format), options.output);
533
313
  });
534
314
  ai.command("research-outline").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
535
315
  const client = await createClient();
536
- await printData(await client.call("ai.research-outline", { securityCode: options.securityCode }), parseFormat(options.format), options.output);
316
+ await printData(await client.call("ai.research-outline", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
537
317
  });
538
318
  ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--category <name>", "Report type: morningBriefing/noonBriefing/afternoonFlash/eveningBriefing", collectList, []).option("--with-related-securities", "Include related securities info").option("--no-with-related-securities", "Exclude related securities info").option("--with-close-reading", "Include close reading content").option("--no-with-close-reading", "Exclude close reading content").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
539
319
  const client = await createClient();
540
320
  const ALL_CATEGORIES = ["morningBriefing", "noonBriefing", "afternoonFlash", "eveningBriefing"];
541
321
  await printData(await client.call("ai.hot-topic", {
542
- from: Number(options.from),
543
- size: options.size === undefined ? undefined : Number(options.size),
322
+ from: parseFrom(options.from),
323
+ size: parseSize(options.size),
544
324
  startDate: options.startDate,
545
325
  endDate: options.endDate,
546
326
  categoryList: options.category.length > 0 ? options.category : ALL_CATEGORIES,
547
327
  withRelatedSecurities: options.withRelatedSecurities === false ? undefined : true,
548
328
  withCloseReading: options.withCloseReading === false ? undefined : true,
549
- }), parseFormat(options.format), options.output);
329
+ }), parseOutputFormat(options.format), options.output);
550
330
  });
551
331
  ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
552
332
  const client = await createClient();
@@ -554,7 +334,7 @@ ai.command("management-discuss-announcement").requiredOption("--report-date <dat
554
334
  reportDate: options.reportDate,
555
335
  securityCode: options.securityCode,
556
336
  discussionDimension: options.dimension,
557
- }), parseFormat(options.format), options.output);
337
+ }), parseOutputFormat(options.format), options.output);
558
338
  });
559
339
  ai.command("management-discuss-earnings-call").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
560
340
  const client = await createClient();
@@ -562,7 +342,7 @@ ai.command("management-discuss-earnings-call").requiredOption("--report-date <da
562
342
  reportDate: options.reportDate,
563
343
  securityCode: options.securityCode,
564
344
  discussionDimension: options.dimension,
565
- }), parseFormat(options.format), options.output);
345
+ }), parseOutputFormat(options.format), options.output);
566
346
  });
567
347
  ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint text (max 1000 chars)").option("--wait", "Wait for content generation (blocking, up to 3 min)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
568
348
  const client = await createClient();
@@ -579,19 +359,19 @@ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint t
579
359
  return;
580
360
  }
581
361
  process.stderr.write(`Got dataId: ${dataId}, waiting for content generation...\n`);
582
- if (!await pollAsyncContent(client, "ai.viewpoint-debate.get-content", dataId, parseFormat(options.format), options.output)) {
362
+ if (!await pollAsyncContent(client, "ai.viewpoint-debate.get-content", dataId, parseOutputFormat(options.format), options.output)) {
583
363
  process.stderr.write(`Content not available after ${POLL_MAX_ATTEMPTS} attempts. Try again later with: gangtise ai viewpoint-debate-check --data-id ${dataId}\n`);
584
364
  process.exitCode = 1;
585
365
  }
586
366
  });
587
367
  ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId from viewpoint-debate").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
588
368
  const client = await createClient();
589
- await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseFormat(options.format), options.output);
369
+ await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output);
590
370
  });
591
371
  const vault = new Command("vault").description("Vault APIs");
592
372
  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) => {
593
373
  const client = await createClient();
594
- await printData(await client.call("vault.drive.list", { from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), parseFormat(options.format), options.output, { endpointKey: "vault.drive.list", idField: "fileId" });
374
+ await printData(await client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), parseOutputFormat(options.format), options.output, { endpointKey: "vault.drive.list", idField: "fileId" });
595
375
  });
596
376
  vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action(async (options) => {
597
377
  const client = await createClient();
@@ -601,7 +381,7 @@ vault.command("drive-download").requiredOption("--file-id <id>").option("--outpu
601
381
  });
602
382
  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) => {
603
383
  const client = await createClient();
604
- await printData(await client.call("vault.record.list", { from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), parseFormat(options.format), options.output, { endpointKey: "vault.record.list", idField: "recordId" });
384
+ await printData(await client.call("vault.record.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), parseOutputFormat(options.format), options.output, { endpointKey: "vault.record.list", idField: "recordId" });
605
385
  });
606
386
  vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action(async (options) => {
607
387
  const client = await createClient();
@@ -611,7 +391,7 @@ vault.command("record-download").requiredOption("--record-id <id>").requiredOpti
611
391
  });
612
392
  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) => {
613
393
  const client = await createClient();
614
- await printData(await client.call("vault.my-conference.list", { from: Number(options.from), size: options.size === undefined ? undefined : Number(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category) }), parseFormat(options.format), options.output, { endpointKey: "vault.my-conference.list", idField: "conferenceId" });
394
+ await printData(await client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category) }), parseOutputFormat(options.format), options.output, { endpointKey: "vault.my-conference.list", idField: "conferenceId" });
615
395
  });
616
396
  vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action(async (options) => {
617
397
  const client = await createClient();
@@ -619,6 +399,14 @@ vault.command("my-conference-download").requiredOption("--conference-id <id>").r
619
399
  const title = options.output ? undefined : await resolveTitle(client, result, "vault.my-conference.list", "conferenceId", options.conferenceId);
620
400
  await saveDownloadResult(result, `conference-${options.conferenceId}`, options.output ?? title);
621
401
  });
402
+ 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
+ const client = await createClient();
404
+ await printData(await client.call("vault.wechat-message.list", buildWechatMessageListBody(options)), parseOutputFormat(options.format), options.output);
405
+ });
406
+ vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Rows to return", "20").option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
407
+ const client = await createClient();
408
+ await printData(await client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options)), parseOutputFormat(options.format), options.output);
409
+ });
622
410
  program.addCommand(vault);
623
411
  program.addCommand(ai);
624
412
  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) => {
@@ -637,7 +425,7 @@ program.command("raw").description("Raw API calls").addCommand(new Command("call
637
425
  await saveDownloadResult(data, "download.bin", options.output);
638
426
  return;
639
427
  }
640
- await printData(data, parseFormat(options.format), options.output);
428
+ await printData(data, parseOutputFormat(options.format), options.output);
641
429
  }));
642
430
  async function main() {
643
431
  try {
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from "./errors.js";
1
2
  export function splitCsv(value) {
2
3
  return value
3
4
  .split(",")
@@ -7,10 +8,35 @@ export function splitCsv(value) {
7
8
  export function collectList(value, previous = []) {
8
9
  return [...previous, ...splitCsv(value)];
9
10
  }
11
+ export function parseNumberOption(value, optionName, config = {}) {
12
+ if (value === undefined || String(value).trim() === "") {
13
+ throw new ValidationError(`Invalid ${optionName}: expected a number`);
14
+ }
15
+ const parsed = typeof value === "number" ? value : Number(value);
16
+ if (!Number.isFinite(parsed)) {
17
+ throw new ValidationError(`Invalid ${optionName}: expected a finite number`);
18
+ }
19
+ if (config.integer && !Number.isInteger(parsed)) {
20
+ throw new ValidationError(`Invalid ${optionName}: expected an integer`);
21
+ }
22
+ if (config.min !== undefined && parsed < config.min) {
23
+ throw new ValidationError(`Invalid ${optionName}: expected a number >= ${config.min}`);
24
+ }
25
+ return parsed;
26
+ }
27
+ export function parseOptionalNumberOption(value, optionName, config = {}) {
28
+ return value === undefined ? undefined : parseNumberOption(value, optionName, config);
29
+ }
30
+ export function parseFrom(value) {
31
+ return parseNumberOption(value ?? "0", "--from", { integer: true, min: 0 });
32
+ }
33
+ export function parseSize(value) {
34
+ return parseOptionalNumberOption(value, "--size", { integer: true, min: 1 });
35
+ }
10
36
  export function collectNumberList(value, previous = []) {
11
37
  return [
12
38
  ...previous,
13
- ...splitCsv(value).map((item) => Number(item)).filter((item) => !Number.isNaN(item)),
39
+ ...splitCsv(value).map((item) => parseNumberOption(item, "number list item")),
14
40
  ];
15
41
  }
16
42
  export function collectKeyValue(value, previous = {}) {
@@ -44,3 +70,12 @@ export function toTimestamp13(value) {
44
70
  return undefined;
45
71
  return ms;
46
72
  }
73
+ export function parseTimestamp13(value, optionName) {
74
+ if (value === undefined)
75
+ return undefined;
76
+ const parsed = toTimestamp13(value);
77
+ if (parsed === undefined) {
78
+ throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or date string`);
79
+ }
80
+ return parsed;
81
+ }
@@ -0,0 +1,50 @@
1
+ import { ApiError } from "./errors.js";
2
+ import { printData } from "./printer.js";
3
+ export const POLL_MAX_ATTEMPTS = 12;
4
+ export const POLL_DELAY_MS = 15_000;
5
+ function isAsyncPending(error) {
6
+ return error instanceof ApiError && error.code === "410110";
7
+ }
8
+ export async function pollAsyncContent(client, getContentEndpoint, dataId, format, output) {
9
+ for (let attempt = 1; attempt <= POLL_MAX_ATTEMPTS; attempt++) {
10
+ try {
11
+ const result = await client.call(getContentEndpoint, { dataId });
12
+ if (result?.content != null) {
13
+ await printData(result, format, output);
14
+ return true;
15
+ }
16
+ }
17
+ catch (error) {
18
+ if (error instanceof ApiError && error.code === "410111") {
19
+ process.stderr.write("Content generation failed (terminal). Do not retry.\n");
20
+ return false;
21
+ }
22
+ if (!isAsyncPending(error))
23
+ throw error;
24
+ }
25
+ 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));
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ export async function checkAsyncContent(client, getContentEndpoint, dataId, format, output) {
33
+ try {
34
+ const result = await client.call(getContentEndpoint, { dataId });
35
+ if (result?.content != null) {
36
+ await printData(result, format, output);
37
+ return;
38
+ }
39
+ }
40
+ catch (error) {
41
+ if (error instanceof ApiError && error.code === "410111") {
42
+ process.stderr.write("Content generation failed (terminal). Do not retry.\n");
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ if (!isAsyncPending(error))
47
+ throw error;
48
+ }
49
+ process.stdout.write(`${JSON.stringify({ dataId, status: "pending", hint: "Content not ready yet, retry in ~2 minutes" })}\n`);
50
+ }
@@ -37,8 +37,18 @@ export class GangtiseClient {
37
37
  });
38
38
  return accessToken;
39
39
  }
40
+ isEnvelope(parsed) {
41
+ return Boolean(parsed && typeof parsed === 'object' && 'code' in parsed);
42
+ }
43
+ throwHttpError(parsed, statusCode) {
44
+ if (this.isEnvelope(parsed)) {
45
+ const code = parsed.code === undefined ? undefined : String(parsed.code);
46
+ throw new ApiError(parsed.msg || `API request failed (HTTP ${statusCode})`, code, statusCode, parsed);
47
+ }
48
+ throw new ApiError(`API request failed (HTTP ${statusCode})`, undefined, statusCode, parsed);
49
+ }
40
50
  unwrapEnvelope(parsed, statusCode) {
41
- if (!parsed || typeof parsed !== 'object' || !('code' in parsed)) {
51
+ if (!this.isEnvelope(parsed)) {
42
52
  return parsed;
43
53
  }
44
54
  const code = parsed.code === undefined ? undefined : String(parsed.code);
@@ -167,15 +177,18 @@ export class GangtiseClient {
167
177
  bodyTimeout: this.config.timeoutMs,
168
178
  });
169
179
  const text = await response.body.text();
170
- if (response.statusCode >= 500) {
171
- throw new ApiError(`Server error (HTTP ${response.statusCode})`, undefined, response.statusCode, text.slice(0, 500));
172
- }
173
180
  let parsed;
174
181
  try {
175
182
  parsed = JSON.parse(text);
176
183
  }
177
184
  catch {
178
- throw new ApiError('Failed to parse API response', undefined, response.statusCode, text.slice(0, 500));
185
+ const message = response.statusCode >= 400
186
+ ? `API request failed (HTTP ${response.statusCode})`
187
+ : 'Failed to parse API response';
188
+ throw new ApiError(message, undefined, response.statusCode, text.slice(0, 500));
189
+ }
190
+ if (response.statusCode >= 400) {
191
+ this.throwHttpError(parsed, response.statusCode);
179
192
  }
180
193
  return this.unwrapEnvelope(parsed, response.statusCode);
181
194
  }
@@ -206,6 +219,9 @@ export class GangtiseClient {
206
219
  }
207
220
  return { text, contentType };
208
221
  }
222
+ if (response.statusCode >= 400) {
223
+ this.throwHttpError(parsed, response.statusCode);
224
+ }
209
225
  const data = this.unwrapEnvelope(parsed, response.statusCode);
210
226
  if (data && typeof data === 'object' && 'url' in data && typeof data.url === 'string') {
211
227
  return { url: String(data.url), contentType };
@@ -0,0 +1,30 @@
1
+ import { maybeArray, parseFrom, parseOptionalNumberOption, parseSize } from "./args.js";
2
+ export function buildQuoteKlineBody(options) {
3
+ return {
4
+ securityList: maybeArray(options.security),
5
+ startDate: options.startDate,
6
+ endDate: options.endDate,
7
+ limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
8
+ fieldList: maybeArray(options.field),
9
+ };
10
+ }
11
+ export function buildWechatMessageListBody(options) {
12
+ return {
13
+ from: parseFrom(options.from),
14
+ size: parseSize(options.size),
15
+ startTime: options.startTime,
16
+ endTime: options.endTime,
17
+ keyword: options.keyword,
18
+ wechatGroupIdList: maybeArray(options.wechatGroupId),
19
+ industryIdList: maybeArray(options.industry),
20
+ categoryList: maybeArray(options.category),
21
+ tagList: maybeArray(options.tag),
22
+ };
23
+ }
24
+ export function buildWechatChatroomListBody(options) {
25
+ return {
26
+ from: parseFrom(options.from),
27
+ size: parseSize(options.size),
28
+ roomName: options.roomName.length > 0 ? options.roomName.join(",") : undefined,
29
+ };
30
+ }
@@ -8,7 +8,7 @@ export function loadConfig() {
8
8
  const timeoutMs = timeoutValue ? Number(timeoutValue) : DEFAULT_TIMEOUT_MS;
9
9
  return {
10
10
  baseUrl: process.env.GANGTISE_BASE_URL ?? DEFAULT_BASE_URL,
11
- timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : DEFAULT_TIMEOUT_MS,
11
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS,
12
12
  accessKey: process.env.GANGTISE_ACCESS_KEY,
13
13
  secretKey: process.env.GANGTISE_SECRET_KEY,
14
14
  token: process.env.GANGTISE_TOKEN,
@@ -0,0 +1,96 @@
1
+ import { extname } from "node:path";
2
+ import { DownloadError } from "./errors.js";
3
+ import { saveOutputIfNeeded } from "./output.js";
4
+ import { lookupTitleCache, readTitleCache, TITLE_LOOKUP_SIZE } from "./titleCache.js";
5
+ const MIME_EXT = {
6
+ "application/pdf": ".pdf",
7
+ "application/msword": ".doc",
8
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
9
+ "application/vnd.ms-excel": ".xls",
10
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
11
+ "application/vnd.ms-powerpoint": ".ppt",
12
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
13
+ "application/zip": ".zip",
14
+ "application/x-rar-compressed": ".rar",
15
+ "application/gzip": ".gz",
16
+ "application/x-7z-compressed": ".7z",
17
+ "application/json": ".json",
18
+ "application/xml": ".xml",
19
+ "text/plain": ".txt",
20
+ "text/html": ".html",
21
+ "text/csv": ".csv",
22
+ "image/png": ".png",
23
+ "image/jpeg": ".jpg",
24
+ "image/gif": ".gif",
25
+ "image/webp": ".webp",
26
+ "image/svg+xml": ".svg",
27
+ "audio/mpeg": ".mp3",
28
+ "video/mp4": ".mp4",
29
+ "application/octet-stream": ".bin",
30
+ };
31
+ export function extFromContentType(contentType) {
32
+ if (!contentType)
33
+ return "";
34
+ const mime = contentType.split(";")[0].trim().toLowerCase();
35
+ return MIME_EXT[mime] ?? "";
36
+ }
37
+ export async function resolveTitle(client, result, listEndpoint, idField, idValue, titleField = "title") {
38
+ const file = result;
39
+ const serverExt = file.filename ? extname(file.filename) : extFromContentType(file.contentType);
40
+ function buildFilename(rawTitle) {
41
+ let title = rawTitle.replace(/[/\\:*?"<>|]/g, "_").trim();
42
+ if (serverExt && !title.toLowerCase().endsWith(serverExt.toLowerCase())) {
43
+ title += serverExt;
44
+ }
45
+ return title;
46
+ }
47
+ try {
48
+ const cacheData = await readTitleCache();
49
+ const cached = lookupTitleCache(cacheData, listEndpoint, idValue);
50
+ if (cached)
51
+ return buildFilename(cached);
52
+ }
53
+ catch {
54
+ // Ignore corrupt cache data and fall back to the list endpoint.
55
+ }
56
+ try {
57
+ const resp = await client.call(listEndpoint, { from: 0, size: TITLE_LOOKUP_SIZE });
58
+ const items = Array.isArray(resp) ? resp : (resp.list ?? []);
59
+ const match = items.find(item => String(item[idField]) === String(idValue));
60
+ const rawTitle = match?.[titleField];
61
+ if (typeof rawTitle === "string" && rawTitle)
62
+ return buildFilename(rawTitle);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ return undefined;
68
+ }
69
+ export async function saveDownloadResult(result, fallbackName, output) {
70
+ if (!(result && typeof result === "object")) {
71
+ throw new DownloadError("Unexpected download response");
72
+ }
73
+ const file = result;
74
+ if (file.data instanceof Uint8Array) {
75
+ const outputPath = output ?? file.filename ?? (fallbackName + extFromContentType(file.contentType));
76
+ await saveOutputIfNeeded(file.data, outputPath);
77
+ process.stdout.write(`${outputPath}\n`);
78
+ return;
79
+ }
80
+ if (typeof file.text === "string") {
81
+ const outputPath = output ?? `${fallbackName}.txt`;
82
+ await saveOutputIfNeeded(file.text, outputPath);
83
+ process.stdout.write(`${outputPath}\n`);
84
+ return;
85
+ }
86
+ if (typeof file.url === "string") {
87
+ if (output) {
88
+ await saveOutputIfNeeded(file.url, output);
89
+ process.stdout.write(`${output}\n`);
90
+ return;
91
+ }
92
+ process.stdout.write(`${file.url}\n`);
93
+ return;
94
+ }
95
+ throw new DownloadError("Unexpected download response");
96
+ }
@@ -176,6 +176,13 @@ export const ENDPOINTS = {
176
176
  kind: "json",
177
177
  description: "Query HK stock daily kline (HK)",
178
178
  },
179
+ quoteIndexDayKline: {
180
+ key: "quote.index-day-kline",
181
+ method: "POST",
182
+ path: "/application/open-quote/index/kline/daily",
183
+ kind: "json",
184
+ description: "Query SH/SZ/BJ index daily kline",
185
+ },
179
186
  fundamentalIncomeStatement: {
180
187
  key: "fundamental.income-statement",
181
188
  method: "POST",
@@ -398,6 +405,21 @@ export const ENDPOINTS = {
398
405
  kind: "download",
399
406
  description: "Download my conference resource",
400
407
  },
408
+ vaultWechatMessageList: {
409
+ key: "vault.wechat-message.list",
410
+ method: "POST",
411
+ path: "/application/open-vault/wechatgroupmsg/list",
412
+ kind: "json",
413
+ description: "List WeChat group messages",
414
+ pagination: { enabled: true, maxPageSize: 50 },
415
+ },
416
+ vaultWechatChatroomList: {
417
+ key: "vault.wechat-chatroom.list",
418
+ method: "POST",
419
+ path: "/application/open-vault/wechatgroupmsg/chatroomId",
420
+ kind: "json",
421
+ description: "List WeChat group chatroom IDs",
422
+ },
401
423
  };
402
424
  export const ENDPOINT_REGISTRY = Object.values(ENDPOINTS).reduce((accumulator, endpoint) => {
403
425
  accumulator[endpoint.key] = endpoint;
@@ -24,5 +24,10 @@ export function normalizeRows(value) {
24
24
  const hasMeta = Object.keys(meta).length > 0;
25
25
  return hasMeta ? { ...meta, list } : list;
26
26
  }
27
+ if (Array.isArray(record.chatRoomList)) {
28
+ const { chatRoomList, ...meta } = record;
29
+ const hasMeta = Object.keys(meta).length > 0;
30
+ return hasMeta ? { ...meta, list: chatRoomList } : chatRoomList;
31
+ }
27
32
  return value;
28
33
  }
@@ -1,4 +1,13 @@
1
1
  import fs from "node:fs/promises";
2
+ import { ConfigError } from "./errors.js";
3
+ const OUTPUT_FORMATS = ["table", "json", "jsonl", "csv", "markdown"];
4
+ export function parseOutputFormat(value) {
5
+ const format = value ?? "table";
6
+ if (OUTPUT_FORMATS.includes(format)) {
7
+ return format;
8
+ }
9
+ throw new ConfigError(`Unsupported format: ${format}`);
10
+ }
2
11
  function formatScalar(value) {
3
12
  if (value === null || value === undefined) {
4
13
  return "";
@@ -0,0 +1,31 @@
1
+ import { normalizeRows } from "./normalize.js";
2
+ import { renderOutput, saveOutputIfNeeded } from "./output.js";
3
+ import { extractTitles, writeTitleCache } from "./titleCache.js";
4
+ export async function printData(data, format, output, cache) {
5
+ const normalized = normalizeRows(data);
6
+ const items = Array.isArray(normalized)
7
+ ? normalized
8
+ : (normalized && typeof normalized === "object" && Array.isArray(normalized.list))
9
+ ? normalized.list
10
+ : null;
11
+ if (cache && items) {
12
+ const titles = extractTitles(items, cache);
13
+ if (Object.keys(titles).length > 0) {
14
+ writeTitleCache(cache.endpointKey, titles).catch(() => { });
15
+ }
16
+ }
17
+ if (normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
18
+ const meta = normalized;
19
+ if (typeof meta.total === "number" && format !== "json") {
20
+ const listLen = Array.isArray(meta.list) ? meta.list.length : 0;
21
+ process.stderr.write(`Total: ${meta.total}, showing: ${listLen}\n`);
22
+ }
23
+ }
24
+ const content = renderOutput(normalized, format);
25
+ if (output) {
26
+ await saveOutputIfNeeded(content, output);
27
+ process.stdout.write(`${output}\n`);
28
+ return;
29
+ }
30
+ process.stdout.write(`${content}\n`);
31
+ }
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export const DEFAULT_TITLE_CACHE_PATH = path.join(os.homedir(), ".config", "gangtise", "title-cache.json");
5
+ export const TITLE_LOOKUP_SIZE = 200;
6
+ const TITLE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ export async function readTitleCache(filePath = DEFAULT_TITLE_CACHE_PATH) {
8
+ try {
9
+ const content = await fs.readFile(filePath, "utf8");
10
+ const parsed = JSON.parse(content);
11
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
12
+ return parsed;
13
+ }
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ return {};
19
+ }
20
+ export async function writeTitleCache(endpoint, titles, filePath = DEFAULT_TITLE_CACHE_PATH) {
21
+ const data = await readTitleCache(filePath);
22
+ data[endpoint] = { titles, ts: Date.now() };
23
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
24
+ await fs.writeFile(filePath, JSON.stringify(data), { encoding: "utf8", mode: 0o600 });
25
+ }
26
+ export function lookupTitleCache(data, endpoint, id) {
27
+ const entry = data[endpoint];
28
+ if (!entry || Date.now() - entry.ts > TITLE_CACHE_TTL_MS)
29
+ return undefined;
30
+ return entry.titles[id];
31
+ }
32
+ export function extractTitles(items, cache) {
33
+ const titleField = cache.titleField ?? "title";
34
+ const titles = {};
35
+ for (const row of items) {
36
+ if (!row || typeof row !== "object")
37
+ continue;
38
+ const record = row;
39
+ const id = record[cache.idField];
40
+ const title = record[titleField];
41
+ if (id != null && typeof title === "string" && title) {
42
+ titles[String(id)] = title;
43
+ }
44
+ }
45
+ return titles;
46
+ }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.10.9";
2
+ export const CLI_VERSION = "0.11.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.10.9",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for Gangtise OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {