gangtise-openapi-cli 0.32.0 → 0.33.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
@@ -6,14 +6,15 @@
6
6
 
7
7
  README 仅列最近 5 个版本摘要:
8
8
 
9
- - **v0.32.0 — 2026-08-08**:新增帕米尔专家纪要列表与下载;跟进 2026-08-07 服务端修复——EDE 缺数据不再整列/整行消失(退出码 3 现在专指「代码没被识别」),条件选股重复指标的 `unreliable` 告警随之移除;`--search-type`/`--rank-type`/`--file-type` 等枚举改为本地拦截(非法值曾被服务端静默忽略并返回全量)。
9
+ - **v0.33.0 — 2026-08-09**:四处行为变更,都是把「静默给出看着正常的错结果」改成显式失败——分页端点返回异形首包(含 `data: null`)改退出码 3;`total` 被服务端封顶时探测并标 `totalCapped` + 退出 3(三个 opinion 端点的 `total` 恒为 10000 而实际远不止,全量导出此前会被静默截断);空结果不再在 stdout 留空行、`null` 不再被渲染成一条记录。另补多处帮助文案与 EDE 占位值(个别指标填 `0` 而非 `null`)的说明。
10
+ - **v0.32.0 — 2026-08-08**:新增帕米尔专家纪要列表与下载;跟进 2026-08-07 服务端修复——EDE 缺数据不再整列/整行消失(退出码 3 现在专指「代码没被识别」),条件选股重复指标的 `unreliable` 告警随之移除;`--search-type`/`--rank-type`/`--file-type` 等枚举改为本地拦截(此前传非法值不会报错,会拿到未过滤的全量)。
10
11
  - **v0.31.0 — 2026-08-03**:修复 v0.30.1 的矩阵维度校验误杀「单指标 × 板块 ID」时序查询的回归;EDE 结果不完整或不可信时改以退出码 3 标记(`partial` / `unreliable`)。
11
12
  - **v0.30.1 — 2026-08-02**:修复 `sDate` 吞掉查询日期导致的区间指标静默错数,让条件选股的文本筛选真正可用,并在服务端整行/整列丢数据时给出警告。
12
13
  - **v0.30.0 — 2026-08-02**:适配 EDE 接口重构(`universe` 取代 `securityCodeList`、截面矩阵转置、指标元数据结构化),新增 `indicator screener` 条件选股,并修正复权参数名。
13
- - **v0.29.0 — 2026-07-25**:新增财报日历与 PDF 解析工具,群消息补 `quoteMsg`,并加强大整数 ID 与高积分调用防护。
14
14
 
15
15
  ### 历史里程碑
16
16
 
17
+ - **v0.29.0**:新增财报日历与 PDF 解析工具,群消息补 `quoteMsg`,并加强大整数 ID 与高积分调用防护。
17
18
  - **v0.26.0–v0.27.0**:建立高积分端点 `no-replay`、原子下载与容错分页机制,并补齐 Skill 分发和发布质量门禁。
18
19
  - **v0.22.0–v0.23.0**:统一“省略 `--size` 即拉全量”的分页语义,引入机器可识别的部分结果、Token 自愈,并完成 API 域名迁移与资金流向、机构搜索支持。
19
20
  - **v0.19.0–v0.20.0**:上线 EDE 证券指标接口,扩展美股公告与财务报表,同时加强凭证脱敏、CSV 正确性和分页容错。
@@ -233,8 +234,8 @@ cp -r "$SKILL_SRC" ~/.hermes/skills/gangtise-openapi
233
234
 
234
235
  ```bash
235
236
  gangtise reference constant-category # 有哪些常量分类、各用于哪些参数
236
- gangtise reference constant-list --category citicIndustry # 中信行业(--industry 通用)
237
- gangtise reference constant-list --category gangtiseIndustry # Gangtise 行业 + 方向(--research-area 用)
237
+ gangtise reference constant-list --category citicIndustry # 中信行业(--industry / --research-area 的行业维度都用它)
238
+ gangtise reference constant-list --category gangtiseIndustry # 研究方向 6 条(宏观/策略/固收/金工/海外/其他),不含行业
238
239
  gangtise reference constant-list --category swIndustry # 申万行业
239
240
  gangtise reference constant-list --category regionCategory # 外资研报区域
240
241
  gangtise reference constant-list --category aShareAnnouncementCategory # A股公告分类(树形)
@@ -258,7 +259,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
258
259
  - **HTTP keep-alive**:所有请求复用同一个 `undici.Agent`(连接池 16),避免重复 TLS 握手。
259
260
  - **流式下载**:指定 `--output` 时,二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;50MB PDF 内存占用近乎为零。
260
261
  - **流式输出**:`jsonl`/`csv` 格式且 `--output` 指定时,超过 1000 行自动切换为逐行写盘,避免一次性构建百 MB 字符串。
261
- - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**贵档端点例外**(one-pager 等生成/提交类 + `tool file-parse` 提交 + 50/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载,共 18 个):5xx/超时不重放——按次计费不幂等,重放即重复扣分;仅连接失败、429 与 token 自愈重试。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(该码 2026-08-01 前还兼表「查询无数据」,现在无数据是保留行列的 `null` 单元格,空表另表示整轴 code 未识别)。**终态码 `999011`(凭证无效)/ `140002`(异步生成失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,异步生成失败是终态。
262
+ - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**贵档端点例外**(one-pager 等生成/提交类 + `tool file-parse` 提交 + 50/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载,共 18 个):5xx/超时不重放——按次计费不幂等,重放即重复扣分;仅连接失败、429 与 token 自愈重试。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(该码 2026-08-01 前还兼表「查询无数据」,现在无数据是保留行列的占位单元格(多数指标 `null`、个别如 `is_dnrpnp` 是 `0`),空表另表示整轴 code 未识别)。**终态码 `999011`(凭证无效)/ `140002`(异步生成失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,异步生成失败是终态。
262
263
  - **Token 自愈**:调用返回 `0000001008` / `999002` 时自动强制刷新 Token 并重试一次。
263
264
  - **K线/资金流向自动分片**:`quote day-kline --security all`、`quote fund-flow --security aShares` 等全市场查询自动按日期切分(A股 K线/资金流向 1 天/片、美股 1 天/片、HK 2 天/片、指数 30 天/片),并发执行后合并结果;按日分片自动跳过周六日。分片时如果用户未传 `--limit`,自动注入 `limit: 10000`(API 上限)避免默认 6000 截断。
264
265
  - **Token 内存缓存**:Token 在进程内存中缓存,避免每次请求读盘。
@@ -299,7 +300,9 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
299
300
  - `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
300
301
  - 安全上限:自动翻页最多 1000 页,防止异常循环
301
302
  - 部分页失败、或服务端实际返回行数与 `total` 矛盾(提前短页)时,不丢弃已取到的数据:结果带 `partial: true`(页失败时另有 `failedPages`;K线分片为 `failedShards`;`--format json` 可见),stderr 输出警告,**进程退出码为 3**(完整成功为 0)
302
- - **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。**v0.32.0 起这个信号的含义变窄了**——2026-08-07 服务端改为给缺数据补 `null`(行列都保留),所以整列/整行消失现在只发生在**服务端解析不了那个 code** 时(指标码拼错,或证券后缀错,如美股写成 `AAPL.US` 而非 `AAPL.O`)。真实的无数据/无覆盖是 `null` 单元格 + 退出码 0。⚠️ **这个检测需要同批里有一个能解析的对照物**:拼错的 code 与正确的 code 混在一批才会标 partial;**整个轴都写错时(如只查一个拼错的指标)响应是空表、退出码 0**,只有 stderr 提示——空表拿到手必须先核对代码拼写与后缀。同版本移除了条件选股重复指标的 `unreliable` / `duplicatedIndicators` 标记(服务端已修复,继续告警会是误报)。**条件选股的缺列另有更严的一档**:把缺列的变量当作无法求值,若表达式(按 `&&`/`||` 的布尔结构)再无任何可成立的分支,则**退出码 1 且不输出**——那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过。⚠️ 这一档以「服务端返回了命中行」为前提;**零命中时一律退出码 0**(没有行需要被质疑),所以空集要先核对指标码拼写,不能直接当成「无标的符合条件」。语义约定:`0` 完整成功(含合法空结果)/`3` 有数据但不完整/`1` 硬失败
303
+ - **`indicator` 命令的退出码 3**(脚本按 `!= 0` 判失败的需留意):服务端整指标/整证券没返回时标 `partial` + `omittedIndicators` / `omittedSecurities` 并退出 3。**v0.32.0 起这个信号的含义变窄了**——2026-08-07 服务端改为给缺数据补占位单元格(行列都保留),所以整列/整行消失现在只发生在**服务端解析不了那个 code** 时(指标码拼错,或证券后缀错,如美股写成 `AAPL.US` 而非 `AAPL.O`)。真实的无数据/无覆盖是占位单元格 + 退出码 0。🔴 **占位值不统一**:多数指标是 `null`,个别(如 `is_dnrpnp`)是 `0`,而 `0` 会穿过比较与聚合——别把它当真值,详见 skill 的 `references/commands/indicator.md`。⚠️ **这个检测需要同批里有一个能解析的对照物**:拼错的 code 与正确的 code 混在一批才会标 partial;**整个轴都写错时(如只查一个拼错的指标)响应是空表、退出码 0**,只有 stderr 提示——空表拿到手必须先核对代码拼写与后缀。同版本移除了条件选股重复指标的 `unreliable` / `duplicatedIndicators` 标记(服务端已修复,继续告警会是误报)。**条件选股的缺列另有更严的一档**:把缺列的变量当作无法求值,若表达式(按 `&&`/`||` 的布尔结构)再无任何可成立的分支,则**退出码 1 且不输出**——那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过。⚠️ 这一档以「服务端返回了命中行」为前提;**零命中时一律退出码 0**(没有行需要被质疑),所以空集要先核对指标码拼写,不能直接当成「无标的符合条件」。语义约定:`0` 完整成功(含合法空结果)/`3` 有数据但不完整/`1` 硬失败
304
+ - **分页端点返回 `null` 也退出 3**:分页端点本该返回 `{total, list}`,真实的空结果是 `{total: 0, list: []}`。若响应体是 `null`(已知一例:`insight foreign-opinion` / `independent-opinion` 传 `--industry`),CLI 在 stderr 告警并**退出码 3**——只给告警的话,脚本无法区分「这个筛选确实没命中」和「这个筛选没生效」。机器格式(jsonl/csv)此时 **stdout 不输出任何字节**(不是空行),`--format json` 仍忠实打印 `null`。⚠️ 带 `--output` 时文件仍会被创建:csv 会写入 3 字节 UTF-8 BOM(Excel 兼容用),jsonl 为 0 字节——**按文件大小判空的脚本要留意 csv 的这 3 个字节**。
305
+ - 🔴 **`total` 被服务端封顶时会标 `totalCapped` 并退出 3**:`insight opinion` / `foreign-opinion` / `independent-opinion` 三个端点的 `total` 恒为 `10000`,而实际记录远不止(把 `from` 加到远超该值仍能取到真实记录)。省略 `--size` 的全量拉取本来会**正好取满 10000 条就停、且不报任何异常**——导出的文件是截断的却看不出来。现在全量拉取结束后会**多探一行**(`from = total`):探到数据就标 `partial` + `totalCapped` 并退出 3。判据不写死 10000,服务端改配置仍然有效;`total` 诚实时探针返回空、不产生计费。传了 `--size` 的有界请求不做此探测。
303
306
  - 分页结果中 `total` 字段会被保留(json 格式输出 `{total, list}`);其他格式下 stderr 输出 `Total: N, showing: M`(json 格式不输出该行)
304
307
 
305
308
  ## 智能文件命名
@@ -604,7 +607,7 @@ gangtise indicator screener --indicator F1:pty_op_scope \
604
607
  --date 2026-07-31 --format table
605
608
 
606
609
  # 复权 / 指标专属参数用 --indicator-param "code:key=value"
607
- # ⚠️ 参数名必须以 search 的 parameterList 为准:传错名服务端静默忽略、退回默认值,不报错
610
+ # ⚠️ 参数名必须以 search 的 parameterList 为准:写错名不会报错,会按默认值取数,结果看着正常但口径不对
608
611
  gangtise indicator cross-section --indicator qte_close --security 600519.SH \
609
612
  --date 2024-01-02 --indicator-param "qte_close:adjustType=3" # 1不复权/2前复权/3后复权/4定点
610
613
  # 不复权 1685.01 → 前复权 1531.225 → 后复权 13609.6168(前复权在最新交易日等于不复权,验证要用历史日)
@@ -713,7 +716,7 @@ CLI 会在本地校验常见数值参数,避免把明显非法的请求发到
713
716
  | `999010` | 接口地址不存在(`raw call` 的 key 可能已下线,用 `raw list` 核对) |
714
717
  | `999012` / `999013` / `999014` | 账号禁用 / 已过期 / 租户失效 |
715
718
  | `999016` | 调用方 IP 不在允许范围 |
716
- | `999999` | Gangtise 系统错误,请稍后重试(`indicator` 端点的「无数据」已不再用此码——有效 code 无数据返回 `null` 单元格,此码基本只剩真故障) |
719
+ | `999999` | Gangtise 系统错误,请稍后重试(`indicator` 端点的「无数据」已不再用此码——有效 code 无数据返回占位单元格(多数 `null`、个别指标 `0`),此码基本只剩真故障) |
717
720
  | `140002` | 终态失败:AI 异步生成失败,或 `indicator` 的参数/表达式错误(枚举越界、语法错)——改参数重提,不重试 |
718
721
  | `100003` | 参数值非法——**最宽的兜底码**;msg 通常已指明字段(如「limit 最小为 1,最大为 10000」),先读 msg |
719
722
  | `100001` | 缺必填参数(msg 带字段名,如「缺少必填参数: reportId」) |
package/dist/src/cli.js CHANGED
@@ -174,13 +174,13 @@ const independentOpinion = new Command("independent-opinion");
174
174
  const officialAccount = new Command("official-account");
175
175
  const qa = new Command("qa");
176
176
  const reportImage = new Command("report-image");
177
- addTimeFilters(opinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--research-area <id>", "Research area ID", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.opinion.list", {
177
+ addTimeFilters(opinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--research-area <id>", "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.opinion.list", {
178
178
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
179
179
  rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
180
180
  securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
181
181
  llmTagList: maybeArray(options.llmTag), sourceList: maybeArray(options.source),
182
182
  })));
183
- addTimeFilters(summary.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--source <number>", "Source type", collectNumberList, []).option("--research-area <id>", "Research area", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.summary.list", {
183
+ addTimeFilters(summary.command("list").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--source <number>", "Source type", collectNumberList, []).option("--research-area <id>", "Research area ID; this endpoint accepts all three code sets: citicIndustry (1008001xx), swIndustry (104xx0000), gangtiseIndustry direction (122000xxx)", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.summary.list", {
184
184
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
185
185
  searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, sourceList: options.source.length ? options.source : undefined,
186
186
  researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
@@ -200,7 +200,7 @@ addDownloadCommand(summary, { endpointKey: "insight.summary.download", idOption:
200
200
  // that put whitelists on securities-search / institution-search / official-account.
201
201
  const PAMIRS_CATEGORIES = ["companyAnalysis", "industryAnalysis"];
202
202
  const PAMIRS_MARKETS = ["aShares", "hkStocks", "usChinaConcept", "usStocks"];
203
- addTimeFilters(pamirsSummary.command("list").description("List Pamirs expert summaries (requires the expert-summary database)").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--research-area <id>", "Research area ID; unlike most insight lists this accepts BOTH citicIndustry (1008001xx) and swIndustry (104xx0000) codes", collectList, []).option("--security <code>", "Security code, e.g. 000001.SZ", collectList, []).option("--category <name>", `Category: ${PAMIRS_CATEGORIES.join(" / ")}`, collectList, []).option("--market <name>", `Market: ${PAMIRS_MARKETS.join(" / ")}`, collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.pamirs-summary.list", {
203
+ addTimeFilters(pamirsSummary.command("list").description("List Pamirs expert summaries (requires the expert-summary database)").addOption(new Option("--search-type <number>", "Search type: 1=title 2=fulltext").choices(["1", "2"]).default("1")).addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--research-area <id>", "Research area ID: accepts BOTH industry code sets -- citicIndustry (1008001xx) and swIndustry (104xx0000) -- but NOT gangtiseIndustry direction codes (122000xxx), which return 0 on this endpoint only", collectList, []).option("--security <code>", "Security code, e.g. 000001.SZ", collectList, []).option("--category <name>", `Category: ${PAMIRS_CATEGORIES.join(" / ")}`, collectList, []).option("--market <name>", `Market: ${PAMIRS_MARKETS.join(" / ")}`, collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.pamirs-summary.list", {
204
204
  from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
205
205
  searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
206
206
  researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security),
@@ -210,7 +210,7 @@ addDownloadCommand(pamirsSummary, { endpointKey: "insight.pamirs-summary.downloa
210
210
  const addScheduleList = (command, endpointKey, fields) => {
211
211
  const list = command.command("list");
212
212
  if (fields.researchArea)
213
- list.option("--research-area <id>", "Research area ID (constant-list category gangtiseIndustry: 1008001xx industries + 122000xxx macro/strategy/fixed-income/quant/overseas directions)", collectList, []);
213
+ list.option("--research-area <id>", "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here", collectList, []);
214
214
  if (fields.institution)
215
215
  list.option("--institution <id>", "Lead institution ID", collectList, []);
216
216
  if (fields.security)
@@ -380,7 +380,7 @@ addTimeFilters(announcementUs.command("list").addOption(new Option("--search-typ
380
380
  securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
381
381
  }), { endpointKey: "insight.announcement-us.list", idField: "announcementId" }));
382
382
  addDownloadCommand(announcementUs, { endpointKey: "insight.announcement-us.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-us", fileType: { description: "File type: 1=original PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement-us.list" });
383
- addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("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", {
383
+ addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code -- UNRELIABLE on this endpoint: cn/us/jp yield a null payload (no error); only gl returns rows. Filter on the region field client-side", collectList, []).option("--industry <id>", "Industry ID -- NOT effective on this endpoint: any value yields a null payload (not an empty list), with no error. Omit it and filter on industryList[] client-side", 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", {
384
384
  from: parseFrom(options.from), size: parseSize(options.size),
385
385
  startTime: options.startTime, endTime: options.endTime,
386
386
  rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
@@ -389,7 +389,7 @@ addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type
389
389
  securityList: maybeArray(options.security), brokerList: maybeArray(options.broker),
390
390
  ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
391
391
  })));
392
- addTimeFilters(independentOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry 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.independent-opinion.list", {
392
+ addTimeFilters(independentOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry ID -- NOT effective on this endpoint: any value yields a null payload (not an empty list), with no error. Omit it and filter on industryList[] client-side", 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.independent-opinion.list", {
393
393
  from: parseFrom(options.from), size: parseSize(options.size),
394
394
  startTime: options.startTime, endTime: options.endTime,
395
395
  rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
@@ -688,9 +688,9 @@ vault.command("drive-list").option("--from <number>", "Starting offset", "0").op
688
688
  addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
689
689
  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>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).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" }));
690
690
  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" });
691
- 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>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).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" }));
691
+ 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>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--research-area <id>", "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here", 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" }));
692
692
  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" });
693
- 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>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).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))));
693
+ 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>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).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 -- NOT effective on this endpoint: both code sets, and even a garbage value, return the unfiltered total. Filter on industryList[] client-side", 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))));
694
694
  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))));
695
695
  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", {})));
696
696
  vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; omit (or 'all') for all pools", collectList).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.stocks", buildStockPoolStocksBody(options))));
@@ -186,10 +186,26 @@ export class GangtiseClient {
186
186
  });
187
187
  if (!this.isPaginatedListResponse(firstPage)) {
188
188
  // Shape drift (e.g. total arriving as a string) silently degrades fetch-all
189
- // to a single page with no partial marker — make it visible on verbose.
190
- if (isVerbose()) {
191
- process.stderr.write(`[gangtise] warning: ${endpoint.key} is marked paginated but the first page has an unexpected shape (no numeric total + list); returning it as-is\n`);
192
- }
189
+ // to a single page with no partial marker. This is NOT hypothetical: passing
190
+ // industryList to insight foreign-opinion / independent-opinion makes the
191
+ // server answer 200 + data:null, which then renders as one phantom row
192
+ // ({"value":null} in jsonl, a blank row in table/markdown). Verbose-only was
193
+ // the wrong bar — a caller who never sets GANGTISE_VERBOSE reads that row as
194
+ // real data, so warn on stderr unconditionally.
195
+ process.stderr.write(`[gangtise] warning: ${endpoint.key} is marked paginated but the first page has an unexpected shape (no numeric total + list); returning it as-is\n`);
196
+ // A warning only helps a human. Scripts read the exit code, and without one they
197
+ // cannot tell "this filter legitimately matched nothing" from "this filter is
198
+ // broken". Reuse exit 3 (result is not what was asked for) rather than inventing a
199
+ // fourth code — fetch-all silently degraded to one page either way.
200
+ //
201
+ // Applies to EVERY malformed shape, not just `null`: a string `total` truncates the
202
+ // result to page 1, which looks complete and is therefore worse than an obviously
203
+ // empty payload. All 24 paginated endpoints are genuine {total, list} lists (the
204
+ // odd-shaped ones like reference.constant-list are not marked paginated), so there
205
+ // is no legitimate response that lands here. Endpoints where `null` IS a valid
206
+ // answer (ai.one-pager for a security with no generated content) are unpaginated
207
+ // and never reach this branch.
208
+ process.exitCode = 3;
193
209
  return firstPage;
194
210
  }
195
211
  const total = firstPage.total;
@@ -295,11 +311,42 @@ export class GangtiseClient {
295
311
  if (truncatedByPageCap) {
296
312
  process.stderr.write(`[gangtise] warning: hit the ${MAX_PAGES}-page safety cap; fetched ${collected.length} of ${total} rows. Narrow the query (e.g. a shorter date range) or pass --size to fetch a bounded subset.\n`);
297
313
  }
314
+ const short = collected.length < target;
315
+ // `total` is not always the real row count. Three opinion endpoints report a fixed
316
+ // 10000 while `from=30000` still returns real rows with monotonically older publish
317
+ // times (the shape of Elasticsearch's default track_total_hits). A fetch-all then
318
+ // stops exactly at the cap with collected === total, so every completeness check
319
+ // below passes and the truncated export looks complete — the worst failure mode we
320
+ // have, because `opinion` bills 30 credits per row.
321
+ //
322
+ // Probe one row past the claimed end rather than hardcoding any number: the server
323
+ // can change the cap, and evidence survives that where a constant would not. Only on
324
+ // a genuine fetch-all that otherwise looked complete — when `total` is honest the
325
+ // probe comes back empty (and bills nothing, since these endpoints charge per row).
326
+ let totalCapped = false;
327
+ if (requestedSize === undefined && total > 0 && !short && !totalDrift && !truncatedByPageCap && failedPages.length === 0) {
328
+ try {
329
+ const probe = await this.requestJson(endpoint, { ...initialBody, from: total, size: 1 });
330
+ if (this.isPaginatedListResponse(probe) && probe.list.length > 0)
331
+ totalCapped = true;
332
+ }
333
+ catch {
334
+ // A failed probe must never fail an otherwise-complete export: we simply do not
335
+ // learn whether the total was capped, which is the pre-existing behaviour.
336
+ }
337
+ }
338
+ if (totalCapped) {
339
+ process.stderr.write(`[gangtise] warning: ${endpoint.key} reported total=${total} but rows exist past that offset — 'total' is a server-side cap, not the real count. This export is TRUNCATED at ${collected.length} rows. Narrow the query (date range / filters) and fetch in slices.\n`);
340
+ }
298
341
  const out = {
299
342
  ...firstPage,
300
343
  total,
301
344
  list: requestedSize === undefined ? collected : collected.slice(0, requestedSize),
302
345
  };
346
+ if (totalCapped) {
347
+ out.partial = true;
348
+ out.totalCapped = true;
349
+ }
303
350
  // Unified completeness backstop. Whatever the cause — a failed/shape-broken page,
304
351
  // a short later page (server page cap < maxPageSize), the MAX_PAGES cap, or `total`
305
352
  // drifting mid-fetch — the result is partial. A short row count or a total drift each
@@ -309,7 +356,6 @@ export class GangtiseClient {
309
356
  // must agree. printData maps partial → exit 3 so a script can't read a truncated export
310
357
  // as complete. The cap and drift branches above already warned on stderr; failedPages
311
358
  // warns below.
312
- const short = collected.length < target;
313
359
  if (short || totalDrift || failedPages.length > 0)
314
360
  out.partial = true;
315
361
  if (failedPages.length > 0) {
@@ -40,7 +40,7 @@ const ERROR_HINTS = {
40
40
  "999001": "检查 GANGTISE_TOKEN 或 AK/SK 是否已 export。",
41
41
  "999002": "有 AK/SK 时 CLI 会自动重新登录重试一次,否则请重新登录。",
42
42
  "999003": "定制接口需联系客户经理开通。",
43
- "999004": "换一条本账号可见的记录重试。",
43
+ "999004": "整库/整接口未开通(如帕米尔专家纪要需单独购买),或该条记录本账号不可见——list 撞上多为前者,先确认该数据库是否已开通。",
44
44
  "999005": "联系客户经理充值,或缩小查询范围降低消耗。",
45
45
  "999006": "触发限流,稍后再试或联系客户经理提额;429 所有端点都退避重试,5xx 仅普通端点重试(贵档 no-replay 端点的 5xx 不重放,但其 429 仍重试)。",
46
46
  // CLI 自身发不出这三类请求(method / Content-Type 都由 endpoint 定义固定),
@@ -51,6 +51,14 @@ function rowsFromList(list) {
51
51
  return list.map((item, index) => ({ index, value: item }));
52
52
  }
53
53
  function toRows(value) {
54
+ // A null/undefined payload means "no data", not "one row whose only column is
55
+ // null". Without this guard it fell through to [{ value }] and jsonl emitted
56
+ // {"value":null} — a phantom record that makes `wc -l` report 1. Real case:
57
+ // insight foreign-opinion --industry answers 200 + data:null.
58
+ // Scalars (0, "", false) are left alone; only the two empty values short-circuit.
59
+ if (value === null || value === undefined) {
60
+ return [];
61
+ }
54
62
  if (Array.isArray(value)) {
55
63
  return rowsFromList(value);
56
64
  }
@@ -53,5 +53,11 @@ export async function printData(data, format, output, cache) {
53
53
  return;
54
54
  }
55
55
  warnIfLargeInMemory(items, format);
56
- process.stdout.write(`${renderOutput(normalized, format)}\n`);
56
+ const rendered = renderOutput(normalized, format);
57
+ // An empty render means "no rows at all". Appending the newline anyway would put
58
+ // one blank line on stdout — `wc -l` reports 1 and `while read` yields one empty
59
+ // record, which is exactly the phantom row the null-payload guard in toRows()
60
+ // removes. Table/markdown render "(empty)" so they are unaffected.
61
+ if (rendered)
62
+ process.stdout.write(`${rendered}\n`);
57
63
  }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.32.0";
2
+ export const CLI_VERSION = "0.33.0";
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: gangtise-openapi
3
- version: "0.32.0"
3
+ version: "0.33.0"
4
4
  description: |-
5
5
  通过 gangtise CLI 直接调用 Gangtise OpenAPI,拉取投研原始数据、批量导出、下载文件、调用 AI 能力。
6
6
 
@@ -30,13 +30,14 @@ description: |-
30
30
  6.1. **日 K 仅历史**:`day-kline` / `day-kline-hk` / `day-kline-us` **不返回盘中实时数据**。当日数据入库时间:A 股 ~15:30 / 港股 ~16:30 / 美股 ~07:00(北京时间)。需要盘中快照请走 `quote realtime`。
31
31
  6.2. **多标的日 K 不自动分片**:只有 `--security all` 才按日切片提额;显式传多个 `--security` 时走单请求(默认 `--limit 6000` / 上限 10000)。**v0.23.0 起:返回行数撞上 `--limit` 时结果会标 `partial`、退出码 3、stderr 警告**(不再静默截断;`--limit` 超 10000 本地直接报错)。仍建议先估 标的数 × 交易日数,接近/超 6000 → 逐只分开拉、或显式 `--limit 10000` 并按日期区间分批。
32
32
  7. **CLI 已内置自动化,不要手动复刻**:
33
- - 翻页 → 首页拿 total 后剩余页并发拉取
33
+ - 翻页 → 首页拿 total 后剩余页并发拉取;🔴 **全量拉取结束会多探一行验证 `total` 是不是服务端封顶**(`opinion` / `foreign-opinion` / `independent-opinion` 的 `total` 恒为 10000 但实际远不止)——探到就标 `partial` + `totalCapped` + 退出 3,**这时导出的是截断结果,要缩小时间范围分片拉**
34
34
  - K 线 `--security all` 跨日期 → 自动按日切片并合并
35
35
  - 5xx / `429` / 网络错误 / `999999` → 自动指数退避重试(🔴 贵档端点例外:仅连接失败 / 429 / token 自愈重试,5xx/超时不重放防重复扣分,v0.26.0;`indicator` 端点对 `999999` 不重试,v0.27.0)
36
36
  - Token 失效(`0000001008` / `999002`,含已废弃的 `8000014`/`8000015`)→ 自动重新登录并重试一次;凭证错 `999011` → **不重试**(AK/SK 不对不会自己好),查环境变量
37
37
  8. **参数命名差异**:Insight/Quote/Vault 用 `--security`,Fundamental/AI 用 `--security-code`(例外:`ai stock-summary` 用 `--security`,`ai security-clue` 用 `--gts-code`)。
38
38
  9. **调试**:`--verbose` 或 `GANGTISE_VERBOSE=1` 打印每个请求的耗时/字节数到 stderr。
39
39
  10. **`--field` 字段名必须核对,不确定就别传**(返回全量最稳):`quote realtime` / `fundamental main-business` / `valuation-analysis` 遇到不存在的字段名时,上游只丢**值**、字段名照请求**回显**,按位置拍平会把值贴到错误的字段上(实测 realtime 传 `close`——它没有这个字段——换手率 28.5573 被贴成 `close`,茅台真实价 1297.41)。v0.28.3 起 CLI 检测到长度不匹配直接报错(退出码 1):**带 `--field` 的命令看到这个报错,先去 `references/fields.md` 核对字段名**(没有 `--field` 的命令如 `alternative edb-data` 报此错则是上游响应结构异常,报障时若报错末尾附了 `(trace …)` 就一并带上)。另:realtime **无 `close`**(用 `latestPrice`)、**无市值**(总市值走 `indicator cross-section --indicator qte_mkt_cptl`,2026-08-03 起 A/港/美股均有数)。
40
+ 11. 🔴 **EDE 取不到数时给的是占位值,而 `0` 和 `null` 都可能**(`indicator` 截面 / 时序 / 选股):多数指标填 `null`,但个别——最常用的是 `is_dnrpnp` 扣非归母净利润——填 **`0`**,且这是**指标属性、与日期对不对无关**(日期落在报告期末时,覆盖不到的证券如美股同样返 `0`,行还在、无告警、退出码 0)。**`null` 通常被聚合跳过,`0` 会照常穿过比较与比率计算**:`screener` 的 `F1 > 0` 可能筛出空集(读起来像「没有盈利的股票」),时序整列求均值可能差几十倍。所以:报告期类指标的日期一律落**报告期末**;拿 `is_dnrpnp` 这类做筛选或聚合前先单查确认不是占位;**别对时序整列直接求均值 / 求和**。详见 `references/commands/indicator.md`。
40
41
 
41
42
  ## 工作流(3 步)
42
43
 
@@ -190,7 +191,7 @@ description: |-
190
191
  - EDE 取数前必须用 `search --format json` 同时核对:`indicatorName` + `description` 语义准确、`scopeList` 覆盖全部目标市场 / 证券类型、`parameterList` 必填参数与枚举可满足;`scopeList` 缺失 / `null` / 空或任一项不符,都视为无法证明覆盖并回退专用接口。专用接口也不覆盖目标市场时,说明当前不可用,不要硬调。`scopeList` 按指标各不相同,不能因 EDE 服务支持 A / 港 / 美股就假定某个指标三市场都覆盖
191
192
  - `indicator search` 免费,`cross-section` / `time-series` 按单元格计费;除多证券批量的效率收益外,仍优先免费 / 低价的 `quote` 或 `fundamental`
192
193
  - 行业 / 宏观指标(空调销量、社融等,无证券维度)走 `alternative edb-*`(EDB),不要与证券级 EDE 混用
193
- - **EDE「没数据」和「代码写错」是两回事**(2026-08-08 实测):①代码有效但无数据 / 无覆盖 / 未来日期 → 单元格 `null`,**行列都保留**(含单指标 × 单证券的 1×1),**退出码 0**;②**指标 code 认不出来** → 该指标整列消失;③**证券 code 认不出来**(拼错,或美股用了 `.US` 而非 `.O`/`.N`)→ 该证券整行消失——②③ CLI 标 `partial: true` + `omittedIndicators`/`omittedSecurities` 并**退出码 3**(**`screener` 例外见下**),`--key-by code` 回填时 key 会直接不存在;④请求里没有任何可解析的 code → **空表**,退出码 0 + stderr 提示。**所以退出码 3 现在读作「你有 code 写错了」,不是「这批数据不完整」**——⚠️ 但它**需要同批有对照物**:整个轴都写错(比如只查一个指标而它拼错了)拿到的是空表 + 退出码 0,所以**空表的第一嫌疑是拼写和证券后缀,不是没数据**;反过来,真实的覆盖缺口一眼可见(就是 `null`),不用再靠「和已知有数的标的一起查」做对照。拿到空表先查证券后缀和 code 拼写,再查参数名。日期语义按指标分两类:财务报表指标=报告期末(可为非交易日)、`finc_pe_ttm` / `finc_pb_mrq` 等日频估值=最新交易日(⚠️ `finc_pb_mrq` 是日频的,用报告期末日期会取到几个月前的陈值);混合取数按各自有效日期分次 `cross-section` 再按证券合并,别塞进同一个 `--date`。详见 `references/commands/indicator.md`
194
+ - **EDE「没数据」和「代码写错」是两回事**(2026-08-08 实测):①代码有效但无数据 / 无覆盖 / 未来日期 → **行列都保留**(含单指标 × 单证券的 1×1)并给一个占位单元格,**退出码 0**。🔴 **占位值不统一、由指标决定**:多数是 `null`,但个别(最常用的是 `is_dnrpnp` 扣非归母净利润)填 **`0`**,而 `0` 会照常穿过比较与聚合——`screener` 的 `F1 > 0` 因此可能筛出空集、整列求均值可能差几十倍。**别把 `0` 当真值**,详见 `references/commands/indicator.md`;②**指标 code 认不出来** → 该指标整列消失;③**证券 code 认不出来**(拼错,或美股用了 `.US` 而非 `.O`/`.N`)→ 该证券整行消失——②③ CLI 标 `partial: true` + `omittedIndicators`/`omittedSecurities` 并**退出码 3**(**`screener` 例外见下**),`--key-by code` 回填时 key 会直接不存在;④请求里没有任何可解析的 code → **空表**,退出码 0 + stderr 提示。**所以退出码 3 现在读作「你有 code 写错了」,不是「这批数据不完整」**——⚠️ 但它**需要同批有对照物**:整个轴都写错(比如只查一个指标而它拼错了)拿到的是空表 + 退出码 0,所以**空表的第一嫌疑是拼写和证券后缀,不是没数据**;反过来,真实的覆盖缺口也留在表里(就是那个占位单元格),多数情况不用再靠「和已知有数的标的一起查」做对照——⚠️ **但只有 `null` 那一档真的一眼可见**,填 `0` 的指标(`is_dnrpnp`)无覆盖时与真值无法区分,对照法仍然必要。拿到空表先查证券后缀和 code 拼写,再查参数名。日期语义按指标分两类:财务报表指标=报告期末(可为非交易日)、`finc_pe_ttm` / `finc_pb_mrq` 等日频估值=最新交易日(⚠️ `finc_pb_mrq` 是日频的,用报告期末日期会取到几个月前的陈值);混合取数按各自有效日期分次 `cross-section` 再按证券合并,别塞进同一个 `--date`。详见 `references/commands/indicator.md`
194
195
  - **EDE 指标参数名一律以 `indicator search --format json` 的 `parameterList` 为准**,不要凭记忆或照抄示例:**参数名写错不报错、按默认值取数**——如复权参数是 `adjustType`,写成 `adjustmentType` 会返回不复权价,看着正常实则口径不对。`--security` 支持板块 ID(`reference sector-search` 的 10 位 `sectorId`;`--indicator` 只收指标编码)。✅ 根级 `--scale` 污染不声明 `scale` 的指标(`qte_close` 被 `--scale 8` 缩成 `0`)**已于 2026-08-08 修复**,价格与金额可以混查了
195
196
  - **2026-08-07 新增指标**:融资融券 21 个 `mgn_*`(`mgn_bal` 两融余额 / `mgn_fin_*` 融资 / `mgn_sl_*` 融券 / `mgn_flag` 是否标的,**仅 A 股有数**);行业分类 `scr_indu`(一个指标覆盖申万/中信/恒生/GICS 四套,**必填** `industryType` 1-4 + `industryLevel` 0-4,A/港/美股均支持,返回字符串)
196
197
  - "业绩点评"双义消歧:**检索已有**(研报/纪要里的业绩点评内容)走 `insight ... list --llm-tag earningsReview`(0.1/条);**AI 现生成**一份走 `ai earnings-review`(异步、50/次)。不确定问一句
@@ -283,7 +284,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
283
284
  | 错误码 | 含义 | CLI 行为 | Agent 是否介入 |
284
285
  |--------|------|---------|--------------|
285
286
  | ✅ `100003` | 参数值非法——**最宽的兜底码**:类型错、`limit` 越界都归这里。**msg 通常已指明字段**(如「请求体字段类型不匹配: size 期望类型 Integer」「limit 最小为 1,最大为 10000」),先读 msg 再猜 | — | 按 msg 指的字段改;msg 没指明才对照 `--help` 查枚举拼写,**不要重试同命令** |
286
- | ✅ `999999` | 系统错误。**`indicator`(EDE)2026-08-01 前用此码表示查询无数据,之后改为返回数据体**,所以这个码基本只剩真故障。⚠️ 现在「无数据」是保留行列的 `null` 单元格,**不是空表**;空表另有含义(整轴 code 未识别) | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | 确认参数无误仍报此码即服务端故障 |
287
+ | ✅ `999999` | 系统错误。**`indicator`(EDE)2026-08-01 前用此码表示查询无数据,之后改为返回数据体**,所以这个码基本只剩真故障。⚠️ 现在「无数据」是保留行列的占位单元格(多为 `null`,个别指标是 `0`,见 `indicator.md`),**不是空表**;空表另有含义(整轴 code 未识别) | 普通端点自动重试 ×2;🔴 贵档与 `indicator` 端点不重试 | 确认参数无误仍报此码即服务端故障 |
287
288
  | ✅ `410110` | **异步生成中**(HTTP 400,旧码未切)。新码 `140001`,CLI 两码都认 | 轮询视为 pending | 继续等 |
288
289
  | ✅ `410111` | **异步生成失败**(HTTP 400,旧码未切)。新码 `140002`,CLI 两码都认 | 终态 | **不重试**,换参数 |
289
290
  | ✅ `130002` | 资源不存在——**下载类的兜底码**:`reportId` 不存在 / 非数字 / `fileType` 非法**全归这里**(`130003`/`130004`/`130005` 实测均未启用) | — | 确认 ID 有效且本账号可见;换 `--file-type` 或换一篇验证 |
@@ -297,16 +298,17 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
297
298
  | ✅ `250001` | 不支持的数据源——**取代旧 `433007`** | — | 检查 `resourceType + sourceId` 组合 |
298
299
  | ✅ `999011` | 开发账号凭证无效——**取代旧 `8000014`/`8000015`,已合并,不再区分 AK 错还是 SK 错** | 登录即失败,**不重试** | 检查 `GANGTISE_ACCESS_KEY`/`GANGTISE_SECRET_KEY` 是否写反或未 export |
299
300
  | ✅ `999010` | 接口地址不存在 | — | `raw call` 的 key 可能已下线,用 `gangtise raw list` 核对 |
301
+ | ✅ `999004` | 无资源权限。**整库未开通与单条记录不可见都走这个码**——`insight pamirs-summary list`(专家纪要库需单独购买)未开通时即报此码 | — | `list` 撞上多为整库未开通,先确认该数据库是否已购买;`download` 撞上再考虑换一条本账号可见的记录 |
300
302
  | ✅ `0000001008` | Token 服务端失效(他处登录挤掉)——**旧码未切,token 自愈依赖它** | **强制重新登录并重试一次** | 无 AK/SK 时无法自愈,提示重新登录 |
301
303
  | ✅ `0000001007` | 请求未携带 Bearer token | — | 检查 `GANGTISE_TOKEN` / AK/SK 是否已 export |
302
304
  | ✅ `900002` | **请求方法不正确**(msg「请求类型有误」,HTTP 405) | — | `raw call` 时确认该 endpoint 是 GET 还是 POST |
303
305
  | ✅ `140002` | 终态参数错:AI 异步生成失败,或 `indicator` 的指标必填参数缺失 / 枚举越界 / 表达式语法错(实测 2026-08-02) | **不重试**(终态码) | 按 msg 改参数重提;EDE 的参数名与枚举读 `indicator search --format json` 的 `parameterList` |
304
306
 
305
307
  **⚠️ 实测发现的坑(都是"不报错"型,最难发现)**
306
- - 🔴 **日期只写 `YYYY-MM-DD`、时间只写 `YYYY-MM-DD HH:mm:ss`(或 10/13 位时间戳);CLI v0.28.0 起 date 与 datetime 两类、含所有 insight/vault 透传参数都本地拦截**。服务端对「年在后」格式**日月顺序随分隔符翻转**且静默误解析(HTTP 200、不回显实际用的日期):`07/01/2026`(斜杠)读成 **2026-01-07**、`07-01-2026`(横杠)读成 **2026-07-01**,差半年。实测 `insight research list --start-time`:`07/01/2026` 命中 1562 条、`07-01-2026` 命中 210 条(分别 = 标准 `2026-01-07` / `2026-07-01`);`quote day-kline`/`kline-hk`/`kline-us`/`index`、`fundamental balance-sheet` 同理。v0.28.0 前透传命令(research/summary/announcement-hk/us/vault/minute-kline 等)**静默放行**,且同值在本地转时间戳的 `announcement`(A 股)与透传的 hk/us 之间还会差半年、都 exit 0。现在全部在发请求前报 `ValidationError`,**但绕过 CLI 直连接口务必自己保证格式**
308
+ - 🔴 **日期只写 `YYYY-MM-DD`、时间只写 `YYYY-MM-DD HH:mm:ss`(或 10/13 位时间戳);CLI v0.28.0 起 date 与 datetime 两类、含所有 insight/vault 透传参数都本地拦截**。服务端对「年在后」格式**日月顺序随分隔符翻转**且静默误解析(HTTP 200、不回显实际用的日期):`07/01/2026`(斜杠)读成 **2026-01-07**、`07-01-2026`(横杠)读成 **2026-07-01**,差半年。实测 `insight research list --start-time`:`07/01/2026` 与 `07-01-2026` 的命中数相差数倍,分别与标准写法 `2026-01-07` / `2026-07-01` 逐位吻合;`quote day-kline`/`kline-hk`/`kline-us`/`index`、`fundamental balance-sheet` 同理。v0.28.0 前透传命令(research/summary/announcement-hk/us/vault/minute-kline 等)**静默放行**,且同值在本地转时间戳的 `announcement`(A 股)与透传的 hk/us 之间还会差半年、都 exit 0。现在全部在发请求前报 `ValidationError`,**但绕过 CLI 直连接口务必自己保证格式**
307
309
  - **财报接口的日期按「报告期末」过滤**,不是公告日:`fundamental balance-sheet` 等的 `--start-date`/`--end-date` 匹配的是 `endDate` 字段(如 `20200630`),响应里的 `announcementDate`(如 `20200729`)只是公告日。**查某期财报要传季度末日期**(`2020-06-30` / `2020-03-31` / `2020-09-30` / `2020-12-31`);传 `2020-07-01` 这类非报告期日期会返回 0 行,属正常行为,不是没数据
308
310
  - 🔴 **Quote 系对非法证券代码不报错**,静默返回 `total:0` 空列表——无法区分"代码写错"和"该票该区间真无数据"。**空结果先回头核对代码后缀**。Fundamental 系会正常报 `120001`
309
- - **枚举值拼错、分页参数越界服务端不报错**——静默忽略该条件返回全量/正常结果。所以 `100004`/`100005` 实测触发不到。最坏一例:非法 `--search-type` 会**连带吞掉 `--keyword`**(`summary list --keyword 茅台 --search-type 99` 返 196988 全库 vs 正常 135)。**v0.32.0 起 CLI 本地拦截**:全部 `--search-type` / `--rank-type`(19 处)、全部 `--file-type`(9 处下载,`foreign-report` 为 1–4、其余 1–2)、`pamirs-summary` 的 `--category` / `--market`、`--top` 上限、以及 `reference securities-search` / `institution-search` / `official-account-search` 的 `--category`。**仍未覆盖**:`insight research/summary --category`、`--market`、`--source`、`--llm-tag` 等仍是自由字符串,拼错不报错也不生效——这些要自己核对。**拼错的筛选条件会伪装成"结果正常",枚举拼写要自己保证**
311
+ - **枚举值拼错、分页参数越界服务端不报错**——静默忽略该条件返回全量/正常结果。所以 `100004`/`100005` 实测触发不到。最坏一例:非法 `--search-type` 会**连带吞掉 `--keyword`**(`summary list --keyword 茅台 --search-type 99` 拿到的是**未经筛选的全库**,与不传 `--keyword` 的结果逐位相同,而不是搜索结果)。**v0.32.0 起 CLI 本地拦截**:全部 `--search-type` / `--rank-type`(19 处)、全部 `--file-type`(9 处下载,`foreign-report` 为 1–4、其余 1–2)、`pamirs-summary` 的 `--category` / `--market`、`--top` 上限、以及 `reference securities-search` / `institution-search` / `official-account-search` 的 `--category`。**仍未覆盖**:`insight research/summary --category`、`--market`、`--source`、`--llm-tag` 等仍是自由字符串,拼错不报错也不生效——这些要自己核对。**拼错的筛选条件会伪装成"结果正常",枚举拼写要自己保证**
310
312
  - **`viewpoint-debate` 传敏感内容不会被提前拦截**——实测不返回 `240002`,而是照常受理、扣满 50 积分、生成阶段才以 `410111` 失败。**提交前自己把关措辞**
311
313
  - **`ai one-pager` 的非法 `mode` 被静默忽略**,照常生成并扣 50 积分
312
314
 
@@ -316,9 +318,9 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
316
318
  |--------|------|---------|
317
319
  | `999001` / `999002` | 缺 token / token 无效 | 实际返回旧码 `0000001007` / `0000001008` |
318
320
  | `999007` / `999008` / `999009` | 方法/媒体类型/请求体不支持 | 实际返回 `900002` / `999999` / `100003` |
319
- | `999003` / `999004` / `999005` / `999006` | 无接口权限 / 无资源权限 / 积分不足 / 限流 | 未构造出(需特定账号状态) |
321
+ | `999003` / `999005` / `999006` | 无接口权限 / 积分不足 / 限流 | 未构造出(需特定账号状态) |
320
322
  | `999012`–`999016` | 账号禁用/过期、租户失效、无长期 token、IP 不合规 | 未构造出 |
321
- | `100002` / `100004` / `100005` | 类型错 / 分页非法 / 枚举非法 | 类型错归 `100003`;后两者服务端静默忽略 |
323
+ | `100002` / `100004` / `100005` | 类型错 / 分页非法 / 枚举非法 | 类型错归 `100003`;后两者不报错,该条件按未传处理 |
322
324
  | `130003` / `130004` / `130005` | 无文件可下 / ID 非数字 / 文件类型不支持 | 全部归 `130002` |
323
325
  | `140001` / `140002` | 结果生成中 / 处理失败 | 异步端点仍用 `410110` / `410111` |
324
326
  | `210001` / `220001` / `230001` | 研报/观点/分享文件不支持下载 | 未构造出 |
@@ -337,7 +339,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
337
339
  - CLI 未安装 → `npm install -g gangtise-openapi-cli`
338
340
  - **退出码 3 = `partial: true`,结果少了数据**(保留已取到的部分;stderr 有 warning,`--format json` 才看得见标记,table/csv/jsonl 只有数据行、看不出问题):
339
341
  - 触发场景:翻页/K线分片有页失败、服务端返回行数与 `total` 矛盾(提前短页)、或 **EDE 有 code 没被服务端认出来**。附带定位字段:页失败 `failedPages`;分片失败 `failedShards`、分片撞行数上限 `truncatedShards`(均带日期区间可缩窗补拉);EDE 为 `omittedIndicators` / `omittedSecurities`
340
- - **EDE 的这两个字段现在读作「这些 code 服务端不认识」**——先查拼写和证券后缀(美股 `.O`/`.N`,不是 `.US`),而不是查 `scopeList` 覆盖:真的没覆盖会返 `null` 单元格、退出码 0
342
+ - **EDE 的这两个字段现在读作「这些 code 服务端不认识」**——先查拼写和证券后缀(美股 `.O`/`.N`,不是 `.US`),而不是查 `scopeList` 覆盖:真的没覆盖会返占位单元格(多数 `null`、个别指标 `0`)、退出码 0
341
343
  - **`screener` 的缺列按表达式的布尔结构判**:把缺列的变量当作「无法求值」,看整个表达式**是否还有一条能成立的分支**——`A && B` 要两边都可求值,`A || B` 只要一边。**一条分支都不剩 → 退出码 1、不输出任何结果**(返回的行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过);**还有分支可求值,或缺的只是未参与表达式的辅助变量 → `partial` + 退出码 3**。所以 `F1 || F2` 缺 F1 是降级(行可能靠 F2 正当命中),缺两个则是致命;`F1 && (F2 || F3)` 缺 F1 仍是致命(F1 是必选合取项)。⚠️ **前提是服务端返回了命中行**——零命中时没有行需要质疑,一律是「nothing matched」+ **退出码 0**,哪怕变量整个求不了值(实测 `F1:拼错的码` 单绑就是空集 + 退出 0)。**空集别当成「条件成立但无标的符合」**,先核对指标码
342
344
  - 拿到 3 就必须告知用户缺了哪段,不能当成功静默继续
343
345
  - (历史:条件选股把同一指标绑到多个变量曾触发 `unreliable: true` + `duplicatedIndicators`,因服务端把所有绑定按最早日期取数。2026-08-08 服务端已修复,该标记**已从 CLI 移除**,这类查询现在正常可用)
@@ -358,7 +360,7 @@ gangtise reference securities-search --keyword <公司名> --category stock --to
358
360
  **`list` 全空但参数看着对**
359
361
  1. 时间窗太窄 → 扩到 30 天试
360
362
  2. `--security` 后缀拼错(如 `300750` 漏了 `.SZ`)
361
- 3. 行业 ID 用错体系:`--industry`(用 `citicIndustry` 码 `1008001xx`)/ `--research-area`(用 `gangtiseIndustry`:行业 `1008001xx` + 方向 `122000xxx`)/ `--gts-code`(申万 `821xxx.SWI`)——三套体系不同,详见 `references/commands/reference-and-lookup.md`
363
+ 3. 行业 ID 用错体系:`--industry`(用 `citicIndustry` 码 `1008001xx`)/ `--research-area`(行业同样用 `citicIndustry` 码 `1008001xx`,方向才用 `gangtiseIndustry` 码 `122000xxx`——`gangtiseIndustry` 里**只有 6 条方向码、没有行业**)/ `--gts-code`(申万 `821xxx.SWI`)。申万数字码 `104xx0000` 用于 `--research-area` 时多数端点返 0,详见 `references/commands/reference-and-lookup.md`
362
364
  4. `--rating` / `--category` 等枚举值拼错(参考对应命令的 references 文件)
363
365
 
364
366
  **`999011` 凭证无效**(旧码 `8000014`/`8000015`;服务端已合并为一个码,不再指明是 AK 错还是 SK 错,**登录直接失败、CLI 不重试**)
@@ -30,8 +30,8 @@ gangtise indicator search --keyword <text> [--limit <n>]
30
30
  - 默认 `--format table` 只适合浏览名称;正式路由 / 取数前必须加 `--format json`,才能完成语义、`scopeList`、`parameterList` 三项校验
31
31
  - 返回字段:`indicatorCode` / `indicatorName` / `description`(算法与口径)/ `scopeList`(该指标适用的市场 + 证券类型 + `usageRestriction`)/ `parameterList`(可传的 `--indicator-param` 参数名及枚举)/ `score`
32
32
  - **市场范围按指标判断**:`scopeList` 会返回实际覆盖范围,且指标之间不同;不能笼统写成每个指标都覆盖 A / 港 / 美股。覆盖面在持续扩,别把抽查结论当常量:截至 **2026-08-03** 实测 `qte_mkt_cptl`/`shr_tot` 已覆盖 A/港/美股,`finc_pe_ttm` 覆盖 A/港股,`finc_pb_mrq` 仍只有 A 股,`is_op_rev_ttm`/`is_shnp_ttm` 覆盖 A/港股。目标列表含任一 scope 外证券时,本批 EDE 校验不通过,应回退专用接口
33
- - **`scopeList[].usageRestriction`**:该指标在具体接口上的限制,`null` = 无限制。⚠️ **它是提示不是硬约束**——标着「不支持指标时间序列接口」的 `qte_vol_intvl` 实测调时序照样返数据、服务端不拦(2026-08-02)。当成「口径可能不对、结果别当真」看,别指望它会报错
34
- - ⚠️ **`scopeList` 是声明不是保证**:它可能**超前于数据**——`finc_pb_mrq` 声称覆盖 A股+港股,实测港股仍无数据(复测 2026-08-08)。反过来也发生过:`qte_mkt_cptl`/`shr_tot` 在 08-02 时声称覆盖港/美股却无数据,08-03 数据补上了。**2026-08-08 起判断成本降了**:无数据一律是 `null` 单元格(行列都保留),不再因「同批还查了什么」变成整列/整行消失,所以抽查一行就能直接看出覆盖情况(详见下方「缺数据 vs 代码写错」)
33
+ - **`scopeList[].usageRestriction`**:该指标在具体接口上的限制,`null` = 无限制。⚠️ **它是提示不是硬约束**——标着「不支持指标时间序列接口」的 `qte_vol_intvl` 实测调时序照样返数据、不会被拦下(2026-08-02)。当成「口径可能不对、结果别当真」看,别指望它会报错
34
+ - ⚠️ **`scopeList` 是声明不是保证**:它可能**超前于数据**——`finc_pb_mrq` 声称覆盖 A股+港股,实测港股仍无数据(复测 2026-08-08)。反过来也发生过:`qte_mkt_cptl`/`shr_tot` 在 08-02 时声称覆盖港/美股却无数据,08-03 数据补上了。**2026-08-08 起判断成本降了**:无数据会保留行列、给一个占位单元格,不再因「同批还查了什么」变成整列/整行消失,所以抽查一行就能看出覆盖情况。⚠️ **但占位值不统一**——多数指标是 `null`,个别是 `0`(详见下方「缺数据 vs 代码写错」)
35
35
  - 美股代码用交易所后缀 `.O`(NASDAQ) / `.N`(NYSE):用 `AAPL.O`,`AAPL.US` 取不到数据
36
36
 
37
37
  ```bash
@@ -84,7 +84,7 @@ gangtise indicator time-series --indicator <code> [--indicator <code2>] \
84
84
  - `--currency` / `--scale`:同 `cross-section`(含根级 `--scale` 的污染坑)
85
85
  - **输出(宽表)**:每行一个日期,列为 `date / <各序列名>…`;序列在「单指标」时是各**证券**,在「单证券多指标」时是各**指标**。**板块 ID 算多证券**——传 1 个 `sectorId` 服务端会展开成 N 只成分股,列就是这 N 只(实测中信白酒 → 19 列)
86
86
  - **`--key-by name|code`**(默认 `name`):同 `cross-section`;`code` 模式下单证券列=各 `indicatorCode`、多证券列=各 `securityCode`,批量按 code 回填用它
87
- - ⚠️ 部分指标标注**不支持时序接口**:`search` 返回的 `scopeList[].usageRestriction` 会写明(如「不支持指标时间序列接口」),`null` 表示无限制。**但它不是硬约束**——实测 `qte_vol_intvl` 带着该标注调时序照样返回数据,服务端不拦。把它当"口径可能不对、结果别当真"的提示,而不是"会报错"的保证
87
+ - ⚠️ 部分指标标注**不支持时序接口**:`search` 返回的 `scopeList[].usageRestriction` 会写明(如「不支持指标时间序列接口」),`null` 表示无限制。**但它不是硬约束**——实测 `qte_vol_intvl` 带着该标注调时序照样返回数据,不会被拦下。把它当"口径可能不对、结果别当真"的提示,而不是"会报错"的保证
88
88
 
89
89
  ```bash
90
90
  # 单个已实现估值指标 × 多证券:列 = 证券
@@ -172,11 +172,28 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
172
172
 
173
173
  **缺数据 vs 代码写错(2026-08-08 服务端改版,判据整个换了)**:
174
174
 
175
- **缺数据一律是 `null` 单元格,行列都保留**,连单指标 × 单证券的 1×1 缺口也是 `null` 而不是空表。判据是**代码认不认识**,跟有没有数据无关:
175
+ **缺数据一律保留行列并给一个占位单元格**,连单指标 × 单证券的 1×1 缺口也有行有列、不是空表。判据是**代码认不认识**,跟有没有数据无关:
176
+
177
+ > 🔴 **占位值不统一,而且由指标决定——这一条会直接毁掉筛选和聚合结果**
178
+ >
179
+ > 多数指标缺数据填 `null`,但**个别指标填 `0`**,其中最常用的是 `is_dnrpnp`(扣非归母净利润)。同一天同一只股票:`is_dnrpnp` 给 `0`、`is_op_rev` 给 `null`。**与日期对不对无关**——日期完全正确(报告期末)时,覆盖不到的证券(如美股)同样给 `0`:同一次请求里茅台、泡泡玛特返真值,苹果、微软的 `is_dnrpnp` 返 `0`、`is_op_rev` 返 `null`,四行都在、无告警、退出码 0。
180
+ >
181
+ > **`0` 会照常穿过比较与比率计算**,`null` 不会。实测 `screener`(`F1 = is_dnrpnp`,三只 A 股):
182
+ >
183
+ > | 表达式 | 非报告期末日期 | 报告期末日期 |
184
+ > | :--- | :--- | :--- |
185
+ > | `F1 > 0` | **命中 0 只** | 命中 3 只 |
186
+ > | `F1 < 50亿` | **命中 3 只**(值全是 `0`) | 命中 1 只 |
187
+ >
188
+ > 只换了日期,两个条件的结果全部反转,全程不报错。「扣非净利 > 0 筛出空集」会被读成「没有盈利的股票」。
189
+ >
190
+ > **时序上更难躲**:报告期类指标按日返回,只有报告期末那几行是真值,其余是占位。`is_op_rev` 的 `null` 通常被聚合跳过、均值仍然对;`is_dnrpnp` 的 `0` 会被算进去——茅台 5 个月 104 行里 102 行是 `0`,整列求均值比正确值低 50 倍以上。而**时序没有任何参数能让它只返回报告期末**(传 `tradeDate`/`reportDate` 都会被拒)。
191
+ >
192
+ > **怎么办**:① 报告期类指标一律把日期落在**报告期末**(`2025-03-31` / `06-30` / `09-30` / `12-31`);② 拿 `is_dnrpnp` 这类做筛选或聚合前,先单查一次确认不是占位;③ **别对时序整列直接求均值 / 求和**,先按报告期末取子集。
176
193
 
177
194
  | 情况 | 服务端怎么返 | CLI |
178
195
  | :--- | :--- | :--- |
179
- | 代码有效、但**无数据 / 无覆盖 / 非交易日 / 未来日期** | 单元格 `null`,行列都在 | **退出 0**,不标 partial |
196
+ | 代码有效、但**无数据 / 无覆盖 / 非交易日 / 未来日期** | 占位单元格(多数 `null`,个别指标 `0`,见上),行列都在 | **退出 0**,不标 partial |
180
197
  | **指标码无法解析**(拼错、不存在) | 该指标整列从 `indicatorList` 消失 | `partial` + `omittedIndicators` + **退出 3** |
181
198
  | **证券码无法解析**(拼错、后缀错,如 `AAPL.US`) | 该证券整行从 `securityCodeList` 消失 | `partial` + `omittedSecurities` + **退出 3** |
182
199
  | 请求里**没有任何**可解析的代码 | 四个数组全空(`Total: 0`) | **退出 0** + stderr 提示,因为无从判断是哪一轴写错了 |
@@ -192,7 +209,7 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
192
209
  | `--indicator qte_close --indicator not_a_real_code` | 只回 `qte_close` 一列 | **退出 3** + `omittedIndicators: ["not_a_real_code"]` |
193
210
  | `--indicator not_a_real_code --security 999999.SH` | 全空 | **退出 0** + stderr 提示 |
194
211
 
195
- **这是个净收益的变化**:`partial` / 退出码 3 从「这批数据不完整」变成了**「你有代码写错了」**——后者原本是完全静默的(退出 0、表看着正常、`--key-by code` 回填时 key 直接不存在)。反过来,真实的覆盖缺口现在一眼可见(就是 `null`),不再需要「和一个已知有数的标的一起查」这种对照法。
212
+ **这是个净收益的变化**:`partial` / 退出码 3 从「这批数据不完整」变成了**「你有代码写错了」**——后者原本是完全静默的(退出 0、表看着正常、`--key-by code` 回填时 key 直接不存在)。反过来,真实的覆盖缺口现在也留在表里(就是那个占位单元格),不再需要「和一个已知有数的标的一起查」这种对照法。⚠️ 但**占位值多数是 `null`、个别是 `0`**(见上方 🔴 段),`0` 那一档并不「一眼可见」——它长得和真值一样。
196
213
 
197
214
  ⚠️ **但这个检测需要同批里有对照物**:拼错的 code 必须和至少一个能解析的 code 同批,差集才算得出来。**整个轴都写错时(最常见的就是只查一个指标、而它拼错了)响应是空表、退出码 0**,只有 stderr 提示。所以「空表」现在的第一嫌疑就是拼写和后缀,不是没数据。
198
215
 
@@ -209,7 +226,7 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
209
226
  | `100001` | **缺少必填参数**:如 `universe` 没传 | 补齐 `--indicator`/`--security` |
210
227
  | `100003`@400 | 入参/表达式错误:`time-series` 传了「多指标 × 多证券」、`expression` 引用未声明变量、`indicatorParamList` 的 code 不在 `indicatorCodeList` 里 | 按 msg 改;多 × 多改用 `cross-section`。CLI 已在本地拦截「表达式引用未绑定变量」,不会白发一次请求 |
211
228
  | `140002`@500 | **终态参数错**:指标必填参数缺失、枚举越界(如「参数 adjustType 的值 99 不在有效范围内 [1,2,3,4]」)、表达式语法错误 | **不重试**(CLI 已把 140002 列为终态码)。读 `search --format json` 的 `parameterList` 改参数名/取值 |
212
- | `999999` | 系统故障。2026-08-01 起「无数据」不再用此码,所以它基本只剩真故障(2026-07-26 曾出现 EDE 取数端全线 999999,08-01 已恢复)。⚠️ 别把它和空表混为一谈:无数据现在是 `null` 单元格,空表表示整轴 code 未识别 | CLI 对 indicator 端点**不重试此码**(v0.27.0);确认参数无误仍报错就是服务端问题 |
229
+ | `999999` | 系统故障。2026-08-01 起「无数据」不再用此码,所以它基本只剩真故障(2026-07-26 曾出现 EDE 取数端全线 999999,08-01 已恢复)。⚠️ 别把它和空表混为一谈:无数据现在是占位单元格(多数 `null`、个别指标 `0`),空表表示整轴 code 未识别 | CLI 对 indicator 端点**不重试此码**(v0.27.0);确认参数无误仍报错就是服务端问题 |
213
230
  | `110003` | **超出账号数据权限的时间范围**。⚠️ EDE 三个接口不一致:截面 / 时序已放宽(本机到 2016-01-01),**`screener` 仍卡 today−3 年滚动**(实测 2023-08-07 报错、2023-08-08 通过;同日同指标同证券 `cross-section` 正常出数) | 把日期移进范围;`screener` 撞界时改用 `cross-section` 拉数再本地筛 |
214
231
  | `130001`(旧 `410004`) | 数据未找到,或**该指标无权限**(内层信封失败会带具体 msg,如"指标无权限";此码被服务端复用) | 检查查询条件与指标权限;换证券/日期仍失败多为无权限,联系管理员开通 |
215
232
 
@@ -263,7 +280,7 @@ gangtise indicator cross-section --indicator scr_indu \
263
280
  ## 取数最佳实践
264
281
 
265
282
  - **先 search 做三项校验**:看 `indicatorName` + `description` 确认语义和口径,看 `scopeList` 确认覆盖全部目标市场 / 证券类型(并看 `usageRestriction` 有无接口限制),再看 `parameterList` 补齐必填参数(required)并核对专属参数**名称**与枚举(`adjustType`/`scale`/`currency` 等);任一不符就回退专用接口。**参数名以 search 返回为准,不要照抄文档示例**——服务端调过参数名,传错名是静默失效不是报错。
266
- - **`scopeList` 声称覆盖 ≠ 真有数**:截至 2026-08-08,`finc_pb_mrq` 的 `scopeList` 写着 A股+港股,实测港股仍无数据。**2026-08-08 起这件事好查了**:无覆盖就是 `null` 单元格(行列都在,跟同批查了什么无关),抽查一行即可判定,不用再靠对照组反推。跨市场批量取数后**逐市场抽查一行**,别默认 scopeList 就是事实——反过来也一样,`qte_mkt_cptl`/`shr_tot` 在 08-02 还是港/美无数据、08-03 就补上了,**否定结论同样会过期**。
283
+ - **`scopeList` 声称覆盖 ≠ 真有数**:截至 2026-08-08,`finc_pb_mrq` 的 `scopeList` 写着 A股+港股,实测港股仍无数据。**2026-08-08 起这件事好查了一半**:无覆盖会留下占位单元格(行列都在,跟同批查了什么无关),抽查一行即可判定,不用再靠对照组反推。🔴 **但只有 `null` 那一档真的「一眼可见」**——`is_dnrpnp` 这类填 `0` 的指标,无覆盖和真值长得一模一样(美股实测:日期正确、行都在,`is_dnrpnp` 返 `0`、`is_op_rev` 返 `null`)。对填 `0` 的指标,抽查一行判不出覆盖,要拿一个已知有数的市场做对照。跨市场批量取数后**逐市场抽查一行**,别默认 scopeList 就是事实——反过来也一样,`qte_mkt_cptl`/`shr_tot` 在 08-02 还是港/美无数据、08-03 就补上了,**否定结论同样会过期**。
267
284
  - **公司类型决定有没有这个科目**:财务科目分公司类型——银行有「存放同业」、券商有「客户资金存款」、保险有「预收保费」,一般企业没有。某指标对茅台返回 `null`(无此科目),换到对应类型证券(招行/中信/平安)就有数。
268
285
  - **日期路由**:
269
286
  - 财务报表类(`bs_`/`is_`/`cf_`/`div_`/`shr_`,以及 description 明确按报告期统计的 `finc_`)→ 用**报告期末**(Q1 `2026-03-31`、年报 `2025-12-31`,无需是交易日)
@@ -271,7 +288,7 @@ gangtise indicator cross-section --indicator scr_indu \
271
288
  - 现金流量表附注/间接法科目(多数 `cf_`)→ **只在年报/半年报披露**,季报日期取不到,改用年报日期 `2025-12-31`
272
289
  - 行情类(`qte_` 等)→ 用**交易日**,但常规行情仍应改走 `quote`
273
290
  - **混合日期语义要拆查询**:同时要“某报告期营收 / EPS”和“估值 PE / PB”时,按各自有效日期分别调用 `cross-section` 再按 `security` 合并(财务=报告期末、PE/PB=最新交易日);不要把不同日期语义的指标塞进同一个 `--date`
274
- - **探索性取数**:缺值一律是 `null` 且保留行列(2026-08-08 起,含 1×1 的最简形态);整列 / 整行消失现在只意味着**那个 code 服务端不认识**。看趋势用 `time-series` + 覆盖报告期的区间,但不能把缺值当成通过语义 / scope 校验。
291
+ - **探索性取数**:缺值会保留行列并给占位单元格(2026-08-08 起,含 1×1 的最简形态),但**占位值不统一**(多数 `null`、个别 `0`,见上);整列 / 整行消失现在只意味着**那个 code 服务端不认识**。看趋势用 `time-series` + 覆盖报告期的区间,但不能把缺值当成通过语义 / scope 校验。
275
292
  - **名称反查 code 要核对,别取首条**:存在同显示名的兄弟指标——单季 `cf_finc_exp_qtr` 与累计 `cf_finc_exp` 都叫「财务费用」,`bs_fmt`/`cf_fmt`/`is_fmt` 都叫「报表格式」。`search` 按名称模糊匹配,目标 code 高概率在 top1 但不绝对,要看 `indicatorCode` 确认。
276
293
  - **批量查询做失败拆分**:某指标**缺必填参数**或入参错误时会整批报 `140002`,逐指标单查能定位是哪个指标缺参/不可查。留意 stderr 的「整列/整行被略过」警告——那不是报错,而是**有 code 没被服务端认出来**,先查拼写和证券后缀。
277
294
  - **市值量纲(复测 2026-08-03)**:`qte_mkt_cptl`(总市值)与 `shr_tot`(总股本)**A/港/美股均已有数**(08-02 时港美股还是空的,服务端已补:泡泡玛特 2165.47 亿 / 腾讯 43207.64 亿 / 苹果 45128.55 亿);**默认返原始「元」**(茅台 ≈ `1.7e12`,即 1.7 万亿),别误当天文数字。用 `scale` 数字码缩放(`0`元 / `3`千 / `4`万 / `6`百万 / `8`亿 / `9`十亿——`scale=8` → `16883` 亿元)、`currency` 换币种(**大写** `DFT`本币 / `CNY` / `HKD` / `USD` …)。**跨证券比市值前先统一 `scale`+`currency`**。
@@ -291,6 +308,16 @@ gangtise indicator cross-section --indicator scr_indu \
291
308
 
292
309
  ## 估值指标的历史序列:两个接口口径不同,要交叉核
293
310
 
311
+ > 🔴 **对照前先避开这个陷阱:两个接口在非交易日的行为不一样。**
312
+ >
313
+ > | 日期 | EDE `finc_pe_ttm` | `fundamental valuation-analysis` |
314
+ > | :-- | :-- | :-- |
315
+ > | 2026-08-07(周五) | 23.1374 | 23.1374 |
316
+ > | **2026-08-08(周六)** | **`null`** | **23.1374**(顺延上一交易日) |
317
+ >
318
+ > 拿非交易日做对照,会看到「EDE 全是 null、对照组搭不起来」,很容易误判成 EDE 没有这些数据——**其实只是它不给非交易日补值,而 `valuation-analysis` 补**。做交叉核对时**日期一律落在交易日上**;已经撞上的,换一个交易日重跑再下结论。
319
+
320
+
294
321
  | 接口 | 财报口径切换时点 |
295
322
  | :--- | :--- |
296
323
  | `indicator time-series`(EDE) | **正式财报披露日** |
@@ -307,6 +334,6 @@ gangtise indicator cross-section --indicator scr_indu \
307
334
 
308
335
  - **发现流程**:`indicator search --format json` → 核对 `indicatorName` + `description`、`scopeList`(含 `usageRestriction`)、`parameterList`(**参数名以此为准**)→ 三项都通过才用 `cross-section` / `time-series` / `screener`
309
336
  - **积分**:`search` 免费;`cross-section` / `time-series` / `screener` 按请求单元格数量计费,标价为每 100 单元格 A 股 0.05 / 港股 0.1 / 美股 0.2 积分,每次查询不足 100 单元格按 100 计
310
- - **空结果排查顺序**:2026-08-08 起真无数据会返回 `null` 单元格而不是空表,所以**空表基本等于「没有任何 code 被认出来」或参数名写错**。按序排查:① 证券代码与后缀对不对(美股 `.O`/`.N`,不是 `.US`)② 指标 code 拼写对不对 ③ 参数名对不对(`indicator search` 的 `parameterList`)④ 日期语义对不对(`tradeDate` vs `reportDate`)
337
+ - **空结果排查顺序**:2026-08-08 起真无数据会返回占位单元格(多数 `null`、个别指标 `0`)而不是空表,所以**空表基本等于「没有任何 code 被认出来」或参数名写错**。按序排查:① 证券代码与后缀对不对(美股 `.O`/`.N`,不是 `.US`)② 指标 code 拼写对不对 ③ 参数名对不对(`indicator search` 的 `parameterList`)④ 日期语义对不对(`tradeDate` vs `reportDate`)
311
338
  - **数据权限**:2026-08-07 起正式账号行情 / 财务 / 指标类由前溯 3 年放宽到**前溯 5 年**;试用账号按服务等级。⚠️ **同一账号下三个 EDE 接口的范围可能不一致**:本机实测扩展权限只落到 `cross-section` / `time-series`,**`screener` 仍是 today−3 年滚动**(2026-08-08 实测边界 2023-08-08;同日同指标同证券截面取得到、选股报 `110003`)。选股撞界时改用 `cross-section` 拉数再本地筛
312
339
  - 所有格式(table/json/jsonl/csv/markdown)均可用;导出宽表给 Excel 直接用 `--format csv --output xxx.csv`
@@ -33,7 +33,7 @@ gangtise insight opinion list [--keyword <text>] [--research-area <id>] [--chief
33
33
 
34
34
  - `--llm-tag`:`strongRcmd` 强烈推荐 | `earningsReview` 业绩点评 | `topBroker` 头部券商 | `newFortune` 新财富团队
35
35
  - `--source`:`realTime` 实时 | `openSource` 开放来源
36
- - `--industry`:用 `citicIndustry` 码 `1008001xx`(申万码 `104xxx` 也等效);`--research-area`:用 `gangtiseIndustry` 码(行业 `1008001xx` + 方向 `122000xxx`,申万码返 0)。详见 `reference-and-lookup.md`
36
+ - `--industry`:用 `citicIndustry` 码 `1008001xx`;申万码 `104xx0000` 也生效,但**两套码的行业成分不同、取回的结果集不一致**(同一行业实测相差约 5%),同一批查询别混用。`--research-area`:行业用 `citicIndustry` 码 `1008001xx`、方向用 `gangtiseIndustry` 码 `122000xxx`,**申万码在本端点返 0**。详见 `reference-and-lookup.md`
37
37
 
38
38
  ## 纪要 `insight summary list/download`
39
39
 
@@ -44,7 +44,7 @@ gangtise insight summary download --summary-id <id> [--file-type <n>] [--output
44
44
 
45
45
  - `--search-type`:`1` 标题搜索(默认,速度快)| `2` 全文搜索
46
46
  - `--source`:`1` 实时 | `2` 开放来源
47
- - `--research-area`:用 `gangtiseIndustry` 码(行业 `1008001xx` + 方向 `122000xxx`);summary 的 spec 额外接受 citic/sw,但统一用 gangtise 最稳
47
+ - `--research-area`:行业用 `citicIndustry` 码 `1008001xx`、方向用 `gangtiseIndustry` 码 `122000xxx`。summary 是少数**申万码 `104xx0000` 也生效**的端点,但两套行业码取到的集合略有出入(同一行业实测相差约 2%),同一批查询里别混用
48
48
  - `--market`:`aShares` | `hkStocks` | `usChinaConcept` | `usStocks`
49
49
  - `--participant-role`:`management` 管理层 | `expert` 专家
50
50
  - `--category`:`earningsCall` 业绩会 | `strategyMeeting` 策略会 | `fundRoadshow` 基金路演 | `shareholdersMeeting` 股东大会 | `maMeeting` 并购会议 | `specialMeeting` 特别会议 | `companyAnalysis` 公司分析 | `industryAnalysis` 行业分析 | `other`
@@ -57,22 +57,24 @@ gangtise insight pamirs-summary list [--search-type <n>] [--rank-type <n>] [--re
57
57
  gangtise insight pamirs-summary download --summary-id <id> [--file-type <n>] [--output <path>]
58
58
  ```
59
59
 
60
- 帕米尔(Pamirs)是平台内一个特殊牵头机构的**专家纪要库**,走独立端点,不是 `summary list` 的一个筛选项。⚠️ **需单独购买专家纪要数据库**,未购买调用报权限错误。**不限制历史数据范围**(不受 3 个月窗口约束)。
60
+ 帕米尔(Pamirs)是平台内一个特殊牵头机构的**专家纪要库**,走独立端点,不是 `summary list` 的一个筛选项。⚠️ **需单独购买专家纪要数据库**:未开通时 `list` 直接报 `999004`(不是返回空列表),**整库拿不到**。**不限制历史数据范围**(不受 3 个月窗口约束)。
61
61
 
62
- - **筛选项比 `summary` 少**:没有 `--source` / `--institution` / `--participant-role`。服务端会静默丢弃不认识的 body 字段,所以别照搬 `summary` 的参数,那样只会拿到没过滤的全量
63
- - `--search-type`:`1` 标题搜索(默认)| `2` 全文搜索。实测 `--keyword PCB`:标题 36 条 vs 全文 113 条
62
+ > 未开通该库时任何查询都直接报 `999004`,不必怀疑参数写错。下面各参数的行为描述来自已开通账号的实测。
63
+
64
+ - **筛选项比 `summary` 少**:没有 `--source` / `--institution` / `--participant-role`。不认识的 body 字段会被丢弃且不报错,所以别照搬 `summary` 的参数,那样只会拿到没过滤的全量
65
+ - `--search-type`:`1` 标题搜索(默认)| `2` 全文搜索。同一关键词全文搜索的命中数明显多于标题搜索
64
66
  - `--rank-type`:`1` 综合排序(默认)| `2` 时间倒序。⚠️ 效果依赖 `--keyword`,详见本文开头的公共说明;本端点是该结论的实测对象之一
65
- - `--category`:`companyAnalysis` 公司分析 | `industryAnalysis` 行业分析(实测 2673 / 279)
67
+ - `--category`:`companyAnalysis` 公司分析 | `industryAnalysis` 行业分析(两者过滤均生效,公司分析占绝大多数)
66
68
  - `--market`:`aShares` | `hkStocks` | `usChinaConcept` | `usStocks`
67
- - `--research-area`:**citic(`1008001xx`)和申万(`104xx0000`)都生效**(实测食品饮料:citic 373 条 / 申万 145 条)——这点与多数 insight list 不同(那些的 `--research-area` 只吃 gangtise 码)
69
+ - `--research-area`:**行业码两套都生效**——citic `1008001xx` 和申万 `104xx0000`。申万码这点与多数 insight list 不同(那些只有 summary 认申万码)。⚠️ 反过来,**方向码 `122000xxx` 在本端点返 0**,别在这里传方向
68
70
  - `--file-type`(download 可选):`1` 原始文件(默认)| `2` HTML;**只有这两种**
69
- - 单页上限 50(CLI 按此翻页;服务端实测未执行该上限,传 100 会返 100),省略 `--size` 自动翻页拉全量。翻页完整性实测干净:三页无重复无缺口、可重放、`total` 不漂移
71
+ - 单页上限:spec 写 50,实际传更大的 `size` 也会照数返回。**CLI 仍按 50 翻页**——保守值在上限某天开始执行时不会被静默截断。省略 `--size` 自动翻页拉全量。翻页完整性实测干净:连续三页无重复无缺口、可重放、`total` 不漂移
70
72
  - 返回字段:`summaryId` / `title` / `brief`(摘要)/ `summaryTime`(纪要注明的生成时间)/ `publishTime`(发布时间)/ `categoryList` / `securityList[]{securityCode, securityName}` / `researchAreaList[]{researchAreaId, researchAreaName}` / `conceptList[]{conceptId, conceptName}` / `marketList`
71
- - ⚠️ **标签字段大面积不回填**(2026-08-08 实测 6 种查法 × 30 条):
72
- - **`conceptList` 在所有查法下恒为空**(无过滤 / category / market / security / researchArea / keyword 全是 `[]`),而接口**没有** concept 过滤参数——所以**目前拿不到主题概念标签,没有变通办法**,别写依赖它的逻辑
73
- - **`categoryList` 与 `marketList` 绑定在一起**:只要用 `--category` **或** `--market` 任一过滤,两个字段就都回填(30/30);其余查法(含无过滤、`--security`、`--research-area`、`--keyword`)两个都是空。回填的是该记录的**全部**值(多市场纪要按 `aShares` 过滤也回 `["aShares","hkStocks"]`,不是"回显过滤值")
74
- - **所以别拉全量再本地分组**——标签会全丢。要按类别/市场分组就逐个枚举值各查一遍再合并(请求数放大 2~4 倍),或直接让服务端筛
75
- - `researchAreaList`(抽 200 条 6% 空)和 `securityList`(10% 空)基本正常
73
+ - ⚠️ **`conceptList` / `categoryList` / `marketList` 三个标签字段稀疏,且是否有值随记录和查法而变**:
74
+ - **不带筛选时经常整条为空**,用 `--category` 或 `--market` 过滤时回填率明显更高(这两个字段是绑定的:用任一过滤,两个都会有值)。有值时给的是该记录的**全部**值(多市场纪要按 `aShares` 过滤也回 `["aShares","hkStocks"]`,不是"回显过滤值")
75
+ - **所以别拉全量再本地分组**——会漏掉大量记录。要按类别/市场分组就逐个枚举值各查一遍再合并(请求数放大 2~4 倍),或直接让服务端筛
76
+ - **反过来也别据此断言某条记录没有该属性**——标签为空只说明这次查询没回填,不代表该纪要真的没有概念/分类/市场归属
77
+ - `researchAreaList` 和 `securityList` 相对完整,抽样空值比例都在 10% 以内
76
78
 
77
79
  ```bash
78
80
  # 近一周的帕米尔纪要
@@ -104,7 +106,7 @@ gangtise insight forum list [--research-area <id>] [--location <id>]
104
106
  - `--participant-role`(仅路演):`management` | `expert`
105
107
  - `--permission`(路演/调研):`1` 公开 | `2` 私密
106
108
  - `--market`:路演 `aShares`|`hkStocks`|`usChinaConcept`|`usStocks`;调研 `aShares`|`hkStocks`|`usChinaConcept`(无 usStocks)
107
- - `--research-area`(路演/调研/论坛):用 `gangtiseIndustry` 码(行业 `1008001xx` + 方向 `122000xxx`,见 `reference-and-lookup.md`)。**strategy 无 `--research-area`,只按 `--institution`/`--location` 筛**
109
+ - `--research-area`(路演/调研/论坛):行业用 `citicIndustry` 码 `1008001xx`、方向用 `gangtiseIndustry` 码 `122000xxx`,**申万码 `104xx0000` 在这三个端点返 0**(见 `reference-and-lookup.md`)。**strategy 无 `--research-area`,只按 `--institution`/`--location` 筛**
108
110
 
109
111
  ## 财报日历 `insight performance-calendar list/download`
110
112
 
@@ -194,7 +196,8 @@ gangtise insight foreign-opinion list [--rank-type <n>] [--security <code>] [--r
194
196
  ```
195
197
 
196
198
  - `--security`:境外证券代码,如 `UBER.N`
197
- - `--region`:`cn` | `cnHk` | `cnTw` | `us` | `jp` | `uk`
199
+ - ⚠️ `--region`:`cn` | `cnHk` | `cnTw` | `us` | `jp` | `uk` | `gl`——**本端点当前不可靠**:`cn` / `us` / `jp` 实测返回 `null` payload(不报错),只有 `gl` 返回数据。需要按区域筛请不带该参数取回后按 `region` 字段本地筛。⚠️ **注意 `insight foreign-report` 的 `--region` 是正常的**(cn / us / gl 都能正确收窄),坏的只是本端点
200
+ - ⚠️ `--industry`:**当前传任何值都拿不到数据**(中信码 / 申万码 / 乱码一样,不报错、退出码 0)。🔴 **返回的 payload 是字面 `null`,不是 `{total:0,list:[]}`** ——按空列表解析的脚本(`data.list.length`、`for row in data["list"]`)会在这里抛错或静默跳过,务必先判 `null`。需要按行业筛时不带该参数取回后本地按 `industryList[]` 筛
198
201
  - `--broker`:外资券商 ID(见 `references/lookup-ids.md`)
199
202
  - `--rating` / `--rating-change`:同研报
200
203
  - 返回字段:`foreignOpinionId` / `title` / `titleTranslate` / `content` / `contentTranslate` / `publishTime` / `publisher{brokerId, brokerName}` / `securityList[]{securityCode, rating, targetPrice, currency}` / `region`
@@ -207,6 +210,7 @@ gangtise insight independent-opinion download --independent-opinion-id <id> --fi
207
210
  ```
208
211
 
209
212
  - `--security`:境外证券代码,如 `GSK.N`
213
+ - ⚠️ `--industry`:**当前传任何值都拿不到数据,且 payload 是字面 `null` 而非空 list**(同 foreign-opinion,注意事项见上),改用不带该参数取回后按 `industryList[]` 本地筛
210
214
  - `--rating` / `--rating-change`:同外资观点
211
215
  - `--file-type`(download **必选**):`1` 原文 HTML | `2` 中文翻译 HTML
212
216
  - 返回字段:`independentOpinionId` / `title` / `titleTranslate` / `brief` / `briefTranslate` / `publishTime` / `analyst{analystId, analystName}` / `securityList[]` / `industryList[]`
@@ -96,17 +96,32 @@ gangtise reference constant-category [--format json]
96
96
  | category | 名称 | 结构 | 用于参数 |
97
97
  |----------|------|------|---------|
98
98
  | `citicIndustry` | 中信一级行业(30,`1008001xx`) | flat | `--industry` 全命令通用首选(见下方说明) |
99
- | `swIndustry` | 申万一级行业(31,`104xxx`) | flat | `--industry`(仅 6 个 insight list;`vault wechat-message-list` 的 `--industry` 当前两套码都不生效) |
100
- | `gangtiseIndustry` | Gangtise 行业(30 行业 `1008001xx` + 6 方向 `122000xxx`) | flat | `--research-area` 首选(含宏观/策略等方向,见下方说明) |
99
+ | `swIndustry` | 申万一级行业(31,`104xx0000`) | flat | `--industry`(opinion / research / foreign-report / official-account 可用,但**与中信码不等效**,见下方说明);`--research-area` 只有 summary / pamirs-summary 认 |
100
+ | `gangtiseIndustry` | Gangtise 研究方向(**只有 6 条方向码** `122000xxx`,不含行业码) | flat | `--research-area` 的方向维度(宏观/策略/固收/金工/海外/其他,见下方说明) |
101
101
  | `domesticCity` | 国内城市(省级 ID) | flat | `--location`(roadshow / site-visit / strategy / forum)|
102
102
  | `aShareAnnouncementCategory` | A股公告分类 | tree(2 级) | `insight announcement --category` |
103
103
  | `hkShareAnnouncementCategory` | 港股公告分类 | tree(2 级) | `insight announcement-hk --category` |
104
104
  | `usShareAnnouncementCategory` | 美股公告分类(`103980xxx` 段) | tree(2 级) | `insight announcement-us --category` |
105
105
  | `regionCategory` | 区域分类 | flat | `insight foreign-report --region` |
106
106
 
107
- > **行业 / 研究方向过滤——选哪套 category(⭐ 权威口径,其他文件引用此处、勿重复枚举命令清单以免漂移;实测 + spec,2026-06-15):**
108
- > - **`--industry`(industryList)→ 用 `citicIndustry`(`1008001xx`)**:opinion / research / foreign-report / foreign-opinion / independent-opinion / official-account 正确过滤。⚠️ **`vault wechat-message-list` 例外——该命令的 `--industry` 当前两套码都不生效**(2026-08-08 复测,详见 `vault.md`)。`swIndustry`(`104xxx`)在 6 个 insight list(含 official-account)上等效(spec 多数命令写 citic+sw)→ 统一用中信码最稳。
109
- > - **`--research-area`(researchAreaList)→ 用 `gangtiseIndustry`**:完整研究方向分类 = 30 个行业(`1008001xx`,与 citicIndustry 相同)+ 6 个方向(宏观 `122000001` / 策略 `122000002` / 固收 `122000003` / 金工 `122000004` / 海外 `122000005` / 其他 `122000007`)。行业码与方向码均已实测正确过滤(opinion / summary / roadshow / site-visit / forum)。`citicIndustry` 也能用但只含行业、无方向;`swIndustry`(`104xxx`)除 summary / my-conference 外返 0,勿用。
107
+ > **行业 / 研究方向过滤——选哪套 category(⭐ 权威口径,其他文件引用此处、勿重复枚举命令清单以免漂移;全部为逐端点实测,2026-08-08):**
108
+ >
109
+ > 先记住三套码的归属,**别把行业码算到 `gangtiseIndustry` 头上**:`citicIndustry` = 30 条行业码 `1008001xx`;`swIndustry` = 31 条行业码 `104xx0000`;`gangtiseIndustry` = **只有 6 条方向码** `122000xxx`(宏观 `122000001` / 策略 `122000002` / 固收 `122000003` / 金工 `122000004` / 海外 `122000005` / 其他 `122000007`),里面**没有任何行业**,去它那里找「食品饮料」永远找不到。
110
+ >
111
+ > - **`--industry`(industryList)→ 用 `citicIndustry`(`1008001xx`)**:opinion / research / foreign-report / official-account 正确过滤。`swIndustry`(`104xx0000`)在这 4 个上**也生效,但不是等效**——两套码的行业成分不同,4 个里有 2 个结果集对不上(research / foreign-report 完全一致;**opinion 与 official-account 传两套码取回的条数相差约 2%–5%**)。所以**统一用中信码**,同一批查询里别把两套码混着传。方向码 `122000xxx` 在 `--industry` 上一律返 0。⚠️ 两个例外:`insight foreign-opinion` / `independent-opinion` 只要传 `--industry` 就拿不到数据(不报错,且 **payload 是字面 `null`、不是空 list**,详见 `insight.md`),只能不带该过滤取回后本地筛;`vault wechat-message-list` 的 `--industry` 两套码都不生效(详见 `vault.md`)。
112
+ > - **`--research-area`(researchAreaList)→ 用 `citicIndustry` 行业码 `1008001xx`,方向再叠 `gangtiseIndustry` 的 `122000xxx`**。⚠️ **申万码 `104xx0000` 只有 `summary` / `pamirs-summary` 认**,其余端点返 0(与传乱码表现一致,不报错)。逐端点实测(同一行业分别传三套码,✅ = 正常过滤,❌ = 返 0):
113
+ >
114
+ > | 端点 | 中信 `1008001xx` | 申万 `104xx0000` | 方向 `122000xxx` |
115
+ > | :--- | :---: | :---: | :---: |
116
+ > | `insight summary` | ✅ | ✅ | ✅ |
117
+ > | `insight pamirs-summary` | ✅ | ✅ | **❌ 返 0** |
118
+ > | `insight opinion` | ✅ | **❌ 返 0** | ✅ |
119
+ > | `insight roadshow` | ✅ | **❌ 返 0** | ✅ |
120
+ > | `insight site-visit` | ✅ | **❌ 返 0** | ✅ |
121
+ > | `insight forum` | ✅ | **❌ 返 0** | ✅ |
122
+ > | `vault my-conference-list` | ✅ | **❌ 返 0** | ✅ |
123
+ >
124
+ > 一句话结论:**行业维度一律传中信码**(唯一全端点通吃的一套),方向维度传 `122000xxx`(`pamirs-summary` 不支持方向)。
110
125
 
111
126
  ## 常量值 `reference constant-list`
112
127
 
@@ -209,9 +224,9 @@ gangtise lookup meeting-org list # 会议/牵头机构全量(--instit
209
224
  | 银行 / 银行股 | 银行 | `104480000` | `821047.SWI` |
210
225
  | 汽车 / 新车 | 汽车 | `104280000` | `821036.SWI` |
211
226
 
212
- > ¹ 第 3 列是**申万码**(`104xxx`),仅在 opinion / research / foreign-report / foreign-opinion / independent-opinion / official-account 这 6 个 insight list 上作 `--industry` 等效;`--research-area` 返 0。`vault wechat-message-list` 的 `--industry` 与码表无关——**两套码都不生效**——通用/推荐的**中信码**(`1008001xx`)见 `references/lookup-ids.md` 中信表或 `constant-list --category citicIndustry`。
227
+ > ¹ 第 3 列是**申万码**(`104xx0000`),仅在 opinion / research / foreign-report / official-account 上可作 `--industry` 使用,且**与中信码不等效**(opinion / official-account 两套码结果集不同,见上方 ⭐ 权威口径);用于 `--research-area` 时只有 summary / pamirs-summary 认,其余端点返 0。`vault wechat-message-list` 的 `--industry` 与码表无关——**两套码都不生效**——通用/推荐的**中信码**(`1008001xx`)见 `references/lookup-ids.md` 中信表或 `constant-list --category citicIndustry`。
213
228
 
214
- > **参数名选择**:`--industry`(industryList,用 `citicIndustry` 码 `1008001xx`)用于 opinion / research / foreign-report / foreign-opinion / independent-opinion / official-account(`vault wechat-message-list` 也有这个参数,但**当前不生效**,见 `vault.md`);`--research-area`(researchAreaList,用 `gangtiseIndustry` 码 = 行业 `1008001xx` + 方向 `122000xxx`)用于 opinion / summary / roadshow / site-visit / forum / my-conference;`--gts-code` 仅用于 `ai security-clue`(需申万格式 `821xxx.SWI`,不是数字 ID)。注意 strategy(线下策略会)无 `--research-area`,只按 `--institution` / `--location` 筛。
229
+ > **参数名选择**:`--industry`(industryList,用 `citicIndustry` 码 `1008001xx`)用于 opinion / research / foreign-report / official-account(`foreign-opinion` / `independent-opinion` / `vault wechat-message-list` 也有这个参数,但**当前不生效**,见上方 ⭐ 权威口径);`--research-area`(researchAreaList,行业用 `citicIndustry` 码 `1008001xx`、方向用 `gangtiseIndustry` 码 `122000xxx`)用于 opinion / summary / pamirs-summary / roadshow / site-visit / forum / my-conference;`--gts-code` 仅用于 `ai security-clue`(需申万格式 `821xxx.SWI`,不是数字 ID)。注意 strategy(线下策略会)无 `--research-area`,只按 `--institution` / `--location` 筛。
215
230
 
216
231
  > **"消费"歧义**:用户说"消费/大消费"覆盖多个子行业(食品饮料 `104340000` / 商贸零售 `104450000` / 社会服务 `104460000` / 家电 `104330000` / 纺织服饰 `104350000` / 美容护理 `104770000`),需向用户确认具体方向,或用 `--keyword 消费` 做宽泛搜索。
217
232
 
@@ -37,7 +37,7 @@ gangtise vault my-conference-download --conference-id <id> --content-type <type>
37
37
 
38
38
  - `--category`:`earningsCall` 业绩会 | `strategyMeeting` 策略会 | `fundRoadshow` 基金路演 | `shareholdersMeeting` 股东大会 | `maMeeting` 并购会议 | `specialMeeting` 特别会议 | `companyAnalysis` 公司分析 | `industryAnalysis` 行业分析 | `other`(可重复)
39
39
  - `--source`:录制来源 `1`=企微会议助理 | `2`=会议服务微信群(可重复;不传返回全部)
40
- - `--keyword` vs `--research-area`:用户说"关于AI的"用 `--keyword AI`;说"电子行业的会议"用 `--research-area 100800126`(research-area 用 `gangtiseIndustry` 码 `1008001xx` + 方向 `122000xxx`,**不要用申万码 `104xxx`**)
40
+ - `--keyword` vs `--research-area`:用户说"关于AI的"用 `--keyword AI`;说"电子行业的会议"用 `--research-area 100800126`(行业用 `citicIndustry` 码 `1008001xx`、方向用 `gangtiseIndustry` 码 `122000xxx`,**不要用申万码 `104xx0000`**——本端点传申万码一律返 0 且不报错,食饮 / 电子 / 医药三个行业交叉验证过,换中信码即正常过滤)
41
41
  - `--content-type`(download **必选**):`asr` 语音识别 | `summary` AI 速记
42
42
  - 返回字段:`conferenceId` / `title` / `publishTime` / `category` / `institution{...}` / `security{...}` / `researchArea{...}` / `guest` / `sourceList`(录制来源,`1`/`2`)
43
43
 
@@ -257,7 +257,7 @@
257
257
  --date 2026-07-31 --key-by code --format json
258
258
  4. 按 security 合并两张宽表(列头即 indicatorCode,各取所需日期的值);不要把不同日期语义的指标塞进同一个 --date。
259
259
  5. 计费:search 免费;两次取数各按请求单元格数量计费,每次不足 100 单元格按 100 计。
260
- 6. 无数据(无覆盖 / 非交易日 / 未来日期)一律是单元格 null 且行列都在,退出码 0。整列/整行消失现在只意味着那个 code 服务端不认识——指标码拼错,或证券后缀错(美股用 .O/.N,不是 .US),CLI 会标 partial + 退出码 3 并在 stderr 列出。空表则是「一个 code 都没认出来」或参数名写错,先核对代码后缀与 parameterList。
260
+ 6. 无数据(无覆盖 / 非交易日 / 未来日期)一律保留行列并给占位单元格,退出码 0——但占位值多数是 null、个别指标(如 is_dnrpnp)是 0,0 会穿过比较与聚合,别当真值(见 commands/indicator.md)。整列/整行消失现在只意味着那个 code 服务端不认识——指标码拼错,或证券后缀错(美股用 .O/.N,不是 .US),CLI 会标 partial + 退出码 3 并在 stderr 列出。空表则是「一个 code 都没认出来」或参数名写错,先核对代码后缀与 parameterList。
261
261
  ```
262
262
 
263
263
  ## 例 15b:帕米尔专家纪要(新库,筛选项与踩坑都和 summary 不同)
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 中信行业分类(--industry 参数)
8
8
 
9
- 完整列表用 `reference constant-list --category citicIndustry`。**`--industry` 全命令通用首选用这套(`1008001xx`)**——opinion / research / foreign-report / foreign-opinion / independent-opinion 都正确过滤(⚠️ `vault wechat-message-list` 例外,该命令的 `--industry` 当前两套码都不生效);这套 ID 同时也是 `--research-area` 的行业部分(见下方研究方向)。
9
+ 完整列表用 `reference constant-list --category citicIndustry`(30 条)。**`--industry` 全命令通用首选用这套(`1008001xx`)**——opinion / research / foreign-report / official-account 都正确过滤(⚠️ 三个例外:`insight foreign-opinion` / `independent-opinion` 传了 `--industry` 拿不到数据、且 **payload 是字面 `null` 不是空 list**;`vault wechat-message-list` 的 `--industry` 两套码都不生效、连乱码也返全量);这套 ID 同时也是 `--research-area` **行业维度唯一全端点通吃的一套**(见下方研究方向)。
10
10
 
11
11
  | ID | 行业 | ID | 行业 |
12
12
  |----|------|----|------|
@@ -30,7 +30,7 @@
30
30
 
31
31
  ## 申万行业(--industry 参数)
32
32
 
33
- 完整列表用 `reference constant-list --category swIndustry`。⚠️ 申万码(`104xxx`)只在**部分** insight list 上作 `--industry` 等效、**不能用于 `--research-area`(返 0)**;`vault wechat-message-list` 的 `--industry` **两套码都不生效**(连不存在的码也返全量,该过滤整个被忽略)——哪套码用于哪些命令的**权威口径见 `references/commands/reference-and-lookup.md`「行业 / 研究方向过滤——选哪套 category」**,勿在此重复枚举。拿不准就统一用上方中信码。
33
+ 完整列表用 `reference constant-list --category swIndustry`(31 条)。⚠️ 申万码(`104xx0000`)只在**部分** insight list 上可作 `--industry` 使用,且**与中信码不等效**——两套码行业成分不同,实测 opinion 与 official-account 上取回的结果集就对不上(同一行业相差约 2%–5%),别混用;用于 `--research-area` 时**只有 `summary` / `pamirs-summary` 认,其余端点返 0**(不报错,与传乱码表现一致)。`vault wechat-message-list` 的 `--industry` **两套码都不生效**(连不存在的码也返全量,该过滤整个被忽略)——哪套码用于哪些命令的**权威口径见 `references/commands/reference-and-lookup.md`「行业 / 研究方向过滤——选哪套 category」**(含逐端点实测矩阵),勿在此重复枚举。拿不准就统一用上方中信码。
34
34
 
35
35
  | ID | 行业 | ID | 行业 | ID | 行业 |
36
36
  |----|------|----|------|----|------|
@@ -109,9 +109,11 @@
109
109
 
110
110
  ## 常用研究方向(--research-area 参数)
111
111
 
112
- `--research-area` 用 **gangtiseIndustry**:= 30 个行业(`1008001xx`,与上方「中信行业分类」表完全相同)+ 6 个方向。完整列表用 `reference constant-list --category gangtiseIndustry`。适用 opinion / summary / roadshow / site-visit / forum / my-conference(strategy 无此参数)。
112
+ `--research-area` 吃两个维度:**行业**用上方「中信行业分类」表的 `1008001xx`(category `citicIndustry`),**方向**用下表的 `122000xxx`(category `gangtiseIndustry`)。适用 opinion / summary / pamirs-summary / roadshow / site-visit / forum / my-conference(strategy 无此参数)。
113
113
 
114
- **6 个研究方向(gangtiseIndustry 独有,citicIndustry 没有,已实测正确过滤):**
114
+ ⚠️ **`gangtiseIndustry` 里只有下面这 6 条方向码,不含任何行业码**——`constant-list --category gangtiseIndustry` 返回的就是 6 条,去它那里找「食品饮料」找不到,行业码要查 `citicIndustry`。
115
+
116
+ **6 个研究方向(citicIndustry 没有,已实测正确过滤):**
115
117
 
116
118
  | ID | 方向 | ID | 方向 |
117
119
  |----|------|----|------|
@@ -119,7 +121,7 @@
119
121
  | 122000003 | 固收 | 122000004 | 金工 |
120
122
  | 122000005 | 海外 | 122000007 | 其他 |
121
123
 
122
- 30 个行业的 ID 直接用上方「中信行业分类」表(`1008001xx`)。⚠️ **申万码 `104xxx` 不能用于 `--research-area`**(除 summary / my-conference 的 spec 例外,其余返 0)。
124
+ 行业维度的 ID 直接用上方「中信行业分类」表(`1008001xx`),这是唯一在全部 7 个端点上都生效的一套。⚠️ **申万码 `104xx0000` 用于 `--research-area` 只有 `summary` / `pamirs-summary` 认**,opinion / roadshow / site-visit / forum / my-conference 一律返 0(与传乱码表现一致,不报错)。反过来方向码 `122000xxx` 在 `pamirs-summary` 上返 0。完整实测矩阵见 `commands/reference-and-lookup.md`。
123
125
 
124
126
  ---
125
127
 
@@ -4,7 +4,7 @@ CLI 自动处理 envelope:`{code, msg, data}` 信封会按 `code === "000000"`
4
4
 
5
5
  > 例外:`indicator`(EDE)四个接口(`search` / `cross-section` / `time-series` / `screener`)成功时**双层信封**(`data` 里再裹一层 `{code, status, data}`)。内层字段名 2026-08-01 起为 `securityCodeList` / `securityNameList` / `indicatorList[{code,name,dataType}]`(screener 另带 `field`),`values` 是 2D 矩阵:**截面与 screener 为 `[证券][指标]`**(该版转置过,此前是 `[指标][证券]`)、时序仍为 `[序列][日期]`。
6
6
  >
7
- > **缺数据 vs 代码写错**(2026-08-08 实测):无数据 / 无覆盖一律是单元格 `null`,行列都保留。仍会整列 / 整行消失的只有**服务端解析不了的 code**——指标 code 拼错,或证券 code 后缀错(美股是 `.O`/`.N`,`AAPL.US` 会整行消失)。请求里没有任何可解析 code 时才是全空数组。CLI 的 `indicator` 子命令已二次解包、拍平成宽表,并在整列/整行被略过时标 `partial` + `omittedIndicators`/`omittedSecurities` + 退出码 3——**这个信号现在读作「有 code 写错了」**。**`screener` 例外**:把缺列的变量当作无法求值,按表达式的**布尔结构**判断是否还有分支能成立(`A && B` 要两边、`A || B` 只要一边)。一条都不剩 → **退出码 1 且不输出**(那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过);仍有分支可求值、或缺的只是输出用的辅助变量 → `partial` + 退出码 3。直接 `raw call indicator.*` 只会剥外层,内层需自行处理。
7
+ > **缺数据 vs 代码写错**(2026-08-08 实测):无数据 / 无覆盖一律保留行列并给占位单元格——**多数指标是 `null`,个别(如 `is_dnrpnp`)是 `0`,`0` 会穿过比较与聚合,详见 `commands/indicator.md`**。仍会整列 / 整行消失的只有**服务端解析不了的 code**——指标 code 拼错,或证券 code 后缀错(美股是 `.O`/`.N`,`AAPL.US` 会整行消失)。请求里没有任何可解析 code 时才是全空数组。CLI 的 `indicator` 子命令已二次解包、拍平成宽表,并在整列/整行被略过时标 `partial` + `omittedIndicators`/`omittedSecurities` + 退出码 3——**这个信号现在读作「有 code 写错了」**。**`screener` 例外**:把缺列的变量当作无法求值,按表达式的**布尔结构**判断是否还有分支能成立(`A && B` 要两边、`A || B` 只要一边)。一条都不剩 → **退出码 1 且不输出**(那些行以「通过了该条件」的名义呈现,而条件根本无法证明被执行过);仍有分支可求值、或缺的只是输出用的辅助变量 → `partial` + 退出码 3。直接 `raw call indicator.*` 只会剥外层,内层需自行处理。
8
8
 
9
9
  ## 通用模式(5 类)
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "CLI for Gangtise OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {