gangtise-openapi-cli 0.10.9 → 0.10.10
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 +16 -2
- package/dist/src/cli.js +76 -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/config.js +1 -1
- package/dist/src/core/download.js +96 -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
|
@@ -193,6 +193,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
193
193
|
- **有时间范围时**(传了 `--start-time/--end-time` 或 `--start-date/--end-date`):**省略 `--size`**,CLI 自动翻页查全
|
|
194
194
|
- **无时间范围时**(未传时间参数):默认 `--size 200`,防止一次查询数据量过大
|
|
195
195
|
- 如果显式传了 `--size`,则按指定值翻页,直到达到 `size` 或数据取完
|
|
196
|
+
- `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
|
|
196
197
|
- 安全上限:自动翻页最多 1000 页,防止异常循环
|
|
197
198
|
- 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`),同时 stderr 输出 `Total: N, showing: M`
|
|
198
199
|
|
|
@@ -362,10 +363,23 @@ gangtise raw call insight.opinion.list --body '{"from":0,"size":120}'
|
|
|
362
363
|
|
|
363
364
|
所有格式均支持 `--output <path>` 输出到文件(自动创建父目录)。
|
|
364
365
|
|
|
366
|
+
## 参数校验
|
|
367
|
+
|
|
368
|
+
CLI 会在本地校验常见数值参数,避免把明显非法的请求发到 API:
|
|
369
|
+
|
|
370
|
+
- `--from`:非负整数
|
|
371
|
+
- `--size` / `--limit` / `--top`:正整数
|
|
372
|
+
- `--file-type` / `--resource-type` 以及数值型列表参数:有限数字
|
|
373
|
+
- 公告 `--start-time` / `--end-time`:可解析的时间字符串或 Unix 时间戳
|
|
374
|
+
|
|
375
|
+
校验失败会输出 `ValidationError: Invalid ...` 并以非 0 状态退出。
|
|
376
|
+
|
|
365
377
|
## 常见错误
|
|
366
378
|
|
|
367
|
-
|
|
|
368
|
-
|
|
379
|
+
| 错误/错误码 | 说明 |
|
|
380
|
+
|-----------|------|
|
|
381
|
+
| `ValidationError` | 本地参数校验失败,检查 `--size` / `--limit` / `--from` / `--file-type` 等数值参数 |
|
|
382
|
+
| `API error (HTTP 4xx/5xx)` | HTTP 层失败;CLI 会把 4xx/5xx 响应视为错误,即使响应体不是标准 `{code,msg,data}` 信封 |
|
|
369
383
|
| `8000014` | `GANGTISE_ACCESS_KEY` 错误 |
|
|
370
384
|
| `8000015` | `GANGTISE_SECRET_KEY` 错误 |
|
|
371
385
|
| `8000016` | 开发账号状态异常 |
|
package/dist/src/cli.js
CHANGED
|
@@ -1,244 +1,19 @@
|
|
|
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";
|
|
4
6
|
import { loadConfig } from "./core/config.js";
|
|
5
|
-
import {
|
|
7
|
+
import { resolveTitle, saveDownloadResult } from "./core/download.js";
|
|
8
|
+
import { ApiError, ConfigError } from "./core/errors.js";
|
|
9
|
+
import { normalizeRows } from "./core/normalize.js";
|
|
10
|
+
import { parseOutputFormat } from "./core/output.js";
|
|
11
|
+
import { printData } from "./core/printer.js";
|
|
6
12
|
// --- Lazy-loaded modules (deferred to action handlers) ---
|
|
7
13
|
async function createClient() {
|
|
8
14
|
const { GangtiseClient } = await import("./core/client.js");
|
|
9
15
|
return new GangtiseClient(loadConfig());
|
|
10
16
|
}
|
|
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
17
|
function addTimeFilters(command) {
|
|
243
18
|
return command
|
|
244
19
|
.option("--from <number>", "Starting offset", "0")
|
|
@@ -257,48 +32,48 @@ program
|
|
|
257
32
|
.option("--format <format>", "Output format", "json")
|
|
258
33
|
.action(async (options) => {
|
|
259
34
|
const client = await createClient();
|
|
260
|
-
await printData(await client.login(),
|
|
35
|
+
await printData(await client.login(), parseOutputFormat(options.format));
|
|
261
36
|
}))
|
|
262
37
|
.addCommand(new Command("status")
|
|
263
38
|
.option("--format <format>", "Output format", "json")
|
|
264
39
|
.action(async (options) => {
|
|
265
40
|
const config = loadConfig();
|
|
266
41
|
const cache = await readTokenCache(config.tokenCachePath);
|
|
267
|
-
await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache },
|
|
42
|
+
await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache }, parseOutputFormat(options.format));
|
|
268
43
|
}));
|
|
269
44
|
const lookup = new Command("lookup").description("Lookup helper APIs");
|
|
270
45
|
lookup
|
|
271
46
|
.addCommand(new Command("research-area").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
|
|
272
47
|
const client = await createClient();
|
|
273
|
-
await printData(await client.call("lookup.research-areas.list"),
|
|
48
|
+
await printData(await client.call("lookup.research-areas.list"), parseOutputFormat(options.format));
|
|
274
49
|
})))
|
|
275
50
|
.addCommand(new Command("broker-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
|
|
276
51
|
const client = await createClient();
|
|
277
|
-
await printData(await client.call("lookup.broker-orgs.list"),
|
|
52
|
+
await printData(await client.call("lookup.broker-orgs.list"), parseOutputFormat(options.format));
|
|
278
53
|
})))
|
|
279
54
|
.addCommand(new Command("meeting-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
|
|
280
55
|
const client = await createClient();
|
|
281
|
-
await printData(await client.call("lookup.meeting-orgs.list"),
|
|
56
|
+
await printData(await client.call("lookup.meeting-orgs.list"), parseOutputFormat(options.format));
|
|
282
57
|
})))
|
|
283
58
|
.addCommand(new Command("industry").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
|
|
284
59
|
const client = await createClient();
|
|
285
|
-
await printData(await client.call("lookup.industries.list"),
|
|
60
|
+
await printData(await client.call("lookup.industries.list"), parseOutputFormat(options.format));
|
|
286
61
|
})))
|
|
287
62
|
.addCommand(new Command("region").description("Foreign report region codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
|
|
288
63
|
const client = await createClient();
|
|
289
|
-
await printData(await client.call("lookup.regions.list"),
|
|
64
|
+
await printData(await client.call("lookup.regions.list"), parseOutputFormat(options.format));
|
|
290
65
|
})))
|
|
291
66
|
.addCommand(new Command("announcement-category").description("Announcement category codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
|
|
292
67
|
const client = await createClient();
|
|
293
|
-
await printData(await client.call("lookup.announcement-categories.list"),
|
|
68
|
+
await printData(await client.call("lookup.announcement-categories.list"), parseOutputFormat(options.format));
|
|
294
69
|
})))
|
|
295
70
|
.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
71
|
const client = await createClient();
|
|
297
|
-
await printData(await client.call("lookup.industry-codes.list"),
|
|
72
|
+
await printData(await client.call("lookup.industry-codes.list"), parseOutputFormat(options.format));
|
|
298
73
|
})))
|
|
299
74
|
.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
75
|
const client = await createClient();
|
|
301
|
-
await printData(await client.call("lookup.theme-ids.list"),
|
|
76
|
+
await printData(await client.call("lookup.theme-ids.list"), parseOutputFormat(options.format));
|
|
302
77
|
})));
|
|
303
78
|
program.addCommand(lookup);
|
|
304
79
|
const insight = new Command("insight").description("Insight APIs");
|
|
@@ -314,20 +89,20 @@ const announcement = new Command("announcement");
|
|
|
314
89
|
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
90
|
const client = await createClient();
|
|
316
91
|
await printData(await client.call("insight.opinion.list", {
|
|
317
|
-
from:
|
|
318
|
-
rankType:
|
|
92
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
93
|
+
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
|
|
319
94
|
securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
|
|
320
95
|
llmTagList: maybeArray(options.llmTag), sourceList: maybeArray(options.source),
|
|
321
|
-
}),
|
|
96
|
+
}), parseOutputFormat(options.format), options.output);
|
|
322
97
|
});
|
|
323
98
|
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
99
|
const client = await createClient();
|
|
325
100
|
await printData(await client.call("insight.summary.list", {
|
|
326
|
-
from:
|
|
327
|
-
searchType:
|
|
101
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
102
|
+
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
103
|
researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
|
|
329
104
|
categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
|
|
330
|
-
}),
|
|
105
|
+
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.summary.list", idField: "summaryId" });
|
|
331
106
|
});
|
|
332
107
|
summary.command("download").requiredOption("--summary-id <id>").option("--output <path>").action(async (options) => {
|
|
333
108
|
const client = await createClient();
|
|
@@ -338,11 +113,11 @@ summary.command("download").requiredOption("--summary-id <id>").option("--output
|
|
|
338
113
|
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
114
|
const client = await createClient();
|
|
340
115
|
await printData(await client.call(endpointKey, {
|
|
341
|
-
from:
|
|
116
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
|
|
342
117
|
researchAreaList: maybeArray(options.researchArea), institutionList: maybeArray(options.institution), securityList: maybeArray(options.security),
|
|
343
118
|
categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
|
|
344
119
|
brokerTypeList: maybeArray(options.brokerType), objectList: maybeArray(options.object), permission: options.permission.length ? options.permission : undefined,
|
|
345
|
-
}),
|
|
120
|
+
}), parseOutputFormat(options.format), options.output);
|
|
346
121
|
});
|
|
347
122
|
addScheduleList(roadshow, "insight.roadshow.list");
|
|
348
123
|
addScheduleList(siteVisit, "insight.site-visit.list");
|
|
@@ -351,49 +126,49 @@ addScheduleList(forum, "insight.forum.list");
|
|
|
351
126
|
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
127
|
const client = await createClient();
|
|
353
128
|
await printData(await client.call("insight.research.list", {
|
|
354
|
-
from:
|
|
355
|
-
searchType:
|
|
129
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
|
|
130
|
+
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
356
131
|
brokerList: maybeArray(options.broker), securityList: maybeArray(options.security), industryList: maybeArray(options.industry),
|
|
357
132
|
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
|
-
}),
|
|
133
|
+
ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
|
|
134
|
+
maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
|
|
135
|
+
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.research.list", idField: "reportId" });
|
|
361
136
|
});
|
|
362
137
|
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
138
|
const client = await createClient();
|
|
364
|
-
const result = await client.call("insight.research.download", undefined, { reportId: options.reportId, fileType:
|
|
139
|
+
const result = await client.call("insight.research.download", undefined, { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
|
|
365
140
|
const title = options.output ? undefined : await resolveTitle(client, result, "insight.research.list", "reportId", options.reportId);
|
|
366
141
|
await saveDownloadResult(result, `research-${options.reportId}`, options.output ?? title);
|
|
367
142
|
});
|
|
368
143
|
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
144
|
const client = await createClient();
|
|
370
145
|
await printData(await client.call("insight.foreign-report.list", {
|
|
371
|
-
from:
|
|
372
|
-
searchType:
|
|
146
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
|
|
147
|
+
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
373
148
|
securityList: maybeArray(options.security), regionList: maybeArray(options.region), categoryList: maybeArray(options.category),
|
|
374
149
|
industryList: maybeArray(options.industry), brokerList: maybeArray(options.broker), llmTagList: maybeArray(options.llmTag),
|
|
375
150
|
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
376
|
-
minReportPages: options.minPages
|
|
377
|
-
}),
|
|
151
|
+
minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
|
|
152
|
+
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.foreign-report.list", idField: "reportId" });
|
|
378
153
|
});
|
|
379
154
|
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
155
|
const client = await createClient();
|
|
381
|
-
const result = await client.call("insight.foreign-report.download", undefined, { reportId: options.reportId, fileType:
|
|
156
|
+
const result = await client.call("insight.foreign-report.download", undefined, { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
|
|
382
157
|
const title = options.output ? undefined : await resolveTitle(client, result, "insight.foreign-report.list", "reportId", options.reportId);
|
|
383
158
|
await saveDownloadResult(result, `foreign-report-${options.reportId}`, options.output ?? title);
|
|
384
159
|
});
|
|
385
160
|
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
161
|
const client = await createClient();
|
|
387
162
|
await printData(await client.call("insight.announcement.list", {
|
|
388
|
-
from:
|
|
389
|
-
startTime:
|
|
390
|
-
searchType:
|
|
163
|
+
from: parseFrom(options.from), size: parseSize(options.size),
|
|
164
|
+
startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
|
|
165
|
+
searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
|
|
391
166
|
securityList: maybeArray(options.security), announcementTypeList: maybeArray(options.announcementType), categoryList: maybeArray(options.category),
|
|
392
|
-
}),
|
|
167
|
+
}), parseOutputFormat(options.format), options.output, { endpointKey: "insight.announcement.list", idField: "announcementId" });
|
|
393
168
|
});
|
|
394
169
|
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
170
|
const client = await createClient();
|
|
396
|
-
const result = await client.call("insight.announcement.download", undefined, { announcementId: options.announcementId, fileType:
|
|
171
|
+
const result = await client.call("insight.announcement.download", undefined, { announcementId: options.announcementId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) });
|
|
397
172
|
const title = options.output ? undefined : await resolveTitle(client, result, "insight.announcement.list", "announcementId", options.announcementId);
|
|
398
173
|
await saveDownloadResult(result, `announcement-${options.announcementId}`, options.output ?? title);
|
|
399
174
|
});
|
|
@@ -410,45 +185,45 @@ program.addCommand(insight);
|
|
|
410
185
|
const quote = new Command("quote").description("Quote APIs");
|
|
411
186
|
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
187
|
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
|
|
188
|
+
await printData(await client.call("quote.day-kline", { securityList: maybeArray(options.security), startDate: options.startDate, endDate: options.endDate, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
414
189
|
});
|
|
415
190
|
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
191
|
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
|
|
192
|
+
await printData(await client.call("quote.day-kline-hk", { securityList: maybeArray(options.security), startDate: options.startDate, endDate: options.endDate, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
|
|
418
193
|
});
|
|
419
194
|
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
195
|
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
|
|
196
|
+
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
197
|
});
|
|
423
198
|
program.addCommand(quote);
|
|
424
199
|
const fundamental = new Command("fundamental").description("Fundamental APIs");
|
|
425
200
|
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
201
|
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) }),
|
|
202
|
+
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
203
|
});
|
|
429
204
|
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
205
|
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) }),
|
|
206
|
+
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
207
|
});
|
|
433
208
|
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
209
|
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) }),
|
|
210
|
+
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
211
|
});
|
|
437
212
|
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
213
|
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) }),
|
|
214
|
+
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
215
|
});
|
|
441
216
|
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
217
|
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) }),
|
|
218
|
+
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
219
|
});
|
|
445
220
|
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
221
|
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) }),
|
|
222
|
+
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
223
|
});
|
|
449
224
|
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
225
|
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
|
|
226
|
+
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
227
|
if (options.skipNull) {
|
|
453
228
|
const normalized = await normalizeRows(data);
|
|
454
229
|
if (normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
@@ -464,43 +239,43 @@ fundamental.command("valuation-analysis").requiredOption("--security-code <code>
|
|
|
464
239
|
}
|
|
465
240
|
}
|
|
466
241
|
}
|
|
467
|
-
await printData(data,
|
|
242
|
+
await printData(data, parseOutputFormat(options.format), options.output);
|
|
468
243
|
});
|
|
469
244
|
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
245
|
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 }),
|
|
246
|
+
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
247
|
});
|
|
473
248
|
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
249
|
const client = await createClient();
|
|
475
250
|
const endDate = options.endDate ?? new Date().toISOString().slice(0, 10);
|
|
476
251
|
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) }),
|
|
252
|
+
await printData(await client.call("fundamental.earning-forecast", { securityCode: options.securityCode, startDate, endDate, consensusList: maybeArray(options.consensus) }), parseOutputFormat(options.format), options.output);
|
|
478
253
|
});
|
|
479
254
|
program.addCommand(fundamental);
|
|
480
255
|
const ai = new Command("ai").description("AI APIs");
|
|
481
256
|
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
257
|
const client = await createClient();
|
|
483
|
-
await printData(await client.call("ai.knowledge-batch", { queries: options.query, top:
|
|
258
|
+
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
259
|
});
|
|
485
260
|
ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action(async (options) => {
|
|
486
261
|
const client = await createClient();
|
|
487
|
-
await saveDownloadResult(await client.call("ai.knowledge-resource.download", undefined, { resourceType:
|
|
262
|
+
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
263
|
});
|
|
489
264
|
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
265
|
const client = await createClient();
|
|
491
|
-
await printData(await client.call("ai.security-clue.list", { from:
|
|
266
|
+
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
267
|
});
|
|
493
268
|
ai.command("one-pager").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
|
|
494
269
|
const client = await createClient();
|
|
495
|
-
await printData(await client.call("ai.one-pager", { securityCode: options.securityCode }),
|
|
270
|
+
await printData(await client.call("ai.one-pager", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
|
|
496
271
|
});
|
|
497
272
|
ai.command("investment-logic").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
|
|
498
273
|
const client = await createClient();
|
|
499
|
-
await printData(await client.call("ai.investment-logic", { securityCode: options.securityCode }),
|
|
274
|
+
await printData(await client.call("ai.investment-logic", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
|
|
500
275
|
});
|
|
501
276
|
ai.command("peer-comparison").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
|
|
502
277
|
const client = await createClient();
|
|
503
|
-
await printData(await client.call("ai.peer-comparison", { securityCode: options.securityCode }),
|
|
278
|
+
await printData(await client.call("ai.peer-comparison", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
|
|
504
279
|
});
|
|
505
280
|
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
281
|
const client = await createClient();
|
|
@@ -517,36 +292,36 @@ ai.command("earnings-review").requiredOption("--security-code <code>").requiredO
|
|
|
517
292
|
return;
|
|
518
293
|
}
|
|
519
294
|
process.stderr.write(`Got dataId: ${dataId}, waiting for content generation...\n`);
|
|
520
|
-
if (!await pollAsyncContent(client, "ai.earnings-review.get-content", dataId,
|
|
295
|
+
if (!await pollAsyncContent(client, "ai.earnings-review.get-content", dataId, parseOutputFormat(options.format), options.output)) {
|
|
521
296
|
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
297
|
process.exitCode = 1;
|
|
523
298
|
}
|
|
524
299
|
});
|
|
525
300
|
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
301
|
const client = await createClient();
|
|
527
|
-
await checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId,
|
|
302
|
+
await checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseOutputFormat(options.format), options.output);
|
|
528
303
|
});
|
|
529
304
|
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
305
|
const client = await createClient();
|
|
531
306
|
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 }),
|
|
307
|
+
await printData(await client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList }), parseOutputFormat(options.format), options.output);
|
|
533
308
|
});
|
|
534
309
|
ai.command("research-outline").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
|
|
535
310
|
const client = await createClient();
|
|
536
|
-
await printData(await client.call("ai.research-outline", { securityCode: options.securityCode }),
|
|
311
|
+
await printData(await client.call("ai.research-outline", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
|
|
537
312
|
});
|
|
538
313
|
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
314
|
const client = await createClient();
|
|
540
315
|
const ALL_CATEGORIES = ["morningBriefing", "noonBriefing", "afternoonFlash", "eveningBriefing"];
|
|
541
316
|
await printData(await client.call("ai.hot-topic", {
|
|
542
|
-
from:
|
|
543
|
-
size:
|
|
317
|
+
from: parseFrom(options.from),
|
|
318
|
+
size: parseSize(options.size),
|
|
544
319
|
startDate: options.startDate,
|
|
545
320
|
endDate: options.endDate,
|
|
546
321
|
categoryList: options.category.length > 0 ? options.category : ALL_CATEGORIES,
|
|
547
322
|
withRelatedSecurities: options.withRelatedSecurities === false ? undefined : true,
|
|
548
323
|
withCloseReading: options.withCloseReading === false ? undefined : true,
|
|
549
|
-
}),
|
|
324
|
+
}), parseOutputFormat(options.format), options.output);
|
|
550
325
|
});
|
|
551
326
|
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
327
|
const client = await createClient();
|
|
@@ -554,7 +329,7 @@ ai.command("management-discuss-announcement").requiredOption("--report-date <dat
|
|
|
554
329
|
reportDate: options.reportDate,
|
|
555
330
|
securityCode: options.securityCode,
|
|
556
331
|
discussionDimension: options.dimension,
|
|
557
|
-
}),
|
|
332
|
+
}), parseOutputFormat(options.format), options.output);
|
|
558
333
|
});
|
|
559
334
|
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
335
|
const client = await createClient();
|
|
@@ -562,7 +337,7 @@ ai.command("management-discuss-earnings-call").requiredOption("--report-date <da
|
|
|
562
337
|
reportDate: options.reportDate,
|
|
563
338
|
securityCode: options.securityCode,
|
|
564
339
|
discussionDimension: options.dimension,
|
|
565
|
-
}),
|
|
340
|
+
}), parseOutputFormat(options.format), options.output);
|
|
566
341
|
});
|
|
567
342
|
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
343
|
const client = await createClient();
|
|
@@ -579,19 +354,19 @@ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint t
|
|
|
579
354
|
return;
|
|
580
355
|
}
|
|
581
356
|
process.stderr.write(`Got dataId: ${dataId}, waiting for content generation...\n`);
|
|
582
|
-
if (!await pollAsyncContent(client, "ai.viewpoint-debate.get-content", dataId,
|
|
357
|
+
if (!await pollAsyncContent(client, "ai.viewpoint-debate.get-content", dataId, parseOutputFormat(options.format), options.output)) {
|
|
583
358
|
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
359
|
process.exitCode = 1;
|
|
585
360
|
}
|
|
586
361
|
});
|
|
587
362
|
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
363
|
const client = await createClient();
|
|
589
|
-
await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId,
|
|
364
|
+
await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output);
|
|
590
365
|
});
|
|
591
366
|
const vault = new Command("vault").description("Vault APIs");
|
|
592
367
|
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
368
|
const client = await createClient();
|
|
594
|
-
await printData(await client.call("vault.drive.list", { from:
|
|
369
|
+
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
370
|
});
|
|
596
371
|
vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action(async (options) => {
|
|
597
372
|
const client = await createClient();
|
|
@@ -601,7 +376,7 @@ vault.command("drive-download").requiredOption("--file-id <id>").option("--outpu
|
|
|
601
376
|
});
|
|
602
377
|
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
378
|
const client = await createClient();
|
|
604
|
-
await printData(await client.call("vault.record.list", { from:
|
|
379
|
+
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
380
|
});
|
|
606
381
|
vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action(async (options) => {
|
|
607
382
|
const client = await createClient();
|
|
@@ -611,7 +386,7 @@ vault.command("record-download").requiredOption("--record-id <id>").requiredOpti
|
|
|
611
386
|
});
|
|
612
387
|
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
388
|
const client = await createClient();
|
|
614
|
-
await printData(await client.call("vault.my-conference.list", { from:
|
|
389
|
+
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
390
|
});
|
|
616
391
|
vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action(async (options) => {
|
|
617
392
|
const client = await createClient();
|
|
@@ -637,7 +412,7 @@ program.command("raw").description("Raw API calls").addCommand(new Command("call
|
|
|
637
412
|
await saveDownloadResult(data, "download.bin", options.output);
|
|
638
413
|
return;
|
|
639
414
|
}
|
|
640
|
-
await printData(data,
|
|
415
|
+
await printData(data, parseOutputFormat(options.format), options.output);
|
|
641
416
|
}));
|
|
642
417
|
async function main() {
|
|
643
418
|
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 };
|
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
|
+
}
|
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.10.
|
|
2
|
+
export const CLI_VERSION = "0.10.10";
|