'use strict'; // 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。 // 隱藏標記:辨識哪些留言是本 action 發的(步驟 2 標註過時時使用)。 const MARK = ''; // 舊留言標註前綴(步驟 2 的降級做法:無 resolve API 時編輯加註)。 const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n'; // 嚴重等級對應的 emoji 與排序權重。 const SEVERITY_EMOJI = { 嚴重: '🔴', 警告: '🟠', 建議: '🔵' }; const SEVERITY_ORDER = { 嚴重: 0, 警告: 1, 建議: 2 }; // 面向代碼對應的中文標籤。 const FOCUS_LABEL = { logic: '邏輯', security: '安全性', efficiency: '效率', style: '風格', testing: '測試', maintainability: '可維護性', verdict: '裁決', }; /** * 把任意文字整理成可安全放進 Markdown 表格儲存格的單行內容。 * * 處理順序:nullish 轉空字串 → 逸出管線符號(`|` → `\|`)→ 換行轉 `
` * → 去除頭尾空白 → 空字串以 `—` 佔位。純函式、無副作用,任何輸入 * (含 null/undefined/數字)都不會拋出例外。 * * @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,null/undefined 視為空字串。 * @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。 * @remarks * 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 4 的 * `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理, * 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。 * 本函式未匯出,僅供模組內部使用。 */ function cell(text) { return String(text ?? '') .replace(/\|/g, '\\|') .replace(/\r?\n/g, '
') .trim() || '—'; } /** * 把審查面向代碼轉成「中文(原文)」的顯示字串。 * * 以模組常數 `FOCUS_LABEL` 查表,支援 logic/security/efficiency/ * style/testing/maintainability/verdict 七種代碼;查表命中回傳 * 「中文(代碼)」格式,未命中則原樣回傳代碼,falsy 輸入回傳 `—`。 * * @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。 * @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。 * @remarks * 使用情境:審查流程步驟 5/7 的角色登場留言——`rolesComment()` * 產生「角色|面向|個性」表格時,以本函式把每位審查員 * (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。 * 本函式未匯出,僅供模組內部使用。 */ function focusLabel(focus) { const label = FOCUS_LABEL[focus]; return label ? `${label}(${focus})` : focus || '—'; } /** * 產生審查流程步驟 3 的「審查工具」PR 留言內容。 * * 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/ * 審查 commit/Run Job 連結)與一張 mermaid 流程圖,說明整條審查管線 * (整理 git diff → 攻擊方找問題 → 防守方裁決 → 保存 findings → 留言到 PR)。 * * @param {Object} params - 工具資訊(解構參數)。 * @param {string} params.toolName - 審查工具名稱,直接以行內程式碼呈現(不經 cell 逸出)。 * @param {string} params.version - 工具版本;經 cell() 防呆。 * @param {string} [params.model] - 使用的 AI 模型;falsy 時顯示「(工具預設)」。 * @param {string} params.sha - 本回合審查的 commit SHA;經 cell() 防呆。 * @param {string|number} params.runNumber - CI run 編號,作為連結文字;經 cell() 防呆。 * @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。 * @remarks * 使用情境:審查流程步驟 3——每回合審查開始時,先把工具身分與 * 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行; * 留言開頭的 MARK 讓步驟 2 能辨識並將舊回合留言標註為過時。 */ function toolComment({ toolName, version, model, sha, runNumber, runLink }) { return `${MARK} ## 🤖 AI Code Review|審查工具 | 項目 | 內容 | | --- | --- | | 工具 | \`${toolName}\` | | 版本 | \`${cell(version)}\` | | 模型 | ${model ? `\`${cell(model)}\`` : '(工具預設)'} | | 審查 commit | \`${cell(sha)}\` | | Run Job | [#${cell(runNumber)}](${runLink}) | \`\`\`mermaid flowchart LR A[整理 git diff] --> B[⚔️ 攻擊方找問題] B --> C[🛡️ 防守方裁決] C --> D[保存 findings] D --> E[留言到 PR] \`\`\` `; } /** * 產生審查流程步驟 4 的「變更摘要(送審 git diff)」PR 留言內容。 * * 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合 * 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」, * 無任何檔案時補上「(無)」佔位列。結尾以引言統計納入審查的檔案數, * 並在有排除檔案時註明 `.reviewignore` 排除數量。 * * @param {Array} rows - 送審檔案清單,每筆一列。 * @param {string} rows[].file - 檔案路徑;經 cell() 防呆後以行內程式碼呈現。 * @param {string} rows[].purpose - 檔案用途說明;經 cell() 防呆。 * @param {number} rows[].lines - 該檔 git diff 行數。 * @param {number} rows[].chars - 該檔 git diff 字元數。 * @param {boolean} [rows[].truncated] - 是否因 diff 過長而截斷送審。 * @param {string} rows[].lastUpdated - 檔案最後更新時間;經 cell() 防呆。 * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:審查流程步驟 4——整理完 git diff 後,把「哪些檔案、多長、 * 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍 * 與 AI 實際看到的內容一致。 */ function diffComment(rows, ignoredCount) { const lines = [ MARK, '## 📋 變更摘要(送審 git diff)', '', '| 檔案 | 用途 | git diff 長度 | 最後更新時間 |', '| --- | --- | --- | --- |', ]; for (const row of rows) { const length = `${row.lines} 行/${row.chars} 字元${row.truncated ? '(過長截斷送審)' : ''}`; lines.push(`| \`${cell(row.file)}\` | ${cell(row.purpose)} | ${length} | ${cell(row.lastUpdated)} |`); } if (rows.length === 0) { lines.push('| (無) | — | — | — |'); } lines.push(''); lines.push(`> 共 ${rows.length} 個檔案納入審查${ignoredCount > 0 ? `;另有 ${ignoredCount} 個檔案依 \`.reviewignore\` 排除` : ''}。`); return lines.join('\n'); } /** * 產生審查流程步驟 5/7 共用的「角色登場」PR 留言內容。 * * 以三欄表格(角色/面向/個性)列出本回合登場的審查員; * 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。 * 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。 * * @param {Object} params - 留言內容(解構參數)。 * @param {string} params.title - 留言標題(接在 `## ` 之後),由呼叫端決定攻擊方或防守方文案;不經逸出。 * @param {Array} params.roles - 登場角色清單,每筆一列。 * @param {Object} params.roles[].meta - 角色的中繼資料。 * @param {string} [params.roles[].meta.badge] - 角色徽章(通常為 emoji);缺省時以空字串呈現。 * @param {string} params.roles[].meta.name - 角色名稱,粗體呈現;經 cell() 防呆。 * @param {string} params.roles[].meta.focus - 審查面向代碼(如 `'logic'`);經 focusLabel() 轉為「中文(原文)」。 * @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:審查流程步驟 5(攻擊方登場)與步驟 7(防守方登場)—— * 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性 * 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。 */ function rolesComment({ title, roles }) { const lines = [ MARK, `## ${title}`, '', '| 角色 | 面向 | 個性 |', '| --- | --- | --- |', ]; for (const role of roles) { lines.push( `| ${role.meta.badge || ''} **${cell(role.meta.name)}** | ${cell(focusLabel(role.meta.focus))} | ${cell(role.meta.personality)} |`, ); } return lines.join('\n'); } /** * 產生審查流程步驟 9 的「單條嚴重問題」留言內容(掛在程式碼行上)。 * * 留言含嚴重度 emoji 標題(審查員具名)、可選的位置與程式碼片段、 * 「問題」「修改建議」段落、可選的「建議寫法」程式碼區塊,並以 * 「開發者可直接回覆本留言討論或說明取捨」收尾。 * * @param {Object} finding - 單條審查發現。 * @param {string} finding.severity - 嚴重等級(嚴重/警告/建議);決定標題 emoji,未知等級 fallback 為 🔴。 * @param {string} [finding.badge] - 審查員徽章(通常為 emoji);缺省時省略。 * @param {string} finding.reviewer - 審查員名稱。 * @param {string} finding.file - 問題所在檔案路徑;僅 withLocation 為 true 時輸出。 * @param {number} finding.startLine - 問題起始行號;僅 withLocation 為 true 時輸出。 * @param {number} finding.endLine - 問題結束行號;僅 withLocation 為 true 時輸出。 * @param {string} [finding.problem] - 問題描述;缺省以 `—` 佔位。 * @param {string} [finding.suggestion] - 修改建議;缺省以 `—` 佔位。 * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @param {string} [snippet] - 問題所在的原始程式碼片段;有值才輸出程式碼圍欄。 * @param {Object} [options] - 選項(解構參數,預設空物件)。 * @param {boolean} [options.withLocation=false] - 是否在內文標明「位置」(檔案與起訖行);掛行留言本身已定位時可省略。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:審查流程步驟 9——防守方裁決後保留的每條「嚴重」finding, * 逐條以 inline review comment 掛在 PR 對應程式碼行上;若平台不支援 * 掛行而降級為一般留言時,改以 `withLocation: true` 在內文標明位置。 */ function severeCommentBody(finding, snippet, { withLocation = false } = {}) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔴'; const parts = [MARK, `### ${emoji} ${finding.severity}|${finding.badge || ''} ${finding.reviewer}`, '']; if (withLocation) { parts.push(`**位置**:\`${finding.file}\` 第 ${finding.startLine}–${finding.endLine} 行`, ''); } if (snippet) { parts.push('```', snippet, '```', ''); } parts.push('**問題**', '', finding.problem || '—', ''); parts.push('**修改建議**', '', finding.suggestion || '—', ''); if (finding.suggestedCode) { parts.push('**建議寫法**', '', '```', finding.suggestedCode, '```', ''); } parts.push('> 開發者可直接回覆本留言討論或說明取捨。'); return parts.join('\n'); } /** * 產生審查流程步驟 9 的「嚴重問題 review 總覽」內容。 * * 作為 PR review 的整體 body:標題標明嚴重問題總數,並說明各條問題 * 已逐條掛在對應程式碼行上(由 severeCommentBody() 產生的 inline * comment),請開發者逐一處理或回覆說明。 * * @param {number} count - 本回合嚴重 findings 的總條數,直接內插進標題。 * @returns {string} review 總覽 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:審查流程步驟 9——把所有嚴重 findings 以單一 PR review * 送出時,本函式產生 review 的 body 總覽,搭配每條 finding 各自的 * inline comment(severeCommentBody),讓開發者先看到總數再逐條處理。 */ function severeReviewBody(count) { return `${MARK} ## 🔴 嚴重問題(共 ${count} 條) 以下嚴重問題已逐條掛在對應程式碼行上,請逐一處理或回覆說明。`; } /** * 產生審查流程步驟 10 的「其他問題(警告+建議)彙整」PR 留言內容。 * * 非嚴重的 findings 不逐條掛在程式碼行上,改以六欄表格 * (等級/審查員/檔案名稱/問題起訖行數/問題描述/修改建議) * 集中呈現;等級欄依 SEVERITY_EMOJI 顯示 emoji(警告 🟠、建議 🔵, * 未知等級 fallback 為 🔵),描述與建議經 cell() 防呆避免撐破表格。 * * @param {Array} findings - 警告+建議等級的審查發現清單,每筆一列。 * @param {string} findings[].severity - 嚴重等級(警告/建議);決定等級欄 emoji。 * @param {string} [findings[].badge] - 審查員徽章(通常為 emoji);缺省時省略。 * @param {string} findings[].reviewer - 審查員名稱;經 cell() 防呆。 * @param {string} findings[].file - 問題所在檔案路徑;經 cell() 防呆後以行內程式碼呈現。 * @param {number} findings[].startLine - 問題起始行號。 * @param {number} findings[].endLine - 問題結束行號。 * @param {string} findings[].problem - 問題描述;經 cell() 防呆。 * @param {string} findings[].suggestion - 修改建議;經 cell() 防呆。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:審查流程步驟 10——防守方裁決後留下的「警告」與「建議」 * 等級 findings,不像嚴重問題逐條掛行(步驟 9),而是彙整成單一 * 表格留言發到 PR,讓開發者一覽非阻擋性的改善事項。 */ function othersComment(findings) { const lines = [ MARK, `## 🟠 其他問題(警告+建議,共 ${findings.length} 條)`, '', '| 等級 | 審查員 | 檔案名稱 | 問題起訖行數 | 問題描述 | 修改建議 |', '| --- | --- | --- | --- | --- | --- |', ]; for (const finding of findings) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; lines.push( `| ${emoji} ${finding.severity} | ${finding.badge || ''} ${cell(finding.reviewer)} | \`${cell(finding.file)}\` | ${finding.startLine}–${finding.endLine} | ${cell(finding.problem)} | ${cell(finding.suggestion)} |`, ); } return lines.join('\n'); } /** * 產生建問題模式(input: create-issue)新 issue 的本文: * 以 PR 描述為主體(缺省時以「(PR 無描述)」佔位), * 尾端附水平線與追溯引言,標明本 issue 由 AI Code Review 依哪個 PR 自動建立、 * 問題明細見 issue 下方留言。 * * @param {Object} params - 解構參數。 * @param {number} params.prNumber - 來源 PR 編號;內插到追溯引言(`PR #N`)。 * @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。 * @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:建問題模式下 `main()`(src/index.js)的 `createIssueAndFlushBufferedComments` 建立 issue 時, * 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue`, * 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。 */ function issueBody({ prNumber, prBody }) { const body = (prBody || '').trim(); return `${MARK} ${body || '(PR 無描述)'} --- > 本問題由 AI Code Review 依 PR #${prNumber} 的審查結果自動建立,問題明細見下方留言。`; } /** * 產生建問題模式(input: create-issue)下單條 finding 的 issue 留言內容。 * * 固定模板:嚴重等級 emoji 標題(審查員具名)→ 位置(檔案與起訖行數,一律輸出) * → 問題描述 → 修改建議 → 可選的「建議寫法」程式碼區塊。 * issue 留言無法掛在程式碼行上,故位置固定以內文標明。 * * @param {Object} finding - 單條審查發現。 * @param {string} finding.severity - 嚴重等級(嚴重/警告/建議);決定標題 emoji,未知等級 fallback 為 🔵。 * @param {string} [finding.badge] - 審查員徽章(通常為 emoji);缺省時省略。 * @param {string} finding.reviewer - 審查員名稱。 * @param {string} finding.file - 問題所在檔案路徑(repo 相對路徑)。 * @param {number} finding.startLine - 問題起始行號(新版檔案行號)。 * @param {number} finding.endLine - 問題結束行號(新版檔案行號)。 * @param {string} [finding.problem] - 問題描述;缺省以 `—` 佔位。 * @param {string} [finding.suggestion] - 修改建議;缺省以 `—` 佔位。 * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:建問題模式下 `review.postSevereToIssue`(src/lib/review.js)把每條嚴重 finding * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上, * 作為問題明細的追蹤紀錄。 */ function issueFindingComment(finding) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; const parts = [ MARK, `### ${emoji} ${finding.severity}|${finding.badge || ''} ${finding.reviewer}`, '', `**位置**:\`${finding.file}\` 第 ${finding.startLine}–${finding.endLine} 行`, '', '**問題描述**', '', finding.problem || '—', '', '**修改建議**', '', finding.suggestion || '—', ]; if (finding.suggestedCode) { parts.push('', '**建議寫法**', '', '```', finding.suggestedCode, '```'); } return parts.join('\n'); } /** * 產生「無可審查變更」時的 PR 留言內容。 * * 當 git diff 套用 `.reviewignore` 過濾後沒有任何檔案需要送審時, * 以本留言取代正常的變更摘要(沿用相同標題「📋 變更摘要」), * 說明本次 PR 沒有可審查的變更並宣告視為審查通過; * 有檔案被排除時加註排除數量。 * * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:審查流程步驟 4 的替代路徑——整理 git diff 時發現 * 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除), * 直接以本留言告知開發者本回合視為審查通過,不再進入 * 攻擊方/防守方審查階段。 */ function nothingToReviewComment(ignoredCount) { return `${MARK} ## 📋 變更摘要(送審 git diff) 本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `(${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`; } /** * 產生建問題模式(input: create-issue)下,回貼到「PR」的追蹤問題連結留言。 * * 建問題模式把審查內容全部發到 issue、不留在 PR;本留言是 PR 上唯一的一則審查留言, * 提供 issue 連結與問題數量統計,讓 PR 讀者一眼看到「本次審查結果在哪個 issue」。 * * @param {Object} params - 解構參數。 * @param {number} params.issueNumber - 追蹤問題的 issue 編號;內插為 Markdown 連結文字。 * @param {string} params.issueUrl - 追蹤問題的 issue 網址;作為 Markdown 連結目標。 * @param {number} params.severeCount - 嚴重問題條數,顯示在統計。 * @param {number} params.otherCount - 警告+建議問題條數,顯示在統計。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks * 使用情境:建問題模式下 `main()`(src/index.js)在 issue 建立並寫入全部審查內容後, * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 */ function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { return `${MARK} ## 🔍 AI Code Review|已建立追蹤問題 本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`; } module.exports = { MARK, OUTDATED_PREFIX, SEVERITY_EMOJI, SEVERITY_ORDER, toolComment, diffComment, rolesComment, severeCommentBody, severeReviewBody, othersComment, issueBody, issueFindingComment, nothingToReviewComment, issueLinkComment, };