gangtise-openapi-cli 0.44.0 → 0.46.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,18 +6,19 @@
6
6
 
7
7
  README 仅列最近 5 个版本摘要:
8
8
 
9
+ - **v0.46.0 — 2026-10-14**:① **`reference securities-search --category bond`**:按代码或简称搜债券,代码用于 `bond` 命令组。⚠️ **可转债可能出现在任何 `--category` 的结果里**(按简称查股票时常排第一),**只取返回行 `category` 等于所查类别的那一行**;债券代码传给 `fund` 命令会返回空结果、不报错。② **场外基金可按名称搜到**(`--category fund` 同时返回 `.OF` 与场内代码)。③ **计费单价按平台最新价目更新**:一页通等 AI 生成 10/次、业绩点评 / 观点 PK 20/次、热点话题 10/篇、个股看点 1/条、路演四件套 10/条、独立观点列表 1/条、纪要 / 外资研报 / 我的会议下载 30/篇、公告下载 A 股 1 / 港美股 2、`edb-data` 30/指标;额度保护随之调整。④ **数据范围**:`SKILL.md` 按官方权限表逐命令列出正式 / 试用两档,规划查询区间按表来。⑤ **报错口径**:EDE 漏传必填的 `fiscalYear` 报 `100001`;`main-business` / `valuation-analysis` 的 `--field` 写错名字报 `100005`;`screener` 计费按筛选前范围保守估算。完整列表见 CHANGELOG。
10
+
11
+ - **v0.45.0 — 2026-10-07**:① **`vault drive-copy` 可复制整个文件夹**:`--folder-id` + `--target-parent-id`,连同子文件夹与文件复制到另一空间,返回 `{folderId, newFolderId}`;复制文件的写法不变,两组参数二选一。② **下载文件名**:响应同时给出 UTF-8 文件名与普通文件名时用 UTF-8 的那个。③ `GANGTISE_BASE_URL` 指向非本机地址却用 `http://` 时 stderr 提示一次(凭证会明文传输)。④ 文档:`bond daily-quote` / `bond valuation` 的数据可能比账号窗口短,**按年份取长序列时核对首行日期**;`bond announcement` 按日期查询时有代码的行都带后缀;基金只知道名称时怎么换代码;分钟 K 的可查历史是滚动的短窗口。
12
+
9
13
  - **v0.44.0 — 2026-09-30**:**新增 `fund` 公募基金 18 个命令,一律 0.4 积分/次**(按次,与返回行数、基金只数无关):`basic-info` / `nav`(日频净值)/ `fee-rate` / `manager-info`(按姓名精确匹配,同名全返回)/ `manager-history` / `asset-size` / `holder-structure` / `top10-holders` / `asset-allocation` / `stock-portfolio` / `industry-allocation` / `bond-portfolio` / `bond-type-allocation` / `fund-portfolio` / `fund-type-allocation` / `etf-pcf-header` / `etf-pcf-components` / `etf-share-change`。`--security` 用带大写后缀的基金代码(场外 `.OF`,场内 `.SH` / `.SZ`),**代码不存在、不带后缀或后缀小写都返回空结果、不报错**。不分页:单次上限 10000 行,超出整批报 `100006`;日期两端都不传取账号可回溯窗口内的全部,起点早于窗口整批报 `110003`。**整族超时 / 5xx 不自动重发**(避免重复扣费)。取数前注意:各命令单位不同(元 / 万元 / 万份 / 万股);`holder-structure` 的 `holderCount` 是带千分位的字符串;持仓明细只返回证券简称、不返回代码;`fund-type-allocation` 季报期只含重仓基金。逐条见 `gangtise-openapi/references/commands/fund.md`。**另有几处行为变化**:只收一个值的参数重复传直接报错;代码类列表也按顿号 / 分号 / 空格分隔并去重;显式 `--size` 与 `fundamental earning-forecast` 的日期区间估算超过 1000 积分同样要 `--yes`;jsonl / csv 文件一律以换行结尾,csv 不再给 `-3.5%` 这类值加 `'`;全市场分片早于账号窗口的部分跳过而不中止;接口返回下载链接时不带 `--output` 也会下载;A 股公告与 `ai knowledge-batch` 只写日期的 `--end-time` 按当日 23:59:59 换算;`--indicator-param` 里的日期值与 `--date` 同样校验;单次请求另有总时长上限(`GANGTISE_TIMEOUT_MS` 的 2 倍,下载与上传 60 倍);环境变量设成空字符串按未设置处理。完整列表见 CHANGELOG。
10
14
 
11
15
  - **v0.43.1 — 2026-09-27**:① **EDE `fiscalYear` 漏传不报错**:预测类(`frcst_*`)与分红类(`div_cash_yr` 等)漏传时按服务端自定的默认年度取数,数看着正常但未必是你要的年度——**一律显式传 `fiscalYear`**;其余必填参数缺失仍报 `100001`。② **翻页去重**:同一 ID 的任一已出现版本再次出现都按重复去掉(计入 `duplicateRows`),不区分字段顺序。③ **观点 `detail`**:某一批夹有空元素或格式异常的元素时,这一批已返回的正文照常输出;空元素的 ID 列在 `missingIds`,其余没取到的列在 `unfetchedIds`(`unfetchedError` 带 traceId)。④ 文档:`finc_roe_avg_avg` 示例补 `reportDate`;债券三个评级命令的计费说明统一。
12
16
 
13
17
  - **v0.43.0 — 2026-09-26**:① **按条计费列表的额度保护**:省略 `--size` 时先拿到 `total` 估算全量积分(整页超过 50 积分的列表只先取 1 条),超过 1000 积分报错退出 1,加 `--yes` 或传 `--size N` 放行。② **翻页结果的完整性**:跨页重复的行去掉并标 `duplicateRows`;同一 ID 在后面的页内容变了,两版都保留并标 `changedRows`;`total` 正好等于偏移窗口时标 `totalCapped`;都退出 3。热点话题、纪要、A 股 / 港股公告、财报日历查不到内容时按空结果、退出 0(此前退出 3)。③ **`quote` 的 `--field` 在多只证券或全市场时自动补身份列**(日 K 补 `securityCode` / `tradeDate`、分钟 K 补 `securityCode` / `tradeTime`、`realtime` 补 `securityCode`;单只不补),**多只证券只点一列的脚本输出会多出这些列**。④ **多证券日 K 合批**:按行数上限装入尽量多的证券,请求数大幅减少;全市场分片按工作日计(港股 2 个工作日一片),全市场关键字须同时给起止日期。⑤ **首行晚到提示**:行情首行比请求起点晚 14 天以上时 stderr 提示(上市较晚或越过回溯窗口)。⑥ **`ai stock-summary` 与 `fundamental earning-forecast` 超时 / 5xx 不再自动重试**(单次可能扣数千积分)。⑦ 四个枚举参数写错时本地报错;Ctrl-C / `kill` / 终端断开时清理暂存文件,进程以该信号结束(`$?` 为 130 / 143 / 129),脚本循环随之停下。⑧ Agent Skill 主文件精简,细节移到 `references/`。
14
18
 
15
- - **v0.42.0 — 2026-09-25**:① **数值参数只收十进制写法**:`--size 0x10`、`--size 1e3`、`--limit 5.0` 这类写法发请求前报错并点名参数(此前会被换算成 16 / 1000 / 5 照常发出);数字列表类参数逐项要求整数。**脚本里用了这类写法的,请改成普通整数。** ② **接管道时保留退出码**:`gangtise … | head` 提前关掉读端时,以已判定的退出码(`3` / `4`)结束,此前一律 `0`;脚本用 `set -o pipefail` 即可看到。③ **`GANGTISE_TIMEOUT_MS` 只收整数毫秒**:低于 1 秒回退默认、高于 1 小时按 1 小时,所写数值没有按原样生效时 stderr 提示一次。④ 观点 `detail` 与估值分析的返回结构与约定不符时报错并附 trace,不再按空结果处理。⑤ `fundamental valuation-analysis` 的 `--field` 不含数值列、接口返回 0 行时,stderr 说明原因。⑥ 性能:大批量导出按块写盘;自动翻页时单个慢页对其余页的拖累更小;`GANGTISE_PAGE_CONCURRENCY` 设到 16 以上时并发随之增加。
16
-
17
- - **v0.41.1 — 2026-09-24**:① **`quote index-day-kline --security all` 改为直接报错**:该接口对 `all` 返回空结果、不报错,与「当天无数据」无法区分;报错信息写明原因与替代写法。指数日 K 请用 `quote day-kline` 逐个传代码;该接口的返回字段与 `day-kline` 相同、不含指数名称,名称用 `reference securities-search --keyword <指数代码> --category index` 返回的 `gtsName`。② **`fundamental valuation-analysis` 长区间不再被静默截断**:序列逐自然日一行(含周末),默认只取最近 2000 行,区间更长时开头会缺失——现在撞满即标 `partial`、退出码 3,并提示把 `--limit` 设到不小于区间天数(按 366 × 年数估算,如 5 年约 1830 行);首行恰好就是 `--start-date` 时说明没丢,不标。起点早于账号回溯下界时该接口从下界起返回、不报错,CLI 在 stderr 提示首行晚于 `--start-date`。③ **估值分析 `--field` 不再可能错列**:CLI 发请求前对字段去重并去掉 `tradeDate`(它总在第一列返回)。旧版在 `--field` 同时含 `tradeDate`(或重复字段)与不存在的字段名时,会输出整体右移一列的数据且退出 0,**用过这类组合的估值结果请重跑**。④ 依赖 `undici` 升至 7.29.1(安全更新)。⑤ 文档:`fundamental valuation-analysis` 的 `--field` 至少要含一个数值列(`value` 等),只传 `tradeDate` 或只传不存在的名字时接口返回 0 行、不报错;`references/errors.md` 的退出码摘要补上 `4`。⑥ **Skill 安装命令改为可重复执行**:目标目录已存在时直接 `cp -r`,新版会被复制进嵌套的 `gangtise-openapi/gangtise-openapi/`、已安装的副本仍是旧版;这样更新过的,按「AI Agent Skill」段的新命令重新执行一次即可。
18
-
19
19
  ### 历史里程碑
20
20
 
21
+ - **v0.42.0**:数值参数只收十进制写法(`0x10` / `1e3` 这类写法发请求前报错);接管道时保留退出码 `3` / `4`。
21
22
  - **v0.41.0**:新增债券 12 个命令与云盘管理 9 个命令;观点列表改走 v2(列表只含摘要 1 积分/条,正文按 ID 另取);新增会议线索与联网搜索。
22
23
  - **v0.40.0**:自选股股票池可增删改(首批写操作,删池须显式 `--yes`);`indicator time-series` 自动判定日期轴;Token 缓存绑定签发它的凭证。
23
24
  - **v0.39.0**:`csv` / `jsonl` 导出的元信息加上字节数与 `sha256`,转交前可核验;并发导出到同一 `--output` 不再互相掺混,文件被另一次导出替换时退出码 4。
@@ -80,7 +81,7 @@ export GANGTISE_TIMEOUT_MS=30000 # 请求超时,整数毫秒(默认 30
80
81
  export GANGTISE_TOKEN_CACHE_PATH=... # 覆盖 token 缓存路径(默认 ~/.config/gangtise/token.json)
81
82
  ```
82
83
 
83
- 环境变量设成空字符串(如 `GANGTISE_BASE_URL=`)按未设置处理,取默认值。
84
+ 环境变量设成空字符串(如 `GANGTISE_BASE_URL=`)按未设置处理,取默认值。`GANGTISE_BASE_URL` 指向非本机地址却用 `http://` 时,CLI 在 stderr 提示一次:登录与请求里的 AK / SK / token 会明文传输。
84
85
 
85
86
  如果没有 `GANGTISE_TOKEN`,CLI 会自动调用 token 接口并缓存到本地(`~/.config/gangtise/token.json`,权限 0600)。服务端判定 Token 失效时,先用其他进程已刷新并写入缓存的 token,不行再重新登录,然后重发请求;凭证本身错(AK/SK 不匹配)不重试,直接报错让你查环境变量。
86
87
 
@@ -167,7 +168,7 @@ install_skill ~/.hermes/skills # Hermes
167
168
  | | `qa list` | 投资者问答 QA(互动平台/电话会议/调研纪要,按证券) |
168
169
  | | `report-image list` / `download` | 研报图表搜索(按关键词,含原图 JPEG 下载) |
169
170
  | | `highlight list` | 会议线索(会议核心要点信息流,按时间倒序;5 积分/条) |
170
- | **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配) |
171
+ | **Reference** | `securities-search` | GTS Code 搜索(按名称/代码/拼音匹配;`--category` 可选 stock / dr / index / fund / bond,按返回行的 `category` 取) |
171
172
  | | `chiefs-search` | 首席分析师 ID 搜索(按姓名/机构/团队匹配) |
172
173
  | | `institution-search` | 机构 ID 搜索(内资券商/外资/牵头/观点机构,按名称匹配) |
173
174
  | | `official-account-search` | 公众号 ID 搜索(按公众号名/机构/分类匹配,返回 accountId) |
@@ -230,7 +231,7 @@ install_skill ~/.hermes/skills # Hermes
230
231
  | | `drive-folder-list` | 云盘目录浏览(某文件夹下的直接子文件夹与文件) |
231
232
  | | `drive-upload` / `drive-create-folder` / `drive-rename` | 上传文件、新建文件夹、重命名(均免费) |
232
233
  | | `drive-move-file` / `drive-move-folder` | 移动文件与文件夹(同一空间内) |
233
- | | `drive-copy` | 把文件复制到另一空间(我的云盘 ↔ 租户云盘;目前只支持文件) |
234
+ | | `drive-copy` | 把文件或整个文件夹复制到另一空间(我的云盘 ↔ 租户云盘) |
234
235
  | | `drive-delete-file` / `drive-delete-folder` | 删除文件 / 文件夹(需 `--yes`;删文件夹连同其中内容一起删除,不可恢复) |
235
236
  | | `record-list` / `record-download` | 录音速记列表与下载 |
236
237
  | | `my-conference-list` / `my-conference-download` | 我的会议列表与下载 |
@@ -243,7 +244,7 @@ install_skill ~/.hermes/skills # Hermes
243
244
  | | `time-series` | 指标时间序列(多指标 × 单证券 或 单指标 × 多证券,按区间) |
244
245
  | | `screener` | 条件选股(按指标表达式从证券/板块范围筛股;前置 `search` 拿 code) |
245
246
  | **Alternative** | `edb-search` | 行业指标搜索(按关键词匹配,返回 indicatorId 等元信息) |
246
- | | `edb-data` | 行业指标时序数据(批量拉取,最多10个指标) |
247
+ | | `edb-data` | 行业指标时序数据(批量拉取,最多10个指标;30 积分/指标) |
247
248
  | | `concept-info` | 题材指数基本信息(定义/投资逻辑/行业空间/竞争格局;50 积分/次,`--full` 另含催化事件 500 积分/次) |
248
249
  | | `concept-securities` | 题材指数成分股(按分组;50 积分/次,`--full` 另含重点个股标识与纳入理由 500 积分/次) |
249
250
  | **Tool** | `file-parse` / `file-parse-check` | PDF 解析为 Markdown + 图片(异步,返回 ZIP) |
@@ -304,7 +305,7 @@ gangtise ai knowledge-batch --query 比亚迪 --query 最近热门概念
304
305
  - **流式下载**:二进制响应(PDF 等)直接 `pipeline` 到磁盘,不经过内存缓冲;不带 `--output` 时先写入临时文件,定好文件名后再改名。50MB PDF 内存占用近乎为零。
305
306
  - **流式输出**:`--format jsonl` 或 `csv` 加 `--output <file>` 时,翻页 / 全市场分片 / 多证券分批请求的行**按到达顺序逐批写盘**,取数阶段不持有整份结果,内存不随行数增长;csv 先落临时行文件、收尾时按列并集写表头再转成 csv(磁盘两遍、内存不变)。jsonl / csv 无论写文件还是输出到终端都按批写出,每行以换行结尾。任一只 / 页 / 片失败,命令退出时后台已无取数与写盘,不留 `.part`。
306
307
  - **导出元信息**:`csv` / `jsonl` 落盘时旁边生成 `<文件>.meta.json`——命令行、数据行数、列名、`complete`(写元信息时的判定:退出 3 即 `false`)、`total` / `partial` / `failedPages` / `failedShards` / `truncatedShards` / `droppedColumns` / `missingFields` / `duplicateRows` / `changedRows` / `totalCapped` 等全部完整性标记(以及不影响完整性判定的 `outOfWindowShards`)、抓取时间与时区、CLI 版本,以及数据文件的字节数 `bytes` 与内容哈希 `sha256`。命令行与结果里的 key / secret / token 类字段写成 `[redacted]`。元信息在数据文件发布之后才就位,导出失败不会留下新数据配旧元信息。**转交或归档前建议核一次**:`shasum -a 256 <文件>` 与元信息里的 `sha256` 相等,才说明这份元信息描述的就是旁边这份数据——两个进程同时导出到**同一个 `--output`** 时,最终文件一定是其中某一次的完整产物,但旁边的元信息有可能来自另一次,核哈希就能发现。**输的那一次自己也会报**:收尾时核对 `--output` 是否仍是本次写出的文件(仍是同一个文件就不必回读,否则回读比对哈希),不符就在 stderr 说明「这个文件不是本次命令的产物」并**退出码 4**。这两种格式的文件本身只有数据行,转交之后靠它核验是否完整;`json` 自带标记,不生成。
307
- - **自动重试**: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 下载 + 题材 `concept-info` / `concept-securities`(含 `--full`);`bond` 全部 12 个命令(多数按次,三个评级命令按条 / 按债券 / 按发行人)、`fund` 全部 18 个命令(按次)与按次计费的 `tool web-search`;按条计费的 `insight highlight list`、观点 `detail` 与 `list --with-content`,以及单次最多 6000 只、按只计费的 `ai stock-summary` 与行数随日期区间增长的 `fundamental earning-forecast`;另加不计分但不可重复执行的 `vault stock-pool-create` 与云盘的上传 / 新建文件夹 / 复制 / 两个删除,共 64 个):5xx/超时不重放,仅连接失败、429 与 token 自愈重试。**两类端点各有各的理由**:贵档是**重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的再扣一次,重发按篇/按条计费的会把已交付的行再计一次;不计分的那几个是**重放会造成副作用或把成功报成失败**——股票池名不允许重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`;云盘允许同名,重发上传 / 新建 / 复制会多出一份;重发一个其实已经删掉的删除,回来的是「文件不存在」或 `130002`。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(EDE 无数据不用此码,而是保留行列的占位单元格 `null`;代码或参数名写错报 `100003`、缺必填参数报 `100001`,漏传 `fiscalYear` 例外、不报错)。**终态码 `999011`(凭证无效)/ `140002`(终态失败:异步生成失败、参数错误或业务处理失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,`140002` 立即重发得到的还是同一结果。
308
+ - **自动重试**:5xx / 429 / `ECONNREFUSED` / `ECONNRESET` / `ETIMEDOUT` / `ENOTFOUND` / `EAI_AGAIN` / `UND_ERR_*`(undici 连接/超时类)/ `999999` 系统错误自动指数退避重试 2 次。**不重放的端点例外**(贵档:one-pager 等生成/提交类 + `tool file-parse` 提交 + 30/篇 的 summary / foreign-report / my-conference 下载 + 单价未公布但保守同档的 pamirs-summary 下载 + 题材 `concept-info` / `concept-securities`(含 `--full`);`bond` 全部 12 个命令(多数按次,三个评级命令按条 / 按债券 / 按发行人)、`fund` 全部 18 个命令(按次)与按次计费的 `tool web-search`;按条计费的 `insight highlight list`、观点 `detail` 与 `list --with-content`,以及单次最多 6000 只、按只计费的 `ai stock-summary` 与行数随日期区间增长的 `fundamental earning-forecast`;另加不计分但不可重复执行的 `vault stock-pool-create` 与云盘的上传 / 新建文件夹 / 复制 / 两个删除,共 64 个):5xx/超时不重放,仅连接失败、429 与 token 自愈重试。**两类端点各有各的理由**:贵档是**重放会重复扣分**——服务端可能已经执行并计费,重发按次计费的再扣一次,重发按篇/按条计费的会把已交付的行再计一次;不计分的那几个是**重放会造成副作用或把成功报成失败**——股票池名不允许重复,重发一个其实已经建成的请求,回来的是 `230006 股票池名称重复`;云盘允许同名,重发上传 / 新建 / 复制会多出一份;重发一个其实已经删掉的删除,回来的是「文件不存在」或 `130002`。**`indicator`(EDE)端点对 `999999` 不重试**——重放一次已计费的查询没有意义(EDE 无数据不用此码,而是保留行列的占位单元格 `null`;代码或参数名写错报 `100003`、缺必填参数报 `100001`)。**终态码 `999011`(凭证无效)/ `140002`(终态失败:异步生成失败、参数错误或业务处理失败)在任何 HTTP 状态下都不重试**——凭证错不会因重试而变,`140002` 立即重发得到的还是同一结果。
308
309
 
309
310
  <!-- no-replay-endpoints
310
311
  上面那句点名的「不重放」端点,完整清单如下(endpoint key,与 `gangtise raw list` 一致):
@@ -411,7 +412,7 @@ vault.stock-pool.create
411
412
 
412
413
  规则:
413
414
  - **省略 `--size` 一律拉全量**(无论是否传时间范围),CLI 自动翻页查完
414
- - **按条计费的列表有额度保护**:省略 `--size` 时,CLI 先拿到 `total`,按 `total × 单价` 估算全量要花的积分,超过 1000 积分就报错、退出码 1,不再往下拉,报错写明估算值。整页不超过 50 积分的列表用第一页拿 `total`;整页更贵的(路演 / 调研 / 策略会 / 论坛、独立观点、会议线索、个股线索、热点话题、`--with-content`)先只取 1 条:结果最多 1 条时它就是全部结果;否则放行后从头翻页,这 1 条会多计一次费(知道要多少条就直接传 `--size N`,不走这一步)。确认要全量就加 `--yes`,只要一部分就传 `--size N`——显式 `--size` 按 `min(--size, total) × 单价` 估算仍超过 1000 积分时同样报错(`--size 100000` 不能当「全量」绕过),要 `--yes`。`fundamental earning-forecast` 不分页,按日期区间估算(工作日数 × 3 条 × 0.5),同一条线。免费列表不受影响
415
+ - **按条计费的列表有额度保护**:省略 `--size` 时,CLI 先拿到 `total`,按 `total × 单价` 估算全量要花的积分,超过 1000 积分就报错、退出码 1,不再往下拉,报错写明估算值。整页不超过 50 积分的列表用第一页拿 `total`;整页更贵的(路演 / 调研 / 策略会 / 论坛、会议线索、个股线索、热点话题、`--with-content`)先只取 1 条:结果最多 1 条时它就是全部结果;否则放行后从头翻页,这 1 条会多计一次费(知道要多少条就直接传 `--size N`,不走这一步)。确认要全量就加 `--yes`,只要一部分就传 `--size N`——显式 `--size` 按 `min(--size, total) × 单价` 估算仍超过 1000 积分时同样报错(`--size 100000` 不能当「全量」绕过),要 `--yes`。`fundamental earning-forecast` 不分页,按日期区间估算(工作日数 × 3 条 × 0.5),同一条线。免费列表不受影响
415
416
  - 数据量未知时,可先 `--size 1` 从 stderr 的 `Total: N` 探明量级,再决定是否全量
416
417
  - 如果显式传了 `--size`,则按指定值翻页,直到达到 `size` 或数据取完
417
418
  - `--from` 必须是非负整数,`--size` 必须是正整数;非法数字会在本地直接报 `ValidationError`,不会继续请求 API
@@ -433,7 +434,7 @@ vault.stock-pool.create
433
434
 
434
435
  推荐工作流:先 `list` 再 `download`,文件名自动正确且零额外成本。
435
436
 
436
- 🔴 **缓存未命中时不会自动回查 list 接口**。回查要拉最近 200 条记录(4 次请求),而这些 list 多数按 **0.1 积分/条**计费,约 20 积分——这笔开销只用于取一个更易读的文件名(下载本身 10–50 积分),所以默认不回查。需要时加 `--resolve-title`:
437
+ 🔴 **缓存未命中时不会自动回查 list 接口**。回查要拉最近 200 条记录(4 次请求),而这些 list 多数按 **0.1 积分/条**计费,约 20 积分——这笔开销只用于取一个更易读的文件名(下载本身 1–30 积分),所以默认不回查。需要时加 `--resolve-title`:
437
438
 
438
439
  ```bash
439
440
  gangtise insight research download --report-id 432092410345574400 --resolve-title
@@ -530,7 +531,7 @@ gangtise insight summary download --summary-id 4906813 --file-type 2
530
531
  # 财报日历:注意用 --start-date/--end-date(按 publishDate 过滤),不是 --start-time
531
532
  gangtise insight performance-calendar list --start-date 2026-07-01 --end-date 2026-07-25 \
532
533
  --market aShares --category performanceForecast --size 20 --format json
533
- # 下载业绩报告原文(仅 hasAttachment: true 的记录;A股 10 积分 / 港美股 20 积分)
534
+ # 下载业绩报告原文(仅 hasAttachment: true 的记录;A股 1 积分 / 港美股 2 积分)
534
535
  gangtise insight performance-calendar download --performance-report-id 33753017 --output ./业绩预告.pdf
535
536
 
536
537
  # 会议线索:会议核心要点信息流(按时间倒序;5 积分/条,务必带 --size 控量)
@@ -550,6 +551,8 @@ gangtise reference securities-search --keyword "贵州茅台" --category stock
550
551
  gangtise reference securities-search --keyword "600519" --category stock
551
552
  gangtise reference securities-search --keyword gzmt --top 5
552
553
  gangtise reference securities-search --keyword "银行" --category stock --category index
554
+ # 债券:按代码或简称查,代码用于 bond 命令组;按简称查股票时同名可转债也会出现,按返回的 category 取
555
+ gangtise reference securities-search --keyword 甬金转债 --category bond
553
556
 
554
557
  # 首席分析师 ID 搜索(按姓名/机构/团队;拿 chiefId 供 insight opinion list --chief 使用)
555
558
  gangtise reference chiefs-search --keyword 东吴证券 --top 3 --format json
@@ -686,7 +689,7 @@ gangtise bond announcement --security 019742.SH --page-size 50
686
689
  gangtise bond announcement --start-date 2026-09-18 --end-date 2026-09-19 --page-no 2 --page-size 50
687
690
  ```
688
691
 
689
- > **`bond announcement` 需要手动翻页**:它是本系列唯一分页的接口,且响应**不返回 `total`**,因此不走 CLI 的自动翻页——`--page-no` 从 1 开始逐页递增,翻过末页返回空数组(退出码 0)即停;条件下一条都没有时第 1 页就返回 `130001`(退出码 1)。⚠️ 按日期区间查询时返回的 `securityCode` 不带市场后缀,要接着查其他债券命令,先用该行的 `securityName` 走 `reference securities-search` 换回带后缀的代码。其余债券命令一次返回全部匹配行。
692
+ > **`bond announcement` 需要手动翻页**:它是本系列唯一分页的接口,且响应**不返回 `total`**,因此不走 CLI 的自动翻页——`--page-no` 从 1 开始逐页递增,翻过末页返回空数组(退出码 0)即停;条件下一条都没有时第 1 页就返回 `130001`(退出码 1)。按日期区间查询时,部分行的 `securityCode` 为空(不对应已上市证券的公告等),这类行无法接着查其他债券命令。其余债券命令一次返回全部匹配行。
690
693
 
691
694
  > **`bond issuer-info` 的行业有两套**:`swIndustry` 是申万「一级/二级」(如 `食品饮料/白酒Ⅱ`),`nationalIndustry` 是国民经济行业分类。评级列 `latestIssuerRating` 混合了境内与境外口径,做信用比较前一并取 `ratingAgency` 判断。
692
695
 
@@ -775,6 +778,8 @@ gangtise vault drive-move-file --file-id 49412 --file-id 43319 --target-folder-i
775
778
  gangtise vault drive-move-folder --folder-id 103 --target-parent-id root
776
779
  # 跨空间复制文件:个人云盘的文件复制到租户云盘某文件夹(root = 租户云盘根目录)
777
780
  gangtise vault drive-copy --file-id 49412 --file-id 43319 --target-folder-id 201
781
+ # 跨空间复制整个文件夹(连同子文件夹与文件)到租户云盘根目录
782
+ gangtise vault drive-copy --folder-id 103 --target-parent-id root
778
783
  # 删除(不可恢复,需 --yes;删文件夹会连同其中全部子文件夹与文件一起删除)
779
784
  gangtise vault drive-delete-file --file-id 49412 --yes
780
785
  gangtise vault drive-delete-folder --folder-id 103 --yes
@@ -867,7 +872,6 @@ gangtise indicator cross-section --indicator qte_close --security 600519.SH \
867
872
 
868
873
  # 必填参数:缺了会报 100001 并点名,按 parameterList 的 required 补齐再取:
869
874
  # N 期统计补 periodNum、年度/分红/预测类补 fiscalYear;区间类要特定区间时传 sDate(可选,起始日,tradeDate 仍是终点)
870
- # ⚠️ fiscalYear 漏传不报错:按一个服务端自定的默认年度取数(不随 --date 变),该年度有数就返回它、看着正常,没有就是 null,要显式传
871
875
  # finc_roe_avg_avg 是报告期类,日期用 reportDate:
872
876
  gangtise indicator cross-section --indicator finc_roe_avg_avg --security 600519.SH \
873
877
  --date 2026-03-31 --indicator-param "finc_roe_avg_avg:periodNum=4" \
@@ -13,7 +13,7 @@ function requireBondCodes(codes, max) {
13
13
  return unique;
14
14
  }
15
15
  const bondSecurityOption = (command) => command
16
- .option("--security <code>", "Bond code, e.g. 019742.SH / 123456.SZ / 220205.IB (repeatable). Short names and pinyin are rejected — resolve them with 'reference securities-search' first", collectList, [])
16
+ .option("--security <code>", "Bond code, e.g. 019742.SH / 123456.SZ / 220205.IB (repeatable). Short names and pinyin are rejected — resolve them with 'reference securities-search --category bond' first", collectList, [])
17
17
  .option("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100003", collectList, [])
18
18
  .option("--format <format>", "Output format", "table").option("--output <path>");
19
19
  const addBondRange = (name, endpointKey, describe, rangeHelp, max) => bondSecurityOption(bond.command(name).description(describe))
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import { collectNames } from "../core/args.js";
3
3
  import { date, format, list, output, query, requiredList, value } from "./shared.js";
4
4
  export const fund = new Command("fund").description("Fund APIs: profiles, NAV, fees, managers, scale, holders, holdings and allocation, ETF creation/redemption");
5
- const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted)", "fundCodeList");
5
+ const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted). From a fund name: 'reference securities-search --category fund', keeping only rows whose category is fund", "fundCodeList");
6
6
  const range = (what) => [
7
7
  date("--start-date <date>", `${what} range start (yyyy-MM-dd); omit both ends for everything in your account's history window`, "startDate"),
8
8
  date("--end-date <date>", `${what} range end (yyyy-MM-dd)`, "endDate"),
@@ -5,7 +5,7 @@ query(reference, "securities-search", {
5
5
  endpoint: "reference.securities-search",
6
6
  fields: [
7
7
  required("--keyword <text>", "Search keyword (name/code/pinyin/English)", "keyword"),
8
- choiceList("--category <type>", "Category: stock/dr/index/fund", "category", ["stock", "dr", "index", "fund"]),
8
+ choiceList("--category <type>", "Category: stock/dr/index/fund/bond. Convertible bonds can come back under any category — keep the rows whose returned category matches", "category", ["stock", "dr", "index", "fund", "bond"]),
9
9
  top(10),
10
10
  format(), output(),
11
11
  ],
@@ -1,6 +1,7 @@
1
1
  import { Command, Option } from "commander";
2
2
  import { collectList, collectNames } from "../core/args.js";
3
3
  import { uploadDriveFile } from "../core/driveUpload.js";
4
+ import { ValidationError } from "../core/errors.js";
4
5
  import { flagFailedItems } from "../core/normalize.js";
5
6
  import { emit, addDownloadCommand, assertConfirmed, field, value, required, list, numberList, format, output, from, size, startTime, endTime, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
6
7
  export const vault = new Command("vault").description("Vault APIs");
@@ -71,15 +72,32 @@ query(vault, "drive-move-folder", {
71
72
  format("json"), output(),
72
73
  ],
73
74
  });
74
- vault.command("drive-copy").description("Copy files to the OTHER space: my drive <-> tenant drive (free; same-space copies are rejected)")
75
- .requiredOption("--file-id <id>", "Source file ID (repeat or comma-separate)", collectList)
76
- .requiredOption("--target-folder-id <id>", "Destination folder ID in the other space, or 'root' for that space's root")
75
+ vault.command("drive-copy").description("Copy files, or one folder with everything inside, to the OTHER space: my drive <-> tenant drive (free; same-space copies are rejected)")
76
+ .option("--file-id <id>", "Source file ID (repeat or comma-separate); use with --target-folder-id", collectList, [])
77
+ .option("--target-folder-id <id>", "Destination folder ID in the other space, or 'root' for that space's root")
78
+ .option("--folder-id <id>", "Source folder ID; use with --target-parent-id")
79
+ .option("--target-parent-id <id>", "Destination parent folder ID in the other space, or 'root' for that space's root")
77
80
  .option("--format <format>", "Output format", "json").option("--output <path>")
78
- .action((options) => emit(options, async (client) => {
79
- const data = await client.call("vault.drive.copy", { copyType: "file", fileIdList: options.fileId, targetFolderId: options.targetFolderId });
80
- flagFailedItems(data, "vault drive-copy");
81
- return data;
82
- }));
81
+ .action((options) => {
82
+ const byFile = options.fileId.length > 0 || options.targetFolderId !== undefined;
83
+ const byFolder = options.folderId !== undefined || options.targetParentId !== undefined;
84
+ if (byFile && byFolder)
85
+ throw new ValidationError("pass either --file-id + --target-folder-id (copy files) or --folder-id + --target-parent-id (copy a folder), not both");
86
+ if (byFile && (!options.fileId.length || options.targetFolderId === undefined))
87
+ throw new ValidationError("copying files needs both --file-id and --target-folder-id");
88
+ if (byFolder && (options.folderId === undefined || options.targetParentId === undefined))
89
+ throw new ValidationError("copying a folder needs both --folder-id and --target-parent-id");
90
+ if (!byFile && !byFolder)
91
+ throw new ValidationError("pass --file-id + --target-folder-id (copy files) or --folder-id + --target-parent-id (copy a folder)");
92
+ const body = byFile
93
+ ? { copyType: "file", fileIdList: options.fileId, targetFolderId: options.targetFolderId }
94
+ : { copyType: "folder", folderId: options.folderId, targetParentId: options.targetParentId };
95
+ return emit(options, async (client) => {
96
+ const data = await client.call("vault.drive.copy", body);
97
+ flagFailedItems(data, "vault drive-copy");
98
+ return data;
99
+ });
100
+ });
83
101
  vault.command("drive-delete-file").description("Delete drive files — irreversible (free)")
84
102
  .requiredOption("--file-id <id>", "File ID to delete (repeat or comma-separate)", collectList)
85
103
  .option("--yes", "Required: confirm the deletion")
@@ -550,7 +550,7 @@ export class GangtiseClient {
550
550
  }).catch((error) => {
551
551
  const isIndicatorFetch = endpoint.key === 'indicator.cross-section' || endpoint.key === 'indicator.time-series' || endpoint.key === 'indicator.screener';
552
552
  if (isIndicatorFetch && error instanceof ApiError && error.code === '999999') {
553
- throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名会报 100003 并指名)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——无数据返回保留行列的 null 单元格;code 或参数名写错报 100003、缺必填参数报 100001,都会指名(fiscalYear 例外:漏传不报错,返回 null 或默认年度的值)。');
553
+ throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名会报 100003 并指名)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——无数据返回保留行列的 null 单元格;code 或参数名写错报 100003、缺必填参数报 100001,都会指名。');
554
554
  }
555
555
  throw error;
556
556
  });
@@ -661,12 +661,10 @@ export class GangtiseClient {
661
661
  throw new ApiError('Download failed', undefined, response.statusCode, text, retryAfterMs);
662
662
  }
663
663
  const contentDisposition = response.headers['content-disposition'];
664
- const filenameMatch = Array.isArray(contentDisposition)
665
- ? contentDisposition[0]?.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i)
666
- : contentDisposition?.match(/filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i);
664
+ const disposition = Array.isArray(contentDisposition) ? contentDisposition[0] : contentDisposition;
665
+ const raw = disposition?.match(/filename\*=UTF-8''([^;]+)/i)?.[1] ?? disposition?.match(/filename="?([^";]+)"?/i)?.[1];
667
666
  let filename;
668
- if (filenameMatch) {
669
- const raw = filenameMatch[1] || filenameMatch[2];
667
+ if (raw) {
670
668
  try {
671
669
  filename = decodeURIComponent(raw);
672
670
  }
@@ -1,9 +1,11 @@
1
+ import { isIPv4 } from "node:net";
1
2
  import os from "node:os";
2
3
  import path from "node:path";
3
4
  export const DEFAULT_BASE_URL = "https://openapi.gangtise.com";
4
5
  export const DEFAULT_TIMEOUT_MS = 30_000;
5
6
  export const DEFAULT_TOKEN_CACHE_PATH = path.join(os.homedir(), ".config", "gangtise", "token.json");
6
7
  let timeoutWarned = false;
8
+ let plainHttpWarned = false;
7
9
  export function loadConfig() {
8
10
  const rawTimeout = process.env.GANGTISE_TIMEOUT_MS?.trim();
9
11
  const timeoutMs = resolveTimeoutEnv(rawTimeout);
@@ -11,8 +13,10 @@ export function loadConfig() {
11
13
  timeoutWarned = true;
12
14
  process.stderr.write(`[gangtise] warning: GANGTISE_TIMEOUT_MS=${rawTimeout} is not in effect, using ${timeoutMs} ms: expected whole milliseconds from ${MIN_TIMEOUT_MS} to ${MAX_TIMEOUT_MS}\n`);
13
15
  }
16
+ const baseUrl = envValue("GANGTISE_BASE_URL") ?? DEFAULT_BASE_URL;
17
+ warnIfPlainHttp(baseUrl);
14
18
  return {
15
- baseUrl: envValue("GANGTISE_BASE_URL") ?? DEFAULT_BASE_URL,
19
+ baseUrl,
16
20
  timeoutMs,
17
21
  accessKey: envValue("GANGTISE_ACCESS_KEY"),
18
22
  secretKey: envValue("GANGTISE_SECRET_KEY"),
@@ -20,6 +24,24 @@ export function loadConfig() {
20
24
  tokenCachePath: envValue("GANGTISE_TOKEN_CACHE_PATH") ?? DEFAULT_TOKEN_CACHE_PATH,
21
25
  };
22
26
  }
27
+ function warnIfPlainHttp(baseUrl) {
28
+ if (plainHttpWarned)
29
+ return;
30
+ let url;
31
+ try {
32
+ url = new URL(baseUrl);
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ if (url.protocol !== "http:")
38
+ return;
39
+ const host = url.hostname.replace(/^\[|\]$/g, "");
40
+ if (host === "localhost" || host === "::1" || (isIPv4(host) && host.startsWith("127.")))
41
+ return;
42
+ plainHttpWarned = true;
43
+ process.stderr.write(`[gangtise] warning: GANGTISE_BASE_URL uses plain http (${url.host}): the access key, secret and token are sent unencrypted. Use https unless this is a trusted local proxy.\n`);
44
+ }
23
45
  function envValue(name) {
24
46
  const value = process.env[name]?.trim();
25
47
  return value ? value : undefined;
@@ -89,7 +89,7 @@ const ENDPOINT_DEFS = {
89
89
  path: "/application/open-insight/summary/v2/download/file",
90
90
  kind: "download",
91
91
  description: "Download summary file",
92
- billing: { per: "document", price: 50 },
92
+ billing: { per: "document", price: 30 },
93
93
  retry: "no-replay",
94
94
  },
95
95
  "insight.pamirs-summary.list": {
@@ -112,7 +112,7 @@ const ENDPOINT_DEFS = {
112
112
  path: "/application/open-insight/schedule/roadshow/getList",
113
113
  kind: "json",
114
114
  description: "List roadshows",
115
- billing: { per: "row", price: 20 },
115
+ billing: { per: "row", price: 10 },
116
116
  pagination: { enabled: true, maxPageSize: 50 },
117
117
  rowId: "id",
118
118
  rowIdUnverified: true,
@@ -122,7 +122,7 @@ const ENDPOINT_DEFS = {
122
122
  path: "/application/open-insight/schedule/site-visit/getList",
123
123
  kind: "json",
124
124
  description: "List site visits",
125
- billing: { per: "row", price: 20 },
125
+ billing: { per: "row", price: 10 },
126
126
  pagination: { enabled: true, maxPageSize: 50 },
127
127
  rowId: "id",
128
128
  rowIdUnverified: true,
@@ -132,7 +132,7 @@ const ENDPOINT_DEFS = {
132
132
  path: "/application/open-insight/schedule/strategy-meeting/getList",
133
133
  kind: "json",
134
134
  description: "List strategy meetings",
135
- billing: { per: "row", price: 20 },
135
+ billing: { per: "row", price: 10 },
136
136
  pagination: { enabled: true, maxPageSize: 50 },
137
137
  rowId: "id",
138
138
  rowIdUnverified: true,
@@ -142,7 +142,7 @@ const ENDPOINT_DEFS = {
142
142
  path: "/application/open-insight/schedule/forum/getList",
143
143
  kind: "json",
144
144
  description: "List forums",
145
- billing: { per: "row", price: 20 },
145
+ billing: { per: "row", price: 10 },
146
146
  pagination: { enabled: true, maxPageSize: 50 },
147
147
  rowId: "id",
148
148
  rowIdUnverified: true,
@@ -160,8 +160,8 @@ const ENDPOINT_DEFS = {
160
160
  method: "GET",
161
161
  path: "/application/open-insight/schedule/performance-calendar/download/file",
162
162
  kind: "download",
163
- description: "Download an earnings report file (A-share 10 credits, HK/US 20)",
164
- billing: { per: "document", price: 20 },
163
+ description: "Download an earnings report file (A-share 1 credit, HK/US 2)",
164
+ billing: { per: "document", price: 2 },
165
165
  },
166
166
  "insight.research.list": {
167
167
  method: "POST",
@@ -193,7 +193,7 @@ const ENDPOINT_DEFS = {
193
193
  path: "/application/open-insight/foreign-report/download/file",
194
194
  kind: "download",
195
195
  description: "Download foreign report",
196
- billing: { per: "document", price: 50 },
196
+ billing: { per: "document", price: 30 },
197
197
  retry: "no-replay",
198
198
  },
199
199
  "insight.announcement.list": {
@@ -210,7 +210,7 @@ const ENDPOINT_DEFS = {
210
210
  path: "/application/open-insight/announcement/download/file",
211
211
  kind: "download",
212
212
  description: "Download A-share announcement file",
213
- billing: { per: "document", price: 10 },
213
+ billing: { per: "document", price: 1 },
214
214
  },
215
215
  "insight.announcement-hk.list": {
216
216
  method: "POST",
@@ -226,7 +226,7 @@ const ENDPOINT_DEFS = {
226
226
  path: "/application/open-insight/announcement-hk/download/file",
227
227
  kind: "download",
228
228
  description: "Download HK announcement file",
229
- billing: { per: "document", price: 20 },
229
+ billing: { per: "document", price: 2 },
230
230
  },
231
231
  "insight.announcement-us.list": {
232
232
  method: "POST",
@@ -242,7 +242,7 @@ const ENDPOINT_DEFS = {
242
242
  path: "/application/open-insight/announcement-us/download/file",
243
243
  kind: "download",
244
244
  description: "Download US announcement file",
245
- billing: { per: "document", price: 20 },
245
+ billing: { per: "document", price: 2 },
246
246
  },
247
247
  "insight.foreign-opinion.list": {
248
248
  method: "POST",
@@ -276,7 +276,7 @@ const ENDPOINT_DEFS = {
276
276
  path: "/application/open-insight/independent-opinion/getList",
277
277
  kind: "json",
278
278
  description: "List foreign independent analyst opinions",
279
- billing: { per: "row", price: 5 },
279
+ billing: { per: "row", price: 1 },
280
280
  pagination: { enabled: true, maxPageSize: 50 },
281
281
  rowId: "independentOpinionId",
282
282
  rowIdUnverified: true,
@@ -812,7 +812,7 @@ const ENDPOINT_DEFS = {
812
812
  path: "/application/open-ai/stock-summary/getList",
813
813
  kind: "json",
814
814
  description: "Stock highlights (refined research summary per security)",
815
- billing: { per: "row", price: 3, maxUnits: 6000 },
815
+ billing: { per: "row", price: 1, maxUnits: 6000 },
816
816
  retry: "no-replay",
817
817
  timeoutMs: 120_000,
818
818
  },
@@ -843,7 +843,7 @@ const ENDPOINT_DEFS = {
843
843
  path: "/application/open-ai/agent/one-pager",
844
844
  kind: "json",
845
845
  description: "Generate one pager",
846
- billing: { per: "call", price: 50 },
846
+ billing: { per: "call", price: 10 },
847
847
  timeoutMs: 120_000,
848
848
  retry: "no-replay",
849
849
  },
@@ -852,7 +852,7 @@ const ENDPOINT_DEFS = {
852
852
  path: "/application/open-ai/agent/investment-logic",
853
853
  kind: "json",
854
854
  description: "Generate investment logic",
855
- billing: { per: "call", price: 50 },
855
+ billing: { per: "call", price: 10 },
856
856
  timeoutMs: 120_000,
857
857
  retry: "no-replay",
858
858
  },
@@ -861,7 +861,7 @@ const ENDPOINT_DEFS = {
861
861
  path: "/application/open-ai/agent/peer-comparison",
862
862
  kind: "json",
863
863
  description: "Generate peer comparison",
864
- billing: { per: "call", price: 50 },
864
+ billing: { per: "call", price: 10 },
865
865
  timeoutMs: 120_000,
866
866
  retry: "no-replay",
867
867
  },
@@ -870,7 +870,7 @@ const ENDPOINT_DEFS = {
870
870
  path: "/application/open-ai/agent/earnings-review-getid",
871
871
  kind: "json",
872
872
  description: "Get earnings review ID",
873
- billing: { per: "call", price: 50 },
873
+ billing: { per: "call", price: 20 },
874
874
  retry: "no-replay",
875
875
  },
876
876
  "ai.earnings-review.get-content": {
@@ -884,7 +884,7 @@ const ENDPOINT_DEFS = {
884
884
  path: "/application/open-ai/agent/theme-tracking",
885
885
  kind: "json",
886
886
  description: "Get theme tracking daily report",
887
- billing: { per: "call", price: 50 },
887
+ billing: { per: "call", price: 10 },
888
888
  timeoutMs: 120_000,
889
889
  retry: "no-replay",
890
890
  },
@@ -893,7 +893,7 @@ const ENDPOINT_DEFS = {
893
893
  path: "/application/open-ai/agent/research-outline",
894
894
  kind: "json",
895
895
  description: "Get company research outline",
896
- billing: { per: "call", price: 50 },
896
+ billing: { per: "call", price: 10 },
897
897
  timeoutMs: 120_000,
898
898
  retry: "no-replay",
899
899
  },
@@ -902,7 +902,7 @@ const ENDPOINT_DEFS = {
902
902
  path: "/application/open-ai/hot-topic/getList",
903
903
  kind: "json",
904
904
  description: "List hot topic reports",
905
- billing: { per: "row", price: 50 },
905
+ billing: { per: "row", price: 10 },
906
906
  pagination: { enabled: true, maxPageSize: 20 },
907
907
  retry: "no-replay",
908
908
  },
@@ -911,7 +911,7 @@ const ENDPOINT_DEFS = {
911
911
  path: "/application/open-ai/management-discuss/from-announcement",
912
912
  kind: "json",
913
913
  description: "Management discussion from financial reports (half-year/annual)",
914
- billing: { per: "call", price: 10 },
914
+ billing: { per: "call", price: 5 },
915
915
  timeoutMs: 120_000,
916
916
  retry: "no-replay",
917
917
  },
@@ -929,7 +929,7 @@ const ENDPOINT_DEFS = {
929
929
  path: "/application/open-ai/agent/viewpoint-debate-getid",
930
930
  kind: "json",
931
931
  description: "Get viewpoint debate ID",
932
- billing: { per: "call", price: 50 },
932
+ billing: { per: "call", price: 20 },
933
933
  retry: "no-replay",
934
934
  },
935
935
  "ai.viewpoint-debate.get-content": {
@@ -1014,7 +1014,7 @@ const ENDPOINT_DEFS = {
1014
1014
  path: "/application/open-vault/drive/copy",
1015
1015
  kind: "json",
1016
1016
  itemFailures: true,
1017
- description: "Copy drive files to the OTHER space (my drive <-> tenant drive)",
1017
+ description: "Copy drive files or a folder to the OTHER space (my drive <-> tenant drive)",
1018
1018
  retry: "no-replay",
1019
1019
  },
1020
1020
  "vault.record.list": {
@@ -1045,7 +1045,7 @@ const ENDPOINT_DEFS = {
1045
1045
  path: "/application/open-vault/my-conference/download/file",
1046
1046
  kind: "download",
1047
1047
  description: "Download my conference resource",
1048
- billing: { per: "document", price: 50 },
1048
+ billing: { per: "document", price: 30 },
1049
1049
  retry: "no-replay",
1050
1050
  },
1051
1051
  "vault.wechat-message.list": {
@@ -1122,6 +1122,7 @@ const ENDPOINT_DEFS = {
1122
1122
  path: "/application/open-alternative/EDB/getData",
1123
1123
  kind: "json",
1124
1124
  description: "Get industry indicator time-series data by indicator ID list",
1125
+ billing: { per: "indicator", price: 30, maxUnits: 10 },
1125
1126
  },
1126
1127
  "alternative.concept-info": {
1127
1128
  method: "POST",
@@ -7,7 +7,7 @@ function traceSuffix(source) {
7
7
  }
8
8
  export function zipFieldRow(fields, row, source) {
9
9
  if (row.length !== fields.length) {
10
- throw new ValidationError(`响应字段数与 fieldList 不匹配(fieldList ${fields.length} 项、该行返回 ${row.length} 个值)——按位置拍平会把值贴到错误的字段上,已拒绝输出。带 --field 的命令多为传了该接口不存在的字段名(上游只返回有效字段的值、字段名却按请求回显):核对 --field 取值(如 quote realtime 没有 close,最新价是 latestPrice),不确定就不传 --field(返回全量字段最稳)。没有 --field 的命令(如 alternative edb-data)出现此错,是上游响应结构异常,请报障${traceSuffix(source)}。`);
10
+ throw new ValidationError(`响应字段数与 fieldList 不匹配(fieldList ${fields.length} 项、该行返回 ${row.length} 个值)——按位置拍平会把值贴到错误的字段上,已拒绝输出。这是响应结构异常:带 --field 的命令先去掉 --field 重试(返回全量字段最稳);没有 --field 的命令(如 alternative edb-data)或去掉后仍报此错,请报障${traceSuffix(source)}。`);
11
11
  }
12
12
  return fields.reduce((acc, field, index) => {
13
13
  acc[String(field)] = row[index];
@@ -299,9 +299,6 @@ export function csvEscape(value) {
299
299
  return out;
300
300
  }
301
301
  export const LINES_PER_WRITE = 1000;
302
- export function writeLines(stream, lines) {
303
- return lines.length === 0 ? Promise.resolve() : writeLine(stream, lines.join("\n"));
304
- }
305
302
  export function writeLine(stream, line) {
306
303
  return new Promise((resolve, reject) => {
307
304
  const ok = stream.write(line + "\n", (err) => err ? reject(err) : undefined);