添加技能

This commit is contained in:
jerron
2026-09-24 15:37:24 +08:00
commit a8d67f3c64
9 changed files with 627 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# 本地敏感配置
.yierp/config.json
.env
.env.*
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
.venv/
venv/
# 通用
.DS_Store
Thumbs.db
+83
View File
@@ -0,0 +1,83 @@
# YIERP CLI 技能说明
这是一个面向 YIERP(基于 ERPNext v16)的数据操作脚本包,适合通过配置 URL、apikey 和 ApiSecret 来执行常见的数据查询与写入操作。
## 目录说明
- `SKILL.md`:技能说明,适合作为 AI/脚本上下文。
- `tools/yierp_cli.py`:YIERP CLI 脚本。
- `config.example.json`:示例配置文件。
- `requirements.txt`:Python 依赖。
## 快速开始
1. 安装依赖:
```bash
python -m pip install -r requirements.txt
```
2. 复制示例配置:
```bash
copy config.example.json .yierp\config.json
```
3. 编辑 `.yierp/config.json`,填入真实参数:
```json
{
"url": "https://yierp.example.com",
"apikey": "你的_api_key",
"ApiSecret": "你的_api_secret",
"timeout": 30,
"verify_ssl": true
}
```
4. 运行命令:
```bash
python tools/yierp_cli.py whoami --config .yierp/config.json
python tools/yierp_cli.py list --doctype ToDo --config .yierp/config.json --limit 10
python tools/yierp_cli.py create --doctype ToDo --config .yierp/config.json --data '{"description":"测试任务","status":"Open"}'
```
## 常用命令
```bash
# 连接检测
python tools/yierp_cli.py whoami
# 查询列表
python tools/yierp_cli.py list --doctype ToDo --filters '{"status":"Open"}' --limit 20
# 获取单条记录
python tools/yierp_cli.py get --doctype ToDo --name "TODO-00001"
# 创建记录
python tools/yierp_cli.py create --doctype ToDo --data '{"description":"新任务","status":"Open"}'
# 更新记录
python tools/yierp_cli.py update --doctype ToDo --name "TODO-00001" --data '{"status":"Closed"}'
# 删除记录
python tools/yierp_cli.py delete --doctype ToDo --name "TODO-00001" --yes
# 调用自定义方法
python tools/yierp_cli.py method --method "your_app.api.test" --args '{"message":"hello"}'
```
## 注意事项
- 对于删除、更新和批量修改类操作,请先确认 DocType、名称和修改内容。
- `ApiSecret` 是敏感信息,请勿提交到代码仓库或日志中。
- 生产环境建议使用环境变量覆盖配置:
```bash
set YIERP_URL=https://yierp.example.com
set YIERP_APIKEY=你的_api_key
set YIERP_APISECRET=你的_api_secret
```
- 如果 YIERP 站点使用自签名证书或内网 HTTPS,请适当调整 `verify_ssl`。
+164
View File
@@ -0,0 +1,164 @@
---
name: yierp-cli
description: 适用于 YIERP(源自 ERPNext v16)的数据访问与管理技能,支持通过配置 url、apikey、ApiSecret 完成查询、创建、更新、删除和自定义方法调用。
---
# YIERP CLI 数据操作技能
这个技能用于访问 YIERP 系统中的业务数据与文档资源。YIERP 继承 ERPNext v16 的 Frappe / REST API 设计,因此可以使用标准的 HTTP API 方式进行 DocType 数据访问、文档操作和后端方法调用。
## 适用目标
- 通过配置 `url`、`apikey`、`ApiSecret` 建立认证
- 支持 `list`、`get`、`create`、`update`、`delete`、`method`、`whoami` 等操作
- 适合脚本自动化、批量同步、业务查询、报表提取和业务集成
- 所有说明、注释、提示和输出都使用简体中文
## 配置方式
### 1. 配置文件方式
建议在项目中创建 `.yierp/config.json`:
```json
{
"url": "https://yierp.example.com",
"apikey": "你的_api_key",
"ApiSecret": "你的_api_secret",
"timeout": 30,
"verify_ssl": true
}
```
### 2. 环境变量方式
```bash
set YIERP_URL=https://yierp.example.com
set YIERP_APIKEY=你的_api_key
set YIERP_APISECRET=你的_api_secret
```
### 3. 命令行参数方式
```bash
python tools/yierp_cli.py list --doctype ToDo --url https://yierp.example.com --apikey xxx --apisecret yyy
```
## 关键命令
### 1. 检查当前登录用户
```bash
python tools/yierp_cli.py whoami --config .yierp/config.json
```
### 2. 查询列表
```bash
python tools/yierp_cli.py list --doctype ToDo --config .yierp/config.json --filters '{"status":"Open"}' --limit 20
```
### 3. 获取单条记录
```bash
python tools/yierp_cli.py get --doctype ToDo --name "TODO-00001" --config .yierp/config.json
```
### 4. 创建记录
```bash
python tools/yierp_cli.py create --doctype ToDo --config .yierp/config.json --data '{"description":"测试任务","status":"Open"}'
```
### 5. 更新记录
```bash
python tools/yierp_cli.py update --doctype ToDo --name "TODO-00001" --config .yierp/config.json --data '{"status":"Closed"}'
```
### 6. 删除记录
```bash
python tools/yierp_cli.py delete --doctype ToDo --name "TODO-00001" --config .yierp/config.json --yes
```
### 7. 调用自定义后端方法
```bash
python tools/yierp_cli.py method --method your_app.api.test --config .yierp/config.json --args '{"message":"hello"}'
```
### 8. 查看当前配置摘要
```bash
python tools/yierp_cli.py config --config .yierp/config.json
```
## 认证方式
YIERP 使用 Frappe / ERPNext 风格的 token 认证:
```text
Authorization: token {apikey}:{ApiSecret}
```
因此,在访问 API 时,必须提供:
- `url`:站点地址,例如 `https://yierp.example.com`
- `apikey`:API 密钥
- `ApiSecret`:对应的 API Secret
## 数据操作能力
### 查询类
- `list`:根据 DocType 查询多条记录
- `get`:获取单个文档详情
- `whoami`:验证当前认证用户
### 写入类
- `create`:新增数据记录
- `update`:修改已有记录
- `delete`:删除记录
### 业务集成类
- `method`:调用后端方法或自定义逻辑
- `config`:查看当前配置状态
## 安全要求
1. 更新或删除前必须确认 DocType、记录名称和修改字段。
2. `ApiSecret` 为敏感信息,必须仅保存在本地配置中,不得写入日志、截图或仓库。
3. 若 YIERP 使用自签名证书,需关闭 SSL 校验或配置 `verify_ssl: false`。
4. 最小权限原则:优先用 `list` 与 `get` 做验证,再执行写入操作。
## 失败处理
遇到失败时,优先检查以下问题:
- URL 是否正确,站点是否可达
- `apikey` 与 `ApiSecret` 是否匹配
- DocType 名称是否存在
- 请求体 JSON 是否符合规范
- API 路径是否正确,是否为 `api/resource` 或 `api/method`
- SSL 证书错误是否需要使用 `--no-verify-ssl`
## 适用场景
- 客户、供应商、订单、发票、库存、出入库等数据查询
- 批量报表导出与任务调度
- ERP 系统与 CRM、WMS、MES 等系统集成
- 调用 YIERP 自定义 Python 方法实现复杂业务逻辑
## 说明
本技能是为 YIERP(源自 ERPNext v16 的定制 ERP 系统)设计的通用接口封装。使用时应优先遵循:
- 站点配置优先:`url` 为核心入口
- 凭证配置优先:`apikey` 与 `ApiSecret` 为认证关键
- 最小动作原则:先查询再写入
- 安全确认原则:删除与更新必须确认目标后再执行
如果用户没有明确指定站点,默认使用配置文件中的 `url`。如果用户要求执行数据操作,请优先采用本技能提供的 CLI 脚本执行。
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec python "$ROOT/tools/yierp_cli.py" "$@"
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if (-not $pythonCmd) {
Write-Error "未找到 python 命令,请先安装 Python 3.9+。"
exit 1
}
& python (Join-Path $repoRoot "tools\yierp_cli.py") @args
exit $LASTEXITCODE
+7
View File
@@ -0,0 +1,7 @@
{
"url": "https://yierp.example.com",
"apikey": "你的_api_key",
"ApiSecret": "你的_api_secret",
"timeout": 30,
"verify_ssl": true
}
+1
View File
@@ -0,0 +1 @@
requests>=2.31.0
+1
View File
@@ -0,0 +1 @@
"""YIERP 统一工具包。"""
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""YIERP CLI:通过 URL、apikey 和 ApiSecret 访问 YIERP 数据接口。
本脚本适配 ERPNext / Frappe 风格的 REST API,适用于 YIERP(源自 ERPNext v16)。
支持的核心能力包括:
- 查询列表
- 获取单条记录
- 创建文档
- 更新文档
- 删除文档
- 调用自定义方法
- 连接检测
使用方法:
python tools/yierp_cli.py --help
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Any, Dict, Optional
import requests
class YIERPConfigError(RuntimeError):
"""配置错误。"""
class YIERPRequestError(RuntimeError):
"""API 请求错误。"""
def _safe_json_loads(raw: Optional[str], label: str) -> Any:
"""安全解析 JSON 字符串。"""
if raw is None:
return None
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"{label} 不是合法的 JSON: {raw}") from exc
def _mask_value(value: Optional[str]) -> str:
"""掩码保护敏感字段。"""
if value is None:
return "未配置"
if len(value) <= 6:
return "******"
return f"{value[:2]}******{value[-2:]}"
def _load_json_file(file_path: str) -> Dict[str, Any]:
"""读取 JSON 配置文件。"""
path = Path(file_path)
if not path.exists():
return {}
try:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
if isinstance(data, dict):
return data
raise YIERPConfigError(f"配置文件不是有效的 JSON 对象:{file_path}")
except json.JSONDecodeError as exc:
raise YIERPConfigError(f"配置文件 JSON 格式错误:{file_path}") from exc
def resolve_config(args: argparse.Namespace) -> Dict[str, Any]:
"""按优先级解析配置:命令行参数 > 环境变量 > 配置文件。"""
config_path = args.config or os.getenv("YIERP_CONFIG_PATH", ".yierp/config.json")
config_data: Dict[str, Any] = _load_json_file(config_path)
data: Dict[str, Any] = dict(config_data)
data["url"] = args.url or os.getenv("YIERP_URL") or data.get("url")
data["apikey"] = (
args.apikey
or os.getenv("YIERP_APIKEY")
or data.get("apikey")
or data.get("api_key")
or data.get("apiKey")
)
data["ApiSecret"] = (
args.apisecret
or os.getenv("YIERP_APISECRET")
or data.get("ApiSecret")
or data.get("api_secret")
or data.get("apiSecret")
)
data["timeout"] = args.timeout or data.get("timeout", 30)
data["verify_ssl"] = data.get("verify_ssl", True)
if args.no_verify_ssl:
data["verify_ssl"] = False
if not data.get("url"):
raise YIERPConfigError("缺少配置:URL 未设置。请配置 url 或设置 YIERP_URL。")
if not data.get("apikey"):
raise YIERPConfigError("缺少配置:apikey 未设置。请配置 apikey 或设置 YIERP_APIKEY。")
if not data.get("ApiSecret"):
raise YIERPConfigError("缺少配置:ApiSecret 未设置。请配置 ApiSecret 或设置 YIERP_APISECRET。")
return data
def ensure_headers(config: Dict[str, Any]) -> Dict[str, str]:
"""构造 Frappe / ERPNext 的认证头。"""
auth_value = f"token {config['apikey']}:{config['ApiSecret']}"
return {
"Authorization": auth_value,
"Accept": "application/json",
"Content-Type": "application/json",
}
def request_json(
config: Dict[str, Any],
method: str,
path: str,
params: Optional[Dict[str, Any]] = None,
payload: Optional[Dict[str, Any]] = None,
) -> Any:
"""统一请求 YIERP REST API。"""
url = f"{config['url'].rstrip('/')}{path}"
headers = ensure_headers(config)
try:
response = requests.request(
method=method.upper(),
url=url,
headers=headers,
params=params,
json=payload,
timeout=config.get("timeout", 30),
verify=config.get("verify_ssl", True),
)
except requests.RequestException as exc:
raise YIERPRequestError(f"请求失败:{exc}") from exc
text = response.text.strip()
try:
payload_json = response.json() if text else {}
except ValueError:
payload_json = {"raw": text}
if response.status_code >= 400:
message = payload_json.get("exc") or payload_json.get("exception") or payload_json.get("message") or text or "未知错误"
raise YIERPRequestError(f"HTTP {response.status_code}:{message}")
return payload_json
def parse_json_argument(value: Optional[str], label: str) -> Optional[Any]:
"""将命令行 JSON 字符串解析为 Python 对象。"""
if value is None:
return None
return _safe_json_loads(value, label)
def build_list_params(filters: Optional[Any], fields: Optional[Any], limit: int, start: int, order_by: Optional[str]) -> Dict[str, Any]:
"""构造列表查询参数。"""
params: Dict[str, Any] = {}
if filters is not None:
params["filters"] = json.dumps(filters)
if fields is not None:
params["fields"] = json.dumps(fields)
if order_by:
params["order_by"] = order_by
if limit is not None:
params["limit_page_length"] = limit
if start is not None:
params["limit_start"] = start
return params
def cmd_whoami(args: argparse.Namespace) -> Dict[str, Any]:
"""检查当前登录用户。"""
config = resolve_config(args)
return request_json(config, "GET", "/api/method/frappe.auth.get_logged_user")
def cmd_list(args: argparse.Namespace) -> Dict[str, Any]:
"""查询文档列表。"""
config = resolve_config(args)
doctype = args.doctype
filters = parse_json_argument(args.filters, "filters")
fields = parse_json_argument(args.fields, "fields")
params = build_list_params(filters, fields, args.limit, args.start, args.order_by)
result = request_json(config, "GET", f"/api/resource/{doctype}", params=params)
return result
def cmd_get(args: argparse.Namespace) -> Dict[str, Any]:
"""获取单条文档。"""
config = resolve_config(args)
doctype = args.doctype
name = args.name
return request_json(config, "GET", f"/api/resource/{doctype}/{name}")
def cmd_create(args: argparse.Namespace) -> Dict[str, Any]:
"""创建单条文档。"""
config = resolve_config(args)
doctype = args.doctype
payload = parse_json_argument(args.data, "data")
if not isinstance(payload, dict):
raise ValueError("创建数据必须是 JSON 对象。")
return request_json(config, "POST", f"/api/resource/{doctype}", payload=payload)
def cmd_update(args: argparse.Namespace) -> Dict[str, Any]:
"""更新单条文档。"""
config = resolve_config(args)
doctype = args.doctype
name = args.name
payload = parse_json_argument(args.data, "data")
if not isinstance(payload, dict):
raise ValueError("更新数据必须是 JSON 对象。")
return request_json(config, "PUT", f"/api/resource/{doctype}/{name}", payload=payload)
def cmd_delete(args: argparse.Namespace) -> Dict[str, Any]:
"""删除单条文档。"""
config = resolve_config(args)
doctype = args.doctype
name = args.name
if not args.yes:
raise YIERPRequestError("删除操作需要显式添加 --yes 参数确认。")
return request_json(config, "DELETE", f"/api/resource/{doctype}/{name}")
def cmd_method(args: argparse.Namespace) -> Dict[str, Any]:
"""调用自定义的挂钩方法或后端方法。"""
config = resolve_config(args)
method_name = args.method
payload = parse_json_argument(args.args, "args")
if payload is None:
payload = {}
if not isinstance(payload, dict):
raise ValueError("方法参数必须是 JSON 对象。")
return request_json(config, "POST", f"/api/method/{method_name}", payload=payload)
def cmd_config_show(args: argparse.Namespace) -> Dict[str, Any]:
"""显示当前配置摘要,隐藏敏感字段。"""
config = resolve_config(args)
return {
"url": config.get("url"),
"apikey": _mask_value(config.get("apikey")),
"ApiSecret": _mask_value(config.get("ApiSecret")),
"timeout": config.get("timeout", 30),
"verify_ssl": config.get("verify_ssl", True),
}
def build_parser() -> argparse.ArgumentParser:
"""构建命令行解释器。"""
parser = argparse.ArgumentParser(description="YIERP CLI:基于 ERPNext v16 REST API 的数据操作工具", add_help=True)
parser.add_argument("--config", default=None, help="配置文件路径,默认 .yierp/config.json")
parser.add_argument("--url", default=None, help="YIERP 站点地址,例如 https://yierp.example.com")
parser.add_argument("--apikey", default=None, help="YIERP API Key")
parser.add_argument("--apisecret", default=None, help="YIERP ApiSecret")
parser.add_argument("--timeout", type=int, default=None, help="请求超时时间,单位秒")
parser.add_argument("--no-verify-ssl", action="store_true", help="跳过 SSL 证书校验,适用于自签名证书场景")
subparsers = parser.add_subparsers(dest="command", required=True)
whoami_parser = subparsers.add_parser("whoami", help="查看当前登录用户")
whoami_parser.set_defaults(func=cmd_whoami)
config_parser = subparsers.add_parser("config", help="显示当前配置摘要")
config_parser.set_defaults(func=cmd_config_show)
list_parser = subparsers.add_parser("list", help="查询数据列表")
list_parser.add_argument("--doctype", required=True, help="DocType,例如 ToDo、Sales Invoice、Customer")
list_parser.add_argument("--filters", default=None, help="筛选条件,JSON 字符串,例如 '{\"status\":\"Open\"}'")
list_parser.add_argument("--fields", default=None, help="查询字段,JSON 数组,例如 '[\"name\",\"status\"]'")
list_parser.add_argument("--limit", type=int, default=20, help="返回条数上限")
list_parser.add_argument("--start", type=int, default=0, help="分页起始位置")
list_parser.add_argument("--order-by", default=None, help="排序字段,例如 name desc")
list_parser.set_defaults(func=cmd_list)
get_parser = subparsers.add_parser("get", help="获取单条记录")
get_parser.add_argument("--doctype", required=True, help="DocType")
get_parser.add_argument("--name", required=True, help="文档名称或主键值")
get_parser.set_defaults(func=cmd_get)
create_parser = subparsers.add_parser("create", help="创建单条文档")
create_parser.add_argument("--doctype", required=True, help="DocType")
create_parser.add_argument("--data", required=True, help="创建数据,JSON 对象,例如 '{\"description\":\"新任务\"}'")
create_parser.set_defaults(func=cmd_create)
update_parser = subparsers.add_parser("update", help="更新单条文档")
update_parser.add_argument("--doctype", required=True, help="DocType")
update_parser.add_argument("--name", required=True, help="文档名称或主键值")
update_parser.add_argument("--data", required=True, help="更新数据,JSON 对象,例如 '{\"status\":\"Closed\"}'")
update_parser.set_defaults(func=cmd_update)
delete_parser = subparsers.add_parser("delete", help="删除单条文档")
delete_parser.add_argument("--doctype", required=True, help="DocType")
delete_parser.add_argument("--name", required=True, help="文档名称或主键值")
delete_parser.add_argument("--yes", action="store_true", help="确认执行删除操作")
delete_parser.set_defaults(func=cmd_delete)
method_parser = subparsers.add_parser("method", help="调用后端方法")
method_parser.add_argument("--method", required=True, help="方法路径,例如 your_app.api.test")
method_parser.add_argument("--args", default=None, help="方法参数,JSON 对象,例如 '{\"message\":\"hello\"}'")
method_parser.set_defaults(func=cmd_method)
return parser
def main() -> int:
"""主函数入口。"""
parser = build_parser()
args = parser.parse_args()
try:
result = args.func(args)
except (YIERPConfigError, YIERPRequestError, ValueError) as exc:
print(f"错误:{exc}", file=sys.stderr)
return 1
except Exception as exc: # pragma: no cover
print(f"未处理错误:{exc}", file=sys.stderr)
return 1
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main())