wind-information-co-ltd/wind-skills包含需要注意的行为
SKILL DETAIL
wind-mcp-skill
wind-information-co-ltd/wind-skills/wind-mcp-skill
wind-mcp-skill 是万得(Wind)官方提供的金融数据技能,通过本地 CLI 调用 Wind 的 7 个 MCP 服务,为用户提供可靠、可验证的全球金融市场数据。该技能覆盖范围广泛,包括 A 股、港股、美股的选股、行情、K 线、财务、估值、股东与事件,以及基金、ETF、指数、板块、债券、公告、财经新闻、宏观经济、汇率、行业、企业、风控、量化指标和衍生品等数据。 技能内置了严格的路由和调用流程,确保数据准确性和可追溯性。用户可以通过自然语言提出查询需求,技能会自动路由到对应的数据服务,并基于返回结果回答,不添加额外评论。该技能支持批量查询、并发控制,并提供了详细的错误处理和自检机制,确保在复杂场景下也能稳定运行。
安装量 · 1,087查看来源
Installation
npx skills add https://github.com/wind-information-co-ltd/wind-skills --skill wind-mcp-skill
技能文件
SKILL.md
最近同步 · 2026年8月29日
references/analytics.md›
# `analytics_data` 工具契约
仅当专项服务无法覆盖结构化取数时使用;不得替代行情、K 线、Quote 或价格指标。自然语言统一使用 `question`。
- 首次调用保持用户原意,不增加筛选条件。
- 首次失败、空数据或明显不匹配后,才可在同一取数意图内改写或拆分一次。
## 工具契约
### `get_financial_data`
根据自然语言描述执行结构化数据的计算与提取,支持自定义指标组合、跨实体聚合与灵活的数据加工。仅当所需结果不能由任何预定义数据工具直接返回时使用本工具:单一实体的属性数据、指数与板块的预定义加权指标、宏观指标均调用相应数据服务;按条件筛选并返回实体列表使用各服务的筛选工具(本工具返回计算结果而非实体列表)。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询中国A股市场过去一年的平均成交量" | 自然语言计算与取数要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含实体范围、指标或计算逻辑、日期(或时间范围、报告期)等要素。|
references/bond.md›
# `bond_data` 工具契约
只用于债券;本服务没有行情快照、K 线或 Quote 工具。参数名称、类型、必填项、示例与默认值和枚举以本文件各工具的契约为准。
## 工具契约
### `get_bond_basicinfo`
返回指定债券的静态档案数据,包括发行要素(如发行规模、票面利率、期限、起息与到期日期)、信用评级、债券类型等。不含行情与估值数据、发债主体信息。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询国债2601的基本信息,包括发行日期、票面利率和期限" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含债券实体、指标名称、日期等查询要素。|
### `get_bond_issuer_info`
返回指定债券发债主体的档案信息,包括主体基本信息、主体信用评级等。债券属性与主体财务数据请使用相应工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询国债2601发债主体的公司信息,包括注册地址和行业分类" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含债券实体、指标名称、日期等查询要素。|
### `get_bond_market_data`
返回指定债券在指定时间范围内的区间行情与估值数据,频率支持日、周、月、年及自定义区间,包括价格、收益率、估值指标等。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询国债2601的行情数据,包括久期和凸性" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含债券实体、指标名称、日期(或时间范围、频率)等查询要素。|
### `get_bond_financial_data`
返回指定债券发债主体的财务数据,时间维度为报告期。主体档案信息与债券本身属性请使用相应工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询国债2601发债主体2024年营业收入和净利润" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含债券实体、指标名称、日期(或报告期)等查询要素。|
references/economic.md›
# `economic_data` 工具契约
只用于宏观和行业 EDB 指标。自然语言统一使用 `question`;日期统一使用 `beginDate` / `endDate`。
- 按职责分两个工具:找指标 / 确认代码用 `search_economic_indicator`;取具体数值时间序列用 `query_economic_indicator_data`。
- `query_economic_indicator_data` 必须提供完整日期范围(`beginDate` + `endDate`)或 `observation`,两者互斥;只给 `question` 会被后端拒绝。
- 日期字段使用 `beginDate` / `endDate`,格式 `yyyy-MM-dd`。
- `observation` 为数字字符串(近 N 期,如 `10`)。
- 后端将合法日期误报为 observation 格式错误时,视为后端问题:停止自动修正并透传错误。
- 不得把日期范围擅自改成 `observation`。
## 工具契约
### `search_economic_indicator`(找指标 / 确认代码,不取数)
根据自然语言需求,从 Wind EDB 经济数据库中检索并匹配相关经济指标,返回指标的元信息(指标名称、指标代码、频率、单位、来源等),**不返回具体数值数据**。适用于查找可用指标、筛选指标,以及提数前确认指标代码的场景。
输入说明:
`question`:用户的自然语言搜索问句,例如“中国近三年GDP相关指标”“上海CPI有哪些”“有哪些出口相关指标”。
返回结果:`metrics` 数组,每条为扁平的指标元信息对象(`code`、`name`、`unit`、`source`、`magnitude`、`currency`、`updateDate`、`freq`;`%` 类指标可能省略 `magnitude`/`currency`),不含时间序列。
| 参数 | 必填 | 类型 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- |
| `question` | 是 | string | 中国近三年GDP相关指标 | 自然语言搜索问句。仅描述要找的指标,不填时间与换算参数。 |
### `query_economic_indicator_data`(取时间序列数值)
根据自然语言问句**或指标代码**,从 Wind EDB 获取宏观经济指标的时间序列数据。`question` 既可传自然语言(如“提取中国GDP数据”),也可直接传指标代码(如 `M5567876`,多个代码用英文逗号分隔)。时间范围只能通过 `beginDate`/`endDate` 或 `observation` 传入,**不要塞进 `question`**。
调用约束:必须显式提供 `beginDate`+`endDate` 或 `observation`;只给 `question` 后端会返回“observation或者[beginDate、endDate]必须填一个”。
返回结果:`metrics` 数组,每条为 `{ meta, date[], value[] }`——`meta` 为指标元信息(同上 8 字段),`date[]` 与 `value[]` 为等长并行的日期与数值数组。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 中国GDP现价当季值 / `M5567876` | 自然语言问句或指标代码(多个代码用英文逗号分隔)。时间范围通过 `beginDate`/`endDate` 或 `observation` 显式传入,不要写进 `question`。 |
| `beginDate` | 否 | string | — | 2025-01-01 | 数据提取开始日期,格式 `yyyy-MM-dd`。须与 `endDate` 成对出现;与 `observation` 互斥。 |
| `endDate` | 否 | string | — | 2025-12-31 | 数据提取结束日期,格式 `yyyy-MM-dd`。须与 `beginDate` 成对出现;与 `observation` 互斥。 |
| `observation` | 否 | string | — | 10 | 观测期数,近 N 期填数字字符串(如近10期填 `10`)。与 `beginDate`/`endDate` 互斥。 |
> 说明:本工具在本 skill 中只接受 `question` 与时间范围参数(`beginDate`/`endDate`/`observation`);跨口径换算 / 对齐交由 `analytics_data` 处理。
references/financial-docs.md›
# `financial_docs` 工具契约
只用于公告和财经新闻。自然语言检索统一使用 `query`(本域是例外,不同于其它领域的 `question`);其余参数以本文件各工具的契约为准。
## 工具契约
### `get_company_announcements`
检索并返回上市公司公告文本,支持按公司、公告类型、时间范围等条件检索。公告对应的结构化字段(如分红金额、事件日期)需查询相应实体的数据;第三方媒体报道不在本工具范围。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `query` | 是 | string | — | 示例:"查询贵州茅台2024年的分红公告" | 自然语言检索要求,应包含公司实体(可选)、公告类型、日期(或时间范围)等检索要素。|
| `top_k` | 否 | integer | — | 默认:5 | 返回的相关文档或片段的最大数量 |
### `get_financial_news`
检索并返回财经新闻文本,支持按主题、实体、时间范围等条件检索。不包含发行人官方公告与券商研报;新闻中提及的结构化数据(如价格、财务数值)以相应实体的数据返回为准。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `query` | 是 | string | — | 示例:"近一周关于宁德时代固态电池的新闻" | 自然语言检索要求,应包含主题或相关实体、日期(或时间范围)等检索要素。|
| `top_k` | 否 | integer | — | 默认:5 | 返回的相关文档或片段的最大数量 |
references/fund-indicators.md›
## `indexes` 行情指标
仅供 `get_fund_price_indicators` 使用。下列字段全部经过真实调用验证,可直接使用;只选择用户明确请求的字段,逐字复制,多个字段用英文逗号连接。表内没有的字段不得猜测。
### 基础行情与元信息
`最新交易日`、`交易时间`、`中文简称`、`最新成交价`、`前收盘价`、`今日开盘价`、`今日最高价`、`今日最低价`、`最新均价`、`涨跌`、`涨跌幅`、`成交量`、`成交额`、`交易状态`、`上市日期`、`近1分钟成交额`、`近3分钟成交额`、`近5分钟成交额`、`近7日平均成交额`、`换手率`、`量比`、`振幅`、`基于Wind算法的量比`
### 盘口与逐笔
`现量`、`现额`、`买一价`、`买二价`、`买三价`、`买四价`、`买五价`、`卖一价`、`卖二价`、`卖三价`、`卖四价`、`卖五价`、`买一量`、`买二量`、`买三量`、`买四量`、`买五量`、`卖一量`、`卖二量`、`卖三量`、`卖四量`、`卖五量`、`外盘`、`内盘`、`成交笔数`、`委比`
### 资金流向
`当日主力净流入额`、`当日主力净流入占比`、`近5日主力净流入额`、`近5日主力净流入占比`、`近5日主力净流入天数`、`近10日主力净流入额`、`近10日主力净流入占比`、`近10日主力净流入天数`、`近20日主力净流入额`、`近20日主力净流入占比`、`近20日主力净流入天数`、`近60日主力净流入额`、`近60日主力净流入占比`、`近60日主力净流入天数`、`主力挂单买入`、`主力挂单卖出`、`主力撤单买入`、`主力撤单卖出`
### 盘中异动
`连续上涨天数`、`连红天数`、`火箭发射`、`高台跳水`、`涨停封板`、`跌停封板`、`涨停开板`、`跌停开板`、`涨幅达到3%`、`跌幅达到3%`、`创20日新高`、`创20日新低`
### 盘前盘后
`盘后最新价`、`盘后涨跌幅`
### 技术指标
`指数平滑异同移动平均`、`DIF快线`、`随机指标K值`、`随机指标D值`、`随机指标J值`、`6周期相对强弱指标`、`12周期相对强弱指标`、`抛物线转向指标`、`布林中轨`、`布林上轨`、`布林下轨`、`5周期移动平均`、`10周期移动平均`、`20周期移动平均`、`60周期移动平均`、`120周期移动平均`、`250日均线`、`5日乖离率`、`36日乖离`、`14周期顺势指标`、`26周期能量指标`、`12周期心理线指标`、`近1分钟涨跌幅`、`近3分钟涨跌幅`、`MACD多头金叉信号`、`MACD空头死叉信号`
### 多周期涨跌幅
`5分钟涨跌幅`、`5日涨跌幅`、`10日涨跌幅`、`20日涨跌幅`、`60日涨跌幅`、`120日涨跌幅`、`250日涨跌幅`、`年初至今涨跌幅`、`上市以来涨跌幅`、`近3年涨跌幅`、`近5年涨跌幅`、`近10年涨跌幅`
### 净值与规模
`流通份额`、`最新净值`、`上期净值`、`累计净值`、`最新净值增长率`、`年初以来净值增长率`、`成立以来净值增长率`、`近一周净值增长率`、`近一月净值增长率`、`近一季净值增长率`、`近半年净值增长率`、`近一年净值增长率`、`近两年净值增长率`、`近三年净值增长率`、`近五年净值增长率`、`贴水率`、`基金规模`、`七日年化收益率`、`万份基金收益`、`IOPV`
### 估值与市值
`流通市值`、`涨停价`、`跌停价`
### 使用说明
- `贴水率` 就是场内溢折率:正值为溢价、负值为折价(实测纳指 QDII 为 16.394,沪深300ETF 为 1.141)。
- `七日年化收益率` 和 `万份基金收益` 只对货币基金返回有效值,股票型 / 指数型 ETF 恒为 0.00。
- `IOPV` 仅部分场内基金返回;货币 ETF 不返回该字段。
- 行情价格、`IOPV`、`贴水率` 是判断 ETF 日内交易的一组核心指标,通常配合使用。
references/fund.md›
# `fund_data` 工具契约
只用于基金、ETF、LOF。参数名称、类型、必填项、示例与默认值和枚举以本文件各工具的契约为准。
- `search_funds` 只用于未指定具体产品的基金筛选。
- `indexes` 逐字取自 `references/fund-indicators.md`;用户未指定字段时省略 `indexes` 走默认值,指定字段时才读取该指标集。
- 场外基金代码如 `005827.OF`;ETF/LOF 代码如 `588200.SH`、`159915.SZ`。
## 目录
- [工具契约](#工具契约)
- 行情指标集:`references/fund-indicators.md`(仅 `get_fund_price_indicators` 需要)
## 工具契约
### `get_fund_price_indicators`
返回指定场内交易基金(ETF/LOF)当前时刻的截面状态,为时点数据,包括最新价、今日开高低、涨跌幅、成交量、成交额、换手率等行情指标的最新值。仅返回时点截面,不含过程序列;场外基金的净值查询不在本工具范围。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 一个或多个基金名称或者基金代码,如588200.SH;多个用英文逗号分隔,单次最多 50 个,超过请分批查询。 |
| `indexes` | 否 | string | — | 默认:"最新交易日,交易时间,最新成交价,前收盘价,今日开盘价,今日最高价,今日最低价,成交量" | 指标字段,多个字段用英文逗号分隔;可选值见 `references/fund-indicators.md`,构造前先读取该文件并逐字复制。 |
### `get_fund_kline`
返回指定场内交易基金(ETF/LOF)在给定时间范围内的聚合价格序列(K 线),聚合周期由 period 指定(分钟级至年,默认日 K)。每条记录代表一个周期,包含开盘价、收盘价、最高价、最低价、成交量、成交额、换手率与均价。当日盘中分钟走势建议用分钟级行情工具(缺省即最新交易日);场外基金无场内行情,其价格口径为净值(属业绩与评价数据)。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | Wind代码,格式如 588200.SH 或 159915.SZ,用于标识具体的场内基金(ETF/LOF) |
| `begin_date` | 是 | string | — | — | 开始日期:必须显式填写绝对日期,格式 yyyy-MM-dd,如 2026-03-25。 |
| `end_date` | 是 | string | — | — | 结束日期:必须显式填写绝对日期,格式 yyyy-MM-dd,如 2026-03-25。 |
| `period` | 否 | string | 1min / 5min / 10min / 15min / 30min / 60min / 120min / 240min / 1d / 1w / 1mo / 1y / 1q / 6mo | 默认:"1d" | K 线周期。 |
| `count` | 否 | integer | — | 默认 0 | 在开始/结束日期区间内取数的条数(整数):正数从开始日期往后取 N 条,负数从结束日期往前取 N 条,0 取区间全部;不会超出日期区间。 |
| `aftype` | 否 | string | 0 / 1 / 2 | 默认 0 | 复权类型:0=前复权,1=后复权,2=不复权。前复权更常用 |
| `issusp` | 否 | string | — | 默认 1 | 是否包含停牌数据:0=不包含,1=包含 |
| `afdate` | 否 | string | — | — | 复权基准日期,格式 yyyy-MM-dd,如 2026-03-25。通常不需要指定。 |
### `get_fund_financials`
返回指定基金的财务报表与分红数据,时间维度为报告期,包括:利润指标(如基金利润、份额利润);收入与费用(如利息收入、投资收益、公允价值变动、管理费、托管费);报告期口径的资产净值;分红记录(如分红次数、单位分红、分红总额、分红条款)。不含净值序列与业绩评价数据。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | — | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含基金实体、指标名称、日期(或报告期)等查询要素。|
### `get_fund_holdings`
返回指定基金的投资组合数据,随定期报告披露更新,包含两族。披露持仓族:资产类别构成(如股票、债券、存款占净值比例及期间变动);重仓资产(如重仓股票、重仓债券、FOF 的重仓基金,及占流通股比例、持仓变动等指标);行业配置(如申万、Wind、中信口径)。组合派生特征族:由持仓计算的组合层面指标(如持股集中度、组合估值、基金换手率等)。仅返回基金组合层面的数据,个券的详细属性需以对应代码查询相应实体的数据。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | — | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含基金实体、指标名称、日期(或报告期)等查询要素。|
### `get_fund_company_info`
返回指定基金的管理人(基金管理公司)档案,为以基金为入口的引用实体查询,包括:公司基本信息(如名称、成立日期、注册资本、管理层);基金经理团队(如人数、人均管理产品数、任职年限统计);在管规模(如合计规模及排名、非货币规模、旗下基金数量);公司层面资产配置。注意本工具返回的是管理人公司而非基金产品本身的数据,基金产品属性请使用其他基金属性查询工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | — | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含基金实体、指标名称、日期等查询要素。|
### `get_fund_quote`
返回指定场内交易基金(ETF/LOF)的分钟级价格序列,每条记录代表一分钟,包含价格、均价、成交量与换手率。时间范围由 begin/end 指定(含首尾),未指定默认最新交易日;单日约 240 条,跨日体积按天数放大,长区间建议改用 K 线工具的聚合周期。仅交易日有数据,非交易日返回空结果。场外基金无场内行情,其价格口径为净值(属业绩与评价数据)。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 基金名称或者基金代码,如588200.SH。|
| `begin` | 否 | string | — | — | 开始日期,格式 yyyy-MM-dd,如 2026-03-25;未指定默认最新交易日。不可只传 end 不传 begin。|
| `end` | 否 | string | — | — | 结束日期,格式 yyyy-MM-dd,如 2026-03-25;未指定默认最新交易日(即只传 begin 时返回 begin 至最新交易日的区间)。|
| `count` | 否 | integer | — | 默认 0 | 在 begin/end 区间内取数的条数(整数):正数从 begin 往后取 N 条,负数从 end 往前取 N 条,0 取区间全部;不会超出区间;未指定 begin/end 时按默认最新交易日计。|
### `get_fund_info`
返回指定基金的产品档案,为静态或准静态数据,包括:代码、简称、全称;投资类型与风格;业绩比较基准与风险等级;费率结构(如管理费、托管费、申购赎回费);基金经理(现任与历任,如任职期限、管理规模);生命周期记录(如成立、转型、清盘、更名);运作状态(如申购赎回状态);管理人与托管人;发行信息(如成立日期、发行规模);指数跟踪信息(如跟踪指数、上市日期、封闭运作期)。不含规模与变动、业绩数据与持仓明细。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | — | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含基金实体、指标名称、日期等查询要素。|
### `get_fund_holders`
返回指定基金的份额、规模与持有人结构,包括:最新规模(资产净值、份额总数)及期间变动;持有人结构(如个人与机构持有比例、持有人户数);申购赎回情况(如报告期及单季度份额变动)。不含静态产品档案。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | — | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含基金实体、指标名称、日期(或报告期)等查询要素。|
### `get_fund_performance`
返回指定基金的净值、业绩度量与评价数据,覆盖三族。净值族:单位净值、累计净值与复权净值的序列及净值增长,净值是基金资产的单位估值,为场外基金的唯一价格口径,场内基金亦适用。业绩度量族:收益率(如区间收益率、年化收益率、绝对收益口径)与同类排名(如按多周期回报、按规模);货币基金以万份收益、7 日年化收益率为专项业绩口径。评价族:风险调整指标(如 Alpha、Beta、夏普比率、最大回撤、波动率、跟踪误差);能力归因指标(如选股能力、选时能力);风格分析(如风格箱、风格暴露);基金评级(为第三方评价结果,非净值派生指标);ETF/LOF 专项指标(如折溢价率、IOPV、净流入额)。不含基金财务报表数据。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | — | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含基金实体、指标名称、日期(或时间范围)等查询要素。 |
### `search_funds`
根据自然语言筛选条件从全市场基金反查产品,返回符合条件的基金代码列表。支持按指标数值(业绩、规模、费率、持仓特征等)与产品分类(投资类型、风格、指数跟踪、管理公司等)组合条件,返回的代码可传入各属性查询工具获取具体数据。已指定具体基金时不要调用本工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"筛选股票型基金中近一年收益率超20%的产品" | 自然语言筛选条件,为指标与指标参数(运算条件、阈值)的组合,无需指定实体。 |
行情指标集已独立为 `references/fund-indicators.md`,仅构造 `get_fund_price_indicators` 的 `indexes` 参数时读取。
references/index-indicators.md›
## `indexes` 行情指标
仅供 `get_index_price_indicators` 使用。下列字段全部经过真实调用验证,可直接使用;只选择用户明确请求的字段,逐字复制,多个字段用英文逗号连接。表内没有的字段不得猜测。
### 基础行情与元信息
`最新交易日`、`交易时间`、`中文简称`、`最新成交价`、`前收盘价`、`今日开盘价`、`今日最高价`、`今日最低价`、`最新均价`、`涨跌`、`涨跌幅`、`成交量`、`成交额`、`交易状态`、`近1分钟成交额`、`近3分钟成交额`、`近5分钟成交额`、`换手率`、`量比`、`振幅`、`基于Wind算法的量比`、`上市日期`
### 盘口与逐笔
`现量`、`现额`
### 资金流向
`当日主力净流入额`、`当日主力净流入占比`、`近5日主力净流入额`、`近5日主力净流入占比`、`近5日主力净流入天数`、`近10日主力净流入额`、`近10日主力净流入占比`、`近10日主力净流入天数`、`近20日主力净流入额`、`近20日主力净流入占比`、`近20日主力净流入天数`、`近60日主力净流入额`、`近60日主力净流入占比`、`近60日主力净流入天数`
### 盘中异动
`连续上涨天数`、`连红天数`
### 技术指标
`指数平滑异同移动平均`、`DIF快线`、`随机指标K值`、`随机指标D值`、`随机指标J值`、`6周期相对强弱指标`、`12周期相对强弱指标`、`抛物线转向指标`、`布林中轨`、`布林上轨`、`布林下轨`、`5周期移动平均`、`10周期移动平均`、`20周期移动平均`、`60周期移动平均`、`120周期移动平均`、`250日均线`、`5日乖离率`、`36日乖离`、`14周期顺势指标`、`26周期能量指标`、`12周期心理线指标`、`近1分钟涨跌幅`、`近3分钟涨跌幅`、`MACD多头金叉信号`、`MACD空头死叉信号`
### 多周期涨跌幅
`5分钟涨跌幅`、`5日涨跌幅`、`10日涨跌幅`、`20日涨跌幅`、`60日涨跌幅`、`120日涨跌幅`、`250日涨跌幅`、`年初至今涨跌幅`、`上市以来涨跌幅`、`近3年涨跌幅`、`近5年涨跌幅`、`近10年涨跌幅`、`近20年涨跌幅`
### 估值与市值
`市净率`、`市净率(LF)`、`市盈率(TTM)`、`市盈率(预测)`、`总市值1`、`流通市值`、`52周最高`、`52周最低`
### 成分统计
`上涨家数`、`下跌家数`、`平盘家数`
### 使用说明
- 指数无盘口、无涨跌停、无盘前盘后交易,相关字段一律不可用。
- `总市值1` 和 `流通市值` 为成分股合计口径。
references/index.md›
# `index_data` 工具契约
只用于指数和板块。参数名称、类型、必填项、示例与默认值和枚举以本文件各工具的契约为准。
- `indexes` 逐字取自 `references/index-indicators.md`;用户未指定字段时省略 `indexes` 走默认值,指定字段时才读取该指标集。
- 已确认的标准代码可直接传,例如 `000300.SH`、`HSI.HI`;不得猜测未知后缀。
## 目录
- [工具契约](#工具契约)
- 行情指标集:`references/index-indicators.md`(仅 `get_index_price_indicators` 需要)
## 工具契约
### `get_index_technicals`
返回指定指数基于历史行情序列计算的派生指标,包括相对窗口统计(如区间涨跌幅、均线、波动)与量价资金流向指标(如 MFI)。不含原始点位序列与实时快照。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询沪深300指数的技术指标,包括MACD、RSI和20日涨跌幅" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含指数或板块实体、指标名称、日期等查询要素。|
### `get_index_quote`
返回指定指数的分钟级点位序列,每条记录代表一分钟。时间范围由 begin/end 指定(含首尾),未指定默认最新交易日;单日约 240 条,跨日体积按天数放大,长区间建议改用 K 线工具的聚合周期。仅交易日有数据,非交易日返回空结果。当前时刻截面状态请用行情快照工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 指数名称或者指数代码,如沪深300或000300.SH |
| `begin` | 否 | string | — | — | 开始日期,格式 yyyy-MM-dd,如 2026-03-25;未指定默认最新交易日。不可只传 end 不传 begin。 |
| `end` | 否 | string | — | — | 结束日期,格式 yyyy-MM-dd,如 2026-03-25;未指定默认最新交易日(即只传 begin 时返回 begin 至最新交易日的区间)。 |
| `count` | 否 | integer | — | 默认 0 | 在 begin/end 区间内取数的条数(整数):正数从 begin 往后取 N 条,负数从 end 往前取 N 条,0 取区间全部;不会超出区间;未指定 begin/end 时按默认最新交易日计。 |
### `get_index_kline`
返回指定指数在给定时间范围内的聚合点位序列(K 线),聚合周期由 period 指定(分钟级至年,默认日 K)。每条记录代表一个周期,包含开盘价、收盘价、最高价、最低价等。当日盘中分钟走势建议用分钟级行情工具(缺省即最新交易日);相对窗口统计与技术指标不在本工具范围。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 指数名称或者指数代码,如沪深300或000300.SH |
| `begin_date` | 是 | string | — | — | 开始日期:必须显式填写绝对日期,格式 yyyy-MM-dd,如 2026-03-25。 |
| `end_date` | 是 | string | — | — | 结束日期:必须显式填写绝对日期,格式 yyyy-MM-dd,如 2026-03-26。 |
| `period` | 否 | string | 1min / 5min / 10min / 15min / 30min / 60min / 120min / 240min / 1d / 1w / 1mo / 1y / 1q / 6mo | 默认:"1d" | K 线周期。 |
| `count` | 否 | integer | — | 默认 0 | 在开始/结束日期区间内取数的条数(整数):正数从开始日期往后取 N 条,负数从结束日期往前取 N 条,0 取区间全部;不会超出日期区间。 |
| `aftype` | 否 | string | 0 / 1 / 2 | 默认 0 | 复权类型:0=前复权,1=后复权,2=不复权。前复权更常用 |
| `issusp` | 否 | string | — | 默认 1 | 是否包含停牌数据:0=不包含,1=包含 |
| `afdate` | 否 | string | — | — | 复权基准日期,格式 yyyy-MM-dd,如 2026-03-25。通常不需要指定。 |
### `get_index_fundamentals`
返回指定指数的基本面与估值指标,按成份股加权聚合口径(如加权 PE、PB、股息率)。成份股个股的财务明细需以个股代码查询相应实体的数据;不含非预定义口径的临时聚合计算。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询沪深300指数2024年的基本面数据,包括PE、PB和成份股加权营收" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含指数或板块实体、指标名称、日期等查询要素。|
### `get_index_price_indicators`
返回指定指数当前时刻的截面状态,为时点数据,包括最新点位、今日开高低、涨跌幅、成交量、成交额等行情指标的最新值。仅返回时点截面,不含过程序列。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 一个或多个指数名称或者指数代码,如沪深300或000300.SH;多个用英文逗号分隔,单次最多 50 个,超过请分批查询。 |
| `indexes` | 否 | string | — | 默认:"最新交易日,交易时间,最新成交价,前收盘价,今日开盘价,今日最高价,今日最低价,成交量" | 指标字段,多个字段用英文逗号分隔;可选值见 `references/index-indicators.md`,构造前先读取该文件并逐字复制。 |
### `get_index_basicinfo`
返回指定指数的静态概况与关联信息,包括:基本信息(如全称、发布机构、基日与基点);集合属性(如指数风格、成份数量);关联信息(如全收益指数、净收益指数代码,被跟踪的基金列表与数量)。仅返回档案、数量与关联信息,不含成份明细与权重、行情数据。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询沪深300指数的基本信息,包括发布机构、基日和成份股数量" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含指数或板块实体、指标名称、日期等查询要素。 |
行情指标集已独立为 `references/index-indicators.md`,仅构造 `get_index_price_indicators` 的 `indexes` 参数时读取。
references/stock-indicators.md›
## `indexes` 行情指标
仅供 `get_stock_price_indicators` 使用。下列字段全部经过真实调用验证,可直接使用;只选择用户明确请求的字段,逐字复制,多个字段用英文逗号连接。表内没有的字段不得猜测。
### 基础行情与元信息
`最新交易日`、`交易时间`、`中文简称`、`最新成交价`、`前收盘价`、`今日开盘价`、`今日最高价`、`今日最低价`、`最新均价`、`涨跌`、`涨跌幅`、`成交量`、`成交额`、`交易状态`、`上市日期`、`近1分钟成交额`、`近3分钟成交额`、`近5分钟成交额`、`K线实体涨跌幅`、`K线实体涨跌额`、`换手率`、`量比`、`振幅`、`基于Wind算法的量比`
### 盘口与逐笔
`现量`、`现额`、`买一价`、`买二价`、`买三价`、`买四价`、`买五价`、`卖一价`、`卖二价`、`卖三价`、`卖四价`、`卖五价`、`买一量`、`买二量`、`买三量`、`买四量`、`买五量`、`卖一量`、`卖二量`、`卖三量`、`卖四量`、`卖五量`、`外盘`、`内盘`、`成交笔数`、`委比`
### 资金流向
`当日主力净流入额`、`当日主力净流入占比`、`近5日主力净流入额`、`近5日主力净流入占比`、`近5日主力净流入天数`、`近10日主力净流入额`、`近10日主力净流入占比`、`近10日主力净流入天数`、`近20日主力净流入额`、`近20日主力净流入占比`、`近20日主力净流入天数`、`近60日主力净流入额`、`近60日主力净流入占比`、`近60日主力净流入天数`、`主力挂单买入`、`主力挂单卖出`、`主力撤单买入`、`主力撤单卖出`、`该日机构资金净流入额`、`该日大户资金净流入额`、`该日中户资金净流入额`、`该日散户资金净流入额`
### 盘中异动
`连红天数`、`连续上涨天数`、`火箭发射`、`高台跳水`、`涨停封板`、`跌停封板`、`涨停开板`、`跌停开板`、`涨幅达到3%`、`跌幅达到3%`、`创20日新高`、`创20日新低`
### 盘前盘后
`盘前最新价`、`盘前涨跌额`、`盘前涨跌幅`、`盘前成交额`、`盘前涨速`、`盘后最新价`、`盘后涨跌幅`、`集合竞价涨跌幅`
### 技术指标
`指数平滑异同移动平均`、`DIF快线`、`随机指标K值`、`随机指标D值`、`随机指标J值`、`6周期相对强弱指标`、`12周期相对强弱指标`、`抛物线转向指标`、`布林中轨`、`布林上轨`、`布林下轨`、`5周期移动平均`、`10周期移动平均`、`20周期移动平均`、`60周期移动平均`、`120周期移动平均`、`250日均线`、`5日乖离率`、`36日乖离`、`14周期顺势指标`、`26周期能量指标`、`12周期心理线指标`、`近1分钟涨跌幅`、`近3分钟涨跌幅`、`MACD多头金叉信号`、`MACD空头死叉信号`
### 多周期涨跌幅
`5分钟涨跌幅`、`5日涨跌幅`、`10日涨跌幅`、`20日涨跌幅`、`60日涨跌幅`、`120日涨跌幅`、`250日涨跌幅`、`年初至今涨跌幅`、`上市以来涨跌幅`、`近3年涨跌幅`、`近5年涨跌幅`、`近10年涨跌幅`、`近20年涨跌幅`
### 净值与规模
`流通份额`
### 估值与市值
`发行价`、`总股本`、`市净率`、`市净率(LF)`、`市盈率(TTM)`、`市盈率(LYR)`、`市盈率(预测)`、`总市值1`、`流通市值`、`总市值2`、`52周最高`、`52周最低`、`股息率`、`涨停价`、`跌停价`
### 成分统计
`成分股贡献点数`、`近5分钟贡献度`
### 使用说明
- `总市值1` 不含限售股,`总市值2` 含限售股;用户只说「总市值」时先确认口径。
references/stock.md›
# `stock_data` 工具契约
只用于股票:A 股、港股、美股共用本服务。参数名称、类型、必填项、示例与默认值和枚举以本文件各工具的契约为准。
- `search_stocks` 只用于未指定具体股票的筛选;已指定具体股票时使用对应行情或领域工具。
- 行情、K 线、分钟行情和价格指标不得改用 `analytics_data` 节省调用次数。
- `indexes` 逐字取自 `references/stock-indicators.md`;用户未指定字段时省略 `indexes` 走默认值,指定字段时才读取该指标集。
- 市值口径:`总市值1`=不含限售股,`总市值2`=含限售股;口径不明确时先询问。
## 目录
- [工具契约](#工具契约)
- 行情指标集:`references/stock-indicators.md`(仅 `get_stock_price_indicators` 需要)
## 工具契约
### `get_stock_price_indicators`
返回指定股票当前时刻的截面状态,为时点数据,包括:最新成交价、前收盘价、今日开盘价、最高价、最低价、涨跌额与涨跌幅;成交量、成交额、换手率、量比、振幅、委比、买卖一档价量;涨停价、跌停价、总市值、流通市值。另含别名指标 PE(TTM)、PB、股息率及 5 日、20 日、60 日与年初至今涨跌幅,同名指标取值与相应主域工具一致。仅返回时点截面,不含过程序列(当日走势与指定时间范围)。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 一个或多个股票名称或者股票代码,如贵州茅台或600519.SH;多个用英文逗号分隔,单次最多 50 个,超过请分批查询。 |
| `indexes` | 否 | string | — | 默认:"最新交易日,交易时间,最新成交价,前收盘价,今日开盘价,今日最高价,今日最低价,成交量" | 指标字段,多个字段用英文逗号分隔;可选值见 `references/stock-indicators.md`,构造前先读取该文件并逐字复制。 |
### `get_risk_metrics`
返回指定股票基于历史价格序列计算的定量风险指标(金融工程口径),覆盖系统性风险(如 Beta、基准相关系数)、收益质量(如 Jensen Alpha、夏普比率)、波动与损失(如年化波动率、VaR、最大回撤、区间下跌天数)等类别。不含财务安全性比率(如资产负债率、速动比率)与择时信号。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询宁德时代(300750.SZ)过去1年的Beta、年化波动率和最大回撤" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含证券实体、指标名称、日期(或窗口参数)等查询要素。|
### `get_stock_events`
返回指定股票的公司行动与事件数据,为结构化记录而非公告文本。事件类型覆盖:首发上市与再融资(配股、增发、并购重组);分红派息;股本与股东事件(大股东增减持、限售解禁);治理与监管事件(风险警示与 ST 变动、违规处罚、司法诉讼)。仅返回结构化字段,不含公告与新闻文本原文,不含公司当前状态标签。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询贵州茅台(600519.SH)的分红派息历史" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含证券实体、事件类型、日期(或时间范围)等查询要素。|
### `get_stock_kline`
返回指定股票在给定时间范围内的聚合量价序列(K 线),聚合周期由 period 指定(分钟级至年,默认日 K)。每条记录代表一个周期,包含开盘价、收盘价、最高价、最低价、成交量、成交额、均价、换手率。当日盘中分钟走势建议用分钟级行情工具(缺省即最新交易日);锚定当前的相对窗口统计(近 N 日涨跌幅、年初至今涨跌幅、52 周高低)与基准指数对比不在本工具范围。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | Wind代码,格式如 贵州茅台 或 600519.SH,用于标识具体的股票 |
| `begin_date` | 是 | string | — | — | 开始日期:必须显式填写绝对日期,格式 yyyy-MM-dd,如 2026-03-25。 |
| `end_date` | 是 | string | — | — | 结束日期:必须显式填写绝对日期,格式 yyyy-MM-dd,如 2026-03-26。 |
| `period` | 否 | string | 1min / 5min / 10min / 15min / 30min / 60min / 120min / 240min / 1d / 1w / 1mo / 1y / 1q / 6mo | 默认:"1d" | K 线周期。 |
| `count` | 否 | integer | — | 默认 0 | 在开始/结束日期区间内取数的条数(整数):正数从开始日期往后取 N 条,负数从结束日期往前取 N 条,0 取区间全部;不会超出日期区间。 |
| `aftype` | 否 | string | 0 / 1 / 2 | 默认 0 | 复权类型:0=前复权,1=后复权,2=不复权。前复权更常用 |
| `issusp` | 否 | string | — | 默认 1 | 是否包含停牌数据:0=不包含,1=包含 |
| `afdate` | 否 | string | — | — | 复权基准日期,格式 yyyy-MM-dd,如 2026-03-25。通常不需要指定。 |
### `get_stock_basicinfo`
返回指定股票的公司身份与分类档案,为静态数据,包括:简称、代码、曾用名、上市板块与上市日期、行业分类、概念标签、指数成份归属、工商注册信息、主营业务简介,以及当前状态标签(是否 ST、是否已摘牌)。仅返回当前状态,不含状态变动历史、财务数据与股东明细。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询贵州茅台股票的基本档案" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含证券实体、指标名称、日期等查询要素。 |
### `get_stock_equity_holders`
返回指定股票的股本结构与股东构成,为准静态数据,随报告期与权益变动更新,包括:总股本、流通 A 股、限售股、自由流通股本及占比;前十大股东与前十大流通股东持仓及变动;机构股东持股;实际控制人与大股东详情;限售解禁时间表与本期解禁数量。不含市值(由价格与股本派生)。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询贵州茅台(600519.SH)的前十大股东及流通A股占比" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含证券实体、指标名称、日期(或报告期)等查询要素。|
### `get_stock_fundamentals`
返回指定股票的两类基本面数据。第一类为财务原始指标:三大报表科目,偿债、盈利、成长、杠杆等财务比率(如 ROE、毛利率、资产负债率、速动比率),以及行业专项指标(如银行净息差、不良贷款率),时间维度为报告期。第二类为衍生估值指标:PE、PB、PS(TTM 口径)、股息率、总市值与流通市值及其历史分位数,随交易日变动。不含价格序列。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询贵州茅台(600519.SH)2024-12-31的ROE、营业收入和净利润" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含证券实体、指标名称、日期(或报告期)等查询要素。 |
### `get_stock_quote`
返回指定股票的分钟级量价序列,每条记录代表一分钟,包含开盘价、最新成交价、最高价、最低价、成交量、成交额、均价与换手率。时间范围由 begin/end 指定(含首尾),未指定默认最新交易日;单日约 240 条,跨日体积按天数放大,长区间建议改用 K 线工具的聚合周期。仅交易日有数据,非交易日返回空结果。当前时刻截面状态请用行情快照工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `windcode` | 是 | string | — | — | 股票名称或者股票代码,如贵州茅台或600519.SH。 |
| `begin` | 否 | string | — | — | 开始日期,格式 yyyy-MM-dd,如 2026-03-25;未指定默认最新交易日。不可只传 end 不传 begin。 |
| `end` | 否 | string | — | — | 结束日期,格式 yyyy-MM-dd,如 2026-03-25;未指定默认最新交易日(即只传 begin 时返回 begin 至最新交易日的区间)。 |
| `count` | 否 | integer | — | 默认 0 | 在 begin/end 区间内取数的条数(整数):正数从 begin 往后取 N 条,负数从 end 往前取 N 条,0 取区间全部;不会超出区间;未指定 begin/end 时按默认最新交易日计。 |
### `get_stock_technicals`
返回指定股票由行情数据计算的派生指标,日频为主,覆盖三族。相对窗口统计:近 N 日、周、月、年及年初至今涨跌幅,52 周最高最低价,相对基准指数表现,均锚定当前滚动。技术指标:趋势类、成交量类、超买超卖类、波动类(如 MACD、KDJ、RSI、BOLL 等)。技术形态:连续涨跌、创新高或新低检测、突破反转、涨跌停与连板状态等。不含原始行情序列、实时快照与风险统计量。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"查询贵州茅台(600519.SH)最近20日的收盘价与MACD" | 自然语言查询要求,为"实体 + 指标 + 指标参数"三要素的组合超集,应包含证券实体、指标名称、日期等查询要素。|
### `search_stocks`
根据自然语言筛选条件从 A 股及海外股票市场反查股票,返回符合条件的股票代码列表。支持按指标数值(行情、财务、估值、资金、技术等)与业务分类(行业板块、主题概念、主营业务)组合条件,返回的代码可传入各属性查询工具获取具体数据。已指定具体股票时不要调用本工具。
| 参数 | 必填 | 类型 | 枚举 | 示例 / 默认 | 官方说明 |
| --- | --- | --- | --- | --- | --- |
| `question` | 是 | string | — | 示例:"筛选沪深市场市值超500亿且连续5日上涨的股票" | 自然语言筛选条件,为指标与指标参数(运算条件、阈值)的组合,无需指定实体。 |
行情指标集已独立为 `references/stock-indicators.md`,仅构造 `get_stock_price_indicators` 的 `indexes` 参数时读取。
scripts/call-rules.json›
{
"schema_version": 18,
"description": "Runtime normalization and validation rules for Wind MCP tool calls.",
"kline_period_map": {
"1min": "1",
"5min": "3",
"10min": "4",
"15min": "5",
"30min": "6",
"60min": "7",
"120min": "8",
"240min": "9",
"1d": "10",
"1w": "11",
"1mo": "12",
"1y": "13",
"1q": "14",
"6mo": "15"
},
"tool_by_domain": {
"price": {
"stock_data": "get_stock_price_indicators",
"fund_data": "get_fund_price_indicators",
"index_data": "get_index_price_indicators"
},
"kline": {
"stock_data": "get_stock_kline",
"fund_data": "get_fund_kline",
"index_data": "get_index_kline"
},
"quote": {
"stock_data": "get_stock_quote",
"fund_data": "get_fund_quote",
"index_data": "get_index_quote"
}
},
"basic": {
"string_keys": [
"question",
"query",
"windcode",
"indexes",
"observation",
"begin_date",
"end_date",
"begin",
"end",
"beginDate",
"endDate",
"date",
"tradeDate",
"afdate"
]
},
"tool_rules": [
{
"name": "question_required",
"label": "自然语言",
"tools": [
"search_stocks",
"get_stock_basicinfo",
"get_stock_fundamentals",
"get_stock_equity_holders",
"get_stock_events",
"get_stock_technicals",
"get_risk_metrics",
"search_funds",
"get_fund_info",
"get_fund_financials",
"get_fund_holdings",
"get_fund_performance",
"get_fund_holders",
"get_fund_company_info",
"get_index_basicinfo",
"get_index_fundamentals",
"get_index_technicals",
"get_bond_basicinfo",
"get_bond_issuer_info",
"get_bond_market_data",
"get_bond_financial_data",
"get_financial_data"
],
"required": ["question"]
},
{
"name": "financial_docs_query_required",
"label": "金融文档",
"tools": ["get_company_announcements", "get_financial_news"],
"required": ["query"]
},
{
"name": "kline",
"label": "K 线",
"tools": ["get_stock_kline", "get_fund_kline", "get_index_kline"],
"required": ["windcode", "begin_date", "end_date"],
"ordered_dates": [["begin_date", "end_date"]],
"enum_fields": {
"period": {
"values_from": "kline_period_map",
"message": "字段 'period' 只能是 ${values},日 K 请传 '1d'"
},
"aftype": {
"values": ["0", "1", "2"],
"message": "字段 'aftype' 只能是 '0'(前复权)、'1'(后复权)或 '2'(不复权)"
},
"issusp": {
"values": ["0", "1"],
"message": "字段 'issusp' 只能是 '0' 或 '1'"
}
},
"patterns": {
"count": {
"pattern": "^-?\\d+$",
"message": "字段 'count' 只能是整数:正数从开始日期往后取 N 条,负数从结束日期往前取 N 条,0 取区间全部"
}
}
},
{
"name": "quote",
"label": "分钟行情",
"tools": ["get_stock_quote", "get_fund_quote", "get_index_quote"],
"required": ["windcode"],
"ordered_dates": [["begin", "end"]],
"patterns": {
"count": {
"pattern": "^-?\\d+$",
"message": "字段 'count' 只能是整数:正数从 begin 往后取 N 条,负数从 end 往前取 N 条,0 取区间全部"
}
}
},
{
"name": "economic_search",
"label": "宏观 EDB 搜索指标",
"tools": ["search_economic_indicator"],
"allowed": ["question"],
"required": ["question"]
},
{
"name": "economic_query",
"label": "宏观 EDB 取数",
"tools": ["query_economic_indicator_data"],
"allowed": ["question", "beginDate", "endDate", "observation"],
"required": ["question"],
"paired": [["beginDate", "endDate"]],
"mutually_exclusive": [["observation", "beginDate"], ["observation", "endDate"]],
"ordered_dates": [["beginDate", "endDate"]],
"patterns": {
"observation": {
"pattern": "^\\d+$",
"message": "字段 'observation' 只能是数字字符串(近 N 期)"
}
},
"required_one_of": [
{
"one_of": [["observation"], ["beginDate", "endDate"]],
"message": "query_economic_indicator_data 必须显式提供 observation 或 beginDate/endDate(不能只给 question)"
}
]
}
]
}
scripts/cli.mjs›
#!/usr/bin/env node
// wind-mcp-skill CLI: thin JSON-envelope wrapper around Wind MCP servers
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname, basename, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { spawn } from 'node:child_process';
// #region 静态:版本、7 个 MCP 地址、路径、HTTP 状态码映射。只含常量,不发网络。
const SKILL_VERSION = '2.0.4';
// 本地 registry: 工具选择可在任何网络调用前失败
const SERVERS = {
stock_data: {
endpoint: 'https://mcp.wind.com.cn/vserver_stock_data/mcp/',
label: 'Wind 股票(选股筛选 + 档案/财务/股本/事件/技术/风险 + 行情/K线/分钟)',
},
fund_data: {
endpoint: 'https://mcp.wind.com.cn/vserver_fund_data/mcp/',
label: 'Wind 基金(基金筛选 + 档案/财务/持仓/业绩/持有人/公司 + 行情/K线/分钟)',
},
index_data: {
endpoint: 'https://mcp.wind.com.cn/vserver_index_data/mcp/',
label: 'Wind 指数/板块(档案/基本面/技术 + 行情/K线/分钟)',
},
bond_data: {
endpoint: 'https://mcp.wind.com.cn/vserver_bond_data/mcp/',
label: 'Wind 债券(基本档案/发债主体/行情估值/主体财务)',
},
financial_docs: {
endpoint: 'https://mcp.wind.com.cn/vserver_financial_docs/mcp/',
label: 'Wind 金融文档 RAG(公告 / 新闻)',
},
economic_data: {
endpoint: 'https://mcp.wind.com.cn/vserver_economic_data/mcp/',
label: 'Wind EDB 宏观/行业经济指标',
},
analytics_data: {
endpoint: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
label: 'Wind 通用分析数据(NL → Wind 数据)',
},
};
const PORTAL_URL = 'https://aifinmarket.wind.com.cn/#/user/overview';
const SKILL_DIR = dirname(dirname(fileURLToPath(
import.meta.url)));
const UPDATE_CHECK_PATH = join(SKILL_DIR, 'scripts', 'update-check.mjs');
const TOOL_MANIFEST_PATH = join(SKILL_DIR, 'scripts', 'tool-manifest.json');
const CALL_RULES_PATH = join(SKILL_DIR, 'scripts', 'call-rules.json');
const SKILL_NAME = basename(SKILL_DIR);
const CALL_EXAMPLES = [
`cli.mjs call stock_data search_stocks '{"question":"筛选沪深市场市值超500亿且连续5日上涨的股票"}'`,
`cli.mjs call stock_data search_stocks '{"question":"筛选港股中市值超1000亿港元的科技股"}'`,
`cli.mjs call fund_data search_funds '{"question":"筛选股票型基金中近一年收益率超20%的产品"}'`,
`cli.mjs call stock_data get_stock_basicinfo '{"question":"600519.SH公司基本档案"}'`,
`cli.mjs call stock_data get_stock_price_indicators '{"windcode":"600519.SH","indexes":"中文简称,最新成交价,涨跌幅"}'`,
`cli.mjs call fund_data get_fund_kline '{"windcode":"588200.SH","begin_date":"2026-04-01","end_date":"2026-04-30"}'`,
`cli.mjs call stock_data get_stock_quote '{"windcode":"AAPL.O","begin":"2026-08-05","end":"2026-08-05","count":-30}'`,
`cli.mjs call index_data get_index_kline '{"windcode":"000300.SH","begin_date":"2026-04-01","end_date":"2026-04-30"}'`,
`cli.mjs call financial_docs get_financial_news '{"query":"美联储利率政策","top_k":3}'`,
`cli.mjs call economic_data search_economic_indicator '{"question":"中国GDP相关指标有哪些"}'`,
`cli.mjs call economic_data query_economic_indicator_data '{"question":"中国GDP","observation":"10"}'`,
`cli.mjs call analytics_data get_financial_data '{"question":"查询中国A股市场过去一年的平均成交量"}'`,
];
const PRICE_INDICATOR_TOOLS = new Set(['get_stock_price_indicators', 'get_fund_price_indicators', 'get_index_price_indicators']);
const QUOTE_TOOLS = new Set(['get_stock_quote', 'get_fund_quote', 'get_index_quote']);
const HTTP_ERROR_MAP = {
401: 'AUTH_ERROR',
429: 'RATE_LIMIT_ERROR',
500: 'NETWORK_ERROR',
502: 'NETWORK_ERROR',
503: 'NETWORK_ERROR',
504: 'NETWORK_ERROR',
};
// #endregion 静态
// #region 自动更新:仅 call 成功后触发;今天已成功则跳过;detached 跑 update-check.mjs,不阻塞取数。
function todayKey() {
return new Date().toISOString().slice(0, 10);
}
function normalizePath(value) {
const normalized = resolve(value).replace(/\\/g, '/');
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
}
function updateScope() {
const globalRoot = normalizePath(join(homedir(), '.agents', 'skills'));
const skillDir = normalizePath(SKILL_DIR);
return skillDir.startsWith(globalRoot + '/') ? 'global' : 'project';
}
function updateStateFile() {
return join(SKILL_DIR, 'scripts', 'update-state.json');
}
function readUpdateState() {
try {
const stateFile = updateStateFile();
if (!existsSync(stateFile)) return null;
return JSON.parse(readFileSync(stateFile, 'utf8'));
} catch {
return null;
}
}
function writeUpdateStatePatch(patch) {
const stateFile = updateStateFile();
mkdirSync(dirname(stateFile), { recursive: true });
const state = { ...(readUpdateState() || {}), ...patch };
writeFileSync(stateFile, JSON.stringify(state, null, 2) + '\n');
}
function alreadyUpdatedToday() {
try {
const state = readUpdateState();
return state && state.date === todayKey() && state.status === 'success';
} catch {
return false;
}
}
function markSkillUsed() {
writeUpdateStatePatch({
lastUsedAt: new Date().toISOString(),
lastUsedPid: process.pid,
});
}
function triggerUpdateCheck() {
try {
if (!existsSync(UPDATE_CHECK_PATH)) return;
if (alreadyUpdatedToday()) return;
markSkillUsed();
const tmpDir = join(homedir(), '.cache', 'wind-aifinmarket');
mkdirSync(tmpDir, { recursive: true });
const runnerPath = join(tmpDir, `update-check-${SKILL_NAME}-${process.pid}.mjs`);
copyFileSync(UPDATE_CHECK_PATH, runnerPath);
const child = spawn('node', [runnerPath, SKILL_DIR], { detached: true, stdio: 'ignore', windowsHide: true });
child.on('error', () => { /* ignore spawn failures; update must not block CLI */ });
child.unref();
} catch { }
}
// #endregion 自动更新
// #region 信封:成功写 MCP result + cli_meta;失败统一写 {ok:false,code,message}。Agent 只读 stdout。
function normalizeSuccessPayload(value, path = '$', state = { warnings: [], tables: [], invalidPaths: [] }, dataCell = false) {
if (dataCell && value === 'INVALID') {
state.invalidPaths.push(path);
return null;
}
if (Array.isArray(value)) {
return value.map((item, index) => normalizeSuccessPayload(item, `${path}[${index}]`, state, dataCell));
}
if (!value || typeof value !== 'object') return value;
const normalized = {};
for (const [key, item] of Object.entries(value)) {
const isStructuredDataArray = Array.isArray(item) && (key === 'rows' || key === 'value');
normalized[key] = normalizeSuccessPayload(item, `${path}.${key}`, state, dataCell || isStructuredDataArray);
}
if (Array.isArray(value.rows)) {
state.tables.push({ path, actual_row_count: value.rows.length });
}
if (Object.hasOwn(value, 'excelTotalCount')) {
state.warnings.push({
code: 'UNRELIABLE_DECLARED_COUNT',
path: `${path}.excelTotalCount`,
message: 'excelTotalCount 仅保留为后端原始字段,不得据此判断结果总数或完整性。',
});
}
return normalized;
}
// 保留 MCP result 外层兼容性;只清洗可解析的 JSON 文本并附加机器可读安全元数据。
function normalizeCallSuccess(result, context = {}) {
const output = result && typeof result === 'object' ? structuredClone(result) : result;
const state = { warnings: [], tables: [], invalidPaths: [] };
if (output && Array.isArray(output.content)) {
for (const item of output.content) {
if (item?.type !== 'text' || typeof item.text !== 'string') continue;
try {
const parsed = JSON.parse(item.text);
item.text = JSON.stringify(normalizeSuccessPayload(parsed, '$', state));
} catch {
// 非 JSON 文本按后端原文透传。
}
}
}
if (state.invalidPaths.length) {
state.warnings.push({
code: 'BACKEND_INVALID_AS_NULL',
count: state.invalidPaths.length,
paths: state.invalidPaths.slice(0, 100),
truncated: state.invalidPaths.length > 100,
message: '结构化数据区中的后端字符串 INVALID 已转换为 null;表示缺失或不适用,禁止按 0 参与计算。',
});
}
if (output && typeof output === 'object') {
output.cli_meta = {
schema_version: '1.0',
server_type: context.server_type || null,
tool_name: context.tool_name || null,
completeness: state.warnings.some(warning => warning.code === 'UNRELIABLE_DECLARED_COUNT') ? 'unknown' : 'not_asserted',
tables: state.tables,
warnings: state.warnings,
};
}
return output;
}
function writeRawCallSuccess(result, context = {}) {
process.stdout.write(JSON.stringify(normalizeCallSuccess(result, context), null, 2) + '\n');
}
function writePlainSuccess(data) {
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
}
const DEFAULT_ERROR_MESSAGES = Object.freeze({
AUTH_ERROR: '认证失败,请检查 API Key',
PARAM_TYPE_ERROR: '参数类型错误,请检查字段类型',
USAGE_ERROR: '命令用法错误,请检查输入参数',
PARAMS_FILE_ERROR: '参数文件读取失败,请检查文件路径和内容',
INVALID_PARAMS_JSON: '参数格式错误,params 必须是 JSON 对象',
ROUTE_ERROR: '工具路由失败,请检查 server_type 和 tool_name',
PARAM_VALIDATION_ERROR: '参数校验失败,请检查字段名和取值',
PARAM_CONFLICT_ERROR: '参数存在冲突,请检查输入组合',
RATE_LIMIT_ERROR: '请求过于频繁,请稍后重试',
NETWORK_ERROR: '服务暂时不可用,请稍后重试',
TOOL_RUNTIME_ERROR: '响应解析失败,请稍后重试',
SETUP_ERROR: '本地配置缺失或无效,请检查技能配置',
UNKNOWN: '调用失败,请稍后重试',
});
const MAPPED_ERROR_MESSAGE_CODES = new Set(
Object.keys(DEFAULT_ERROR_MESSAGES).filter((code) => code !== 'UNKNOWN'),
);
function normalizeErrorMessage(code, detail, metadata = {}) {
if (!MAPPED_ERROR_MESSAGE_CODES.has(code)) {
if (typeof metadata.error_message === 'string' && metadata.error_message.trim()) {
return metadata.error_message.trim();
}
if (typeof detail === 'string' && detail.trim()) {
return detail.trim().slice(0, 2000);
}
return DEFAULT_ERROR_MESSAGES.UNKNOWN;
}
if (typeof detail === 'string' && detail.trim()) {
return detail.trim().slice(0, 500);
}
return DEFAULT_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGES.UNKNOWN;
}
function writeErrorEnvelope(code, detail, metadata = {}) {
const envelope = {
ok: false,
code,
message: normalizeErrorMessage(code, detail, metadata),
};
process.stdout.write(JSON.stringify(envelope, null, 2) + '\n');
}
function die(code, detail = null, exitCode = 1, metadata = {}) {
writeErrorEnvelope(code, detail, metadata);
process.exit(exitCode);
}
function exitWithUsage(usage, exitCode = 0) {
die('USAGE_ERROR', `USAGE:\n${usage}`, exitCode);
}
// #endregion 信封
// #region 认证:Key 顺序为 ~/.wind-aifinmarket/config > skill config.json > 环境变量 WIND_API_KEY。
function maskKey(key) {
if (!key || key.length < 8) return '***';
return key.slice(0, 4) + '***' + key.slice(-4);
}
// dotenv 解析: 兼容注释 / 引号 / export 前缀
function parseDotenv(content) {
const env = {};
for (const rawLine of content.split('\n')) {
let line = rawLine.replace(/^\uFEFF/, '').trim();
if (!line || line.startsWith('#')) continue;
if (line.startsWith('export ')) line = line.slice(7).trim();
const eq = line.indexOf('=');
if (eq <= 0) continue;
const key = line.slice(0, eq).trim();
let val = line.slice(eq + 1).trim();
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1);
} else {
const hashIdx = val.indexOf(' #');
if (hashIdx >= 0) val = val.slice(0, hashIdx).trim();
}
env[key] = val;
}
return env;
}
function getApiKey() {
const globalConfig = join(homedir(), '.wind-aifinmarket', 'config');
if (existsSync(globalConfig)) {
try {
const env = parseDotenv(readFileSync(globalConfig, 'utf8'));
const key = env.WIND_API_KEY?.trim();
if (key) return key;
} catch { }
}
const localConfig = join(SKILL_DIR, 'config.json');
if (existsSync(localConfig)) {
try {
const cfg = JSON.parse(readFileSync(localConfig, 'utf8'));
const key = typeof cfg.wind_api_key === 'string' ? cfg.wind_api_key.trim() : '';
if (key) return key;
} catch { }
}
const envKey = process.env.WIND_API_KEY?.trim();
if (envKey) return envKey;
die('AUTH_ERROR', 'WIND_API_KEY 未配置(CLI 已完整检查:用户全局配置 > Skill 本地配置 > 环境变量)');
}
// #endregion 认证
// #region 路由:校验 server_type、tool_name 是否在 SERVERS 与 tool-manifest.json。非法则 ROUTE_ERROR。
function getServer(server_type) {
const server = SERVERS[server_type];
if (!server) {
die('ROUTE_ERROR', `未知 server_type: ${server_type}. 可用: ${Object.keys(SERVERS).join(' / ')}`);
}
return server;
}
function loadToolManifest() {
try {
// tool-manifest.json is the authority for legal server_type + tool_name combinations.
const manifest = JSON.parse(readFileSync(TOOL_MANIFEST_PATH, 'utf8'));
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
throw new Error('manifest 顶层必须是对象');
}
for (const [serverType, tools] of Object.entries(manifest)) {
if (!SERVERS[serverType]) {
throw new Error(`manifest 包含未知 server_type: ${serverType}`);
}
if (!Array.isArray(tools) || tools.some(tool => typeof tool !== 'string' || !tool)) {
throw new Error(`manifest 中 ${serverType} 的工具清单必须是非空字符串数组`);
}
}
for (const serverType of Object.keys(SERVERS)) {
if (!Array.isArray(manifest[serverType])) {
throw new Error(`manifest 缺少 server_type: ${serverType}`);
}
}
return manifest;
} catch (err) {
die('UNKNOWN', `工具清单读取失败: ${err.message}`);
}
}
function validateToolSelection(server_type, toolName) {
getServer(server_type);
const manifest = loadToolManifest();
const tools = manifest[server_type];
if (!tools.includes(toolName)) {
die('ROUTE_ERROR', `工具名 "${toolName}" 不属于 server_type "${server_type}"。`);
}
}
// #endregion 路由
// #region 规则加载:读 call-rules.json,得到 K 线周期映射、按域改写工具名、参数校验规则。
function readCallRules() {
try {
return JSON.parse(readFileSync(CALL_RULES_PATH, 'utf8'));
} catch (err) {
die('UNKNOWN', `调用规则读取失败: ${err.message}`);
}
}
function prepareNormalizationRules(rules) {
return {
klinePeriodMap: new Map(Object.entries(rules.kline_period_map || {})),
toolByDomain: rules.tool_by_domain || {},
};
}
const CALL_RULES = readCallRules();
const NORMALIZATION_RULES = prepareNormalizationRules(CALL_RULES);
const KLINE_PERIOD_MAP = NORMALIZATION_RULES.klinePeriodMap;
const PUBLIC_KLINE_PERIODS = new Set(KLINE_PERIOD_MAP.keys());
const KLINE_PERIODS = new Set(KLINE_PERIOD_MAP.values());
const TOOL_BY_DOMAIN = NORMALIZATION_RULES.toolByDomain;
const TOOL_VALIDATION_RULES = {
basic: CALL_RULES.basic || {},
toolRules: Array.isArray(CALL_RULES.tool_rules) ? CALL_RULES.tool_rules : [],
};
const KLINE_TOOLS = new Set(TOOL_VALIDATION_RULES.toolRules.find(rule => rule.name === 'kline')?.tools || []);
// #endregion 规则加载
// #region 规范化:整理 windcode/indexes/period。不给中文名称猜交易所后缀。
function normalizeIndexes(indexes) {
if (typeof indexes !== 'string') return indexes;
return indexes.split(',').map((item) => item.trim()).filter(Boolean).join(',');
}
function normalizeWindcode(windcode) {
if (typeof windcode !== 'string') return windcode;
const raw = windcode.trim();
const upper = raw.toUpperCase();
// Keep natural-language names untouched. Wind's backend NER is responsible
// for resolving names/aliases; the CLI must not guess exchange suffixes.
if (/[\u4e00-\u9fff]/.test(raw)) return raw;
if (/^0\d{4}\.HK$/.test(upper)) return upper.slice(1);
if (/^\d{4}\.HK$/.test(upper)) return upper;
if (/^\d{6}\.(SH|SZ|BJ|OF)$/.test(upper)) return upper;
if (/^[A-Z]{1,5}\.(O|N|A|HK|SH|SZ|BJ)$/.test(upper)) return upper;
return raw;
}
function toolFamily(toolName) {
if (PRICE_INDICATOR_TOOLS.has(toolName)) return 'price';
if (KLINE_TOOLS.has(toolName)) return 'kline';
if (QUOTE_TOOLS.has(toolName)) return 'quote';
return null;
}
function normalizeCall(server_type, toolName, args) {
const family = toolFamily(toolName);
if (family) toolName = TOOL_BY_DOMAIN[family]?.[server_type] || toolName;
const normalizedArgs = { ...args };
const normalizationErrors = [];
if (typeof normalizedArgs.indexes === 'string') normalizedArgs.indexes = normalizeIndexes(normalizedArgs.indexes);
if (typeof normalizedArgs.windcode === 'string') normalizedArgs.windcode = normalizeWindcode(normalizedArgs.windcode);
// count 是整型字段:把整数字符串收敛成 number,非整数原样留给 patterns 校验拦截。
if (typeof normalizedArgs.count === 'string' && /^-?\d+$/.test(normalizedArgs.count.trim())) {
normalizedArgs.count = Number(normalizedArgs.count.trim());
}
if (KLINE_TOOLS.has(toolName) && normalizedArgs.period === undefined) normalizedArgs.period = '1d';
if (typeof normalizedArgs.period === 'string') {
const key = normalizedArgs.period.trim();
const backendPeriod = KLINE_PERIOD_MAP.get(key);
normalizedArgs.period = backendPeriod || key;
if (!backendPeriod && KLINE_PERIODS.has(key)) {
normalizationErrors.push({
message: `字段 'period' 只能是 ${Array.from(PUBLIC_KLINE_PERIODS).join('/')},日 K 请传 '1d'`,
field: 'period',
issue: 'invalid_enum',
actual: key,
allowed_values: Array.from(PUBLIC_KLINE_PERIODS),
});
}
}
return { server_type, toolName, args: normalizedArgs, normalizationErrors };
}
// #endregion 规范化
// #region 校验:按 call-rules 查必填、枚举、成对/互斥字段、日期顺序。发网络前拦住非法参数。
function validateBasicParams(params) {
const errors = [];
if (!params || typeof params !== 'object' || Array.isArray(params)) {
return [{
code: 'PARAM_TYPE_ERROR',
message: 'params 必须是 JSON object',
field: 'params',
issue: 'invalid_type',
expected_type: 'object',
actual_type: Array.isArray(params) ? 'array' : typeof params,
}];
}
const basic = TOOL_VALIDATION_RULES.basic;
for (const key of basic.string_keys || []) {
if (!(key in params)) continue;
if (typeof params[key] !== 'string') {
errors.push({ message: `字段 '${key}' 必须是字符串`, field: key, issue: 'invalid_type', expected_type: 'string', actual_type: Array.isArray(params[key]) ? 'array' : typeof params[key] });
} else if (params[key].trim().length === 0) {
errors.push({ message: `字段 '${key}' 不能为空或全空白`, field: key, issue: 'empty_value', expected: 'non-empty string' });
}
}
return errors;
}
function hasParamValue(params, key) {
return params[key] !== undefined && params[key] !== null && params[key] !== '';
}
function resolveValidationValues(fieldRule) {
if (Array.isArray(fieldRule.values)) return fieldRule.values.map(String);
if (fieldRule.values_from === 'kline_period_map') return Array.from(KLINE_PERIODS).map(String);
return [];
}
function resolveValidationDisplayValues(fieldRule) {
if (fieldRule.values_from === 'kline_period_map') return Array.from(PUBLIC_KLINE_PERIODS).map(String);
return resolveValidationValues(fieldRule);
}
function renderValidationMessage(template, values) {
return String(template || '').replace('${values}', values.join('/'));
}
function validationErrorMessage(error) {
return typeof error === 'string' ? error : error.message;
}
function validationErrorCode(error) {
return typeof error === 'object' && error?.code ? error.code : null;
}
function validateToolParams(toolName, params) {
const errors = [];
const rules = TOOL_VALIDATION_RULES.toolRules.filter(rule => Array.isArray(rule.tools) && rule.tools.includes(toolName));
for (const rule of rules) {
const ruleLabel = rule.label || rule.name || toolName;
if (Array.isArray(rule.allowed)) {
const allowedKeys = new Set(rule.allowed);
for (const key of Object.keys(params)) {
if (!allowedKeys.has(key)) errors.push({ message: `${ruleLabel} 工具不支持字段 '${key}'`, field: key, issue: 'unknown_field', allowed_fields: [...allowedKeys] });
}
}
for (const key of rule.required || []) {
if (!hasParamValue(params, key)) errors.push({ message: `${ruleLabel} 工具缺少必填字段 '${key}'`, field: key, issue: 'missing_required', required_fields: rule.required || [] });
}
for (const [field, fieldRule] of Object.entries(rule.enum_fields || {})) {
if (!(field in params)) continue;
const values = resolveValidationValues(fieldRule);
if (!values.includes(String(params[field]))) {
const displayValues = resolveValidationDisplayValues(fieldRule);
errors.push({ message: renderValidationMessage(fieldRule.message, displayValues), field, issue: 'invalid_enum', actual: params[field], allowed_values: displayValues });
}
}
for (const fields of rule.paired || []) {
const present = fields.filter(key => hasParamValue(params, key));
if (present.length > 0 && present.length < fields.length) {
errors.push({ message: `字段 '${fields.join("' 和 '")}' 应成对填写`, fields, issue: 'incomplete_pair', expected_fields: fields });
}
}
for (const fields of rule.mutually_exclusive || []) {
const present = fields.filter(key => hasParamValue(params, key));
if (present.length > 1) {
errors.push({ message: `字段 '${fields.join('/')}' 互斥,不应同时填写`, fields, issue: 'mutually_exclusive' });
}
}
for (const [startKey, endKey] of rule.ordered_dates || []) {
if (params[startKey] && params[endKey] && params[startKey] > params[endKey]) {
errors.push({ message: `字段 '${startKey}' 不能晚于 '${endKey}'`, fields: [startKey, endKey], issue: 'invalid_order', expected: `${startKey} <= ${endKey}` });
}
}
for (const [field, patternRule] of Object.entries(rule.patterns || {})) {
if (!(field in params)) continue;
const pattern = new RegExp(patternRule.pattern);
if (!pattern.test(String(params[field]))) {
errors.push({ message: patternRule.message || `字段 '${field}' 格式不合法`, field, issue: 'invalid_format', actual: params[field], expected_pattern: patternRule.pattern });
}
}
for (const conditional of rule.required_one_of_when || []) {
if (!conditional.values?.map(String).includes(String(params[conditional.field]))) continue;
const satisfied = conditional.one_of?.some(group => group.every(key => hasParamValue(params, key)));
if (!satisfied) errors.push({ message: conditional.message || `字段 '${conditional.field}' 当前取值缺少配套参数`, field: conditional.field, issue: 'missing_conditional_fields', one_of: conditional.one_of });
}
for (const requirement of rule.required_one_of || []) {
const satisfied = requirement.one_of?.some(group => group.every(key => hasParamValue(params, key)));
if (!satisfied) errors.push({ message: requirement.message || `${ruleLabel} 工具缺少一组必填字段`, issue: 'missing_one_of', one_of: requirement.one_of });
}
}
return errors;
}
// #endregion 校验
// #region MCP:裸 HTTP JSON-RPC + SSE。先 initialize 再 tools/call。本地/网络错误由 CLI 收口,接口错误统一 backend_error。
function parseSSE(text) {
const trimmed = text.trim();
// 后端正常 SSE, 部分错误场景纯 JSON
if (trimmed.startsWith('{')) {
try {
return JSON.parse(trimmed);
} catch { }
}
const lines = text.split(/\r?\n/);
let last = null;
for (const line of lines) {
if (line.startsWith('data: ')) last = line.slice(6);
}
if (last) {
try {
return JSON.parse(last);
} catch (e) {
throw new Error(`SSE data 行 JSON 解析失败:${e.message}。原文前 200 字符:${text.slice(0, 200)}`);
}
}
throw new Error(`响应格式无法识别(既非 SSE 也非纯 JSON)。原文前 200 字符:${text.slice(0, 200)}`);
}
async function fetchWithRetry(fetchFn, url, optionsOrFactory, {
attempts = 3,
delaysMs = [300, 1000],
onAttemptError = null,
} = {}) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const options = typeof optionsOrFactory === 'function'
? optionsOrFactory(attempt)
: optionsOrFactory;
return await fetchFn(url, options);
} catch (err) {
lastError = err;
onAttemptError?.(err, attempt, attempts);
const delayMs = delaysMs[Math.min(attempt - 1, delaysMs.length - 1)] || 0;
if (attempt < attempts && delayMs > 0) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw lastError;
}
async function mcpRequest(server_type, method, params, {
timeoutMs = 60_000,
} = {}) {
const server = getServer(server_type);
const apiKey = getApiKey();
const headers = {
Authorization: `Bearer ${apiKey}`,
Accept: 'application/json, text/event-stream',
'Content-Type': 'application/json',
};
const body = JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method,
params
});
const dieInterfaceError = (message) => {
die('backend_error', null, 1, {
error_message: String(message ?? '').slice(0, 2000),
});
};
let resp;
try {
resp = await fetchWithRetry(
fetch,
server.endpoint,
() => ({
method: 'POST',
headers,
body,
signal: AbortSignal.timeout(timeoutMs),
}),
{
attempts: 3,
delaysMs: [300, 1000],
onAttemptError: process.env.WIND_DEBUG === '1'
? (err, attempt, total) => {
const causeCode = err?.cause?.code || err?.code || 'UNKNOWN_CAUSE';
process.stderr.write(`[wind-mcp fetch retry ${attempt}/${total}] ${causeCode}: ${err?.message || err}\n`);
}
: null,
},
);
} catch {
die('NETWORK_ERROR');
}
if (!resp.ok) {
await resp.text().catch(() => '');
die(HTTP_ERROR_MAP[resp.status] || 'NETWORK_ERROR');
}
const text = await resp.text();
let payload;
try {
payload = parseSSE(text);
} catch (err) {
die('TOOL_RUNTIME_ERROR', `${err.message} (server=${server_type})`);
}
if (payload.error) {
const msg = typeof payload.error === 'string'
? payload.error
: (payload.error.message || JSON.stringify(payload.error));
dieInterfaceError(msg);
}
if (payload.result?.isError) {
const msg = payload.result.content?.[0]?.text || JSON.stringify(payload.result);
dieInterfaceError(msg);
}
// 部分工具把业务错误包在 content[0].text 的 JSON 字符串里, 必须二次解析
const innerText = payload.result?.content?.[0]?.text;
if (typeof innerText === 'string') {
let inner;
try {
inner = JSON.parse(innerText);
} catch {
inner = null;
}
if (inner) {
if (typeof inner.mcp_tool_error_code === 'number' && inner.mcp_tool_error_code !== 0) {
const msg = inner.mcp_tool_error_msg || JSON.stringify(inner);
dieInterfaceError(msg);
}
if (inner.error && (inner.error.code || inner.error.message)) {
const errorMessage = inner.error.message || JSON.stringify(inner.error);
dieInterfaceError(errorMessage);
}
if (inner?.data && typeof inner.data === 'object') {
const numericCode = typeof inner.data.code === 'number'
? inner.data.code
: (typeof inner.data.code === 'string' && /^\d+$/.test(inner.data.code.trim()) ? Number(inner.data.code) : null);
const isSuccessCode = numericCode === 0
|| (numericCode !== null && numericCode >= 200 && numericCode < 300);
if (numericCode !== null && !isSuccessCode) {
dieInterfaceError(typeof inner.data.message === 'string' ? inner.data.message : JSON.stringify(inner.data));
}
}
}
}
return payload.result;
}
async function mcpInitializeAndCall(server_type, method, params) {
await mcpRequest(server_type, 'initialize', {
protocolVersion: '2025-03-26',
capabilities: {},
clientInfo: {
name: SKILL_NAME,
version: SKILL_VERSION
},
}, {
timeoutMs: 30_000
});
return mcpRequest(server_type, method, params, {
timeoutMs: 600_000,
});
}
// #endregion MCP
// #region 命令:call 取数;list-tools 拉 schema;setup-key / open-portal 配 Key;diagnose 看更新状态。
function loadParamsInput(paramsInput) {
if (!paramsInput.startsWith('@')) {
return { jsonText: paramsInput, source: 'inline' };
}
const fileArg = paramsInput.slice(1);
if (!fileArg) {
const error = new Error('@file 缺少文件路径');
error.code = 'PARAMS_FILE_ERROR';
error.file = fileArg;
throw error;
}
const filePath = resolve(process.cwd(), fileArg);
try {
const jsonText = readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
return { jsonText, source: 'file', filePath };
} catch (cause) {
const error = new Error(`无法读取 params 文件:${filePath} (${cause.code || cause.message})`);
error.code = 'PARAMS_FILE_ERROR';
error.file = filePath;
error.cause = cause;
throw error;
}
}
async function cmdCall(server_type, toolName, paramsInput) {
if (!server_type || !toolName || !paramsInput) {
exitWithUsage(
`用法:call <server_type> <tool_name> '<params_json>|@params_file'\n` +
`可用 server_type: ${Object.keys(SERVERS).join(' / ')}\n` +
`典型:\n ${CALL_EXAMPLES.join('\n ')}`,
1,
);
}
let paramsSource;
try {
paramsSource = loadParamsInput(paramsInput);
} catch (e) {
die('PARAMS_FILE_ERROR', e.message);
}
let args;
try {
args = JSON.parse(paramsSource.jsonText);
} catch (e) {
const sourceDetail = paramsSource.source === 'file'
? `文件:${paramsSource.filePath}`
: `原文:${paramsSource.jsonText.slice(0, 200)}`;
die('INVALID_PARAMS_JSON', `params JSON 解析失败:${e.message} | ${sourceDetail}`);
}
if (!args || typeof args !== 'object' || Array.isArray(args)) {
die('PARAM_TYPE_ERROR', 'params 必须是 JSON object');
}
let normalizationErrors;
({ server_type, toolName, args, normalizationErrors } = normalizeCall(server_type, toolName, args));
validateToolSelection(server_type, toolName);
const validationErrors = [...normalizationErrors, ...validateBasicParams(args)];
const paramsShapeInvalid = validationErrors.some(error => validationErrorCode(error) === 'PARAM_TYPE_ERROR' && error.field === 'params');
if (!paramsShapeInvalid) validationErrors.push(...validateToolParams(toolName, args));
if (validationErrors.length > 0) {
const explicitCode = validationErrors.map(validationErrorCode).find(Boolean);
const messages = validationErrors.map(validationErrorMessage);
const hasTypeError = validationErrors.some(error => typeof error === 'object' && error?.issue === 'invalid_type');
die(explicitCode || (hasTypeError ? 'PARAM_TYPE_ERROR' : 'PARAM_VALIDATION_ERROR'), messages.join(';'));
}
const result = await mcpInitializeAndCall(server_type, 'tools/call', {
name: toolName,
arguments: args,
_meta: { clientVersion: SKILL_VERSION },
});
return {
server_type,
tool: toolName,
result,
};
}
async function cmdListTools(server_type) {
if (!server_type) {
exitWithUsage(
`用法:list-tools <server_type>\n` +
`可用 server_type: ${Object.keys(SERVERS).join(' / ')}`,
1,
);
}
getServer(server_type);
const result = await mcpInitializeAndCall(server_type, 'tools/list', {});
return { server_type, ...result };
}
async function cmdSetupKey(...rawArgs) {
const key = rawArgs[0];
if (!key || key.startsWith('--')) {
exitWithUsage(
`用法:cli.mjs setup-key <KEY> --scope <global|skill>\n\n` +
`scope: global=全局共享;skill=仅当前 skill。调用前先让用户选择。`,
1,
);
}
let scope = null;
for (let i = 1; i < rawArgs.length; i++) {
const a = rawArgs[i];
if (a === '--scope' && rawArgs[i + 1]) {
scope = rawArgs[i + 1];
break;
}
if (a.startsWith('--scope=')) {
scope = a.slice(8);
break;
}
}
if (!scope) {
exitWithUsage(
`setup-key 缺 --scope 参数。\n\n` +
`先让用户选择 global 或 skill,再重试:cli.mjs setup-key ${maskKey(key)} --scope <global|skill>`,
1,
);
}
if (!['global', 'skill'].includes(scope)) {
die('SETUP_ERROR', `setup-key 未知 scope: ${scope} (可选: global / skill)`);
}
let file;
try {
if (scope === 'global') {
const dir = join(homedir(), '.wind-aifinmarket');
if (!existsSync(dir)) mkdirSync(dir, {
recursive: true
});
file = join(dir, 'config');
let lines = [];
if (existsSync(file)) {
lines = readFileSync(file, 'utf8').split('\n')
.filter(l => l.length > 0 && !/^\s*(export\s+)?WIND_API_KEY\s*=/.test(l));
}
lines.push(`WIND_API_KEY=${key}`);
writeFileSync(file, lines.join('\n') + '\n', {
mode: 0o600
});
} else {
file = join(SKILL_DIR, 'config.json');
writeFileSync(file, JSON.stringify({ wind_api_key: key }, null, 2) + '\n', { mode: 0o600 });
}
} catch (err) {
die('SETUP_ERROR', `配置写入失败 (scope=${scope}, path=${file || 'n/a'}): ${err.message}`);
}
return {
scope,
path: file,
key_masked: maskKey(key),
next: '现在可以重试原 Wind 调用',
};
}
async function cmdOpenPortal() {
const platform = process.platform;
let bin, args;
if (platform === 'darwin') {
bin = 'open';
args = [PORTAL_URL];
} else if (platform === 'win32') {
bin = 'cmd';
args = ['/c', 'start', '', PORTAL_URL];
} else {
bin = 'xdg-open';
args = [PORTAL_URL];
}
let spawnError = null;
try {
const child = spawn(bin, args, {
stdio: 'ignore',
detached: true,
windowsHide: true
});
child.unref();
spawnError = await new Promise((resolve) => {
child.once('error', resolve);
setTimeout(() => resolve(null), 300);
});
} catch (err) {
spawnError = err;
}
const data = {
url: PORTAL_URL,
platform,
spawn_command: `${bin} ${args.join(' ')}`,
flow_note: '未登录时会自动跳转到登录页(/#/login);登录完成后回到 overview 页面即可获取 API Key。',
fallback_message: `如果浏览器没有自动弹出,请手动访问:${PORTAL_URL}`,
};
if (spawnError) {
die('SETUP_ERROR', `本地无法启动浏览器: ${spawnError.message} | 用户应手动打开 ${data.url}`);
}
return data;
}
// 诊断: 输出自动更新状态
async function cmdDiagnose() {
let updateState = null;
try {
const stateFile = updateStateFile();
if (existsSync(stateFile)) {
updateState = JSON.parse(readFileSync(stateFile, 'utf8'));
}
} catch {
updateState = { status: 'unreadable' };
}
return {
platform: process.platform,
node_pid: process.pid,
update_scope: updateScope(),
update_state_file: updateStateFile(),
update_state: updateState,
next_update_needed: !alreadyUpdatedToday(),
};
}
// #endregion 命令
// #region 主入口:IS_MAIN 避免测试 import 时跑副作用。无参打 USAGE;仅 call 成功才触发更新检查。
const IS_MAIN = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (IS_MAIN) runMain();
function runMain() {
const [cmd, ...args] = process.argv.slice(2);
const USAGE =
`wind-mcp-skill\n` +
`访问万得 Wind 金融数据(按数据域分类调用)\n\n` +
`用法:\n` +
` cli.mjs call <server_type> <tool_name> '<params_json>|@params_file'\n` +
` cli.mjs list-tools <server_type> # 获取后端官方工具描述和 inputSchema\n` +
` cli.mjs open-portal # 打开万得开发者中心拿 API Key\n` +
` cli.mjs setup-key <KEY> --scope <global|skill> # 配置 API Key(先问用户存放位置)\n\n` +
`可用 server_type:\n` +
Object.entries(SERVERS).map(([k, v]) => ` ${k.padEnd(20)}${v.label}`).join('\n') + '\n\n' +
`典型:\n` +
` ${CALL_EXAMPLES.join('\n ')}`;
const commands = {
call: () => cmdCall(args[0], args[1], args[2]),
'list-tools': () => cmdListTools(args[0]),
'open-portal': () => cmdOpenPortal(),
'setup-key': () => cmdSetupKey(...args),
diagnose: () => cmdDiagnose(),
};
if (!cmd) {
// help: 直接输出 USAGE 纯文本
process.stdout.write(USAGE + '\n');
process.exit(0);
}
if (!commands[cmd]) {
die('USAGE_ERROR', `未知命令: ${cmd}\nUSAGE:\n${USAGE}`);
}
commands[cmd]()
.then((data) => {
if (cmd === 'call') {
// call: 透传 result 内容 (parse JSON if applicable, else raw text)
writeRawCallSuccess(data?.result, { server_type: data?.server_type, tool_name: data?.tool });
setTimeout(triggerUpdateCheck, 0);
} else {
// open-portal / setup-key: 直接输出结构化数据 (无 envelope 包裹)
writePlainSuccess(data);
}
})
.catch((err) => {
die('UNKNOWN', `执行失败: ${err.message || err}${err.stack ? ' | stack: ' + err.stack.slice(0, 300) : ''}`);
});
}
// #endregion 主入口
scripts/tool-manifest.json›
{
"stock_data": [
"get_stock_price_indicators",
"get_risk_metrics",
"get_stock_events",
"get_stock_kline",
"get_stock_basicinfo",
"get_stock_equity_holders",
"get_stock_fundamentals",
"get_stock_quote",
"get_stock_technicals",
"search_stocks"
],
"fund_data": [
"get_fund_price_indicators",
"get_fund_kline",
"get_fund_financials",
"get_fund_holdings",
"get_fund_company_info",
"get_fund_quote",
"get_fund_info",
"get_fund_holders",
"get_fund_performance",
"search_funds"
],
"index_data": [
"get_index_technicals",
"get_index_quote",
"get_index_kline",
"get_index_fundamentals",
"get_index_price_indicators",
"get_index_basicinfo"
],
"bond_data": [
"get_bond_basicinfo",
"get_bond_issuer_info",
"get_bond_market_data",
"get_bond_financial_data"
],
"financial_docs": [
"get_company_announcements",
"get_financial_news"
],
"economic_data": [
"search_economic_indicator",
"query_economic_indicator_data"
],
"analytics_data": [
"get_financial_data"
]
}
scripts/update-check.mjs›
#!/usr/bin/env node
// Daily background updater for wind-mcp-skill.
// The CLI starts this script detached; failures are recorded but never block data calls.
import {
closeSync,
existsSync,
mkdirSync,
openSync,
readFileSync,
readdirSync,
statSync,
unlinkSync,
writeFileSync,
} from 'node:fs';
import {
createHash
} from 'node:crypto';
import {
spawnSync
} from 'node:child_process';
import {
homedir
} from 'node:os';
import {
basename,
dirname,
join,
resolve
} from 'node:path';
import {
fileURLToPath
} from 'node:url';
const SCRIPT_DIR = dirname(fileURLToPath(
import.meta.url));
const SKILL_DIR = process.argv[2] ? resolve(process.argv[2]) : dirname(SCRIPT_DIR);
const SKILL_SCRIPTS_DIR = join(SKILL_DIR, 'scripts');
const LOCK_FILE = join(SKILL_SCRIPTS_DIR, 'update.lock');
const SKILL_NAME = basename(SKILL_DIR);
const DEFAULT_SOURCES = [
'Wind-Information-Co-Ltd/wind-skills',
'[email protected]:wind_info/wind-skills.git',
];
const LOCK_STALE_MS = 30 * 60 * 1000;
const QUIET_MS = 10 * 1000;
const MAX_WAIT_MS = 10 * 60 * 1000;
function normalizePath(value) {
const normalized = resolve(value).replace(/\\/g, '/');
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
}
function updateScope() {
const globalRoot = normalizePath(join(homedir(), '.agents', 'skills'));
const skillDir = normalizePath(SKILL_DIR);
return skillDir.startsWith(`${globalRoot}/`) ? 'global' : 'project';
}
function projectRoot() {
return resolve(SKILL_DIR, '..', '..', '..');
}
function uniquePaths(paths) {
const seen = new Set();
const result = [];
for (const path of paths.filter(Boolean).map((value) => resolve(value))) {
const key = normalizePath(path);
if (seen.has(key)) continue;
seen.add(key);
result.push(path);
}
return result;
}
function updateCommand() {
const command = ['npx', 'skills', 'update', SKILL_NAME, '-y'];
if (updateScope() === 'global') command.push('-g');
return command;
}
function projectLockCandidates() {
const roots = [projectRoot(), process.cwd(), process.env.INIT_CWD];
let current = resolve(SKILL_DIR);
while (true) {
roots.push(current);
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
return uniquePaths(
roots.filter(Boolean).map((root) => join(root, 'skills-lock.json')),
);
}
function globalLockCandidates() {
const xdg = process.env.XDG_STATE_HOME;
return uniquePaths([
xdg ? join(xdg, 'skills', '.skill-lock.json') : null,
join(homedir(), '.agents', '.skill-lock.json'),
]);
}
function lockFileCandidates() {
const globalCandidates = globalLockCandidates();
const projectCandidates = projectLockCandidates();
return updateScope() === 'global' ?
uniquePaths([...globalCandidates, ...projectCandidates]) :
uniquePaths([...projectCandidates, ...globalCandidates]);
}
function readLockInfo() {
const candidates = lockFileCandidates();
let firstExistingFile = null;
for (const file of candidates) {
try {
if (!existsSync(file)) continue;
firstExistingFile ||= file;
const data = JSON.parse(readFileSync(file, 'utf8'));
const entry = data?.skills?.[SKILL_NAME] || null;
if (entry) return {
file,
entry,
candidates
};
} catch {}
}
return {
file: firstExistingFile || candidates[0] || null,
entry: null,
candidates,
};
}
function readLockEntry() {
return readLockInfo().entry;
}
function isGiteeSource(entry) {
const values = [entry?.sourceType, entry?.source, entry?.sourceUrl]
.filter(Boolean)
.map((value) => String(value).toLowerCase());
return values.some((value) => value.includes('gitee'));
}
function sourceUrl(entry) {
if (!entry) return null;
if (entry.sourceUrl) return entry.sourceUrl;
if (entry.sourceType === 'github' && /^[^/\s]+\/[^/\s]+$/.test(entry.source || '')) {
return `https://github.com/${entry.source}.git`;
}
if (
(entry.sourceType === 'gitee' || entry.sourceType === 'git') &&
/^[^/\s]+\/[^/\s]+$/.test(entry.source || '')
) {
return `https://gitee.com/${entry.source}.git`;
}
return entry.source || null;
}
function updateEnv() {
return {
...process.env
};
}
function remoteHead(entry) {
const source = sourceUrl(entry);
if (!source) return null;
try {
const result = spawnSync('git', ['ls-remote', source, 'HEAD'], {
encoding: 'utf8',
env: updateEnv(),
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 60 * 1000,
windowsHide: true,
});
if (result.status !== 0) return null;
const head = (result.stdout || '').trim().split(/\s+/)[0];
return /^[0-9a-f]{40}$/i.test(head) ? head : null;
} catch {
return null;
}
}
function addCommandForSource(source) {
if (!source) return null;
const command = ['npx', 'skills', 'add', source, '--skill', SKILL_NAME, '-y'];
if (updateScope() === 'global') command.push('-g');
return command;
}
function addCommand(entry) {
return addCommandForSource(sourceUrl(entry));
}
function fallbackAddCommands(entry) {
const sources = [sourceUrl(entry), ...DEFAULT_SOURCES];
const seen = new Set();
return sources
.filter(Boolean)
.filter((source) => {
const key = String(source).toLowerCase();
if (seen.has(key)) return false;
seen.add(key);
return true;
})
.map(addCommandForSource)
.filter(Boolean);
}
function commandForUpdate() {
const entry = readLockEntry();
if (isGiteeSource(entry)) {
const command = addCommand(entry);
if (command) {
return {
command,
method: 'add',
sourceType: entry?.sourceType || null,
};
}
}
return {
command: updateCommand(),
method: 'update',
sourceType: entry?.sourceType || null,
};
}
function updateStateFile() {
return join(SKILL_SCRIPTS_DIR, 'update-state.json');
}
function todayKey() {
return new Date().toISOString().slice(0, 10);
}
function readState() {
try {
const stateFile = updateStateFile();
if (!existsSync(stateFile)) return null;
return JSON.parse(readFileSync(stateFile, 'utf8'));
} catch {
return null;
}
}
function alreadyUpdatedToday() {
const state = readState();
if (!state || state.date !== todayKey() || state.status !== 'success') return false;
const entry = readLockEntry();
if (!entry || isGiteeSource(entry)) return true;
const head = remoteHead(entry);
return !head || head === state.lastAppliedRemoteHead;
}
function lastUsedAt() {
try {
const state = readState();
const timestamp = new Date(state?.lastUsedAt).getTime();
return Number.isFinite(timestamp) ? timestamp : 0;
} catch {
return 0;
}
}
function quietLongEnough() {
const last = lastUsedAt();
return last === 0 || Date.now() - last >= QUIET_MS;
}
function sleep(ms) {
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
}
async function waitForQuietWindow() {
const startedAt = Date.now();
while (!quietLongEnough()) {
if (Date.now() - startedAt >= MAX_WAIT_MS) return false;
await sleep(QUIET_MS);
}
return true;
}
function acquireLock() {
try {
if (!existsSync(SKILL_SCRIPTS_DIR)) mkdirSync(SKILL_SCRIPTS_DIR, {
recursive: true
});
try {
const st = statSync(LOCK_FILE);
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) unlinkSync(LOCK_FILE);
} catch {}
return openSync(LOCK_FILE, 'wx');
} catch {
return null;
}
}
function releaseLock(fd) {
try {
if (fd !== null) closeSync(fd);
} catch {}
try {
unlinkSync(LOCK_FILE);
} catch {}
}
function writeState(patch) {
const {
command,
method,
sourceType
} = commandForUpdate();
const lock = readLockInfo();
const stateFile = updateStateFile();
const state = {
date: todayKey(),
scope: updateScope(),
lockFile: lock.file,
lockFound: Boolean(lock.entry),
command: command.join(' '),
method,
sourceType,
updatedAt: new Date().toISOString(),
...patch,
};
mkdirSync(dirname(stateFile), {
recursive: true
});
writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`);
}
function hashSkillDir() {
const hash = createHash('sha256');
const files = [];
function walk(dir) {
for (const entry of readdirSync(dir, {
withFileTypes: true
})) {
const full = join(dir, entry.name);
const rel = full.slice(SKILL_DIR.length + 1).replace(/\\/g, '/');
if (rel === 'config.json' || rel === 'scripts/update-state.json') continue;
if (entry.isDirectory()) {
walk(full);
} else if (entry.isFile()) {
files.push({
full,
rel
});
}
}
}
walk(SKILL_DIR);
files.sort((a, b) => a.rel.localeCompare(b.rel));
for (const file of files) {
hash.update(file.rel);
hash.update('\0');
hash.update(readFileSync(file.full));
hash.update('\0');
}
return hash.digest('hex');
}
function runSkillCommand(command, method) {
const cwd = updateScope() === 'global' ? homedir() : projectRoot();
const isWin = process.platform === 'win32';
const bin = isWin ? 'cmd.exe' : 'npx';
const args = isWin ? ['/d', '/s', '/c', command.join(' ')] : command.slice(1);
const result = spawnSync(bin, args, {
cwd,
encoding: 'utf8',
env: updateEnv(),
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 10 * 60 * 1000,
windowsHide: true,
});
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
const failedByOutput =
/failed to (update|add|install)|No installed skills found matching/i.test(output);
return {
command,
method,
result,
output,
ok: result.status === 0 && !failedByOutput,
error: result.error ?
String(result.error.message || result.error) :
failedByOutput ?
`npx skills ${method} reported failure` :
null,
};
}
function runUpdate() {
const entry = readLockEntry();
const {
command,
method,
sourceType
} = commandForUpdate();
const state = readState();
const beforeRemoteHead = remoteHead(entry);
const remoteChanged = Boolean(beforeRemoteHead && beforeRemoteHead !== state?.lastAppliedRemoteHead);
const beforeHash = hashSkillDir();
let attempt = runSkillCommand(command, method);
let usedFallback = false;
let fallbackReason = null;
if ((!attempt.ok || (attempt.ok && remoteChanged && beforeHash === hashSkillDir())) && method !== 'add') {
const fallbackCommands = fallbackAddCommands(entry);
if (fallbackCommands.length > 0) {
fallbackReason = entry ?
attempt.ok ?
'remote changed but update did not change local files' :
'update failed' :
'lock entry missing or update did not find installed skill';
const outputs = [attempt.output].filter(Boolean);
usedFallback = true;
for (const fallbackCommand of fallbackCommands) {
const fallback = runSkillCommand(fallbackCommand, 'add');
outputs.push(fallback.output);
attempt = {
...fallback,
output: outputs.filter(Boolean).join('\n\n--- fallback: npx skills add ---\n\n'),
error: fallback.ok ? null : fallback.error || attempt.error,
};
if (fallback.ok) break;
}
}
}
const afterHash = hashSkillDir();
writeState({
status: attempt.ok ? 'success' : 'failed',
finishedAt: new Date().toISOString(),
exitCode: attempt.result.status,
method: attempt.method,
usedFallback,
fallbackReason,
sourceType,
command: attempt.command.join(' '),
error: attempt.error,
remoteHead: beforeRemoteHead,
remoteChanged,
lastAppliedRemoteHead: attempt.ok ?
beforeRemoteHead || state?.lastAppliedRemoteHead || null :
state?.lastAppliedRemoteHead || null,
changed: beforeHash !== afterHash,
beforeHash,
afterHash,
output: attempt.output.slice(-2000),
});
}
async function main() {
if (alreadyUpdatedToday()) return;
const fd = acquireLock();
if (fd === null) return;
try {
if (alreadyUpdatedToday()) return;
if (!(await waitForQuietWindow())) {
writeState({
status: 'deferred',
finishedAt: new Date().toISOString(),
exitCode: null,
error: 'skill kept being used; update deferred after max wait',
changed: false,
});
return;
}
writeState({
status: 'updating',
startedAt: new Date().toISOString(),
exitCode: null,
changed: false,
});
runUpdate();
} finally {
releaseLock(fd);
}
}
main().catch((err) => {
try {
writeState({
status: 'failed',
finishedAt: new Date().toISOString(),
exitCode: null,
error: String(err?.message || err),
changed: false,
});
} catch {}
});
SKILL.md›
---
name: wind-mcp-skill
description: >-
用户需要查询、筛选、获取、比较或验证金融市场数据时,优先调用本 Skill 获取可靠、可验证数据,而非仅依赖模型记忆或通用信息来源。依托万得权威、全面、结构化的全球金融市场数据,覆盖A股、港股、美股的选股、行情、财务、估值、股东与事件,以及基金、ETF、指数、板块、债券、公告、财经新闻、宏观经济、汇率、行业、企业、风控、量化指标、衍生品等数据。
author: Wind
homepage: https://aifinmarket.wind.com.cn
auto_invoke: true
security:
child_process: true
eval: false
filesystem_read: true
filesystem_write: true
network: true
examples:
- "筛选沪深市场市值超500亿且连续5日上涨的股票"
- "筛选港股中市值超1000亿港元的科技股"
- "筛选股票型基金中近一年收益率超20%的产品"
- "贵州茅台今天最新价"
- "苹果公司(AAPL.O)最近30日K线"
- "易方达蓝筹精选(005827.OF)最新规模和经理"
- "中证500指数PE/PB历史分位"
- "贵州茅台2024年年度报告内容"
- "中国近10年新能源汽车产销量"
---
<!-- ENCODING: UTF-8. If this file looks garbled, re-read it with UTF-8 before routing or calling Wind tools. -->
# Wind 万得金融数据
通过本地 CLI 调用 Wind 的 7 个 MCP 服务取数,只基于返回结果回答。只报告 Wind 返回值和必要限制,不补常识、不补点评。
每个问题按四步处理:**① 定路由 → ② 发命令 → ③ 读回执 → ④ 收口**。②③ 之间可以按回执里的错误信息修正参数后再调用,每次再调用前都要过一遍第 3 节的自检项。
## 1. 定路由
先按标的类型选 `server_type`,只读该行的一份契约;参数一律以这份契约为准,不读其它领域的契约,不凭记忆填参数名或字段值。
| `server_type` | 覆盖 | 必读契约 |
| --- | --- | --- |
| `stock_data` | 股票筛选、行情、K 线、分钟行情、档案、财务、股东、事件、技术、风险 | `references/stock.md` |
| `fund_data` | 基金 / ETF / LOF 筛选、行情、K 线、分钟行情、档案、财务、持仓、业绩、持有人、公司 | `references/fund.md` |
| `index_data` | 指数 / 板块行情、K 线、分钟行情、档案、基本面、技术 | `references/index.md` |
| `bond_data` | 债券档案、发债主体、行情估值、主体财务 | `references/bond.md` |
| `financial_docs` | 公告、年报、季报、招股书、财经新闻 | `references/financial-docs.md` |
| `economic_data` | 宏观、行业和汇率 EDB 指标 | `references/economic.md` |
| `analytics_data` | 跨标的聚合、加权平均、排名、复合指标推导 | `references/analytics.md` |
意图可能多义时按这个顺序仲裁:
1. 公告、年报、季报、招股书、监管披露 → `financial_docs.get_company_announcements`
2. 新闻、快讯、报道、评论 → `financial_docs.get_financial_news`
3. 宏观、行业或汇率 EDB 指标(产销量、CPI、利率、汇率指标等,即使未出现“宏观”字样)→ 只需指标元信息/确认代码走 `economic_data.search_economic_indicator`,要具体数值时间序列走 `economic_data.query_economic_indicator_data`
4. 未指定具体标的的筛选请求 → 对应领域的 `search_*`;`analytics_data` 返回计算结果,不返回实体列表。
5. 最新价、涨跌幅、成交量、K 线、分钟线、区间走势 → 对应领域行情工具;历史区间一律走 K 线。
6. 财务、股本、股东、事件、技术、风险、持仓、业绩 → 对应领域自然语言工具。
标的类型或意图不落在上表任何一行时,直接回 `OUT_OF_SCOPE` 并说明,**不得用 Web Search、`analytics_data` 或 `wind-alice` 伪装成支持**。
`analytics_data` 处理跨标的聚合、加权平均、排名和复合指标推导。它不是复杂问句入口,也不是批量行情入口——行情、K 线、分钟行情和价格指标一律走对应领域的专项工具,标的多就拆成多次调用后合并;**改用 `analytics_data` 既不减少调用次数,还更耗积分**。上一次用它取到了数据,不构成下一次跳过专项工具的理由。专项工具因字段、口径或无结果而无法覆盖剩余结构化数据时,才可用它补取。
涉及行业且用户未指定分类体系时,默认 Wind 行业分类。
## 2. 发命令
先 `cd` 到本 `SKILL.md` 所在目录(**不是当前项目目录**),再用相对路径执行:
```bash
node scripts/cli.mjs call <server_type> <tool_name> '<params_json>'
```
一个可直接运行的完整例子:
```bash
node scripts/cli.mjs call stock_data get_stock_price_indicators '{"windcode":"600519.SH"}'
```
参数取值一律回契约拿,不得从本例外推。
**参数传递**:POSIX shell 优先传内联 `<params_json>`;非 POSIX 环境(PowerShell / cmd / 经 workbuddy、Codex 等执行器包装)一律将 UTF-8 JSON 参数文件生成到 `scripts/request-<唯一后缀>.json`,以 `@scripts/request-<唯一后缀>.json` 传入,调用后删除。不复用共享文件,不在 skill 根目录生成。
**Key**:不得只检查部分配置来源就声称没有 API Key。必须先实跑一次;只有返回 `AUTH_ERROR` 且明确为未配置,才能判定缺失,并按信封中的指引处理。
**批量与并发**:默认串行(并发 1)。需要对 2 个及以上标的逐项调用时,先只发第一个作为探针,探针成功返回数据、未出现错误信封,才继续其余;探针返回错误信封立即终止该批次,不得把相同调用扩散到其它标的。不同 `server_type + tool_name` 或不同参数结构分别分组,每组各发一次探针。用户明确要求并发时上限 10,一旦某次返回 `RATE_LIMIT_ERROR` 或 `backend_error` 就停止新请求并恢复串行。
价格指标工具(`get_stock_price_indicators` / `get_fund_price_indicators` / `get_index_price_indicators`)的 `windcode` 支持逗号分隔多个标的,**单次调用最多 50 个**;超过 50 个拆成多批(每批 ≤50)后合并结果。该上限约束"单次调用内的代码数",与上面的并发上限 10(约束"同时并发的调用数")相互独立。请求较宽的指标集(`indexes` 字段数较多)时相应减少单批代码数,因为响应体积随"代码数 × 字段数"增长。
## 3. 读回执
每次调用的 stdout 只有两种形态:成功是数据对象,失败是带 `ok:false` 的错误信封。
**成功**:stdout 是数据对象,后端结果在 `content[0].text` 里(多为 JSON 字符串),CLI 另附一个 `cli_meta`。直接读;若存在 `content[0].text`,优先解析其中的文本或 JSON。
- 数值的单位和**量级**以返回体自带的元数据为准:行情类在 `data.unit`,列定义中可能带 `unit`,EDB 在 `meta.unit` 与 `meta.magnitude`。元数据未给出时保留原值并说明单位未知,不得自行换算。
**失败**:stdout 是 `{ "ok": false, "code": "...", "message": "..." }`。本地/参数/网络类错误的 `code` 指明原因(`AUTH_ERROR`、`PARAMS_FILE_ERROR`、`INVALID_PARAMS_JSON`、`PARAM_TYPE_ERROR`、`PARAM_VALIDATION_ERROR`、`ROUTE_ERROR`、`USAGE_ERROR`、`RATE_LIMIT_ERROR`、`NETWORK_ERROR`、`TOOL_RUNTIME_ERROR`、`SETUP_ERROR`、`UNKNOWN`);接口层错误的 `code` 固定为 `backend_error`,`message` 为接口原文。据此向用户说明,或按下面的自检修正后再调用。
**修正后再调用前自检**(逐条核对):
- 明确上一次的 `code` 与 `message`。
- 保持同一 `server_type` 和 `tool_name`;只有当前契约证明该工具无法表达所需字段或口径时,才可在同业务域切换。
- 除非错误是 `INVALID_PARAMS_JSON`,不得修改命令引号或 JSON 转义。
- 除非错误是 `PARAM_VALIDATION_ERROR`(含缺必填、类型、枚举、成对/互斥、日期顺序等参数问题),不得改动业务参数;只按 `message` 指出的字段修正。
- 参数名和字段值必须来自当前领域契约。
## 4. 收口
标的未识别或 NER 失败时,询问用户准确全称或 Wind 标准代码,不得自行补交易所后缀或把名称猜成代码。参数错误时优先按 `message` 中给出的期望类型、格式、枚举或字段集修正;无法唯一确定时再询问用户。
认证、额度、网络、后端不可用、命令传递、路由错误:直接报告,**不得切 `analytics_data` 或 `wind-alice`**。
`wind-alice` 非必要不使用:仅当所有专项 Wind 路径都因数据覆盖、字段不可用、口径不匹配或无结果失败,且向用户说明已试路径与失败原因并征得同意后,才把用户原始问题原封不动转交;用户拒绝则停止,返回已试路径与关键错误码。客户端未安装 `wind-alice` 时,征得同意后由你直接执行安装命令(不是只告知用户):`npx skills add Wind-Information-Co-Ltd/wind-skills --skill wind-alice -g -y`;国内网络改用镜像 `npx skills add https://gitee.com/wind_info/wind-skills.git --skill wind-alice -g -y`;仅安装到当前项目时去掉 `-g`。安装成功后再转交;安装失败时报告命令原始报错,不得静默放弃。
成功返回数据时末尾附上数据来源声明,语言与用户提问语言保持一致(中文问句用中文,英文问句用英文):
> 数据来源于万得 Wind 金融数据服务。
> Data sourced from Wind Financial Data Service.
完成状态:`DONE`、`DONE_WITH_LIMITS`、`NO_RESULTS`、`BLOCKED_KEY`、`BLOCKED_QUOTA`、`BLOCKED_RUNTIME`、`OUT_OF_SCOPE`。