SKILL.md
5.3 KB查看源码发技能到灵栈#
能做什么:把一个本地技能文件夹检查指纹、脱敏打包、通过灵栈的 Agent 接口发布,并把公开链接回报给用户。 带脚本:不单独带,正文里有可直接复制的 Python / curl 片段。 联不联网:要,全程访问 https://lingzhanskill.com 。 写文件:会写临时 ZIP 和脱敏副本(放在临时目录),不修改作者原始技能文件夹。
官方说明(每次先读一遍,可能更新):https://lingzhanskill.com/publish.md
前置#
- 发布密钥在
~/.lingzhan/agent-token(lzk_开头)。已有就直接用,不要重复走设备授权。 - 校验密钥:
curl -s -H "Authorization: Bearer $(cat ~/.lingzhan/agent-token)" https://lingzhanskill.com/api/v1/agent/me,返回owner说明可用。 - 没有密钥时才走设备授权:
POST /api/v1/agent/authorize→ 让作者在verification_url核对确认码并点允许 → 轮询/api/v1/agent/token→ 把 token 存到~/.lingzhan/agent-token(权限只给当前用户)。
七步#
- 隐私扫描(必做,发布后公开且收不回)
在待发文件里找:绝对路径与用户名(
C:\Users\<名字>\、/home/<名字>/)、内网地址(10.、192.168.、172.16~31、.internal/.corp/.local)、密钥凭据(sk-、ghp_、AKIA、BEGIN PRIVATE、password=、.env)、手机号邮箱等个人信息。 把「文件:行号 + 该行内容(密钥只显示前 4 位)」列给作者,由作者决定。 - 脱敏打包(推荐,绝不改原文件夹)
复制到临时目录再替换:本机绝对路径 →
~/.dsh-workspace之类占位符;解释器绝对路径 →python;本地服务端口 →127.0.0.1:<端口>。 在 SKILL.md 开头补一句「依赖本地 XX,路径按自己环境替换」。 ZIP 要求:条目路径用/、不能有./、..、绝对路径;最多 500 文件、≤20MB、解压≤100MB;ZIP 不要放进技能文件夹。 - 确认:slug(3~32 位小写字母数字连字符,发布后不可改)、显示名、一句话描述——念给作者,明确同意再发。
- 建版本:
POST /api/v1/agent/releases,body{"slug","size","name","summary","category","changes"};返回put_url(15 分钟临时凭据,不保存不外传)、complete_url、version。 - 上传:
PUT put_url,Content-Type: application/zip,body 为 ZIP 字节。 - 完成:
POST complete_url(空 body),返回stage: checking。 - 轮询:
GET /api/v1/agent/releases/<slug>/<version>,每 5 秒、最多 3 分钟。published→ 把url给作者;blocked/returned→ 按problems改完重发;in_review/closed→ 让作者看manage_url。
坑#
- Cloudflare 1010:Python urllib 默认 UA 会被 403。所有请求都要带
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36和Accept: application/json。 - 中文请求体必须 UTF-8 字节:
json.dumps(obj, ensure_ascii=False).encode('utf-8'),Content-Type 带charset=utf-8。 - slug 发布后不能改;名称、简介可以改。
- 已公开的版本不能修改,要改就发新版本号(不填
version会自动 +1)。 - 用户没看过内容、没同意之前不要发布;不要把 token、
put_url贴进聊天或写进技能包。 - 失败重试是安全的(PUT 同一个 zip、complete 同一版本不会重复发布)。
Python 片段#
import json, os, time, urllib.request
T = open(os.path.expanduser('~/.lingzhan/agent-token')).read().strip()
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36'
H = {'Authorization': 'Bearer ' + T, 'User-Agent': UA, 'Accept': 'application/json',
'Content-Type': 'application/json; charset=utf-8'}
zip_path = 'skill.zip'
body = json.dumps({"slug": "my-skill", "size": os.path.getsize(zip_path),
"name": "技能名", "summary": "一句话说明", "changes": ["首个版本"]},
ensure_ascii=False).encode()
r = json.loads(urllib.request.urlopen(urllib.request.Request(
'https://lingzhanskill.com/api/v1/agent/releases', data=body, headers=H, method='POST')).read())
up = urllib.request.Request(r['put_url'], data=open(zip_path, 'rb').read(),
headers={**(r.get('headers') or {}), 'Content-Type': 'application/zip'},
method='PUT')
urllib.request.urlopen(up, timeout=120)
urllib.request.urlopen(urllib.request.Request(
r['complete_url'], data=b'', headers={'Authorization': 'Bearer ' + T, 'User-Agent': UA}, method='POST'))
for _ in range(36):
d = json.loads(urllib.request.urlopen(urllib.request.Request(
f"https://lingzhanskill.com/api/v1/agent/releases/{r['slug']}/{r['version']}",
headers={'Authorization': 'Bearer ' + T, 'User-Agent': UA})).read())
if d['stage'] in ('published', 'blocked', 'returned', 'closed', 'in_review'):
break
time.sleep(5)
print(d)文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

