Skip to content

[codex] stabilize repository browsing and settings workflows#12

Merged
mars167 merged 9 commits intomainfrom
fix/repository-pull-requests-endpoint
Mar 28, 2026
Merged

[codex] stabilize repository browsing and settings workflows#12
mars167 merged 9 commits intomainfrom
fix/repository-pull-requests-endpoint

Conversation

@mars167
Copy link
Copy Markdown
Owner

@mars167 mars167 commented Mar 28, 2026

Summary

This branch hardens several user-facing stability paths across repository browsing, auth/settings, deployment health checks, and LLM configuration.

  • normalize repository coordinate handling and stabilize repository/platform browsing flows
  • fix auth and settings regressions, including password input behavior and failed-login logging
  • add multi-LLM profile configuration with modal editing and trusted CORS origin support
  • harden deployment/frontend health checks and repository list page caching
  • stabilize frontend checks, seed data, and key e2e paths

Root Cause

The regressions on this branch were caused by a mix of stale repository metadata, unstable client-side state/caching behavior, and outdated assumptions between the UI, backend contracts, and deployment/runtime setup.

More concretely:

  • repository and PR flows were relying on non-canonical repository coordinates
  • the settings experience had unstable input/render behavior and single-profile assumptions
  • repository page caching allowed empty page responses to be reused as if they were valid list data
  • deployment and health checks were not aligned with the actual frontend/backend runtime topology

User Impact

  • repository lists and repository-specific PR actions are more reliable across platforms
  • dashboard/settings interactions are stable, including admin password changes and LLM profile editing
  • LLM API settings now support multiple switchable profiles instead of a single configuration
  • trusted origins can be configured as multiple env values without incorrect CORS rejection
  • repository pages no longer get stuck behind an empty cached list when real repositories are available

Validation

  • npm run check
  • npm run test:e2e
  • npm --prefix server run typecheck
  • npm --prefix server test -- --runInBand src/auth/routes.test.ts src/services/SystemSettingsService.test.ts src/config/index.test.ts src/review/llmClient.test.ts
  • npm --prefix web run lint -- app/dashboard/repositories/page.tsx app/dashboard/settings/page.tsx src/components/ui/Input.tsx app/dashboard/page.tsx
  • npm --prefix web run build
  • browser verification on http://localhost:3000 for dashboard/settings/repositories critical paths

], 'https://app.example.com')).toBe(true);
});

it('supports wildcard origins', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · 通配符 '*' 允许任意来源存在安全风险

测试用例验证了通配符 '' 会允许任意 origin(如 'https://preview.example.com')。在 CORS 上下文中使用 '' 是严重的安全配置错误,会绕过 CORS 保护,使应用暴露于跨站请求伪造(CSRF)攻击风险中。生产环境绝不应使用 '*' 作为 allowed origins。

建议:1) 确认生产代码是否真的会接受 '' 作为配置值,如果是,这是一个需要立即修复的安全漏洞。2) 如果 '' 确实是有意设计的开发环境功能,应添加相应的开发环境限定测试,或添加安全注释说明其使用场景。3) 考虑在实现中添加警告日志,当检测到 '*' 配置时发出安全提示。

}
}

recoverInterruptedJobs(): { recoveredCount: number; recoveredJobIds: number[] } {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · recoverInterruptedJobs 缺少事务保护,存在部分失败风险

方法遍历所有 processing 状态的作业,对每个作业依次调用 jobModel.markPending、analysisModel.markPending、analysisJobModel.markPending。这三个操作之间没有事务包装,如果在执行过程中发生错误(如数据库连接中断),会导致部分状态更新成功、部分失败,造成数据不一致:job 已标记为 pending 但关联的 analysis 或 analysisJob 仍处于 processing 状态。

建议:将整个恢复逻辑包裹在数据库事务中,或者引入补偿机制确保原子性。示例:使用 jobModel 的事务 API,或在操作前记录恢复计划、操作后验证一致性。考虑使用一个批次操作替代逐条循环。

*/
updateStatus(id: number, status: JobStatus): AnalysisJob | null {
const fields: string[] = [];
const fields: string[] = ['status = ?'];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · 修复 updateStatus SQL 构建 bug

原代码 fields: string[] = [] 会导致 SQL 变成 SET status = ?, , updated_at = ...,出现连续逗号使 UPDATE 语句失败。这是一个会导致所有 status 更新操作直接抛出 SQL 语法错误的严重 bug。

建议:此修改已正确修复该问题,将 fields 初始化为 ['status = ?'] 确保 status 被正确包含在 SET 子句中。无需进一步操作。

log_info "检查 API 健康状态..."

local backend_url="http://localhost:${BACKEND_PORT:-7900}/api/health"
local backend_url="http://localhost:${BACKEND_PORT:-7900}/health"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 健康检查端点路径可能与服务端实际端点不匹配

将健康检查端点从 /api/health 改为 /health,但根据 docs/api.md:205 中 API 版本路径为 /api/v1/,且服务端路由 (server/src/auth/routes.ts:8) 使用 /api/auth/verify 前缀模式,如果服务端健康检查实际端点不是 /health,此改动将导致 verify.sh 无法正确检测后端健康状态,使得部署验证失败。

建议:确认服务端实际提供的健康检查端点路径。如果服务端使用统一 /api 前缀,应使用 /api/health;如果使用 /api/v1 版本,应使用 /api/v1/health;如果服务端确实提供无前缀的 /health,则此改动正确。

```bash
# 开发环境
CORS_ORIGINS=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · CORS 配置示例同时包含 localhost 和 127.0.0.1 可能导致认证问题

AGENTS.md:37 指出 'using 127.0.0.1 can create a false negative during login and settings verification'。虽然 localhost 和 127.0.0.1 指向同一台机器,但浏览器可能将它们视为不同的 origin(域名 origin 的判断依赖字符串比较),这可能导致 cookie/session 无法正确共享,造成认证失败。文档明确建议两者都包含,但未警告潜在风险。

建议:在开发环境配置示例后添加说明,或改为只推荐使用 localhost。例如:'注意:如果登录或设置验证遇到问题,尝试只使用 localhost 而不包含 127.0.0.1'。

await page.goto('/dashboard/jobs');
await expect(page.getByRole('heading', { name: '作业状态' })).toBeVisible();
await expect(page.getByText('analyze_pr')).toBeVisible();
await expect(page.getByRole('link', { name: `Job #9301 · ${seededRepositoryName} · PR #42` })).toBeVisible();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 硬编码的 Job ID 和 PR 编号与 seed 数据耦合

测试用例在 L37 硬编码了 Job #9301PR #42,这些值由 seed-e2e-data.js 脚本生成。如果 seed 脚本修改了这些 ID(时间戳增量等),测试将立即失败,且需要同步修改测试代码。这增加了维护负担和脆弱性。

建议:考虑从 seed 脚本导出这些动态值(如通过 JSON 文件或环境变量),或使用更宽松的选择器(如 getByText('codagraph-lite-e2e-fixture').first()),避免对具体编号的强依赖。


const dbPath = path.join(__dirname, '..', 'server', 'data', 'codagraph-lite.db');
const db = new Database(dbPath);
const seededTimestamp = "datetime('now', 'localtime', '+1 day')";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 未来时间戳可能导致测试断言不稳定

seededTimestamp 使用 datetime('now', 'localtime', '+1 day') 设置为未来1天的时间。如果E2E测试中有任何依赖当前时间的断言(如检查token是否过期、判断记录是否'最近'创建等),这些测试可能因为时间差而失败。特别是 oauth_installations.token_expires_at 字段虽然当前未填充值,但若应用代码未来依赖该字段与当前时间做比较,测试数据将无法覆盖这些场景。

建议:考虑使用 datetime('now', 'localtime') 获取当前时间戳,仅在明确需要测试'过期'或'未来'场景时才使用固定偏移。如果必须使用未来时间,建议在脚本顶部添加注释说明原因,并确保相关测试明确处理时间边界情况。

.post('/login')
.send({
username: 'admin',
password: 'wrong-password',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 疑似引入敏感信息

变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。

建议:将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。

@codegraphmars
Copy link
Copy Markdown

codegraphmars bot commented Mar 28, 2026

CodaGraph Review 摘要

有 2 个文件因为缺少 patch 或类型不受支持而被跳过,当前结论应按 partial review 理解。 模式:LLM + 规则;风险等级:critical;置信度:high。 共审查 28/30 个文件,发现 33 个高价值问题(critical 3 / high 19 / medium 5 / low 6)。 另外有 2 个文件未被完整审查,当前结果应按 partial review 理解。

摘要中的补充问题

  • 缺少边界条件测试 (server/src/config/index.test.ts:1)

    • 严重性: high
    • 描述: 测试仅覆盖了 happy path,缺少关键边界情况的验证:空数组输入(allowedOrigins=[])、单元素数组、非法 URL 格式、空字符串 origin、null/undefined 处理等。这些边界情况如果处理不当可能导致运行时错误或安全漏洞。
    • 建议: 添加以下测试用例:1) 空数组 [] 应拒绝所有 origin 返回 false。2) 带协议前缀不匹配的 origin(如允许 'https://' 但请求 'http://')。3) 子域名攻击场景(允许 'https://example.com' 但请求 'https://evil.example.com')。4) 包含特殊字符的恶意 origin。
  • CORS 检查逻辑变更需确认实现安全性 (server/src/index.ts:112)

    • 严重性: high
    • 描述: 将 config.cors.corsOrigins.includes(origin) 替换为 isCorsOriginAllowed() 函数调用。如果新函数包含正则表达式或复杂匹配逻辑,可能存在正则表达式拒绝服务(ReDoS)风险或边界情况绕过。diff 无法展示该函数的实现细节。
    • 建议: 确认 isCorsOriginAllowed 函数实现满足:(1) 无 ReDoS 风险的正则表达式 (2) 正确处理通配符和子域名匹配 (3) 包含对 origin 参数的输入校验。审查该函数的单元测试覆盖情况。
  • 测试覆盖不足:未验证恢复中断任务的实际行为 (server/src/jobs/CodeReviewWorker.test.ts:32)

    • 严重性: high
    • 描述: 测试仅验证了 recoverInterruptedJobs 被调用一次,但未验证其返回值(recoveredCount、recoveredJobIds)如何影响后续逻辑。如果 start() 方法根据恢复的 job 数量执行额外操作(如重新入队、通知等),这些关键路径未被测试覆盖。
    • 建议: 添加测试用例验证当 recoverInterruptedJobs 返回非空 recoveredJobIds 时,CodeReviewWorker.start() 是否正确处理这些恢复的任务,例如验证 executegetNextJob 的调用情况。
  • 启动时作业恢复失败会导致状态不一致 (server/src/jobs/CodeReviewWorker.ts:53)

    • 严重性: high
    • 描述: 在 start() 方法中,recoverInterruptedJobs() 调用在设置 this.isRunning = true 之前。如果恢复过程中抛出异常,Worker 的 isRunning 状态仍为 false,但启动流程实际上失败了,可能导致调用方误以为 Worker 已启动成功。
    • 建议: 将 recoverInterruptedJobs() 调用包裹在 try-catch 中,或将 this.isRunning = true 移到恢复逻辑之前,确保状态转换的原子性。例如:this.isRunning = true; try { ... } catch { this.isRunning = false; throw; }
  • 缺少错误处理和部分失败场景的测试覆盖 (server/src/jobs/QueueService.test.ts:60)

    • 严重性: high
    • 描述: 测试只覆盖了 happy path(单个 job 全部恢复成功)。如果 markPendingjobLogModel.create 在部分 job 上抛出异常,recoverInterruptedJobs 的错误处理逻辑未被验证,可能导致服务重启恢复时静默失败。
    • 建议: 添加测试用例:1) mock markPending 抛出异常,验证错误是否被正确传播或记录;2) 测试多个 jobs 时部分失败的行为;3) 验证 result 返回值在异常情况下的状态。
  • 未测试 payload 解析的边界情况 (server/src/jobs/QueueService.test.ts:68)

    • 严重性: high
    • 描述: 测试假设 payload 始终是有效的 JSON 字符串。如果 findProcessing 返回的 job 对象缺少 payload 字段、或 payload 为 null/undefined/无效 JSON,JSON.parse 将抛出异常,但当前测试未覆盖此类场景。
    • 建议: 添加边界测试用例:1) payload 为 null;2) payload 为空字符串;3) payload 为无效 JSON。验证 QueueService 是否能正确处理这些异常情况。
  • parsePayload 解析失败时静默返回空对象,可能导致关联记录恢复被跳过 (server/src/jobs/QueueService.ts:89)

    • 严重性: high
    • 描述: 当 JSON.parse 失败时,方法静默返回 {}。这会导致 payload.analysis_id 和 payload.analysis_job_id 都是 undefined,后续的 NaN 检查会跳过 analysisModel.markPending 和 analysisJobModel.markPending 的调用。如果 payload 本身因数据损坏无法解析,但 job 和 analysis 记录确实存在关联,则关联记录的恢复会被静默忽略。
    • 建议: 在解析失败时记录错误日志并返回包含原始 rawPayload 的对象,或在调用处增加对解析失败的检测,如果解析失败则记录警告日志并跳过该作业的恢复(而不是静默继续)。
  • recoverInterruptedJobs 中单个作业处理失败会导致整批恢复中断 (server/src/jobs/QueueService.ts:114)

    • 严重性: high
    • 描述: for...of 循环中对每个 job 的处理没有任何 try-catch 保护。如果某个 job 的恢复过程中出现异常(例如 analysisModel.markPending 抛出),整个循环会中断,导致剩余的 processing jobs 无法被恢复。这在大量作业积压时尤其危险。
    • 建议: 在 for 循环内增加 try-catch,为每个 job 的恢复操作添加独立的错误处理,单个作业失败不影响其他作业的恢复,并记录失败详情。

建议动作

    1. 确认生产代码是否真的会接受 '' 作为配置值,如果是,这是一个需要立即修复的安全漏洞。2) 如果 '' 确实是有意设计的开发环境功能,应添加相应的开发环境限定测试,或添加安全注释说明其使用场景。3) 考虑在实现中添加警告日志,当检测到 '*' 配置时发出安全提示。
  • 将整个恢复逻辑包裹在数据库事务中,或者引入补偿机制确保原子性。示例:使用 jobModel 的事务 API,或在操作前记录恢复计划、操作后验证一致性。考虑使用一个批次操作替代逐条循环。

  • 此修改已正确修复该问题,将 fields 初始化为 ['status = ?'] 确保 status 被正确包含在 SET 子句中。无需进一步操作。

  • 确认服务端实际提供的健康检查端点路径。如果服务端使用统一 /api 前缀,应使用 /api/health;如果使用 /api/v1 版本,应使用 /api/v1/health;如果服务端确实提供无前缀的 /health,则此改动正确。

@mars167 mars167 force-pushed the fix/repository-pull-requests-endpoint branch from 19adc5d to 949c0f8 Compare March 28, 2026 18:01
describe('CORS origin matching', () => {
it('allows multiple trusted origins from env-style lists', () => {
expect(isCorsOriginAllowed([
'http://localhost:3000',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · 测试用例覆盖不足 - 未验证拒绝不匹配的 origin

测试套件缺少验证不匹配 origin 应返回 false 的用例。如果 isCorsOriginAllowed(['https://app.example.com'], 'https://evil.com') 返回 true 或抛异常,将导致严重的安全漏洞——任何恶意网站都可以绕过 CORS 保护。

建议:添加测试用例:expect(isCorsOriginAllowed(['https://app.example.com'], 'https://evil.com')).toBe(false)

package.json Outdated
"build:web": "npm --prefix web run build",
"build:server": "npm --prefix server run build",
"build": "npm run build:web && npm run build:server",
"check:web": "npm --prefix web run lint -- app src next.config.ts",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · check:web 脚本依赖未验证的子项目配置

check:web 调用 web 目录下的 lint 脚本,但未确认 web/package.json 中是否已定义 lint 命令。如果 web 项目未配置 ESLint 或对应的 lint 脚本,此命令将失败,导致 CI/CD 流程中断。

建议:在添加此脚本前,确保 web/package.json 中已正确定义 'lint' 脚本,或考虑在 check 脚本中添加容错逻辑(如使用 || true 避免阻断,或明确记录前置条件)。

"build:server": "npm --prefix server run build",
"build": "npm run build:web && npm run build:server",
"check:web": "npm --prefix web run lint -- app src next.config.ts",
"check:server": "npm --prefix server run typecheck",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · check:server 脚本依赖未验证的子项目配置

check:server 调用 server 目录下的 typecheck 命令,但未确认 server/package.json 中是否存在 typecheck 脚本。如果 server 项目未配置 TypeScript 类型检查,此命令将失败。

建议:在添加此脚本前,确保 server/package.json 中已正确定义 'typecheck' 脚本(如 tsc --noEmit)。

updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT OR REPLACE INTO oauth_installations (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 新增 oauth_installations 表的 INSERT 缺少幂等性保护

repository、analysis、jobs 表都使用了 INSERT OR REPLACE 保证幂等性,但新增的 oauth_installations 表(第11-25行创建)也使用 INSERT OR REPLACE,却缺少表可能已存在数据的保护逻辑。如果该表已存在且包含其他记录,INSERT OR REPLACE 可能导致意外的数据丢失。

建议:确认 oauth_installations 表的预期状态,或考虑使用 DELETE 配合条件后再 INSERT,确保与测试预期一致。

.post('/login')
.send({
username: 'admin',
password: 'wrong-password',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 疑似引入敏感信息

变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。

建议:将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。


it('returns 401 for unknown users without trying to write an invalid activity log row', async () => {
adminModelMock.findByUsername.mockReturnValue(null);
adminModelMock.verifyPassword.mockReturnValue(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 测试中 mock 了不会被调用的方法

第二个测试用例在 L77 设置 findByUsername.mockReturnValue(null) 返回 null 用户后,L78 又调用了 verifyPassword.mockReturnValue(false)。由于用户不存在,routes.ts 中的实现路径应该是先查用户再验证密码,所以 verifyPassword 在该场景下根本不会被调用。这个 mock 设置是无效的,不会影响测试结果,但会误导后续维护者认为该 mock 行为被验证过。

建议:移除 L78 的 verifyPassword.mockReturnValue(false) 调用,或在该行前添加注释说明该 mock 是为了与其他测试保持一致。如果 verifyPassword 确实会在 null 用户场景下被调用,则需要检查 routes.ts 的实现逻辑是否符合预期。

@@ -0,0 +1,20 @@
import { isCorsOriginAllowed } from './index';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 通配符 '*' 的安全风险未被说明

测试验证了通配符 origin 被允许,但这在生产环境中是极度危险的行为。* 会允许任何第三方网站发起跨域请求。如果这不是预期行为,应在配置层面禁止通配符 origin。

建议:确认业务需求:如果生产环境必须支持通配符,添加注释说明原因;如果不需要,应在配置验证阶段拒绝通配符 origin。

@codegraphmars
Copy link
Copy Markdown

codegraphmars bot commented Mar 28, 2026

CodaGraph Review 摘要

有 2 个文件因为缺少 patch 或类型不受支持而被跳过,当前结论应按 partial review 理解。 模式:LLM + 规则;风险等级:critical;置信度:high。 共审查 28/30 个文件,发现 37 个高价值问题(critical 1 / high 21 / medium 7 / low 8)。 另外有 2 个文件未被完整审查,当前结果应按 partial review 理解。

摘要中的补充问题

  • 缺少边界条件测试 (server/src/config/index.test.ts:6)

    • 严重性: high
    • 描述: 测试未覆盖以下边界情况:空数组 []、null/undefined 列表、包含空字符串的列表。这些输入可能导致函数抛出异常或产生未定义行为。
    • 建议: 添加测试:expect(isCorsOriginAllowed([], 'https://app.example.com')).toBe(false)expect(() => isCorsOriginAllowed(null, '...')).toThrow()
  • 状态转换顺序风险:recoverInterruptedJobs() 异常可能污染启动状态 (server/src/jobs/CodeReviewWorker.ts:53)

    • 严重性: high
    • 描述: recoverInterruptedJobs() 调用发生在 this.isRunning = true 之前(L57)。如果恢复逻辑抛出异常,start() 方法会在 isRunning 仍为 false 时退出。但 processingLoop() 是异步的,此时仍可能被触发。
    • 建议: 考虑将 this.isRunning = true 移到 recoverInterruptedJobs() 之前,或将恢复逻辑包装在 try-catch 中并在 catch 中设置 isRunning = false 且返回。
  • 缺少对 recoverInterruptedJobs() 返回值的防御性检查 (server/src/jobs/CodeReviewWorker.ts:54)

    • 严重性: high
    • 描述: 直接访问 recovery.recoveredCount,但未检查 recovery 是否为 null 或 undefined。如果 queueService.recoverInterruptedJobs() 异常或返回非对象值,将在此处抛出 TypeError,而非在 L53 处暴露原始错误。
    • 建议: 在 L53 后添加:if (recovery && typeof recovery.recoveredCount === 'number') { ... },或使用可选链:if (recovery?.recoveredCount > 0) { ... }
  • 测试覆盖不足:缺少边界场景 (server/src/jobs/QueueService.test.ts:60)

    • 严重性: high
    • 描述: 当前测试仅覆盖单个 job 恢复成功的场景,未测试 findProcessing 返回空数组(无中断作业)和返回多个 jobs 的批量恢复场景。批量恢复时,如果某个 job 恢复失败,后续 job 是否还会继续处理?缺少对这一逻辑的验证。
    • 建议: 添加两个测试用例:1) findProcessing 返回 [] 时的断言(recoveredCount 应为 0);2) findProcessing 返回多个 jobs 时,验证每个 job 的 markPending 和 create 都按预期被调用。
  • 批量恢复操作缺乏原子性保证 (server/src/jobs/QueueService.ts:114)

    • 严重性: high
    • 描述: recoverInterruptedJobs 在 for 循环中依次调用 jobModel.markPending()、analysisModel.markPending()、analysisJobModel.markPending()。如果中途任意一次调用失败或数据库写入部分成功,会导致状态不一致:Job 已标记为 pending,但其关联的 Analysis/AnalysisJob 可能仍处于 processing 状态或已被清理。这在服务重启恢复场景下尤其危险。
    • 建议: 考虑引入事务机制或重构为批量操作,确保要么全部恢复成功,要么全部失败。或者在恢复后增加校验逻辑,验证 Job 与关联记录的最终状态是否一致。
  • 恢复分析记录前缺少存在性校验 (server/src/jobs/QueueService.ts:116)

    • 严重性: high
    • 描述: 在 recoverInterruptedJobs 中,代码直接调用 this.analysisModel.markPending(analysisId),但没有验证该 analysis 记录是否仍然存在于数据库中。如果 analysis 记录已被手动删除或归档,会导致数据不一致——Job 处于 pending 状态,但关联的 Analysis 记录已不存在或处于其他状态。
    • 建议: 在调用 markPending 前,先检查 this.analysisModel.findById(analysisId) 是否返回有效记录。同样对 analysisJobId 执行相同校验。如果记录不存在,跳过该操作并记录警告日志而非静默继续。
  • adminId 缺少有效性校验 (server/src/models/ActivityLog.ts:115)

    • 严重性: high
    • 描述: logFailedLogin 现在要求传入 adminId,但调用方传入的 admin.id 可能在认证失败场景下为 undefined、null 或非法值。如果传入无效的 adminId,数据库外键约束可能失败,或日志记录本身失败影响安全审计。
    • 建议: 在 logFailedLogin 或 create 方法中添加 adminId 的有效性校验:if (!Number.isInteger(adminId) || adminId <= 0) { throw new Error('无效的管理员ID'); } 或者明确允许 0/null 表示未知用户的情况。
  • username 直接拼接到日志详情存在风险 (server/src/models/ActivityLog.ts:116)

    • 严重性: high
    • 描述: 尝试登录的用户名被直接嵌入 details 字段。如果日志在 UI 中展示且未经转义,可能导致 XSS 问题。此外,恶意构造的长 username 可能用于日志注入或耗尽存储。
    • 建议: 在 details 中对 username 进行长度限制(如最多 255 字符)并转义特殊字符,或在日志展示层进行严格的内容安全策略。

建议动作

  • 添加测试用例:expect(isCorsOriginAllowed(['https://app.example.com'], 'https://evil.com')).toBe(false)

  • 考虑让 seed 脚本将生成的 Job ID/PR 写入环境变量或临时文件,测试读取后进行断言;或者放宽断言条件,仅验证存在包含 seededRepositoryNamePR #42 的链接,而不依赖固定序号。

  • 在添加此脚本前,确保 web/package.json 中已正确定义 'lint' 脚本,或考虑在 check 脚本中添加容错逻辑(如使用 || true 避免阻断,或明确记录前置条件)。

  • 在添加此脚本前,确保 server/package.json 中已正确定义 'typecheck' 脚本(如 tsc --noEmit)。


# 检查后端
if curl -sf http://localhost:7900/api/health >/dev/null 2>&1; then
if curl -sf "http://localhost:${BACKEND_PORT}/health" >/dev/null 2>&1; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · 后端健康检查路径与实际 API 不匹配

后端健康检查端点从 /api/health 改为了 /health。这会导致健康检查永远失败,除非后端 server 确实暴露 /health 端点而非 /api/health。语义上下文显示后端启动命令为 BACKEND_PORT=7901 npm run start:server,需确认后端路由是否支持 /health 路径。

建议:确认 server/dist 中的路由实现。如果后端健康检查端点仍是 /api/health,应将 L134 改为:if curl -sf "http://localhost:${BACKEND_PORT}/api/health" >/dev/null 2>&1; then


### Failed Login Returning 500

- Symptom: wrong admin password returns `500 {"details":"FOREIGN KEY constraint failed"}` instead of `401`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 文档中暴露了内部错误信息格式,可能被用于探测系统

错误消息 {"details":"FOREIGN KEY constraint failed"} 暴露了数据库约束细节和内部表关系。攻击者可利用此信息:(1) 确认用户账号是否存在(返回500而非401);(2) 推断数据库表结构。虽然这是对现有 bug 的文档化记录,但文档本身无需包含完整的 JSON 响应体示例。

建议:将 L24 改为:'Symptom: wrong admin password returns 500 with internal database error details instead of 401.' 移除具体的 JSON 响应体示例,避免在文档中固化敏感的错误响应格式。

log_info "检查 API 健康状态..."

local backend_url="http://localhost:${BACKEND_PORT:-7900}/api/health"
local backend_url="http://localhost:${BACKEND_PORT:-7900}/health"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 健康检查端点路径变更需确认服务器端实现

健康检查路径从 /api/health 改为 /health。根据语义上下文,server/src/auth/routes.ts 定义了 /api/auth/verify 路由,但未找到 /api/health/health 端点的服务器端定义。如果服务器未实现 /health 端点,部署验证将持续失败。

建议:确认服务器端健康检查端点的实际路径。如果服务器使用 Express,通常在入口文件中有 app.get('/health', ...)app.get('/api/health', ...) 的路由定义。建议在修改前验证目标路径在服务器端确实存在。

await page.goto('/dashboard/jobs');
await expect(page.getByRole('heading', { name: '作业状态' })).toBeVisible();
await expect(page.getByText('analyze_pr')).toBeVisible();
await expect(page.getByRole('link', { name: `Job #9301 · ${seededRepositoryName} · PR #42` })).toBeVisible();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 硬编码 Job ID 导致测试脆弱

测试断言中硬编码了 Job #9301,这是 e2e 测试数据的自增 ID。如果 seed 脚本重新运行或数据被清理后重新生成,Job ID 可能不是 9301,导致测试失败。e2e 测试的 fixture 数据通常不稳定,硬编码具体 ID 是常见失败原因。

建议:考虑两种方案:(1) 在 seed 脚本返回创建的数据信息,测试中动态获取 job ID;(2) 如果该 job 始终是最新创建的作业,使用模糊匹配如 getByRole('link', { name: new RegExp('Job #.*· .*e2e-fixture') });或使用 last() 等定位器策略而非固定 ID。


await page.goto('/dashboard/history');
await expect(page.getByRole('heading', { name: '分析历史' })).toBeVisible();
await expect(page.getByRole('heading', { name: '按 PR 维度回看 Review 历史' })).toBeVisible();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 页面标题变更需确认 UI 同步更新

history 页面的标题从 '分析历史' 改为 '按 PR 维度回看 Review 历史'。这是一个用户可见的文本变更,如果后端代码已更新但前端页面尚未同步此文本,测试将失败。需确认这是预期的前端 UI 变更。

建议:确认 scripts/seed-e2e-data.js 或相关前端组件是否同步更新了标题文本,确保 PR 变更与 UI 代码变更一并部署。

@codegraphmars
Copy link
Copy Markdown

codegraphmars bot commented Mar 28, 2026

CodaGraph Review 摘要

有 2 个文件因为缺少 patch 或类型不受支持而被跳过,当前结论应按 partial review 理解。 模式:LLM + 规则;风险等级:critical;置信度:high。 共审查 28/30 个文件,发现 48 个高价值问题(critical 4 / high 29 / medium 6 / low 9)。 另外有 2 个文件未被完整审查,当前结果应按 partial review 理解。

摘要中的补充问题

  • 数据库迁移缺少输入校验 (deploy/start.sh:88)

    • 严重性: high
    • 描述: RUN_MIGRATIONS 变量直接决定是否执行数据库迁移脚本 node server/dist/database/migrate.js,但未校验变量值的有效性。任意非空值都会触发迁移,存在误操作风险。
    • 建议: 改为检查具体值,如:if [ "$RUN_MIGRATIONS" = "true" ] || [ "$RUN_MIGRATIONS" = "1" ]; then
  • 健康检查端点未验证存在 (playwright.config.ts:13)

    • 严重性: high
    • 描述: webServer 配置使用 /api/health 作为后端服务就绪检查,但该端点是否真实存在于后端服务中未经验证。如果后端没有实现该端点,Playwright 将持续等待超时,导致测试启动失败。
    • 建议: 确认后端服务已实现 GET /api/health 端点,或使用后端实际提供的健康检查路径替换。
  • access_token 明文硬编码 (scripts/seed-e2e-data.js:85)

    • 严重性: high
    • 描述: L85 硬编码了 access_token 为 'e2e-token',虽然在 E2E 测试脚本中可接受,但此类凭证容易被意外提交到代码仓库或用于生产环境。
    • 建议: 建议使用环境变量注入,如 process.env.E2E_ACCESS_TOKEN || 'e2e-token',避免凭证污染源码。
  • 冗余的 mock 配置 (server/src/auth/routes.test.ts:55)

    • 严重性: high
    • 描述: 在第二个测试中,findByUsername 返回 null,这意味着实际代码路径不会走到 verifyPassword 调用(因为用户不存在)。此时对 verifyPassword 设置 mockReturnValue(false) 没有实际作用,但会误导后续阅读者认为该方法被调用了。更严重的是,如果实际实现代码在用户不存在时仍错误地调用了 verifyPassword,这个测试无法发现该 bug。
    • 建议: 移除第二个测试中第 78 行的 adminModelMock.verifyPassword.mockReturnValue(false);。若需要更强的保护,可以添加 expect(adminModelMock.verifyPassword).not.toHaveBeenCalled(); 来确保该方法在用户不存在时确实不被调用。
  • 疑似引入敏感信息 (server/src/auth/routes.test.ts:61)

    • 严重性: high
    • 描述: 变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。
    • 建议: 将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。
  • 通配符 '*' 允许任意 origin,缺少安全场景测试 (server/src/config/index.test.ts:18)

    • 严重性: high
    • 描述: 测试用例验证通配符允许任意 origin,但这实际上是一个安全风险场景。如果生产配置中意外设置 ALLOWED_ORIGINS=*,所有来源的跨域请求都会被接受。当前测试只验证了这个行为存在,没有验证在正常配置下应该如何拒绝未授权 origin。
    • 建议: 添加负面测试用例:expect(isCorsOriginAllowed(['https://app.example.com'], 'https://evil.example.com')).toBe(false),确保函数正确拒绝未授权 origin。
  • CORS 校验逻辑变更可能引入安全回归 (server/src/index.ts:112)

    • 严重性: high
    • 描述: CORS 校验从简单的 config.cors.corsOrigins.includes(origin) 改为调用 isCorsOriginAllowed(config.cors.corsOrigins, origin)。这是一个安全边界变更,需要确认新函数的精确行为:如果新函数实现了更宽松的匹配(如通配符、动态来源解析等),可能导致恶意跨域请求绕过 CORS 限制;如果实现了更严格的检查,则可能误杀合法请求。
    • 建议: 1. 确认 isCorsOriginAllowed 的实现逻辑,特别是它与 includes 的差异点。2. 如果涉及正则匹配或动态解析,添加白名单用例测试。3. 建议在安全测试环境中验证 CORS 边界情况。
  • ReviewExecutionServiceMock 未被使用 (server/src/jobs/CodeReviewWorker.test.ts:11)

    • 严重性: high
    • 描述: reviewExecutionServiceMock 被创建和 mock,但在测试中没有调用任何方法来验证其行为。如果 CodeReviewWorker.start() 应该在 recovery 后调用 ReviewExecutionService.execute(),当前测试无法发现这个调用链断裂。
    • 建议: 添加测试用例验证 recoveredJobIds 被正确传递给 ReviewExecutionService.execute(),或者在注释中说明为什么不需要验证这个调用链。

建议动作

  • 确认 server/dist 中的路由实现。如果后端健康检查端点仍是 /api/health,应将 L134 改为:if curl -sf "http://localhost:${BACKEND_PORT}/api/health" >/dev/null 2>&1; then

  • 添加 affectedRows 检查:const result = this.db.execute(...); if (result.affectedRows === 0) return null; 在 UPDATE 之前可以先 findById(id) 确认记录存在,或直接检查 affectedRows 并抛出/返回明确的错误。

  • 恢复原逻辑,仅使用 platform+owner+name 查找。如果需要支持通过 full_name 查找,应作为单独的同步策略方法,不应混入 upsert 的唯一性判断。

  • 将文件重命名为GiteeClient.test.ts并移动到正确的位置(如有独立目录则移动)。

Copy link
Copy Markdown

@codegraphmars codegraphmars bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodaGraph Review 摘要

  • PR: #12 [codex] stabilize repository browsing and settings workflows
  • 风险等级: critical
  • 运行模式: improve
  • 置信度: high
  • 覆盖情况: 28/30 文件
  • 跳过文件: 2
  • 报告 ID: 9266
  • 已发布行级评论: 8

有 2 个文件因为缺少 patch 或类型不受支持而被跳过,当前结论应按 partial review 理解。 模式:LLM + 规则;风险等级:critical;置信度:high。 共审查 28/30 个文件,发现 37 个高价值问题(critical 1 / high 20 / medium 11 / low 5)。 另外有 2 个文件未被完整审查,当前结果应按 partial review 理解。

摘要中的补充问题

  • 密码验证逻辑顺序变更可能引入时序攻击风险 (server/src/auth/routes.ts:52)

    • 严重性: high
    • 描述: 原逻辑先调用 verifyPassword 再检查用户是否存在,新逻辑先 findByUsername 再 verifyPassword。攻击者可能通过测量响应时间差异(用户存在但密码错误 vs 用户不存在)推断用户名是否有效。虽然当前实现中这种差异可能不明显,但这是认证系统中的已知安全反模式。
    • 建议: 考虑在 verifyPassword 内部处理用户不存在的情况,使其返回一致的响应时间。或者在 findByUsername 后添加人工延迟以平衡响应时间差异。参考 timing-safe 比较的实现模式。
  • 用户存在性检查变为死代码 (server/src/auth/routes.ts:64)

    • 严重性: high
    • 描述: diff 显示 if (!admin) 检查在 L64-L67 被删除,因为新代码在 L52 已经获取了 admin 对象。这个检查现在永远不会触发,因为如果 admin 不存在,isValid 在 L53 已经是 false,会在 L55 的 if (!isValid) 块中提前返回。这是一个逻辑回归。
    • 建议: 删除 L64-L67 的死代码块(if (!admin) return 401 '用户不存在')。如果需要保留用户不存在时的特殊处理逻辑(如记录到单独的错误日志),需要重新设计这个分支。
  • 缺少 CORS 拒绝场景测试 (server/src/config/index.test.ts:1)

    • 严重性: high
    • 描述: 测试仅覆盖 origin 被允许的正面场景,未验证不可信 origin 应被拒绝的行为。若 isCorsOriginAllowed 错误地始终返回 true,所有现有测试仍会通过,无法发现安全回归。
    • 建议: 添加测试用例验证:1) 不在允许列表中的 origin 应返回 false;2) 来自不同域的 origin 应被拒绝;3) 空数组输入应返回 false。
  • 通配符 '*' 测试存在安全语义歧义 (server/src/config/index.test.ts:17)

    • 严重性: high
    • 描述: 测试 isCorsOriginAllowed(['*'], 'https://preview.example.com') 通过,但通配符 origin 在 CORS 中允许任意来源是危险行为,需确认这是有意的测试设计而非误用。
    • 建议: 若通配符是合法配置场景,明确在测试名称中说明;若为误用,应测试更严格的域名匹配逻辑。
  • 失败登录场景中 adminId 语义矛盾 (server/src/models/ActivityLog.ts:115)

    • 严重性: high
    • 描述: logFailedLogin 用于记录未认证用户的登录失败,此时用户身份未确认,理论上不存在有效的 admin_id。原逻辑使用 0 占位,新逻辑改为传入 admin.id,这暗示调用方在认证失败时仍能获取 admin 对象。如果 admin.id 来自不可信输入(如从请求解析),可能导致数据不一致或日志被滥用记录。
    • 建议: 确认 auth/routes.ts 中 admin.id 的来源:若来自已存在的 session 或数据库查询则安全;若是从请求参数直接解析则需重新审视业务逻辑。建议在此处添加断言或校验 adminId > 0,或考虑在用户身份未确认时使用 NULL 而非默认值 0。
  • 测试断言与实现耦合度过高,脆弱性强 (server/src/platform/client.test.ts:189)

    • 严重性: high
    • 描述: 两个 GitHubApiClient 测试用例使用了完全相同的参数组合 (owner='mars167', repo='CodaGraph-lite', pr=12) 和相同的 mock fetch 行为。如果未来需要测试其他 owner/repo 组合的错误处理,或者不同 API 端点 (如 issues, commits) 的错误格式化,需要大量重复代码。
    • 建议: 考虑将通用错误格式化测试提取为辅助函数,或使用 describe.each/describe.each 来参数化测试不同场景,减少重复并提高测试覆盖率的可扩展性。
  • response.text() 消费响应流导致后续无法读取 (server/src/platform/client.ts:186)

    • 严重性: high
    • 描述: 在 describeErrorResponse 中调用 response.text() 会消费响应体。如果调用方需要根据错误响应 body 判断错误类型(如 409 Conflict、422 Validation Error)并决定重试或恢复策略,响应流已被消费,无法再次读取。这限制了错误处理和重试逻辑的灵活性。
    • 建议: 使用 response.clone() 先克隆响应对象:
const clone = response.clone();
const rawBody = await clone.text();

或者只读取前 N 个字符(现代浏览器支持 ReadableStream),避免消费整个响应体。

  • 疑似引入敏感信息 (server/src/services/SystemSettingsService.test.ts:64)
    • 严重性: high
    • 描述: 变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。
    • 建议: 将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。

建议动作

  • 确认后端服务是否已实现 /health 端点。如果后端仍在使用 /api/health(或其他路径),请将 L134 改回 curl -sf "http://localhost:${BACKEND_PORT}/api/health" 或确保前后端健康检查端点保持一致。也可考虑同时检查两个端点以实现平滑迁移。

  • 确认后端健康检查端点的实际路径。如果后端同时支持两路径则无问题;如果只支持 /api/health,需回退此变更或同时更新后端路由配置。建议在变更前验证后端实际暴露的端点。

  • 考虑从 seed-e2e-data.js 导出生成的 Job ID 和 PR 编号,或在测试中通过查询接口动态获取最新创建的 Job/PR,然后在断言中使用这些动态值,而非硬编码数字。

  • 建议在 web 项目的 package.json 中预先配置 lint 的默认路径参数(如 "lint": "next lint app src next.config.ts"),然后 check:web 脚本简化为 "npm --prefix web run lint"。这样更可靠且易于维护。


# 检查后端
if curl -sf http://localhost:7900/api/health >/dev/null 2>&1; then
if curl -sf "http://localhost:${BACKEND_PORT}/health" >/dev/null 2>&1; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · 后端健康检查端点被静默修改,可能导致部署失败

diff 显示后端健康检查端点从 /api/health 变更为 /health。这是一个破坏性变更——如果后端服务没有同步实现 /health 端点,健康检查将持续失败,导致部署超时退出。当前 diff 中没有看到后端代码同步修改的证据。

建议:确认后端服务是否已实现 /health 端点。如果后端仍在使用 /api/health(或其他路径),请将 L134 改回 curl -sf "http://localhost:${BACKEND_PORT}/api/health" 或确保前后端健康检查端点保持一致。也可考虑同时检查两个端点以实现平滑迁移。

log_info "检查 API 健康状态..."

local backend_url="http://localhost:${BACKEND_PORT:-7900}/api/health"
local backend_url="http://localhost:${BACKEND_PORT:-7900}/health"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · API 端点路径变更需与后端配置对齐

健康检查端点从 /api/health 变更为 /health。从 Impact references 可见,系统存在多个 API 端点(如 /api/v1/auth/verify/api/auth/verify),且使用 /api/ 前缀。如果后端健康检查实际仍为 /api/health,此变更将导致健康检查永远失败,进而导致部署验证流程中断。

建议:确认后端健康检查端点的实际路径。如果后端同时支持两路径则无问题;如果只支持 /api/health,需回退此变更或同时更新后端路由配置。建议在变更前验证后端实际暴露的端点。

await page.goto('/dashboard/jobs');
await expect(page.getByRole('heading', { name: '作业状态' })).toBeVisible();
await expect(page.getByText('analyze_pr')).toBeVisible();
await expect(page.getByRole('link', { name: `Job #9301 · ${seededRepositoryName} · PR #42` })).toBeVisible();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 硬编码的 Job 和 PR 标识符与 seed 数据耦合

L37 中 Job #9301PR #42 是魔法数字,与 seed-e2e-data.js 脚本的输出紧密耦合。当 seed 脚本的 ID 生成逻辑发生变化(如数据库重置、时间戳变化等)时,这些测试会隐性失败,而非产生明确的错误。

建议:考虑从 seed-e2e-data.js 导出生成的 Job ID 和 PR 编号,或在测试中通过查询接口动态获取最新创建的 Job/PR,然后在断言中使用这些动态值,而非硬编码数字。

package.json Outdated
"build:web": "npm --prefix web run build",
"build:server": "npm --prefix server run build",
"build": "npm run build:web && npm run build:server",
"check:web": "npm --prefix web run lint -- app src next.config.ts",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · check:web 脚本的路径参数传递可能存在兼容性问题

使用 -- 将额外参数传递给子项目脚本的写法,在某些 npm 版本或 CI 环境中可能无法正确将 'app src next.config.ts' 作为 lint 的路径参数传递。

建议:建议在 web 项目的 package.json 中预先配置 lint 的默认路径参数(如 "lint": "next lint app src next.config.ts"),然后 check:web 脚本简化为 "npm --prefix web run lint"。这样更可靠且易于维护。

timeout: 30_000,
},
{
command: 'cd web && mkdir -p .next/standalone/.next && ln -sfn ../../static .next/standalone/.next/static && PORT=3001 HOSTNAME=127.0.0.1 NEXT_PUBLIC_API_URL=http://127.0.0.1:7901 node .next/standalone/server.js',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 静态资源符号链接路径缺少存在性校验

ln -sfn ../../static .next/standalone/.next/static 假设 ../../static 目录存在。如果该目录不存在,命令会失败(创建指向不存在目标的死链接),导致前端启动异常。

建议:在 ln 命令前添加存在性检查:test -d ../../static && ln -sfn ../../static .next/standalone/.next/static || echo 'Static directory not found, skipping symlink'

},
{
command: 'cd web && mkdir -p .next/standalone/.next && ln -sfn ../../static .next/standalone/.next/static && PORT=3001 HOSTNAME=127.0.0.1 NEXT_PUBLIC_API_URL=http://127.0.0.1:7901 node .next/standalone/server.js',
url: 'http://127.0.0.1:3001/login',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 健康检查端点使用非标准页面路由

使用 /login 页面作为前端 webServer 的健康检查端点不可靠。登录页面可能因路由守卫、认证逻辑或中间件问题而返回非 200 状态码,即使服务器实际已启动成功。

建议:为前端服务创建一个专门的状态端点(如 /api/health),或在 .next/standalone 中添加 health check endpoint。确保该端点仅检查进程存活状态,不依赖业务逻辑。

'completed', 3, 1, 3, NULL, ${seededTimestamp}, ${seededTimestamp}, ${seededTimestamp}, ${seededTimestamp}
);
`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 缺少错误处理和数据库关闭保证

db.exec()执行可能因磁盘空间不足、权限问题或数据库锁而失败,当前代码没有try-catch包装。如果执行中途失败,数据库连接也不会被正确关闭(db.close()在L114),可能导致资源泄漏。

建议:使用try-finally或try-catch-finally确保db.close()始终执行:

try {
  db.exec(`...`);
  console.log(`Seeded E2E data into ${dbPath}`);
} finally {
  db.close();
}

.post('/login')
.send({
username: 'admin',
password: 'wrong-password',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 疑似引入敏感信息

变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。

建议:将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。

@mars167 mars167 marked this pull request as ready for review March 28, 2026 18:23
@mars167 mars167 requested a review from Copilot March 28, 2026 18:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens several stability paths across the dashboard (repositories/settings/auth), adds multi-LLM profile support, improves health checks, and strengthens platform client error reporting to reduce flaky browsing/configuration workflows.

Changes:

  • Introduce multi-profile LLM settings (profiles list + active selection) with modal editing and backend normalization/persistence.
  • Stabilize repository browsing via safer session cache handling and optional remote sync skipping for E2E.
  • Improve operational checks (frontend /api/health, backend /health + /api/health), CORS origin normalization, and richer platform-client error messages; refresh E2E seeding + Playwright startup.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
web/src/types/index.ts Add LlmProfile and extend SystemSettings with profiles + active profile id.
web/src/components/ui/Input.tsx Add endAdornment support; refactor PasswordInput to use it.
web/app/login/page.tsx Replace anchor with Next.js Link for navigation.
web/app/dashboard/users/page.tsx Wrap initial data load in useCallback + effect dependency stabilization.
web/app/dashboard/settings/page.tsx Add LLM profiles UI (list + modal editor) and normalize settings/profiles client-side.
web/app/dashboard/repositories/page.tsx Sanitize session cache store and avoid caching empty repository pages.
web/app/dashboard/page.tsx Layout tweaks to prevent overflow and improve responsive grids.
web/app/dashboard/connections/page.tsx Wrap initial load in useCallback + effect dependency stabilization.
web/app/api/health/route.ts Add frontend health endpoint.
server/src/services/SystemSettingsService.ts Add LLM profiles to settings schema; normalize legacy flat fields into active profile; persist selection.
server/src/services/SystemSettingsService.test.ts Add tests for LLM profile normalization and persistence.
server/src/routes/settingsRoutes.ts Expand LLM test payload typing to include profile fields.
server/src/routes/repositoryRoutes.ts Allow skipping remote repository sync via env flag for stability/E2E.
server/src/platform/client.ts Centralize JSON requests; improve fetch/error diagnostics incl. body previews and cause metadata.
server/src/platform/client.test.ts Add GitHub client tests for enhanced error messages.
server/src/models/ActivityLog.ts Change failed-login logging to accept a valid adminId.
server/src/index.ts Add /api/health alias for backend health; CORS origin check via helper.
server/src/config/index.ts Normalize CORS origins; add isCorsOriginAllowed helper.
server/src/config/index.test.ts Add tests for CORS origin matching/normalization.
server/src/auth/routes.ts Avoid FK failures on failed login by logging only when admin exists.
server/src/auth/routes.test.ts Add tests covering failed-login behavior for existing vs unknown admins.
scripts/seed-e2e-data.js Improve module resolution for sqlite; create oauth table; refresh seeded records for E2E.
playwright.config.ts Update baseURL/ports; add webServer orchestration for backend + standalone frontend.
package.json Add check scripts and test:e2e wiring.
e2e/dashboard-pages.spec.ts Update assertions to match seeded repo/job names and new UI text/flows.
docs/configuration.md Document multi-LLM initialization behavior; update CORS examples.
deploy/verify.sh Adjust backend health check path.
deploy/systemd/codagraph-lite-frontend.service Update service type and ensure Next binds to 0.0.0.0:3000.
deploy/start.sh Harden startup health checks for both frontend and backend.
AGENTS.md Add project “memory” notes about known regressions and validation workflow.
.env.example Update CORS docs and clarify LLM env vars as defaults for initial profile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

[Service]
Type=notify
Type=simple
NotifyAccess=all
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit switches to Type=simple but keeps NotifyAccess=all. NotifyAccess is only relevant for notify-based services, so this is likely a leftover and may produce confusing configuration warnings. Either remove NotifyAccess or keep Type=notify and ensure the process actually sends readiness notifications.

Suggested change
NotifyAccess=all

Copilot uses AI. Check for mistakes.
const rawMaxRetries = typeof profile?.maxRetries === 'number' ? profile.maxRetries : Number.NaN;

return {
id: profile?.id?.trim() || defaultLlmProfile.id || `llm-profile-${index + 1}`,
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeLlmProfile() always falls back to defaultLlmProfile.id when profile.id is empty, even for index > 0. That can cause multiple profiles to collapse to the same id and be de-duplicated by the filter, effectively dropping profiles returned from the backend if any id is missing/blank. Use an index-based fallback for non-first profiles (or generate a new id) so each profile remains distinct.

Suggested change
id: profile?.id?.trim() || defaultLlmProfile.id || `llm-profile-${index + 1}`,
id:
(typeof profile?.id === 'string' && profile.id.trim().length > 0)
? profile.id.trim()
: index === 0
? (defaultLlmProfile.id || `llm-profile-${index + 1}`)
: `llm-profile-${index + 1}`,

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +81
<button
type="button"
onClick={() => setShowPassword((current) => !current)}
className="inline-flex items-center text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
tabIndex={-1}
>
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PasswordInput’s visibility toggle button is not keyboard-focusable (tabIndex={-1}) and has no accessible name. This makes the control unusable for keyboard/screen-reader users. Consider removing tabIndex={-1} and adding an aria-label (and/or visually hidden text) describing the action (e.g., show/hide password).

Copilot uses AI. Check for mistakes.
Comment on lines +255 to +262
export function isCorsOriginAllowed(allowedOrigins: string[], origin: string): boolean {
const normalizedAllowedOrigins = normalizeCorsOrigins(allowedOrigins);
if (normalizedAllowedOrigins.includes('*')) {
return true;
}

return normalizedAllowedOrigins.includes(normalizeCorsOrigin(origin));
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isCorsOriginAllowed() re-normalizes the full allowedOrigins list on every call. Since loadConfig() already normalizes corsOrigins once, this adds unnecessary per-request work in the CORS callback. Consider normalizing allowedOrigins once (at config load) and, here, only normalizing the incoming origin before matching (or accept a pre-normalized list).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 949c0f8153

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 474 to +478
const response = await apiClient.testLlmSettings({
llmProvider: settings.llmProvider,
llmApiKey: settings.llmApiKey,
llmApiBaseUrl: settings.llmApiBaseUrl,
llmModel: settings.llmModel,
llmMaxRetries: settings.llmMaxRetries,
llmProvider: editingLlmProfile.provider,
llmApiKey: editingLlmProfile.apiKey,
llmApiBaseUrl: editingLlmProfile.apiBaseUrl,
llmModel: editingLlmProfile.model,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Send edited profile identity in LLM test payload

This test request only sends flat llm* fields, but the backend now resolves effective config from llmProfiles plus activeLlmProfileId. As a result, clicking “测试当前编辑配置” while editing a non-active (or newly added unsaved) profile can test the saved active profile instead, producing misleading pass/fail results for the wrong credentials. Include the selected profile context (e.g., llmProfiles with activeLlmProfileId set to selectedLlmProfileId) when calling the test API.

Useful? React with 👍 / 👎.

Comment on lines +305 to +309
llmProvider: activeLlmProfile.provider,
llmApiKey: activeLlmProfile.apiKey,
llmApiBaseUrl: activeLlmProfile.apiBaseUrl,
llmModel: activeLlmProfile.model,
llmMaxRetries: activeLlmProfile.maxRetries,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve flat LLM overrides when normalizing settings

Normalization now always mirrors llmProvider/llmApiKey/llmApiBaseUrl/llmModel/llmMaxRetries from the resolved active profile, so callers that pass only legacy flat overrides are silently ignored whenever llmProfiles exists. That breaks backward-compatible update paths (and contributes to testing/saving the wrong profile unless callers also rewrite profile arrays).

Useful? React with 👍 / 👎.

@mars167 mars167 merged commit 5428ef0 into main Mar 28, 2026
@mars167 mars167 deleted the fix/repository-pull-requests-endpoint branch March 28, 2026 18:30
], 'https://app.example.com')).toBe(true);
});

it('supports wildcard origins', () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL · 测试用例将通配符 CORS 配置验证为安全行为

第 17-19 行的测试用例将 ['*'] 作为可信来源列表,并期望任意来源(如 'https://preview.example.com')都能通过验证。这是在测试一个严重的安全漏洞:通配符 Access-Control-Allow-Origin: * 允许任何恶意网站向该服务端点发起跨域请求,可能导致 CSRF 攻击和数据泄露。此测试用例将这种不安全行为规范化为预期正确的功能,没有任何警告或风险提示。

建议:如果业务上确实需要允许所有来源(例如公开 API),应使用明确的域名白名单或添加安全警告注释。如果 '' 通配符是错误配置,测试应反映这一点(期望失败或标记为危险配置)。建议添加一个专门描述通配符风险的测试用例,或在 CORS 配置模块中添加对 '' 的安全警告。


# 检查后端
if curl -sf http://localhost:7900/api/health >/dev/null 2>&1; then
if curl -sf "http://localhost:${BACKEND_PORT}/health" >/dev/null 2>&1; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 后端健康检查端点与原配置不一致

健康检查端点从 /api/health 变更为 /health。旧代码检查的是 /api/health,新代码检查 /health。这可能导致健康检查失败,如果后端服务实际只暴露了 /api/health 端点。

建议:确认 server/dist 的实际健康检查路由。如果 server 同时暴露两个端点则无问题,否则需将端点改回 /api/health 或同步更新 server 的路由配置。

log_info "检查 API 健康状态..."

local backend_url="http://localhost:${BACKEND_PORT:-7900}/api/health"
local backend_url="http://localhost:${BACKEND_PORT:-7900}/health"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 健康检查端点路径变更可能导致验证失败

将健康检查端点从 /api/health 改为 /health。根据语义上下文,docs/api.md 定义的 API 路径为 /api/v1/auth/verify,前端 API 客户端调用的是 /api/auth/verify。如果后端服务将所有 /api/* 路径作为业务 API 统一管理,健康检查可能实际由 /api/health 提供而非 /health。此变更可能导致部署验证脚本无法正确检测后端服务状态。

建议:确认后端实际提供的健康检查端点路径。如果使用路径前缀 /api 统一管理服务,应保持 /api/health。可通过检查 server 代码中的路由配置(如 Express/ Gin 的 health 路由定义)确认。

@@ -19,9 +21,10 @@ test('repositories page renders seeded repository', async ({ page }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 页面错误监听器注册时机晚于页面导航

在第20行 page.on('pageerror', ...) 注册错误监听器后,第22行才执行 page.goto('/dashboard/repositories')。如果页面加载过程中立即抛出错误(如 JS 执行初始化脚本失败),此时错误监听器可能尚未完全就绪,导致页面错误无法被捕获,测试无法正确验证页面稳定性。jobs 和 history 测试存在相同问题(第34行注册、L35导航;第47行注册、L49导航)。

建议:将 page.on('pageerror', ...) 调用移至 beforeEach 中,在任何 page.goto 之前统一注册一次。或者在每个测试的开头、第一行就注册监听器,再进行导航。

await expect(page.getByText('analyze_pr')).toBeVisible();
await expect(
page.getByRole('link', {
name: new RegExp(`Job #\\d+ · ${seededRepositoryName} · PR #\\d+`),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 正则表达式硬编码 Job 链接格式,脆弱且无回退断言

正则表达式 Job #\d+ · ${seededRepositoryName} · PR #\d+ 假设前端 Job 链接的文本格式完全匹配。如果前端 UI 文本发生微小变化(如分隔符从 · 改为 -、顺序调整、翻译变更),测试会失败,即使后端数据和核心功能正常。这违反了测试金字塔原则——过度依赖 UI 文本细节使测试变得脆弱。

建议:增加一个或多个不依赖精确文本格式的断言,例如检查链接元素存在且 href 指向正确的端点:expect(page.locator('a[href*="/jobs/"]')).toBeVisible()。将正则匹配降级为可选的辅助断言。

"build": "npm run build:web && npm run build:server",
"check:web": "npm --prefix web run lint",
"check:server": "npm --prefix server run typecheck",
"check": "npm run check:web && npm run check:server && npm run build",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · check 脚本的执行顺序可能导致资源浪费

check 脚本按顺序执行 check:web && check:server && npm run build。如果 lint/typecheck 失败,后续的 build 仍会被尝试执行,浪费构建时间。通常 lint/typecheck 应该在 build 之前运行,以便尽早发现问题。

建议:考虑将 check 脚本改为 npm run check:web && npm run check:server(去掉 build),将 build 交给单独的 build 命令。或者,如果确实需要 check + build 的组合,应在 check 失败时立即退出,不执行 build。

timeout: 30_000,
},
{
command: 'cd web && mkdir -p .next/standalone/.next && if [ -d .next/static ]; then ln -sfn ../../static .next/standalone/.next/static; fi && PORT=3001 HOSTNAME=127.0.0.1 NEXT_PUBLIC_API_URL=http://127.0.0.1:7901 node .next/standalone/server.js',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 前端服务器启动命令链过长,单点故障难以诊断

webServer 命令使用 && 链式执行 5 个操作:mkdir、if 条件判断、ln 命令、PORT/HOSTNAME/NEXT_PUBLIC_API_URL 设置、node 启动。如果任一步骤失败(如 standalone 构建缺失、文件权限问题、目录不存在),Playwright 仅报告超时,开发者需要手动运行命令才能定位具体故障点。这会显著增加调试成本,特别是在 CI 环境中。

建议:考虑拆分为两步:(1) 在项目根目录添加 scripts/start-frontend-for-e2e.ts 脚本,负责预检查和准备逻辑;(2) webServer command 改为调用该脚本,使失败信息更明确。或将关键步骤的错误输出捕获并显示。

const seededTimestamp = "datetime('now', 'localtime', '+1 day')";
const seededAdminPasswordHash = crypto
.createHash('sha256')
.update('changeme')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH · 硬编码测试凭证存在意外泄露风险

admin 密码和 OAuth token 使用硬编码字符串('changeme'、'e2e-token')。虽然这是 E2E 测试脚本,但如果错误地作为生产数据库初始化脚本运行,这些凭证可能被意外使用。

建议:考虑从环境变量读取敏感数据,例如:const adminPassword = process.env.E2E_ADMIN_PASSWORD || 'changeme';。或在脚本头部添加明确的危险警告注释。

@codegraphmars
Copy link
Copy Markdown

codegraphmars bot commented Mar 28, 2026

CodaGraph Review 摘要

有 2 个文件因为缺少 patch 或类型不受支持而被跳过,当前结论应按 partial review 理解。 模式:LLM + 规则;风险等级:critical;置信度:high。 共审查 28/30 个文件,发现 30 个高价值问题(critical 1 / high 17 / medium 6 / low 6)。 另外有 2 个文件未被完整审查,当前结果应按 partial review 理解。

摘要中的补充问题

  • 疑似引入敏感信息 (server/src/auth/routes.test.ts:61)

    • 严重性: high
    • 描述: 变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。
    • 建议: 将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。
  • 测试假设与预期生产行为不一致 (server/src/auth/routes.test.ts:79)

    • 严重性: high
    • 描述: 第二个测试用例在 findByUsername 返回 null 后,强制让 verifyPassword mock 返回 false,并断言该方法被调用。但生产代码中,如果用户不存在(findByUsername 返回 null),路由应该直接返回 401,根本不会调用 verifyPassword。当前测试掩盖了这个行为差异,无法验证生产代码是否正确实现了"用户不存在时跳过密码验证"的逻辑。
    • 建议: 移除 adminModelMock.verifyPassword.mockReturnValue(false) 这一行,以及 L92 对 verifyPassword 调用的断言。或者,如果确有必要测试时序攻击防护,则需要在测试描述中明确说明,并在路由代码中确认实际调用了 verifyPassword
  • 失败登录审计日志可能缺失关键场景 (server/src/auth/routes.ts:55)

    • 严重性: high
    • 描述: 原代码在密码验证失败时无论用户是否存在都会记录日志(使用 username)。新代码改为仅在 admin 存在时才记录失败登录。这意味着针对不存在用户名的登录尝试将不会被记录到活动日志中,可能影响安全审计能力。攻击者可以通过枚举测试来验证哪些用户名存在于系统中,而不会被记录。
    • 建议: 建议在日志层面保持一致:如果要记录失败登录,应记录到 activityLogModel,可以考虑添加一个针对不存在用户的失败登录记录(使用 username 而非 admin.id)。或者明确这是预期行为,并在安全策略文档中说明。参考 README 中提到的密码重置功能会用到管理员操作审计。
  • logFailedLogin 调用方语义不明确可能导致运行时错误 (server/src/models/ActivityLog.ts:115)

    • 严重性: high
    • 描述: 函数签名新增了 adminId: number 参数,替代了原有的 admin_id = 0 占位值。从调用点 auth/routes.ts:57 传递 admin.id 的模式来看,失败登录时可能通过用户名反查到了 admin 记录(密码错误场景)。但若用户名不存在于数据库中,调用方可能拿到 null/undefined,直接传参会触发运行时错误(如 TypeError: Cannot read property 'id' of null)。当前 ActivityLogModel 自身无校验,且 CREATE TABLE 时 admin_id 可能 NOT NULL,导致数据库写入失败。
    • 建议: 在 ActivityLogModel 或调用层增加防御:1) 在 create() 中对 admin_id 为 0 的场景做特殊记录或跳过写入;2) 或要求调用方在 adminId 为 undefined/null 时传入 0 作为降级方案,并在 findByAdminId 等查询处过滤 0 值。确保任何登录尝试(包括用户名不存在)都能被记录。
  • 测试断言中硬编码敏感错误消息可能掩盖生产环境安全问题 (server/src/platform/client.test.ts:210)

    • 严重性: high
    • 描述: 测试断言明确期望 GitHub 返回 'Bad credentials' 的原始响应体出现在错误消息中。如果生产代码直接将 HTTP 响应体拼接进错误日志/消息而不做脱敏,当 API 返回包含 token、session ID 或其他敏感信息的错误体时,会直接泄露。测试通过意味着这个行为被认可为预期行为,但该行为本身存在信息泄露风险。
    • 建议: 确认 GitHubApiClient 的实现是否对错误响应体做了脱敏处理(如只保留 message 字段名,去除实际值)。如果已实现脱敏,应更新测试断言期望脱敏后的消息(如 'body={"message":"***"}');如果未实现,应在实现中添加脱敏逻辑。
  • getLlmConfig 方法对新增字段的处理需确认 (server/src/routes/settingsRoutes.ts:12)

    • 严重性: high
    • 描述: LlmTestPayload 新增了 llmProfiles 和 activeLlmProfileId 字段,这些字段用于覆盖或指定 LLM 配置。当前 getLlmConfig(payload) 会接收这些字段,如果该方法未针对这两个字段做正确处理,可能导致配置来源混乱或使用过期的系统设置。
    • 建议: 确认 settingsService.getLlmConfig() 方法的实现已适配 llmProfiles 和 activeLlmProfileId 字段,包括:1) 当 payload 提供 activeLlmProfileId 时是否正确选择对应 profile;2) 当 payload 提供 llmProfiles 时是否优先使用而非系统设置;3) 边界情况如 activeLlmProfileId 指向不存在的 profile 时的处理逻辑。
  • 疑似引入敏感信息 (server/src/services/SystemSettingsService.test.ts:64)

    • 严重性: high
    • 描述: 变更中出现了密码、token 或 API key 相关字段,需要确认没有把秘密写进仓库。
    • 建议: 将密钥迁移到环境变量或密钥管理系统,并避免在源码中硬编码。
  • setState 使用对象展开而非函数式更新,可能丢失并发状态 (web/app/dashboard/connections/page.tsx:72)

    • 严重性: high
    • 描述: handleRefreshToken 不是 useCallback,每次渲染都会创建新函数实例。L72 使用 setActionLoading({ ...actionLoading, [id]: true }),其中的 actionLoading 闭包捕获的是旧值。相比之下,L80 已正确使用函数式更新 setActionLoading((prev) => ({ ...prev, [id]: false }))。如果用户在 token 刷新完成前触发其他操作(刷新其他 token 或断开连接),可能导致 actionLoading 状态丢失。
    • 建议: 将 L72 改为 setActionLoading((prev) => ({ ...prev, [id]: true })),与 L80 保持一致,确保状态更新基于最新状态而非闭包捕获的旧值。

建议动作

  • 如果业务上确实需要允许所有来源(例如公开 API),应使用明确的域名白名单或添加安全警告注释。如果 '' 通配符是错误配置,测试应反映这一点(期望失败或标记为危险配置)。建议添加一个专门描述通配符风险的测试用例,或在 CORS 配置模块中添加对 '' 的安全警告。

  • 确认 server/dist 的实际健康检查路由。如果 server 同时暴露两个端点则无问题,否则需将端点改回 /api/health 或同步更新 server 的路由配置。

  • 确认后端实际提供的健康检查端点路径。如果使用路径前缀 /api 统一管理服务,应保持 /api/health。可通过检查 server 代码中的路由配置(如 Express/ Gin 的 health 路由定义)确认。

  • page.on('pageerror', ...) 调用移至 beforeEach 中,在任何 page.goto 之前统一注册一次。或者在每个测试的开头、第一行就注册监听器,再进行导航。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants