gangtise-openapi-cli 0.22.1 → 0.23.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 +23 -5
- package/dist/src/cli.js +86 -4
- package/dist/src/core/args.js +3 -0
- package/dist/src/core/client.js +1 -72
- package/dist/src/core/commandBodies.js +1 -1
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/endpoints.js +14 -2
- package/dist/src/core/normalize.js +0 -5
- package/dist/src/core/quoteSharding.js +45 -7
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
## Changelog
|
|
6
6
|
|
|
7
|
+
### v0.23.0 — 2026-07-05
|
|
8
|
+
|
|
9
|
+
**行为变更(注意)**
|
|
10
|
+
- ⚠️ 默认 API 域名迁移:`https://open.gangtise.com` → `https://openapi.gangtise.com`。旧域名仍可用,CLI 只是切换了默认值(新旧域名多接口实测等价);如需固定旧域名设 `GANGTISE_BASE_URL=https://open.gangtise.com`
|
|
11
|
+
- `vault wechat-chatroom-list`:服务端接口改版为返回 `{ total, list }`(此前无 `total`、列名 `chatRoomList`)。CLI 相应改为按 `total` 并发翻页(不再串行翻页);省略 `--size` 仍拉全量、传 `--size N` 取前 N 条,`Total:` 提示恢复
|
|
12
|
+
- 无翻页的行情端点(`quote fund-flow` / `minute-kline` / 显式多标的的日 K:`day-kline`·`-hk`·`-us`·`index-day-kline`)返回行数撞上单次 `--limit` 时标 `partial`(退出码 3)+ stderr 警告,避免被静默截断;`--limit` 现本地校验 ≤ 10000(撞服务端上限也不漏标)。K 线 `--security all` 仍走日期分片自动补全,不受影响
|
|
13
|
+
|
|
14
|
+
**新增**
|
|
15
|
+
- `quote fund-flow` — A股个股日资金流向(沪深京),含小/中/大/特大单流入流出金额及占比、主力净流入等字段;`--security`(或 `aShares` 全市场)、`--start-date` / `--end-date`、`--limit`(默认 6000,上限 10000)、`--field` 指定返回字段;无积分消耗(单只证券无翻页,撞 `--limit` 时的截断处理见上「行为变更」;**`aShares` 全市场须显式传 `--start-date`/`--end-date`,CLI 按日自动分片并发合并——缺日期会本地报错**)
|
|
16
|
+
- `reference institution-search` — 机构 ID 搜索,输入机构名/简称返回 `institutionId` 及适用接口参数(`usageScopes`);`--keyword`(必填)、`--category`(`domesticBroker`/`foreignInstitution`/`leadInstitution`/`opinionInstitution`/`foreignOpinionInstitution`,可重复)、`--top`(默认 10,上限 10);免费。覆盖既有 `--broker`/`--institution` 全部机构类(research/foreign-report/opinion/foreign-opinion/summary/roadshow/site-visit/strategy/my-conference)
|
|
17
|
+
- `vault my-conference-list` 新增 `--source` — 按录制来源筛选(`1`=企微会议助理 `2`=会议服务微信群,可重复;不传返回全部)
|
|
18
|
+
|
|
7
19
|
### v0.22.1 — 2026-07-03
|
|
8
20
|
|
|
9
21
|
**修复**
|
|
@@ -149,7 +161,7 @@ npm run dev -- --help
|
|
|
149
161
|
```bash
|
|
150
162
|
export GANGTISE_ACCESS_KEY="your-ak"
|
|
151
163
|
export GANGTISE_SECRET_KEY="your-sk"
|
|
152
|
-
export GANGTISE_BASE_URL="https://
|
|
164
|
+
export GANGTISE_BASE_URL="https://openapi.gangtise.com"
|
|
153
165
|
export GANGTISE_TOKEN="Bearer xxx"
|
|
154
166
|
|
|
155
167
|
# 性能/调试可选项
|
|
@@ -227,7 +239,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
227
239
|
| 模块 | 子命令 | 说明 |
|
|
228
240
|
|------|--------|------|
|
|
229
241
|
| **Auth** | `login` / `status` | 认证登录、状态查询 |
|
|
230
|
-
| **Lookup** | `broker-org list` / `meeting-org list` |
|
|
242
|
+
| **Lookup** | `broker-org list` / `meeting-org list` | 券商/会议机构本地全量枚举表(按名称找 ID 优先 `reference institution-search`;行业/区域/公告分类/题材/申万码已改用 Reference 接口) |
|
|
231
243
|
| **Insight** | `opinion list` | 内资机构观点 |
|
|
232
244
|
| | `summary list` / `download` | 纪要(含下载,支持 `--file-type` 选原始/HTML) |
|
|
233
245
|
| | `roadshow list` | 路演 |
|
|
@@ -244,6 +256,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
244
256
|
| | `official-account list` / `download` | 产业公众号资讯(含 txt/HTML 下载) |
|
|
245
257
|
| **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
|
|
246
258
|
| | `chiefs-search` | 首席分析师 ID 搜索(按姓名/机构/团队匹配) |
|
|
259
|
+
| | `institution-search` | 机构 ID 搜索(内资券商/外资/牵头/观点机构,按名称匹配) |
|
|
247
260
|
| | `constant-category` | 常量分类列表(含各分类适用的接口与参数) |
|
|
248
261
|
| | `constant-list` | 按分类导出常量值全量列表(行业/城市/公告分类/区域等) |
|
|
249
262
|
| | `concept-search` | 题材 ID 搜索(名称/拼音/分组名匹配) |
|
|
@@ -253,6 +266,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
|
|
|
253
266
|
| | `index-day-kline` | 沪深京指数日K线 |
|
|
254
267
|
| | `minute-kline` | A股分钟K线 |
|
|
255
268
|
| | `realtime` | 个股实时行情快照(A股/港股/美股) |
|
|
269
|
+
| | `fund-flow` | A股个股日资金流向(沪深京;小/中/大/特大单 + 主力净流入) |
|
|
256
270
|
| **Fundamental** | `income-statement` / `balance-sheet` / `cash-flow` | A股三大财务报表(累计) |
|
|
257
271
|
| | `income-statement-quarterly` / `cash-flow-quarterly` | A股利润表/现金流量表(单季度) |
|
|
258
272
|
| | `income-statement-hk` / `balance-sheet-hk` / `cash-flow-hk` | 港股三大财务报表(中国会计准则) |
|
|
@@ -362,7 +376,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
362
376
|
- `vault record-list`
|
|
363
377
|
- `vault my-conference-list`
|
|
364
378
|
- `vault wechat-message-list`
|
|
365
|
-
- `vault wechat-chatroom-list
|
|
379
|
+
- `vault wechat-chatroom-list`
|
|
366
380
|
- `ai hot-topic`
|
|
367
381
|
|
|
368
382
|
规则:
|
|
@@ -373,7 +387,6 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
|
|
|
373
387
|
- 安全上限:自动翻页最多 1000 页,防止异常循环
|
|
374
388
|
- 部分页失败、或服务端实际返回行数与 `total` 矛盾(提前短页)时,不丢弃已取到的数据:结果带 `partial: true`(页失败时另有 `failedPages`;K线分片为 `failedShards`;`--format json` 可见),stderr 输出警告,**进程退出码为 3**(完整成功为 0)
|
|
375
389
|
- 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`);其他格式下 stderr 输出 `Total: N, showing: M`(json 格式不输出该行)
|
|
376
|
-
- `vault wechat-chatroom-list` 是特例:接口不返回 `total`,CLI 改为串行翻页——省略 `--size` 拉全量、传 `--size N` 取前 N 条,单页 50,无 `Total:` 提示
|
|
377
390
|
|
|
378
391
|
## 智能文件命名
|
|
379
392
|
|
|
@@ -462,6 +475,8 @@ gangtise reference securities-search --keyword "银行" --category stock --categ
|
|
|
462
475
|
# 首席分析师 ID 搜索(按姓名/机构/团队;拿 chiefId 供 insight opinion list --chief 使用)
|
|
463
476
|
gangtise reference chiefs-search --keyword 东吴证券 --top 3 --format json
|
|
464
477
|
gangtise reference chiefs-search --keyword 芦哲 --format json
|
|
478
|
+
# 机构 ID 搜索(--category: domesticBroker/foreignInstitution/leadInstitution/opinionInstitution)
|
|
479
|
+
gangtise reference institution-search --keyword 招商证券 --category domesticBroker --top 3 --format json
|
|
465
480
|
|
|
466
481
|
# 常量查询:先看分类,再按分类导出全量常量值
|
|
467
482
|
gangtise reference constant-category --format json
|
|
@@ -502,6 +517,8 @@ gangtise quote minute-kline --security 600519.SH --start-time "2026-04-15 09:30:
|
|
|
502
517
|
gangtise quote realtime --security 600519.SH --security 00700.HK --security AAPL.O --field securityCode --field tradeTime --field latestPrice --field pctChange --field volume --format json
|
|
503
518
|
# 实时行情:全市场批量(建议配合 --field 精简字段)
|
|
504
519
|
gangtise quote realtime --security aShares --field securityCode --field latestPrice --field pctChange --field volume --format json
|
|
520
|
+
# A股个股日资金流向(沪深京;--security aShares 全市场;--limit 上限 10000,超限缩短日期区间分批)
|
|
521
|
+
gangtise quote fund-flow --security 600519.SH --security 000001.SZ --start-date 2026-06-01 --end-date 2026-06-05 --field mainNetInflow --field largeInflow --field xlargeInflow --format json
|
|
505
522
|
```
|
|
506
523
|
|
|
507
524
|
> **历史 vs 实时**:`day-kline*` 仅返回历史数据(当日数据入库时间:A 股 ~15:30 / 港股 ~16:30 / 美股 ~07:00 北京时间)。盘中需要最新成交价、振幅等实时字段必须走 `quote realtime`。
|
|
@@ -598,8 +615,9 @@ gangtise vault record-list --keyword 晨会 --category upload --category mobile
|
|
|
598
615
|
# 录音速记下载(--content-type: original/asr/summary)
|
|
599
616
|
gangtise vault record-download --record-id 49412 --content-type summary
|
|
600
617
|
|
|
601
|
-
#
|
|
618
|
+
# 我的会议列表(--source 录制来源:1=企微会议助理 2=会议服务微信群,可重复;不传返回全部)
|
|
602
619
|
gangtise vault my-conference-list --keyword AI --category earningsCall --institution C100000027
|
|
620
|
+
gangtise vault my-conference-list --source 2 --category earningsCall --size 20
|
|
603
621
|
# 我的会议下载(--content-type: asr/summary)
|
|
604
622
|
gangtise vault my-conference-download --conference-id 43319 --content-type asr
|
|
605
623
|
|
package/dist/src/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readTokenCache, redactTokenCache } from "./core/auth.js";
|
|
|
5
5
|
import { collectKeyValue, collectList, collectNumberList, maybeArray, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize, parseTimestamp13 } from "./core/args.js";
|
|
6
6
|
import { buildIndicatorCrossSectionBody, buildIndicatorTimeSeriesBody, buildQuoteKlineBody, buildStockPoolStocksBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "./core/commandBodies.js";
|
|
7
7
|
import { flattenCrossSection, flattenTimeSeries, unwrapIndicatorData } from "./core/indicatorMatrix.js";
|
|
8
|
-
import { callKlineWithSharding } from "./core/quoteSharding.js";
|
|
8
|
+
import { callKlineWithSharding, isAllMarket, isFullMarket } from "./core/quoteSharding.js";
|
|
9
9
|
import { loadConfig } from "./core/config.js";
|
|
10
10
|
import { resolveTitle, saveDownloadResult, uniquePath } from "./core/download.js";
|
|
11
11
|
import { ENDPOINTS } from "./core/endpoints.js";
|
|
@@ -35,6 +35,33 @@ async function emit(options, produce, cache) {
|
|
|
35
35
|
async function withClient(fn) {
|
|
36
36
|
await fn(await createClient());
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Server-side default row cap shared by the limit-capped, non-paginated quote endpoints
|
|
40
|
+
* (fund-flow, minute-kline, day/index kline — all default to 6000 per the API docs). We
|
|
41
|
+
* send it EXPLICITLY when `--limit` is omitted (rather than letting the server apply its
|
|
42
|
+
* own default) so the request limit and the truncation `cap` below are always the same
|
|
43
|
+
* number — never a guess about the server's default that can drift out of sync.
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_QUOTE_LIMIT = 6000;
|
|
46
|
+
/**
|
|
47
|
+
* Limit-capped, non-paginated endpoints (fund-flow, kline) report `total` as the
|
|
48
|
+
* RETURNED row count, not the true total, so a full page (rows == the limit we sent) is
|
|
49
|
+
* the only truncation signal. Flag the result partial (printData → exit 3) + warn so a
|
|
50
|
+
* capped export isn't mistaken for the full set. `cap` MUST be the exact limit the caller
|
|
51
|
+
* sent on the request; `--limit` is validated to <= 10000 so `cap` can't exceed the
|
|
52
|
+
* server ceiling and hide a truncation.
|
|
53
|
+
*/
|
|
54
|
+
function flagIfLimitTruncated(data, cap, label) {
|
|
55
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
56
|
+
return;
|
|
57
|
+
const rec = data;
|
|
58
|
+
if (rec.partial === true)
|
|
59
|
+
return;
|
|
60
|
+
if (Array.isArray(rec.list) && rec.list.length >= cap) {
|
|
61
|
+
rec.partial = true;
|
|
62
|
+
process.stderr.write(`[gangtise] warning: ${label} returned ${rec.list.length} rows = the ${cap}-row limit; results are likely truncated (this endpoint has no pagination). Narrow --start-date/--end-date or raise --limit (max 10000), fetching in date batches.\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
/**
|
|
39
66
|
* Run a download. If `output` is set we already know the destination, so the
|
|
40
67
|
* client streams the body straight to disk (no in-memory Uint8Array copy);
|
|
@@ -307,13 +334,63 @@ const addKlineCommand = (name, endpointKey, securityHelp, shardDays) => quote.co
|
|
|
307
334
|
.option("--field <field>", "Field", collectList, [])
|
|
308
335
|
.option("--format <format>", "Output format", "table")
|
|
309
336
|
.option("--output <path>")
|
|
310
|
-
.action((options) =>
|
|
337
|
+
.action((options) => withClient(async (client) => {
|
|
338
|
+
const format = parseOutputFormat(options.format);
|
|
339
|
+
const body = buildQuoteKlineBody(options);
|
|
340
|
+
if (isAllMarket(body)) {
|
|
341
|
+
// `--security all` is date-sharded: callKlineWithSharding lifts the limit to the
|
|
342
|
+
// API max and owns completeness (partial / failedShards), so leave `limit` unset
|
|
343
|
+
// and skip the single-request truncation guard.
|
|
344
|
+
const data = await callKlineWithSharding(client, endpointKey, body, { shardDays });
|
|
345
|
+
await printData(data, format, options.output);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Explicit securities go out as one request: pin the limit to the known default so
|
|
349
|
+
// the sent limit and the truncation cap are the same number by construction.
|
|
350
|
+
const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
|
|
351
|
+
const data = await callKlineWithSharding(client, endpointKey, { ...body, limit }, { shardDays });
|
|
352
|
+
flagIfLimitTruncated(data, limit, name);
|
|
353
|
+
await printData(data, format, options.output);
|
|
354
|
+
}));
|
|
311
355
|
addKlineCommand("day-kline", "quote.day-kline", "Security code (A-share: .SH/.SZ/.BJ, or 'all' for full market)", 1);
|
|
312
356
|
addKlineCommand("day-kline-hk", "quote.day-kline-hk", "Security code (HK stock: .HK, or 'all' for full market)", 2);
|
|
313
357
|
addKlineCommand("day-kline-us", "quote.day-kline-us", "Security code (US stock: e.g. AAPL.O, or 'all' for full market)", 1);
|
|
314
358
|
addKlineCommand("index-day-kline", "quote.index-day-kline", "Index code (.SH/.SZ/.BJ, or 'all' for full market)", 30);
|
|
315
|
-
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:
|
|
359
|
+
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: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
360
|
+
const format = parseOutputFormat(options.format);
|
|
361
|
+
const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 10000 }) ?? DEFAULT_QUOTE_LIMIT;
|
|
362
|
+
const data = await client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit, fieldList: maybeArray(options.field) });
|
|
363
|
+
flagIfLimitTruncated(data, limit, "minute-kline");
|
|
364
|
+
await printData(data, format, options.output);
|
|
365
|
+
}));
|
|
316
366
|
quote.command("realtime").description("Realtime quote snapshot (A-share / HK / US)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK / AAPL.O), or market keyword: aShares / hkStocks / usStocks", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList: maybeArray(options.field) })));
|
|
367
|
+
quote.command("fund-flow").description("A-share daily fund flow (SH/SZ/BJ)").option("--security <code>", "Security code (e.g. 600519.SH / 872931.BJ), or 'aShares' for full A-share market — auto-sharded by day (repeat)", collectList, []).option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)").option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000; single-security cap — aShares auto-shards by day)").option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
|
|
368
|
+
const format = parseOutputFormat(options.format);
|
|
369
|
+
const body = {
|
|
370
|
+
securityList: maybeArray(options.security),
|
|
371
|
+
startDate: options.startDate,
|
|
372
|
+
endDate: options.endDate,
|
|
373
|
+
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 10000 }),
|
|
374
|
+
fieldList: maybeArray(options.field),
|
|
375
|
+
};
|
|
376
|
+
if (isFullMarket(body, "aShares")) {
|
|
377
|
+
// Full-market fund-flow: the server errors (430012/430013) instead of truncating when
|
|
378
|
+
// a single request exceeds the row cap, so date-shard by day (~5.4k A-share rows/day,
|
|
379
|
+
// under the lifted API cap) and merge — same mechanism as `--security all` kline.
|
|
380
|
+
// Sharding needs an explicit range; without both dates it would fall back to one
|
|
381
|
+
// doomed full-market request, so require the range up front with a clear message.
|
|
382
|
+
if (!body.startDate || !body.endDate) {
|
|
383
|
+
throw new ValidationError("quote fund-flow --security aShares requires both --start-date and --end-date (the full market is fetched via per-day shards)");
|
|
384
|
+
}
|
|
385
|
+
const data = await callKlineWithSharding(client, "quote.fund-flow", body, { shardDays: 1, fullMarketValue: "aShares" });
|
|
386
|
+
await printData(data, format, options.output);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
|
|
390
|
+
const data = await client.call("quote.fund-flow", { ...body, limit });
|
|
391
|
+
flagIfLimitTruncated(data, limit, "fund-flow");
|
|
392
|
+
await printData(data, format, options.output);
|
|
393
|
+
}));
|
|
317
394
|
program.addCommand(quote);
|
|
318
395
|
const fundamental = new Command("fundamental").description("Fundamental APIs");
|
|
319
396
|
const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) })));
|
|
@@ -479,13 +556,18 @@ reference.command("chiefs-search").requiredOption("--keyword <text>", "Search ke
|
|
|
479
556
|
keyword: options.keyword,
|
|
480
557
|
top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
|
|
481
558
|
})));
|
|
559
|
+
reference.command("institution-search").requiredOption("--keyword <text>", "Search keyword (institution name / abbreviation)").option("--category <name>", "Category: domesticBroker/foreignInstitution/leadInstitution/opinionInstitution/foreignOpinionInstitution (repeat); omit for all", collectList, []).option("--top <number>", "Max results (default: 10, max: 10)", "10").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.institution-search", {
|
|
560
|
+
keyword: options.keyword,
|
|
561
|
+
categoryList: maybeArray(options.category),
|
|
562
|
+
top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
|
|
563
|
+
})));
|
|
482
564
|
program.addCommand(reference);
|
|
483
565
|
const vault = new Command("vault").description("Vault APIs");
|
|
484
566
|
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((options) => emit(options, (client) => 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 }), { endpointKey: "vault.drive.list", idField: "fileId" }));
|
|
485
567
|
addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
|
|
486
568
|
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((options) => emit(options, (client) => 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 }), { endpointKey: "vault.record.list", idField: "recordId" }));
|
|
487
569
|
addDownloadCommand(vault, { endpointKey: "vault.record.download", name: "record-download", idOption: "--record-id", idField: "recordId", fallbackPrefix: "record", contentTypeDescription: "Content type: original/asr/summary", titleListEndpoint: "vault.record.list" });
|
|
488
|
-
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((options) => emit(options, (client) => 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) }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
|
|
570
|
+
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("--source <number>", "Recording source: 1=企微会议助理 2=会议服务微信群 (repeat)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => 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), sourceList: options.source.length ? options.source : undefined }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
|
|
489
571
|
addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "my-conference-download", idOption: "--conference-id", idField: "conferenceId", fallbackPrefix: "conference", contentTypeDescription: "Content type: asr/summary", titleListEndpoint: "vault.my-conference.list" });
|
|
490
572
|
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("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).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((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
|
|
491
573
|
vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").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((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
|
package/dist/src/core/args.js
CHANGED
|
@@ -24,6 +24,9 @@ export function parseNumberOption(value, optionName, config = {}) {
|
|
|
24
24
|
if (config.min !== undefined && parsed < config.min) {
|
|
25
25
|
throw new ValidationError(`Invalid ${optionName}: expected a number >= ${config.min}`);
|
|
26
26
|
}
|
|
27
|
+
if (config.max !== undefined && parsed > config.max) {
|
|
28
|
+
throw new ValidationError(`Invalid ${optionName}: expected a number <= ${config.max}`);
|
|
29
|
+
}
|
|
27
30
|
return parsed;
|
|
28
31
|
}
|
|
29
32
|
export function parseOptionalNumberOption(value, optionName, config = {}) {
|
package/dist/src/core/client.js
CHANGED
|
@@ -294,75 +294,6 @@ export class GangtiseClient {
|
|
|
294
294
|
}
|
|
295
295
|
return out;
|
|
296
296
|
}
|
|
297
|
-
/**
|
|
298
|
-
* Sequential pagination for endpoints that page by offset but return NO `total`
|
|
299
|
-
* and use a non-standard list key (e.g. wechat chatroom's `chatRoomList`). We
|
|
300
|
-
* can't fan out like requestPaginated (no total ⇒ unknown page count), so page
|
|
301
|
-
* serially until a short page (fewer rows than requested) signals the end.
|
|
302
|
-
* Returns `{ [listKey]: rows }` so normalize/printer treat it like any list.
|
|
303
|
-
*/
|
|
304
|
-
async requestSequentialPaginated(endpoint, body) {
|
|
305
|
-
const initialBody = body && typeof body === 'object' ? { ...body } : {};
|
|
306
|
-
if ('from' in initialBody && (typeof initialBody.from !== 'number' || !Number.isFinite(initialBody.from) || initialBody.from < 0)) {
|
|
307
|
-
throw new ValidationError('Invalid from: expected a non-negative number');
|
|
308
|
-
}
|
|
309
|
-
if ('size' in initialBody && initialBody.size !== undefined && (typeof initialBody.size !== 'number' || !Number.isFinite(initialBody.size) || initialBody.size <= 0)) {
|
|
310
|
-
throw new ValidationError('Invalid size: expected a positive number');
|
|
311
|
-
}
|
|
312
|
-
const listKey = endpoint.pagination?.listKey ?? 'list';
|
|
313
|
-
const maxPageSize = endpoint.pagination?.maxPageSize ?? 50;
|
|
314
|
-
const startFrom = typeof initialBody.from === 'number' && Number.isFinite(initialBody.from) ? initialBody.from : 0;
|
|
315
|
-
const requestedSize = typeof initialBody.size === 'number' && Number.isFinite(initialBody.size) ? initialBody.size : undefined;
|
|
316
|
-
const extractList = (page) => {
|
|
317
|
-
if (!page || typeof page !== 'object')
|
|
318
|
-
return null;
|
|
319
|
-
const arr = page[listKey];
|
|
320
|
-
return Array.isArray(arr) ? arr : null;
|
|
321
|
-
};
|
|
322
|
-
const collected = [];
|
|
323
|
-
let firstPage = null;
|
|
324
|
-
let from = startFrom;
|
|
325
|
-
const MAX_PAGES = 1000;
|
|
326
|
-
let truncatedByPageCap = false;
|
|
327
|
-
let partialShape = false;
|
|
328
|
-
for (let page = 0;; page++) {
|
|
329
|
-
const remaining = requestedSize === undefined ? maxPageSize : requestedSize - collected.length;
|
|
330
|
-
if (requestedSize !== undefined && remaining <= 0)
|
|
331
|
-
break;
|
|
332
|
-
const size = Math.min(maxPageSize, remaining);
|
|
333
|
-
const pageData = await this.requestJson(endpoint, { ...initialBody, from, size });
|
|
334
|
-
if (firstPage === null)
|
|
335
|
-
firstPage = pageData;
|
|
336
|
-
const list = extractList(pageData);
|
|
337
|
-
if (list === null) {
|
|
338
|
-
// First response isn't a paginated-list shape → return it untouched. But a
|
|
339
|
-
// LATER page losing shape must NOT discard the rows already collected (mirrors
|
|
340
|
-
// requestPaginated's fail-soft fan-out): stop, keep them, and warn loudly.
|
|
341
|
-
if (page === 0)
|
|
342
|
-
return firstPage;
|
|
343
|
-
partialShape = true;
|
|
344
|
-
process.stderr.write(`[gangtise] warning: a page response had unexpected shape; results are partial — ${collected.length} rows fetched.\n`);
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
for (const item of list)
|
|
348
|
-
collected.push(item);
|
|
349
|
-
if (list.length < size)
|
|
350
|
-
break; // short page ⇒ no more rows
|
|
351
|
-
if (page + 1 >= MAX_PAGES) {
|
|
352
|
-
truncatedByPageCap = true;
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
from += list.length;
|
|
356
|
-
}
|
|
357
|
-
if (truncatedByPageCap) {
|
|
358
|
-
process.stderr.write(`[gangtise] warning: hit the ${MAX_PAGES}-page safety cap; fetched ${collected.length} rows. Pass --size to fetch a bounded subset.\n`);
|
|
359
|
-
}
|
|
360
|
-
const rows = requestedSize === undefined ? collected : collected.slice(0, requestedSize);
|
|
361
|
-
const out = { [listKey]: rows };
|
|
362
|
-
if (partialShape)
|
|
363
|
-
out.partial = true;
|
|
364
|
-
return out;
|
|
365
|
-
}
|
|
366
297
|
async login() {
|
|
367
298
|
const authorization = await this.getAuthorizationHeader();
|
|
368
299
|
const cache = await readTokenCache(this.config.tokenCachePath);
|
|
@@ -572,9 +503,7 @@ export class GangtiseClient {
|
|
|
572
503
|
return this.download(endpoint, query ?? {}, options);
|
|
573
504
|
}
|
|
574
505
|
if (endpoint.kind === 'json' && endpoint.pagination?.enabled) {
|
|
575
|
-
return endpoint
|
|
576
|
-
? this.requestSequentialPaginated(endpoint, body)
|
|
577
|
-
: this.requestPaginated(endpoint, body);
|
|
506
|
+
return this.requestPaginated(endpoint, body);
|
|
578
507
|
}
|
|
579
508
|
return this.requestJson(endpoint, body);
|
|
580
509
|
}
|
|
@@ -4,7 +4,7 @@ export function buildQuoteKlineBody(options) {
|
|
|
4
4
|
securityList: maybeArray(options.security),
|
|
5
5
|
startDate: options.startDate,
|
|
6
6
|
endDate: options.endDate,
|
|
7
|
-
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
|
|
7
|
+
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: 10000 }),
|
|
8
8
|
fieldList: maybeArray(options.field),
|
|
9
9
|
};
|
|
10
10
|
}
|
package/dist/src/core/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
export const DEFAULT_BASE_URL = "https://
|
|
3
|
+
export const DEFAULT_BASE_URL = "https://openapi.gangtise.com";
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
5
|
export const DEFAULT_TOKEN_CACHE_PATH = path.join(os.homedir(), ".config", "gangtise", "token.json");
|
|
6
6
|
export function loadConfig() {
|
|
@@ -181,6 +181,12 @@ const ENDPOINT_DEFS = {
|
|
|
181
181
|
kind: "json",
|
|
182
182
|
description: "Search chief analyst IDs by name / institution / team",
|
|
183
183
|
},
|
|
184
|
+
"reference.institution-search": {
|
|
185
|
+
method: "POST",
|
|
186
|
+
path: "/application/open-reference/institutions/search",
|
|
187
|
+
kind: "json",
|
|
188
|
+
description: "Search institution IDs by keyword (domestic broker / foreign / lead / opinion institution)",
|
|
189
|
+
},
|
|
184
190
|
"reference.constant-category": {
|
|
185
191
|
method: "GET",
|
|
186
192
|
path: "/application/open-reference/constants/category",
|
|
@@ -248,6 +254,12 @@ const ENDPOINT_DEFS = {
|
|
|
248
254
|
kind: "json",
|
|
249
255
|
description: "Query realtime quote snapshot (A-share / HK / US)",
|
|
250
256
|
},
|
|
257
|
+
"quote.fund-flow": {
|
|
258
|
+
method: "POST",
|
|
259
|
+
path: "/application/open-quote/fund-flow/daily",
|
|
260
|
+
kind: "json",
|
|
261
|
+
description: "Query A-share daily fund flow (SH/SZ/BJ; small/medium/large/xlarge orders + main net inflow)",
|
|
262
|
+
},
|
|
251
263
|
// ─── fundamental ───
|
|
252
264
|
"fundamental.income-statement": {
|
|
253
265
|
method: "POST",
|
|
@@ -490,8 +502,8 @@ const ENDPOINT_DEFS = {
|
|
|
490
502
|
path: "/application/open-vault/wechatgroupmsg/chatroomId",
|
|
491
503
|
kind: "json",
|
|
492
504
|
description: "List WeChat group chatroom IDs",
|
|
493
|
-
//
|
|
494
|
-
pagination: { enabled: true, maxPageSize: 50
|
|
505
|
+
// Response is `{ total, list }` (server caps size at 50); auto-paginate by total.
|
|
506
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
495
507
|
},
|
|
496
508
|
"vault.stock-pool.list": {
|
|
497
509
|
method: "POST",
|
|
@@ -24,11 +24,6 @@ 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
|
-
}
|
|
32
27
|
if (Array.isArray(record.constants)) {
|
|
33
28
|
const { constants, ...meta } = record;
|
|
34
29
|
const hasMeta = Object.keys(meta).length > 0;
|
|
@@ -16,11 +16,16 @@ function parseDate(value) {
|
|
|
16
16
|
function formatDate(d) {
|
|
17
17
|
return d.toISOString().slice(0, 10);
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
export function isFullMarket(body, fullMarketValue) {
|
|
20
20
|
const list = body.securityList;
|
|
21
21
|
if (!Array.isArray(list) || list.length !== 1)
|
|
22
22
|
return false;
|
|
23
|
-
return list[0] ===
|
|
23
|
+
return list[0] === fullMarketValue;
|
|
24
|
+
}
|
|
25
|
+
/** Kline uses `all` as its whole-market keyword; fund-flow uses `aShares`. Thin wrapper
|
|
26
|
+
* so kline call sites read naturally. */
|
|
27
|
+
export function isAllMarket(body) {
|
|
28
|
+
return isFullMarket(body, "all");
|
|
24
29
|
}
|
|
25
30
|
function buildShards(start, end, shardDays) {
|
|
26
31
|
const shards = [];
|
|
@@ -43,7 +48,8 @@ function buildShards(start, end, shardDays) {
|
|
|
43
48
|
* single-security queries this is a no-op.
|
|
44
49
|
*/
|
|
45
50
|
export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
46
|
-
|
|
51
|
+
const fullMarketValue = config.fullMarketValue ?? "all";
|
|
52
|
+
if (!isFullMarket(body, fullMarketValue)) {
|
|
47
53
|
return client.call(endpointKey, body);
|
|
48
54
|
}
|
|
49
55
|
// `--security all` returns thousands of rows per day; lift the default 6000-row
|
|
@@ -51,17 +57,40 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
51
57
|
// must apply even when a date is missing (no sharding possible then, but the
|
|
52
58
|
// single request still needs the lifted cap).
|
|
53
59
|
const allMarketBody = { ...body, limit: body.limit ?? ALL_MARKET_LIMIT };
|
|
60
|
+
const perShardLimit = allMarketBody.limit ?? ALL_MARKET_LIMIT;
|
|
61
|
+
// A shard maxes out for two reasons with different fixes: a user-set low --limit is
|
|
62
|
+
// raisable; hitting the API cap itself is not (only a smaller internal shardDays window
|
|
63
|
+
// would help). Word the truncation warning accordingly.
|
|
64
|
+
const truncationHint = perShardLimit < ALL_MARKET_LIMIT
|
|
65
|
+
? "raise or omit --limit to fetch the full market"
|
|
66
|
+
: `a single ${config.shardDays}-day window exceeds the ${ALL_MARKET_LIMIT}-row API cap`;
|
|
67
|
+
// A full-market response whose row count reaches the per-request limit was itself
|
|
68
|
+
// capped (a low user --limit, or a single day exceeding the API row cap) — its slice
|
|
69
|
+
// is incomplete, so the result must be flagged partial rather than shown as complete.
|
|
70
|
+
const isTruncated = (rec) => Array.isArray(rec.list) && rec.list.length >= perShardLimit;
|
|
71
|
+
// A single full-market request (missing/unparseable dates, or a range that fits one
|
|
72
|
+
// shard) skips the merge loop below, so it needs the same truncation check inline — or
|
|
73
|
+
// a low --limit / oversized day slips through as a silent exit-0 success.
|
|
74
|
+
const callSingle = async () => {
|
|
75
|
+
const single = await client.call(endpointKey, allMarketBody);
|
|
76
|
+
if (single && typeof single === "object" && !Array.isArray(single) && isTruncated(single)) {
|
|
77
|
+
;
|
|
78
|
+
single.partial = true;
|
|
79
|
+
process.stderr.write(`[gangtise] warning: full-market request hit the ${perShardLimit}-row limit and was likely truncated; results are partial — ${truncationHint}.\n`);
|
|
80
|
+
}
|
|
81
|
+
return single;
|
|
82
|
+
};
|
|
54
83
|
if (!body.startDate || !body.endDate) {
|
|
55
|
-
return
|
|
84
|
+
return callSingle();
|
|
56
85
|
}
|
|
57
86
|
const start = parseDate(body.startDate);
|
|
58
87
|
const end = parseDate(body.endDate);
|
|
59
88
|
if (!start || !end || end < start) {
|
|
60
|
-
return
|
|
89
|
+
return callSingle();
|
|
61
90
|
}
|
|
62
91
|
const totalDays = Math.floor((end.getTime() - start.getTime()) / DAY_MS) + 1;
|
|
63
92
|
if (totalDays <= config.shardDays) {
|
|
64
|
-
return
|
|
93
|
+
return callSingle();
|
|
65
94
|
}
|
|
66
95
|
const shards = buildShards(start, end, config.shardDays);
|
|
67
96
|
// isVerbose() (not a direct env read) so the global --verbose flag reaches
|
|
@@ -89,6 +118,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
89
118
|
let fieldList;
|
|
90
119
|
let header = null;
|
|
91
120
|
const merged = [];
|
|
121
|
+
let truncatedShards = 0;
|
|
92
122
|
for (const r of results) {
|
|
93
123
|
if (!(r && typeof r === "object"))
|
|
94
124
|
continue;
|
|
@@ -97,6 +127,8 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
97
127
|
header = rec;
|
|
98
128
|
if (!fieldList && Array.isArray(rec.fieldList))
|
|
99
129
|
fieldList = rec.fieldList;
|
|
130
|
+
if (isTruncated(rec))
|
|
131
|
+
truncatedShards++;
|
|
100
132
|
// Append one-by-one rather than push(...list): a future higher row cap could
|
|
101
133
|
// make a single shard's list large enough to overflow the stack via spread.
|
|
102
134
|
if (Array.isArray(rec.list))
|
|
@@ -110,7 +142,9 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
110
142
|
}
|
|
111
143
|
if (!header)
|
|
112
144
|
return { list: [] };
|
|
113
|
-
|
|
145
|
+
// `total` on a shard is that shard's own row count; overwrite it with the merged count
|
|
146
|
+
// so the JSON `total` and the `Total:` stderr line reflect the whole combined result.
|
|
147
|
+
const out = { ...header, total: merged.length, list: merged };
|
|
114
148
|
if (fieldList)
|
|
115
149
|
out.fieldList = fieldList;
|
|
116
150
|
if (failedShards.length > 0) {
|
|
@@ -118,5 +152,9 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
|
|
|
118
152
|
out.failedShards = failedShards;
|
|
119
153
|
process.stderr.write(`[gangtise] warning: ${failedShards.length}/${shards.length} shards failed; results are partial (see failedShards)\n`);
|
|
120
154
|
}
|
|
155
|
+
if (truncatedShards > 0) {
|
|
156
|
+
out.partial = true;
|
|
157
|
+
process.stderr.write(`[gangtise] warning: ${truncatedShards}/${shards.length} shard(s) hit the ${perShardLimit}-row limit and were likely truncated; results are partial — ${truncationHint}.\n`);
|
|
158
|
+
}
|
|
121
159
|
return out;
|
|
122
160
|
}
|
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.23.0";
|