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 +25 -2
- package/dist/src/cli.js +89 -301
- package/dist/src/core/args.js +36 -1
- package/dist/src/core/asyncContent.js +50 -0
- package/dist/src/core/client.js +21 -5
- package/dist/src/core/commandBodies.js +30 -0
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/download.js +96 -0
- package/dist/src/core/endpoints.js +22 -0
- package/dist/src/core/normalize.js +5 -0
- package/dist/src/core/output.js +9 -0
- package/dist/src/core/printer.js +31 -0
- package/dist/src/core/titleCache.js +46 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
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 {
|
|
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 {
|
|
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(),
|
|
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 },
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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"),
|
|
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:
|
|
318
|
-
rankType:
|
|
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
|
-
}),
|
|
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:
|
|
327
|
-
searchType:
|
|
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
|
-
}),
|
|
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:
|
|
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
|
-
}),
|
|
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:
|
|
355
|
-
searchType:
|
|
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
|
|
359
|
-
maxReportPages: options.maxPages
|
|
360
|
-
}),
|
|
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:
|
|
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:
|
|
372
|
-
searchType:
|
|
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
|
|
377
|
-
}),
|
|
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:
|
|
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:
|
|
389
|
-
startTime:
|
|
390
|
-
searchType:
|
|
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
|
-
}),
|
|
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:
|
|
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",
|
|
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",
|
|
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
|
|
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) }),
|
|
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) }),
|
|
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) }),
|
|
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) }),
|
|
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) }),
|
|
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) }),
|
|
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
|
|
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,
|
|
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 }),
|
|
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) }),
|
|
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:
|
|
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:
|
|
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:
|
|
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 }),
|
|
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 }),
|
|
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 }),
|
|
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,
|
|
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,
|
|
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 }),
|
|
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 }),
|
|
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:
|
|
543
|
-
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
|
-
}),
|
|
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
|
-
}),
|
|
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
|
-
}),
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
428
|
+
await printData(data, parseOutputFormat(options.format), options.output);
|
|
641
429
|
}));
|
|
642
430
|
async function main() {
|
|
643
431
|
try {
|
package/dist/src/core/args.js
CHANGED
|
@@ -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) =>
|
|
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
|
+
}
|
package/dist/src/core/client.js
CHANGED
|
@@ -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 (!
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/src/core/config.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/core/output.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated — DO NOT EDIT
|
|
2
|
-
export const CLI_VERSION = "0.
|
|
2
|
+
export const CLI_VERSION = "0.11.0";
|