gangtise-openapi-cli 0.14.4 → 0.15.1

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,30 @@
4
4
 
5
5
  ## Changelog
6
6
 
7
+ ### v0.15.0 — 2026-05-29
8
+
9
+ **新增接口**
10
+ - `alternative concept-info` — 题材指数基本信息:返回题材整体画像(定义 / 投资逻辑 / 行业空间 / 竞争格局 / 催化事件)。按 `--concept-id` 查询,仅返回最新截面数据,不支持历史回溯
11
+ - `alternative concept-securities` — 题材指数成分股(题材深度 F8):按分组结构返回当前成分股,每只含是否重点个股 `isKey` 与纳入理由 `inclusionReason`。按 `--concept-id` 查询
12
+
13
+ **接口变更**
14
+ - `quote index-day-kline` 返回字段新增 `securityName`(指数名称,如"上证指数")
15
+
16
+ > `--concept-id` 与主题跟踪 `ai theme-tracking --theme-id` 共用同一套题材 ID 体系,可用 `gangtise lookup theme-id list` 按名称查询(如 机器人 → `121000130`)。
17
+
18
+ ### v0.14.4 — 2026-05-29
19
+
20
+ **Bug fix(全市场 K 线分片容错)**
21
+ - `quote day-kline --security all` 等全市场查询的日期分片改为容错:部分分片失败时返回已成功分片的数据并标记 `partial: true` + `failedShards`(失败的日期区间),同时向 stderr 告警;只有全部分片失败才抛错。此前为 fail-fast,单片失败会让整次查询失败,或在异常路径上被误判为空结果。
22
+
23
+ ### v0.14.3 — 2026-05-29
24
+
25
+ **性能 / 健壮性**
26
+ - 标题缓存按端点封顶(5000 条/端点)并清理过期项,修复 `title-cache.json` 无上限增长(曾达 ~58MB)拖慢启动的问题
27
+ - 下载接口遇鉴权失效(`8000014` / `8000015`)自动刷新 token 并重试一次(此前仅普通 JSON 调用具备 token 自愈)
28
+ - CLI handler 抽出 `emit` / `withClient` 公共封装去除重复样板;CSV 转义逻辑去重;翻页与 K 线分片统一走 `GANGTISE_PAGE_CONCURRENCY` 并发控制
29
+ - 补齐多个 core 模块的单元测试
30
+
7
31
  ### v0.14.2 — 2026-05-22
8
32
 
9
33
  **Bug fix(A 股 / HK 全市场 K 线同源问题)**
@@ -101,6 +125,22 @@ cd gangtise-openapi-cli
101
125
  npm install
102
126
  npm run dev -- --help
103
127
  ```
128
+
129
+ ## 发布
130
+
131
+ npm 发版通过 GitHub Actions Trusted Publishing 完成,不需要 `NPM_TOKEN`。npm 包设置里的 Trusted Publisher 需要匹配本仓库和 workflow 文件名 `publish.yml`。
132
+
133
+ ```bash
134
+ npm version patch --no-git-tag-version
135
+ npm run prepare
136
+ VERSION=$(node -p "require('./package.json').version")
137
+ git commit -am "chore: release v$VERSION"
138
+ git tag "v$VERSION"
139
+ git push --follow-tags
140
+ ```
141
+
142
+ 推送 `v*` tag 后,`.github/workflows/publish.yml` 会在 GitHub-hosted runner 上使用 OIDC 发布到 `https://registry.npmjs.org/`。也可以从 GitHub Actions 页面手动运行该 workflow。
143
+
104
144
  ## 版本更新
105
145
 
106
146
  查看当前版本(自动与线上版本比对):
@@ -240,6 +280,8 @@ cp -r gangtise-openapi ~/.hermes/skills/gangtise-openapi
240
280
  | | `stock-pool-list` / `stock-pool-stocks` | 自选股股票池列表与证券明细 |
241
281
  | **Alternative** | `edb-search` | 行业指标搜索(按关键词匹配,返回 indicatorId 等元信息) |
242
282
  | | `edb-data` | 行业指标时序数据(批量拉取,最多10个指标) |
283
+ | | `concept-info` | 题材指数基本信息(投资逻辑/行业空间/竞争格局/催化事件) |
284
+ | | `concept-securities` | 题材指数成分股(题材深度F8,按分组,标记重点个股) |
243
285
  | **Raw** | `call` | 原始接口调用(可访问任意 endpoint) |
244
286
 
245
287
  ## 命令概览
@@ -549,6 +591,12 @@ gangtise alternative edb-data \
549
591
  --end-date 2024-12-31 \
550
592
  --format csv \
551
593
  --output ./indicator.csv
594
+
595
+ # 题材指数:先查 conceptId(与 theme-id 共用 ID 体系),再拉画像 / 成分股
596
+ gangtise lookup theme-id list | grep 机器人 # → 121000130
597
+ gangtise alternative concept-info --concept-id 121000130 --format json
598
+ # 题材成分股(题材深度 F8,按分组返回,标记重点个股)
599
+ gangtise alternative concept-securities --concept-id 121000130 --format json
552
600
  ```
553
601
 
554
602
  ### Raw
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")
@@ -80,15 +109,20 @@ program
80
109
  await printData({ hasEnvToken: Boolean(config.token), hasCachedToken: Boolean(cache?.accessToken), cache }, parseOutputFormat(options.format));
81
110
  }));
82
111
  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")))));
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("research-area", "lookup.research-areas.list");
119
+ addLookupList("broker-org", "lookup.broker-orgs.list");
120
+ addLookupList("meeting-org", "lookup.meeting-orgs.list");
121
+ addLookupList("industry", "lookup.industries.list");
122
+ addLookupList("region", "lookup.regions.list", "Foreign report region codes");
123
+ addLookupList("announcement-category", "lookup.announcement-categories.list", "Announcement category codes");
124
+ addLookupList("industry-code", "lookup.industry-codes.list", "Shenwan industry codes for security-clue --gts-code");
125
+ addLookupList("theme-id", "lookup.theme-ids.list", "Theme IDs for theme-tracking --theme-id");
92
126
  program.addCommand(lookup);
93
127
  const insight = new Command("insight").description("Insight APIs");
94
128
  const opinion = new Command("opinion");
@@ -115,16 +149,7 @@ addTimeFilters(summary.command("list").option("--search-type <number>", "Search
115
149
  researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
116
150
  categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
117
151
  }), { 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
- }));
152
+ 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" });
128
153
  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, {
129
154
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
130
155
  researchAreaList: maybeArray(options.researchArea), institutionList: maybeArray(options.institution), securityList: maybeArray(options.security),
@@ -143,13 +168,7 @@ addTimeFilters(research.command("list").option("--search-type <number>", "Search
143
168
  ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
144
169
  maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
145
170
  }), { 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
- }));
171
+ 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
172
  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
173
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
155
174
  searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
@@ -158,26 +177,14 @@ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "S
158
177
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
159
178
  minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
160
179
  }), { 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
- }));
180
+ 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
181
  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
182
  from: parseFrom(options.from), size: parseSize(options.size),
170
183
  startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
171
184
  searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
172
185
  securityList: maybeArray(options.security), announcementTypeList: maybeArray(options.announcementType), categoryList: maybeArray(options.category),
173
186
  }), { 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
- }));
187
+ 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
188
  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
189
  from: parseFrom(options.from), size: parseSize(options.size),
183
190
  startTime: options.startTime, endTime: options.endTime,
@@ -186,13 +193,7 @@ addTimeFilters(announcementHk.command("list").option("--search-type <number>", "
186
193
  keyword: options.keyword,
187
194
  securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
188
195
  }), { 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
- }));
196
+ addDownloadCommand(announcementHk, { endpointKey: "insight.announcement-hk.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-hk", titleListEndpoint: "insight.announcement-hk.list" });
196
197
  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
198
  from: parseFrom(options.from), size: parseSize(options.size),
198
199
  startTime: options.startTime, endTime: options.endTime,
@@ -210,12 +211,7 @@ addTimeFilters(independentOpinion.command("list").option("--rank-type <number>",
210
211
  industryList: maybeArray(options.industry), securityList: maybeArray(options.security),
211
212
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
212
213
  })));
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
- }));
214
+ 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
215
  insight.addCommand(opinion);
220
216
  insight.addCommand(summary);
221
217
  insight.addCommand(roadshow);
@@ -362,29 +358,11 @@ reference.command("securities-search").requiredOption("--keyword <text>", "Searc
362
358
  program.addCommand(reference);
363
359
  const vault = new Command("vault").description("Vault APIs");
364
360
  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
- }));
361
+ addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
372
362
  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
- }));
363
+ 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
364
  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
- }));
365
+ 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
366
  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
367
  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
368
  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,6 +390,8 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
412
390
  }
413
391
  await printData(data, parseOutputFormat(options.format), options.output);
414
392
  }));
393
+ 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 })));
394
+ 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 })));
415
395
  program.addCommand(alternative);
416
396
  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) => {
417
397
  const endpoint = ENDPOINTS[endpointKey];
@@ -537,4 +537,18 @@ export const ENDPOINTS = {
537
537
  kind: "json",
538
538
  description: "Get industry indicator time-series data by indicator ID list",
539
539
  },
540
+ "alternative.concept-info": {
541
+ key: "alternative.concept-info",
542
+ method: "POST",
543
+ path: "/application/open-alternative/concept/info",
544
+ kind: "json",
545
+ description: "Query latest concept (theme index) profile by conceptId",
546
+ },
547
+ "alternative.concept-securities": {
548
+ key: "alternative.concept-securities",
549
+ method: "POST",
550
+ path: "/application/open-alternative/concept/securities",
551
+ kind: "json",
552
+ description: "Query concept (theme index) constituent securities, grouped",
553
+ },
540
554
  };
@@ -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,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.14.4";
2
+ export const CLI_VERSION = "0.15.1";
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.14.4",
3
+ "version": "0.15.1",
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
  },