14 Commits

Author SHA1 Message Date
b1e93a323f docs: generalize skill template development guide
All checks were successful
技能自动化发布 / release (push) Successful in 23s
2026-04-14 10:05:16 +08:00
f214598470 docs: explain developer_ids in skill template
All checks were successful
技能自动化发布 / release (push) Successful in 44s
2026-04-14 09:19:01 +08:00
5abe67f340 docs: add developer_ids to skill template metadata
All checks were successful
技能自动化发布 / release (push) Successful in 42s
2026-04-14 09:04:31 +08:00
599d7cff48 docs: add Codex and open tool links in new tab
All checks were successful
技能自动化发布 / release (push) Successful in 41s
2026-04-13 15:20:50 +08:00
f1060f2a26 docs: add recommended AI development tools
All checks were successful
技能自动化发布 / release (push) Successful in 33s
2026-04-13 15:11:51 +08:00
864c1a5e2b docs: open documentation links in new tab
All checks were successful
技能自动化发布 / release (push) Successful in 24s
2026-04-13 15:04:26 +08:00
3e3d8c79ad docs: add requirements link to README
All checks were successful
技能自动化发布 / release (push) Successful in 23s
2026-04-13 14:59:19 +08:00
ba3d86553f docs: add requirements template
All checks were successful
技能自动化发布 / release (push) Successful in 25s
2026-04-13 14:48:28 +08:00
a5b56c09f5 docs: add production release verification guide
All checks were successful
技能自动化发布 / release (push) Successful in 25s
2026-04-13 14:28:16 +08:00
daadf8e25c docs: add direct development guide link
All checks were successful
技能自动化发布 / release (push) Successful in 31s
2026-04-13 13:52:40 +08:00
298448840d docs: standardize skill-template and add development guide
All checks were successful
技能自动化发布 / release (push) Successful in 22s
2026-04-13 13:46:23 +08:00
f11c596bde Revert "feat: release ZIP keeps scripts/ tree; JIANGCHANG_SKILLS_ROOT and sibling skills root"
This reverts commit f973208fe3.
2026-04-06 18:45:28 +08:00
f973208fe3 feat: release ZIP keeps scripts/ tree; JIANGCHANG_SKILLS_ROOT and sibling skills root
All checks were successful
skill-release-placeholder / release (push) Successful in 1s
2026-04-06 18:38:29 +08:00
5bc3ce6810 chore: sync from OpenClaw workspace (2026-04-05 09:17) 2026-04-05 09:17:42 +08:00
45 changed files with 2047 additions and 459 deletions

View File

@@ -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

5
.gitignore vendored
View File

@@ -1,8 +1,3 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.pyo
.Python
.venv/
venv/
.env .env
.env.*

View File

@@ -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 正文) | | `SKILL.md` | 技能清单与触发说明模板 |
| `scripts/skill_main.py` | CLI 入口:`health` / `version` | | `assets/` | 示例输出与轻量 schema |
| `docs/` | 运行时与可移植性说明 | | `references/` | 面向用户与编排的文档模板 |
| `optional/` | 可选片段路径、SQLite 等),默认不引用 | | `scripts/` | 规范分层后的代码骨架 |
| `tests/` | 单元测试或最小回归测试 |
| `evals/` | 人工/半自动评估材料 |
| `.github/workflows/release_skill.yaml` | 标准发布工作流 |
| `release.ps1` | 对齐现有 skill 的发布脚本入口 |
## 本地试跑 ## 最小命令
```bash ```bash
python scripts/skill_main.py health python scripts/main.py health
python scripts/skill_main.py version python scripts/main.py version
``` ```
## 版本 ## 注意
`SKILL.md` `version` 字段对齐更新。 - 不要再往模板里引入旧式 `docs/` `optional/` 目录。
- 新技能若不需要某些目录,也建议先保留结构,再按实际业务填充内容。
- `metadata.openclaw.developer_ids` 是发布元数据,不是用户展示文案。它的作用是让发布后的非公开技能自动授权给指定开发者查看。

View File

@@ -1,52 +1,48 @@
--- ---
# --------------------------------------------------------------------------- name: 技能开发模板(复制后请修改)
# 技能清单Skill Manifest description: "这是 OpenClaw 技能开发模板仓库,不直接作为业务技能发布。复制为新技能仓库后,按本模板替换 slug、名称、说明、CLI 子命令与 service 实现。"
# --------------------------------------------------------------------------- version: 1.0.13
name: 头条号批量发布
description: 头条号批量发布技能(骨架阶段:仅健康检查与版本;发布逻辑待实现)。
version: 0.1.0
author: 深圳匠厂科技有限公司 author: 深圳匠厂科技有限公司
metadata: metadata:
openclaw: openclaw:
slug: toutiao-publisher slug: your-skill-slug
emoji: "📰" emoji: "📦"
category: "内容发布" category: "通用"
skill: developer_ids:
slug: toutiao-publisher - 1032
emoji: "📰" - 12428
category: "内容发布"
allowed-tools: allowed-tools:
- bash - 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 ```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/` 目录但不主动调用。
``` - `metadata.openclaw.developer_ids` 用于声明技能发布后的默认开发者可见用户 ID 列表。
- 当技能在平台中 `access_scope = 0`(不公开)时,发布流程会把 `developer_ids` 中的用户自动补写到 `skill_user_access`,使这些开发者仍可在技能市场中查看该技能。
### 发布与其它子命令 - `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id`
- 面向用户与编排的文档写在 `references/`,不要再新增旧式 `docs/` / `optional/` 结构。
实现中:后续将在 `scripts/` 下增加发布入口,并在此文档补充命令与参数说明。
## 环境依赖
详见本仓库 `docs/RUNTIME.md``CLAW_DATA_ROOT``CLAW_USER_ID` 等)。
## 数据与隐私
本技能若产生持久化数据,应仅写入 `{CLAW_DATA_ROOT}/{CLAW_USER_ID}/toutiao-publisher/` 下;不得将用户数据提交到版本库。

7
assets/README.md Normal file
View File

@@ -0,0 +1,7 @@
# assets
- `examples/`CLI 成功输出形状示例(虚构路径与数据)。
- `schemas/`:轻量 JSON Schema 示例(`additionalProperties: true` 允许扩展字段)。
- 面向用户的介绍见 `references/README.md`
- 面向编排/CLI 的细节见 `references/CLI.md``RUNTIME.md``SCHEMA.md`

View File

@@ -0,0 +1,4 @@
# 示例 JSON
- `version-response.json``python main.py version` 成功时 stdout 单行对象的形状参考。
- `log-get-response.json`:若技能有日志表,`log-get` 成功时单条对象的形状参考。

View 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"
}

View File

@@ -0,0 +1,4 @@
{
"version": "1.0.0",
"skill": "your-skill-slug"
}

View 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -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

View File

@@ -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` 注释示例)。**不要在业务模块中散落多套变量名判断。**

View File

@@ -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
View File

@@ -0,0 +1,5 @@
# evals
这里放人工评估脚本、示例输入输出、回归验收清单等材料。
模板仓库保持最小占位,不再放旧式杂项代码片段。

View File

@@ -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 与表名。

View File

@@ -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)

View File

@@ -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
View 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
View 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`
下面这张图演示了在仓库页进入“工作流”的位置:
![Gitea 仓库工作流入口](../assets/screenshots/gitea-workflow-tab.png)
下面这张图演示了工作流成功后的状态页面:
![Gitea 工作流成功示例](../assets/screenshots/gitea-workflow-success.png)
### 第三步:确认工作流执行成功
只有当工作流状态为成功,才说明正式发布产物已经正确构建完成。
如果工作流失败,不要继续做平台验证,而应该先回到代码仓库排查,例如:
- 发布脚本是否正常推送
- `SKILL.md`、`scripts/`、`references/` 是否齐全
- 工作流文件是否存在
- 发布包结构是否符合模板规范
### 第四步:进入匠厂平台下载安装包
当工作流成功后,就可以进入匠厂平台验证最终安装效果。
匠厂产品下载地址:
- [https://jc2009.com/product.html](https://jc2009.com/product.html)
打开页面后,下载并安装匠厂客户端。下面这张图演示了下载入口位置:
![匠厂客户端下载入口](../assets/screenshots/jc2009-download-page.png)
匠厂产品页可从这里进入:[产品下载 - 匠厂](https://jc2009.com/product.html)
### 第五步:安装匠厂后,在技能市场检查最新 skill
安装并启动匠厂后,进入左侧“技能市场”,搜索或查找刚刚发布的 skill确认以下内容
- 技能可以被正常检索到
- 技能名称、说明、版本信息正确
- 最新版本已经同步出来
- 可以正常安装或更新
下面这张图演示了在技能市场中查看已发布 skill 的位置:
![技能市场中的已发布技能](../assets/screenshots/market-installed-skill.png)
### 第六步:安装并实际启用该 skill
在技能市场中找到目标 skill 后,执行安装或更新操作,确保客户端本地已经拿到最新版本。
这一步建议至少确认:
- 安装按钮可以正常执行
- 安装后状态正常
- 不会出现缺文件、缺入口或安装失败的问题
### 第七步:在“新建任务”中实际使用该 skill
安装完成后,不要只停留在“已安装”状态,还需要进入“新建任务”页面,真正调用一次该 skill完成最终验证。
建议至少验证:
- 新任务中可以正常选择或触发该 skill
- skill 能被正确唤起
- 主要命令或主流程可以运行
- 返回结果、日志和行为符合预期
下面这张图演示了安装后在任务界面中实际使用 skill 的场景:
![新建任务中使用技能](../assets/screenshots/new-task-usage.png)
## 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,104 @@
"""CLIargparse 与分发模板。"""
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
View 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()

View 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()

View File

@@ -0,0 +1 @@
# Vendored from jiangchang-platform-kit/sdk/jiangchang_skill_core/ — keep runtime_env + unified_logging in sync.

View 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()

View 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
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
skill-template CLI 入口。
复制为新技能后,请修改注释与常量,但保留当前分层结构:
cliargv→ 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())

View 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, ""

View 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 请复制模板后将本文件改名为具体平台模块并实现后台自动化逻辑"

View 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

View 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")

View File

@@ -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())

View 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)

View File

@@ -0,0 +1,5 @@
"""技能标识与版本(复制后请修改)。"""
SKILL_SLUG = "your-skill-slug"
SKILL_VERSION = "1.0.13"
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"

View 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",
]

View 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
View 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
View File

@@ -0,0 +1,8 @@
# tests
这里放模板复制后的单元测试或最小回归测试。
模板仓库本身不强制附带业务测试,但建议新 skill 至少补:
- `health` / `version` 最小冒烟测试
- 关键 `service` 层函数的单元测试