feat(ai-code-review): 新增 AI 多角色 code review action(攻防審查、findings 保存、建問題模式)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jeffery
2026-07-17 16:53:27 +08:00
co-authored by Claude Fable 5
parent 6a0573b7c0
commit d08b97bd87
18 changed files with 2884 additions and 19 deletions
+94
View File
@@ -0,0 +1,94 @@
'use strict';
const fs = require('fs');
const path = require('path');
// 角色提示載入:讀取 src/prompts/roles/*.md,解析 YAML frontmatter 取出角色中繼資料。
/**
* 載入指定目錄下的全部角色提示檔(`*.md`),解析各檔開頭的 YAML frontmatter 為中繼資料。
*
* 只處理副檔名為 `.md` 的檔案,並依檔名字串排序,確保輸出順序穩定。
* frontmatter 採輕量解析:僅支援位於檔案開頭、以 `---` 包夾的「鍵: 值」單行欄位
* (鍵名限英文字母與底線),值外層的一對雙引號會被去除;不支援巢狀或多行值。
*
* @param {string} rolesDir - 角色提示檔所在目錄的路徑(例如 action 內的 `src/prompts/roles`)。
* @returns {Array<{file: string, meta: Object.<string, string>, body: string, raw: string}>}
* 角色物件陣列(依檔名排序):
* - `file`:檔名(不含目錄),例如 `mage.md`。
* - `meta`frontmatter 鍵值物件(如 `name`、`side`、`focus`、`badge`、`color`、`personality`);
* 檔案無 frontmatter 時為空物件。
* - `body`:去除 frontmatter 後的 Markdown 內文;無 frontmatter 時等於全文。
* - `raw`:原始完整檔案內容。
* @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時,
* 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。
* @remarks
* 使用情境:`src/index.js` 於審查流程步驟 4 呼叫
* `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色,
* 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位
* 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin),
* 供後續組裝各角色的 review 提示詞。
*/
function loadRoles(rolesDir) {
return fs
.readdirSync(rolesDir)
.filter((file) => file.endsWith('.md'))
.sort()
.map((file) => {
const raw = fs.readFileSync(path.join(rolesDir, file), 'utf8');
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
const meta = {};
if (match) {
for (const line of match[1].split(/\r?\n/)) {
const kv = /^([A-Za-z_]+):\s*(.*)$/.exec(line.trim());
if (kv) meta[kv[1]] = kv[2].replace(/^"(.*)"$/, '$1');
}
}
return {
file,
meta,
body: match ? raw.slice(match[0].length) : raw,
raw,
};
});
}
/**
* 從角色陣列中過濾出攻擊方角色(frontmatter `side: attack`)。
*
* 以嚴格相等比對 `role.meta.side === 'attack'`(大小寫敏感),
* 回傳新陣列且保留原輸入順序(即 {@link loadRoles} 的檔名排序),不修改原陣列。
*
* @param {Array<{file: string, meta: Object.<string, string>, body: string, raw: string}>} roles
* {@link loadRoles} 回傳的角色物件陣列。
* @returns {Array<{file: string, meta: Object.<string, string>, body: string, raw: string}>}
* 僅含 `meta.side === 'attack'` 的角色新陣列;無符合者回傳空陣列。
* @remarks
* 使用情境:`src/index.js` 在 `loadRoles(...)` 之後呼叫 `attackersOf(roles)`
* 取得攻擊方角色(Mage 邏輯、Assassin 安全、Rogue 效率、Bard 風格、
* Leo 可維護性、Maya 測試)以對 PR diff 發動各面向的攻擊式 review。
*/
function attackersOf(roles) {
return roles.filter((role) => role.meta.side === 'attack');
}
/**
* 從角色陣列中過濾出防守方角色(frontmatter `side: defend`)。
*
* 以嚴格相等比對 `role.meta.side === 'defend'`(大小寫敏感),
* 回傳新陣列且保留原輸入順序(即 {@link loadRoles} 的檔名排序),不修改原陣列。
*
* @param {Array<{file: string, meta: Object.<string, string>, body: string, raw: string}>} roles
* {@link loadRoles} 回傳的角色物件陣列。
* @returns {Array<{file: string, meta: Object.<string, string>, body: string, raw: string}>}
* 僅含 `meta.side === 'defend'` 的角色新陣列;無符合者回傳空陣列。
* @remarks
* 使用情境:`src/index.js` 在 `loadRoles(...)` 之後呼叫 `defendersOf(roles)`
* 取得防守方角色(現況為 Paladin`focus: verdict`)擔任裁決者,
* 依原始碼脈絡與排除事項裁定攻擊方提出的 findings 是否成立。
*/
function defendersOf(roles) {
return roles.filter((role) => role.meta.side === 'defend');
}
module.exports = { loadRoles, attackersOf, defendersOf };