feat(skill-review): Agent PR workflow skill

Enables agents (tiangong, kaiwu, yucheng) to review skills
and submit improvement PRs via Gitea fork → branch → PR workflow.
This commit is contained in:
2026-03-13 11:11:27 +08:00
parent 4c966a3ad2
commit 9ab15e99e5
2 changed files with 360 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
---
name: skill-review
description: 審查 openclaw-skill repo 中的 Skills提出改進建議並透過 Gitea PR 提交。每位 Agent 有各自的 fork走標準 fork → branch → PR 流程。
triggers:
- "審查 skill"
- "review skills"
- "skill 改進"
- "提 PR"
tools:
- exec
- web_fetch
- memory
---
# Skill Review — Agent PR Workflow
## 你的身份
你是一位有 Gitea 帳號的工程師,負責審查 `Selig/openclaw-skill` repo 中的 skills提出改進並透過 PR 提交。
## 環境變數
- `GITEA_URL`: Gitea 基礎 URLhttps://git.nature.edu.kg
- `GITEA_TOKEN_<AGENT>`: 你的 Gitea API token根據 agent ID 取對應的)
- Agent → Gitea 帳號對應:
- tiangong → `tiangong`(天工,架構/安全)
- kaiwu → `kaiwu`開物UX/前端)
- yucheng → `yucheng`(玉成,全棧/測試)
## 審查重點
根據你的角色,重點審查不同面向:
### 天工tiangong— 架構設計師
- SKILL.md 的 trigger 設計是否合理、會不會誤觸發
- handler.ts 的錯誤處理、邊界情況
- 安全性:有無注入風險、敏感資訊洩漏
- Skill 之間的協作與依賴關係
### 開物kaiwu— 前端視覺
- SKILL.md 的使用者體驗:描述是否清楚、觸發詞是否直覺
- handler.ts 的輸出格式Telegram markdown 排版、emoji 使用
- 回覆內容的可讀性與美觀度
### 玉成yucheng— 全棧整合
- handler.ts 的程式碼品質:型別安全、效能、可維護性
- 缺少的功能或整合機會
- 測試邊界:空值處理、異常輸入
- 文件完整性
## 工作流程
### Step 1: 同步 fork
```
POST /api/v1/repos/{owner}/{repo}/mirror-sync # 如果有 mirror
```
或者直接用最新的 upstream 內容。
### Step 2: 讀取所有 Skills
讀取 repo 中 `skills/` 目錄下的每個 skill 的 SKILL.md 和 handler.ts。
### Step 3: 選擇改進目標
- 每次只改 **1 個 skill 的 1 個面向**(小而精確的 PR
- 如果所有 skill 都很好,可以提出新 skill 的建議
### Step 4: 透過 Gitea API 提交
1. **建立分支**(從 main
```
POST /api/v1/repos/{owner}/{repo}/branches
{"new_branch_name": "improve/daily-briefing-error-handling", "old_branch_name": "main"}
```
2. **更新檔案**
```
PUT /api/v1/repos/{owner}/{repo}/contents/{filepath}
{"content": "<base64>", "message": "commit message", "branch": "improve/...", "sha": "<current-sha>"}
```
3. **建立 PR**(從 fork 到 upstream
```
POST /api/v1/repos/Selig/openclaw-skill/pulls
{
"title": "improve(daily-briefing): 加強天氣查詢錯誤處理",
"body": "## 改進說明\n...\n## 變更內容\n...\n## 測試建議\n...",
"head": "<agent-username>:improve/daily-briefing-error-handling",
"base": "main"
}
```
## PR 格式規範
### 標題
```
<type>(<skill>): <簡述>
```
Type: `improve`, `fix`, `feat`, `docs`, `refactor`
### 內文
```markdown
## 改進說明
為什麼要做這個改動?發現了什麼問題?
## 變更內容
- 具體改了什麼
## 測試建議
- 如何驗證這個改動是正確的
---
🤖 由 <agent-name> 自動審查並提交
```
## 注意事項
- **一次只提一個 PR**,不要批量修改多個 skill
- **不要修改** handler.ts 中涉及認證、密碼、token 的部分
- **不要刪除** 現有功能,只能改進或新增
- 如果沒有值得改進的地方,回覆「所有 Skills 目前狀態良好,無需改動」即可
- PR 建立後,回覆 PR 的 URL 讓使用者知道

View File

@@ -0,0 +1,240 @@
/**
* skill-review handler
* 提供 Gitea API 操作的輔助函式,供 agent 審查 skill 並提交 PR。
*/
const GITEA_URL = process.env.GITEA_URL || 'https://git.nature.edu.kg';
const UPSTREAM_OWNER = 'Selig';
const REPO_NAME = 'openclaw-skill';
// Agent ID → Gitea 帳號 & token 環境變數對應
const AGENT_MAP: Record<string, { username: string; tokenEnv: string }> = {
tiangong: { username: 'tiangong', tokenEnv: 'GITEA_TOKEN_TIANGONG' },
kaiwu: { username: 'kaiwu', tokenEnv: 'GITEA_TOKEN_KAIWU' },
yucheng: { username: 'yucheng', tokenEnv: 'GITEA_TOKEN_YUCHENG' },
};
interface GiteaFile {
name: string;
path: string;
sha: string;
content?: string;
encoding?: string;
}
async function giteaApi(
token: string,
method: string,
path: string,
body?: any
): Promise<any> {
const url = `${GITEA_URL}/api/v1${path}`;
const opts: RequestInit = {
method,
headers: {
Authorization: `token ${token}`,
'Content-Type': 'application/json',
},
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch(url, opts);
const text = await res.text();
if (!res.ok) {
throw new Error(`Gitea API ${method} ${path}${res.status}: ${text}`);
}
return text ? JSON.parse(text) : null;
}
/** 同步 fork用 Gitea merge upstream API */
async function syncFork(token: string, owner: string): Promise<void> {
try {
// Gitea 1.25: POST /repos/{owner}/{repo}/merge-upstream
await giteaApi(token, 'POST', `/repos/${owner}/${REPO_NAME}/merge-upstream`, {
branch: 'main',
});
} catch (e: any) {
// 如果 API 不存在或已同步,忽略
if (!e.message.includes('409')) {
console.warn('syncFork warning:', e.message);
}
}
}
/** 列出 skills 目錄下的所有 skill */
async function listSkills(token: string, owner: string): Promise<string[]> {
const items = await giteaApi(
token,
'GET',
`/repos/${owner}/${REPO_NAME}/contents/skills?ref=main`
);
return items
.filter((item: any) => item.type === 'dir')
.map((item: any) => item.name);
}
/** 讀取檔案內容 */
async function readFile(
token: string,
owner: string,
filepath: string,
ref = 'main'
): Promise<GiteaFile> {
return giteaApi(
token,
'GET',
`/repos/${owner}/${REPO_NAME}/contents/${filepath}?ref=${ref}`
);
}
/** 建立分支 */
async function createBranch(
token: string,
owner: string,
branchName: string
): Promise<void> {
await giteaApi(token, 'POST', `/repos/${owner}/${REPO_NAME}/branches`, {
new_branch_name: branchName,
old_branch_name: 'main',
});
}
/** 更新檔案(需要 sha */
async function updateFile(
token: string,
owner: string,
filepath: string,
content: string,
sha: string,
branch: string,
message: string
): Promise<void> {
const b64 = Buffer.from(content, 'utf-8').toString('base64');
await giteaApi(
token,
'PUT',
`/repos/${owner}/${REPO_NAME}/contents/${filepath}`,
{ content: b64, sha, message, branch }
);
}
/** 建立新檔案 */
async function createFile(
token: string,
owner: string,
filepath: string,
content: string,
branch: string,
message: string
): Promise<void> {
const b64 = Buffer.from(content, 'utf-8').toString('base64');
await giteaApi(
token,
'POST',
`/repos/${owner}/${REPO_NAME}/contents/${filepath}`,
{ content: b64, message, branch }
);
}
/** 建立 PR從 fork 到 upstream */
async function createPR(
token: string,
agentUsername: string,
title: string,
body: string,
branch: string
): Promise<{ url: string; number: number }> {
const pr = await giteaApi(
token,
'POST',
`/repos/${UPSTREAM_OWNER}/${REPO_NAME}/pulls`,
{
title,
body,
head: `${agentUsername}:${branch}`,
base: 'main',
}
);
return { url: pr.html_url, number: pr.number };
}
export async function handler(ctx: any) {
// 偵測當前 agent
const agentId = ctx.env?.OPENCLAW_AGENT_ID || ctx.agentId || 'unknown';
const agentConfig = AGENT_MAP[agentId];
if (!agentConfig) {
return {
reply: `❌ 無法辨識 agent: ${agentId}\n支援的 agent: ${Object.keys(AGENT_MAP).join(', ')}`,
};
}
const token = ctx.env?.[agentConfig.tokenEnv] || process.env[agentConfig.tokenEnv];
if (!token) {
return {
reply: `❌ 找不到 ${agentConfig.tokenEnv},請確認 .env 設定。`,
};
}
const username = agentConfig.username;
try {
// Step 1: 同步 fork
await syncFork(token, username);
// Step 2: 列出所有 skills
const skills = await listSkills(token, username);
// Step 3: 讀取每個 skill 的內容
const skillContents: Record<string, { skillMd: string; handlerTs: string }> = {};
for (const skill of skills) {
try {
const skillMdFile = await readFile(token, username, `skills/${skill}/SKILL.md`);
const handlerFile = await readFile(token, username, `skills/${skill}/handler.ts`);
skillContents[skill] = {
skillMd: Buffer.from(skillMdFile.content || '', 'base64').toString('utf-8'),
handlerTs: Buffer.from(handlerFile.content || '', 'base64').toString('utf-8'),
};
} catch {
// 跳過讀取失敗的 skill
}
}
// 回傳資料供 agent 分析
return {
reply: `✅ Fork 已同步,共讀取 ${Object.keys(skillContents).length} 個 skills。\n\n請根據你的角色審查以下 skills選擇一個提出改進 PR。`,
data: {
agentId,
username,
skills: skillContents,
// 提供 API helper 資訊,讓 agent 知道可以用 exec 呼叫
api: {
createBranch: 'handler.createBranch(token, owner, branchName)',
updateFile: 'handler.updateFile(token, owner, filepath, content, sha, branch, message)',
createFile: 'handler.createFile(token, owner, filepath, content, branch, message)',
createPR: 'handler.createPR(token, agentUsername, title, body, branch)',
},
},
metadata: { agentId, username, skillCount: Object.keys(skillContents).length },
};
} catch (err: any) {
return {
reply: `❌ 執行失敗: ${err.message}`,
error: err.message,
};
}
}
// 匯出 helper 供 agent 透過 exec 使用
export {
syncFork,
listSkills,
readFile,
createBranch,
updateFile,
createFile,
createPR,
giteaApi,
AGENT_MAP,
};