diff --git a/content/auth-page-recovery.js b/content/auth-page-recovery.js index 102d2f7..15bebbc 100644 --- a/content/auth-page-recovery.js +++ b/content/auth-page-recovery.js @@ -80,6 +80,36 @@ }; } + async function waitForRetryPageRecoveryAfterClick(options = {}) { + const { + pathPatterns = [], + pollIntervalMs = 250, + settleAfterClickMs = 3000, + } = options; + const startedAt = Date.now(); + + while (Date.now() - startedAt < settleAfterClickMs) { + if (typeof throwIfStopped === 'function') { + throwIfStopped(); + } + + const retryState = getAuthTimeoutErrorPageState({ pathPatterns }); + if (!retryState) { + return { + recovered: true, + elapsedMs: Date.now() - startedAt, + }; + } + + await sleep(pollIntervalMs); + } + + return { + recovered: false, + elapsedMs: Date.now() - startedAt, + }; + } + async function recoverAuthRetryPage(options = {}) { const { logLabel = '', @@ -87,7 +117,7 @@ pollIntervalMs = 250, step = null, timeoutMs = 12000, - waitAfterClickMs = 1200, + waitAfterClickMs = 3000, } = options; const start = Date.now(); let clickCount = 0; @@ -116,7 +146,18 @@ await humanPause(300, 800); } simulateClick(retryState.retryButton); - await sleep(waitAfterClickMs); + const recoveryResult = await waitForRetryPageRecoveryAfterClick({ + pathPatterns, + pollIntervalMs, + settleAfterClickMs: waitAfterClickMs, + }); + if (recoveryResult.recovered) { + return { + recovered: true, + clickCount, + url: location.href, + }; + } continue; } diff --git a/content/signup-page.js b/content/signup-page.js index d10617a..8dc8833 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -955,7 +955,7 @@ async function recoverCurrentAuthRetryPage(payload = {}) { logLabel = '', step = null, timeoutMs = 12000, - waitAfterClickMs = 1200, + waitAfterClickMs = 3000, } = payload; const pathPatterns = getAuthRetryPathPatternsForFlow(flow); if (authPageRecovery?.recoverAuthRetryPage) { @@ -986,7 +986,18 @@ async function recoverCurrentAuthRetryPage(payload = {}) { log(`${logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`}(第 ${clickCount} 次)...`, 'warn'); await humanPause(300, 800); simulateClick(retryState.retryButton); - await sleep(waitAfterClickMs); + const settleStart = Date.now(); + while (Date.now() - settleStart < waitAfterClickMs) { + throwIfStopped(); + if (!getCurrentAuthRetryPageState(flow)) { + return { + recovered: true, + clickCount, + url: location.href, + }; + } + await sleep(250); + } continue; } diff --git a/tests/auth-page-recovery.test.js b/tests/auth-page-recovery.test.js index ed92d1a..582c4c5 100644 --- a/tests/auth-page-recovery.test.js +++ b/tests/auth-page-recovery.test.js @@ -44,10 +44,19 @@ function createRecoveryApi(state) { log: () => {}, simulateClick: () => { state.clickCount += 1; + if (typeof state.onClick === 'function') { + state.onClick(state); + return; + } state.retryVisible = false; state.pageText = 'Recovered login form'; }, - sleep: async () => {}, + sleep: async (ms = 0) => { + await new Promise((resolve) => setTimeout(resolve, Math.max(1, Math.min(5, ms)))); + if (typeof state.onSleep === 'function') { + state.onSleep(state); + } + }, throwIfStopped: () => {}, titlePattern: /something went wrong/i, }); @@ -94,3 +103,35 @@ test('auth page recovery clicks retry and waits until page recovers', async () = assert.equal(state.clickCount, 1); assert.equal(state.retryVisible, false); }); + +test('auth page recovery can click retry twice before page recovers', async () => { + const state = { + clickCount: 0, + pageText: 'Something went wrong. Operation timed out.', + retryVisible: true, + onClick(currentState) { + if (currentState.clickCount >= 2) { + currentState.retryVisible = false; + currentState.pageText = 'Recovered login form'; + } + }, + }; + const api = createRecoveryApi(state); + + const result = await api.recoverAuthRetryPage({ + logLabel: '步骤 8:检测到重试页,正在点击“重试”恢复', + pathPatterns: [/\/log-in(?:[/?#]|$)/i], + step: 8, + timeoutMs: 200, + waitAfterClickMs: 10, + pollIntervalMs: 1, + }); + + assert.deepStrictEqual(result, { + recovered: true, + clickCount: 2, + url: 'https://auth.openai.com/log-in', + }); + assert.equal(state.clickCount, 2); + assert.equal(state.retryVisible, false); +}); diff --git a/项目开发规范(AI协作).md b/项目开发规范(AI协作).md index dd172db..fdc753f 100644 --- a/项目开发规范(AI协作).md +++ b/项目开发规范(AI协作).md @@ -1,4 +1,4 @@ -# 项目开发规范(AI协作) +# 项目开发规范(AI协作) 本文档是面向 AI 与开发者的项目开发规范。 @@ -12,7 +12,8 @@ - 目标是“让项目更清晰、更可维护、更可测试”,不是单纯把代码拆碎。 - 重构优先考虑稳定性、职责边界与可理解性。 -- 任何新增功能都必须沿现有分层接入,禁止重新堆回巨石文件。 +- 任何新增功能都必须沿现有分层接入,禁止重新堆回大文件。 +- 乱码问题视为阻塞问题,不是“后面再顺手修”的小问题。 ## 1. 架构原则 @@ -31,13 +32,13 @@ - 步骤顺序统一由: - [data/step-definitions.js](c:/Users/projectf/Downloads/codex注册扩展/data/step-definitions.js) - [background/steps/registry.js](c:/Users/projectf/Downloads/codex注册扩展/background/steps/registry.js) - 共同管理。 + 共同管理。 ### 1.3 前后端步骤定义共享原则 -- 任何步骤标题、顺序、key 变更,必须优先改 [data/step-definitions.js](c:/Users/projectf/Downloads/codex注册扩展/data/step-definitions.js) -- 不允许只改 sidepanel 文案而不改共享定义 -- 不允许只改 registry 而不改共享定义 +- 任何步骤标题、顺序、key 变更,必须优先改 [data/step-definitions.js](c:/Users/projectf/Downloads/codex注册扩展/data/step-definitions.js)。 +- 不允许只改 sidepanel 文案而不改共享定义。 +- 不允许只改 registry 而不改共享定义。 ## 2. 模块边界规则 @@ -49,7 +50,7 @@ - 某个单独步骤文件 - 某个单独 manager 文件 -### 2.2 不应该继续膨胀的文件 +### 2.2 不应继续膨胀的文件 - [background.js](c:/Users/projectf/Downloads/codex注册扩展/background.js) - [sidepanel/sidepanel.js](c:/Users/projectf/Downloads/codex注册扩展/sidepanel/sidepanel.js) @@ -77,7 +78,7 @@ 1. 是否有纯工具模块 2. 是否需要 background provider 调度逻辑 3. 是否需要 sidepanel 配置项 -4. 是否需要 Step 4 / 7 验证码链路接入 +4. 是否需要 Step 4 / 8 验证码链路接入 5. 是否需要成功收尾逻辑 6. 是否需要 README 与完整链路文档更新 @@ -105,7 +106,7 @@ 3. 导入导出 4. state restore 5. sidepanel UI -6. 是否挂在正确的职责域下 +6. 是否挂在正确的职责域中 7. 文档 ## 4. 测试规范 @@ -116,7 +117,7 @@ - 优先测试: - 模块是否接入 - 核心纯函数是否仍可验证 - - 回退/停止/异常传播是否仍正确 + - 回退、停止、异常传播是否仍正确 ### 4.2 不允许的做法 @@ -145,11 +146,11 @@ npm test ### 5.1 必须更新文档的场景 -- 文件新增/删除/重命名 +- 文件新增、删除、重命名后 更新 [项目文件结构说明.md](c:/Users/projectf/Downloads/codex注册扩展/项目文件结构说明.md) -- 功能链路变化 +- 功能链路变化 更新 [项目完整链路说明.md](c:/Users/projectf/Downloads/codex注册扩展/项目完整链路说明.md) -- 开发流程、边界、约束变化 +- 开发流程、边界、约束变化 更新当前文件 ### 5.2 文档更新要求 @@ -159,6 +160,14 @@ npm test - 不能让结构文档漏文件 - 不能让链路文档落后于真实实现 +### 5.3 乱码要求 + +- 所有中文文档、中文注释、中文日志、sidepanel 文案、错误提示文案都必须避免乱码。 +- 修改任何包含中文的文件时,必须把“乱码检查”视为与“功能是否正确”同级的必做项。 +- 如果某次修改引入了可见乱码,则该次开发视为未完成,不能提交为最终结果。 +- 不允许把“当前终端显示有点乱,但文件也许没问题”当作默认成立;必须做显式检查。 +- 如果文件历史上曾出现过编码问题,修改后必须再次审查该文件整体,而不是只看改动片段。 + ## 6. 命名规范 ### 6.1 文件命名 @@ -177,11 +186,12 @@ npm test - 优先复用现有模块,不重复发明一套新流程 - 共享逻辑先提公共层,再让步骤层调用 - 代码新增后应尽量减少主文件体积,而不是只做“形式拆分” -- 观测、留档、日志、导出这类横切能力必须优先挂在独立配置域下,不能借某个 provider 的业务模式开关隐式控制 +- 观察、留档、日志、导出这类横切能力必须优先挂在独立配置域下,不能借某个 provider 的业务开关隐式控制 - 保留少量兼容型薄包装是允许的,但必须有明确目的: - 运行时装配 - 测试迁移过渡 - 如果某个薄包装已经没有存在意义,应在后续重构中清掉 +- 涉及中文内容的文件必须保持稳定编码,修改后要主动检查是否出现乱码、错码、异常替换字符。 ## 8. AI 开发时的自检清单 @@ -193,7 +203,8 @@ npm test 4. 我有没有补或迁移测试? 5. 我有没有更新三份根目录文档? 6. 我新增或修改的文件是否有可见乱码? -7. 如果改动影响 Gmail / 2925 别名邮箱逻辑,我有没有同步检查 `managed-alias-utils.js`、sidepanel 接线、background 调度、auto-run reset 和回归测试? +7. 我有没有逐个检查本次改动涉及的中文文案、日志、注释、文档没有乱码? +8. 如果改动影响 Gmail / 2925 别名邮箱逻辑,我有没有同步检查 `managed-alias-utils.js`、sidepanel 接线、background 调度、auto-run reset 和回归测试? ## 9. 完成标准 @@ -204,6 +215,7 @@ npm test - 全量测试通过 - 三份根目录文档已同步 - 没有可见乱码 +- 已对本次修改涉及的文件做过乱码审查 ## 10. 特别要求 @@ -214,4 +226,11 @@ npm test - [项目完整链路说明.md](c:/Users/projectf/Downloads/codex注册扩展/项目完整链路说明.md) - [项目开发规范(AI协作).md](c:/Users/projectf/Downloads/codex注册扩展/项目开发规范(AI协作).md) +- 每次开发结束前,必须审查本次修改文件与关键运行文案没有乱码: + - 文档正文 + - sidepanel 中文文案 + - 日志文案 + - 报错文案 + - 中文注释 + 这是硬要求,不是建议。