Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1e93a323f | |||
| f214598470 | |||
| 5abe67f340 | |||
| 599d7cff48 | |||
| f1060f2a26 | |||
| 864c1a5e2b | |||
| 3e3d8c79ad | |||
| ba3d86553f | |||
| a5b56c09f5 | |||
| daadf8e25c | |||
| 298448840d | |||
| f11c596bde | |||
| f973208fe3 | |||
| 5bc3ce6810 |
28
.github/workflows/release_skill.yaml
vendored
28
.github/workflows/release_skill.yaml
vendored
@@ -1,27 +1,13 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# 技能发布工作流(占位)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 不同组织使用不同的复用工作流、制品格式与加密策略。
|
||||
# 使用本模板时,请将下面 jobs 整段替换为你们自己的 workflow,
|
||||
# 或删除本文件若暂不需要 GitHub Actions。
|
||||
#
|
||||
# 设计原则(行业常见):
|
||||
# - 通过 tag 触发发布(如 v*)
|
||||
# - 制品与版本号可追溯,与 SKILL.md 中 version 对齐
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
name: skill-release-placeholder
|
||||
|
||||
name: 技能自动化发布
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Placeholder
|
||||
run: |
|
||||
echo "Replace this workflow with your organization's reusable workflow."
|
||||
echo "Example pattern: jobs.release.uses: <org>/<kit>/.github/workflows/reusable-release-skill.yaml@ref"
|
||||
exit 0
|
||||
uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
|
||||
secrets:
|
||||
PYARMOR_REG_B64: ${{ secrets.PYARMOR_REG_B64 }}
|
||||
with:
|
||||
artifact_platform: windows
|
||||
pyarmor_platform: windows.x86_64
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,8 +1,3 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
.env.*
|
||||
|
||||
54
README.md
54
README.md
@@ -1,23 +1,51 @@
|
||||
# 头条号批量发布(toutiao-publisher)
|
||||
# 匠厂 技能开发模板
|
||||
|
||||
基于团队 **skill-template** 骨架的头条号发布技能仓库;当前为**占位阶段**,仅含 CLI 骨架与文档,**发布功能待后续迭代**。
|
||||
这是一个**规范化的新技能模板仓库**,用于复制出新的 skill 项目;它本身**不是业务 skill**。
|
||||
|
||||
## 目录一览
|
||||
## 模板目标
|
||||
|
||||
| 路径 | 作用 |
|
||||
- 对齐当前规范 skill 的目录结构:`assets/`、`references/`、`scripts/`、`tests/`、`evals/`
|
||||
- 对齐当前规范脚手架分层:`scripts/cli`、`scripts/db`、`scripts/service`、`scripts/util`、`scripts/jiangchang_skill_core`
|
||||
- 提供最小可运行入口:`python scripts/main.py health` / `version`
|
||||
- 让新技能从一开始就按规范落地,不再沿用旧模板的 `docs/`、`optional/`、`skill_main.py` 结构
|
||||
|
||||
## 新技能使用步骤
|
||||
|
||||
1. 复制本目录为新的 skill 仓库。
|
||||
2. 全局替换 `your-skill-slug`、`your-platform-key`、`您的技能显示名称`、`你的平台名` 等占位内容。
|
||||
3. 修改 `SKILL.md`、`references/` 和 `scripts/util/constants.py`。
|
||||
4. 在 `scripts/service/` 中补业务 service 与真正的发布/执行逻辑。
|
||||
5. 用 `python scripts/main.py health` 和 `python scripts/main.py version` 做最小验证。
|
||||
|
||||
如果你的技能在平台里默认是非公开的(`access_scope = 0`),建议在 `SKILL.md` 的 `metadata.openclaw.developer_ids` 中填写开发者用户 ID 列表。这样发布后,平台会自动给这些开发者补可见权限,避免“技能已发布但开发者自己在市场中看不到”。
|
||||
|
||||
开发教程入口:
|
||||
|
||||
- <a href="references/REQUIREMENTS.md" target="_blank" rel="noopener noreferrer">需求文档模板</a>:给技术人员编写和查看研发需求的标准模板
|
||||
- <a href="references/DEVELOPMENT.md" target="_blank" rel="noopener noreferrer">开发教程</a>:给技术人员的完整开发步骤说明
|
||||
|
||||
## 目录说明
|
||||
|
||||
| 路径 | 用途 |
|
||||
|------|------|
|
||||
| `SKILL.md` | 技能清单(YAML 头 + Markdown 正文) |
|
||||
| `scripts/skill_main.py` | CLI 入口:`health` / `version` |
|
||||
| `docs/` | 运行时与可移植性说明 |
|
||||
| `optional/` | 可选片段(路径、SQLite 等),默认不引用 |
|
||||
| `SKILL.md` | 技能清单与触发说明模板 |
|
||||
| `assets/` | 示例输出与轻量 schema |
|
||||
| `references/` | 面向用户与编排的文档模板 |
|
||||
| `scripts/` | 规范分层后的代码骨架 |
|
||||
| `tests/` | 单元测试或最小回归测试 |
|
||||
| `evals/` | 人工/半自动评估材料 |
|
||||
| `.github/workflows/release_skill.yaml` | 标准发布工作流 |
|
||||
| `release.ps1` | 对齐现有 skill 的发布脚本入口 |
|
||||
|
||||
## 本地试跑
|
||||
## 最小命令
|
||||
|
||||
```bash
|
||||
python scripts/skill_main.py health
|
||||
python scripts/skill_main.py version
|
||||
python scripts/main.py health
|
||||
python scripts/main.py version
|
||||
```
|
||||
|
||||
## 版本
|
||||
## 注意
|
||||
|
||||
与 `SKILL.md` 中 `version` 字段对齐更新。
|
||||
- 不要再往模板里引入旧式 `docs/` 或 `optional/` 目录。
|
||||
- 新技能若不需要某些目录,也建议先保留结构,再按实际业务填充内容。
|
||||
- `metadata.openclaw.developer_ids` 是发布元数据,不是用户展示文案。它的作用是让发布后的非公开技能自动授权给指定开发者查看。
|
||||
|
||||
66
SKILL.md
66
SKILL.md
@@ -1,52 +1,48 @@
|
||||
---
|
||||
# ---------------------------------------------------------------------------
|
||||
# 技能清单(Skill Manifest)
|
||||
# ---------------------------------------------------------------------------
|
||||
name: 头条号批量发布
|
||||
description: 头条号批量发布技能(骨架阶段:仅健康检查与版本;发布逻辑待实现)。
|
||||
version: 0.1.0
|
||||
name: 技能开发模板(复制后请修改)
|
||||
description: "这是 OpenClaw 技能开发模板仓库,不直接作为业务技能发布。复制为新技能仓库后,按本模板替换 slug、名称、说明、CLI 子命令与 service 实现。"
|
||||
version: 1.0.13
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: toutiao-publisher
|
||||
emoji: "📰"
|
||||
category: "内容发布"
|
||||
skill:
|
||||
slug: toutiao-publisher
|
||||
emoji: "📰"
|
||||
category: "内容发布"
|
||||
slug: your-skill-slug
|
||||
emoji: "📦"
|
||||
category: "通用"
|
||||
developer_ids:
|
||||
- 1032
|
||||
- 12428
|
||||
allowed-tools:
|
||||
- bash
|
||||
---
|
||||
|
||||
# 头条号批量发布(toutiao-publisher)
|
||||
# 技能开发模板(skill-template)
|
||||
|
||||
## 使用时机
|
||||
这是一个**用于复制的新技能模板**,不是业务技能本身。新建技能时,应复制本仓库结构,再把占位内容替换成你的真实业务实现。
|
||||
|
||||
- 用户需要**在头条号侧批量或自动化发布内容**时(具体话术与流程待业务实现后补充)。
|
||||
## 模板使用方式
|
||||
|
||||
## 执行步骤
|
||||
1. 复制目录为你的新 skill 仓库。
|
||||
2. 全局替换 `your-skill-slug`、`技能开发模板(复制后请修改)` 等占位词。
|
||||
3. 按 `references/CLI.md`、`scripts/` 分层与 `README.md` 的说明补业务逻辑。
|
||||
|
||||
### 健康检查
|
||||
## 目录约定
|
||||
|
||||
- 根目录结构参考现有规范技能:`assets/`、`references/`、`scripts/`、`tests/`、`evals/`。
|
||||
- CLI 入口固定为 `scripts/main.py`。
|
||||
- 业务逻辑按 `cli / db / service / util / jiangchang_skill_core` 分层。
|
||||
|
||||
## 最小命令
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/skill_main.py health
|
||||
python {baseDir}/scripts/main.py health
|
||||
python {baseDir}/scripts/main.py version
|
||||
```
|
||||
|
||||
### 查看版本
|
||||
## 重要说明
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/skill_main.py version
|
||||
```
|
||||
|
||||
### 发布与其它子命令
|
||||
|
||||
实现中:后续将在 `scripts/` 下增加发布入口,并在此文档补充命令与参数说明。
|
||||
|
||||
## 环境依赖
|
||||
|
||||
详见本仓库 `docs/RUNTIME.md`(`CLAW_DATA_ROOT`、`CLAW_USER_ID` 等)。
|
||||
|
||||
## 数据与隐私
|
||||
|
||||
本技能若产生持久化数据,应仅写入 `{CLAW_DATA_ROOT}/{CLAW_USER_ID}/toutiao-publisher/` 下;不得将用户数据提交到版本库。
|
||||
- 复制后请同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`。
|
||||
- 如技能无需持久化,可保留 `db/` 目录但不主动调用。
|
||||
- `metadata.openclaw.developer_ids` 用于声明技能发布后的默认开发者可见用户 ID 列表。
|
||||
- 当技能在平台中 `access_scope = 0`(不公开)时,发布流程会把 `developer_ids` 中的用户自动补写到 `skill_user_access`,使这些开发者仍可在技能市场中查看该技能。
|
||||
- `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id`。
|
||||
- 面向用户与编排的文档写在 `references/`,不要再新增旧式 `docs/` / `optional/` 结构。
|
||||
|
||||
7
assets/README.md
Normal file
7
assets/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# assets
|
||||
|
||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||
- `schemas/`:轻量 JSON Schema 示例(`additionalProperties: true` 允许扩展字段)。
|
||||
|
||||
- 面向用户的介绍见 `references/README.md`。
|
||||
- 面向编排/CLI 的细节见 `references/CLI.md`、`RUNTIME.md`、`SCHEMA.md`。
|
||||
4
assets/examples/README.md
Normal file
4
assets/examples/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# 示例 JSON
|
||||
|
||||
- `version-response.json`:`python main.py version` 成功时 stdout 单行对象的形状参考。
|
||||
- `log-get-response.json`:若技能有日志表,`log-get` 成功时单条对象的形状参考。
|
||||
10
assets/examples/log-get-response.json
Normal file
10
assets/examples/log-get-response.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": 1,
|
||||
"account_id": "demo_account_1",
|
||||
"article_id": 12,
|
||||
"article_title": "示例标题",
|
||||
"status": "published",
|
||||
"error_msg": null,
|
||||
"created_at": "2026-04-01T10:00:00",
|
||||
"updated_at": "2026-04-01T10:00:00"
|
||||
}
|
||||
4
assets/examples/version-response.json
Normal file
4
assets/examples/version-response.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"skill": "your-skill-slug"
|
||||
}
|
||||
19
assets/schemas/publish-log-record.schema.json
Normal file
19
assets/schemas/publish-log-record.schema.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openclaw.local/skill-template/publish-log-record.schema.json",
|
||||
"title": "PublishLogRecord",
|
||||
"description": "发布型技能 log-get 返回的单条日志对象模板",
|
||||
"type": "object",
|
||||
"required": ["id", "account_id", "article_id", "article_title", "status", "created_at", "updated_at"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"account_id": { "type": ["string", "integer"] },
|
||||
"article_id": { "type": "integer" },
|
||||
"article_title": { "type": "string" },
|
||||
"status": { "type": "string" },
|
||||
"error_msg": { "type": ["string", "null"] },
|
||||
"created_at": { "type": ["string", "null"] },
|
||||
"updated_at": { "type": ["string", "null"] }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
BIN
assets/screenshots/gitea-workflow-success.png
Normal file
BIN
assets/screenshots/gitea-workflow-success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/screenshots/gitea-workflow-tab.png
Normal file
BIN
assets/screenshots/gitea-workflow-tab.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
assets/screenshots/jc2009-download-page.png
Normal file
BIN
assets/screenshots/jc2009-download-page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
assets/screenshots/market-installed-skill.png
Normal file
BIN
assets/screenshots/market-installed-skill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/screenshots/new-task-usage.png
Normal file
BIN
assets/screenshots/new-task-usage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -1,56 +0,0 @@
|
||||
# 多宿主(多 Claw)可移植性说明
|
||||
|
||||
## 设计目标
|
||||
|
||||
同一套技能仓库应能在**不同 Claw 实现**下工作,只要宿主满足本文档的**最小契约**:能启动进程、传入环境变量、并把 `SKILL.md` 提供给编排层阅读。
|
||||
|
||||
## 行业上常见的「技能包」形态
|
||||
|
||||
- **声明式清单**(Markdown + YAML 头):描述名称、描述、版本、工具权限、触发场景。
|
||||
- **可执行入口**:一至多个脚本/二进制,由宿主通过 `bash` / `python` 等调用。
|
||||
- **用户数据与代码分离**:持久化数据落在用户可写目录,不写在安装目录内。
|
||||
|
||||
本模板按上述惯例组织;具体宿主如何解析 `SKILL.md` 的 YAML 键名,以各宿主文档为准。
|
||||
|
||||
## 标识技能:`metadata.skill`
|
||||
|
||||
本模板在 `SKILL.md` 中使用 **`metadata.skill.slug`** 作为**可移植**的机器可读标识(短横线命名,如 `my-skill`)。
|
||||
|
||||
若你的宿主仍要求其它键名(例如历史实现里的嵌套字段),请在宿主侧做**映射**,或在 `SKILL.md` 中**并列声明**两组 metadata(保持 `slug` 值一致)。不要在业务代码里写死某一宿主品牌名。
|
||||
|
||||
## 环境变量:推荐前缀 `CLAW_*`
|
||||
|
||||
为减少对单一产品名的耦合,模板文档与可选片段推荐使用:
|
||||
|
||||
| 变量 | 含义 |
|
||||
|------|------|
|
||||
| `CLAW_DATA_ROOT` | 用户数据根目录(多技能共享的上一级) |
|
||||
| `CLAW_USER_ID` | 当前工作空间或用户标识,用于数据隔离 |
|
||||
| `CLAW_SKILLS_ROOT` | 可选;多个技能并排安装时的根目录,便于 `subprocess` 调用兄弟技能 |
|
||||
|
||||
宿主若已使用其它名称,推荐在**启动子进程时**注入别名,例如:
|
||||
|
||||
- 将宿主内部的「数据根」映射为 `CLAW_DATA_ROOT`
|
||||
- 将宿主内部「用户 ID」映射为 `CLAW_USER_ID`
|
||||
|
||||
这样技能脚本无需分支判断宿主品牌。
|
||||
|
||||
## 路径布局约定(逻辑路径)
|
||||
|
||||
在 `CLAW_DATA_ROOT` 与 `CLAW_USER_ID` 可用时,本技能推荐将私有数据放在:
|
||||
|
||||
```text
|
||||
{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/
|
||||
```
|
||||
|
||||
其中 `skill_slug` 与 `SKILL.md` 中 `metadata.skill.slug` 一致。若环境变量缺失,`optional/paths_snippet.py` 中提供了**仅用于开发机**的 fallback(见该文件注释),生产环境应由宿主注入变量。
|
||||
|
||||
## 发布与制品
|
||||
|
||||
不同组织对加密、签名、制品格式要求不同。`.github/workflows/release_skill.yaml` 仅作占位:**务必替换**为你们自己的复用工作流或删除。模板不假设必须使用某一家的发布流水线。
|
||||
|
||||
## 自检
|
||||
|
||||
- [ ] `SKILL.md` 中 `slug` 与目录名/制品名策略是否与宿主一致
|
||||
- [ ] 宿主文档要求的环境变量是否已全部注入
|
||||
- [ ] 是否在文档中说明了「未注入变量时的行为」(拒绝运行 / 本地 fallback)
|
||||
@@ -1,55 +0,0 @@
|
||||
# 运行时契约(环境变量与目录)
|
||||
|
||||
本文档定义技能进程**建议依赖**的外部条件,便于不同 Claw 宿主统一接入。业务技能应**读取环境变量**,而不是在代码里写死路径或用户名。
|
||||
|
||||
## 必需程度说明
|
||||
|
||||
- **强烈建议**:生产环境由宿主注入;技能应对缺失给出明确错误提示,避免静默写到意外目录。
|
||||
- **可选**:没有时技能仍可部分运行(例如只读 `health`)。
|
||||
|
||||
## 变量一览
|
||||
|
||||
### `CLAW_DATA_ROOT`(强烈建议)
|
||||
|
||||
用户数据根。多个技能、多个用户的数据都在此根之下分区。
|
||||
|
||||
- 典型场景:组织策略指定的盘符路径或 `~/.your-org-data`。
|
||||
- 未设置时:技能**不应**猜测网络盘;开发机 fallback 仅限 `optional/paths_snippet.py` 中说明的情形。
|
||||
|
||||
### `CLAW_USER_ID`(强烈建议)
|
||||
|
||||
当前会话所代表的用户或工作空间 ID(字符串)。与数据隔离强相关。
|
||||
|
||||
- 用于拼接:`{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/`
|
||||
- 未设置时:可用匿名占位(如 `_anon`)**仅用于开发**,生产应显式注入。
|
||||
|
||||
### `CLAW_SKILLS_ROOT`(可选)
|
||||
|
||||
多个技能以并列目录安装时的根路径,例如:
|
||||
|
||||
```text
|
||||
{CLAW_SKILLS_ROOT}/skill-a/scripts/...
|
||||
{CLAW_SKILLS_ROOT}/skill-b/scripts/...
|
||||
```
|
||||
|
||||
编排型技能若需要通过子进程调用兄弟技能,应基于该变量定位脚本,避免写死绝对路径。
|
||||
|
||||
## 本技能推荐的数据目录
|
||||
|
||||
```text
|
||||
{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/
|
||||
```
|
||||
|
||||
- `skill_slug`:与 `SKILL.md` 内 `metadata.skill.slug` 一致。
|
||||
- 在此目录下可放置 SQLite 文件、缓存、上传临时文件等;**不要**向版本库提交该目录内容。
|
||||
|
||||
## 标准输出约定(建议)
|
||||
|
||||
为便于宿主与自动化解析,建议:
|
||||
|
||||
- 致命错误:单行前缀 `ERROR:`,例如 `ERROR:MISSING_ENV_CLAW_DATA_ROOT`
|
||||
- 成功:人类可读一行或多行;若有机读需求,可用 `JSON` 单行输出并在 `SKILL.md` 中说明格式。
|
||||
|
||||
## 与具体宿主的关系
|
||||
|
||||
若某宿主文档规定了另一套变量名,应在**宿主启动技能子进程时**注入为本文档中的 `CLAW_*` 名称,或在技能内使用一层极薄的 `getenv` 封装(见 `optional/paths_snippet.py` 注释示例)。**不要在业务模块中散落多套变量名判断。**
|
||||
@@ -1,33 +0,0 @@
|
||||
# 技能形态与自检清单(无业务)
|
||||
|
||||
开发前先选定形态,避免把「编排、存储、浏览器」混在一个脚本里难以测试。
|
||||
|
||||
## 类型 A:无状态工具型
|
||||
|
||||
- **特征**:不持久化用户数据,或只读配置文件;输入输出主要在 stdin/stdout。
|
||||
- **数据目录**:通常不需要 `CLAW_DATA_ROOT` 下的专属库;若需要缓存,仍建议放在契约目录下。
|
||||
- **自检**:离线可跑;`health` 不访问网络也可成功。
|
||||
|
||||
## 类型 B:本地持久化型
|
||||
|
||||
- **特征**:使用 SQLite、本地文件等保存用户数据。
|
||||
- **数据目录**:必须使用 `{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/`。
|
||||
- **自检**:首次运行自动建库/建表;文档中说明库文件路径与备份方式。
|
||||
|
||||
## 类型 C:编排型(调用其它技能或外部 CLI)
|
||||
|
||||
- **特征**:自身逻辑薄,主要 `subprocess` 或 HTTP 调用其它组件。
|
||||
- **依赖**:在 `SKILL.md` 中明确写出**先决条件**(兄弟技能已安装、某 CLI 在 PATH 等)。
|
||||
- **自检**:`health` 可检查兄弟可执行文件是否存在;缺失时打印清晰错误。
|
||||
|
||||
## 类型 D:混合型
|
||||
|
||||
- **特征**:既有本地存储,又调用外部能力。
|
||||
- **建议**:拆模块(存储 / 编排 / 领域逻辑),入口脚本只做参数解析与调度。
|
||||
|
||||
## 发布前通用自检
|
||||
|
||||
- [ ] `SKILL.md` 中触发条件与示例命令与实际入口一致
|
||||
- [ ] 未注入 `CLAW_DATA_ROOT` / `CLAW_USER_ID` 时行为已文档化
|
||||
- [ ] 不向仓库提交用户数据、密钥、大型二进制
|
||||
- [ ] 错误信息包含「如何修复」(缺什么环境变量、缺哪个依赖)
|
||||
5
evals/README.md
Normal file
5
evals/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# evals
|
||||
|
||||
这里放人工评估脚本、示例输入输出、回归验收清单等材料。
|
||||
|
||||
模板仓库保持最小占位,不再放旧式杂项代码片段。
|
||||
@@ -1,17 +0,0 @@
|
||||
# optional/ 目录说明
|
||||
|
||||
本目录下的文件**不会**被 `scripts/skill_main.py` 自动引用。
|
||||
|
||||
## 为什么要单独放
|
||||
|
||||
- 避免模板入口脚本依赖过多,同事从「最小 health」起步时零干扰。
|
||||
- 需要时**整文件复制**到 `scripts/` 或你们自己的包路径下,再按文件头注释改名、改常量。
|
||||
|
||||
## 文件列表
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `paths_snippet.py` | `CLAW_*` 数据目录解析与 fallback 说明 |
|
||||
| `sqlite_minimal.py` | 无业务含义的 SQLite 建表示例 |
|
||||
|
||||
本技能数据子目录与 `SKILL_SLUG` 为 `toutiao-publisher`;若复制片段到其它项目请改为对应 slug 与表名。
|
||||
@@ -1,92 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
可选片段:路径与环境变量(请复制到 scripts/ 或作为独立模块使用)
|
||||
================================================================
|
||||
|
||||
【用途】
|
||||
统一解析「用户数据根」「用户 ID」「本技能私有目录」,避免在业务代码里重复 os.getenv。
|
||||
|
||||
【使用步骤】
|
||||
1. 将本文件复制到 skills/your-slug/scripts/paths.py(或任意模块名)。
|
||||
2. 把 SKILL_SLUG 改为与 SKILL.md 中 metadata.skill.slug 一致。
|
||||
3. 在业务代码中: from paths import get_skill_data_dir (按实际包路径调整)
|
||||
|
||||
【可移植性】
|
||||
- 优先读取标准名 CLAW_*(见 docs/RUNTIME.md)。
|
||||
- 若你的组织在宿主侧仍使用历史变量名,可在此文件 _aliases 列表中追加 (标准名, 备选名),
|
||||
由宿主注入其一即可;不要在业务里再写第三套名字。
|
||||
|
||||
【注意】
|
||||
- 未设置 CLAW_DATA_ROOT 时的 fallback 仅适合开发机;生产环境应由宿主注入。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# TODO: 复制本文件后改为你的 slug(与 SKILL.md metadata.skill.slug 一致)
|
||||
SKILL_SLUG = "toutiao-publisher"
|
||||
|
||||
|
||||
def _getenv_first(names: tuple[str, ...]) -> str:
|
||||
"""按顺序读取多个环境变量名,返回第一个非空值。"""
|
||||
for n in names:
|
||||
v = (os.getenv(n) or "").strip()
|
||||
if v:
|
||||
return v
|
||||
return ""
|
||||
|
||||
|
||||
def get_data_root() -> str:
|
||||
"""
|
||||
用户数据根目录。
|
||||
顺序:CLAW_DATA_ROOT → (可选)宿主别名,见下方元组。
|
||||
若皆空:Windows 默认 D:\\claw-data;其它系统默认 ~/.claw-data —— 仅开发便利,生产请注入 CLAW_DATA_ROOT。
|
||||
"""
|
||||
root = _getenv_first(
|
||||
(
|
||||
"CLAW_DATA_ROOT",
|
||||
# 在此追加组织内别名,例如 "MYORG_USER_DATA_ROOT",
|
||||
)
|
||||
)
|
||||
if root:
|
||||
return root
|
||||
if sys.platform == "win32":
|
||||
return r"D:\claw-data"
|
||||
return os.path.join(os.path.expanduser("~"), ".claw-data")
|
||||
|
||||
|
||||
def get_user_id() -> str:
|
||||
"""当前用户或工作空间 ID;未设置时用 _anon(仅开发)。"""
|
||||
uid = _getenv_first(
|
||||
(
|
||||
"CLAW_USER_ID",
|
||||
# 在此追加别名,例如 "MYORG_WORKSPACE_ID",
|
||||
)
|
||||
)
|
||||
return uid or "_anon"
|
||||
|
||||
|
||||
def get_skill_data_dir() -> str:
|
||||
"""
|
||||
本技能可写目录:{数据根}/{用户ID}/{skill_slug}/
|
||||
会自动 os.makedirs(..., exist_ok=True)。
|
||||
"""
|
||||
path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_skills_root() -> str:
|
||||
"""
|
||||
可选:并列安装的多技能根目录。
|
||||
未设置 CLAW_SKILLS_ROOT 时,默认使用本文件所在仓库的上一级目录的上一级
|
||||
(即:.../toutiao-publisher/optional/ → 仅作开发时并列技能推断参考)。
|
||||
"""
|
||||
root = _getenv_first(("CLAW_SKILLS_ROOT",))
|
||||
if root:
|
||||
return root
|
||||
# 模板占位:脚本位于 scripts/,仓库根为 dirname(dirname(__file__))
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.dirname(here)
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
可选片段:最小 SQLite 示例(请复制后按需修改)
|
||||
==============================================
|
||||
|
||||
【用途】
|
||||
演示「单表 + 自增主键 + INTEGER 时间戳」的一种严谨写法;与具体业务无关。
|
||||
|
||||
【使用步骤】
|
||||
1. 复制到 scripts/db_example.py(或并入你的模块)。
|
||||
2. 修改 TABLE_SQL 中的表名与字段;保持时间字段为 INTEGER Unix 秒(UTC)若需跨时区一致。
|
||||
3. 在入口脚本中仅在需要持久化时 import。
|
||||
|
||||
【注意】
|
||||
- 不在模板中做迁移兼容;schema 变更请用你们组织的迁移策略。
|
||||
- 数据库文件路径建议:get_skill_data_dir() / "skill.db"(paths_snippet 中函数)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import time
|
||||
|
||||
# 示例表:与任何业务无关
|
||||
TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS template_audit (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键
|
||||
action TEXT NOT NULL, -- 动作标识,如 health_ok
|
||||
created_at INTEGER NOT NULL -- Unix 秒 UTC
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def connect(db_path: str) -> sqlite3.Connection:
|
||||
return sqlite3.connect(db_path)
|
||||
|
||||
|
||||
def init_db(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(TABLE_SQL)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def record_action(conn: sqlite3.Connection, action: str) -> None:
|
||||
conn.execute(
|
||||
"INSERT INTO template_audit (action, created_at) VALUES (?, ?)",
|
||||
(action, int(time.time())),
|
||||
)
|
||||
conn.commit()
|
||||
36
references/CLI.md
Normal file
36
references/CLI.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# skill-template CLI 模板
|
||||
|
||||
将 `{baseDir}` 替换为技能根目录(含 `SKILL.md`、`scripts/` 的目录)。所有命令通过 `python {baseDir}/scripts/main.py` 调用。
|
||||
|
||||
## 最小命令
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/main.py health
|
||||
python {baseDir}/scripts/main.py version
|
||||
```
|
||||
|
||||
## 若你的技能是发布型
|
||||
|
||||
建议继续扩展这些子命令:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/main.py publish
|
||||
python {baseDir}/scripts/main.py publish -a <账号id> -i <文章id>
|
||||
python {baseDir}/scripts/main.py logs
|
||||
python {baseDir}/scripts/main.py log-get <log_id>
|
||||
```
|
||||
|
||||
## 若你的技能依赖兄弟技能
|
||||
|
||||
并列技能与 `{baseDir}` 同级时,兄弟技能路径写为:
|
||||
|
||||
```bash
|
||||
python {baseDir}/../content-manager/scripts/main.py ...
|
||||
python {baseDir}/../account-manager/scripts/main.py ...
|
||||
```
|
||||
|
||||
## 模板约定
|
||||
|
||||
- 最小模板至少保留 `health` / `version`
|
||||
- 发布型技能建议使用 `publish` / `logs` / `log-get`
|
||||
- 不要再用旧模板的 `scripts/skill_main.py`
|
||||
661
references/DEVELOPMENT.md
Normal file
661
references/DEVELOPMENT.md
Normal file
@@ -0,0 +1,661 @@
|
||||
# 技能开发教程
|
||||
|
||||
这份文档是给**技术人员**看的,目标不是解释概念,而是让你拿到 `skill-template` 后,可以**一步一步开发出一个新的 skill**。
|
||||
|
||||
本文默认你开发的是当前最常见的一类技能:
|
||||
|
||||
- 有明确的 `scripts/main.py` CLI 入口
|
||||
- 可能需要读写本地 SQLite
|
||||
- 可能需要调用兄弟技能
|
||||
- 业务逻辑主要放在 `scripts/service/`
|
||||
|
||||
如果你开发的是发布型 skill,这个模板就是直接可用的起点。
|
||||
|
||||
## 推荐 AI 开发工具
|
||||
|
||||
当前 skill 开发建议尽量配合 AI 编程工具使用。这样做不是为了替代技术人员,而是为了提升以下环节的效率:
|
||||
|
||||
- 搭建标准目录结构
|
||||
- 生成样板代码
|
||||
- 理解旧项目代码
|
||||
- 批量补文档、注释和测试
|
||||
- 辅助排查报错与重构代码
|
||||
|
||||
建议团队统一选择 1 到 2 个主力工具长期使用,避免每个人工具链差异太大,导致协作方式不一致。
|
||||
|
||||
下面先列国外主流工具,再列国内主流工具。链接优先使用官方站点、官方文档或官方安装入口。
|
||||
|
||||
### 国外主流工具
|
||||
|
||||
| 工具 | 类型 | 适合场景 | 官方入口 |
|
||||
|------|------|----------|----------|
|
||||
| Cursor | 独立 AI IDE | 代码编辑、Agent 开发、整仓理解 | <a href="https://www.cursor.com/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://www.cursor.com/downloads" target="_blank" rel="noopener noreferrer">下载</a> |
|
||||
| Windsurf | 独立 AI IDE | Agent 编程、项目生成、连续开发流 | <a href="https://docs.codeium.com/windsurf" target="_blank" rel="noopener noreferrer">文档</a> / <a href="https://windsurf.com/download" target="_blank" rel="noopener noreferrer">下载</a> |
|
||||
| GitHub Copilot | IDE 插件 / 编程助手 | 日常补全、解释代码、生成函数、配合 VS Code 或 JetBrains 使用 | <a href="https://github.com/copilot" target="_blank" rel="noopener noreferrer">官网</a> |
|
||||
| Claude Code | 终端 / IDE 编程代理 | 命令行开发、代码库分析、自动改代码、运行命令 | <a href="https://www.anthropic.com/claude-code" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://docs.anthropic.com/en/docs/claude-code/" target="_blank" rel="noopener noreferrer">文档</a> |
|
||||
| Codex | 终端 / IDE / Web 编程代理 | OpenAI 官方编码代理,适合代码生成、理解、调试、评审 | <a href="https://developers.openai.com/codex/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://developers.openai.com/codex/quickstart" target="_blank" rel="noopener noreferrer">快速开始</a> |
|
||||
| Aider | 终端 AI 编程工具 | 已有代码仓库的增量开发、终端协作、快速提交 | <a href="https://www.aider.chat/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://aider.chat/docs/" target="_blank" rel="noopener noreferrer">文档</a> |
|
||||
| Cline | VS Code / JetBrains 插件 | 编辑器内 Agent 开发、命令执行、浏览器联动调试 | <a href="https://cline.bot/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://docs.cline.bot/introduction/welcome" target="_blank" rel="noopener noreferrer">文档</a> / <a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank" rel="noopener noreferrer">VS Code 插件</a> |
|
||||
|
||||
### 国内主流工具
|
||||
|
||||
| 工具 | 类型 | 适合场景 | 官方入口 |
|
||||
|------|------|----------|----------|
|
||||
| Trae | 独立 AI IDE | AI 辅助写代码、项目搭建、对话式开发 | <a href="https://www.trae.ai/home" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://www.trae.ai/download" target="_blank" rel="noopener noreferrer">下载</a> |
|
||||
| 通义灵码 | 独立 IDE / IDE 插件 | 国内团队日常编码、问答、补全、代码生成 | <a href="https://tongyi.aliyun.com/lingma/?channel=yy_AiBot" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://tongyi.aliyun.com/lingma/download" target="_blank" rel="noopener noreferrer">下载</a> |
|
||||
| CodeGeeX | IDE 插件 / 开源助手 | 代码补全、生成、注释、跨语言辅助 | <a href="https://github.com/zai-org/CodeGeeX" target="_blank" rel="noopener noreferrer">GitHub</a> / <a href="https://marketplace.visualstudio.com/items?itemName=aminer.codegeex" target="_blank" rel="noopener noreferrer">VS Code 插件</a> |
|
||||
| 腾讯 CodeBuddy | IDE 插件 | 代码补全、测试生成、智能问答、腾讯云开发体系协作 | <a href="https://www.codebuddy.ai/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://www.tencentcloud.com/document/product/1256" target="_blank" rel="noopener noreferrer">文档</a> / <a href="https://marketplace.visualstudio.com/items?itemName=Tencent-Cloud.coding-copilot" target="_blank" rel="noopener noreferrer">VS Code 插件</a> |
|
||||
| 百度文心快码(Baidu Comate) | IDE 插件 | 国内研发团队辅助编码、解释、测试、优化 | <a href="https://comate.baidu.com/zh" target="_blank" rel="noopener noreferrer">官网</a> |
|
||||
|
||||
### 选型建议
|
||||
|
||||
如果你们团队主要做这类 Python skill 开发,我建议这样选:
|
||||
|
||||
- 想要一体化最强体验:优先试 `Cursor` 或 `Windsurf`
|
||||
- 想要命令行深度协作:优先试 `Claude Code` 或 `Aider`
|
||||
- 想继续基于 VS Code 插件体系:优先试 `GitHub Copilot`、`Cline`、`通义灵码`、`CodeBuddy`
|
||||
- 想优先使用国内生态与中文支持:优先试 `Trae`、`通义灵码`、`CodeGeeX`、`CodeBuddy`、`文心快码`
|
||||
|
||||
### 团队落地建议
|
||||
|
||||
为了减少培训成本,建议内部至少统一一套主工具方案:
|
||||
|
||||
- 国外方案:`Cursor` + `Claude Code`
|
||||
- 国内方案:`Trae` + `通义灵码`
|
||||
- VS Code 插件方案:`GitHub Copilot` + `Cline`
|
||||
|
||||
不建议每位技术人员完全自由发挥,否则后续在:
|
||||
|
||||
- 提示词写法
|
||||
- 代码修改习惯
|
||||
- 调试方式
|
||||
- 提交节奏
|
||||
|
||||
这些方面会越来越不统一。
|
||||
|
||||
## 1. 先理解模板的定位
|
||||
|
||||
`skill-template` 不是业务 skill,它只是一个**新 skill 仓库模板**。
|
||||
|
||||
你不应该直接在这个仓库里开发业务,而应该:
|
||||
|
||||
1. 复制这个目录
|
||||
2. 改成新 skill 的目录名
|
||||
3. 把占位内容替换掉
|
||||
4. 再开始写业务逻辑
|
||||
|
||||
## 2. 新 skill 的标准目录结构
|
||||
|
||||
复制模板后,你应该保留下面这套结构:
|
||||
|
||||
```text
|
||||
your-skill/
|
||||
├─ .github/
|
||||
├─ assets/
|
||||
├─ evals/
|
||||
├─ references/
|
||||
├─ scripts/
|
||||
│ ├─ cli/
|
||||
│ ├─ db/
|
||||
│ ├─ jiangchang_skill_core/
|
||||
│ ├─ service/
|
||||
│ └─ util/
|
||||
├─ tests/
|
||||
├─ .gitignore
|
||||
├─ README.md
|
||||
├─ release.ps1
|
||||
└─ SKILL.md
|
||||
```
|
||||
|
||||
各目录职责如下:
|
||||
|
||||
- `assets/`:放示例输出、schema、静态说明资源
|
||||
- `references/`:放研发和编排需要长期维护的文档
|
||||
- `scripts/`:放真正的代码
|
||||
- `tests/`:放自动化测试
|
||||
- `evals/`:放人工验收材料、评估清单、示例场景
|
||||
|
||||
## 3. `scripts/` 内部如何分层
|
||||
|
||||
`scripts/` 不是乱放 Python 文件,而是按职责分层:
|
||||
|
||||
```text
|
||||
scripts/
|
||||
├─ main.py
|
||||
├─ cli/
|
||||
├─ db/
|
||||
├─ jiangchang_skill_core/
|
||||
├─ service/
|
||||
└─ util/
|
||||
```
|
||||
|
||||
每层的职责要明确:
|
||||
|
||||
- `main.py`
|
||||
作用:唯一 CLI 入口
|
||||
只做启动、编码兼容、引入 `cli.app`
|
||||
|
||||
- `cli/`
|
||||
作用:参数解析、命令分发、用法说明
|
||||
不写具体业务逻辑
|
||||
|
||||
- `db/`
|
||||
作用:SQLite 连接、建表、repository
|
||||
不写页面操作和业务编排
|
||||
|
||||
- `service/`
|
||||
作用:核心业务逻辑
|
||||
比如发布流程、调用兄弟技能、浏览器自动化
|
||||
|
||||
- `util/`
|
||||
作用:常量、日志、路径、时间工具、通用帮助函数
|
||||
|
||||
- `jiangchang_skill_core/`
|
||||
作用:运行时环境与统一日志副本
|
||||
一般按现有规范技能复制,不要自己乱改结构
|
||||
|
||||
## 4. 开发一个新 skill 的标准步骤
|
||||
|
||||
下面这套顺序建议严格按步骤做,不要一上来就直接写 `service`。
|
||||
|
||||
### 第一步:复制模板并改目录名
|
||||
|
||||
例如你要开发 `weibo-publisher`:
|
||||
|
||||
1. 复制 `skill-template`
|
||||
2. 新目录改成 `weibo-publisher`
|
||||
3. 初始化为独立 git 仓库
|
||||
4. 关联它自己的远端仓库
|
||||
|
||||
目录名要和 skill slug 对齐,后面很多地方都依赖这个命名。
|
||||
|
||||
### 第二步:先改 4 个最关键的标识
|
||||
|
||||
复制后优先改下面这些地方:
|
||||
|
||||
1. `SKILL.md`
|
||||
2. `scripts/util/constants.py`
|
||||
3. `references/` 下的文案
|
||||
4. `scripts/service/` 下的平台占位文件名
|
||||
|
||||
最先要统一的是:
|
||||
|
||||
- 技能中文名
|
||||
- `slug`
|
||||
- 平台中文名
|
||||
- 平台内部键
|
||||
- 日志 logger 名
|
||||
|
||||
此外,如果该技能发布后默认不公开(`access_scope = 0`),建议一开始就把 `SKILL.md` 中的 `metadata.openclaw.developer_ids` 配好。这样后续发布到平台时,开发者本人仍能在技能市场中看到并验证该技能。
|
||||
|
||||
## 5. 哪些占位内容必须替换
|
||||
|
||||
复制后,至少要全局检查并替换下面这些内容:
|
||||
|
||||
- `your-skill-slug`
|
||||
- `your-platform-key`
|
||||
- `技能开发模板(复制后请修改)`
|
||||
- `你的平台名`
|
||||
- `platform_playwright.py`
|
||||
- `openclaw.skill.your_skill_slug`
|
||||
|
||||
如果你是做发布型 skill,通常还要替换:
|
||||
|
||||
- `publish` 命令中的中文提示
|
||||
- `references/CLI.md` 的命令示例
|
||||
- `references/README.md` 的用户话术
|
||||
- `references/SCHEMA.md` 的数据库文件名
|
||||
|
||||
## 6. `SKILL.md` 应该怎么写
|
||||
|
||||
`SKILL.md` 是技能清单,不是设计文档。
|
||||
|
||||
应该重点写:
|
||||
|
||||
- 技能名称
|
||||
- 技能描述
|
||||
- `slug`
|
||||
作用:技能的唯一英文标识,通常用于仓库名、发布包名、运行时目录名、平台主键匹配等
|
||||
示例:`weibo-publisher`、`douyin-publisher`、`xiaohongshu-publisher`
|
||||
- `category`
|
||||
作用:技能在平台中的分类,用于市场展示与归类,不是代码内部键
|
||||
示例:`通用`、`内容`、`办公协作`、`物流`、`抖音`、`小红书`
|
||||
- `developer_ids`(如需给非公开技能自动补开发者可见权限)
|
||||
作用:声明发布后默认拥有可见权限的开发者用户 ID 列表
|
||||
- `dependencies`
|
||||
作用:声明该技能依赖的兄弟技能或运行前置能力,便于平台或编排层识别依赖关系
|
||||
示例:
|
||||
```yaml
|
||||
dependencies:
|
||||
required:
|
||||
- account-manager
|
||||
- content-manager
|
||||
```
|
||||
- 何时使用本技能
|
||||
- 对用户的引导话术
|
||||
- CLI 使用原则
|
||||
|
||||
不要在 `SKILL.md` 里写大量实现细节。
|
||||
实现细节放在:
|
||||
|
||||
- `references/`
|
||||
- 代码注释
|
||||
- `service/` 实现里
|
||||
|
||||
### 关于 `metadata.openclaw.developer_ids`
|
||||
|
||||
这是一个平台发布元数据字段,用于解决下面这个问题:
|
||||
|
||||
- 技能发布后若平台记录中的 `access_scope = 0`,技能默认不公开
|
||||
- 如果不额外授权,连开发者自己也可能在技能市场里看不到这个技能
|
||||
|
||||
因此可以在 `SKILL.md` 中声明:
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: your-skill-slug
|
||||
category: 通用
|
||||
developer_ids:
|
||||
- 1032
|
||||
- 12428
|
||||
```
|
||||
|
||||
约定如下:
|
||||
|
||||
- 只允许填写正整数用户 ID
|
||||
- 推荐使用数组,即使当前只有 1 个开发者
|
||||
- 发布时平台会把这些用户自动补写到 `skill_user_access`
|
||||
- 第一个 ID 会同步到 `skills.developer_id`
|
||||
- 一期只做“补授权”,不会因为你 later 修改数组而自动撤销旧授权
|
||||
|
||||
## 7. `references/` 应该放什么
|
||||
|
||||
`references/` 是当前规范 skill 的文档中心,建议至少有这些:
|
||||
|
||||
- `README.md`
|
||||
面向内部说明和技能作用介绍
|
||||
|
||||
- `CLI.md`
|
||||
写清楚命令、参数、默认值、兄弟技能调用方式
|
||||
|
||||
- `RUNTIME.md`
|
||||
写清楚运行时目录、环境变量、入口约定
|
||||
|
||||
- `SCHEMA.md`
|
||||
写清楚数据库路径、核心表结构、日志表结构
|
||||
|
||||
- `DEVELOPMENT.md`
|
||||
写给技术人员的开发教程,也就是本文档
|
||||
|
||||
如果后面某个 skill 需要更细的说明,可以再加:
|
||||
|
||||
- `ERRORS.md`
|
||||
- `INTEGRATION.md`
|
||||
- `PLATFORMS.md`
|
||||
|
||||
## 8. `assets/` 应该放什么
|
||||
|
||||
`assets/` 不放业务代码,只放辅助材料。
|
||||
|
||||
建议放:
|
||||
|
||||
- `examples/`
|
||||
比如 `version` 输出示例、`log-get` 输出示例
|
||||
|
||||
- `schemas/`
|
||||
比如日志记录、机读 JSON 的 schema
|
||||
|
||||
不要把正式研发文档放到 `assets/`。
|
||||
文档应该进 `references/`。
|
||||
|
||||
## 9. `cli` 层怎么写
|
||||
|
||||
建议保持一个原则:
|
||||
|
||||
- `cli` 只负责解析参数和路由
|
||||
- `service` 才负责干活
|
||||
|
||||
也就是说,`cli/app.py` 的职责是:
|
||||
|
||||
1. 打印帮助
|
||||
2. 定义 `publish / logs / log-get / health / version`
|
||||
3. 把参数转交给 `service.publish_service`
|
||||
|
||||
不要在 `cli/app.py` 里直接写:
|
||||
|
||||
- 浏览器自动化
|
||||
- 子进程调用兄弟技能
|
||||
- SQLite 逻辑
|
||||
|
||||
## 10. `service` 层怎么写
|
||||
|
||||
`service` 是核心层。
|
||||
|
||||
通常可以这样拆:
|
||||
|
||||
- `publish_service.py`
|
||||
放命令编排、参数兜底、结果分流
|
||||
|
||||
- `sibling_bridge.py`
|
||||
放兄弟技能调用,例如调 `account-manager`、`content-manager`
|
||||
|
||||
- `*_playwright.py`
|
||||
放浏览器后台自动化
|
||||
|
||||
- `entitlement_service.py`
|
||||
放鉴权逻辑
|
||||
|
||||
### 一个很重要的原则
|
||||
|
||||
不要把所有逻辑都堆进一个文件。
|
||||
|
||||
推荐流向是:
|
||||
|
||||
`cli.app` -> `service.publish_service` -> `service.sibling_bridge` / `service.xxx_playwright` -> `db`
|
||||
|
||||
## 11. `db` 层怎么写
|
||||
|
||||
如果你的 skill 需要记录日志或本地状态,建议:
|
||||
|
||||
- `db/connection.py`
|
||||
只做连接和建表
|
||||
|
||||
- `db/publish_logs_repository.py`
|
||||
只做增删查改
|
||||
|
||||
不要在 `db` 层里:
|
||||
|
||||
- 调浏览器
|
||||
- 打印用户提示
|
||||
- 拼接业务流程
|
||||
|
||||
## 12. 如何接兄弟技能
|
||||
|
||||
如果 skill 要依赖兄弟 skill,不要在业务代码里写死绝对路径。
|
||||
|
||||
统一通过:
|
||||
|
||||
- `scripts/util/runtime_paths.py`
|
||||
- `scripts/service/sibling_bridge.py`
|
||||
|
||||
来做调用。
|
||||
|
||||
常见调用对象是:
|
||||
|
||||
- `account-manager`
|
||||
- `content-manager`
|
||||
|
||||
调用原则:
|
||||
|
||||
1. 通过 `get_skills_root()` 找到兄弟技能根目录
|
||||
2. 再拼出对应 `scripts/main.py`
|
||||
3. 用子进程调用
|
||||
4. 机器可读输出优先 JSON
|
||||
|
||||
## 13. 如何开发一个新 skill
|
||||
|
||||
不管你开发的是发布类、采集类、分析类还是知识库类 skill,建议都先按下面这个顺序推进:
|
||||
|
||||
1. 先把目录结构搭完整
|
||||
2. 先让 `health` / `version` 跑通
|
||||
3. 再把核心 `service` 骨架跑通
|
||||
4. 再接兄弟技能桥接、数据库或外部系统
|
||||
5. 最后再补浏览器自动化、复杂流程编排或高风险集成
|
||||
|
||||
不要一开始就直接写页面选择器、复杂接口编排或深层业务逻辑。
|
||||
|
||||
推荐先确保这些基础能力正常:
|
||||
|
||||
- CLI 入口能跑通
|
||||
- 基础命令输出稳定
|
||||
- 关键依赖能取到
|
||||
- 日志或本地状态能落下来
|
||||
- 错误返回值格式定好了
|
||||
|
||||
如果你的 skill 恰好是 publisher 类,可以把上面的“核心 `service`”具体落成 `publish_service.py`,再逐步接 `sibling_bridge.py`、`*_playwright.py`。但这只是示例,不代表模板只适合发布类技能。
|
||||
|
||||
## 14. 本地开发的最小验证顺序
|
||||
|
||||
建议每次新 skill 开发时按下面顺序验证:
|
||||
|
||||
### 1. 验证入口
|
||||
|
||||
```bash
|
||||
python scripts/main.py health
|
||||
python scripts/main.py version
|
||||
```
|
||||
|
||||
### 2. 验证 CLI 路由
|
||||
|
||||
```bash
|
||||
python scripts/main.py -h
|
||||
python scripts/main.py <your-command> -h
|
||||
```
|
||||
|
||||
### 3. 验证本地日志与数据库
|
||||
|
||||
如果你的 skill 需要本地日志或数据库,再继续:
|
||||
|
||||
```bash
|
||||
python scripts/main.py <your-log-command>
|
||||
python scripts/main.py <your-detail-command> <id>
|
||||
```
|
||||
|
||||
如果你沿用了模板中的发布型骨架,那么这里可以具体对应成:
|
||||
|
||||
```bash
|
||||
python scripts/main.py logs
|
||||
python scripts/main.py log-get 1
|
||||
```
|
||||
|
||||
### 4. 最后再验证真实业务
|
||||
|
||||
比如:
|
||||
|
||||
```bash
|
||||
python scripts/main.py <your-command>
|
||||
```
|
||||
|
||||
## 15. 发布到正式环境验证
|
||||
|
||||
当本地开发、自测和联调完成后,还需要把 skill 发布到正式环境做一次完整验证。建议技术人员严格按下面顺序执行,不要跳步。
|
||||
|
||||
### 第一步:在 skill 根目录执行 `release.ps1`
|
||||
|
||||
在当前 skill 仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
.\release.ps1
|
||||
```
|
||||
|
||||
如果你的技能使用了 `metadata.openclaw.developer_ids`,那么这一步触发的发布工作流除了同步 `skills` / `skill_versions` 外,还会在平台侧自动补开发者可见权限。测试非公开技能时,建议重点验证这部分是否生效。
|
||||
|
||||
这一步会自动完成标准发布动作,包括:
|
||||
|
||||
1. 检查当前仓库状态
|
||||
2. 自动提交尚未提交的改动
|
||||
3. 推送最新代码到远程仓库
|
||||
4. 自动创建新的语义化版本 tag
|
||||
5. 推送 tag,触发后续 CI 流程
|
||||
|
||||
如果你只是想先预览发布动作,可以先执行:
|
||||
|
||||
```powershell
|
||||
.\release.ps1 -DryRun
|
||||
```
|
||||
|
||||
### 第二步:到 Gitea 仓库查看工作流
|
||||
|
||||
发布命令执行完成后,远程仓库会自动触发 Gitea 工作流。此时应立即进入对应 skill 的仓库页面,切换到“工作流”页签查看执行状态。
|
||||
|
||||
重点确认:
|
||||
|
||||
- 是否已经出现最新一次发布记录
|
||||
- 是否由最新提交触发
|
||||
- 是否成功执行完 `release_skill.yaml`
|
||||
|
||||
下面这张图演示了在仓库页进入“工作流”的位置:
|
||||
|
||||

|
||||
|
||||
下面这张图演示了工作流成功后的状态页面:
|
||||
|
||||

|
||||
|
||||
### 第三步:确认工作流执行成功
|
||||
|
||||
只有当工作流状态为成功,才说明正式发布产物已经正确构建完成。
|
||||
|
||||
如果工作流失败,不要继续做平台验证,而应该先回到代码仓库排查,例如:
|
||||
|
||||
- 发布脚本是否正常推送
|
||||
- `SKILL.md`、`scripts/`、`references/` 是否齐全
|
||||
- 工作流文件是否存在
|
||||
- 发布包结构是否符合模板规范
|
||||
|
||||
### 第四步:进入匠厂平台下载安装包
|
||||
|
||||
当工作流成功后,就可以进入匠厂平台验证最终安装效果。
|
||||
|
||||
匠厂产品下载地址:
|
||||
|
||||
- [https://jc2009.com/product.html](https://jc2009.com/product.html)
|
||||
|
||||
打开页面后,下载并安装匠厂客户端。下面这张图演示了下载入口位置:
|
||||
|
||||

|
||||
|
||||
匠厂产品页可从这里进入:[产品下载 - 匠厂](https://jc2009.com/product.html)
|
||||
|
||||
### 第五步:安装匠厂后,在技能市场检查最新 skill
|
||||
|
||||
安装并启动匠厂后,进入左侧“技能市场”,搜索或查找刚刚发布的 skill,确认以下内容:
|
||||
|
||||
- 技能可以被正常检索到
|
||||
- 技能名称、说明、版本信息正确
|
||||
- 最新版本已经同步出来
|
||||
- 可以正常安装或更新
|
||||
|
||||
下面这张图演示了在技能市场中查看已发布 skill 的位置:
|
||||
|
||||

|
||||
|
||||
### 第六步:安装并实际启用该 skill
|
||||
|
||||
在技能市场中找到目标 skill 后,执行安装或更新操作,确保客户端本地已经拿到最新版本。
|
||||
|
||||
这一步建议至少确认:
|
||||
|
||||
- 安装按钮可以正常执行
|
||||
- 安装后状态正常
|
||||
- 不会出现缺文件、缺入口或安装失败的问题
|
||||
|
||||
### 第七步:在“新建任务”中实际使用该 skill
|
||||
|
||||
安装完成后,不要只停留在“已安装”状态,还需要进入“新建任务”页面,真正调用一次该 skill,完成最终验证。
|
||||
|
||||
建议至少验证:
|
||||
|
||||
- 新任务中可以正常选择或触发该 skill
|
||||
- skill 能被正确唤起
|
||||
- 主要命令或主流程可以运行
|
||||
- 返回结果、日志和行为符合预期
|
||||
|
||||
下面这张图演示了安装后在任务界面中实际使用 skill 的场景:
|
||||
|
||||

|
||||
|
||||
## 16. 发布前检查清单
|
||||
|
||||
每个新 skill 发布前,建议技术人员逐条确认:
|
||||
|
||||
- [ ] 目录结构符合当前模板
|
||||
- [ ] `SKILL.md` 中 slug、名称、描述都已替换
|
||||
- [ ] `scripts/util/constants.py` 已修改
|
||||
- [ ] `references/CLI.md` 示例命令已改成真实命令
|
||||
- [ ] `service` 下的平台文件名已改对
|
||||
- [ ] 没有残留旧平台名
|
||||
- [ ] `health` / `version` 可运行
|
||||
- [ ] `.gitignore` 生效,没有把 `__pycache__` 提交进去
|
||||
- [ ] `release.ps1` 存在
|
||||
- [ ] `.github/workflows/release_skill.yaml` 存在
|
||||
|
||||
## 17. 常见错误
|
||||
|
||||
### 错误 1:只改了目录名,没改 slug
|
||||
|
||||
表现:
|
||||
|
||||
- 数据目录不对
|
||||
- 版本输出不对
|
||||
- 日志名不对
|
||||
|
||||
要检查:
|
||||
|
||||
- `SKILL.md`
|
||||
- `scripts/util/constants.py`
|
||||
|
||||
### 错误 2:平台中文名改了,内部键没改
|
||||
|
||||
表现:
|
||||
|
||||
- 兄弟技能筛选账号失败
|
||||
- 发布命令走错平台
|
||||
|
||||
要检查:
|
||||
|
||||
- `publish_service.py`
|
||||
- `sibling_bridge.py`
|
||||
- `references/CLI.md`
|
||||
|
||||
### 错误 3:把业务逻辑写进 CLI
|
||||
|
||||
表现:
|
||||
|
||||
- 文件越来越乱
|
||||
- 不方便测
|
||||
- 参数和业务耦合太重
|
||||
|
||||
要改回:
|
||||
|
||||
- `cli` 只做参数解析
|
||||
- `service` 才做业务
|
||||
|
||||
### 错误 4:保留了旧模板结构
|
||||
|
||||
表现:
|
||||
|
||||
- 仓库同时存在 `docs/`、`optional/`、`skill_main.py`
|
||||
- 技术人员不知道看哪一套
|
||||
|
||||
现在的新模板原则是:
|
||||
|
||||
- 不做旧结构兼容
|
||||
- 统一走 `references/` + `scripts/main.py`
|
||||
|
||||
## 18. 推荐开发顺序总结
|
||||
|
||||
如果让一个新人照着做,我建议他按这个顺序:
|
||||
|
||||
1. 复制模板并改目录名
|
||||
2. 改 `SKILL.md`
|
||||
3. 改 `scripts/util/constants.py`
|
||||
4. 改 `references/`
|
||||
5. 改 `scripts/cli/app.py`
|
||||
6. 改 `scripts/service/`
|
||||
7. 跑 `health` / `version`
|
||||
8. 再做业务联调
|
||||
9. 最后 release
|
||||
|
||||
## 19. 这份模板的底线要求
|
||||
|
||||
以后新建 skill,至少要满足这几点:
|
||||
|
||||
- 目录结构统一
|
||||
- 入口统一为 `scripts/main.py`
|
||||
- 文档统一放 `references/`
|
||||
- 业务核心逻辑统一放 `scripts/service/`
|
||||
- 不再使用旧模板历史结构
|
||||
|
||||
如果做不到这些,后面 skill 一多,就会越来越乱。
|
||||
46
references/README.md
Normal file
46
references/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
description: "这是规范化的新技能模板说明,不直接作为业务技能使用。复制后请替换技能名、平台名、CLI 示例与 service 实现。"
|
||||
---
|
||||
|
||||
# 技能模板说明
|
||||
|
||||
这个仓库是**给开发者复制的新技能模板**,不是终端用户直接调用的业务 skill。
|
||||
|
||||
## 它提供什么
|
||||
|
||||
- 标准目录结构
|
||||
- 最小 CLI 入口
|
||||
- 发布型技能常见的日志表骨架
|
||||
- `service` 层占位模块
|
||||
- 与现有规范 skill 一致的发布脚本与 GitHub workflow
|
||||
|
||||
## 复制后你需要改什么
|
||||
|
||||
- `SKILL.md` 中的名称、描述、slug、触发说明
|
||||
- `SKILL.md` 中 `metadata.openclaw.developer_ids`(如需让非公开技能默认授权给开发者查看)
|
||||
- `references/CLI.md` 里的命令示例
|
||||
- `scripts/util/constants.py` 中的 slug / 版本 / logger 名
|
||||
- `scripts/service/` 下的真实业务实现
|
||||
|
||||
## `developer_ids` 是做什么的
|
||||
|
||||
`metadata.openclaw.developer_ids` 是平台发布元数据,不是终端用户文案。
|
||||
|
||||
它用于声明:当技能发布后,如果平台侧将该技能设置为 `access_scope = 0`(不公开),哪些开发者用户仍应自动获得可见权限。
|
||||
|
||||
约定如下:
|
||||
|
||||
- 字段位置:`SKILL.md` -> `metadata.openclaw.developer_ids`
|
||||
- 推荐格式:正整数数组,例如 `[1032, 12428]`
|
||||
- 发布效果:发布接口会把这些用户补写到 `skill_user_access`
|
||||
- 第一个 ID 会作为主开发者同步到 `skills.developer_id`
|
||||
|
||||
如果你的技能本来就是公开技能,或暂时不需要开发者专属可见性,这个字段可以留空数组。
|
||||
|
||||
## 不建议再保留的旧结构
|
||||
|
||||
- 旧模板里的 `docs/`
|
||||
- 旧模板里的 `optional/`
|
||||
- 旧入口 `scripts/skill_main.py`
|
||||
|
||||
新模板统一使用 `scripts/main.py` 作为入口。
|
||||
338
references/REQUIREMENTS.md
Normal file
338
references/REQUIREMENTS.md
Normal file
@@ -0,0 +1,338 @@
|
||||
# 技能需求文档模板
|
||||
|
||||
这份文档用于给技术人员、产品人员和实施人员统一描述某个 skill 的研发需求。
|
||||
|
||||
建议原则:
|
||||
|
||||
- 一个 skill 对应一份主需求文档
|
||||
- 需求先写清楚,再进入开发
|
||||
- 文档描述“要做什么”和“做到什么程度”,不要在这里堆实现细节
|
||||
- 实现方式、代码结构、发布流程,分别放到 `DEVELOPMENT.md`、`CLI.md`、`SCHEMA.md`、`RUNTIME.md`
|
||||
|
||||
如果你是从 `skill-template` 复制新技能,请复制本文件后,把下面所有占位内容替换成你的真实项目内容。
|
||||
|
||||
---
|
||||
|
||||
# REQUIREMENTS
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
说明这份需求文档的作用,以及它解决什么问题。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 本文档用于明确 `【技能名称】` 的研发范围、交付标准、依赖关系和验收要求。
|
||||
- 技术人员应以本文件作为开发范围依据,以避免实现偏差、范围蔓延或理解不一致。
|
||||
|
||||
示例:
|
||||
|
||||
- 本文档用于明确 `weibo-publisher` 的研发需求,约束该 skill 的功能边界、输入输出和验收标准,作为开发、测试和上线验证的统一依据。
|
||||
|
||||
## 2. 业务背景
|
||||
|
||||
说明为什么要做这个 skill,它服务什么业务场景,解决什么实际问题。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 当前业务场景中,用户需要在 `【目标平台】` 完成 `【业务动作】`。
|
||||
- 现有流程主要依赖人工处理,存在 `【效率低 / 容易出错 / 不可批量 / 不可追踪】` 等问题。
|
||||
- 因此需要开发 `【技能名称】`,将该流程标准化、自动化,并接入匠厂技能体系中。
|
||||
|
||||
示例:
|
||||
|
||||
- 当前内容运营团队需要将内容库中的文章批量发布到微博平台,但人工登录后台、复制文章、填写标题和确认发布的流程效率较低,且不便于统一记录发布历史。
|
||||
- 因此需要开发 `weibo-publisher`,用于把内容库文章按规范发布到微博平台,并记录发布结果,支持后续排查与复用。
|
||||
|
||||
## 3. 开发目标
|
||||
|
||||
说明本次开发希望最终交付什么结果。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 完成 `【技能名称】` 的标准目录结构搭建
|
||||
- 完成 CLI 入口与基础命令
|
||||
- 完成核心业务流程
|
||||
- 完成与兄弟技能或外部系统的必要集成
|
||||
- 完成正式环境发布验证
|
||||
|
||||
示例:
|
||||
|
||||
- 完成 `weibo-publisher` 的标准 skill 结构建设
|
||||
- 提供 `health`、`version`、`publish`、`logs`、`log-get` 等命令
|
||||
- 支持从兄弟技能读取账号与文章,并执行微博发布流程
|
||||
- 支持记录发布日志,并可在正式环境中安装验证
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
说明这次开发明确要实现哪些功能。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 支持 `【命令1】`
|
||||
- 支持 `【命令2】`
|
||||
- 支持读取 `【数据来源】`
|
||||
- 支持执行 `【核心业务动作】`
|
||||
- 支持记录 `【日志 / 状态 / 结果】`
|
||||
|
||||
建议将功能拆成“必须实现”和“可后续扩展”。
|
||||
|
||||
示例:
|
||||
|
||||
### 必须实现
|
||||
|
||||
- 支持 `python scripts/main.py health`
|
||||
- 支持 `python scripts/main.py version`
|
||||
- 支持 `python scripts/main.py publish`
|
||||
- 支持从 `account-manager` 获取可用账号
|
||||
- 支持从 `content-manager` 获取待发布文章
|
||||
- 支持执行微博后台发布流程
|
||||
- 支持写入发布日志
|
||||
|
||||
### 可后续扩展
|
||||
|
||||
- 支持定时发布
|
||||
- 支持失败自动重试
|
||||
- 支持多账号轮询发布
|
||||
|
||||
## 5. 非功能要求
|
||||
|
||||
说明除功能外,对稳定性、可维护性、目录规范、日志、编码等方面的要求。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 目录结构必须符合当前 skill 模板规范
|
||||
- 入口必须统一为 `scripts/main.py`
|
||||
- 输出格式应尽量机读友好
|
||||
- 错误前缀统一为 `ERROR:`
|
||||
- Windows 环境下需保证 UTF-8 输出兼容
|
||||
- 必须具备基本日志能力
|
||||
|
||||
示例:
|
||||
|
||||
- 项目结构必须对齐 `skill-template`
|
||||
- CLI 入口统一使用 `scripts/main.py`
|
||||
- 关键执行结果应可通过 JSON 或固定文本结构返回
|
||||
- 发布日志必须可追踪
|
||||
- 不允许重新引入旧模板中的 `docs/`、`optional/` 或 `scripts/skill_main.py`
|
||||
|
||||
## 6. 输入输出要求
|
||||
|
||||
说明这个 skill 接收什么输入,产出什么输出。
|
||||
|
||||
模板写法:
|
||||
|
||||
### 输入
|
||||
|
||||
- `【输入参数1】`
|
||||
- `【输入参数2】`
|
||||
- `【依赖系统返回的数据】`
|
||||
|
||||
### 输出
|
||||
|
||||
- `【标准输出】`
|
||||
- `【错误输出】`
|
||||
- `【数据库记录】`
|
||||
- `【日志文件】`
|
||||
|
||||
示例:
|
||||
|
||||
### 输入
|
||||
|
||||
- `account_id`:可选,指定发布账号
|
||||
- `article_id`:可选,指定发布文章
|
||||
- `account-manager` 返回的账号信息
|
||||
- `content-manager` 返回的文章信息
|
||||
|
||||
### 输出
|
||||
|
||||
- 发布成功时返回成功结果
|
||||
- 发布失败时返回 `ERROR:` 或 `FAIL:` 前缀的错误信息
|
||||
- 在本地数据库中写入 `publish_logs`
|
||||
- 在日志目录中写入执行日志
|
||||
|
||||
## 7. 依赖的兄弟技能或外部系统
|
||||
|
||||
说明开发该 skill 时依赖哪些内部技能或外部平台。
|
||||
|
||||
模板写法:
|
||||
|
||||
### 兄弟技能依赖
|
||||
|
||||
- `【skill-a】`:作用是 `【用途】`
|
||||
- `【skill-b】`:作用是 `【用途】`
|
||||
|
||||
### 外部系统依赖
|
||||
|
||||
- `【平台名】`:作用是 `【用途】`
|
||||
- `【浏览器 / API / 第三方服务】`:作用是 `【用途】`
|
||||
|
||||
示例:
|
||||
|
||||
### 兄弟技能依赖
|
||||
|
||||
- `account-manager`:提供账号信息
|
||||
- `content-manager`:提供文章数据
|
||||
|
||||
### 外部系统依赖
|
||||
|
||||
- 微博后台:目标发布平台
|
||||
- Chromium / Chrome / Edge:用于浏览器自动化
|
||||
- Gitea:用于代码托管和发布工作流
|
||||
- 匠厂客户端:用于正式环境安装与验证
|
||||
|
||||
## 8. 不在本次范围内的内容
|
||||
|
||||
这一节非常重要,用来明确本次“不做什么”,避免研发越做越散。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 本次不实现 `【功能A】`
|
||||
- 本次不处理 `【平台B】`
|
||||
- 本次不做 `【高级能力】`
|
||||
- 本次不兼容 `【旧结构 / 旧逻辑】`
|
||||
|
||||
示例:
|
||||
|
||||
- 本次不实现多平台统一发布
|
||||
- 本次不实现定时任务调度
|
||||
- 本次不实现自动重试机制
|
||||
- 本次不兼容旧模板中的历史目录结构
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
说明什么情况下才算真正开发完成。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 代码结构符合模板规范
|
||||
- 最小命令可运行
|
||||
- 核心命令可运行
|
||||
- 正式环境可安装
|
||||
- 匠厂中可看到最新版本
|
||||
- 可以在新建任务中实际使用
|
||||
|
||||
示例:
|
||||
|
||||
- `health`、`version` 命令执行正常
|
||||
- `publish` 主流程可运行
|
||||
- 发布后 Gitea 工作流成功
|
||||
- 匠厂技能市场能看到最新版本
|
||||
- skill 可以正常安装
|
||||
- 安装后可在“新建任务”中调用
|
||||
|
||||
## 10. 开发注意事项
|
||||
|
||||
说明研发过程中必须特别注意的事项。
|
||||
|
||||
模板写法:
|
||||
|
||||
- 不要修改无关 skill
|
||||
- 不要破坏现有模板规范
|
||||
- 优先遵守目录和分层约定
|
||||
- 不要在 CLI 层堆业务逻辑
|
||||
- 发布前必须先做本地最小验证
|
||||
|
||||
示例:
|
||||
|
||||
- 只允许修改当前 skill 仓库,不要改动其他兄弟项目
|
||||
- 如使用浏览器自动化,优先复用已验证过的定位器和流程
|
||||
- `cli` 只做参数解析,核心逻辑统一放到 `service`
|
||||
- 发布前必须完成本地验证、工作流验证和正式环境安装验证
|
||||
|
||||
## 11. 变更记录
|
||||
|
||||
这一节用于记录需求文档本身的变化,不是 git 提交记录的替代,而是给技术人员看“需求范围怎么变了”。
|
||||
|
||||
模板写法:
|
||||
|
||||
| 日期 | 版本 | 变更人 | 变更内容 |
|
||||
|------|------|--------|----------|
|
||||
| 2026-04-13 | v1.0 | 张三 | 初版需求文档 |
|
||||
| 2026-04-14 | v1.1 | 李四 | 增加正式环境验收要求 |
|
||||
|
||||
---
|
||||
|
||||
## 建议使用方式
|
||||
|
||||
建议每个新 skill 开发时,按下面顺序使用文档:
|
||||
|
||||
1. 先写 `references/REQUIREMENTS.md`
|
||||
2. 再按 `references/DEVELOPMENT.md` 进入开发
|
||||
3. 开发过程中补充 `CLI.md`、`SCHEMA.md`、`RUNTIME.md`
|
||||
4. 发布前回到 `REQUIREMENTS.md` 对照验收标准逐项检查
|
||||
|
||||
## 最小模板示例
|
||||
|
||||
下面给出一个可直接参考的简化版示例:
|
||||
|
||||
```md
|
||||
# REQUIREMENTS
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档用于明确 `weibo-publisher` 的研发需求、范围和验收标准,作为开发与测试的统一依据。
|
||||
|
||||
## 2. 业务背景
|
||||
|
||||
运营团队需要将内容库中的文章发布到微博平台,现有人工操作效率低,且缺少统一的发布记录。
|
||||
|
||||
## 3. 开发目标
|
||||
|
||||
- 完成 `weibo-publisher` 的标准 skill 结构
|
||||
- 支持文章发布主流程
|
||||
- 支持发布日志记录
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
- 支持 `health`
|
||||
- 支持 `version`
|
||||
- 支持 `publish`
|
||||
- 支持查询发布日志
|
||||
|
||||
## 5. 非功能要求
|
||||
|
||||
- 入口统一为 `scripts/main.py`
|
||||
- 保持 UTF-8 输出
|
||||
- 结构符合 skill 模板规范
|
||||
|
||||
## 6. 输入输出要求
|
||||
|
||||
### 输入
|
||||
|
||||
- `account_id`
|
||||
- `article_id`
|
||||
|
||||
### 输出
|
||||
|
||||
- 成功结果
|
||||
- 错误信息
|
||||
- 发布日志记录
|
||||
|
||||
## 7. 依赖的兄弟技能或外部系统
|
||||
|
||||
- `account-manager`
|
||||
- `content-manager`
|
||||
- 微博后台
|
||||
|
||||
## 8. 不在本次范围内的内容
|
||||
|
||||
- 不实现定时发布
|
||||
- 不实现自动重试
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
- 命令可运行
|
||||
- 能完成正式环境安装验证
|
||||
- 能在新建任务中使用
|
||||
|
||||
## 10. 开发注意事项
|
||||
|
||||
- 不修改无关项目
|
||||
- 不引入旧模板结构
|
||||
|
||||
## 11. 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更人 | 变更内容 |
|
||||
|------|------|--------|----------|
|
||||
| 2026-04-13 | v1.0 | 张三 | 初版 |
|
||||
```
|
||||
40
references/RUNTIME.md
Normal file
40
references/RUNTIME.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 运行时约定
|
||||
|
||||
## 目录结构
|
||||
|
||||
新技能建议采用以下根目录结构:
|
||||
|
||||
- `assets/`
|
||||
- `references/`
|
||||
- `scripts/`
|
||||
- `tests/`
|
||||
- `evals/`
|
||||
|
||||
## `scripts/` 分层
|
||||
|
||||
- `scripts/main.py`:唯一 CLI 入口
|
||||
- `scripts/cli/`:参数解析与命令分发
|
||||
- `scripts/db/`:SQLite 或本地持久化层
|
||||
- `scripts/service/`:业务用例与外部交互
|
||||
- `scripts/util/`:通用工具、常量、日志、路径
|
||||
- `scripts/jiangchang_skill_core/`:运行时与统一日志副本
|
||||
|
||||
## 数据路径
|
||||
|
||||
推荐:
|
||||
|
||||
```text
|
||||
{CLAW_DATA_ROOT}/{CLAW_USER_ID}/{skill_slug}/
|
||||
```
|
||||
|
||||
数据库文件建议:
|
||||
|
||||
```text
|
||||
{...}/{skill_slug}.db
|
||||
```
|
||||
|
||||
## 编码与输出
|
||||
|
||||
- Windows 终端建议在 `scripts/main.py` 里做 UTF-8 stdout/stderr 包装
|
||||
- 机读输出优先单行 JSON
|
||||
- 错误前缀建议统一 `ERROR:`
|
||||
23
references/SCHEMA.md
Normal file
23
references/SCHEMA.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 数据存储模板
|
||||
|
||||
## 数据库路径
|
||||
|
||||
`{DATA_ROOT}/{USER_ID}/your-skill-slug/your-skill-slug.db`
|
||||
|
||||
## 发布型技能推荐日志表
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `id` | 自增主键 |
|
||||
| `account_id` | 账号 id |
|
||||
| `article_id` | 文章 id |
|
||||
| `article_title` | 标题快照 |
|
||||
| `status` | `published` / `failed` / `require_login` |
|
||||
| `error_msg` | 错误说明 |
|
||||
| `created_at` | Unix 时间戳 |
|
||||
| `updated_at` | Unix 时间戳 |
|
||||
|
||||
## 模板原则
|
||||
|
||||
- 模板不做历史迁移兼容设计
|
||||
- 新 skill 直接从当前 schema 起步
|
||||
104
scripts/cli/app.py
Normal file
104
scripts/cli/app.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""CLI:argparse 与分发模板。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from service.publish_service import (
|
||||
cmd_health,
|
||||
cmd_log_get,
|
||||
cmd_logs,
|
||||
cmd_publish,
|
||||
cmd_version,
|
||||
)
|
||||
from util.argparse_zh import ZhArgumentParser
|
||||
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
|
||||
from util.logging_config import get_skill_logger, setup_skill_logging
|
||||
|
||||
|
||||
def _cli_str_or_none(raw: Optional[str]) -> Optional[str]:
|
||||
if raw is None:
|
||||
return None
|
||||
v = str(raw).strip()
|
||||
return v or None
|
||||
|
||||
|
||||
def _handle_publish(args: argparse.Namespace) -> int:
|
||||
tail = [str(x).strip() for x in (args.publish_tail or []) if str(x).strip()]
|
||||
if len(tail) > 2:
|
||||
print("❌ 参数过多。")
|
||||
print("用法:python main.py publish [账号id [文章id]] | publish [-a 账号id] [-i 文章id]")
|
||||
return 1
|
||||
|
||||
t_acc: Optional[str] = None
|
||||
t_art: Optional[str] = None
|
||||
if len(tail) == 2:
|
||||
t_acc, t_art = tail[0], tail[1]
|
||||
elif len(tail) == 1:
|
||||
if tail[0].isdigit():
|
||||
t_art = tail[0]
|
||||
else:
|
||||
t_acc = tail[0]
|
||||
|
||||
pick_a = _cli_str_or_none(getattr(args, "account_id", None))
|
||||
pick_i = _cli_str_or_none(getattr(args, "article_id", None))
|
||||
acc = pick_a or t_acc
|
||||
art = pick_i or t_art
|
||||
return cmd_publish(account_id=acc, article_id=art)
|
||||
|
||||
|
||||
def _print_full_usage() -> None:
|
||||
print("模板技能(main.py)可用命令:")
|
||||
print(" python main.py publish [账号id [文章id]] [-a 账号] [-i 文章id]")
|
||||
print(" python main.py logs [--limit N] [--status s] [--account-id a]")
|
||||
print(" python main.py log-get <log_id>")
|
||||
print(" python main.py health")
|
||||
print(" python main.py version")
|
||||
|
||||
|
||||
def build_parser() -> ZhArgumentParser:
|
||||
p = ZhArgumentParser(
|
||||
prog="main.py",
|
||||
description="模板技能:发布命令骨架、日志查询、健康检查、版本输出。",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True, parser_class=ZhArgumentParser)
|
||||
|
||||
sp = sub.add_parser("publish", help="发布型技能命令骨架")
|
||||
sp.add_argument("--account-id", "-a", default=None, metavar="账号id")
|
||||
sp.add_argument("--article-id", "-i", default=None, metavar="文章id")
|
||||
sp.add_argument("publish_tail", nargs="*", metavar="位置参数")
|
||||
sp.set_defaults(handler=_handle_publish)
|
||||
|
||||
sp = sub.add_parser("logs", help="查看发布记录")
|
||||
sp.add_argument("--limit", type=int, default=10)
|
||||
sp.add_argument("--status", default=None)
|
||||
sp.add_argument("--account-id", default=None)
|
||||
sp.set_defaults(handler=lambda a: cmd_logs(limit=a.limit, status=a.status, account_id=a.account_id))
|
||||
|
||||
sp = sub.add_parser("log-get", help="按 log_id 查看单条发布记录(JSON)")
|
||||
sp.add_argument("log_id")
|
||||
sp.set_defaults(handler=lambda a: cmd_log_get(a.log_id))
|
||||
|
||||
sp = sub.add_parser("health", help="健康检查")
|
||||
sp.set_defaults(handler=lambda _a: cmd_health())
|
||||
|
||||
sp = sub.add_parser("version", help="版本信息(JSON)")
|
||||
sp.set_defaults(handler=lambda _a: cmd_version())
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
argv = argv if argv is not None else sys.argv[1:]
|
||||
setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)
|
||||
get_skill_logger().info("cli_start argv=%s", sys.argv)
|
||||
if not argv:
|
||||
_print_full_usage()
|
||||
return 1
|
||||
if len(argv) == 2 and argv[0] not in {"publish", "logs", "log-get", "health", "version", "-h", "--help"}:
|
||||
return cmd_publish(account_id=argv[0], article_id=argv[1])
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return int(args.handler(args))
|
||||
34
scripts/db/connection.py
Normal file
34
scripts/db/connection.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""SQLite 连接与日志表迁移模板。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from util.runtime_paths import get_db_path
|
||||
|
||||
|
||||
def get_conn() -> sqlite3.Connection:
|
||||
return sqlite3.connect(get_db_path())
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS publish_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_id TEXT NOT NULL,
|
||||
article_id INTEGER NOT NULL,
|
||||
article_title TEXT,
|
||||
status TEXT NOT NULL,
|
||||
error_msg TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
76
scripts/db/publish_logs_repository.py
Normal file
76
scripts/db/publish_logs_repository.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""publish_logs 表读写模板。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from db.connection import get_conn, init_db
|
||||
from util.timeutil import now_unix
|
||||
|
||||
|
||||
def save_publish_log(
|
||||
account_id: str,
|
||||
article_id: int,
|
||||
article_title: str,
|
||||
status: str,
|
||||
error_msg: Optional[str] = None,
|
||||
) -> int:
|
||||
init_db()
|
||||
now = now_unix()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO publish_logs (account_id, article_id, article_title, status, error_msg, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(account_id, int(article_id), article_title or "", status, error_msg, now, now),
|
||||
)
|
||||
new_id = int(cur.lastrowid)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return new_id
|
||||
|
||||
|
||||
def list_publish_logs(
|
||||
limit: int,
|
||||
status: Optional[str] = None,
|
||||
account_id: Optional[str] = None,
|
||||
) -> List[Tuple[Any, ...]]:
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"SELECT id, account_id, article_id, article_title, status, error_msg, created_at, updated_at "
|
||||
"FROM publish_logs WHERE 1=1 "
|
||||
)
|
||||
params: List[Any] = []
|
||||
if status:
|
||||
sql += "AND status = ? "
|
||||
params.append(status)
|
||||
if account_id:
|
||||
sql += "AND account_id = ? "
|
||||
params.append(account_id)
|
||||
sql += "ORDER BY created_at DESC, id DESC LIMIT ?"
|
||||
params.append(int(limit))
|
||||
cur.execute(sql, tuple(params))
|
||||
return list(cur.fetchall())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_publish_log_by_id(log_id: int) -> Optional[Tuple[Any, ...]]:
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, account_id, article_id, article_title, status, error_msg, created_at, updated_at FROM publish_logs WHERE id = ?",
|
||||
(int(log_id),),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
1
scripts/jiangchang_skill_core/__init__.py
Normal file
1
scripts/jiangchang_skill_core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Vendored from jiangchang-platform-kit/sdk/jiangchang_skill_core/ — keep runtime_env + unified_logging in sync.
|
||||
113
scripts/jiangchang_skill_core/runtime_env.py
Normal file
113
scripts/jiangchang_skill_core/runtime_env.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
JIANGCHANG_* 数据根与用户目录:解析规则 + 可选本地 CLI 默认值。
|
||||
|
||||
模板复制后通常无需大改;如组织环境不同,再按项目实际调整。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
CLI_LOCAL_DEV_ENABLED = True
|
||||
DEFAULT_LOCAL_USER_ID = "10032"
|
||||
WIN_DEFAULT_DATA_ROOT = r"D:\jiangchang-data"
|
||||
WIN_DEFAULT_JIANGCHANG_APP_ROOT = r"D:\AI\jiangchang"
|
||||
|
||||
|
||||
def platform_default_data_root() -> str:
|
||||
if sys.platform == "win32":
|
||||
return WIN_DEFAULT_DATA_ROOT
|
||||
return os.path.join(os.path.expanduser("~"), ".jiangchang-data")
|
||||
|
||||
|
||||
def get_data_root() -> str:
|
||||
env = (
|
||||
os.getenv("CLAW_DATA_ROOT")
|
||||
or os.getenv("JIANGCHANG_DATA_ROOT")
|
||||
or ""
|
||||
).strip()
|
||||
if env:
|
||||
return env
|
||||
return platform_default_data_root()
|
||||
|
||||
|
||||
def get_user_id() -> str:
|
||||
return (
|
||||
os.getenv("CLAW_USER_ID")
|
||||
or os.getenv("JIANGCHANG_USER_ID")
|
||||
or ""
|
||||
).strip() or "_anon"
|
||||
|
||||
|
||||
def _looks_like_skills_root(path: str) -> bool:
|
||||
if not path or not os.path.isdir(path):
|
||||
return False
|
||||
for marker in (
|
||||
"llm-manager",
|
||||
"content-manager",
|
||||
"account-manager",
|
||||
"sohu-publisher",
|
||||
"toutiao-publisher",
|
||||
"gongzhonghao-publisher",
|
||||
"weibo-publisher",
|
||||
"skill-template",
|
||||
):
|
||||
if os.path.isdir(os.path.join(path, marker)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_skills_root() -> str:
|
||||
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
|
||||
v = (os.getenv(key) or "").strip()
|
||||
if v:
|
||||
return os.path.normpath(v)
|
||||
|
||||
app = (os.getenv("JIANGCHANG_APP_ROOT") or "").strip()
|
||||
if sys.platform == "win32" and not app:
|
||||
app = WIN_DEFAULT_JIANGCHANG_APP_ROOT
|
||||
if app:
|
||||
nested = os.path.join(app, "skills")
|
||||
if _looks_like_skills_root(nested):
|
||||
return os.path.normpath(nested)
|
||||
if _looks_like_skills_root(app):
|
||||
return os.path.normpath(app)
|
||||
|
||||
if sys.platform == "win32":
|
||||
nested = os.path.join(WIN_DEFAULT_JIANGCHANG_APP_ROOT, "skills")
|
||||
if _looks_like_skills_root(nested):
|
||||
return os.path.normpath(nested)
|
||||
if _looks_like_skills_root(WIN_DEFAULT_JIANGCHANG_APP_ROOT):
|
||||
return os.path.normpath(WIN_DEFAULT_JIANGCHANG_APP_ROOT)
|
||||
|
||||
return os.path.normpath(os.path.join(os.path.expanduser("~"), ".openclaw", "skills"))
|
||||
|
||||
|
||||
def get_sibling_skills_root(skill_scripts_dir: str | None = None) -> str:
|
||||
if skill_scripts_dir:
|
||||
scripts = os.path.abspath(skill_scripts_dir)
|
||||
skill_root = os.path.dirname(scripts)
|
||||
inferred = os.path.dirname(skill_root)
|
||||
if _looks_like_skills_root(inferred):
|
||||
return os.path.normpath(inferred)
|
||||
|
||||
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
|
||||
v = (os.getenv(key) or "").strip()
|
||||
if v:
|
||||
return os.path.normpath(v)
|
||||
|
||||
return get_skills_root()
|
||||
|
||||
|
||||
def apply_cli_local_defaults() -> None:
|
||||
enabled = CLI_LOCAL_DEV_ENABLED
|
||||
if not enabled:
|
||||
v = (os.getenv("JIANGCHANG_CLI_LOCAL_DEV") or "").strip().lower()
|
||||
enabled = v in ("1", "true", "yes", "on")
|
||||
if not enabled:
|
||||
return
|
||||
if not (os.getenv("CLAW_DATA_ROOT") or os.getenv("JIANGCHANG_DATA_ROOT") or "").strip():
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = platform_default_data_root()
|
||||
if not (os.getenv("CLAW_USER_ID") or os.getenv("JIANGCHANG_USER_ID") or "").strip():
|
||||
os.environ["JIANGCHANG_USER_ID"] = DEFAULT_LOCAL_USER_ID.strip()
|
||||
137
scripts/jiangchang_skill_core/unified_logging.py
Normal file
137
scripts/jiangchang_skill_core/unified_logging.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
统一文件日志:{DATA_ROOT}/{USER_ID}/logs/jiangchang.log
|
||||
按日轮转;行内带 trace_id 与 skill_slug,便于跨技能排查。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from typing import Optional
|
||||
|
||||
from .runtime_env import get_data_root, get_user_id
|
||||
|
||||
_skill_slug: str = ""
|
||||
_logger_name: str = ""
|
||||
|
||||
|
||||
def get_unified_logs_dir() -> str:
|
||||
path = os.path.join(get_data_root(), get_user_id(), "logs")
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_skill_log_file_path() -> str:
|
||||
override = (os.getenv("JIANGCHANG_LOG_FILE") or "").strip()
|
||||
if override:
|
||||
parent = os.path.dirname(os.path.abspath(override))
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
return os.path.abspath(override)
|
||||
return os.path.join(get_unified_logs_dir(), "jiangchang.log")
|
||||
|
||||
|
||||
def ensure_trace_for_process() -> str:
|
||||
existing = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip()
|
||||
if existing:
|
||||
return existing
|
||||
tid = uuid.uuid4().hex[:12]
|
||||
os.environ["JIANGCHANG_TRACE_ID"] = tid
|
||||
return tid
|
||||
|
||||
|
||||
def subprocess_env_with_trace(environ: Optional[dict] = None) -> dict:
|
||||
ensure_trace_for_process()
|
||||
tid = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip() or ensure_trace_for_process()
|
||||
base = os.environ if environ is None else environ
|
||||
return {**base, "JIANGCHANG_TRACE_ID": tid}
|
||||
|
||||
|
||||
class _SkillContextFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
record.trace_id = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip() or "-"
|
||||
record.skill_slug = _skill_slug or "-"
|
||||
return True
|
||||
|
||||
|
||||
_FORMAT = "%(asctime)s | %(levelname)-8s | %(trace_id)s | %(skill_slug)s | %(name)s | %(message)s"
|
||||
_DATEFMT = "%Y-%m-%dT%H:%M:%S"
|
||||
|
||||
|
||||
def _log_level_from_env() -> int:
|
||||
v = (os.getenv("JIANGCHANG_LOG_LEVEL") or "INFO").strip().upper()
|
||||
return getattr(logging, v, None) or logging.INFO
|
||||
|
||||
|
||||
def _backup_count() -> int:
|
||||
try:
|
||||
n = int((os.getenv("JIANGCHANG_LOG_BACKUP_COUNT") or "30").strip())
|
||||
return max(1, min(n, 365))
|
||||
except ValueError:
|
||||
return 30
|
||||
|
||||
|
||||
def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
|
||||
global _skill_slug, _logger_name
|
||||
ensure_trace_for_process()
|
||||
_skill_slug = skill_slug
|
||||
_logger_name = logger_name
|
||||
|
||||
log = logging.getLogger(logger_name)
|
||||
if log.handlers:
|
||||
return
|
||||
log.setLevel(_log_level_from_env())
|
||||
path = get_skill_log_file_path()
|
||||
fh = TimedRotatingFileHandler(
|
||||
path,
|
||||
when="midnight",
|
||||
interval=1,
|
||||
backupCount=_backup_count(),
|
||||
encoding="utf-8",
|
||||
delay=True,
|
||||
)
|
||||
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||
fh.setFormatter(fmt)
|
||||
fh.addFilter(_SkillContextFilter())
|
||||
log.addHandler(fh)
|
||||
if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in ("1", "true", "yes", "on"):
|
||||
sh = logging.StreamHandler(sys.stderr)
|
||||
sh.setLevel(logging.WARNING)
|
||||
sh.setFormatter(fmt)
|
||||
sh.addFilter(_SkillContextFilter())
|
||||
log.addHandler(sh)
|
||||
log.propagate = False
|
||||
|
||||
|
||||
def get_skill_logger() -> logging.Logger:
|
||||
if not _logger_name:
|
||||
raise RuntimeError("get_skill_logger: call setup_skill_logging first")
|
||||
return logging.getLogger(_logger_name)
|
||||
|
||||
|
||||
def attach_unified_file_handler(
|
||||
log_path: str,
|
||||
*,
|
||||
skill_slug: str,
|
||||
logger_name: str,
|
||||
level: int = logging.DEBUG,
|
||||
) -> logging.Logger:
|
||||
global _skill_slug
|
||||
ensure_trace_for_process()
|
||||
_skill_slug = skill_slug
|
||||
lg = logging.getLogger(logger_name)
|
||||
lg.handlers.clear()
|
||||
lg.setLevel(level)
|
||||
parent = os.path.dirname(os.path.abspath(log_path))
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
fh = logging.FileHandler(log_path, encoding="utf-8")
|
||||
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||
fh.setFormatter(fmt)
|
||||
fh.addFilter(_SkillContextFilter())
|
||||
lg.addHandler(fh)
|
||||
lg.propagate = False
|
||||
return lg
|
||||
31
scripts/main.py
Normal file
31
scripts/main.py
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
skill-template CLI 入口。
|
||||
|
||||
复制为新技能后,请修改注释与常量,但保留当前分层结构:
|
||||
cli(argv)→ service(业务编排)→ db(持久化)→ util(通用工具)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
import io
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
_scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if _scripts_dir not in sys.path:
|
||||
sys.path.insert(0, _scripts_dir)
|
||||
|
||||
from jiangchang_skill_core.runtime_env import apply_cli_local_defaults
|
||||
|
||||
apply_cli_local_defaults()
|
||||
|
||||
from cli.app import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
46
scripts/service/entitlement_service.py
Normal file
46
scripts/service/entitlement_service.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""在线鉴权(可选模板)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def check_entitlement(skill_slug: str) -> Tuple[bool, str]:
|
||||
auth_base = (os.getenv("JIANGCHANG_AUTH_BASE_URL") or "").strip().rstrip("/")
|
||||
if not auth_base:
|
||||
return True, ""
|
||||
user_id = (os.getenv("CLAW_USER_ID") or os.getenv("JIANGCHANG_USER_ID") or "").strip()
|
||||
if not user_id:
|
||||
return False, "鉴权失败:缺少用户身份(CLAW_USER_ID / JIANGCHANG_USER_ID)"
|
||||
|
||||
auth_api_key = (os.getenv("JIANGCHANG_AUTH_API_KEY") or "").strip()
|
||||
timeout = int((os.getenv("JIANGCHANG_AUTH_TIMEOUT_SECONDS") or "5").strip())
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if auth_api_key:
|
||||
headers["Authorization"] = f"Bearer {auth_api_key}"
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"skill_slug": skill_slug,
|
||||
"trace_id": (os.getenv("JIANGCHANG_TRACE_ID") or "").strip(),
|
||||
"context": {"entry": "main.py"},
|
||||
}
|
||||
try:
|
||||
res = requests.post(f"{auth_base}/api/entitlements/check", json=payload, headers=headers, timeout=timeout)
|
||||
except requests.RequestException as exc:
|
||||
return False, f"鉴权请求失败:{exc}"
|
||||
if res.status_code != 200:
|
||||
return False, f"鉴权服务异常:HTTP {res.status_code}"
|
||||
try:
|
||||
body = res.json()
|
||||
except ValueError:
|
||||
return False, "鉴权服务异常:返回非 JSON"
|
||||
code = body.get("code")
|
||||
data = body.get("data") or {}
|
||||
if code != 200:
|
||||
return False, str(body.get("msg") or "鉴权失败")
|
||||
if not data.get("allow", False):
|
||||
return False, str(data.get("reason") or "未购买或已过期")
|
||||
return True, ""
|
||||
10
scripts/service/platform_playwright.py
Normal file
10
scripts/service/platform_playwright.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""后台自动化占位模块模板。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
async def publish(account: Dict[str, Any], article: Dict[str, Any], account_id: str) -> str:
|
||||
_ = (account, article, account_id)
|
||||
return "ERROR:NOT_IMPLEMENTED 请复制模板后将本文件改名为具体平台模块并实现后台自动化逻辑"
|
||||
83
scripts/service/publish_service.py
Normal file
83
scripts/service/publish_service.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""发布编排、日志查询模板。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from db import publish_logs_repository as plr
|
||||
from service.entitlement_service import check_entitlement
|
||||
from util.constants import SKILL_SLUG, SKILL_VERSION
|
||||
from util.timeutil import unix_to_iso
|
||||
|
||||
|
||||
def cmd_publish(account_id: Optional[str] = None, article_id: Optional[str] = None) -> int:
|
||||
_ = (account_id, article_id)
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
print(f"❌ {reason}")
|
||||
return 1
|
||||
print("❌ 这是模板仓库,请复制后在 scripts/service/ 中实现真正的发布逻辑。")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_logs(limit: int = 10, status: Optional[str] = None, account_id: Optional[str] = None) -> int:
|
||||
if limit <= 0:
|
||||
limit = 10
|
||||
rows = plr.list_publish_logs(limit, status, account_id)
|
||||
if not rows:
|
||||
print("暂无发布记录")
|
||||
return 0
|
||||
|
||||
sep_line = "_" * 39
|
||||
for idx, r in enumerate(rows):
|
||||
rid, aid, arid, title, st, err, cat, uat = r
|
||||
print(f"id:{rid}")
|
||||
print(f"account_id:{aid or ''}")
|
||||
print(f"article_id:{arid}")
|
||||
print(f"article_title:{title or ''}")
|
||||
print(f"status:{st or ''}")
|
||||
print(f"error_msg:{err or ''}")
|
||||
print(f"created_at:{unix_to_iso(cat) or str(cat or '')}")
|
||||
print(f"updated_at:{unix_to_iso(uat) or str(uat or '')}")
|
||||
if idx != len(rows) - 1:
|
||||
print(sep_line)
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_log_get(log_id: str) -> int:
|
||||
if not str(log_id).isdigit():
|
||||
print("❌ log_id 必须是数字")
|
||||
return 1
|
||||
row = plr.get_publish_log_by_id(int(log_id))
|
||||
if not row:
|
||||
print("❌ 没有这条发布记录")
|
||||
return 1
|
||||
rid, aid, arid, title, st, err, cat, uat = row
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"id": int(rid),
|
||||
"account_id": aid,
|
||||
"article_id": int(arid),
|
||||
"article_title": title,
|
||||
"status": st,
|
||||
"error_msg": err,
|
||||
"created_at": unix_to_iso(cat),
|
||||
"updated_at": unix_to_iso(uat),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_health() -> int:
|
||||
return 0 if sys.version_info >= (3, 10) else 1
|
||||
|
||||
|
||||
def cmd_version() -> int:
|
||||
print(json.dumps({"version": SKILL_VERSION, "skill": SKILL_SLUG}, ensure_ascii=False))
|
||||
return 0
|
||||
39
scripts/service/sibling_bridge.py
Normal file
39
scripts/service/sibling_bridge.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""兄弟技能 CLI 调用模板。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from util.logging_config import subprocess_env_with_trace
|
||||
from util.runtime_paths import get_skills_root
|
||||
|
||||
|
||||
def _call_json_script(script_path: str, args: List[str]) -> Optional[Dict[str, Any]]:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, script_path, *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=subprocess_env_with_trace(),
|
||||
)
|
||||
raw = (proc.stdout or "").strip()
|
||||
if not raw or raw.startswith("ERROR"):
|
||||
return None
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
|
||||
|
||||
def get_account_manager_main_path() -> str:
|
||||
return os.path.join(get_skills_root(), "account-manager", "scripts", "main.py")
|
||||
|
||||
|
||||
def get_content_manager_main_path() -> str:
|
||||
return os.path.join(get_skills_root(), "content-manager", "scripts", "main.py")
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技能入口脚本(模板)
|
||||
====================
|
||||
|
||||
【职责】
|
||||
- 作为宿主调用时的统一 CLI 入口(子进程、终端、CI 均可)。
|
||||
- 只做:参数解析、环境检查、分发到具体子命令;复杂逻辑放到同目录其它模块。
|
||||
|
||||
【如何扩展】
|
||||
1. 在 main() 的 dispatch 字典中增加 "your_cmd": handler 项。
|
||||
2. 实现 handler(argv) 或 handler();出错时打印 ERROR: 前缀信息并 sys.exit(非0)。
|
||||
3. 在仓库根目录 SKILL.md「执行步骤」中补充示例命令。
|
||||
|
||||
【多宿主注意】
|
||||
- 不要在本文件写死某一品牌宿主名。
|
||||
- 路径与环境变量约定见 ../docs/RUNTIME.md;可选辅助代码见 ../optional/paths_snippet.py(需自行复制或 import 路径按项目调整)。
|
||||
|
||||
【编码】
|
||||
Windows 下若宿主仍使用系统默认编码,可在宿主侧设置 UTF-8;本模板不强制改 sys.stdout(避免与宿主捕获冲突)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
# 与 SKILL.md 中 metadata.openclaw.slug / metadata.skill.slug 保持一致
|
||||
SKILL_SLUG = "toutiao-publisher"
|
||||
|
||||
|
||||
def cmd_version(_args: argparse.Namespace) -> int:
|
||||
"""打印版本信息(与 SKILL.md frontmatter 中 version 应对齐,此处为占位)。"""
|
||||
payload = {
|
||||
"skill_slug": SKILL_SLUG,
|
||||
"version": "0.1.0",
|
||||
"entry": "skill_main.py",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_health(_args: argparse.Namespace) -> int:
|
||||
"""
|
||||
健康检查:应快速、可离线(除非技能本身强依赖网络)。
|
||||
失败时打印 ERROR: 前缀,便于宿主与自动化解析。
|
||||
"""
|
||||
# 示例:检查 Python 版本(可按需改为检查关键依赖 import)
|
||||
if sys.version_info < (3, 9):
|
||||
print("ERROR:PYTHON_VERSION need >= 3.9", file=sys.stderr)
|
||||
return 1
|
||||
print(f"OK skill={SKILL_SLUG} python={sys.version.split()[0]}")
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser(
|
||||
description="toutiao-publisher — Toutiao batch publish skill CLI (skeleton).",
|
||||
)
|
||||
sub = p.add_subparsers(dest="command", required=True)
|
||||
|
||||
sp = sub.add_parser("version", help="Print version JSON.")
|
||||
sp.set_defaults(handler=cmd_version)
|
||||
|
||||
sp = sub.add_parser("health", help="Quick health check.")
|
||||
sp.set_defaults(handler=cmd_health)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
argv = argv if argv is not None else sys.argv[1:]
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
handler: Callable[[argparse.Namespace], int] = args.handler
|
||||
return handler(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
12
scripts/util/argparse_zh.py
Normal file
12
scripts/util/argparse_zh.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""argparse 中文错误说明。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
class ZhArgumentParser(argparse.ArgumentParser):
|
||||
def error(self, message: str) -> None:
|
||||
print(f"参数错误:{message}\n请执行:python main.py -h 查看帮助", file=sys.stderr)
|
||||
self.exit(2)
|
||||
5
scripts/util/constants.py
Normal file
5
scripts/util/constants.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""技能标识与版本(复制后请修改)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.13"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
21
scripts/util/logging_config.py
Normal file
21
scripts/util/logging_config.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Re-export unified logging (implementation: jiangchang_skill_core.unified_logging)."""
|
||||
|
||||
from jiangchang_skill_core.unified_logging import (
|
||||
attach_unified_file_handler,
|
||||
ensure_trace_for_process,
|
||||
get_skill_log_file_path,
|
||||
get_skill_logger,
|
||||
get_unified_logs_dir,
|
||||
setup_skill_logging,
|
||||
subprocess_env_with_trace,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"attach_unified_file_handler",
|
||||
"ensure_trace_for_process",
|
||||
"get_skill_log_file_path",
|
||||
"get_skill_logger",
|
||||
"get_unified_logs_dir",
|
||||
"setup_skill_logging",
|
||||
"subprocess_env_with_trace",
|
||||
]
|
||||
34
scripts/util/runtime_paths.py
Normal file
34
scripts/util/runtime_paths.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""数据根、技能目录、兄弟技能根路径。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from jiangchang_skill_core.runtime_env import get_data_root, get_sibling_skills_root, get_user_id
|
||||
|
||||
from util.constants import SKILL_SLUG
|
||||
|
||||
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def get_skill_root() -> str:
|
||||
return os.path.dirname(_SCRIPTS_DIR)
|
||||
|
||||
|
||||
def get_openclaw_root() -> str:
|
||||
return get_sibling_skills_root(_SCRIPTS_DIR)
|
||||
|
||||
|
||||
def get_skills_root() -> str:
|
||||
return get_sibling_skills_root(_SCRIPTS_DIR)
|
||||
|
||||
|
||||
def get_skill_data_dir() -> str:
|
||||
path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_db_path(filename: str | None = None) -> str:
|
||||
name = filename or f"{SKILL_SLUG}.db"
|
||||
return os.path.join(get_skill_data_dir(), name)
|
||||
20
scripts/util/timeutil.py
Normal file
20
scripts/util/timeutil.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""时间戳与 ISO 展示。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def now_unix() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def unix_to_iso(ts: Optional[int]) -> Optional[str]:
|
||||
if ts is None:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds")
|
||||
except (ValueError, OSError, OverflowError):
|
||||
return None
|
||||
8
tests/README.md
Normal file
8
tests/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# tests
|
||||
|
||||
这里放模板复制后的单元测试或最小回归测试。
|
||||
|
||||
模板仓库本身不强制附带业务测试,但建议新 skill 至少补:
|
||||
|
||||
- `health` / `version` 最小冒烟测试
|
||||
- 关键 `service` 层函数的单元测试
|
||||
Reference in New Issue
Block a user