add
This commit is contained in:
BIN
public/download/hydata-skill.zip
Normal file
BIN
public/download/hydata-skill.zip
Normal file
Binary file not shown.
372
public/download/hydata-skill/SKILL.md
Normal file
372
public/download/hydata-skill/SKILL.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# 海宇数据 API 对接 Skill
|
||||||
|
|
||||||
|
> 海宇数据 (haiyudata.com) 企业级大数据 API 接口对接
|
||||||
|
> 官网: https://www.haiyudata.com/ | 控制台: https://console.haiyudata.com/
|
||||||
|
|
||||||
|
## 核心理解
|
||||||
|
|
||||||
|
**所有接口调用方式完全一致**,区别仅在于:
|
||||||
|
1. **接口编码**(下表第二列)— 决定调用哪个产品
|
||||||
|
2. **入参** — 不同接口需要的参数不同
|
||||||
|
|
||||||
|
## ⚠️ 重要:入参仅为示例!
|
||||||
|
|
||||||
|
下方所有入参中的身份证号、手机号、姓名、车牌号等**均为示例占位值**,调用时**必须替换为真实数据**。
|
||||||
|
|
||||||
|
## 标准入参字段
|
||||||
|
|
||||||
|
所有接口统一使用以下字段名:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id_card` | string | 身份证号 |
|
||||||
|
| `name` | string | 姓名 |
|
||||||
|
| `mobile_no` | string | 手机号 |
|
||||||
|
| `vin` | string | 车架号 |
|
||||||
|
| `plate` | string | 车牌号 |
|
||||||
|
| `image` | string | 图片 URL 或 Base64 |
|
||||||
|
| `bankcard` | string | 银行卡号 |
|
||||||
|
| `ent_code` | string | 企业统一社会信用代码 |
|
||||||
|
| `ent_name` | string | 企业名称 |
|
||||||
|
| `callback_url` | string | 回调地址 |
|
||||||
|
| `token` | string | 任务凭证 |
|
||||||
|
|
||||||
|
> 单人查询传 `id_card` + `name`;双人查询传双方的 `id_card` + `name`;企业查询传 `ent_code` 或 `ent_name`。
|
||||||
|
|
||||||
|
## 调用方式
|
||||||
|
|
||||||
|
### 请求
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://api.haiyudata.com/api/v1/{接口编码}?t={13位时间戳}
|
||||||
|
```
|
||||||
|
|
||||||
|
- URL 中的 `t` 为当前时间的 13 位毫秒级时间戳,需实时生成
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Type: application/json
|
||||||
|
Access-Id: 你的 Access-Id
|
||||||
|
```
|
||||||
|
|
||||||
|
### 请求体(AES 加密)
|
||||||
|
|
||||||
|
业务参数需先 JSON 序列化,再用 **AES-128-CBC** 加密,最后 Base64 编码,放入 `data` 字段:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": "加密后的Base64字符串"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 加密规则
|
||||||
|
|
||||||
|
- 算法:AES-128-CBC
|
||||||
|
- 密钥:账户的 Access Key(16 进制字符串,`bytes.fromhex()` 转为 16 字节密钥)
|
||||||
|
- IV:每次随机生成 16 字节
|
||||||
|
- 填充:PKCS7
|
||||||
|
- 传输格式:`Base64( IV + 密文 )`
|
||||||
|
|
||||||
|
#### 解密规则
|
||||||
|
|
||||||
|
返回的 `data` 字段同样为加密数据:
|
||||||
|
1. Base64 解码
|
||||||
|
2. 前 16 字节为 IV,其余为密文
|
||||||
|
3. AES-128-CBC 解密
|
||||||
|
4. 去除 PKCS7 填充
|
||||||
|
|
||||||
|
### 返回格式
|
||||||
|
|
||||||
|
#### 外层(明文,所有接口统一)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "业务成功",
|
||||||
|
"transaction_id": "平台流水号(海宇计费流水号,重要!)",
|
||||||
|
"data": "加密后的Base64字符串(需解密后才是真正的业务数据)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **`transaction_id` 是海宇数据的计费流水号,每次调用都会返回。**
|
||||||
|
> 如果对某次调用有疑问(如重复扣费、结果异常、数据争议),**凭此 ID 向海宇客服提交查询**。
|
||||||
|
> 请务必在日志中记录每次调用的 `transaction_id`。
|
||||||
|
|
||||||
|
**先看 `code`,只有 `code = 0` 时 `data` 才有意义的业务数据。**
|
||||||
|
|
||||||
|
| code | 含义 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 0 | 业务成功 | data 中为查询结果 |
|
||||||
|
| 1003 | 参数校验不正确 | 入参字段名错误或缺失,检查字段拼写 |
|
||||||
|
| 1004 | 未经授权的IP | 调用方IP未加入白名单,需到控制台添加 |
|
||||||
|
| 1005 | 缺少Access-Id | 请求头未传 Access-Id |
|
||||||
|
|
||||||
|
#### data 解密后(业务数据,因接口而异)
|
||||||
|
|
||||||
|
`data` 字段是 AES 加密的 JSON 字符串,解密后得到该接口的具体业务结果。
|
||||||
|
|
||||||
|
**以 `IVYZSQ0E`(公安二要素)为例,解密后:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"result": 0, // 核验结果:0=一致,1=不一致,2=查无
|
||||||
|
"desc": "一致", // 结果中文描述
|
||||||
|
"address": "XX省XX市", // 身份证登记地址
|
||||||
|
"sex": "男", // 性别
|
||||||
|
"birthday": "19790713",// 出生日期
|
||||||
|
"order_no": "xxxxx" // 本次计费订单号
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见业务返回字段:**
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `result` | 核验结论编码(0=一致/通过,1=不一致/不通过,2=查无/未命中) |
|
||||||
|
| `desc` | 结论中文描述 |
|
||||||
|
| `order_no` | 计费订单号(每次调用消耗费用的凭证) |
|
||||||
|
| `address` | 身份证登记地址(身份验证类接口) |
|
||||||
|
| `sex` | 性别(身份验证类接口) |
|
||||||
|
| `birthday` | 出生日期(身份验证类接口) |
|
||||||
|
|
||||||
|
> ⚠️ 不同接口返回的 data 字段结构不同,以上为身份验证类的示例。金融风控、企业信息、车辆等接口返回字段更多,以实际返回为准。
|
||||||
|
|
||||||
|
**调用完成后,应该向用户说明:**
|
||||||
|
1. 接口是否调用成功(code 是否为 0)
|
||||||
|
2. 查询的核验结论(result / desc)
|
||||||
|
3. 返回的关键信息(地址、性别、评分等)
|
||||||
|
4. 如调用失败,说明失败原因(IP未授权/参数错误/余额不足等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口编码清单(真实编码)
|
||||||
|
|
||||||
|
### 📱 运营商验证
|
||||||
|
|
||||||
|
| 接口名称 | 编码 | 入参(示例值,请替换为真实数据) |
|
||||||
|
|---------|------|------|
|
||||||
|
| 移动手机号易诉分 | `YYSYS7Y1` | `mobile_no` |
|
||||||
|
| 手机停机验证 | `YYSY7R4V` | `mobile_no` |
|
||||||
|
| 手机消费区间验证 | `YYSY2M8Q` | `mobile_no` |
|
||||||
|
| 全网手机三要素验证 | `YYSYGLSF` | `mobile_no, id_card, name` |
|
||||||
|
| 运营商二要素V即时版 | `YYSY0YYV` | `mobile_no, name` |
|
||||||
|
| 运营商三要素简版V即时版 | `YYSYXHHO` | `mobile_no, id_card, name` |
|
||||||
|
| 运营商三要素简版V政务版 | `YYSYN8DI` | `mobile_no, id_card, name` |
|
||||||
|
| 运营商三要素详版V即时版 | `YYSYS66T` | `mobile_no, id_card, name` |
|
||||||
|
| 号码二次放号V即时版 | `YYSYP7PL` | `mobile_no` |
|
||||||
|
| 手机空号检测V即时版 | `YYSYJXZF` | `mobile_no` |
|
||||||
|
| 手机携号转网V即时版 | `YYSYXAES` | `mobile_no` |
|
||||||
|
| 手机在网状态V即时版 | `YYSYKTQO` | `mobile_no` |
|
||||||
|
| 手机号码在网时长V即时版 | `YYSYUO7E` | `mobile_no` |
|
||||||
|
|
||||||
|
### 🪪 身份验证
|
||||||
|
|
||||||
|
| 接口名称 | 编码 | 入参(示例值,请替换为真实数据) |
|
||||||
|
|---------|------|------|
|
||||||
|
| 公安二要素认证即时版 | `IVYZSQ0E` | `id_card, name` |
|
||||||
|
| 公安二要素政务版 | `IVYZFO5K` | `id_card, name` |
|
||||||
|
| 公安三要素即时版 | `IVYZE42I` | `id_card, name, mobile_no` |
|
||||||
|
| 人脸身份证比对 | `IVYZDXMD` | `id_card, name, image` |
|
||||||
|
| 身份证OCR | `IVYZZSCG` | `image` |
|
||||||
|
| 身份证OCR-A | `IVYZ9OHN` | `image` |
|
||||||
|
| 活体识别V步骤1 | `IVYZWVYA` | `callback_url` |
|
||||||
|
| 活体识别步骤二 | `IVYZKJ31` | `token` |
|
||||||
|
| 收入等级VJJ6 | `IVYZ6W0U` | `id_card, name` |
|
||||||
|
| 身份风险V106 | `IVYZOQFB` | `id_card, name` |
|
||||||
|
| 消费能力 | `IVYZBI6F` | `id_card, name` |
|
||||||
|
| 双人婚姻评估查询V1 | `IVYZPYSO` | 双方 `id_card, name` |
|
||||||
|
| 双人婚姻状态查询 | `IVYZRHT5` | 双方 `id_card, name` |
|
||||||
|
| 双人婚姻状况核验 | `IVYZYDQM` | 双方 `id_card, name` |
|
||||||
|
| 单人婚姻状态查询 | `IVYZO4RX` | `id_card, name` |
|
||||||
|
| 单人婚姻状况核验 | `IVYZYQKX` | `id_card, name` |
|
||||||
|
|
||||||
|
### 🚗 汽车相关
|
||||||
|
|
||||||
|
| 接口名称 | 编码 | 入参(示例值,请替换为真实数据) |
|
||||||
|
|---------|------|------|
|
||||||
|
| 汽车车辆五项 | `QCXG1S2L` | `vin` 或 `plate` |
|
||||||
|
| 全国车辆配置查验 | `QCXGA8V3` | `vin` |
|
||||||
|
| 全国车辆配置查验(车辆详情) | `QCXGCP77` | `vin` |
|
||||||
|
| 行驶证信息核验V2 | `QCXGX2X6` | `vin` |
|
||||||
|
| 名下车辆 | `QCXGIJY3` | `id_card, name` |
|
||||||
|
| 车辆静态信息查询 | `QCXG521L` | `vin` |
|
||||||
|
| 车辆维保记录 | `QCXGC6W8` | `vin` |
|
||||||
|
| 新能源车检测报告V1 | `QCXGX7N1` | `vin` |
|
||||||
|
| 新能源车检测报告(里程查询) | `QCXGX7N2` | `vin` |
|
||||||
|
| 疑似营运车辆注册平台数 | `QCXGFV9W` | `plate` |
|
||||||
|
| 疑似运营车辆查询(月度里程) | `QCXG76VA` | `plate` |
|
||||||
|
| 疑似运营车辆查询(季度里程) | `QCXGYQDC` | `plate` |
|
||||||
|
| 疑似运营车辆查询(半年度里程) | `QCXGGFWW` | `plate` |
|
||||||
|
| 疑似运营车辆查询(年度里程) | `QCXGYWSV` | `plate` |
|
||||||
|
|
||||||
|
### 🏢 企业相关
|
||||||
|
|
||||||
|
| 接口名称 | 编码 | 入参(示例值,请替换为真实数据) |
|
||||||
|
|---------|------|------|
|
||||||
|
| 企业全量信息核验V2 | `QYGLALPK` | `ent_code` 或 `ent_name` |
|
||||||
|
| 企业综合风险报告V2 | `QYGL7HBN` | `ent_code` 或 `ent_name` |
|
||||||
|
| 企业涉诉V2版 | `QYGLLUCM` | `ent_code` 或 `ent_name` |
|
||||||
|
| 企业年报信息核验 | `QYGLUDJG` | `ent_code` 或 `ent_name` |
|
||||||
|
| 企业进出口信用核查 | `QYGLM7AZ` | `ent_code` 或 `ent_name` |
|
||||||
|
| 企业税收违法核查 | `QYGLWYEK` | `ent_code` 或 `ent_name` |
|
||||||
|
| 企业股权结构全景 | `QYGL9MYB` | `ent_code` 或 `ent_name` |
|
||||||
|
|
||||||
|
### ⚖️ 风险管控
|
||||||
|
|
||||||
|
| 接口名称 | 编码 | 入参(示例值,请替换为真实数据) |
|
||||||
|
|---------|------|------|
|
||||||
|
| 个人涉诉V2版 | `FLXGC4CT` | `id_card, name` |
|
||||||
|
| 董监高司法综合信息核验A | `FLXGJI17` | `id_card` |
|
||||||
|
|
||||||
|
### 💰 金融验证
|
||||||
|
|
||||||
|
| 接口名称 | 编码 | 入参(示例值,请替换为真实数据) |
|
||||||
|
|---------|------|------|
|
||||||
|
| 银行卡四要素验证(详版) | `JRZQOICN` | `name, id_card, bankcard, mobile_no` |
|
||||||
|
| 银行卡OCR | `JRZQMDQ1` | `image` |
|
||||||
|
| 银行卡OCR-A | `JRZQDMLO` | `image` |
|
||||||
|
| 洞侦多头履约行为 | `JRZQ1AIJ` | `id_card, name, mobile_no` |
|
||||||
|
| 信用全景V21 | `JRZQV7YZ` | `id_card, name, mobile_no` |
|
||||||
|
| 借贷意向验证 | `JRZQX5DM` | `id_card, name, mobile_no` |
|
||||||
|
| 风险量级V8 | `JRZQNKEI` | `id_card, name, mobile_no` |
|
||||||
|
| 风险量级V9 | `JRZQQKEC` | `id_card, name, mobile_no` |
|
||||||
|
| 风险量级V10 | `JRZQI079` | `id_card, name, mobile_no` |
|
||||||
|
| 智瞳分尊享版 | `JRZQAH34` | `id_card, name` |
|
||||||
|
| 智瞳-通用版 | `JRZQLY6D` | `id_card, name` |
|
||||||
|
| 智享分 | `JRZQNEP0` | `id_card, name` |
|
||||||
|
| 支付行为指数 | `JRZQ65ZO` | `id_card, name` |
|
||||||
|
| 坤羽模型V3-标签版 | `JRZQML9G` | `id_card, name, mobile_no` |
|
||||||
|
| 租赁申请意向 | `JRZQZ05I` | `id_card, name, mobile_no` |
|
||||||
|
| 租赁申请意向V22 | `JRZQNK43` | `id_card, name, mobile_no` |
|
||||||
|
| 人企关联 | `QYGLWV7U` | `id_card, name` |
|
||||||
|
| 个人涉诉定制版 | `FLXGMMG7` | `id_card, name` |
|
||||||
|
| 企业诉讼定制版 | `QYGL7Z0O` | `ent_code` 或 `ent_name` |
|
||||||
|
| 行为黑名单 | `JRZQTM1V` | `id_card, name, mobile_no` |
|
||||||
|
| 黑名单V110_c10 | `JRZQBQIR` | `id_card, name, mobile_no` |
|
||||||
|
| 特殊名单 | `JRZQMLZX` | `id_card, name, mobile_no` |
|
||||||
|
| 债务逾期黑名单V3_1 | `JRZQQD4F` | `id_card, name, mobile_no` |
|
||||||
|
| 风险变量V5F4 | `JRZQNVM8` | `id_card, name, mobile_no` |
|
||||||
|
| 投诉风险筛查V709 | `JRZQVBHJ` | `id_card, name, mobile_no` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Python 完整调用代码
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json, time, base64, os, requests
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from Crypto.Util.Padding import pad, unpad
|
||||||
|
|
||||||
|
ACCESS_ID = os.getenv("HAIYU_ACCESS_ID", "") # 控制台获取
|
||||||
|
ACCESS_KEY = os.getenv("HAIYU_ACCESS_KEY", "") # 控制台获取(16进制字符串)
|
||||||
|
BASE_URL = "https://api.haiyudata.com"
|
||||||
|
|
||||||
|
|
||||||
|
def _encrypt(plaintext: str, key_hex: str) -> str:
|
||||||
|
"""AES-128-CBC 加密,返回 Base64(IV + 密文)"""
|
||||||
|
key = bytes.fromhex(key_hex)
|
||||||
|
iv = os.urandom(16)
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
enc = cipher.encrypt(pad(plaintext.encode("utf-8"), AES.block_size))
|
||||||
|
return base64.b64encode(iv + enc).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt(b64_str: str, key_hex: str) -> str:
|
||||||
|
"""AES-128-CBC 解密"""
|
||||||
|
key = bytes.fromhex(key_hex)
|
||||||
|
raw = base64.b64decode(b64_str)
|
||||||
|
iv, ct = raw[:16], raw[16:]
|
||||||
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||||
|
return unpad(cipher.decrypt(ct), AES.block_size).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def call_api(api_code: str, **params) -> dict:
|
||||||
|
"""
|
||||||
|
调用海宇数据接口(统一入口)
|
||||||
|
|
||||||
|
:param api_code: 接口编码,如 "IVYZSQ0E"
|
||||||
|
:param params: 入参(请传入真实值),如 id_card="...", name="..."
|
||||||
|
:return: {"code": 0, "message": "...", "data": {...}, ...}
|
||||||
|
|
||||||
|
示例:
|
||||||
|
call_api("IVYZSQ0E", id_card="真实身份证号", name="真实姓名")
|
||||||
|
call_api("YYSYJXZF", mobile_no="真实手机号")
|
||||||
|
"""
|
||||||
|
ts = str(int(time.time() * 1000))
|
||||||
|
url = f"{BASE_URL}/api/v1/{api_code}?t={ts}"
|
||||||
|
|
||||||
|
encrypted = _encrypt(json.dumps(params, ensure_ascii=False), ACCESS_KEY)
|
||||||
|
|
||||||
|
resp = requests.post(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Id": ACCESS_ID,
|
||||||
|
},
|
||||||
|
json={"data": encrypted},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
raw = resp.json()
|
||||||
|
|
||||||
|
# 解密 data 字段
|
||||||
|
if raw.get("data") and isinstance(raw["data"], str):
|
||||||
|
try:
|
||||||
|
decrypted = _decrypt(raw["data"], ACCESS_KEY)
|
||||||
|
raw["data"] = json.loads(decrypted)
|
||||||
|
except Exception:
|
||||||
|
pass # data 可能不是 JSON
|
||||||
|
|
||||||
|
return raw
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用示例(入参请替换为真实值)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 公安二要素
|
||||||
|
result = call_api("IVYZSQ0E", id_card="真实身份证号", name="真实姓名")
|
||||||
|
|
||||||
|
# 手机空号检测
|
||||||
|
result = call_api("YYSYJXZF", mobile_no="真实手机号")
|
||||||
|
|
||||||
|
# 个人涉诉V2版
|
||||||
|
result = call_api("FLXGC4CT", id_card="真实身份证号", name="真实姓名")
|
||||||
|
|
||||||
|
# 企业全量信息核验
|
||||||
|
result = call_api("QYGLALPK", ent_name="真实企业名称")
|
||||||
|
|
||||||
|
# 汽车车辆五项
|
||||||
|
result = call_api("QCXG1S2L", vin="真实车架号")
|
||||||
|
|
||||||
|
# 银行卡四要素验证
|
||||||
|
result = call_api("JRZQOICN", name="真实姓名", id_card="真实身份证号", bankcard="真实银行卡号", mobile_no="真实手机号")
|
||||||
|
|
||||||
|
# 运营商三要素详版
|
||||||
|
result = call_api("YYSYS66T", mobile_no="真实手机号", id_card="真实身份证号", name="真实姓名")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 入参相关
|
||||||
|
1. **入参为示例值** — 文档中所有身份证、手机号、姓名等均为占位示例,调用时必须替换为真实数据
|
||||||
|
2. **字段名严格匹配** — 必须用 `id_card`(不是 idcard/idCard)、`mobile_no`(不是 mobile/phone)、`name` 等标准字段名,字段名错误会返回 `code: 1003 参数校验不正确`
|
||||||
|
|
||||||
|
### 鉴权与网络
|
||||||
|
3. **IP 白名单** — 需在控制台添加调用方服务器公网 IP,否则返回 `code: 1004 未经授权的IP`
|
||||||
|
4. **Access Key 是 16 进制字符串** — 加密时用 `bytes.fromhex()` 转为 16 字节密钥,不是直接把字符串当 key
|
||||||
|
5. **时间戳实时生成** — URL 中的 `t` 参数每次请求都要重新生成当前的 13 位毫秒时间戳
|
||||||
|
6. **Access-Id 在请求头** — 不是放在请求体里,是在 HTTP Header 中
|
||||||
|
|
||||||
|
### 加密
|
||||||
|
7. **请求和响应都加密** — 发送的参数和返回的 data 都是 AES-128-CBC 加密的
|
||||||
|
8. **IV 每次随机** — 加密时随机生成 16 字节 IV,拼在密文前面一起 Base64 编码
|
||||||
|
9. **解密时先取 IV** — Base64 解码后,前 16 字节是 IV,剩余是密文
|
||||||
|
|
||||||
|
### 合规与安全
|
||||||
|
10. **敏感数据合规** — 查询个人信息需确保业务合规、用户已授权
|
||||||
|
11. **日志脱敏** — 记录日志时对身份证、手机号等做脱敏处理(如 110101****1234)
|
||||||
|
12. **按次计费** — 所有接口按调用次数扣费,注意余额;失败请求(code 非 0)一般不计费
|
||||||
|
13. **保留 transaction_id** — 每次调用都会返回 `transaction_id`(计费流水号),如对调用结果有疑问(重复扣费、数据异常等),凭此 ID 向海宇客服提交查询,务必在日志中记录
|
||||||
@@ -5,6 +5,12 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import type { Skill } from "@/content/skills";
|
import type { Skill } from "@/content/skills";
|
||||||
import { categoryMeta } from "@/content/skills";
|
import { categoryMeta } from "@/content/skills";
|
||||||
|
import {
|
||||||
|
buildNaturalLanguageExample,
|
||||||
|
buildSkillLoadLink,
|
||||||
|
buildSlashCommand,
|
||||||
|
skillInstallPackage,
|
||||||
|
} from "@/content/skill-install-package";
|
||||||
import { site } from "@/content/site";
|
import { site } from "@/content/site";
|
||||||
import { SkillExampleContent } from "@/components/skill-example-content";
|
import { SkillExampleContent } from "@/components/skill-example-content";
|
||||||
|
|
||||||
@@ -32,7 +38,16 @@ export function SkillDetail({ skill }: SkillDetailProps) {
|
|||||||
|
|
||||||
const [company, setCompany] = useState(skill.detail.runDefaultCompany);
|
const [company, setCompany] = useState(skill.detail.runDefaultCompany);
|
||||||
const [promptCopied, setPromptCopied] = useState(false);
|
const [promptCopied, setPromptCopied] = useState(false);
|
||||||
|
const [loadLinkCopied, setLoadLinkCopied] = useState(false);
|
||||||
|
const [skillMdCopied, setSkillMdCopied] = useState(false);
|
||||||
const [configCopied, setConfigCopied] = useState(false);
|
const [configCopied, setConfigCopied] = useState(false);
|
||||||
|
const [skillMdOpen, setSkillMdOpen] = useState(false);
|
||||||
|
const [skillMd, setSkillMd] = useState("");
|
||||||
|
const [skillMdLoading, setSkillMdLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadLink = buildSkillLoadLink(skill.title);
|
||||||
|
const usageExample = buildNaturalLanguageExample(skill.id, skill.title);
|
||||||
|
const slashCommand = buildSlashCommand(skill.slug);
|
||||||
|
|
||||||
const reports = site.intelligentReport.reports;
|
const reports = site.intelligentReport.reports;
|
||||||
const reportById = useMemo(() => {
|
const reportById = useMemo(() => {
|
||||||
@@ -54,10 +69,16 @@ export function SkillDetail({ skill }: SkillDetailProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!promptCopied) return;
|
if (!loadLinkCopied) return;
|
||||||
const t = setTimeout(() => setPromptCopied(false), 1800);
|
const t = setTimeout(() => setLoadLinkCopied(false), 1800);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [promptCopied]);
|
}, [loadLinkCopied]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!skillMdCopied) return;
|
||||||
|
const t = setTimeout(() => setSkillMdCopied(false), 1800);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [skillMdCopied]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!configCopied) return;
|
if (!configCopied) return;
|
||||||
@@ -65,10 +86,40 @@ export function SkillDetail({ skill }: SkillDetailProps) {
|
|||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [configCopied]);
|
}, [configCopied]);
|
||||||
|
|
||||||
const copyPrompt = async () => {
|
useEffect(() => {
|
||||||
|
if (tab !== "install" || skillMd) return;
|
||||||
|
let cancelled = false;
|
||||||
|
setSkillMdLoading(true);
|
||||||
|
fetch(skillInstallPackage.skillMdPath)
|
||||||
|
.then((res) => (res.ok ? res.text() : ""))
|
||||||
|
.then((text) => {
|
||||||
|
if (!cancelled && text) setSkillMd(text);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/* ignore */
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setSkillMdLoading(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [tab, skillMd]);
|
||||||
|
|
||||||
|
const copyLoadLink = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(skill.detail.installPrompt);
|
await navigator.clipboard.writeText(loadLink);
|
||||||
setPromptCopied(true);
|
setLoadLinkCopied(true);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copySkillMd = async () => {
|
||||||
|
if (!skillMd) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(skillMd);
|
||||||
|
setSkillMdCopied(true);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
@@ -173,7 +224,10 @@ export function SkillDetail({ skill }: SkillDetailProps) {
|
|||||||
完成海宇数科 MCP 接入
|
完成海宇数科 MCP 接入
|
||||||
</h2>
|
</h2>
|
||||||
<p className="skill-install__desc type-body-md text-body">
|
<p className="skill-install__desc type-body-md text-body">
|
||||||
注册账号、获取 API Key、粘贴配置到你的 AI 工具。只需做一次,所有 SKILL 共用。
|
注册账号、获取 API Key、粘贴配置到你的 AI 工具。只需做一次,所有 SKILL 共用。{" "}
|
||||||
|
<Link href={skillInstallPackage.guideHref} className="skill-install__guide-link">
|
||||||
|
{skillInstallPackage.guideLabel}
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<div className="skill-code-block" data-open="true">
|
<div className="skill-code-block" data-open="true">
|
||||||
<div className="skill-code-block__header">
|
<div className="skill-code-block__header">
|
||||||
@@ -201,26 +255,148 @@ export function SkillDetail({ skill }: SkillDetailProps) {
|
|||||||
<h3 className="skill-install__subtitle type-display-sm text-ink">
|
<h3 className="skill-install__subtitle type-display-sm text-ink">
|
||||||
加载本 SKILL 到 AI 工具
|
加载本 SKILL 到 AI 工具
|
||||||
</h3>
|
</h3>
|
||||||
<p className="skill-install__desc type-body-md text-body">
|
|
||||||
复制下方提示词,粘贴到 AI 对话框即可加载「{skill.title}」技能。
|
<div className="skill-install__method">
|
||||||
</p>
|
<div className="skill-install__method-head">
|
||||||
<div className="skill-code-block" data-open="true">
|
<h4 className="skill-install__method-title type-caption-strong text-ink">
|
||||||
<div className="skill-code-block__header">
|
方式 A · 下载 Skill 安装包
|
||||||
<span className="skill-code-block__filename">
|
</h4>
|
||||||
<span className="skill-code-block__dot" aria-hidden />
|
<span className="skill-install__badge">推荐</span>
|
||||||
AI 加载提示词
|
</div>
|
||||||
</span>
|
<p className="skill-install__desc type-body-md text-body">
|
||||||
<div className="skill-code-block__actions">
|
{skillInstallPackage.zipDescription}
|
||||||
<button type="button" onClick={copyPrompt}>
|
</p>
|
||||||
{promptCopied
|
<div className="skill-install__download">
|
||||||
? site.skillMarket.howToUse.copiedLabel
|
<span className="skill-install__download-name type-caption-strong text-ink">
|
||||||
: site.skillMarket.howToUse.copyLabel}
|
{skillInstallPackage.zipFileName}
|
||||||
</button>
|
</span>
|
||||||
</div>
|
<a
|
||||||
|
href={skillInstallPackage.zipHref}
|
||||||
|
download={skillInstallPackage.zipFileName}
|
||||||
|
className="btn-primary btn-sm"
|
||||||
|
>
|
||||||
|
下载安装包
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="skill-install__method">
|
||||||
|
<div className="skill-install__method-head">
|
||||||
|
<h4 className="skill-install__method-title type-caption-strong text-ink">
|
||||||
|
方式 B · 复制 AI 加载链接
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className="skill-install__desc type-body-md text-body">
|
||||||
|
{skillInstallPackage.loadLinkHint}
|
||||||
|
</p>
|
||||||
|
<div className="skill-code-block" data-open="true">
|
||||||
|
<div className="skill-code-block__header">
|
||||||
|
<span className="skill-code-block__filename">
|
||||||
|
<span className="skill-code-block__dot" aria-hidden />
|
||||||
|
AI 加载链接
|
||||||
|
</span>
|
||||||
|
<div className="skill-code-block__actions">
|
||||||
|
<button type="button" onClick={copyLoadLink}>
|
||||||
|
{loadLinkCopied
|
||||||
|
? site.skillMarket.howToUse.copiedLabel
|
||||||
|
: site.skillMarket.howToUse.copyLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="skill-code-block__pre skill-code-block__pre--text">
|
||||||
|
<code>{loadLink}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="skill-install__method">
|
||||||
|
<div className="skill-install__method-head">
|
||||||
|
<h4 className="skill-install__method-title type-caption-strong text-ink">
|
||||||
|
方式 C · 兜底:复制 SKILL.md 全文
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className="skill-install__desc type-body-md text-body">
|
||||||
|
{skillInstallPackage.skillMdFallbackHint}
|
||||||
|
</p>
|
||||||
|
<div className="skill-code-block" data-open={skillMdOpen ? "true" : "false"}>
|
||||||
|
<div className="skill-code-block__header">
|
||||||
|
<span className="skill-code-block__filename">
|
||||||
|
<span className="skill-code-block__dot" aria-hidden />
|
||||||
|
完整 SKILL.md 全文({skillInstallPackage.skillMdSizeLabel} · 含 API
|
||||||
|
对接规则与接口清单)
|
||||||
|
</span>
|
||||||
|
<div className="skill-code-block__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="skill-code-block__btn skill-code-block__btn--toggle"
|
||||||
|
onClick={() => setSkillMdOpen((open) => !open)}
|
||||||
|
aria-expanded={skillMdOpen}
|
||||||
|
>
|
||||||
|
{skillMdOpen
|
||||||
|
? site.skillMarket.howToUse.collapseLabel
|
||||||
|
: site.skillMarket.howToUse.expandLabel}
|
||||||
|
<span className="skill-code-block__chevron" aria-hidden>
|
||||||
|
▾
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copySkillMd}
|
||||||
|
disabled={!skillMd || skillMdLoading}
|
||||||
|
>
|
||||||
|
{skillMdCopied
|
||||||
|
? site.skillMarket.howToUse.copiedLabel
|
||||||
|
: site.skillMarket.howToUse.copyLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{skillMdOpen ? (
|
||||||
|
<pre className="skill-code-block__pre skill-code-block__pre--text">
|
||||||
|
<code>
|
||||||
|
{skillMdLoading
|
||||||
|
? "加载中…"
|
||||||
|
: skillMd || "SKILL.md 暂时无法加载,请稍后重试或直接下载安装包。"}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="skill-install__step">
|
||||||
|
<h3 className="skill-install__subtitle type-display-sm text-ink">
|
||||||
|
使用本 SKILL
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="skill-install__method">
|
||||||
|
<div className="skill-install__method-head">
|
||||||
|
<h4 className="skill-install__method-title type-caption-strong text-ink">
|
||||||
|
方式 A · 自然语言对话
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className="skill-install__desc type-body-md text-body">
|
||||||
|
AI 加载本 SKILL 后,像平时聊天一样说出你的需求,AI 会自动识别并调用本
|
||||||
|
SKILL:
|
||||||
|
</p>
|
||||||
|
<div className="skill-install__quote type-body-md text-body">
|
||||||
|
{usageExample}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="skill-install__method">
|
||||||
|
<div className="skill-install__method-head">
|
||||||
|
<h4 className="skill-install__method-title type-caption-strong text-ink">
|
||||||
|
方式 B · 精准命令调用(专业用户)
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<p className="skill-install__desc type-body-md text-body">
|
||||||
|
熟悉本 SKILL 后可直接用 slash 命令精准触发,节省 token 与等待时间:
|
||||||
|
</p>
|
||||||
|
<div className="skill-code-block" data-open="true">
|
||||||
|
<pre className="skill-code-block__pre skill-code-block__pre--text">
|
||||||
|
<code>{slashCommand}</code>
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<pre className="skill-code-block__pre skill-code-block__pre--text">
|
|
||||||
<code>{skill.detail.installPrompt}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
35
src/content/skill-install-package.ts
Normal file
35
src/content/skill-install-package.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { absoluteUrl } from "@/lib/site-url";
|
||||||
|
|
||||||
|
export const skillInstallPackage = {
|
||||||
|
zipHref: "/download/hydata-skill.zip",
|
||||||
|
zipFileName: "hydata-skill.zip",
|
||||||
|
skillMdPath: "/download/hydata-skill/SKILL.md",
|
||||||
|
skillMdSizeLabel: "14 KB",
|
||||||
|
zipDescription:
|
||||||
|
"包含 SKILL.md、Python 客户端(haiyu_client.py)与测试脚本。解压后放入 Cursor / Claude Code / 小龙虾 等工具的 skills 目录即可。",
|
||||||
|
loadLinkHint:
|
||||||
|
"兼容 Cursor / Claude Code / 小龙虾 / OpenClaw 等主流 AI 应用。复制后粘贴到 AI 对话框即可。",
|
||||||
|
skillMdFallbackHint:
|
||||||
|
"适用于不会主动 fetch URL 的 AI 工具(如 CherryStudio)。",
|
||||||
|
guideHref: "/docs/getting-started",
|
||||||
|
guideLabel: "查看接入指南 →",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function buildSkillLoadLink(title: string): string {
|
||||||
|
return `请加载海宇数科 SKILL(${title}):${absoluteUrl(skillInstallPackage.skillMdPath)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildNaturalLanguageExample(
|
||||||
|
skillId: string,
|
||||||
|
title: string
|
||||||
|
): string {
|
||||||
|
if (skillId === "kyb-verification") {
|
||||||
|
return "「请帮我对 [公司名称] 做一份完整 KYB 核验报告,重点看主体真实性、股东结构、最近 3 年的司法风险」";
|
||||||
|
}
|
||||||
|
return `「请使用 ${title},对 [公司名称] 做一次完整尽调并输出结构化报告」`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSlashCommand(slug: string): string {
|
||||||
|
const cmd = slug.replace(/^\//, "");
|
||||||
|
return `/${cmd} [公司名称]`;
|
||||||
|
}
|
||||||
@@ -2292,6 +2292,78 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skill-install__guide-link {
|
||||||
|
color: var(--color-accent-ink);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__guide-link:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__method {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border: 1px solid var(--color-hairline);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__method-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__method-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-accent-ink);
|
||||||
|
background-color: var(--color-accent-wash);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-accent-ink) 18%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__download {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border: 1px solid var(--color-hairline);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background-color: var(--color-canvas-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__download-name {
|
||||||
|
font-family: var(--font-mono-caps);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-install__quote {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
border-left: 3px solid var(--color-accent-ink);
|
||||||
|
background-color: var(--color-canvas-muted);
|
||||||
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
.skill-code-block__pre--text {
|
.skill-code-block__pre--text {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|||||||
Reference in New Issue
Block a user