From c8ecc39dc86e41e8b34baf36dccc7cfd0aa966bd Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:00:28 +0000 Subject: [PATCH] =?UTF-8?q?refactor(=E5=88=86=E9=A0=81):=20=E6=8A=8A?= =?UTF-8?q?=E9=80=90=E9=A0=81=E8=B5=B0=E8=A8=AA=E6=8A=BD=E6=88=90=20lib.pa?= =?UTF-8?q?ges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findIssueByTitle 原本自己寫了一份「翻到短頁為止、超過上限就報錯」的迴圈, 而新的留言走訪需要同一套規則。抽成非同步產生器之後,呼叫端仍能在找到目標時 提早離開,規則卻只寫一次。 錯誤碼與訊息由呼叫端指定:查重讀不完要講的是「可能重建議題」,數留言讀不完 要講的是「數不完未整併的則數」,處置不同就不該共用一句話。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/lib.js | 63 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/scripts/lib.js b/scripts/lib.js index d9cba81..5b43d13 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -443,25 +443,56 @@ export async function listLabels(login, repo) { * @param {string} title 要找的標題 * @returns {Promise} 找到的議題,或 null */ -export async function findIssueByTitle(login, repo, title) { - const wanted = title.trim(); - const pageSize = 50; - // 翻頁上限。查不完就停手並報錯,不能無聲回 null——那會讓呼叫端把既有議題再建一次, - // 正好是冪等查重要防的事。 - const maxPages = 200; +/** + * 逐頁走訪一個回傳陣列的 Gitea 端點。 + * + * 所有「必須看完整份清單」的走訪都走這裡:讀不完就要大聲報錯,不能無聲回傳半份。 + * 半份清單比報錯更危險——查重會漏掉既有議題而重建一顆,數留言會少算而讓下游 + * 拿著過期描述做事。 + * @param {{base: string, token: string}} login + * @param {string} path + * @param {{query?: object, pageSize?: number, maxPages?: number, limitCode?: string, limitHint?: string}} options + * @returns {AsyncGenerator} 每次 yield 一頁 + */ +export async function* pages(login, path, options = {}) { + const { + query = {}, + pageSize = 50, + maxPages = 200, + limitCode = 'PAGE_LIMIT', + limitHint = `${path} 的資料量超出可走訪範圍`, + } = options; for (let page = 1; page <= maxPages; page += 1) { - const response = await giteaRequest(login, 'GET', `/repos/${repo}/issues`, { - query: { state: 'all', limit: pageSize, page }, + const response = await giteaRequest(login, 'GET', path, { + query: { ...query, limit: pageSize, page }, }); - const issues = expectOk(response, `GET /repos/${repo}/issues`) ?? []; - const hit = issues.find((issue) => (issue.title ?? '').trim() === wanted); - if (hit) return hit; - if (issues.length < pageSize) return null; + const items = expectOk(response, `GET ${path}`) ?? []; + yield items; + if (items.length < pageSize) return; } - throw new ScriptError( - 'DEDUPE_LIMIT', - `翻完 ${maxPages * pageSize} 筆議題仍未查完 ${repo},無法確認「${wanted}」是否已存在;請縮小範圍或手動確認`, - ); + throw new ScriptError(limitCode, `${limitHint}(已讀 ${maxPages * pageSize} 筆仍未讀完)`); +} + +/** + * 依標題找出既有議題,讓寫入型腳本中斷重跑時不會產生重複議題。 + * 比對前後空白修掉:同一顆議題不該因為標題多了一個空格就被當成新的。 + * @param {{base: string, token: string}} login + * @param {string} repo owner/name + * @param {string} title 要找的標題 + * @returns {Promise} 找到的議題,或 null + */ +export async function findIssueByTitle(login, repo, title) { + const wanted = title.trim(); + + for await (const issues of pages(login, `/repos/${repo}/issues`, { + query: { state: 'all' }, + limitCode: 'DEDUPE_LIMIT', + limitHint: `翻不完 ${repo} 的議題,無法確認「${wanted}」是否已存在;請縮小範圍或手動確認`, + })) { + const hit = issues.find((issue) => (issue.title ?? '').trim() === wanted); + if (hit) return hit; + } + return null; }