Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 864c1a5e2b | |||
| 3e3d8c79ad | |||
| ba3d86553f | |||
| a5b56c09f5 | |||
| daadf8e25c | |||
| 298448840d | |||
| f11c596bde |
28
.github/workflows/release_skill.yaml
vendored
28
.github/workflows/release_skill.yaml
vendored
@@ -1,27 +1,13 @@
|
|||||||
# -----------------------------------------------------------------------------
|
name: 技能自动化发布
|
||||||
# 技能发布工作流(占位)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# 不同组织使用不同的复用工作流、制品格式与加密策略。
|
|
||||||
# 使用本模板时,请将下面 jobs 整段替换为你们自己的 workflow,
|
|
||||||
# 或删除本文件若暂不需要 GitHub Actions。
|
|
||||||
#
|
|
||||||
# 设计原则(行业常见):
|
|
||||||
# - 通过 tag 触发发布(如 v*)
|
|
||||||
# - 制品与版本号可追溯,与 SKILL.md 中 version 对齐
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
name: skill-release-placeholder
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ["v*"]
|
tags: ["v*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
|
||||||
steps:
|
secrets:
|
||||||
- name: Placeholder
|
PYARMOR_REG_B64: ${{ secrets.PYARMOR_REG_B64 }}
|
||||||
run: |
|
with:
|
||||||
echo "Replace this workflow with your organization's reusable workflow."
|
artifact_platform: windows
|
||||||
echo "Example pattern: jobs.release.uses: <org>/<kit>/.github/workflows/reusable-release-skill.yaml@ref"
|
pyarmor_platform: windows.x86_64
|
||||||
exit 0
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,9 +1,3 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.pyo
|
|
||||||
.Python
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
|
||||||
.claude/
|
|
||||||
64
README.md
64
README.md
@@ -1,40 +1,48 @@
|
|||||||
# Claw 技能项目模板(通用)
|
# 匠厂 技能开发模板
|
||||||
|
|
||||||
本目录是一个**与具体业务无关**的技能(Skill)工程骨架,供团队在各类 **Claw / Agent 宿主**(桌面端、网关、IDE 插件等)上交付可安装技能包时参考。
|
这是一个**规范化的新技能模板仓库**,用于复制出新的 skill 项目;它本身**不是业务 skill**。
|
||||||
|
|
||||||
## 你从这里能得到什么
|
## 模板目标
|
||||||
|
|
||||||
- **行业标准对齐**:技能以「清单文件 + 可执行入口 + 文档」组织——与常见的 Agent Skill、CLI 工具包、内部自动化脚本仓库的惯例一致;不绑定某一厂商私有协议。
|
- 对齐当前规范 skill 的目录结构:`assets/`、`references/`、`scripts/`、`tests/`、`evals/`
|
||||||
- **可移植约定**:数据目录、用户隔离、兄弟技能路径等通过**环境变量契约**描述(见 `docs/RUNTIME.md`);不同宿主只需注入同名变量或做一层别名映射。
|
- 对齐当前规范脚手架分层:`scripts/cli`、`scripts/db`、`scripts/service`、`scripts/util`、`scripts/jiangchang_skill_core`
|
||||||
- **低学习成本**:每个文件顶部与关键步骤都有注释;按下面顺序做即可跑通第一个命令。
|
- 提供最小可运行入口:`python scripts/main.py health` / `version`
|
||||||
|
- 让新技能从一开始就按规范落地,不再沿用旧模板的 `docs/`、`optional/`、`skill_main.py` 结构
|
||||||
|
|
||||||
## 建议的上手顺序(约 15~30 分钟)
|
## 新技能使用步骤
|
||||||
|
|
||||||
1. **复制本模板**为新仓库或新目录,全局把占位符 `your-skill-slug` / `Your Skill Display Name` 换成你的技能标识(与 `SKILL.md` 里 `metadata.skill.slug` 一致)。
|
1. 复制本目录为新的 skill 仓库。
|
||||||
2. **阅读** `docs/RUNTIME.md`,确认你的宿主会注入哪些环境变量;若宿主使用另一套名字,在宿主侧做映射,或改 `optional/paths_snippet.py` 中的读取顺序(文件内有说明)。
|
2. 全局替换 `your-skill-slug`、`your-platform-key`、`您的技能显示名称`、`你的平台名` 等占位内容。
|
||||||
3. **本地试跑**:`python scripts/skill_main.py health` 应输出成功信息。
|
3. 修改 `SKILL.md`、`references/` 和 `scripts/util/constants.py`。
|
||||||
4. **扩展子命令**:在 `scripts/skill_main.py` 的 `dispatch` 中增加分支;业务逻辑放在同目录其它模块或子包中,保持入口轻薄。
|
4. 在 `scripts/service/` 中补业务 service 与真正的发布/执行逻辑。
|
||||||
5. **编写/调整 `SKILL.md`**:只改「何时触发、如何调用、参数含义」,不要写实现细节;实现细节放在 `docs/` 或代码注释里。
|
5. 用 `python scripts/main.py health` 和 `python scripts/main.py version` 做最小验证。
|
||||||
6. **发布**:若使用 GitHub Actions,编辑 `.github/workflows/release_skill.yaml`,把 `uses:` 指向**你们组织**的复用工作流;若不用 CI,可删除该目录。
|
|
||||||
7. **一键打标签推送(与匠厂 monorepo 对齐)**:在技能仓库根目录执行 `.\release.ps1`(需与 `jiangchang-platform-kit` 位于同一父目录,以便调用 `..\jiangchang-platform-kit\tools\release.ps1`)。支持 `-DryRun`、`-AutoCommit`、`-CommitMessage` 等参数,与 `account-manager` / `sohu-publisher` 一致。
|
|
||||||
|
|
||||||
## 目录一览
|
开发教程入口:
|
||||||
|
|
||||||
| 路径 | 作用 |
|
- <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 正文),供宿主与协作者阅读 |
|
| `SKILL.md` | 技能清单与触发说明模板 |
|
||||||
| `release.ps1` | 转调平台套件的发布脚本(提交/推送/语义化 tag);依赖并列的 `jiangchang-platform-kit` |
|
| `assets/` | 示例输出与轻量 schema |
|
||||||
| `scripts/skill_main.py` | 推荐唯一 CLI 入口;含 `health` / `version` 示例 |
|
| `references/` | 面向用户与编排的文档模板 |
|
||||||
| `docs/RUNTIME.md` | 环境与目录契约(多宿主通用) |
|
| `scripts/` | 规范分层后的代码骨架 |
|
||||||
| `docs/SKILL_TYPES.md` | 常见技能形态与自检清单 |
|
| `tests/` | 单元测试或最小回归测试 |
|
||||||
| `docs/PORTABILITY.md` | 多 Claw 宿主差异与兼容建议 |
|
| `evals/` | 人工/半自动评估材料 |
|
||||||
| `optional/` | 可选复制进项目的片段(路径、SQLite 示例),**不默认 import** |
|
| `.github/workflows/release_skill.yaml` | 标准发布工作流 |
|
||||||
|
| `release.ps1` | 对齐现有 skill 的发布脚本入口 |
|
||||||
|
|
||||||
## 不要做的事
|
## 最小命令
|
||||||
|
|
||||||
- 不要在模板中提交真实密钥、真实业务表结构或平台专用逻辑。
|
```bash
|
||||||
- 不要把模板改成只支持某一种宿主;特殊项写在 `docs/PORTABILITY.md` 的「宿主附录」中。
|
python scripts/main.py health
|
||||||
|
python scripts/main.py version
|
||||||
|
```
|
||||||
|
|
||||||
## 版本
|
## 注意
|
||||||
|
|
||||||
模板自身版本见 `SKILL.md` 的 `version` 字段;与你技能的业务版本一致更新即可。
|
- 不要再往模板里引入旧式 `docs/` 或 `optional/` 目录。
|
||||||
|
- 新技能若不需要某些目录,也建议先保留结构,再按实际业务填充内容。
|
||||||
|
|||||||
54
SKILL.md
54
SKILL.md
@@ -1,56 +1,42 @@
|
|||||||
---
|
---
|
||||||
# ---------------------------------------------------------------------------
|
name: 技能开发模板(复制后请修改)
|
||||||
# 技能清单(Skill Manifest)
|
description: "这是 OpenClaw 技能开发模板仓库,不直接作为业务技能发布。复制为新技能仓库后,按本模板替换 slug、名称、说明、CLI 子命令与 service 实现。"
|
||||||
# 行业常见做法:YAML 头描述元数据,正文 Markdown 描述「何时用、怎么调」。
|
|
||||||
# 不同 Claw 宿主解析字段可能略有差异;可移植标识请使用 metadata.skill.slug。
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
name: 您的技能显示名称
|
|
||||||
description: 一句话说明技能做什么(给编排层与人类阅读,不写实现细节)。
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
skill:
|
openclaw:
|
||||||
# 机器可读、稳定标识:小写字母、数字、短横线;与数据子目录名一致。
|
|
||||||
slug: your-skill-slug
|
slug: your-skill-slug
|
||||||
emoji: "📦"
|
emoji: "📦"
|
||||||
category: "通用"
|
category: "通用"
|
||||||
# 宿主若限制可调用的工具类型,在此列出(按宿主文档填写)。
|
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- bash
|
- bash
|
||||||
---
|
---
|
||||||
|
|
||||||
# Your Skill Display Name
|
# 技能开发模板(skill-template)
|
||||||
|
|
||||||
## 使用时机
|
这是一个**用于复制的新技能模板**,不是业务技能本身。新建技能时,应复制本仓库结构,再把占位内容替换成你的真实业务实现。
|
||||||
|
|
||||||
<!-- 由技能作者填写:用户在自然语言里怎样表达时应触发本技能。 -->
|
## 模板使用方式
|
||||||
|
|
||||||
- 示例:用户说「检查技能是否可用」「运行某某任务」
|
1. 复制目录为你的新 skill 仓库。
|
||||||
|
2. 全局替换 `your-skill-slug`、`技能开发模板(复制后请修改)` 等占位词。
|
||||||
|
3. 按 `references/CLI.md`、`scripts/` 分层与 `README.md` 的说明补业务逻辑。
|
||||||
|
|
||||||
## 执行步骤
|
## 目录约定
|
||||||
|
|
||||||
<!-- 将 {baseDir} 替换为宿主提供的技能根目录;若宿主使用其它占位符,以宿主文档为准。 -->
|
- 根目录结构参考现有规范技能:`assets/`、`references/`、`scripts/`、`tests/`、`evals/`。
|
||||||
|
- CLI 入口固定为 `scripts/main.py`。
|
||||||
|
- 业务逻辑按 `cli / db / service / util / jiangchang_skill_core` 分层。
|
||||||
|
|
||||||
### 健康检查(推荐自动化先跑)
|
## 最小命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 {baseDir}/scripts/skill_main.py health
|
python {baseDir}/scripts/main.py health
|
||||||
|
python {baseDir}/scripts/main.py version
|
||||||
```
|
```
|
||||||
|
|
||||||
### 查看版本
|
## 重要说明
|
||||||
|
|
||||||
```bash
|
- 复制后请同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`。
|
||||||
python3 {baseDir}/scripts/skill_main.py version
|
- 如技能无需持久化,可保留 `db/` 目录但不主动调用。
|
||||||
```
|
- 面向用户与编排的文档写在 `references/`,不要再新增旧式 `docs/` / `optional/` 结构。
|
||||||
|
|
||||||
### 扩展子命令
|
|
||||||
|
|
||||||
在 `scripts/skill_main.py` 中增加新的子命令分支,并在此处追加对应示例命令与参数说明。
|
|
||||||
|
|
||||||
## 环境依赖
|
|
||||||
|
|
||||||
详见本仓库 `docs/RUNTIME.md`(`CLAW_DATA_ROOT`、`CLAW_USER_ID` 等)。
|
|
||||||
|
|
||||||
## 数据与隐私
|
|
||||||
|
|
||||||
本技能若产生持久化数据,应仅写入 `{CLAW_DATA_ROOT}/{CLAW_USER_ID}/your-skill-slug/` 下;不得将用户数据提交到版本库。
|
|
||||||
|
|||||||
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,76 +0,0 @@
|
|||||||
# OpenClaw 技能日志约定(各技能统一)
|
|
||||||
|
|
||||||
规范目标:同一用户工作区下**一份按日轮转的主日志**,行内带 **技能标识** 与 **trace_id**,便于跨技能排查。
|
|
||||||
|
|
||||||
## 目录与文件
|
|
||||||
|
|
||||||
- 统一日志目录(与技能数据目录**无关**,不按 skill 分子目录):
|
|
||||||
- `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/`
|
|
||||||
- 主日志文件(默认):
|
|
||||||
- `{...}/logs/jiangchang.log`(午夜轮转,历史带日期后缀,由 `TimedRotatingFileHandler` 管理)
|
|
||||||
- 覆盖主日志**绝对路径**(可选):
|
|
||||||
- `JIANGCHANG_LOG_FILE`
|
|
||||||
|
|
||||||
实现源码以 **`jiangchang-platform-kit/sdk/jiangchang_skill_core/unified_logging.py`** 为准;各技能 `scripts/jiangchang_skill_core/unified_logging.py` 为发布用副本,**修改后请同步各技能**。
|
|
||||||
|
|
||||||
数据根与用户 id 的解析与本地 CLI 注入见同目录 **`runtime_env.py`**(`get_data_root` / `get_user_id` / `apply_cli_local_defaults`);新技能应在 `main.py` 最早阶段调用 `apply_cli_local_defaults()`(在 import 业务包之前)。
|
|
||||||
|
|
||||||
## 轮转与编码
|
|
||||||
|
|
||||||
- `TimedRotatingFileHandler`,`when=midnight`,保留份数默认 30,可用 `JIANGCHANG_LOG_BACKUP_COUNT` 覆盖(1~365)。
|
|
||||||
- 编码 **UTF-8**。
|
|
||||||
- 目录在首次写日志前创建(`exist_ok=True`)。
|
|
||||||
|
|
||||||
## 日志格式(行文本)
|
|
||||||
|
|
||||||
```text
|
|
||||||
%(asctime)s | %(levelname)-8s | %(trace_id)s | %(skill_slug)s | %(name)s | %(message)s
|
|
||||||
```
|
|
||||||
|
|
||||||
- `datefmt`:`%Y-%m-%dT%H:%M:%S`(本地时间)
|
|
||||||
- `skill_slug`:与 `SKILL.md` 中 `metadata.openclaw.slug` 一致(如 `account-manager`)
|
|
||||||
- `name`:分层 logger,如 `openclaw.skill.account_manager`、`openclaw.skill.account_manager.login_child`
|
|
||||||
- `trace_id`:调用链 ID,见下节
|
|
||||||
|
|
||||||
## 调用链(trace)
|
|
||||||
|
|
||||||
- 环境变量 **`JIANGCHANG_TRACE_ID`**:同一 shell/进程树内子进程默认继承;跨技能 `subprocess` 时父进程应使用 `subprocess_env_with_trace()` 构造 `env`,保证子进程沿用同一 trace。
|
|
||||||
- 若未设置,各技能在 `setup_skill_logging` 时会生成 12 位 hex 并写入环境,供后续子进程继承。
|
|
||||||
|
|
||||||
## Logger 命名
|
|
||||||
|
|
||||||
- 主 logger:`openclaw.skill.<slug_下划线>`(如 `openclaw.skill.content_manager`)
|
|
||||||
- 子进程专用 logger:如 `openclaw.skill.account_manager.login_child`,仍写入**同一**主日志文件(`FileHandler` 追加,格式与主进程一致)
|
|
||||||
|
|
||||||
## 环境变量
|
|
||||||
|
|
||||||
| 变量 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `JIANGCHANG_DATA_ROOT` | 数据根 |
|
|
||||||
| `JIANGCHANG_USER_ID` | 用户/工作区 id,默认 `_anon` |
|
|
||||||
| `JIANGCHANG_LOG_LEVEL` | 默认 `INFO` |
|
|
||||||
| `JIANGCHANG_LOG_TO_STDERR` | `1`/`true` 时 WARNING+ 同时打 stderr |
|
|
||||||
| `JIANGCHANG_LOG_FILE` | 覆盖主日志文件绝对路径 |
|
|
||||||
| `JIANGCHANG_LOG_BACKUP_COUNT` | 轮转保留份数,默认 `30` |
|
|
||||||
| `JIANGCHANG_TRACE_ID` | 可选;由运行时注入以串联调用链 |
|
|
||||||
|
|
||||||
## 技能侧接入(最少改动)
|
|
||||||
|
|
||||||
1. 保证 `scripts/jiangchang_skill_core/` 与平台 kit 中实现保持同步。
|
|
||||||
2. `main.py` 将 `scripts` 目录加入 `sys.path`(若尚未加入)。
|
|
||||||
3. CLI `main` 早期调用:`setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)`。
|
|
||||||
4. 业务代码:`get_skill_logger()` 打日志。
|
|
||||||
5. 拉起子技能 CLI 时:`subprocess.run(..., env=subprocess_env_with_trace())`。
|
|
||||||
|
|
||||||
## 级别与敏感信息
|
|
||||||
|
|
||||||
- **INFO**:子命令入口、成功结束(含关键业务 id)。
|
|
||||||
- **WARNING**:可恢复问题、校验失败。
|
|
||||||
- **ERROR**:不可继续的异常。
|
|
||||||
- **DEBUG**:高频细节;默认关闭。
|
|
||||||
- 日志可能含路径、账号信息,注意权限与留存。
|
|
||||||
|
|
||||||
## 参考实现
|
|
||||||
|
|
||||||
- `jiangchang-platform-kit/sdk/jiangchang_skill_core/unified_logging.py`
|
|
||||||
- `account-manager` / `content-manager` / `llm-manager` 的 `util/logging_config.py`(re-export)与 CLI 入口
|
|
||||||
@@ -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,67 +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/...
|
|
||||||
```
|
|
||||||
|
|
||||||
编排型技能若需要通过子进程调用兄弟技能,应基于该变量定位脚本,避免写死绝对路径。
|
|
||||||
|
|
||||||
### `JIANGCHANG_SKILLS_ROOT`(可选)
|
|
||||||
|
|
||||||
与 `CLAW_SKILLS_ROOT` 同义;匠厂桌面宿主可只注入此名。未设置时,技能可从「本技能 `scripts/` 的上一级目录的上一级」推断并列根(OpenClaw 开发仓与 `skills/<slug>/` 解压布局均适用)。
|
|
||||||
|
|
||||||
### `JIANGCHANG_APP_ROOT`(可选)
|
|
||||||
|
|
||||||
匠厂应用安装根。未设置且未设置上述技能根时,Windows 下会尝试用默认 `D:\AI\jiangchang`:若存在子目录 `skills` 且其下像并列技能安装,则技能根为 `{APP}/skills`;否则若 `{APP}` 下直接可见各技能 slug 子目录,则 `{APP}` 即技能根。其他平台默认回退为 `~/.openclaw/skills`。
|
|
||||||
|
|
||||||
### 加密发布包布局
|
|
||||||
|
|
||||||
CI 产物 ZIP 解压后与仓库技能目录结构一致:含 `SKILL.md` 与 **`scripts/`**(PyArmor 输出在 `scripts/` 内)。宿主启动入口应为 **`python scripts/main.py`**(工作目录为技能根)。
|
|
||||||
|
|
||||||
## 本技能推荐的数据目录
|
|
||||||
|
|
||||||
```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 建表示例 |
|
|
||||||
|
|
||||||
复制后务必全局替换 `your-skill-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 = "your-skill-slug"
|
|
||||||
|
|
||||||
|
|
||||||
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 时,默认使用本文件所在仓库的上一级目录的上一级
|
|
||||||
(即:.../skill-template/scripts/ → 误推断;复制后请改为 BASE_DIR 逻辑)。
|
|
||||||
"""
|
|
||||||
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`
|
||||||
547
references/DEVELOPMENT.md
Normal file
547
references/DEVELOPMENT.md
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
# 技能开发教程
|
||||||
|
|
||||||
|
这份文档是给**技术人员**看的,目标不是解释概念,而是让你拿到 `skill-template` 后,可以**一步一步开发出一个新的 skill**。
|
||||||
|
|
||||||
|
本文默认你开发的是当前最常见的一类技能:
|
||||||
|
|
||||||
|
- 有明确的 `scripts/main.py` CLI 入口
|
||||||
|
- 可能需要读写本地 SQLite
|
||||||
|
- 可能需要调用兄弟技能
|
||||||
|
- 业务逻辑主要放在 `scripts/service/`
|
||||||
|
|
||||||
|
如果你开发的是发布型 skill,这个模板就是直接可用的起点。
|
||||||
|
|
||||||
|
## 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 名
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- category
|
||||||
|
- dependencies
|
||||||
|
- 何时使用本技能
|
||||||
|
- 对用户的引导话术
|
||||||
|
- CLI 使用原则
|
||||||
|
|
||||||
|
不要在 `SKILL.md` 里写大量实现细节。
|
||||||
|
实现细节放在:
|
||||||
|
|
||||||
|
- `references/`
|
||||||
|
- 代码注释
|
||||||
|
- `service/` 实现里
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
如果你开发的是 publisher 类 skill,建议按这个顺序做:
|
||||||
|
|
||||||
|
1. 先把目录结构搭完整
|
||||||
|
2. 先让 `health` / `version` 跑通
|
||||||
|
3. 再让 `publish_service.py` 的骨架跑通
|
||||||
|
4. 再接 `sibling_bridge.py`
|
||||||
|
5. 最后再写 `*_playwright.py`
|
||||||
|
|
||||||
|
不要一开始就直接写页面选择器。
|
||||||
|
|
||||||
|
推荐先确保这些基础能力正常:
|
||||||
|
|
||||||
|
- 能取到账号
|
||||||
|
- 能取到文章
|
||||||
|
- 能写日志
|
||||||
|
- CLI 子命令通了
|
||||||
|
- 错误返回值格式定好了
|
||||||
|
|
||||||
|
然后再进浏览器自动化。
|
||||||
|
|
||||||
|
## 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 publish -h
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证本地日志与数据库
|
||||||
|
|
||||||
|
如果是发布型 skill,再继续:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/main.py logs
|
||||||
|
python scripts/main.py log-get 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 最后再验证真实业务
|
||||||
|
|
||||||
|
比如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/main.py publish
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. 发布到正式环境验证
|
||||||
|
|
||||||
|
当本地开发、自测和联调完成后,还需要把 skill 发布到正式环境做一次完整验证。建议技术人员严格按下面顺序执行,不要跳步。
|
||||||
|
|
||||||
|
### 第一步:在 skill 根目录执行 `release.ps1`
|
||||||
|
|
||||||
|
在当前 skill 仓库根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\release.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
这一步会自动完成标准发布动作,包括:
|
||||||
|
|
||||||
|
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 一多,就会越来越乱。
|
||||||
30
references/README.md
Normal file
30
references/README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
description: "这是规范化的新技能模板说明,不直接作为业务技能使用。复制后请替换技能名、平台名、CLI 示例与 service 实现。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 技能模板说明
|
||||||
|
|
||||||
|
这个仓库是**给开发者复制的新技能模板**,不是终端用户直接调用的业务 skill。
|
||||||
|
|
||||||
|
## 它提供什么
|
||||||
|
|
||||||
|
- 标准目录结构
|
||||||
|
- 最小 CLI 入口
|
||||||
|
- 发布型技能常见的日志表骨架
|
||||||
|
- `service` 层占位模块
|
||||||
|
- 与现有规范 skill 一致的发布脚本与 GitHub workflow
|
||||||
|
|
||||||
|
## 复制后你需要改什么
|
||||||
|
|
||||||
|
- `SKILL.md` 中的名称、描述、slug、触发说明
|
||||||
|
- `references/CLI.md` 里的命令示例
|
||||||
|
- `scripts/util/constants.py` 中的 slug / 版本 / logger 名
|
||||||
|
- `scripts/service/` 下的真实业务实现
|
||||||
|
|
||||||
|
## 不建议再保留的旧结构
|
||||||
|
|
||||||
|
- 旧模板里的 `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.skill.slug 保持一致(模板占位,复制后请修改)
|
|
||||||
SKILL_SLUG = "your-skill-slug"
|
|
||||||
|
|
||||||
|
|
||||||
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="Claw skill CLI template — replace SKILL_SLUG and add subcommands.",
|
|
||||||
)
|
|
||||||
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.0"
|
||||||
|
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