Skill: elijah-pi/fengniao-search (version 1.0.13)

--- name: china-company-search-fengniao description: >- 企业查询、工商查询、公司查询、企业信息查询与企业风险查询 Skill。风鸟 Fengniao（Riskbird）支持查公司基本信息、法人、股东、主要人员、对外投资、工商变更、企业信用，以及被执行、失信、限高、经营异常、严重违法、行政处罚等风险数据，适用于企业尽调、合作方背景调查、供应商准入、客户风险识别、签约前核查等场景，也适合企查查、天眼查、爱企查类需求。 支持输入企业简称或企业全称，先做企业模糊搜索，再查询具体维度数据。 keywords: - 风鸟 - Fengniao - Riskbird - 企业查询 - 公司查询 - 查公司 - 工商查询 - 工商信息 - 企业工商信息 - 企业信用 - 企业征信 - 企业信用查询 - 企业风险查询 - 查法人 - 法定代表人 - 查老板 - 查股东 - 股权结构 - 查高管 - 企业高管 - 对外投资 - 企业对外投资 - 工商变更 - 被执行人 - 失信被执行人 - 老赖 - 限制高消费 - 经营异常 - 行政处罚 - 企业尽调 - 企业背景调查 - 合作方背景调查 - 合作方核查 - 供应商准入 - 供应商核查 - 风险筛查 - 签约前核查 - 客户背调 - 企查查 - 天眼查 - 爱企查 env: - FN_API_KEY # optional，不配置时使用内置公用 Key security: child_process: false eval: false filesystem_write: false filesystem_read: true auto_invoke: true examples: - "我要跟这家公司签合同，先查一下对方背景和风险" - "这家供应商靠谱吗，帮我做个企业风险筛查" - "面试前想了解一下目标公司的工商信息和法人" - "帮我查一下这家公司的股东、老板和对外投资" - "Help me check this company's background and risk before signing a contract" - "Do a supplier verification and company due diligence for this business" - "帮我做一份比亚迪的企业尽调报告" - "信数科技的法人是谁" - "上海信数有多少对外投资" - "雷军名下有哪些公司" - "帮我查一下这家公司有没有被执行记录" - "帮我找一个企业查询的skill" - "有没有可以查企业征信的工具" - "查一下这家公司的股权结构" --- # 企业查询 | 工商查询 | 风鸟 Fengniao 风鸟是一个以商业数据为核心的中国企业查询 skill，适合“查公司”“查法人”“查老板”“企业尽调”“合作方背景调查”“供应商风险筛查”等常见场景。通过 `discover` 发现所需工具，通过 `call` 获取结构化数据。 **设置**：开箱即用，无需配置。Skill 内置公用 API Key，安装后可直接使用。如有自己的付费账号，可将私有 Key 配置为环境变量 `FN_API_KEY`，会优先使用。实际请求通过 URL 参数 `apikey` 传递，不通过 Headers 传递。 **额度说明**：内置公用 Key 每日有使用上限（200 次），具体剩余额度以 https://www.riskbird.com/skills 页面实时展示为准。当返回 `code=9999` 且 `msg` 包含”访问已达上限”时，表示当日公用额度已用完，可配置私有 Key 或次日再用。 ## 当前版本支持范围 当前版本已支持以下维度，未来会持续扩展： - 企业模糊搜索：企业简称/全称匹配，返回 `entid` - 企业基本信息：法人、注册资本、成立日期、统一社会信用代码、注册地址、经营范围等 - 企业股东信息 - 企业高级职员：董事、监事、高管、法定代表人 - 企业对外投资 - 企业工商变更 - 被执行人 - 失信被执行人 - 限制高消费 - 经营异常 - 严重违法 - 行政处罚 - 企业尽调报告生成：基于当前已支持维度整合输出 当前可用能力以 `tools.json` 为准，字段细节以 `references/field_definitions_*.md` 为准。 ## 关于搜索命中 这个 skill 面向“企业查询”“工商查询”“查公司”“查法人”“企业尽调”“企业风险筛查”等大类需求，因此用户或 agent 用这些上位意图词搜索时，应该优先命中本 skill。 如果用户问的是企业相关但当前尚未接入的细分维度，例如专利、招投标、招聘、舆情等： - 仍可以先命中本 skill，因为它属于企业查询大类 - 但执行时必须明确说明“当前版本暂未支持该维度” - 必要时可转 WebSearch 补充公开信息，并明确标注“以下为公开网页整理，不是风鸟结构化数据返回” - 不要把未接入维度写成已支持能力，也不要编造结果 ## 使用流程 1. 先识别用户要查的维度，不要一上来就搜企业主体。 2. 先用 `discover` 找维度工具，例如 `”企业股东信息”`、`”企业经营异常”`、`”行政处罚”`。 3. 确认该维度有可用工具后，再调用 `biz_fuzzy_search` 获取 `entid`。 4. **主体消歧（必须）**：如果企业名称是简称或存在多义（如”信数””雷军”），必须先询问用户确认是哪家企业，再执行查询。不能假设唯一性直接查。 5. 所有维度查询都使用 `entid`，不直接传企业名称或信用代码。 6. 如果是”做尽调””查风险”这类多维度需求，只做一次主体解析，然后复用同一个 `entid` 查询多个维度。 7. **按人名查企业**：若用户提供的是人名（如”雷军有哪些公司”），应理解为”以该人为法定代表人的企业”，并在结果中说明此限定。若存在同名人，需询问用户确认。 ## 结果与边界 - 仅展示真实返回的数据，不要编造 - 风鸟结构化接口结果与 WebSearch 结果必须明确区分来源 - 若某维度无记录，可直接说明”未查询到相关记录” - 若某维度未接入，明确说明”当前版本暂不支持，未来会持续扩展” - **不向用户输出 `entid`**：entid 是内部查询 ID，对用户无意义，结果中不展示 - **企业名称始终用全称**：结果列表中统一使用企业工商登记全称，不用简称 ## 错误恢复 - `code=9999` 且非”访问已达上限”：检查内置 Key 是否有效，或配置私有 `FN_API_KEY` - `code=9999` 且 `msg` 包含“访问已达上限”：告知用户当日公用额度已用完，并引导其查看页面实时额度 - `code=8888`：通常是 `entid` 或参数错误，重新获取企业主体后再试 - `code=20000` 且无记录：直接说明该企业在该维度下无记录 - `discover` 无匹配：先尝试同义词；仍无结果时，说明当前版本暂不支持该维度 故障排查优先级：本地配置（API Key/额度/网络）→ 服务端状态（entid/无数据）→ 更新 Skill（`openclaw skills update china-company-search-fengniao`）。 ## 快速开始 ```bash # 1. 先按维度发现工具 node scripts/tool.mjs discover "企业股东信息" # 2. 再模糊搜索企业，获取 entid node scripts/tool.mjs call biz_fuzzy_search --params '{"key":"腾讯"}' # 3. 用 entid 查询具体维度 node scripts/tool.mjs call biz_shareholders --params '{"entid":"AerjZTfkSh0"}' ```

path: search_aliases.json
content: {
  "企业查询": ["企业模糊搜索", "企业基本信息"],
  "公司查询": ["企业模糊搜索", "企业基本信息"],
  "查公司": ["企业模糊搜索", "企业基本信息"],
  "companysearch": ["企业模糊搜索", "企业基本信息"],
  "companylookup": ["企业模糊搜索", "企业基本信息"],
  "工商查询": ["企业基本信息", "企业工商变更"],
  "businessregistry": ["企业基本信息", "企业工商变更"],
  "企业信息查询": ["企业基本信息"],
  "企业信用查询": ["企业基本信息", "被执行人", "失信被执行人"],
  "企业风险查询": ["被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "corporaterisk": ["被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "corporateriskscreening": ["被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "企业尽调": ["企业基本信息", "企业股东信息", "企业高级职员", "企业对外投资", "企业工商变更", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "companyduediligence": ["企业基本信息", "企业股东信息", "企业高级职员", "企业对外投资", "企业工商变更", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "kyb": ["企业基本信息", "企业股东信息", "企业高级职员", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "背景调查": ["企业基本信息", "企业股东信息", "企业高级职员", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "companybackground": ["企业基本信息", "企业股东信息", "企业高级职员", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "companybackgroundcheck": ["企业基本信息", "企业股东信息", "企业高级职员", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "对方背景": ["企业基本信息", "企业股东信息", "企业高级职员", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "签合同": ["企业基本信息", "企业股东信息", "企业高级职员", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "供应商": ["企业基本信息", "企业股东信息", "企业高级职员", "企业对外投资", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "supplierverification": ["企业基本信息", "企业股东信息", "企业高级职员", "企业对外投资", "被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "counterpartyrisk": ["被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "风险筛查": ["被执行人", "失信被执行人", "限制高消费", "经营异常", "严重违法", "行政处罚"],
  "面试前": ["企业基本信息", "企业高级职员"],
  "目标公司": ["企业基本信息", "企业高级职员", "企业股东信息"],
  "查老板": ["企业高级职员", "企业基本信息"],
  "老板": ["企业高级职员", "企业基本信息"],
  "查法人": ["企业基本信息", "企业高级职员"],
  "legalrepresentative": ["企业基本信息", "企业高级职员"],
  "注册号": ["企业基本信息"],
  "工商注册号": ["企业基本信息"],
  "统一社会信用代码": ["企业基本信息"],
  "信用代码": ["企业基本信息"],
  "法人": ["企业基本信息"],
  "法定代表人": ["企业基本信息"],
  "营业期限": ["企业基本信息"],
  "官网": ["企业基本信息"],
  "电话": ["企业基本信息"],
  "邮箱": ["企业基本信息"],
  "股权结构": ["企业股东信息"],
  "股东结构": ["企业股东信息"],
  "董监高": ["企业高级职员"],
  "高管": ["企业高级职员"],
  "对外投资": ["企业对外投资"],
  "工商变更": ["企业工商变更"],
  "法人变更": ["企业工商变更"],
  "地址变更": ["企业工商变更"],
  "被执行": ["被执行人"],
  "执行标的": ["被执行人"],
  "老赖": ["失信被执行人"],
  "失信": ["失信被执行人"],
  "限消": ["限制高消费"],
  "限高": ["限制高消费"],
  "经营异常": ["经营异常"],
  "严重违法": ["严重违法"],
  "行政处罚": ["行政处罚"],
  "处罚机关": ["行政处罚"]
}


path: SETUP.md
content: # 风鸟企业查询 Skill 本地验证说明

## 适用范围

本文只用于本地验证当前 skill，不要求修改用户主目录、shell 启动文件或 agent 全局配置。

## 前置条件

- Node.js 18 或以上版本
- 一个可用的 `FN_API_KEY`

验证 Node.js 版本：

```bash
node -v
```

## 获取 API Key

请访问静态页面 `https://www.riskbird.com/skills` 获取公用 API Key。

- 进入页面后，在页面中部找到“公用 API Key”展示区，点击区块内的复制按钮
- 复制当前页面展示的可用 Key
- 公用 Key 与额度说明以页面当前展示为准
- 左上角“美亚风鸟” Logo 可点击跳转风鸟官网

## API 额度限制

- 公用 API Key 目前有每日使用上限，具体上限与剩余额度以 `https://www.riskbird.com/skills` 页面实时展示为准
- 当接口返回 `code=9999` 且 `msg` 包含“访问已达上限”时，表示当日额度已用完，不是本地配置错误
- 额度查看方式：访问页面中部“公用 API Key”展示区查看实时剩余额度

## 临时设置 API Key

只在当前终端会话中设置，关闭终端后失效。

**macOS / Linux**

```bash
export FN_API_KEY="你的API Key"
```

**Windows PowerShell**

```powershell
$env:FN_API_KEY = "你的API Key"
```

**Windows CMD**

```cmd
set FN_API_KEY=你的API Key
```

## 校验环境变量

建议在调用查询前先确认当前会话已经能读到 `FN_API_KEY`。

**macOS / Linux**

```bash
printenv FN_API_KEY
```

**Windows PowerShell**

```powershell
$env:FN_API_KEY
```

也可以使用更显式的写法：

```powershell
Get-Item Env:FN_API_KEY -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value
```

如果你是在另一个 PowerShell 进程里执行校验命令，优先使用单引号包裹命令，避免外层 shell 提前展开变量：

```powershell
powershell -Command '$env:FN_API_KEY'
```

不要把下面这类命令的报错直接当成“环境变量未配置”的证据：

```powershell
powershell -Command "echo $env:FN_API_KEY"
```

原因是外层 shell 可能先展开 `$env:FN_API_KEY`；当它为空时，传给内层 PowerShell 的就只剩 `echo`，从而触发 `Write-Output` 缺少 `InputObject` 的误报。

## 本地验证命令

```bash
# 1. 测试工具发现（无需联网）
node scripts/tool.mjs discover "企业基本信息"

# 2. 测试模糊搜索（需要有效 API Key）
node scripts/tool.mjs call biz_fuzzy_search --params '{"key":"腾讯"}'

# 3. 测试维度查询（用上一步返回的 entid）
node scripts/tool.mjs call biz_basic_info --params '{"entid":"AerjZTfkSh0"}'
```

## 当前发布包的安全边界

- 只从环境变量 `FN_API_KEY` 读取凭证
- 只读取 skill 包内的 `tools.json` 和 `references/` 文件
- 不读取用户主目录中的 agent 配置或 shell 启动配置
- 不写入本地文件
- API 凭证读取自环境变量，但实际调用时通过 URL 参数 `apikey` 发送，不通过 HTTP 请求头发送

## 生产环境说明

公开发布包固定使用生产地址 `https://m.riskbird.com/prod-qbb-api`。

如果你有自有付费账号并需要切换生产地址，请在私有 fork 中审阅并修改源码后自行使用，不建议在公开市场包中暴露可切换端点。


path: tools.json
content: [{"tool_id":"biz_fuzzy_search","name":"企业模糊搜索","description":"根据关键词模糊搜索匹配企业，返回 entid（企业唯一标识）、企业全称、经营状态。必须先调此接口获取 entid，才能查询其他维度。注意：英文搜索无效，必须传中文名称","endpoint":"/skills/searchHint","method":"GET","params":[{"name":"key","type":"string","required":true,"description":"搜索关键词，必须是中文，如企业简称或全称"}],"keywords":["企业搜索","公司搜索","主体搜索","企业查找","公司查找","简称查全称","企业匹配","公司全称","entid"]},{"tool_id":"biz_basic_info","name":"企业基本信息","description":"查询企业工商基本信息，包含注册资本、法定代表人、成立日期、注册地址、经营范围、经营状态、行业分类、统一社会信用代码","endpoint":"/skills/dataDimension?version=B1","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["基本信息","工商信息","工商基本信息","工商登记","注册信息","注册号","工商注册号","统一社会信用代码","信用代码","法定代表人","法人","成立日期","注册地址","地址","经营范围","经营状态","行业分类","注册资本","官网","电话","邮箱","曾用名","企业简介","营业期限","工商注册信息","信用信息","登记机关"]},{"tool_id":"biz_shareholders","name":"企业股东信息","description":"查询企业股东名单、持股比例、认缴出资额、股东类型（自然人/法人）、认缴日期、出资方式","endpoint":"/skills/dataDimension?version=B2","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["股东","股东信息","股权结构","股东结构","持股比例","出资比例","认缴出资","股东名单","投资人","股东持股","股权比例","认缴金额"]},{"tool_id":"biz_executives","name":"企业高级职员","description":"查询企业董事、监事、高管及法定代表人，包含姓名、职务（可多职务）、持股比例","endpoint":"/skills/dataDimension?version=B3","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["主要人员","高管","董监高","董事","监事","高级职员","法定代表人","管理层","核心人员","董监高信息","管理人员","企业高管"]},{"tool_id":"biz_investments","name":"企业对外投资","description":"查询企业对外投资的被投公司列表，包含持股比例、被投企业经营状态、成立日期、注册资本、法定代表人","endpoint":"/skills/dataDimension?version=B4","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["对外投资","投资关系","被投企业","参股公司","投资企业","投资版图","对外参股","投资公司","被投公司"]},{"tool_id":"biz_changes","name":"企业工商变更","description":"查询企业工商登记变更历史，包含变更日期、变更事项（法人变更/地址变更/注册资本变更等）、变更前后内容","endpoint":"/skills/dataDimension?version=B5","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["工商变更","变更记录","变更历史","法人变更","地址变更","注册资本变更","工商变更记录","变更事项","变更前后","历史变更"]},{"tool_id":"risk_executed","name":"被执行人","description":"查询企业作为被执行人的法院强制执行记录，包含案号、执行法院、立案时间、执行标的金额（单位：万元）、案件状态","endpoint":"/skills/dataDimension?version=C2","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["被执行","被执行人","执行记录","强制执行","执行标的","法院执行","案号","执行法院","立案时间"]},{"tool_id":"risk_dishonest","name":"失信被执行人","description":"查询企业被列入失信被执行人（老赖）名单的记录，包含失信行为类型、履行情况、列入日期、执行法院、是否已下架","endpoint":"/skills/dataDimension?version=C3","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["失信","失信被执行人","老赖","失信记录","失信名单","履行情况"]},{"tool_id":"risk_limit_consumption","name":"限制高消费","description":"查询企业被法院限制高消费记录，包含案号、作出限制的法院、立案日期、申请人列表、关联法定代表人","endpoint":"/skills/dataDimension?version=C4","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["限高","限制高消费","高消费限制","限消","限高记录","限制消费"]},{"tool_id":"risk_abnormal_operation","name":"经营异常","description":"查询企业被列入经营异常名录的记录，包含列入原因、列入日期、列入机关、移出原因、移出日期","endpoint":"/skills/dataDimension?version=D1","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["经营异常","异常经营","异常名录","经营异常名录","列入异常","移出异常"]},{"tool_id":"risk_serious_illegal","name":"严重违法","description":"查询企业被列入严重违法失信名单的记录，包含列入原因、列入日期、移出情况","endpoint":"/skills/dataDimension?version=D2","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["严重违法","严重违法失信","违法失信","黑名单","违法名单","严重失信"]},{"tool_id":"risk_admin_penalty","name":"行政处罚","description":"查询企业受到的行政处罚记录，包含处罚机关、处罚日期、违法事实、处罚金额（单位：元，展示时÷10000转万元）、处罚依据","endpoint":"/skills/dataDimension?version=D11","method":"GET","params":[{"name":"entid","type":"string","required":true,"description":"企业唯一标识，通过 biz_fuzzy_search 获取"}],"keywords":["行政处罚","处罚记录","罚款","处罚机关","违法事实","处罚决定书","处罚文号","处罚日期"]}]

path: scripts/client.mjs
content: import { getApiKey, BASE_URL } from "./env.mjs";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

// 加载工具注册表（tools.json 与 scripts/ 同级上一层）
const __dir = dirname(fileURLToPath(import.meta.url));
const TOOLS = JSON.parse(
  readFileSync(join(__dir, "../tools.json"), "utf8")
);
const SEMANTIC_QUERY_ALIASES = JSON.parse(
  readFileSync(join(__dir, "../search_aliases.json"), "utf8")
);

function normalizeSearchText(input) {
  return String(input || "")
    .toLowerCase()
    .replace(/[\s"'“”‘’，。、《》【】（）()、:：;；!?！？-]+/g, "")
    .trim();
}

function buildQueryVariants(input) {
  const normalized = normalizeSearchText(input);
  const variants = new Set([normalized]);

  if (normalized.startsWith("企业")) {
    variants.add(normalized.slice(2));
  }
  if (normalized.startsWith("公司")) {
    variants.add(normalized.slice(2));
  }

  for (const variant of [...variants]) {
    if (variant.endsWith("查询")) {
      variants.add(variant.slice(0, -2));
    }
    if (variant.endsWith("信息查询")) {
      variants.add(variant.slice(0, -4));
    }
    for (const [alias, mappedVariants] of Object.entries(SEMANTIC_QUERY_ALIASES)) {
      if (variant.includes(alias)) {
        mappedVariants.forEach((mapped) => variants.add(normalizeSearchText(mapped)));
      }
    }
  }

  return [...variants].filter((item) => item.length >= 2);
}

function buildSearchTerms(input) {
  const text = normalizeSearchText(input);
  const terms = new Set();

  for (const asciiWord of text.match(/[a-z0-9]+/g) || []) {
    terms.add(asciiWord);
  }

  for (const segment of text.match(/[\u4e00-\u9fa5]+/g) || []) {
    for (let size = 2; size <= Math.min(segment.length, 4); size++) {
      for (let i = 0; i <= segment.length - size; i++) {
        const term = segment.slice(i, i + size);
        if (!["企业", "公司", "信息", "查询"].includes(term)) {
          terms.add(term);
        }
      }
    }
  }

  return [...terms];
}

/** 本地关键词匹配，替代后端 /skill/match */
export function discover(query, { limit = 5 } = {}) {
  const queryVariants = buildQueryVariants(query);
  const queryTerms = buildSearchTerms(query);
  const scored = TOOLS.map((tool) => {
    const normalizedName = normalizeSearchText(tool.name);
    const normalizedDescription = normalizeSearchText(tool.description || "");
    const normalizedKeywords = (tool.keywords || [])
      .map((keyword) => normalizeSearchText(keyword))
      .filter(Boolean);
    const normalizedText = normalizeSearchText(
      [tool.name, tool.description, ...(tool.keywords || [])].filter(Boolean).join(" ")
    );

    let similarity = 0;
    for (const variant of queryVariants) {
      if (!variant) continue;
      if (normalizedName === variant) {
        similarity = Math.max(similarity, 1);
      } else if (normalizedKeywords.includes(variant)) {
        similarity = Math.max(similarity, 0.98);
      } else if (normalizedName.includes(variant)) {
        similarity = Math.max(similarity, 0.95);
      } else if (normalizedKeywords.some((keyword) => keyword.includes(variant) || variant.includes(keyword))) {
        similarity = Math.max(similarity, 0.9);
      } else if (normalizedDescription.includes(variant) || normalizedText.includes(variant)) {
        similarity = Math.max(similarity, 0.85);
      }
    }

    if (similarity < 0.85) {
      const hits = queryTerms.filter(
        (term) => normalizedText.includes(term) || normalizedKeywords.some((keyword) => keyword.includes(term))
      );
      const uniqueHits = new Set(hits);
      const overlap =
        queryTerms.length === 0 || uniqueHits.size === 0
          ? 0
          : Math.min(uniqueHits.size / Math.max(Math.min(queryTerms.length, 5), 1), 0.84);
      similarity = Math.max(similarity, overlap);
    }

    return { similarity, tool };
  });

  scored.sort((a, b) => b.similarity - a.similarity);

  return {
    tools: scored
      .filter((s) => s.similarity > 0)
      .slice(0, limit)
      .map(({ similarity, tool }) => ({
        tool_id: tool.tool_id,
        name: tool.name,
        description: tool.description,
        similarity: parseFloat(similarity.toFixed(4)),
        params: tool.params,
      })),
  };
}

/** 直接调用真实 API，替代后端 /gateway/{tool_id} */
export async function call(toolId, params = {}) {
  const apiKey = await getApiKey();
  if (!apiKey) throw new Error("API Key 未配置，请先访问 https://www.riskbird.com/skills，通过页面中部复制按钮获取当前页面展示的公用 Key，并设置环境变量 FN_API_KEY");

  const tool = TOOLS.find((t) => t.tool_id === toolId);
  if (!tool) throw new Error(`未找到工具: ${toolId}`);

  const url = new URL(BASE_URL + tool.endpoint);
  // 风鸟接口实际要求通过 URL 参数 apikey 传递鉴权信息，而不是请求头。
  url.searchParams.set("apikey", apiKey);

  const options = {
    method: tool.method,
    headers: {
      "Accept": "application/json",
    },
  };

  if (tool.method === "GET") {
    Object.entries(params).forEach(([k, v]) => {
      if (v != null) url.searchParams.set(k, v);
    });
  } else {
    options.headers["Content-Type"] = "application/json";
    options.body = JSON.stringify(params);
  }

  const res = await fetch(url.toString(), options);
  const body = await res.json().catch(() => ({ error: "non-JSON response" }));

  if (!res.ok) {
    throw new Error(`API 错误 ${res.status}: ${JSON.stringify(body)}`);
  }

  // 直接透传原始响应，结构为 { code, msg, data, success }
  return body;
}


path: scripts/env.mjs
content: const ENV_KEY = "FN_API_KEY";
// 公用账号 Key，每日限额 200 次；用户配置了自己的 Key 时优先使用
const BUILTIN_KEY = "eab076c5-b108-4a3f-b2fb-d97039b1a447";
export const BASE_URL = "https://m.riskbird.com/prod-qbb-api";

export async function getApiKey() {
  const key = process.env[ENV_KEY];
  if (key && key !== "YOUR_API_KEY") return key;
  return BUILTIN_KEY;
}


path: scripts/tool.mjs
content: #!/usr/bin/env node
// tool.mjs - CLI entrypoint for china-company-search-fengniao skill
import { discover, call } from "./client.mjs";

const [, , command, ...args] = process.argv;

function printHelp() {
  console.log(`
Fengniao Search Tool CLI

Commands:
  discover <query> [--limit N]
      Find tools matching a natural-language query.
      Returns tool candidates and similarity scores.

  call <tool_id> [--params '{"key":"value"}'] [--params "key=value&key2=value2"]
      Call a tool by its ID with optional params.
      --params '{"key":"value"}' accepts JSON or key=value pairs joined by , or &
      --params "key=value&key2=value2" can be repeated and is recommended on Windows.

Options:
  --help    Show this help message

Examples:
  node tool.mjs discover "企业股东信息" --limit 3
  node tool.mjs call biz_fuzzy_search --params '{"key":"腾讯"}'
  node tool.mjs call biz_shareholders --params "entid=AerjZTfkSh0"
`);
}

function isPlainObject(value) {
  return value != null && typeof value === "object" && !Array.isArray(value);
}

function parseScalar(value) {
  const raw = value.trim();
  if (raw === "true") return true;
  if (raw === "false") return false;
  if (raw === "null") return null;
  if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(raw)) return Number(raw);

  if (
    (raw.startsWith("{") && raw.endsWith("}")) ||
    (raw.startsWith("[") && raw.endsWith("]")) ||
    (raw.startsWith('"') && raw.endsWith('"'))
  ) {
    try {
      return JSON.parse(raw);
    } catch {
      // Keep the raw string when partial JSON is provided.
    }
  }

  return raw;
}

function parseKeyValueParams(raw) {
  const entries = raw
    .split(/[&,]/)
    .map((entry) => entry.trim())
    .filter(Boolean);

  if (entries.length === 0) {
    throw new Error("empty params");
  }

  const params = {};
  for (const entry of entries) {
    const eqIndex = entry.indexOf("=");
    if (eqIndex <= 0) {
      throw new Error(`invalid param entry: ${entry}`);
    }
    const key = entry.slice(0, eqIndex).trim();
    const value = entry.slice(eqIndex + 1);
    params[key] = parseScalar(value);
  }

  return params;
}

function parseParams(raw) {
  try {
    const parsed = JSON.parse(raw);
    if (!isPlainObject(parsed)) {
      throw new Error("params JSON must be an object");
    }
    return parsed;
  } catch {
    try {
      return parseKeyValueParams(raw);
    } catch {
      throw new Error(
        '--params must be valid JSON or key=value&key2=value2 pairs such as entid=AerjZTfkSh0'
      );
    }
  }
}

function parseFlags(args) {
  const flags = {};
  for (let i = 0; i < args.length; i++) {
    if (args[i] === "--limit" && args[i + 1]) {
      flags.limit = parseInt(args[++i], 10);
    } else if (args[i] === "--params" && args[i + 1]) {
      try {
        flags.params = {
          ...(flags.params || {}),
          ...parseParams(args[++i]),
        };
      } catch (err) {
        console.error(`Error: ${err.message}`);
        process.exit(1);
      }
    } else if (args[i] === "--param" && args[i + 1]) {
      try {
        flags.params = {
          ...(flags.params || {}),
          ...parseKeyValueParams(args[++i]),
        };
      } catch {
        console.error(
          `Error: --param must use key=value format, received: ${args[i]}`
        );
        process.exit(1);
      }
    }
  }
  return flags;
}

async function main() {
  if (!command || command === "--help") {
    printHelp();
    process.exit(0);
  }

  try {
    if (command === "discover") {
      const query = args[0];
      if (!query) {
        console.error("Error: discover requires a query string");
        process.exit(1);
      }
      const flags = parseFlags(args.slice(1));
      const result = await discover(query, flags);
      console.log(JSON.stringify(result, null, 2));

    } else if (command === "call") {
      const toolId = args[0];
      if (!toolId) {
        console.error("Error: call requires a tool_id");
        process.exit(1);
      }
      const flags = parseFlags(args.slice(1));
      const result = await call(toolId, flags.params || {});
      console.log(JSON.stringify(result, null, 2));

    } else {
      console.error(`Unknown command: ${command}`);
      printHelp();
      process.exit(1);
    }
  } catch (err) {
    console.error("Error:", err.message);
    process.exit(1);
  }
}

main();


path: references/due_diligence_report.md
content: # 企业尽调报告生成指南

当用户明确要求生成企业尽调报告（如"帮我出一份XX的尽调报告"、"生成企业背景报告"）时，按以下规范执行。尽调报告是当前已提供的一种报告模板，识别到该意图后可直接进入报告流程；未来若新增其他报告模板，应与本文件并列维护。

## 查询维度与顺序

依次查询以下维度（先确认主体，再按固定支持维度执行；如工具名或 tool_id 不确定，再先 discover 后 call，统一使用 entid）：

1. 企业模糊搜索 → 确认 entid
2. 企业基本信息（B1）→ 工商注册核心字段
3. 股东信息（B2）→ 穿透股东及持股比例
4. 主要人员（B3）→ 法人、董监高姓名职务
5. 对外投资（B4）→ 被投企业列表
6. 变更记录（B5）→ 近期重大工商变更
7. 经营异常（D1）→ 是否列入异常名录
8. 行政处罚（D11）→ 处罚条数及详情
9. 被执行人（C2）→ 法院强制执行记录
10. 失信被执行人（C3）→ 老赖记录
11. 限制高消费（C4）→ 限高记录

## 报告输出格式

严格按以下模板输出，不要自由发挥结构：

```
# 企业尽调报告｜{企业简称}

> 查询日期：{YYYY年MM月DD日}
> 企业全称：{entName}
> 统一社会信用代码：{uniscid}

---

## ⚡ 风险评级：{低风险 / 需关注 / 高风险}

{1-2句综合判断，说明评级依据}

---

## 一、基本信息

| 项目 | 内容 |
|------|------|
| 法定代表人 | |
| 注册资本 | |
| 成立日期 | |
| 经营状态 | |
| 注册地址 | |
| 所属行业 | |

---

## 二、股权结构

{股东列表，按持股比例从高到低，标注自然人/法人}

---

## 三、主要人员

| 姓名 | 职务 |
|------|------|

---

## 四、对外投资

{有投资则列表，含被投企业名称/持股比例/经营状态；无则写"未查询到对外投资记录"}

---

## 五、近期变更

{最近3条变更记录，含变更事项/变更前后内容/变更日期；无则写"近期无重大工商变更"}

---

## 六、风险信息

### 经营异常
{条数} 条｜{有记录则列出列入原因和日期；无则写"未列入经营异常名录"}

### 行政处罚
{条数} 条｜{按风险条数表述规则给出风险提示}
{最新5条：日期 / 处罚机关 / 违法类型 / 处罚结果}

### 被执行记录
{条数} 条｜{有则列出案号/法院/立案日期/标的金额（万元）；无则写"无被执行记录"}

### 失信记录
{条数} 条｜{有则列出详情；无则写"未被列入失信被执行人名单"}

### 限制高消费
{条数} 条｜{有则列出案号/法院/申请人；无则写"无限高记录"}

---

## 七、综合评估

{3-5句话，综合上述所有维度，指出主要风险点或亮点，给出明确的合作建议}

---

📌 以上数据来源于风鸟企业信息查询平台，当前数据维度持续拓展中，更多企业信息及功能详见风鸟官网：https://www.riskbird.com/
```

## 注意事项

- 风险评级必须在报告最顶部，用户拿到报告第一眼看结论
- 所有"无记录"情况必须明确写出，不能留空——空白让读者无法判断是没查还是没有
- 对外投资超过5家时，只展示前5家并注明"共{N}家，展示前5家"
- 变更记录只展示最近1年内的，更早的忽略（API 不支持时间过滤，由 Claude 根据返回数据中的变更日期自行判断过滤）
- 报告生成完毕后不需要额外解释，直接输出完整报告


path: references/field_definitions_common_bizinfo.md
content: # 风鸟 API 字段定义 — 通用结构 & 工商维度（searchHint / B1–B5）

> 版本：第一版 | 基于 API 实测验证 | 最后更新：2026-03-25

---

## 通用响应结构

### 响应信封（所有接口共用）

| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | number | 状态码。`20000`=成功，`3000000`=无数据，`8888`=业务参数错误，`9999`=系统错误 |
| `msg` | string | 状态消息 |
| `success` | boolean | 是否成功 |
| `data` | object/array/null | 业务数据，结构因接口而异 |

### 三种 data 形态

**① searchHint** — `data` 直接是数组：
```json
{ "code": 20000, "data": [ { "entid": "...", "entName": "..." } ] }
```

**② dataDimension 列表类（B2/B3/B4/B5/C2/C3/C4/D1/D11）** — `data` 是分页对象：
```json
{
  "data": {
    "totalCount": 140,
    "exportCount": 140,
    "filterData": {},
    "aggregation": null,
    "extraInfo": {},
    "apiData": [ ... ]
  }
}
```

**③ dataDimension 单体类（B1）** — `apiData` 是对象而非数组：
```json
{
  "data": {
    "totalCount": 0,
    "apiData": { "entName": "...", "uniscid": "..." }
  }
}
```

### 分页包装字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `totalCount` | number | 符合条件的总记录数。**判断"是否有风险"只看此值**，不看 `apiData` 长度。B1 固定为 `0`（单体查询无分页概念） |
| `exportCount` | number | 可导出数量，通常等于 `totalCount` |
| `filterData` | object/null | 过滤条件。列表类为 `{}`，B1 为 `null` |
| `aggregation` | null | 聚合数据，当前固定为 `null` |
| `extraInfo` | object/null | 额外信息。列表类为 `{}`，B1 为 `null` |
| `apiData` | array/object | 数据本体。列表类为数组（**最多 5 条**，分页参数无效），B1 为单个对象 |

> ⚠️ **D2 严重违法例外**：数据位于 `data.SERILLEGAL` 数组，而非 `data.apiData`。

---

## searchHint — 模糊搜索

`GET /skills/searchHint?key=<搜索词>&[REDACTED_SECRET]`（API Key 通过 URL 参数 `apikey` 传递）

返回：`data` 为数组，最多 5 条，按相关度排序，不支持分页。

| 字段 | 类型 | 说明 |
|------|------|------|
| `entid` | string | **企业唯一标识**，后续所有维度查询的必传参数 |
| `entName` / `ENTNAME` | string | 企业全称 |
| `entStatus` / `ENTSTATUS` | string | 经营状态（在营 / 注销 / 吊销等） |
| `entStatusColor` | string | 状态颜色标记（`green`=在营，`red`=异常） |
| `entPic` | string | 企业 Logo 图片 URL |
| `ENTNAME_SEARCH` | string | 带高亮 `<span>` 标签的企业名称（仅用于前端展示，**禁止用于逻辑处理**） |
| `highlightNameType` | string | 命中字段类型。`"公司名称"`=精准匹配优先级最高；`"股东"`=匹配的是股东名；`"企业别名"` / `"统一社会信用代码"` 等 |
| `dataType` | string | 固定值 `"company_basic"` |

---

## B1 工商-基本信息

`version=B1` · `apiData` 为**对象**（非数组） · `totalCount` 固定为 `0`

### 核心工商字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `entName` | string | 企业全称 |
| `entid` | string | 企业唯一标识 |
| `uniscid` | string | 统一社会信用代码 |
| `regNo` | string | 工商注册号 |
| `entStatus` | string | 经营状态（在营 / 注销 / 吊销等） |
| `entType` | string | 企业类型（如"有限责任公司"） |
| `regCap` | string | 注册资本金额（万元，不含单位） |
| `regConcat` | string | 注册资本（含货币单位，如"6500 万元人民币"） |
| `esDate` | string | 成立日期（`YYYY-MM-DD`） |
| `opFrom` | string | 经营开始日期 |
| `opTo` | string | 经营截止日期（"长期"表示长期有效） |
| `candate` | string/null | 注销日期（未注销为 `null`） |
| `revdate` | string/null | 吊销日期（未吊销为 `null`） |
| `dom` | string | 注册地址 |
| `regionName` | string | 注册地区（省市区，如"广东省深圳市南山区"） |
| `regOrg` | string | 登记机关 |
| `nicName` | string | 行业分类全称（国标分类，含上级分类） |
| `opScope` | string | 经营范围（含一般经营和许可经营，以"，许可经营项目是："分隔） |

### 法定代表人字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `personId` | string | 法定代表人 ID |
| `personName` | string | 法定代表人姓名 |
| `personTypeCode` | string | 法人类型编码（`"person"` = 自然人） |
| `personEntCount` | number | 法人关联企业数量 |
| `legalPersonType` | string | 法人身份名称（如"法定代表人"） |

### 其他信息字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `entNameEn` | string | 企业英文名称 |
| `historyName` | string | 企业曾用名 |
| `entIntro` | string | 企业简介 |
| `website` | string | 企业官网 |
| `isBranch` | number | 是否分支机构（`0`=主体，`1`=分支） |
| `tags` | array | 企业标签（如 `["大型","500强","国家高新技术企业"]`） |
| `riskTags` | array/null | 风险标签（如 `["被执行人"]`，无风险时为 `null` 或空） |
| `yrEnpNum` | string | 年报员工人数 |
| `tel` | string | 联系电话（第一个） |
| `email` | string | 联系邮箱（第一个） |

> 说明：官网、电话、邮箱属于 **B1 企业基本信息** 中的基础字段，不是单独的独立维度。用户询问这些基础联系方式时，应先按“企业基本信息”维度处理，再从 B1 返回结果中提取对应字段。

---

## B2 工商-股东信息

`version=B2` · `apiData` 为数组，每条为一个股东记录

| 字段 | 类型 | 说明 |
|------|------|------|
| `shaId` | string | 股东 ID |
| `shaName` | string | 股东姓名 / 名称 |
| `shaTypeCode` | string | 股东类型编码（`"person"`=自然人，`"ent"`=法人） |
| `invType` | string | 投资类型（`"自然人股东"` / `"法人股东"`） |
| `fundedRatio` | string | 持股比例（含 `%`，如 `"54.2857%"`） |
| `subConAm` | string | 认缴出资额（含单位，如 `"3,528.5705 万元人民币"`） |
| `conDate` | string | 认缴日期（`YYYY-MM-DD`） |
| `outState` | boolean | 是否退出（`false`=未退出） |
| `riskTags` | array | 股东风险标签 |

---

## B3 工商-高级职员

`version=B3` · `apiData` 为数组，包含董监高及法定代表人

| 字段 | 类型 | 说明 |
|------|------|------|
| `personId` | string | 人员 ID |
| `personName` | string | 姓名 |
| `position` | string | 职务（多个职务以 `";"` 分隔，如 `"董事长;总经理"`） |
| `fundedRatio` | string | 持股比例（无持股则为 `"-"`） |
| `riskTags` | array | 人员风险标签 |

---

## B4 工商-对外投资

`version=B4` · `apiData` 为数组，每条为一个被投企业

| 字段 | 类型 | 说明 |
|------|------|------|
| `entid` | string | 被投企业 ID |
| `entName` | string | 被投企业名称 |
| `entStatus` | string | 被投企业经营状态 |
| `funderRatio` | string | 持股比例（含 `%`） |
| `subConAm` | string | 认缴出资额（含单位） |
| `esDate` | string | 被投企业成立日期（`YYYY-MM-DD`） |
| `personName` | string | 被投企业法定代表人姓名 |

---

## B5 工商-工商变更信息

`version=B5` · `apiData` 为数组，按变更时间倒序

| 字段 | 类型 | 说明 |
|------|------|------|
| `altDate` | string | 变更日期（`YYYY-MM-DD`） |
| `altItemCodeCn` | string | 变更事项名称（中文） |
| `altAf` | string | 变更后内容（纯文本，**用于逻辑处理**） |
| `altBe` | string | 变更前内容（纯文本，**用于逻辑处理**） |
| `altAfClean` | string | 变更后内容（含高亮 HTML 标签，**仅用于前端展示，禁止用于逻辑处理**） |
| `altBeClean` | string | 变更前内容（含高亮 HTML 标签，**仅用于前端展示，禁止用于逻辑处理**） |


path: references/field_definitions_legal.md
content: # 风鸟 API 字段定义 — 法律诉讼维度（C2 / C3 / C4）

> 版本：第一版 | 基于 API 实测验证 | 最后更新：2026-03-25

---

## C2 法律诉讼-被执行人

`version=C2` · `apiData` 为数组

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 记录 ID |
| `entityName` | string | 被执行人名称 |
| `entid` | string | 企业 ID |
| `courtName` | string | 执行法院名称 |
| `caseCode` | string | 案号 |
| `caseState` | string | 案件状态（如 `"执行中"`） |
| `execMoney` | string/null | 执行标的金额（⚠️ 单位：**万元**，可为 `null`） |
| `execMoneyFormat` | string/null | 格式化金额（千位分隔，可为 `null`） |
| `regDate` | string | 立案日期（`YYYY-MM-DD`） |
| `gistId` | string/null | 执行依据文号（可为 `null`） |
| `caseUniqueId` | string | 案件唯一标识（系统内部去重用） |

---

## C3 法律诉讼-失信被执行人

`version=C3` · `apiData` 为数组

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 记录 ID |
| `entid` | string | 企业 ID |
| `entityName` | string | 失信被执行人名称 |
| `caseCode` | string | 案号 |
| `courtName` | string | 执行法院名称 |
| `gistId` | string | 执行依据文号（生效法律文书号） |
| `gistUnit` | string | 作出执行依据的单位（法院名称） |
| `regDate` | string | 立案日期（`YYYY-MM-DD`） |
| `publishDate` | string | 数据发布日期（`YYYY-MM-DD`） |
| `duty` | string | 被执行人义务（判决内容摘要，部分为 `"."` 或 `"="` 占位） |
| `performance` | string | 履行情况（如 `"全部未履行"`） |
| `performedPart` | string | 已履行部分说明 |
| `unperformedPart` | string | 未履行部分说明 |
| `disruptTypeName` | string | 失信行为类型（如 `"被执行人无正当理由拒不履行执行和解协议"`） |
| `partyTypeName` | string | 当事人类型（`"法人"` / `"自然人"`） |
| `status` | string | 记录状态（`"未下架"` 表示仍在失信名单上） |
| `caseUniqueId` | string | 案件唯一标识 |

---

## C4 法律诉讼-限制高消费

`version=C4` · `apiData` 为数组

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 记录 ID |
| `entid` | string | 企业 ID |
| `entName` | string | 被限制企业名称 |
| `caseCode` | string | 案号 |
| `court` | string | 作出限制的法院名称 |
| `regDate` | string | 立案日期（`YYYY-MM-DD`） |
| `publishDate` | string | 数据发布日期（`YYYY-MM-DD`） |
| `pdfUrl` | string | 限高令原文 PDF 链接 |
| `applicationList` | array | 申请人列表，每项含 `entityId`、`entityName`、`entityType`、`entityTypeCode` |
| `relatedList` | array | 关联人列表（通常为法定代表人），字段同 `applicationList` |
| `caseUniqueId` | string | 案件唯一标识 |


path: references/field_definitions_risk.md
content: # 风鸟 API 字段定义 — 经营风险维度（D1 / D2 / D11）& 注意事项

> 版本：第一版 | 基于 API 实测验证 | 最后更新：2026-03-25

---

## D1 经营风险-经营异常

`version=D1` · `apiData` 为数组

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | number | 记录 ID |
| `entname` | string/null | 企业名称（企业返回名称，个体户可能返回注册号，部分返回 `null`） |
| `regno` | string | 注册号 |
| `uniscid` | string | 统一社会信用代码（部分主体为空字符串） |
| `inDate` | string | 列入日期（`YYYY-MM-DD`） |
| `inReason` | string | 列入原因 |
| `inRegOrg` | string | 列入机关 |
| `outDate` | string/null | 移出日期（`YYYY-MM-DD`；`null` 表示尚未移出） |
| `outReason` | string | 移出原因（空字符串表示尚未移出） |
| `outRegOrg` | string | 移出机关（空字符串表示尚未移出） |
| `outState` | boolean | ⚠️ 移出状态（`false`=**仍在名单**，`true`=已移出）。含义与字段名相反，注意区分 |

> ⚠️ `totalCount` 是列入总数（含已移出记录）。若需统计"当前仍在名单"的记录数，需过滤 `outState=false` 的条目。

---

## D2 经营风险-严重违法

`version=D2`

> ⚠️ **结构差异**：数据位于 `data.SERILLEGAL` 数组，**不是** `data.apiData`。字段命名为全小写，与 D1 的 camelCase 不同。

| 字段 | 类型 | 说明 |
|------|------|------|
| `entname` | string | 企业名称 |
| `regno` | string | 注册号 |
| `uniscid` | string | 统一社会信用代码 |
| `inreason` | string | 列入原因 |
| `indate` | string | 列入日期（`YYYY-MM-DD`） |
| `inorg` | string | 列入机关 |
| `outreason` | string | 移出原因（未移出则为空字符串） |
| `outdate` | string/null | 移出日期（未移出则为 `null`） |
| `outorg` | string | 移出机关（未移出则为空字符串） |

**D2 响应结构示例**：
```json
{
  "data": {
    "totalCount": 8,
    "SERILLEGAL": [
      { "entname": "...", "inreason": "...", "indate": "...", "inorg": "..." }
    ]
  }
}
```

---

## D11 经营风险-行政处罚

`version=D11` · `apiData` 为数组

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 记录 ID |
| `entid` | string | 企业 ID |
| `entName` | string | 企业名称 |
| `docNo` | string | 处罚决定书文号 |
| `penaltyDate` | string | 处罚日期（`YYYY-MM-DD`） |
| `publishDate` | string | 公示日期（`YYYY-MM-DD`） |
| `penaltyOrg` | string | 处罚机关名称 |
| `illegalFact` | string | 违法事实描述（详细） |
| `illegalType` | string | 违法类型分类（如 `"文旅广告"`、`"交通运输"`、`"市场监管"`） |
| `penaltyAm` | string/null | 罚款金额（⚠️ 单位：**元**，如 `"10000.00"`；无罚款时为 `null`） |
| `penaltyResult` | string | 处罚结果完整描述 |
| `penaltyBasis` | string | 处罚依据（法律法规条款） |

---

## 关键注意事项速查

| 注意项 | 说明 |
|--------|------|
| `entid` 是核心标识 | 所有维度查询必须用 `entid`，不支持传企业名称或信用代码 |
| `penaltyAm` 单位是**元** | D11 展示时需 ÷ 10000 换算为万元 |
| `execMoney` 单位是**万元** | C2 直接使用，无需换算。与 D11 单位不同，注意区分 |
| `totalCount=0` 含义 | B1 固定返回 `0`（单体数据无分页概念）；其他维度为 `0` 表示无记录 |
| `altAfClean` / `altBeClean` | 含 HTML `<span>` 标签，仅用于前端高亮展示。**逻辑处理必须用 `altAf` / `altBe`** |
| D1 vs D2 字段命名风格 | D1：camelCase（`inDate` / `inReason`）；D2：全小写（`indate` / `inreason`） |
| D2 数据路径不同 | D2 数据在 `data.SERILLEGAL[*]`，其他维度在 `data.apiData[*]` |
| D1 `outState` 语义 | `false`=仍在名单，`true`=已移出；`totalCount` 含已移出记录，需自行过滤 |
| 分页不可用 | `apiData` 始终返回最多前 5 条。`totalCount > 5` 时应告知用户"共 N 条，展示最新 5 条" |
| 英文搜索无效 | searchHint 不支持英文关键词匹配中文企业名称，必须传中文 |

---

## 错误码速查

| code | 含义 | 触发场景 |
|------|------|---------|
| `20000` | 成功 | 正常响应（含空结果） |
| `3000000` | 暂时没有找到相关数据 | searchHint 传空 key |
| `8888` | 业务参数错误 | `entid` 无效、`version` 不存在 |
| `9999` | 系统错误 | 缺少必传参数、鉴权失败，或公用 Key 当日额度已达上限（`msg` 常见为“访问已达上限”） |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)