From 9ab15e99e5632c53e5a5a5f034f39e3671366eb0 Mon Sep 17 00:00:00 2001 From: Selig Date: Fri, 13 Mar 2026 11:11:27 +0800 Subject: [PATCH] feat(skill-review): Agent PR workflow skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables agents (tiangong, kaiwu, yucheng) to review skills and submit improvement PRs via Gitea fork → branch → PR workflow. --- skills/skill-review/SKILL.md | 120 +++++++++++++++++ skills/skill-review/handler.ts | 240 +++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 skills/skill-review/SKILL.md create mode 100644 skills/skill-review/handler.ts diff --git a/skills/skill-review/SKILL.md b/skills/skill-review/SKILL.md new file mode 100644 index 0000000..f30259c --- /dev/null +++ b/skills/skill-review/SKILL.md @@ -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 基礎 URL(https://git.nature.edu.kg) +- `GITEA_TOKEN_`: 你的 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": "", "message": "commit message", "branch": "improve/...", "sha": ""} + ``` + +3. **建立 PR**(從 fork 到 upstream) + ``` + POST /api/v1/repos/Selig/openclaw-skill/pulls + { + "title": "improve(daily-briefing): 加強天氣查詢錯誤處理", + "body": "## 改進說明\n...\n## 變更內容\n...\n## 測試建議\n...", + "head": ":improve/daily-briefing-error-handling", + "base": "main" + } + ``` + +## PR 格式規範 + +### 標題 +``` +(): <簡述> +``` +Type: `improve`, `fix`, `feat`, `docs`, `refactor` + +### 內文 +```markdown +## 改進說明 +為什麼要做這個改動?發現了什麼問題? + +## 變更內容 +- 具體改了什麼 + +## 測試建議 +- 如何驗證這個改動是正確的 + +--- +🤖 由 自動審查並提交 +``` + +## 注意事項 + +- **一次只提一個 PR**,不要批量修改多個 skill +- **不要修改** handler.ts 中涉及認證、密碼、token 的部分 +- **不要刪除** 現有功能,只能改進或新增 +- 如果沒有值得改進的地方,回覆「所有 Skills 目前狀態良好,無需改動」即可 +- PR 建立後,回覆 PR 的 URL 讓使用者知道 diff --git a/skills/skill-review/handler.ts b/skills/skill-review/handler.ts new file mode 100644 index 0000000..0fcb82d --- /dev/null +++ b/skills/skill-review/handler.ts @@ -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 = { + 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 { + 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 { + 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 { + 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 { + return giteaApi( + token, + 'GET', + `/repos/${owner}/${REPO_NAME}/contents/${filepath}?ref=${ref}` + ); +} + +/** 建立分支 */ +async function createBranch( + token: string, + owner: string, + branchName: string +): Promise { + 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 { + 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 { + 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 = {}; + + 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, +};