<!-- 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. 定路由 先按标的类型选 ,只读该行的一份契约;参数一律以这份契约为准,不读其它领域的契约,不凭记忆填参数名或字段值。 | | 覆盖 | 必读契约 | | --- | --- | --- | | | 股票筛选、行情、K 线、分钟行情、档案、财务、股东、事件、技术、风险 | | | | 基金 / ETF / LOF 筛选、行情、K 线、分钟行情、档案、财务、持仓、业绩、持有人、公司 | | | | 指数 / 板块行情、K 线、分钟行情、档案、基本面、技术 | | | | 债券档案、发债主体、行情估值、主体财务 | | | | 公告、年报、季报、招股书、财经新闻 | | | | 宏观、行业和汇率 EDB 指标 | | |…

, state = { warnings: [], tables: [], invalidPaths: [] }, dataCell = false) {\n if (dataCell && value === 'INVALID') {\n state.invalidPaths.push(path);\n return null;\n }\n if (Array.isArray(value)) {\n return value.map((item, index) => normalizeSuccessPayload(item, `${path}[${index}]`, state, dataCell));\n }\n if (!value || typeof value !== 'object') return value;\n\n const normalized = {};\n for (const [key, item] of Object.entries(value)) {\n const isStructuredDataArray = Array.isArray(item) && (key === 'rows' || key === 'value');\n normalized[key] = normalizeSuccessPayload(item, `${path}.${key}`, state, dataCell || isStructuredDataArray);\n }\n if (Array.isArray(value.rows)) {\n state.tables.push({ path, actual_row_count: value.rows.length });\n }\n if (Object.hasOwn(value, 'excelTotalCount')) {\n state.warnings.push({\n code: 'UNRELIABLE_DECLARED_COUNT',\n path: `${path}.excelTotalCount`,\n message: 'excelTotalCount 仅保留为后端原始字段,不得据此判断结果总数或完整性。',\n });\n }\n return normalized;\n}\n\n// 保留 MCP result 外层兼容性;只清洗可解析的 JSON 文本并附加机器可读安全元数据。\nfunction normalizeCallSuccess(result, context = {}) {\n const output = result && typeof result === 'object' ? structuredClone(result) : result;\n const state = { warnings: [], tables: [], invalidPaths: [] };\n if (output && Array.isArray(output.content)) {\n for (const item of output.content) {\n if (item?.type !== 'text' || typeof item.text !== 'string') continue;\n try {\n const parsed = JSON.parse(item.text);\n item.text = JSON.stringify(normalizeSuccessPayload(parsed, '

<!-- 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. 定路由 先按标的类型选 ,只读该行的一份契约;参数一律以这份契约为准,不读其它领域的契约,不凭记忆填参数名或字段值。 | | 覆盖 | 必读契约 | | --- | --- | --- | | | 股票筛选、行情、K 线、分钟行情、档案、财务、股东、事件、技术、风险 | | | | 基金 / ETF / LOF 筛选、行情、K 线、分钟行情、档案、财务、持仓、业绩、持有人、公司 | | | | 指数 / 板块行情、K 线、分钟行情、档案、基本面、技术 | | | | 债券档案、发债主体、行情估值、主体财务 | | | | 公告、年报、季报、招股书、财经新闻 | | | | 宏观、行业和汇率 EDB 指标 | | |…

, state));\n } catch {\n // 非 JSON 文本按后端原文透传。\n }\n }\n }\n if (state.invalidPaths.length) {\n state.warnings.push({\n code: 'BACKEND_INVALID_AS_NULL',\n count: state.invalidPaths.length,\n paths: state.invalidPaths.slice(0, 100),\n truncated: state.invalidPaths.length > 100,\n message: '结构化数据区中的后端字符串 INVALID 已转换为 null;表示缺失或不适用,禁止按 0 参与计算。',\n });\n }\n if (output && typeof output === 'object') {\n output.cli_meta = {\n schema_version: '1.0',\n server_type: context.server_type || null,\n tool_name: context.tool_name || null,\n completeness: state.warnings.some(warning => warning.code === 'UNRELIABLE_DECLARED_COUNT') ? 'unknown' : 'not_asserted',\n tables: state.tables,\n warnings: state.warnings,\n };\n }\n return output;\n}\n\nfunction writeRawCallSuccess(result, context = {}) {\n process.stdout.write(JSON.stringify(normalizeCallSuccess(result, context), null, 2) + '\\n');\n}\n\nfunction writePlainSuccess(data) {\n process.stdout.write(JSON.stringify(data, null, 2) + '\\n');\n}\n\nconst DEFAULT_ERROR_MESSAGES = Object.freeze({\n AUTH_ERROR: '认证失败,请检查 API Key',\n PARAM_TYPE_ERROR: '参数类型错误,请检查字段类型',\n USAGE_ERROR: '命令用法错误,请检查输入参数',\n PARAMS_FILE_ERROR: '参数文件读取失败,请检查文件路径和内容',\n INVALID_PARAMS_JSON: '参数格式错误,params 必须是 JSON 对象',\n ROUTE_ERROR: '工具路由失败,请检查 server_type 和 tool_name',\n PARAM_VALIDATION_ERROR: '参数校验失败,请检查字段名和取值',\n PARAM_CONFLICT_ERROR: '参数存在冲突,请检查输入组合',\n RATE_LIMIT_ERROR: '请求过于频繁,请稍后重试',\n NETWORK_ERROR: '服务暂时不可用,请稍后重试',\n TOOL_RUNTIME_ERROR: '响应解析失败,请稍后重试',\n SETUP_ERROR: '本地配置缺失或无效,请检查技能配置',\n UNKNOWN: '调用失败,请稍后重试',\n});\n\nconst MAPPED_ERROR_MESSAGE_CODES = new Set(\n Object.keys(DEFAULT_ERROR_MESSAGES).filter((code) => code !== 'UNKNOWN'),\n);\n\nfunction normalizeErrorMessage(code, detail, metadata = {}) {\n if (!MAPPED_ERROR_MESSAGE_CODES.has(code)) {\n if (typeof metadata.error_message === 'string' && metadata.error_message.trim()) {\n return metadata.error_message.trim();\n }\n if (typeof detail === 'string' && detail.trim()) {\n return detail.trim().slice(0, 2000);\n }\n return DEFAULT_ERROR_MESSAGES.UNKNOWN;\n }\n if (typeof detail === 'string' && detail.trim()) {\n return detail.trim().slice(0, 500);\n }\n return DEFAULT_ERROR_MESSAGES[code] || DEFAULT_ERROR_MESSAGES.UNKNOWN;\n}\n\nfunction writeErrorEnvelope(code, detail, metadata = {}) {\n const envelope = {\n ok: false,\n code,\n message: normalizeErrorMessage(code, detail, metadata),\n };\n process.stdout.write(JSON.stringify(envelope, null, 2) + '\\n');\n}\n\nfunction die(code, detail = null, exitCode = 1, metadata = {}) {\n writeErrorEnvelope(code, detail, metadata);\n process.exit(exitCode);\n}\n\nfunction exitWithUsage(usage, exitCode = 0) {\n die('USAGE_ERROR', `USAGE:\\n${usage}`, exitCode);\n}\n// #endregion 信封\n\n// #region 认证:Key 顺序为 ~/.wind-aifinmarket/config > skill config.json > 环境变量 WIND_API_KEY。\nfunction maskKey(key) {\n if (!key || key.length \u003c 8) return '***';\n return key.slice(0, 4) + '***' + key.slice(-4);\n}\n\n// dotenv 解析: 兼容注释 / 引号 / export 前缀\nfunction parseDotenv(content) {\n const env = {};\n for (const rawLine of content.split('\\n')) {\n let line = rawLine.replace(/^\\uFEFF/, '').trim();\n if (!line || line.startsWith('#')) continue;\n if (line.startsWith('export ')) line = line.slice(7).trim();\n const eq = line.indexOf('=');\n if (eq \u003c= 0) continue;\n const key = line.slice(0, eq).trim();\n let val = line.slice(eq + 1).trim();\n if ((val.startsWith('\"') && val.endsWith('\"')) || (val.startsWith(\"'\") && val.endsWith(\"'\"))) {\n val = val.slice(1, -1);\n } else {\n const hashIdx = val.indexOf(' #');\n if (hashIdx >= 0) val = val.slice(0, hashIdx).trim();\n }\n env[key] = val;\n }\n return env;\n}\n\nfunction getApiKey() {\n const globalConfig = join(homedir(), '.wind-aifinmarket', 'config');\n if (existsSync(globalConfig)) {\n try {\n const env = parseDotenv(readFileSync(globalConfig, 'utf8'));\n const key = env.WIND_API_KEY?.trim();\n if (key) return key;\n } catch { }\n }\n\n const localConfig = join(SKILL_DIR, 'config.json');\n if (existsSync(localConfig)) {\n try {\n const cfg = JSON.parse(readFileSync(localConfig, 'utf8'));\n const key = typeof cfg.wind_api_key === 'string' ? cfg.wind_api_key.trim() : '';\n if (key) return key;\n } catch { }\n }\n\n const envKey = process.env.WIND_API_KEY?.trim();\n if (envKey) return envKey;\n\n die('AUTH_ERROR', 'WIND_API_KEY 未配置(CLI 已完整检查:用户全局配置 > Skill 本地配置 > 环境变量)');\n}\n// #endregion 认证\n\n// #region 路由:校验 server_type、tool_name 是否在 SERVERS 与 tool-manifest.json。非法则 ROUTE_ERROR。\nfunction getServer(server_type) {\n const server = SERVERS[server_type];\n if (!server) {\n die('ROUTE_ERROR', `未知 server_type: ${server_type}. 可用: ${Object.keys(SERVERS).join(' / ')}`);\n }\n return server;\n}\n\nfunction loadToolManifest() {\n try {\n // tool-manifest.json is the authority for legal server_type + tool_name combinations.\n const manifest = JSON.parse(readFileSync(TOOL_MANIFEST_PATH, 'utf8'));\n if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {\n throw new Error('manifest 顶层必须是对象');\n }\n for (const [serverType, tools] of Object.entries(manifest)) {\n if (!SERVERS[serverType]) {\n throw new Error(`manifest 包含未知 server_type: ${serverType}`);\n }\n if (!Array.isArray(tools) || tools.some(tool => typeof tool !== 'string' || !tool)) {\n throw new Error(`manifest 中 ${serverType} 的工具清单必须是非空字符串数组`);\n }\n }\n for (const serverType of Object.keys(SERVERS)) {\n if (!Array.isArray(manifest[serverType])) {\n throw new Error(`manifest 缺少 server_type: ${serverType}`);\n }\n }\n return manifest;\n } catch (err) {\n die('UNKNOWN', `工具清单读取失败: ${err.message}`);\n }\n}\n\nfunction validateToolSelection(server_type, toolName) {\n getServer(server_type);\n const manifest = loadToolManifest();\n const tools = manifest[server_type];\n if (!tools.includes(toolName)) {\n die('ROUTE_ERROR', `工具名 \"${toolName}\" 不属于 server_type \"${server_type}\"。`);\n }\n}\n// #endregion 路由\n\n// #region 规则加载:读 call-rules.json,得到 K 线周期映射、按域改写工具名、参数校验规则。\nfunction readCallRules() {\n try {\n return JSON.parse(readFileSync(CALL_RULES_PATH, 'utf8'));\n } catch (err) {\n die('UNKNOWN', `调用规则读取失败: ${err.message}`);\n }\n}\n\nfunction prepareNormalizationRules(rules) {\n return {\n klinePeriodMap: new Map(Object.entries(rules.kline_period_map || {})),\n toolByDomain: rules.tool_by_domain || {},\n };\n}\n\nconst CALL_RULES = readCallRules();\nconst NORMALIZATION_RULES = prepareNormalizationRules(CALL_RULES);\nconst KLINE_PERIOD_MAP = NORMALIZATION_RULES.klinePeriodMap;\nconst PUBLIC_KLINE_PERIODS = new Set(KLINE_PERIOD_MAP.keys());\nconst KLINE_PERIODS = new Set(KLINE_PERIOD_MAP.values());\nconst TOOL_BY_DOMAIN = NORMALIZATION_RULES.toolByDomain;\n\nconst TOOL_VALIDATION_RULES = {\n basic: CALL_RULES.basic || {},\n toolRules: Array.isArray(CALL_RULES.tool_rules) ? CALL_RULES.tool_rules : [],\n};\nconst KLINE_TOOLS = new Set(TOOL_VALIDATION_RULES.toolRules.find(rule => rule.name === 'kline')?.tools || []);\n// #endregion 规则加载\n\n// #region 规范化:整理 windcode/indexes/period。不给中文名称猜交易所后缀。\nfunction normalizeIndexes(indexes) {\n if (typeof indexes !== 'string') return indexes;\n return indexes.split(',').map((item) => item.trim()).filter(Boolean).join(',');\n}\n\nfunction normalizeWindcode(windcode) {\n if (typeof windcode !== 'string') return windcode;\n const raw = windcode.trim();\n const upper = raw.toUpperCase();\n // Keep natural-language names untouched. Wind's backend NER is responsible\n // for resolving names/aliases; the CLI must not guess exchange suffixes.\n if (/[\\u4e00-\\u9fff]/.test(raw)) return raw;\n if (/^0\\d{4}\\.HK$/.test(upper)) return upper.slice(1);\n if (/^\\d{4}\\.HK$/.test(upper)) return upper;\n if (/^\\d{6}\\.(SH|SZ|BJ|OF)$/.test(upper)) return upper;\n if (/^[A-Z]{1,5}\\.(O|N|A|HK|SH|SZ|BJ)$/.test(upper)) return upper;\n return raw;\n}\n\nfunction toolFamily(toolName) {\n if (PRICE_INDICATOR_TOOLS.has(toolName)) return 'price';\n if (KLINE_TOOLS.has(toolName)) return 'kline';\n if (QUOTE_TOOLS.has(toolName)) return 'quote';\n return null;\n}\n\nfunction normalizeCall(server_type, toolName, args) {\n const family = toolFamily(toolName);\n if (family) toolName = TOOL_BY_DOMAIN[family]?.[server_type] || toolName;\n const normalizedArgs = { ...args };\n const normalizationErrors = [];\n if (typeof normalizedArgs.indexes === 'string') normalizedArgs.indexes = normalizeIndexes(normalizedArgs.indexes);\n if (typeof normalizedArgs.windcode === 'string') normalizedArgs.windcode = normalizeWindcode(normalizedArgs.windcode);\n // count 是整型字段:把整数字符串收敛成 number,非整数原样留给 patterns 校验拦截。\n if (typeof normalizedArgs.count === 'string' && /^-?\\d+$/.test(normalizedArgs.count.trim())) {\n normalizedArgs.count = Number(normalizedArgs.count.trim());\n }\n if (KLINE_TOOLS.has(toolName) && normalizedArgs.period === undefined) normalizedArgs.period = '1d';\n if (typeof normalizedArgs.period === 'string') {\n const key = normalizedArgs.period.trim();\n const backendPeriod = KLINE_PERIOD_MAP.get(key);\n normalizedArgs.period = backendPeriod || key;\n if (!backendPeriod && KLINE_PERIODS.has(key)) {\n normalizationErrors.push({\n message: `字段 'period' 只能是 ${Array.from(PUBLIC_KLINE_PERIODS).join('/')},日 K 请传 '1d'`,\n field: 'period',\n issue: 'invalid_enum',\n actual: key,\n allowed_values: Array.from(PUBLIC_KLINE_PERIODS),\n });\n }\n }\n return { server_type, toolName, args: normalizedArgs, normalizationErrors };\n}\n// #endregion 规范化\n\n// #region 校验:按 call-rules 查必填、枚举、成对/互斥字段、日期顺序。发网络前拦住非法参数。\nfunction validateBasicParams(params) {\n const errors = [];\n if (!params || typeof params !== 'object' || Array.isArray(params)) {\n return [{\n code: 'PARAM_TYPE_ERROR',\n message: 'params 必须是 JSON object',\n field: 'params',\n issue: 'invalid_type',\n expected_type: 'object',\n actual_type: Array.isArray(params) ? 'array' : typeof params,\n }];\n }\n\n const basic = TOOL_VALIDATION_RULES.basic;\n for (const key of basic.string_keys || []) {\n if (!(key in params)) continue;\n if (typeof params[key] !== 'string') {\n errors.push({ message: `字段 '${key}' 必须是字符串`, field: key, issue: 'invalid_type', expected_type: 'string', actual_type: Array.isArray(params[key]) ? 'array' : typeof params[key] });\n } else if (params[key].trim().length === 0) {\n errors.push({ message: `字段 '${key}' 不能为空或全空白`, field: key, issue: 'empty_value', expected: 'non-empty string' });\n }\n }\n return errors;\n}\n\nfunction hasParamValue(params, key) {\n return params[key] !== undefined && params[key] !== null && params[key] !== '';\n}\n\nfunction resolveValidationValues(fieldRule) {\n if (Array.isArray(fieldRule.values)) return fieldRule.values.map(String);\n if (fieldRule.values_from === 'kline_period_map') return Array.from(KLINE_PERIODS).map(String);\n return [];\n}\n\nfunction resolveValidationDisplayValues(fieldRule) {\n if (fieldRule.values_from === 'kline_period_map') return Array.from(PUBLIC_KLINE_PERIODS).map(String);\n return resolveValidationValues(fieldRule);\n}\n\nfunction renderValidationMessage(template, values) {\n return String(template || '').replace('${values}', values.join('/'));\n}\n\nfunction validationErrorMessage(error) {\n return typeof error === 'string' ? error : error.message;\n}\n\nfunction validationErrorCode(error) {\n return typeof error === 'object' && error?.code ? error.code : null;\n}\n\nfunction validateToolParams(toolName, params) {\n const errors = [];\n const rules = TOOL_VALIDATION_RULES.toolRules.filter(rule => Array.isArray(rule.tools) && rule.tools.includes(toolName));\n\n for (const rule of rules) {\n const ruleLabel = rule.label || rule.name || toolName;\n if (Array.isArray(rule.allowed)) {\n const allowedKeys = new Set(rule.allowed);\n for (const key of Object.keys(params)) {\n if (!allowedKeys.has(key)) errors.push({ message: `${ruleLabel} 工具不支持字段 '${key}'`, field: key, issue: 'unknown_field', allowed_fields: [...allowedKeys] });\n }\n }\n\n for (const key of rule.required || []) {\n if (!hasParamValue(params, key)) errors.push({ message: `${ruleLabel} 工具缺少必填字段 '${key}'`, field: key, issue: 'missing_required', required_fields: rule.required || [] });\n }\n\n for (const [field, fieldRule] of Object.entries(rule.enum_fields || {})) {\n if (!(field in params)) continue;\n const values = resolveValidationValues(fieldRule);\n if (!values.includes(String(params[field]))) {\n const displayValues = resolveValidationDisplayValues(fieldRule);\n errors.push({ message: renderValidationMessage(fieldRule.message, displayValues), field, issue: 'invalid_enum', actual: params[field], allowed_values: displayValues });\n }\n }\n\n for (const fields of rule.paired || []) {\n const present = fields.filter(key => hasParamValue(params, key));\n if (present.length > 0 && present.length \u003c fields.length) {\n errors.push({ message: `字段 '${fields.join(\"' 和 '\")}' 应成对填写`, fields, issue: 'incomplete_pair', expected_fields: fields });\n }\n }\n\n for (const fields of rule.mutually_exclusive || []) {\n const present = fields.filter(key => hasParamValue(params, key));\n if (present.length > 1) {\n errors.push({ message: `字段 '${fields.join('/')}' 互斥,不应同时填写`, fields, issue: 'mutually_exclusive' });\n }\n }\n\n for (const [startKey, endKey] of rule.ordered_dates || []) {\n if (params[startKey] && params[endKey] && params[startKey] > params[endKey]) {\n errors.push({ message: `字段 '${startKey}' 不能晚于 '${endKey}'`, fields: [startKey, endKey], issue: 'invalid_order', expected: `${startKey} \u003c= ${endKey}` });\n }\n }\n\n for (const [field, patternRule] of Object.entries(rule.patterns || {})) {\n if (!(field in params)) continue;\n const pattern = new RegExp(patternRule.pattern);\n if (!pattern.test(String(params[field]))) {\n errors.push({ message: patternRule.message || `字段 '${field}' 格式不合法`, field, issue: 'invalid_format', actual: params[field], expected_pattern: patternRule.pattern });\n }\n }\n\n for (const conditional of rule.required_one_of_when || []) {\n if (!conditional.values?.map(String).includes(String(params[conditional.field]))) continue;\n const satisfied = conditional.one_of?.some(group => group.every(key => hasParamValue(params, key)));\n if (!satisfied) errors.push({ message: conditional.message || `字段 '${conditional.field}' 当前取值缺少配套参数`, field: conditional.field, issue: 'missing_conditional_fields', one_of: conditional.one_of });\n }\n\n for (const requirement of rule.required_one_of || []) {\n const satisfied = requirement.one_of?.some(group => group.every(key => hasParamValue(params, key)));\n if (!satisfied) errors.push({ message: requirement.message || `${ruleLabel} 工具缺少一组必填字段`, issue: 'missing_one_of', one_of: requirement.one_of });\n }\n }\n return errors;\n}\n// #endregion 校验\n\n// #region MCP:裸 HTTP JSON-RPC + SSE。先 initialize 再 tools/call。本地/网络错误由 CLI 收口,接口错误统一 backend_error。\nfunction parseSSE(text) {\n const trimmed = text.trim();\n // 后端正常 SSE, 部分错误场景纯 JSON\n if (trimmed.startsWith('{')) {\n try {\n return JSON.parse(trimmed);\n } catch { }\n }\n const lines = text.split(/\\r?\\n/);\n let last = null;\n for (const line of lines) {\n if (line.startsWith('data: ')) last = line.slice(6);\n }\n if (last) {\n try {\n return JSON.parse(last);\n } catch (e) {\n throw new Error(`SSE data 行 JSON 解析失败:${e.message}。原文前 200 字符:${text.slice(0, 200)}`);\n }\n }\n throw new Error(`响应格式无法识别(既非 SSE 也非纯 JSON)。原文前 200 字符:${text.slice(0, 200)}`);\n}\n\nasync function fetchWithRetry(fetchFn, url, optionsOrFactory, {\n attempts = 3,\n delaysMs = [300, 1000],\n onAttemptError = null,\n} = {}) {\n let lastError;\n for (let attempt = 1; attempt \u003c= attempts; attempt += 1) {\n try {\n const options = typeof optionsOrFactory === 'function'\n ? optionsOrFactory(attempt)\n : optionsOrFactory;\n return await fetchFn(url, options);\n } catch (err) {\n lastError = err;\n onAttemptError?.(err, attempt, attempts);\n const delayMs = delaysMs[Math.min(attempt - 1, delaysMs.length - 1)] || 0;\n if (attempt \u003c attempts && delayMs > 0) {\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n }\n }\n throw lastError;\n}\n\nasync function mcpRequest(server_type, method, params, {\n timeoutMs = 60_000,\n} = {}) {\n const server = getServer(server_type);\n const apiKey = getApiKey();\n const headers = {\n Authorization: `Bearer ${apiKey}`,\n Accept: 'application/json, text/event-stream',\n 'Content-Type': 'application/json',\n };\n\n const body = JSON.stringify({\n jsonrpc: '2.0',\n id: Date.now(),\n method,\n params\n });\n const dieInterfaceError = (message) => {\n die('backend_error', null, 1, {\n error_message: String(message ?? '').slice(0, 2000),\n });\n };\n let resp;\n try {\n resp = await fetchWithRetry(\n fetch,\n server.endpoint,\n () => ({\n method: 'POST',\n headers,\n body,\n signal: AbortSignal.timeout(timeoutMs),\n }),\n {\n attempts: 3,\n delaysMs: [300, 1000],\n onAttemptError: process.env.WIND_DEBUG === '1'\n ? (err, attempt, total) => {\n const causeCode = err?.cause?.code || err?.code || 'UNKNOWN_CAUSE';\n process.stderr.write(`[wind-mcp fetch retry ${attempt}/${total}] ${causeCode}: ${err?.message || err}\\n`);\n }\n : null,\n },\n );\n } catch {\n die('NETWORK_ERROR');\n }\n\n if (!resp.ok) {\n await resp.text().catch(() => '');\n die(HTTP_ERROR_MAP[resp.status] || 'NETWORK_ERROR');\n }\n\n const text = await resp.text();\n let payload;\n try {\n payload = parseSSE(text);\n } catch (err) {\n die('TOOL_RUNTIME_ERROR', `${err.message} (server=${server_type})`);\n }\n\n if (payload.error) {\n const msg = typeof payload.error === 'string'\n ? payload.error\n : (payload.error.message || JSON.stringify(payload.error));\n dieInterfaceError(msg);\n }\n\n if (payload.result?.isError) {\n const msg = payload.result.content?.[0]?.text || JSON.stringify(payload.result);\n dieInterfaceError(msg);\n }\n\n // 部分工具把业务错误包在 content[0].text 的 JSON 字符串里, 必须二次解析\n const innerText = payload.result?.content?.[0]?.text;\n if (typeof innerText === 'string') {\n let inner;\n try {\n inner = JSON.parse(innerText);\n } catch {\n inner = null;\n }\n if (inner) {\n if (typeof inner.mcp_tool_error_code === 'number' && inner.mcp_tool_error_code !== 0) {\n const msg = inner.mcp_tool_error_msg || JSON.stringify(inner);\n dieInterfaceError(msg);\n }\n if (inner.error && (inner.error.code || inner.error.message)) {\n const errorMessage = inner.error.message || JSON.stringify(inner.error);\n dieInterfaceError(errorMessage);\n }\n if (inner?.data && typeof inner.data === 'object') {\n const numericCode = typeof inner.data.code === 'number'\n ? inner.data.code\n : (typeof inner.data.code === 'string' && /^\\d+$/.test(inner.data.code.trim()) ? Number(inner.data.code) : null);\n const isSuccessCode = numericCode === 0\n || (numericCode !== null && numericCode >= 200 && numericCode \u003c 300);\n if (numericCode !== null && !isSuccessCode) {\n dieInterfaceError(typeof inner.data.message === 'string' ? inner.data.message : JSON.stringify(inner.data));\n }\n }\n }\n }\n return payload.result;\n}\n\nasync function mcpInitializeAndCall(server_type, method, params) {\n await mcpRequest(server_type, 'initialize', {\n protocolVersion: '2025-03-26',\n capabilities: {},\n clientInfo: {\n name: SKILL_NAME,\n version: SKILL_VERSION\n },\n }, {\n timeoutMs: 30_000\n });\n\n return mcpRequest(server_type, method, params, {\n timeoutMs: 600_000,\n });\n}\n// #endregion MCP\n\n// #region 命令:call 取数;list-tools 拉 schema;setup-key / open-portal 配 Key;diagnose 看更新状态。\nfunction loadParamsInput(paramsInput) {\n if (!paramsInput.startsWith('@')) {\n return { jsonText: paramsInput, source: 'inline' };\n }\n\n const fileArg = paramsInput.slice(1);\n if (!fileArg) {\n const error = new Error('@file 缺少文件路径');\n error.code = 'PARAMS_FILE_ERROR';\n error.file = fileArg;\n throw error;\n }\n\n const filePath = resolve(process.cwd(), fileArg);\n try {\n const jsonText = readFileSync(filePath, 'utf8').replace(/^\\uFEFF/, '');\n return { jsonText, source: 'file', filePath };\n } catch (cause) {\n const error = new Error(`无法读取 params 文件:${filePath} (${cause.code || cause.message})`);\n error.code = 'PARAMS_FILE_ERROR';\n error.file = filePath;\n error.cause = cause;\n throw error;\n }\n}\n\nasync function cmdCall(server_type, toolName, paramsInput) {\n if (!server_type || !toolName || !paramsInput) {\n exitWithUsage(\n `用法:call \u003cserver_type> \u003ctool_name> '\u003cparams_json>|@params_file'\\n` +\n `可用 server_type: ${Object.keys(SERVERS).join(' / ')}\\n` +\n `典型:\\n ${CALL_EXAMPLES.join('\\n ')}`,\n 1,\n );\n }\n\n let paramsSource;\n try {\n paramsSource = loadParamsInput(paramsInput);\n } catch (e) {\n die('PARAMS_FILE_ERROR', e.message);\n }\n\n let args;\n try {\n args = JSON.parse(paramsSource.jsonText);\n } catch (e) {\n const sourceDetail = paramsSource.source === 'file'\n ? `文件:${paramsSource.filePath}`\n : `原文:${paramsSource.jsonText.slice(0, 200)}`;\n die('INVALID_PARAMS_JSON', `params JSON 解析失败:${e.message} | ${sourceDetail}`);\n }\n\n if (!args || typeof args !== 'object' || Array.isArray(args)) {\n die('PARAM_TYPE_ERROR', 'params 必须是 JSON object');\n }\n\n let normalizationErrors;\n ({ server_type, toolName, args, normalizationErrors } = normalizeCall(server_type, toolName, args));\n validateToolSelection(server_type, toolName);\n\n const validationErrors = [...normalizationErrors, ...validateBasicParams(args)];\n const paramsShapeInvalid = validationErrors.some(error => validationErrorCode(error) === 'PARAM_TYPE_ERROR' && error.field === 'params');\n if (!paramsShapeInvalid) validationErrors.push(...validateToolParams(toolName, args));\n if (validationErrors.length > 0) {\n const explicitCode = validationErrors.map(validationErrorCode).find(Boolean);\n const messages = validationErrors.map(validationErrorMessage);\n const hasTypeError = validationErrors.some(error => typeof error === 'object' && error?.issue === 'invalid_type');\n die(explicitCode || (hasTypeError ? 'PARAM_TYPE_ERROR' : 'PARAM_VALIDATION_ERROR'), messages.join(';'));\n }\n\n const result = await mcpInitializeAndCall(server_type, 'tools/call', {\n name: toolName,\n arguments: args,\n _meta: { clientVersion: SKILL_VERSION },\n });\n return {\n server_type,\n tool: toolName,\n result,\n };\n}\n\nasync function cmdListTools(server_type) {\n if (!server_type) {\n exitWithUsage(\n `用法:list-tools \u003cserver_type>\\n` +\n `可用 server_type: ${Object.keys(SERVERS).join(' / ')}`,\n 1,\n );\n }\n getServer(server_type);\n const result = await mcpInitializeAndCall(server_type, 'tools/list', {});\n return { server_type, ...result };\n}\n\nasync function cmdSetupKey(...rawArgs) {\n const key = rawArgs[0];\n\n if (!key || key.startsWith('--')) {\n exitWithUsage(\n `用法:cli.mjs setup-key \u003cKEY> --scope \u003cglobal|skill>\\n\\n` +\n `scope: global=全局共享;skill=仅当前 skill。调用前先让用户选择。`,\n 1,\n );\n }\n\n let scope = null;\n for (let i = 1; i \u003c rawArgs.length; i++) {\n const a = rawArgs[i];\n if (a === '--scope' && rawArgs[i + 1]) {\n scope = rawArgs[i + 1];\n break;\n }\n if (a.startsWith('--scope=')) {\n scope = a.slice(8);\n break;\n }\n }\n\n if (!scope) {\n exitWithUsage(\n `setup-key 缺 --scope 参数。\\n\\n` +\n `先让用户选择 global 或 skill,再重试:cli.mjs setup-key ${maskKey(key)} --scope \u003cglobal|skill>`,\n 1,\n );\n }\n\n if (!['global', 'skill'].includes(scope)) {\n die('SETUP_ERROR', `setup-key 未知 scope: ${scope} (可选: global / skill)`);\n }\n\n let file;\n try {\n if (scope === 'global') {\n const dir = join(homedir(), '.wind-aifinmarket');\n if (!existsSync(dir)) mkdirSync(dir, {\n recursive: true\n });\n file = join(dir, 'config');\n let lines = [];\n if (existsSync(file)) {\n lines = readFileSync(file, 'utf8').split('\\n')\n .filter(l => l.length > 0 && !/^\\s*(export\\s+)?WIND_API_KEY\\s*=/.test(l));\n }\n lines.push(`WIND_API_KEY=${key}`);\n writeFileSync(file, lines.join('\\n') + '\\n', {\n mode: 0o600\n });\n } else {\n file = join(SKILL_DIR, 'config.json');\n writeFileSync(file, JSON.stringify({ wind_api_key: key }, null, 2) + '\\n', { mode: 0o600 });\n }\n } catch (err) {\n die('SETUP_ERROR', `配置写入失败 (scope=${scope}, path=${file || 'n/a'}): ${err.message}`);\n }\n\n return {\n scope,\n path: file,\n key_masked: maskKey(key),\n next: '现在可以重试原 Wind 调用',\n };\n}\n\nasync function cmdOpenPortal() {\n const platform = process.platform;\n let bin, args;\n if (platform === 'darwin') {\n bin = 'open';\n args = [PORTAL_URL];\n } else if (platform === 'win32') {\n bin = 'cmd';\n args = ['/c', 'start', '', PORTAL_URL];\n } else {\n bin = 'xdg-open';\n args = [PORTAL_URL];\n }\n\n let spawnError = null;\n try {\n const child = spawn(bin, args, {\n stdio: 'ignore',\n detached: true,\n windowsHide: true\n });\n child.unref();\n spawnError = await new Promise((resolve) => {\n child.once('error', resolve);\n setTimeout(() => resolve(null), 300);\n });\n } catch (err) {\n spawnError = err;\n }\n\n const data = {\n url: PORTAL_URL,\n platform,\n spawn_command: `${bin} ${args.join(' ')}`,\n flow_note: '未登录时会自动跳转到登录页(/#/login);登录完成后回到 overview 页面即可获取 API Key。',\n fallback_message: `如果浏览器没有自动弹出,请手动访问:${PORTAL_URL}`,\n };\n if (spawnError) {\n die('SETUP_ERROR', `本地无法启动浏览器: ${spawnError.message} | 用户应手动打开 ${data.url}`);\n }\n return data;\n}\n\n// 诊断: 输出自动更新状态\nasync function cmdDiagnose() {\n let updateState = null;\n try {\n const stateFile = updateStateFile();\n if (existsSync(stateFile)) {\n updateState = JSON.parse(readFileSync(stateFile, 'utf8'));\n }\n } catch {\n updateState = { status: 'unreadable' };\n }\n return {\n platform: process.platform,\n node_pid: process.pid,\n update_scope: updateScope(),\n update_state_file: updateStateFile(),\n update_state: updateState,\n next_update_needed: !alreadyUpdatedToday(),\n };\n}\n// #endregion 命令\n\n// #region 主入口:IS_MAIN 避免测试 import 时跑副作用。无参打 USAGE;仅 call 成功才触发更新检查。\nconst IS_MAIN = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;\n\nif (IS_MAIN) runMain();\n\nfunction runMain() {\n const [cmd, ...args] = process.argv.slice(2);\n\n const USAGE =\n `wind-mcp-skill\\n` +\n `访问万得 Wind 金融数据(按数据域分类调用)\\n\\n` +\n `用法:\\n` +\n ` cli.mjs call \u003cserver_type> \u003ctool_name> '\u003cparams_json>|@params_file'\\n` +\n ` cli.mjs list-tools \u003cserver_type> # 获取后端官方工具描述和 inputSchema\\n` +\n ` cli.mjs open-portal # 打开万得开发者中心拿 API Key\\n` +\n ` cli.mjs setup-key \u003cKEY> --scope \u003cglobal|skill> # 配置 API Key(先问用户存放位置)\\n\\n` +\n `可用 server_type:\\n` +\n Object.entries(SERVERS).map(([k, v]) => ` ${k.padEnd(20)}${v.label}`).join('\\n') + '\\n\\n' +\n `典型:\\n` +\n ` ${CALL_EXAMPLES.join('\\n ')}`;\n\n const commands = {\n call: () => cmdCall(args[0], args[1], args[2]),\n 'list-tools': () => cmdListTools(args[0]),\n 'open-portal': () => cmdOpenPortal(),\n 'setup-key': () => cmdSetupKey(...args),\n diagnose: () => cmdDiagnose(),\n };\n\n if (!cmd) {\n // help: 直接输出 USAGE 纯文本\n process.stdout.write(USAGE + '\\n');\n process.exit(0);\n }\n\n if (!commands[cmd]) {\n die('USAGE_ERROR', `未知命令: ${cmd}\\nUSAGE:\\n${USAGE}`);\n }\n\n commands[cmd]()\n .then((data) => {\n if (cmd === 'call') {\n // call: 透传 result 内容 (parse JSON if applicable, else raw text)\n writeRawCallSuccess(data?.result, { server_type: data?.server_type, tool_name: data?.tool });\n setTimeout(triggerUpdateCheck, 0);\n } else {\n // open-portal / setup-key: 直接输出结构化数据 (无 envelope 包裹)\n writePlainSuccess(data);\n }\n })\n .catch((err) => {\n die('UNKNOWN', `执行失败: ${err.message || err}${err.stack ? ' | stack: ' + err.stack.slice(0, 300) : ''}`);\n });\n}\n// #endregion 主入口\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":38415,"content_sha256":"f3da6a78754c4600d2d42e18db3d9032a53037a3aefe211dee662c881abc8b41"},{"filename":"scripts/tool-manifest.json","content":"{\n \"stock_data\": [\n \"get_stock_price_indicators\",\n \"get_risk_metrics\",\n \"get_stock_events\",\n \"get_stock_kline\",\n \"get_stock_basicinfo\",\n \"get_stock_equity_holders\",\n \"get_stock_fundamentals\",\n \"get_stock_quote\",\n \"get_stock_technicals\",\n \"search_stocks\"\n ],\n \"fund_data\": [\n \"get_fund_price_indicators\",\n \"get_fund_kline\",\n \"get_fund_financials\",\n \"get_fund_holdings\",\n \"get_fund_company_info\",\n \"get_fund_quote\",\n \"get_fund_info\",\n \"get_fund_holders\",\n \"get_fund_performance\",\n \"search_funds\"\n ],\n \"index_data\": [\n \"get_index_technicals\",\n \"get_index_quote\",\n \"get_index_kline\",\n \"get_index_fundamentals\",\n \"get_index_price_indicators\",\n \"get_index_basicinfo\"\n ],\n \"bond_data\": [\n \"get_bond_basicinfo\",\n \"get_bond_issuer_info\",\n \"get_bond_market_data\",\n \"get_bond_financial_data\"\n ],\n \"financial_docs\": [\n \"get_company_announcements\",\n \"get_financial_news\"\n ],\n \"economic_data\": [\n \"search_economic_indicator\",\n \"query_economic_indicator_data\"\n ],\n \"analytics_data\": [\n \"get_financial_data\"\n ]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":1118,"content_sha256":"b19f6c3ebb812ea3e48396dcc2ec266c6bb955bbff7fc8925f33e263eeaad06c"},{"filename":"scripts/update-check.mjs","content":"#!/usr/bin/env node\n // Daily background updater for wind-mcp-skill.\n// The CLI starts this script detached; failures are recorded but never block data calls.\n\nimport {\n closeSync,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n readdirSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport {\n createHash\n} from 'node:crypto';\nimport {\n spawnSync\n} from 'node:child_process';\nimport {\n homedir\n} from 'node:os';\nimport {\n basename,\n dirname,\n join,\n resolve\n} from 'node:path';\nimport {\n fileURLToPath\n} from 'node:url';\n\nconst SCRIPT_DIR = dirname(fileURLToPath(\n import.meta.url));\nconst SKILL_DIR = process.argv[2] ? resolve(process.argv[2]) : dirname(SCRIPT_DIR);\nconst SKILL_SCRIPTS_DIR = join(SKILL_DIR, 'scripts');\nconst LOCK_FILE = join(SKILL_SCRIPTS_DIR, 'update.lock');\nconst SKILL_NAME = basename(SKILL_DIR);\nconst DEFAULT_SOURCES = [\n 'Wind-Information-Co-Ltd/wind-skills',\n '[email protected]:wind_info/wind-skills.git',\n];\nconst LOCK_STALE_MS = 30 * 60 * 1000;\nconst QUIET_MS = 10 * 1000;\nconst MAX_WAIT_MS = 10 * 60 * 1000;\n\nfunction normalizePath(value) {\n const normalized = resolve(value).replace(/\\\\/g, '/');\n return process.platform === 'win32' ? normalized.toLowerCase() : normalized;\n}\n\nfunction updateScope() {\n const globalRoot = normalizePath(join(homedir(), '.agents', 'skills'));\n const skillDir = normalizePath(SKILL_DIR);\n return skillDir.startsWith(`${globalRoot}/`) ? 'global' : 'project';\n}\n\nfunction projectRoot() {\n return resolve(SKILL_DIR, '..', '..', '..');\n}\n\nfunction uniquePaths(paths) {\n const seen = new Set();\n const result = [];\n\n for (const path of paths.filter(Boolean).map((value) => resolve(value))) {\n const key = normalizePath(path);\n if (seen.has(key)) continue;\n seen.add(key);\n result.push(path);\n }\n\n return result;\n}\n\nfunction updateCommand() {\n const command = ['npx', 'skills', 'update', SKILL_NAME, '-y'];\n if (updateScope() === 'global') command.push('-g');\n return command;\n}\n\nfunction projectLockCandidates() {\n const roots = [projectRoot(), process.cwd(), process.env.INIT_CWD];\n let current = resolve(SKILL_DIR);\n\n while (true) {\n roots.push(current);\n const parent = dirname(current);\n if (parent === current) break;\n current = parent;\n }\n\n return uniquePaths(\n roots.filter(Boolean).map((root) => join(root, 'skills-lock.json')),\n );\n}\n\nfunction globalLockCandidates() {\n const xdg = process.env.XDG_STATE_HOME;\n return uniquePaths([\n xdg ? join(xdg, 'skills', '.skill-lock.json') : null,\n join(homedir(), '.agents', '.skill-lock.json'),\n ]);\n}\n\nfunction lockFileCandidates() {\n const globalCandidates = globalLockCandidates();\n const projectCandidates = projectLockCandidates();\n\n return updateScope() === 'global' ?\n uniquePaths([...globalCandidates, ...projectCandidates]) :\n uniquePaths([...projectCandidates, ...globalCandidates]);\n}\n\nfunction readLockInfo() {\n const candidates = lockFileCandidates();\n let firstExistingFile = null;\n\n for (const file of candidates) {\n try {\n if (!existsSync(file)) continue;\n firstExistingFile ||= file;\n\n const data = JSON.parse(readFileSync(file, 'utf8'));\n const entry = data?.skills?.[SKILL_NAME] || null;\n if (entry) return {\n file,\n entry,\n candidates\n };\n } catch {}\n }\n\n return {\n file: firstExistingFile || candidates[0] || null,\n entry: null,\n candidates,\n };\n}\n\nfunction readLockEntry() {\n return readLockInfo().entry;\n}\n\nfunction isGiteeSource(entry) {\n const values = [entry?.sourceType, entry?.source, entry?.sourceUrl]\n .filter(Boolean)\n .map((value) => String(value).toLowerCase());\n\n return values.some((value) => value.includes('gitee'));\n}\n\nfunction sourceUrl(entry) {\n if (!entry) return null;\n if (entry.sourceUrl) return entry.sourceUrl;\n\n if (entry.sourceType === 'github' && /^[^/\\s]+\\/[^/\\s]+$/.test(entry.source || '')) {\n return `https://github.com/${entry.source}.git`;\n }\n\n if (\n (entry.sourceType === 'gitee' || entry.sourceType === 'git') &&\n /^[^/\\s]+\\/[^/\\s]+$/.test(entry.source || '')\n ) {\n return `https://gitee.com/${entry.source}.git`;\n }\n\n return entry.source || null;\n}\n\nfunction updateEnv() {\n return {\n ...process.env\n };\n}\n\nfunction remoteHead(entry) {\n const source = sourceUrl(entry);\n if (!source) return null;\n\n try {\n const result = spawnSync('git', ['ls-remote', source, 'HEAD'], {\n encoding: 'utf8',\n env: updateEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 60 * 1000,\n windowsHide: true,\n });\n\n if (result.status !== 0) return null;\n const head = (result.stdout || '').trim().split(/\\s+/)[0];\n return /^[0-9a-f]{40}$/i.test(head) ? head : null;\n } catch {\n return null;\n }\n}\n\nfunction addCommandForSource(source) {\n if (!source) return null;\n\n const command = ['npx', 'skills', 'add', source, '--skill', SKILL_NAME, '-y'];\n if (updateScope() === 'global') command.push('-g');\n return command;\n}\n\nfunction addCommand(entry) {\n return addCommandForSource(sourceUrl(entry));\n}\n\nfunction fallbackAddCommands(entry) {\n const sources = [sourceUrl(entry), ...DEFAULT_SOURCES];\n const seen = new Set();\n\n return sources\n .filter(Boolean)\n .filter((source) => {\n const key = String(source).toLowerCase();\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n })\n .map(addCommandForSource)\n .filter(Boolean);\n}\n\nfunction commandForUpdate() {\n const entry = readLockEntry();\n\n if (isGiteeSource(entry)) {\n const command = addCommand(entry);\n if (command) {\n return {\n command,\n method: 'add',\n sourceType: entry?.sourceType || null,\n };\n }\n }\n\n return {\n command: updateCommand(),\n method: 'update',\n sourceType: entry?.sourceType || null,\n };\n}\n\nfunction updateStateFile() {\n return join(SKILL_SCRIPTS_DIR, 'update-state.json');\n}\n\nfunction todayKey() {\n return new Date().toISOString().slice(0, 10);\n}\n\nfunction readState() {\n try {\n const stateFile = updateStateFile();\n if (!existsSync(stateFile)) return null;\n return JSON.parse(readFileSync(stateFile, 'utf8'));\n } catch {\n return null;\n }\n}\n\nfunction alreadyUpdatedToday() {\n const state = readState();\n if (!state || state.date !== todayKey() || state.status !== 'success') return false;\n\n const entry = readLockEntry();\n if (!entry || isGiteeSource(entry)) return true;\n\n const head = remoteHead(entry);\n return !head || head === state.lastAppliedRemoteHead;\n}\n\nfunction lastUsedAt() {\n try {\n const state = readState();\n const timestamp = new Date(state?.lastUsedAt).getTime();\n return Number.isFinite(timestamp) ? timestamp : 0;\n } catch {\n return 0;\n }\n}\n\nfunction quietLongEnough() {\n const last = lastUsedAt();\n return last === 0 || Date.now() - last >= QUIET_MS;\n}\n\nfunction sleep(ms) {\n return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));\n}\n\nasync function waitForQuietWindow() {\n const startedAt = Date.now();\n\n while (!quietLongEnough()) {\n if (Date.now() - startedAt >= MAX_WAIT_MS) return false;\n await sleep(QUIET_MS);\n }\n\n return true;\n}\n\nfunction acquireLock() {\n try {\n if (!existsSync(SKILL_SCRIPTS_DIR)) mkdirSync(SKILL_SCRIPTS_DIR, {\n recursive: true\n });\n\n try {\n const st = statSync(LOCK_FILE);\n if (Date.now() - st.mtimeMs > LOCK_STALE_MS) unlinkSync(LOCK_FILE);\n } catch {}\n\n return openSync(LOCK_FILE, 'wx');\n } catch {\n return null;\n }\n}\n\nfunction releaseLock(fd) {\n try {\n if (fd !== null) closeSync(fd);\n } catch {}\n\n try {\n unlinkSync(LOCK_FILE);\n } catch {}\n}\n\nfunction writeState(patch) {\n const {\n command,\n method,\n sourceType\n } = commandForUpdate();\n const lock = readLockInfo();\n const stateFile = updateStateFile();\n const state = {\n date: todayKey(),\n scope: updateScope(),\n lockFile: lock.file,\n lockFound: Boolean(lock.entry),\n command: command.join(' '),\n method,\n sourceType,\n updatedAt: new Date().toISOString(),\n ...patch,\n };\n\n mkdirSync(dirname(stateFile), {\n recursive: true\n });\n writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\\n`);\n}\n\nfunction hashSkillDir() {\n const hash = createHash('sha256');\n const files = [];\n\n function walk(dir) {\n for (const entry of readdirSync(dir, {\n withFileTypes: true\n })) {\n const full = join(dir, entry.name);\n const rel = full.slice(SKILL_DIR.length + 1).replace(/\\\\/g, '/');\n\n if (rel === 'config.json' || rel === 'scripts/update-state.json') continue;\n if (entry.isDirectory()) {\n walk(full);\n } else if (entry.isFile()) {\n files.push({\n full,\n rel\n });\n }\n }\n }\n\n walk(SKILL_DIR);\n files.sort((a, b) => a.rel.localeCompare(b.rel));\n\n for (const file of files) {\n hash.update(file.rel);\n hash.update('\\0');\n hash.update(readFileSync(file.full));\n hash.update('\\0');\n }\n\n return hash.digest('hex');\n}\n\nfunction runSkillCommand(command, method) {\n const cwd = updateScope() === 'global' ? homedir() : projectRoot();\n const isWin = process.platform === 'win32';\n const bin = isWin ? 'cmd.exe' : 'npx';\n const args = isWin ? ['/d', '/s', '/c', command.join(' ')] : command.slice(1);\n const result = spawnSync(bin, args, {\n cwd,\n encoding: 'utf8',\n env: updateEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 10 * 60 * 1000,\n windowsHide: true,\n });\n const output = `${result.stdout || ''}${result.stderr || ''}`.trim();\n const failedByOutput =\n /failed to (update|add|install)|No installed skills found matching/i.test(output);\n\n return {\n command,\n method,\n result,\n output,\n ok: result.status === 0 && !failedByOutput,\n error: result.error ?\n String(result.error.message || result.error) :\n failedByOutput ?\n `npx skills ${method} reported failure` :\n null,\n };\n}\n\nfunction runUpdate() {\n const entry = readLockEntry();\n const {\n command,\n method,\n sourceType\n } = commandForUpdate();\n const state = readState();\n const beforeRemoteHead = remoteHead(entry);\n const remoteChanged = Boolean(beforeRemoteHead && beforeRemoteHead !== state?.lastAppliedRemoteHead);\n const beforeHash = hashSkillDir();\n let attempt = runSkillCommand(command, method);\n let usedFallback = false;\n let fallbackReason = null;\n\n if ((!attempt.ok || (attempt.ok && remoteChanged && beforeHash === hashSkillDir())) && method !== 'add') {\n const fallbackCommands = fallbackAddCommands(entry);\n\n if (fallbackCommands.length > 0) {\n fallbackReason = entry ?\n attempt.ok ?\n 'remote changed but update did not change local files' :\n 'update failed' :\n 'lock entry missing or update did not find installed skill';\n const outputs = [attempt.output].filter(Boolean);\n usedFallback = true;\n\n for (const fallbackCommand of fallbackCommands) {\n const fallback = runSkillCommand(fallbackCommand, 'add');\n outputs.push(fallback.output);\n attempt = {\n ...fallback,\n output: outputs.filter(Boolean).join('\\n\\n--- fallback: npx skills add ---\\n\\n'),\n error: fallback.ok ? null : fallback.error || attempt.error,\n };\n if (fallback.ok) break;\n }\n }\n }\n\n const afterHash = hashSkillDir();\n\n writeState({\n status: attempt.ok ? 'success' : 'failed',\n finishedAt: new Date().toISOString(),\n exitCode: attempt.result.status,\n method: attempt.method,\n usedFallback,\n fallbackReason,\n sourceType,\n command: attempt.command.join(' '),\n error: attempt.error,\n remoteHead: beforeRemoteHead,\n remoteChanged,\n lastAppliedRemoteHead: attempt.ok ?\n beforeRemoteHead || state?.lastAppliedRemoteHead || null :\n state?.lastAppliedRemoteHead || null,\n changed: beforeHash !== afterHash,\n beforeHash,\n afterHash,\n output: attempt.output.slice(-2000),\n });\n}\n\nasync function main() {\n if (alreadyUpdatedToday()) return;\n\n const fd = acquireLock();\n if (fd === null) return;\n\n try {\n if (alreadyUpdatedToday()) return;\n\n if (!(await waitForQuietWindow())) {\n writeState({\n status: 'deferred',\n finishedAt: new Date().toISOString(),\n exitCode: null,\n error: 'skill kept being used; update deferred after max wait',\n changed: false,\n });\n return;\n }\n\n writeState({\n status: 'updating',\n startedAt: new Date().toISOString(),\n exitCode: null,\n changed: false,\n });\n runUpdate();\n } finally {\n releaseLock(fd);\n }\n}\n\nmain().catch((err) => {\n try {\n writeState({\n status: 'failed',\n finishedAt: new Date().toISOString(),\n exitCode: null,\n error: String(err?.message || err),\n changed: false,\n });\n } catch {}\n});\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":12886,"content_sha256":"396f031a265d3464b4d3646fb6485ee3e1a3bdbbeb794427684ac7cadc1c8549"}],"content_json":{"type":"doc","content":[{"type":"paragraph","content":[{"text":"\u003c!-- ENCODING: UTF-8. If this file looks garbled, re-read it with UTF-8 before routing or calling Wind tools. -->","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Wind 万得金融数据","type":"text"}]},{"type":"paragraph","content":[{"text":"通过本地 CLI 调用 Wind 的 7 个 MCP 服务取数,只基于返回结果回答。只报告 Wind 返回值和必要限制,不补常识、不补点评。","type":"text"}]},{"type":"paragraph","content":[{"text":"每个问题按四步处理:","type":"text"},{"text":"① 定路由 → ② 发命令 → ③ 读回执 → ④ 收口","type":"text","marks":[{"type":"strong"}]},{"text":"。②③ 之间可以按回执里的错误信息修正参数后再调用,每次再调用前都要过一遍第 3 节的自检项。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"1. 定路由","type":"text"}]},{"type":"paragraph","content":[{"text":"先按标的类型选 ","type":"text"},{"text":"server_type","type":"text","marks":[{"type":"code_inline"}]},{"text":",只读该行的一份契约;参数一律以这份契约为准,不读其它领域的契约,不凭记忆填参数名或字段值。","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"server_type","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"覆盖","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"必读契约","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stock_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"股票筛选、行情、K 线、分钟行情、档案、财务、股东、事件、技术、风险","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/stock.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fund_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"基金 / ETF / LOF 筛选、行情、K 线、分钟行情、档案、财务、持仓、业绩、持有人、公司","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/fund.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"index_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"指数 / 板块行情、K 线、分钟行情、档案、基本面、技术","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/index.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bond_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"债券档案、发债主体、行情估值、主体财务","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/bond.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"financial_docs","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"公告、年报、季报、招股书、财经新闻","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/financial-docs.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"economic_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"宏观、行业和汇率 EDB 指标","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/economic.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analytics_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"跨标的聚合、加权平均、排名、复合指标推导","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/analytics.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"意图可能多义时按这个顺序仲裁:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"公告、年报、季报、招股书、监管披露 → ","type":"text"},{"text":"financial_docs.get_company_announcements","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"新闻、快讯、报道、评论 → ","type":"text"},{"text":"financial_docs.get_financial_news","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"宏观、行业或汇率 EDB 指标(产销量、CPI、利率、汇率指标等,即使未出现“宏观”字样)→ 只需指标元信息/确认代码走 ","type":"text"},{"text":"economic_data.search_economic_indicator","type":"text","marks":[{"type":"code_inline"}]},{"text":",要具体数值时间序列走 ","type":"text"},{"text":"economic_data.query_economic_indicator_data","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"未指定具体标的的筛选请求 → 对应领域的 ","type":"text"},{"text":"search_*","type":"text","marks":[{"type":"code_inline"}]},{"text":";","type":"text"},{"text":"analytics_data","type":"text","marks":[{"type":"code_inline"}]},{"text":" 返回计算结果,不返回实体列表。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"最新价、涨跌幅、成交量、K 线、分钟线、区间走势 → 对应领域行情工具;历史区间一律走 K 线。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"财务、股本、股东、事件、技术、风险、持仓、业绩 → 对应领域自然语言工具。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"标的类型或意图不落在上表任何一行时,直接回 ","type":"text"},{"text":"OUT_OF_SCOPE","type":"text","marks":[{"type":"code_inline"}]},{"text":" 并说明,","type":"text"},{"text":"不得用 Web Search、","type":"text","marks":[{"type":"strong"}]},{"text":"analytics_data","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 或 ","type":"text","marks":[{"type":"strong"}]},{"text":"wind-alice","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 伪装成支持","type":"text","marks":[{"type":"strong"}]},{"text":"。","type":"text"}]},{"type":"paragraph","content":[{"text":"analytics_data","type":"text","marks":[{"type":"code_inline"}]},{"text":" 处理跨标的聚合、加权平均、排名和复合指标推导。它不是复杂问句入口,也不是批量行情入口——行情、K 线、分钟行情和价格指标一律走对应领域的专项工具,标的多就拆成多次调用后合并;","type":"text"},{"text":"改用 ","type":"text","marks":[{"type":"strong"}]},{"text":"analytics_data","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 既不减少调用次数,还更耗积分","type":"text","marks":[{"type":"strong"}]},{"text":"。上一次用它取到了数据,不构成下一次跳过专项工具的理由。专项工具因字段、口径或无结果而无法覆盖剩余结构化数据时,才可用它补取。","type":"text"}]},{"type":"paragraph","content":[{"text":"涉及行业且用户未指定分类体系时,默认 Wind 行业分类。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"2. 发命令","type":"text"}]},{"type":"paragraph","content":[{"text":"先 ","type":"text"},{"text":"cd","type":"text","marks":[{"type":"code_inline"}]},{"text":" 到本 ","type":"text"},{"text":"SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" 所在目录(","type":"text"},{"text":"不是当前项目目录","type":"text","marks":[{"type":"strong"}]},{"text":"),再用相对路径执行:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"node scripts/cli.mjs call \u003cserver_type> \u003ctool_name> '\u003cparams_json>'","type":"text"}]},{"type":"paragraph","content":[{"text":"一个可直接运行的完整例子:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"node scripts/cli.mjs call stock_data get_stock_price_indicators '{\"windcode\":\"600519.SH\"}'","type":"text"}]},{"type":"paragraph","content":[{"text":"参数取值一律回契约拿,不得从本例外推。","type":"text"}]},{"type":"paragraph","content":[{"text":"参数传递","type":"text","marks":[{"type":"strong"}]},{"text":":POSIX shell 优先传内联 ","type":"text"},{"text":"\u003cparams_json>","type":"text","marks":[{"type":"code_inline"}]},{"text":";非 POSIX 环境(PowerShell / cmd / 经 workbuddy、Codex 等执行器包装)一律将 UTF-8 JSON 参数文件生成到 ","type":"text"},{"text":"scripts/request-\u003c唯一后缀>.json","type":"text","marks":[{"type":"code_inline"}]},{"text":",以 ","type":"text"},{"text":"@scripts/request-\u003c唯一后缀>.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" 传入,调用后删除。不复用共享文件,不在 skill 根目录生成。","type":"text"}]},{"type":"paragraph","content":[{"text":"Key","type":"text","marks":[{"type":"strong"}]},{"text":":不得只检查部分配置来源就声称没有 API Key。必须先实跑一次;只有返回 ","type":"text"},{"text":"AUTH_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":" 且明确为未配置,才能判定缺失,并按信封中的指引处理。","type":"text"}]},{"type":"paragraph","content":[{"text":"批量与并发","type":"text","marks":[{"type":"strong"}]},{"text":":默认串行(并发 1)。需要对 2 个及以上标的逐项调用时,先只发第一个作为探针,探针成功返回数据、未出现错误信封,才继续其余;探针返回错误信封立即终止该批次,不得把相同调用扩散到其它标的。不同 ","type":"text"},{"text":"server_type + tool_name","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或不同参数结构分别分组,每组各发一次探针。用户明确要求并发时上限 10,一旦某次返回 ","type":"text"},{"text":"RATE_LIMIT_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":" 或 ","type":"text"},{"text":"backend_error","type":"text","marks":[{"type":"code_inline"}]},{"text":" 就停止新请求并恢复串行。","type":"text"}]},{"type":"paragraph","content":[{"text":"价格指标工具(","type":"text"},{"text":"get_stock_price_indicators","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get_fund_price_indicators","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"get_index_price_indicators","type":"text","marks":[{"type":"code_inline"}]},{"text":")的 ","type":"text"},{"text":"windcode","type":"text","marks":[{"type":"code_inline"}]},{"text":" 支持逗号分隔多个标的,","type":"text"},{"text":"单次调用最多 50 个","type":"text","marks":[{"type":"strong"}]},{"text":";超过 50 个拆成多批(每批 ≤50)后合并结果。该上限约束\"单次调用内的代码数\",与上面的并发上限 10(约束\"同时并发的调用数\")相互独立。请求较宽的指标集(","type":"text"},{"text":"indexes","type":"text","marks":[{"type":"code_inline"}]},{"text":" 字段数较多)时相应减少单批代码数,因为响应体积随\"代码数 × 字段数\"增长。","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"3. 读回执","type":"text"}]},{"type":"paragraph","content":[{"text":"每次调用的 stdout 只有两种形态:成功是数据对象,失败是带 ","type":"text"},{"text":"ok:false","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的错误信封。","type":"text"}]},{"type":"paragraph","content":[{"text":"成功","type":"text","marks":[{"type":"strong"}]},{"text":":stdout 是数据对象,后端结果在 ","type":"text"},{"text":"content[0].text","type":"text","marks":[{"type":"code_inline"}]},{"text":" 里(多为 JSON 字符串),CLI 另附一个 ","type":"text"},{"text":"cli_meta","type":"text","marks":[{"type":"code_inline"}]},{"text":"。直接读;若存在 ","type":"text"},{"text":"content[0].text","type":"text","marks":[{"type":"code_inline"}]},{"text":",优先解析其中的文本或 JSON。","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"数值的单位和","type":"text"},{"text":"量级","type":"text","marks":[{"type":"strong"}]},{"text":"以返回体自带的元数据为准:行情类在 ","type":"text"},{"text":"data.unit","type":"text","marks":[{"type":"code_inline"}]},{"text":",列定义中可能带 ","type":"text"},{"text":"unit","type":"text","marks":[{"type":"code_inline"}]},{"text":",EDB 在 ","type":"text"},{"text":"meta.unit","type":"text","marks":[{"type":"code_inline"}]},{"text":" 与 ","type":"text"},{"text":"meta.magnitude","type":"text","marks":[{"type":"code_inline"}]},{"text":"。元数据未给出时保留原值并说明单位未知,不得自行换算。","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"失败","type":"text","marks":[{"type":"strong"}]},{"text":":stdout 是 ","type":"text"},{"text":"{ \"ok\": false, \"code\": \"...\", \"message\": \"...\" }","type":"text","marks":[{"type":"code_inline"}]},{"text":"。本地/参数/网络类错误的 ","type":"text"},{"text":"code","type":"text","marks":[{"type":"code_inline"}]},{"text":" 指明原因(","type":"text"},{"text":"AUTH_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"PARAMS_FILE_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"INVALID_PARAMS_JSON","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"PARAM_TYPE_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"PARAM_VALIDATION_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"ROUTE_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"USAGE_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"RATE_LIMIT_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"NETWORK_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"TOOL_RUNTIME_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"SETUP_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"UNKNOWN","type":"text","marks":[{"type":"code_inline"}]},{"text":");接口层错误的 ","type":"text"},{"text":"code","type":"text","marks":[{"type":"code_inline"}]},{"text":" 固定为 ","type":"text"},{"text":"backend_error","type":"text","marks":[{"type":"code_inline"}]},{"text":",","type":"text"},{"text":"message","type":"text","marks":[{"type":"code_inline"}]},{"text":" 为接口原文。据此向用户说明,或按下面的自检修正后再调用。","type":"text"}]},{"type":"paragraph","content":[{"text":"修正后再调用前自检","type":"text","marks":[{"type":"strong"}]},{"text":"(逐条核对):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"明确上一次的 ","type":"text"},{"text":"code","type":"text","marks":[{"type":"code_inline"}]},{"text":" 与 ","type":"text"},{"text":"message","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"保持同一 ","type":"text"},{"text":"server_type","type":"text","marks":[{"type":"code_inline"}]},{"text":" 和 ","type":"text"},{"text":"tool_name","type":"text","marks":[{"type":"code_inline"}]},{"text":";只有当前契约证明该工具无法表达所需字段或口径时,才可在同业务域切换。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"除非错误是 ","type":"text"},{"text":"INVALID_PARAMS_JSON","type":"text","marks":[{"type":"code_inline"}]},{"text":",不得修改命令引号或 JSON 转义。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"除非错误是 ","type":"text"},{"text":"PARAM_VALIDATION_ERROR","type":"text","marks":[{"type":"code_inline"}]},{"text":"(含缺必填、类型、枚举、成对/互斥、日期顺序等参数问题),不得改动业务参数;只按 ","type":"text"},{"text":"message","type":"text","marks":[{"type":"code_inline"}]},{"text":" 指出的字段修正。","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"参数名和字段值必须来自当前领域契约。","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"4. 收口","type":"text"}]},{"type":"paragraph","content":[{"text":"标的未识别或 NER 失败时,询问用户准确全称或 Wind 标准代码,不得自行补交易所后缀或把名称猜成代码。参数错误时优先按 ","type":"text"},{"text":"message","type":"text","marks":[{"type":"code_inline"}]},{"text":" 中给出的期望类型、格式、枚举或字段集修正;无法唯一确定时再询问用户。","type":"text"}]},{"type":"paragraph","content":[{"text":"认证、额度、网络、后端不可用、命令传递、路由错误:直接报告,","type":"text"},{"text":"不得切 ","type":"text","marks":[{"type":"strong"}]},{"text":"analytics_data","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" 或 ","type":"text","marks":[{"type":"strong"}]},{"text":"wind-alice","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":"。","type":"text"}]},{"type":"paragraph","content":[{"text":"wind-alice","type":"text","marks":[{"type":"code_inline"}]},{"text":" 非必要不使用:仅当所有专项 Wind 路径都因数据覆盖、字段不可用、口径不匹配或无结果失败,且向用户说明已试路径与失败原因并征得同意后,才把用户原始问题原封不动转交;用户拒绝则停止,返回已试路径与关键错误码。客户端未安装 ","type":"text"},{"text":"wind-alice","type":"text","marks":[{"type":"code_inline"}]},{"text":" 时,征得同意后由你直接执行安装命令(不是只告知用户):","type":"text"},{"text":"npx skills add Wind-Information-Co-Ltd/wind-skills --skill wind-alice -g -y","type":"text","marks":[{"type":"code_inline"}]},{"text":";国内网络改用镜像 ","type":"text"},{"text":"npx skills add https://gitee.com/wind_info/wind-skills.git --skill wind-alice -g -y","type":"text","marks":[{"type":"code_inline"}]},{"text":";仅安装到当前项目时去掉 ","type":"text"},{"text":"-g","type":"text","marks":[{"type":"code_inline"}]},{"text":"。安装成功后再转交;安装失败时报告命令原始报错,不得静默放弃。","type":"text"}]},{"type":"paragraph","content":[{"text":"成功返回数据时末尾附上数据来源声明,语言与用户提问语言保持一致(中文问句用中文,英文问句用英文):","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"数据来源于万得 Wind 金融数据服务。","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Data sourced from Wind Financial Data Service.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"完成状态:","type":"text"},{"text":"DONE","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"DONE_WITH_LIMITS","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"NO_RESULTS","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"BLOCKED_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"BLOCKED_QUOTA","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"BLOCKED_RUNTIME","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"OUT_OF_SCOPE","type":"text","marks":[{"type":"code_inline"}]},{"text":"。","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"wind-mcp-skill","author":"Wind","source":{"stars":148,"repo_name":"AliceMarket","origin_url":"https://github.com/Wind-Alice/AliceMarket/blob/HEAD/skills/wind-mcp-skill/SKILL.md","repo_owner":"Wind-Alice","body_sha256":"b17cce0cad7769fbd5554ed5300b5131a8acf90a0c9a0dee797908e4ade10c2d","cluster_key":"fa960ff35f7e9822b386ffbd32f95e6e5f052ee001447d390a6ba4a4eba89464","clean_bundle":{"format":"clean-skill-bundle-v1","source":"Wind-Alice/AliceMarket/skills/wind-mcp-skill/SKILL.md","text_bytes":104685,"source_commit":"ab8fa4cfc7904fa28d2a7297cd293d192eda3b5e","skills_sh_rank":368,"attachment_count":14,"text_attachments":14,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/wind-mcp-skill/SKILL.md","import_metadata":{"date":"2026-11-02","author":"Wind","version":"v1","category":"finance-legal-compliance","category_label":"Finance"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"finance-legal-compliance","examples":["筛选沪深市场市值超500亿且连续5日上涨的股票","筛选港股中市值超1000亿港元的科技股","筛选股票型基金中近一年收益率超20%的产品","贵州茅台今天最新价","苹果公司(AAPL.O)最近30日K线","易方达蓝筹精选(005827.OF)最新规模和经理","中证500指数PE/PB历史分位","贵州茅台2024年年度报告内容","中国近10年新能源汽车产销量"],"homepage":"https://aifinmarket.wind.com.cn","security":{"eval":false,"network":true,"child_process":true,"filesystem_read":true,"filesystem_write":true},"import_tag":"skills-sh-top20k-2026-11","auto_invoke":true,"description":"用户需要查询、筛选、获取、比较或验证金融市场数据时,优先调用本 Skill 获取可靠、可验证数据,而非仅依赖模型记忆或通用信息来源。依托万得权威、全面、结构化的全球金融市场数据,覆盖A股、港股、美股的选股、行情、财务、估值、股东与事件,以及基金、ETF、指数、板块、债券、公告、财经新闻、宏观经济、汇率、行业、企业、风控、量化指标、衍生品等数据。"}},"renderedAt":1793661624953}

<!-- 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. 定路由 先按标的类型选 ,只读该行的一份契约;参数一律以这份契约为准,不读其它领域的契约,不凭记忆填参数名或字段值。 | | 覆盖 | 必读契约 | | --- | --- | --- | | | 股票筛选、行情、K 线、分钟行情、档案、财务、股东、事件、技术、风险 | | | | 基金 / ETF / LOF 筛选、行情、K 线、分钟行情、档案、财务、持仓、业绩、持有人、公司 | | | | 指数 / 板块行情、K 线、分钟行情、档案、基本面、技术 | | | | 债券档案、发债主体、行情估值、主体财务 | | | | 公告、年报、季报、招股书、财经新闻 | | | | 宏观、行业和汇率 EDB 指标 | | |…