gangtise-openapi-cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,20 @@
4
4
 
5
5
  ## Changelog
6
6
 
7
+ ### v0.16.0 — 2026-06-12
8
+
9
+ **新增接口(参考数据 · 常量查询,均免积分)**
10
+ - `reference constant-category` — 查询常量分类:全量导出常量分类及各分类适用于哪些接口的哪些参数(7 个分类:中信/申万/Gangtise 行业、国内城市、A股/港股公告分类、区域)
11
+ - `reference constant-list --category <code>` — 查询常量值:按分类导出全量常量(`constantId` / `constantName`,树形分类含 `children` 嵌套)
12
+ - `reference concept-search --keyword <kw>` — 查询题材 ID:按名称/拼音/分组名搜索,返回 `conceptId`(供 `alternative concept-info / concept-securities`、`ai theme-tracking` 使用)
13
+ - `reference sector-search --keyword <kw>` — 查询板块 ID:返回 `sectorId` + `hierarchy` 层级路径
14
+ - `reference sector-constituents --sector-id <id>` — 查询板块成分股:返回该板块全量成分股(`gtsCode` / `gtsName`);注意 sectorId 必须来自 sector-search,题材 conceptId 查不到成分
15
+
16
+ **接口变更(Breaking)**
17
+ - 移除已被新 API 覆盖的 6 个本地 lookup 子命令及静态数据:`lookup research-area / industry / region / announcement-category / theme-id / industry-code list`,请改用 `reference constant-list` / `reference concept-search` / `reference sector-constituents`(申万行业代码 `821xxx.SWI` 全量:`sector-constituents --sector-id 2000000014`,即申万一级行业指数板块)
18
+ - `lookup` 仅保留 2 个 API 未覆盖的本地表:`broker-org` / `meeting-org`
19
+ - 路演/调研/策略会/论坛 list 新增 `--location <id>` 按城市过滤(domesticCity 常量 ID;实测 2026-06-12 服务端过滤暂未生效)
20
+
7
21
  ### v0.15.0 — 2026-05-29
8
22
 
9
23
  **新增接口**
@@ -125,6 +139,22 @@ cd gangtise-openapi-cli
125
139
  npm install
126
140
  npm run dev -- --help
127
141
  ```
142
+
143
+ ## 发布
144
+
145
+ npm 发版通过 GitHub Actions Trusted Publishing 完成,不需要 `NPM_TOKEN`。npm 包设置里的 Trusted Publisher 需要匹配本仓库和 workflow 文件名 `publish.yml`。
146
+
147
+ ```bash
148
+ npm version patch --no-git-tag-version
149
+ npm run prepare
150
+ VERSION=$(node -p "require('./package.json').version")
151
+ git commit -am "chore: release v$VERSION"
152
+ git tag -a "v$VERSION" -m "v$VERSION" # 必须 annotated:--follow-tags 不推 lightweight tag
153
+ git push --follow-tags
154
+ ```
155
+
156
+ 推送 `v*` tag 后,`.github/workflows/publish.yml` 会在 GitHub-hosted runner 上使用 OIDC 发布到 `https://registry.npmjs.org/`。也可以从 GitHub Actions 页面手动运行该 workflow。
157
+
128
158
  ## 版本更新
129
159
 
130
160
  查看当前版本(自动与线上版本比对):
@@ -219,7 +249,7 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
219
249
  | 模块 | 子命令 | 说明 |
220
250
  |------|--------|------|
221
251
  | **Auth** | `login` / `status` | 认证登录、状态查询 |
222
- | **Lookup** | `research-area list` / `broker-org list` / `meeting-org list` / `industry list` / `industry-code list` / `region list` / `announcement-category list` / `theme-id list` | 枚举速查(内置,无需额外文档) |
252
+ | **Lookup** | `broker-org list` / `meeting-org list` | 本地枚举表(API 未覆盖的部分;行业/区域/公告分类/题材/申万行业代码改用 Reference 接口) |
223
253
  | **Insight** | `opinion list` | 内资机构观点 |
224
254
  | | `summary list` / `download` | 纪要(含下载,支持 `--file-type` 选原始/HTML) |
225
255
  | | `roadshow list` | 路演 |
@@ -233,6 +263,11 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
233
263
  | | `foreign-opinion list` | 外资机构观点 |
234
264
  | | `independent-opinion list` / `download` | 外资独立分析师观点(含原文/翻译HTML下载) |
235
265
  | **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
266
+ | | `constant-category` | 常量分类列表(含各分类适用的接口与参数) |
267
+ | | `constant-list` | 按分类导出常量值全量列表(行业/城市/公告分类/区域等) |
268
+ | | `concept-search` | 题材 ID 搜索(名称/拼音/分组名匹配) |
269
+ | | `sector-search` | 板块 ID 搜索(返回层级路径) |
270
+ | | `sector-constituents` | 板块成分股查询 |
236
271
  | **Quote** | `day-kline` / `day-kline-hk` / `day-kline-us` | A股/港股/美股历史日K线 |
237
272
  | | `index-day-kline` | 沪深京指数日K线 |
238
273
  | | `minute-kline` | A股分钟K线 |
@@ -286,13 +321,14 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
286
321
  先查枚举/参数:
287
322
 
288
323
  ```bash
289
- gangtise lookup research-area list
290
- gangtise lookup broker-org list
291
- gangtise lookup meeting-org list
292
- gangtise lookup industry list
293
- gangtise lookup region list # 外资研报区域
294
- gangtise lookup announcement-category list # 公告分类
295
- gangtise lookup industry-code list # 申万行业代码(用于 security-clue --gts-code)
324
+ gangtise reference constant-category # 有哪些常量分类、各用于哪些参数
325
+ gangtise reference constant-list --category citicIndustry # 中信行业(--industry / --research-area)
326
+ gangtise reference constant-list --category swIndustry # 申万行业
327
+ gangtise reference constant-list --category regionCategory # 外资研报区域
328
+ gangtise reference constant-list --category aShareAnnouncementCategory # A股公告分类(树形)
329
+ gangtise reference sector-constituents --sector-id 2000000014 # 申万行业代码 821xxx.SWI 全量(security-clue --gts-code 用)
330
+ gangtise lookup broker-org list # 券商机构(本地表)
331
+ gangtise lookup meeting-org list # 会议机构(本地表)
296
332
  ```
297
333
 
298
334
  再调用业务命令:
@@ -420,6 +456,19 @@ gangtise reference securities-search --keyword "贵州茅台" --category stock
420
456
  gangtise reference securities-search --keyword "600519" --category stock
421
457
  gangtise reference securities-search --keyword gzmt --top 5
422
458
  gangtise reference securities-search --keyword "银行" --category stock --category index
459
+
460
+ # 常量查询:先看分类,再按分类导出全量常量值
461
+ gangtise reference constant-category --format json
462
+ gangtise reference constant-list --category citicIndustry --format json
463
+ gangtise reference constant-list --category aShareAnnouncementCategory --format json # 树形,含 children
464
+
465
+ # 题材 ID 搜索(供 concept-info / concept-securities / theme-tracking 使用)
466
+ gangtise reference concept-search --keyword 机器人 --top 3 --format json
467
+ gangtise reference concept-search --keyword jqr # 拼音首字母
468
+
469
+ # 板块:先搜板块 ID,再查成分股(sectorId 必须来自 sector-search)
470
+ gangtise reference sector-search --keyword 半导体设备 --format json
471
+ gangtise reference sector-constituents --sector-id 1000001005 --format json
423
472
  ```
424
473
 
425
474
  ### Quote
@@ -577,7 +626,7 @@ gangtise alternative edb-data \
577
626
  --output ./indicator.csv
578
627
 
579
628
  # 题材指数:先查 conceptId(与 theme-id 共用 ID 体系),再拉画像 / 成分股
580
- gangtise lookup theme-id list | grep 机器人 # → 121000130
629
+ gangtise reference concept-search --keyword 机器人 --format json # → 121000130
581
630
  gangtise alternative concept-info --concept-id 121000130 --format json
582
631
  # 题材成分股(题材深度 F8,按分组返回,标记重点个股)
583
632
  gangtise alternative concept-securities --concept-id 121000130 --format json
package/dist/src/cli.js CHANGED
@@ -46,6 +46,35 @@ async function runDownload(client, endpointKey, query, options) {
46
46
  const resolved = options.resolveOutputPath ? await options.resolveOutputPath(result) : undefined;
47
47
  await saveDownloadResult(result, options.fallbackName, resolved);
48
48
  }
49
+ /**
50
+ * Register a download subcommand. All download commands share one shape: a
51
+ * required id option, optionally --file-type / --content-type, then --output.
52
+ * `idField` doubles as the commander option key and the query/title-cache
53
+ * field, so it must stay the camelCase twin of `idOption`.
54
+ */
55
+ function addDownloadCommand(parent, spec) {
56
+ const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id>`);
57
+ if (spec.fileType?.required)
58
+ cmd.requiredOption("--file-type <number>", spec.fileType.description);
59
+ else if (spec.fileType)
60
+ cmd.option("--file-type <number>", spec.fileType.description, spec.fileType.default);
61
+ if (spec.contentTypeDescription)
62
+ cmd.requiredOption("--content-type <type>", spec.contentTypeDescription);
63
+ cmd.option("--output <path>").action((options) => withClient(async (client) => {
64
+ const id = options[spec.idField];
65
+ const qp = { [spec.idField]: id };
66
+ if (spec.fileType && options.fileType)
67
+ qp.fileType = parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 });
68
+ if (spec.contentTypeDescription)
69
+ qp.contentType = options.contentType;
70
+ const titleList = spec.titleListEndpoint;
71
+ await runDownload(client, spec.endpointKey, qp, {
72
+ output: options.output,
73
+ fallbackName: `${spec.fallbackPrefix}-${id}`,
74
+ resolveOutputPath: titleList ? (result) => resolveTitle(client, result, titleList, spec.idField, id) : undefined,
75
+ });
76
+ }));
77
+ }
49
78
  function addTimeFilters(command) {
50
79
  return command
51
80
  .option("--from <number>", "Starting offset", "0")
@@ -79,16 +108,15 @@ program
79
108
  const cache = await readTokenCache(config.tokenCachePath);
80
109
  await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache }, parseOutputFormat(options.format));
81
110
  }));
82
- const lookup = new Command("lookup").description("Lookup helper APIs");
83
- lookup
84
- .addCommand(new Command("research-area").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.research-areas.list")))))
85
- .addCommand(new Command("broker-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.broker-orgs.list")))))
86
- .addCommand(new Command("meeting-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.meeting-orgs.list")))))
87
- .addCommand(new Command("industry").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.industries.list")))))
88
- .addCommand(new Command("region").description("Foreign report region codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.regions.list")))))
89
- .addCommand(new Command("announcement-category").description("Announcement category codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.announcement-categories.list")))))
90
- .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((options) => emit(options, (client) => client.call("lookup.industry-codes.list")))))
91
- .addCommand(new Command("theme-id").description("Theme IDs for theme-tracking --theme-id").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.theme-ids.list")))));
111
+ const lookup = new Command("lookup").description("Local lookup tables (IDs not covered by 'reference constant-list')");
112
+ const addLookupList = (name, endpointKey, description) => {
113
+ const cmd = new Command(name);
114
+ if (description)
115
+ cmd.description(description);
116
+ lookup.addCommand(cmd.addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call(endpointKey)))));
117
+ };
118
+ addLookupList("broker-org", "lookup.broker-orgs.list");
119
+ addLookupList("meeting-org", "lookup.meeting-orgs.list");
92
120
  program.addCommand(lookup);
93
121
  const insight = new Command("insight").description("Insight APIs");
94
122
  const opinion = new Command("opinion");
@@ -115,21 +143,13 @@ addTimeFilters(summary.command("list").option("--search-type <number>", "Search
115
143
  researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
116
144
  categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
117
145
  }), { endpointKey: "insight.summary.list", idField: "summaryId" }));
118
- summary.command("download").requiredOption("--summary-id <id>").option("--file-type <number>", "File type: 1=original(default) 2=HTML; only affects meeting platform summaries").option("--output <path>").action((options) => withClient(async (client) => {
119
- const qp = { summaryId: options.summaryId };
120
- if (options.fileType)
121
- qp.fileType = parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 });
122
- await runDownload(client, "insight.summary.download", qp, {
123
- output: options.output,
124
- fallbackName: `summary-${options.summaryId}`,
125
- resolveOutputPath: (result) => resolveTitle(client, result, "insight.summary.list", "summaryId", options.summaryId),
126
- });
127
- }));
128
- 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((options) => emit(options, (client) => client.call(endpointKey, {
146
+ addDownloadCommand(summary, { endpointKey: "insight.summary.download", idOption: "--summary-id", idField: "summaryId", fallbackPrefix: "summary", fileType: { description: "File type: 1=original(default) 2=HTML; only affects meeting platform summaries" }, titleListEndpoint: "insight.summary.list" });
147
+ 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("--location <id>", "Location ID (domesticCity constant, via 'reference constant-list')", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call(endpointKey, {
129
148
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
130
149
  researchAreaList: maybeArray(options.researchArea), institutionList: maybeArray(options.institution), securityList: maybeArray(options.security),
131
150
  categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
132
151
  brokerTypeList: maybeArray(options.brokerType), objectList: maybeArray(options.object), permission: options.permission.length ? options.permission : undefined,
152
+ locationList: maybeArray(options.location),
133
153
  })));
134
154
  addScheduleList(roadshow, "insight.roadshow.list");
135
155
  addScheduleList(siteVisit, "insight.site-visit.list");
@@ -143,13 +163,7 @@ addTimeFilters(research.command("list").option("--search-type <number>", "Search
143
163
  ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
144
164
  maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
145
165
  }), { endpointKey: "insight.research.list", idField: "reportId" }));
146
- research.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action((options) => withClient(async (client) => {
147
- await runDownload(client, "insight.research.download", { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
148
- output: options.output,
149
- fallbackName: `research-${options.reportId}`,
150
- resolveOutputPath: (result) => resolveTitle(client, result, "insight.research.list", "reportId", options.reportId),
151
- });
152
- }));
166
+ addDownloadCommand(research, { endpointKey: "insight.research.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "research", fileType: { description: "File type: 1=PDF 2=Markdown", default: "1" }, titleListEndpoint: "insight.research.list" });
153
167
  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((options) => emit(options, (client) => client.call("insight.foreign-report.list", {
154
168
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
155
169
  searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
@@ -158,26 +172,14 @@ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "S
158
172
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
159
173
  minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
160
174
  }), { endpointKey: "insight.foreign-report.list", idField: "reportId" }));
161
- 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((options) => withClient(async (client) => {
162
- await runDownload(client, "insight.foreign-report.download", { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
163
- output: options.output,
164
- fallbackName: `foreign-report-${options.reportId}`,
165
- resolveOutputPath: (result) => resolveTitle(client, result, "insight.foreign-report.list", "reportId", options.reportId),
166
- });
167
- }));
175
+ addDownloadCommand(foreignReport, { endpointKey: "insight.foreign-report.download", idOption: "--report-id", idField: "reportId", fallbackPrefix: "foreign-report", fileType: { description: "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", default: "1" }, titleListEndpoint: "insight.foreign-report.list" });
168
176
  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((options) => emit(options, (client) => client.call("insight.announcement.list", {
169
177
  from: parseFrom(options.from), size: parseSize(options.size),
170
178
  startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
171
179
  searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
172
180
  securityList: maybeArray(options.security), announcementTypeList: maybeArray(options.announcementType), categoryList: maybeArray(options.category),
173
181
  }), { endpointKey: "insight.announcement.list", idField: "announcementId" }));
174
- announcement.command("download").requiredOption("--announcement-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action((options) => withClient(async (client) => {
175
- await runDownload(client, "insight.announcement.download", { announcementId: options.announcementId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
176
- output: options.output,
177
- fallbackName: `announcement-${options.announcementId}`,
178
- resolveOutputPath: (result) => resolveTitle(client, result, "insight.announcement.list", "announcementId", options.announcementId),
179
- });
180
- }));
182
+ addDownloadCommand(announcement, { endpointKey: "insight.announcement.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement", fileType: { description: "File type: 1=PDF 2=Markdown", default: "1" }, titleListEndpoint: "insight.announcement.list" });
181
183
  addTimeFilters(announcementHk.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. 01913.HK)", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement-hk.list", {
182
184
  from: parseFrom(options.from), size: parseSize(options.size),
183
185
  startTime: options.startTime, endTime: options.endTime,
@@ -186,13 +188,7 @@ addTimeFilters(announcementHk.command("list").option("--search-type <number>", "
186
188
  keyword: options.keyword,
187
189
  securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
188
190
  }), { endpointKey: "insight.announcement-hk.list", idField: "announcementId" }));
189
- announcementHk.command("download").requiredOption("--announcement-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
190
- await runDownload(client, "insight.announcement-hk.download", { announcementId: options.announcementId }, {
191
- output: options.output,
192
- fallbackName: `announcement-hk-${options.announcementId}`,
193
- resolveOutputPath: (result) => resolveTitle(client, result, "insight.announcement-hk.list", "announcementId", options.announcementId),
194
- });
195
- }));
191
+ addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", titleListEndpoint: "insight.announcement-hk.list" });
196
192
  addTimeFilters(foreignOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-opinion.list", {
197
193
  from: parseFrom(options.from), size: parseSize(options.size),
198
194
  startTime: options.startTime, endTime: options.endTime,
@@ -210,12 +206,7 @@ addTimeFilters(independentOpinion.command("list").option("--rank-type <number>",
210
206
  industryList: maybeArray(options.industry), securityList: maybeArray(options.security),
211
207
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
212
208
  })));
213
- independentOpinion.command("download").requiredOption("--independent-opinion-id <id>").requiredOption("--file-type <number>", "File type: 1=original HTML 2=CN-translated HTML").option("--output <path>").action((options) => withClient(async (client) => {
214
- await runDownload(client, "insight.independent-opinion.download", { independentOpinionId: options.independentOpinionId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
215
- output: options.output,
216
- fallbackName: `independent-opinion-${options.independentOpinionId}`,
217
- });
218
- }));
209
+ addDownloadCommand(independentOpinion, { endpointKey: "insight.independent-opinion.download", idOption: "--independent-opinion-id", idField: "independentOpinionId", fallbackPrefix: "independent-opinion", fileType: { description: "File type: 1=original HTML 2=CN-translated HTML", required: true } });
219
210
  insight.addCommand(opinion);
220
211
  insight.addCommand(summary);
221
212
  insight.addCommand(roadshow);
@@ -306,7 +297,7 @@ ai.command("earnings-review").requiredOption("--security-code <code>").requiredO
306
297
  }
307
298
  }));
308
299
  ai.command("earnings-review-check").requiredOption("--data-id <id>", "dataId from earnings-review").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient((client) => checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseOutputFormat(options.format), options.output)));
309
- ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use lookup theme-id list)").requiredOption("--date <date>", "Date (yyyy-MM-dd)").option("--type <name>", "Report type: morning/night", collectList, []).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
300
+ ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use 'reference concept-search')").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((options) => emit(options, (client) => {
310
301
  const typeList = options.type.length ? options.type : undefined;
311
302
  return client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList });
312
303
  }));
@@ -359,32 +350,25 @@ reference.command("securities-search").requiredOption("--keyword <text>", "Searc
359
350
  category: options.category.length ? options.category : undefined,
360
351
  top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
361
352
  })));
353
+ reference.command("constant-category").description("List constant categories and which API params accept them").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.constant-category")));
354
+ reference.command("constant-list").requiredOption("--category <code>", "Category code from 'reference constant-category' (e.g. citicIndustry/swIndustry/regionCategory)").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.constant-list", { category: options.category })));
355
+ reference.command("concept-search").requiredOption("--keyword <text>", "Search keyword (name/pinyin/group name)").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.concept-search", {
356
+ keyword: options.keyword,
357
+ top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
358
+ })));
359
+ reference.command("sector-search").option("--keyword <text>", "Search keyword (name/pinyin)").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.sector-search", {
360
+ keyword: options.keyword,
361
+ top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
362
+ })));
363
+ reference.command("sector-constituents").requiredOption("--sector-id <id>", "Sector ID from 'reference sector-search'").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.sector-constituents", { sectorId: options.sectorId })));
362
364
  program.addCommand(reference);
363
365
  const vault = new Command("vault").description("Vault APIs");
364
366
  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" }));
365
- vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
366
- await runDownload(client, "vault.drive.download", { fileId: options.fileId }, {
367
- output: options.output,
368
- fallbackName: `file-${options.fileId}`,
369
- resolveOutputPath: (result) => resolveTitle(client, result, "vault.drive.list", "fileId", options.fileId),
370
- });
371
- }));
367
+ addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
372
368
  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" }));
373
- vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action((options) => withClient(async (client) => {
374
- await runDownload(client, "vault.record.download", { recordId: options.recordId, contentType: options.contentType }, {
375
- output: options.output,
376
- fallbackName: `record-${options.recordId}`,
377
- resolveOutputPath: (result) => resolveTitle(client, result, "vault.record.list", "recordId", options.recordId),
378
- });
379
- }));
369
+ 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" });
380
370
  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" }));
381
- vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action((options) => withClient(async (client) => {
382
- await runDownload(client, "vault.my-conference.download", { conferenceId: options.conferenceId, contentType: options.contentType }, {
383
- output: options.output,
384
- fallbackName: `conference-${options.conferenceId}`,
385
- resolveOutputPath: (result) => resolveTitle(client, result, "vault.my-conference.list", "conferenceId", options.conferenceId),
386
- });
387
- }));
371
+ 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" });
388
372
  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))));
389
373
  vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Rows to return", "20").option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
390
374
  vault.command("stock-pool-list").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.list", {})));
@@ -412,8 +396,8 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
412
396
  }
413
397
  await printData(data, parseOutputFormat(options.format), options.output);
414
398
  }));
415
- alternative.command("concept-info").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise lookup theme-id list'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-info", { conceptId: options.conceptId })));
416
- alternative.command("concept-securities").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise lookup theme-id list'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-securities", { conceptId: options.conceptId })));
399
+ alternative.command("concept-info").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-info", { conceptId: options.conceptId })));
400
+ alternative.command("concept-securities").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-securities", { conceptId: options.conceptId })));
417
401
  program.addCommand(alternative);
418
402
  program.command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
419
403
  const endpoint = ENDPOINTS[endpointKey];
@@ -101,14 +101,8 @@ export class GangtiseClient {
101
101
  }
102
102
  async readLocalLookup(endpoint) {
103
103
  const keyMapping = {
104
- "lookup.research-areas.list": "research-areas",
105
104
  "lookup.broker-orgs.list": "broker-orgs",
106
105
  "lookup.meeting-orgs.list": "meeting-orgs",
107
- "lookup.industries.list": "industries",
108
- "lookup.regions.list": "regions",
109
- "lookup.announcement-categories.list": "announcement-categories",
110
- "lookup.industry-codes.list": "industry-codes",
111
- "lookup.theme-ids.list": "theme-ids",
112
106
  };
113
107
  const lookupKey = keyMapping[endpoint.key];
114
108
  if (lookupKey) {
@@ -8,13 +8,6 @@ export const ENDPOINTS = {
8
8
  description: "Get access token",
9
9
  },
10
10
  // ─── lookup (served from local data, not HTTP) ───
11
- "lookup.research-areas.list": {
12
- key: "lookup.research-areas.list",
13
- method: "GET",
14
- path: "/guide/research-area-local",
15
- kind: "json",
16
- description: "List research areas from local docs",
17
- },
18
11
  "lookup.broker-orgs.list": {
19
12
  key: "lookup.broker-orgs.list",
20
13
  method: "GET",
@@ -29,41 +22,6 @@ export const ENDPOINTS = {
29
22
  kind: "json",
30
23
  description: "List meeting orgs from local docs",
31
24
  },
32
- "lookup.industries.list": {
33
- key: "lookup.industries.list",
34
- method: "GET",
35
- path: "/guide/industries-local",
36
- kind: "json",
37
- description: "List industries from local docs",
38
- },
39
- "lookup.regions.list": {
40
- key: "lookup.regions.list",
41
- method: "GET",
42
- path: "/guide/regions-local",
43
- kind: "json",
44
- description: "List regions from local docs",
45
- },
46
- "lookup.announcement-categories.list": {
47
- key: "lookup.announcement-categories.list",
48
- method: "GET",
49
- path: "/guide/announcement-categories-local",
50
- kind: "json",
51
- description: "List announcement categories from local docs",
52
- },
53
- "lookup.industry-codes.list": {
54
- key: "lookup.industry-codes.list",
55
- method: "GET",
56
- path: "/guide/industry-codes-local",
57
- kind: "json",
58
- description: "List Shenwan industry codes from local docs",
59
- },
60
- "lookup.theme-ids.list": {
61
- key: "lookup.theme-ids.list",
62
- method: "GET",
63
- path: "/guide/theme-ids-local",
64
- kind: "json",
65
- description: "List theme IDs from local docs",
66
- },
67
25
  // ─── insight ───
68
26
  "insight.opinion.list": {
69
27
  key: "insight.opinion.list",
@@ -211,6 +169,41 @@ export const ENDPOINTS = {
211
169
  kind: "json",
212
170
  description: "Search GTS codes (securities)",
213
171
  },
172
+ "reference.constant-category": {
173
+ key: "reference.constant-category",
174
+ method: "GET",
175
+ path: "/application/open-reference/constants/category",
176
+ kind: "json",
177
+ description: "List constant categories and their API usage scopes",
178
+ },
179
+ "reference.constant-list": {
180
+ key: "reference.constant-list",
181
+ method: "POST",
182
+ path: "/application/open-reference/constants/getList",
183
+ kind: "json",
184
+ description: "List all constant values of a category",
185
+ },
186
+ "reference.concept-search": {
187
+ key: "reference.concept-search",
188
+ method: "POST",
189
+ path: "/application/open-reference/concepts/search",
190
+ kind: "json",
191
+ description: "Search concept (theme) IDs by keyword",
192
+ },
193
+ "reference.sector-search": {
194
+ key: "reference.sector-search",
195
+ method: "POST",
196
+ path: "/application/open-reference/sectors/search",
197
+ kind: "json",
198
+ description: "Search sector IDs by keyword",
199
+ },
200
+ "reference.sector-constituents": {
201
+ key: "reference.sector-constituents",
202
+ method: "POST",
203
+ path: "/application/open-reference/sectors/constituents",
204
+ kind: "json",
205
+ description: "List constituent securities of a sector",
206
+ },
214
207
  // ─── quote ───
215
208
  "quote.day-kline": {
216
209
  key: "quote.day-kline",
@@ -21,6 +21,13 @@ const ERROR_HINTS = {
21
21
  "8000016": "开发账号状态异常。",
22
22
  "8000018": "开发账号已到期。",
23
23
  "903301": "今日调用次数已达上限。",
24
+ "410110": "异步内容生成中,稍后用对应 *-check 命令查询。",
25
+ "410111": "异步内容生成失败(终态),请更换参数后重新提交。",
26
+ "410004": "数据未找到,请检查查询条件。",
27
+ "430004": "下载失败(官方未文档化错误码),请确认 reportId 有效或更换 --file-type 重试。",
28
+ "430007": "行情查询超出限制,请缩短日期范围。",
29
+ "433007": "数据源不匹配,请检查 resourceType 与 sourceId 组合。",
30
+ "10011401": "白名单未开通,请联系管理员。",
24
31
  };
25
32
  export class ApiError extends CliError {
26
33
  code;
@@ -1,13 +1,7 @@
1
1
  const cache = new Map();
2
2
  const loaders = {
3
- "research-areas": () => import("./research-areas.js"),
4
3
  "broker-orgs": () => import("./broker-orgs.js"),
5
4
  "meeting-orgs": () => import("./meeting-orgs.js"),
6
- "industries": () => import("./industries.js"),
7
- "regions": () => import("./regions.js"),
8
- "announcement-categories": () => import("./announcement-categories.js"),
9
- "industry-codes": () => import("./industry-codes.js"),
10
- "theme-ids": () => import("./theme-ids.js"),
11
5
  };
12
6
  export async function getLookupData(key) {
13
7
  if (cache.has(key))
@@ -29,5 +29,10 @@ export function normalizeRows(value) {
29
29
  const hasMeta = Object.keys(meta).length > 0;
30
30
  return hasMeta ? { ...meta, list: chatRoomList } : chatRoomList;
31
31
  }
32
+ if (Array.isArray(record.constants)) {
33
+ const { constants, ...meta } = record;
34
+ const hasMeta = Object.keys(meta).length > 0;
35
+ return hasMeta ? { ...meta, list: constants } : constants;
36
+ }
32
37
  return value;
33
38
  }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.15.0";
2
+ export const CLI_VERSION = "0.16.0";
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "CLI for Gangtise OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+ssh://git@github.com/gangtiser/gangtise-openapi-cli.git"
8
+ "url": "git+https://github.com/gangtiser/gangtise-openapi-cli.git"
9
9
  },
10
10
  "bugs": {
11
11
  "url": "https://github.com/gangtiser/gangtise-openapi-cli/issues"
12
12
  },
13
13
  "homepage": "https://github.com/gangtiser/gangtise-openapi-cli#readme",
14
14
  "type": "module",
15
+ "publishConfig": {
16
+ "registry": "https://registry.npmjs.org/"
17
+ },
15
18
  "bin": {
16
19
  "gangtise": "dist/src/cli.js"
17
20
  },