# Reasonix project memory Notes the user pinned via the `#` prompt prefix. The whole file is loaded into the immutable system prefix every session — keep it terse. - 保留自定义页面远程浏览器登录态 ## Summary - 根因:自定义页面查看器在启动、切换、卸载时会调用 DELETE /api/browser-sessions/{id},服务端随即关闭 Playwright persistent context。虽然 profile 目录在 data/browser-profiles/page-* 里保留,但每次前端都主动销毁会话,体验上就是“断开后重开”。 - 修复目标:普通断开只断 WebSocket,不关闭服务端浏览器;再次打开同一自定义页面时复用同一个 page/profile,从而保留登录态。 - 隐私策略:默认保留登录态,并提供显式“清除登录态/重建浏览器”能力。 ## Key Changes - 调整 PageViewer.vue 的远程浏览器生命周期: - 启动前不再先 closeRemoteSession(),只 stopRemoteWs(),让后端可复用已有 session。 - onBeforeUnmount、active=false、同页重连时只断开 WebSocket 和释放前端截图 blob,不调用后端 DELETE。 - 页面 ID 变化时断开当前 WS,再为新页面创建/复用对应 session;不主动销毁旧页面 session。 - 保留当前后端 BrowserSessionService.create() 的复用逻辑: - 同一 custom_page_id + origin 已有活跃 session 时直接返回,并调整 viewport。 - profile 目录继续使用 BROWSER_PROFILES_DIR,当前 Docker 已挂载到 ./data:/app/data。 - 新增显式清理能力: - 后端增加清除 profile 的接口,例如 DELETE /api/browser-sessions/profiles/{custom_page_id},行为是关闭该页面现有 session 并删除对应 page-{id}-{origin} profile 目录。 - 前端远程浏览器工具栏增加“清除登录态”按钮,二次确认后调用清理接口,再重新创建 session。 - 保留现有 DELETE /api/browser-sessions/{id} 作为“关闭当前会话但不删除 profile”的轻量关闭,不作为普通页面离开的默认动作。 ## Public Interfaces - browserSessionsApi 增加 clearProfile(customPageId: number)。 - 后端新增一个清理 profile 的管理接口;普通创建、截图、事件、WebSocket 接口保持兼容。 - 不改变自定义页面模型、数据库字段或现有 BROWSER_PROFILES_DIR 配置。 ## Test Plan - 打开一个远程浏览器自定义页面并登录,离开页面再返回,确认无需重新登录。 - 刷新 SmartUp 前端页面后重新进入同一自定义页面,确认服务端仍能复用活跃 session;若服务端重启,则从落盘 profile 恢复登录态。 - 点击“清除登录态”,确认 profile 被删除并重新打开后需要重新登录。 - 切换两个不同远程浏览器页面,确认各自登录态互不串。 - 回归检查远程浏览器的后退、前进、刷新、复制选中文本、一键刷新上游凭证仍可用。 ## Assumptions - 本次只处理“自定义页面查看”的远程浏览器登录态,不改“上游认证提取弹窗”的临时 profile 行为。 - 登录态保留依赖目标站点 cookie/localStorage 本身有效;如果目标站点设置极短过期或服务端强制失效,SmartUp 无法绕过。 - 后续可再加 idle TTL 清理后台浏览器进程,但本次优先解决频繁重开导致的重复登录体验。 优化 - 保留自定义页面远程浏览器登录态 ## Summary - 根因:自定义页面查看器在启动、切换、卸载时会调用 DELETE /api/browser-sessions/{id},服务端随即关闭 Playwright persistent context。虽然 profile 目录在 data/browser-profiles/page-* 里保留,但每次前端都主动销毁会话,体验上就是“断开后重开”。 - 修复目标:普通断开只断 WebSocket,不关闭服务端浏览器;再次打开同一自定义页面时复用同一个 page/profile,从而保留登录态。 - 隐私策略:默认保留登录态,并提供显式“清除登录态/重建浏览器”能力。 ## Key Changes - 调整 PageViewer.vue 的远程浏览器生命周期: - 启动前不再先 closeRemoteSession(),只 stopRemoteWs(),让后端可复用已有 session。 - onBeforeUnmount、active=false、同页重连时只断开 WebSocket 和释放前端截图 blob,不调用后端 DELETE。 - 页面 ID 变化时断开当前 WS,再为新页面创建/复用对应 session;不主动销毁旧页面 session。 - 保留当前后端 BrowserSessionService.create() 的复用逻辑: - 同一 custom_page_id + origin 已有活跃 session 时直接返回,并调整 viewport。 - profile 目录继续使用 BROWSER_PROFILES_DIR,当前 Docker 已挂载到 ./data:/app/data。 - 新增显式清理能力: - 后端增加清除 profile 的接口,例如 DELETE /api/browser-sessions/profiles/{custom_page_id},行为是关闭该页面现有 session 并删除对应 page-{id}-{origin} profile 目录。 - 前端远程浏览器工具栏增加“清除登录态”按钮,二次确认后调用清理接口,再重新创建 session。 - 保留现有 DELETE /api/browser-sessions/{id} 作为“关闭当前会话但不删除 profile”的轻量关闭,不作为普通页面离开的默认动作。 ## Public Interfaces - browserSessionsApi 增加 clearProfile(customPageId: number)。 - 后端新增一个清理 profile 的管理接口;普通创建、截图、事件、WebSocket 接口保持兼容。 - 不改变自定义页面模型、数据库字段或现有 BROWSER_PROFILES_DIR 配置。 ## Test Plan - 打开一个远程浏览器自定义页面并登录,离开页面再返回,确认无需重新登录。 - 刷新 SmartUp 前端页面后重新进入同一自定义页面,确认服务端仍能复用活跃 session;若服务端重启,则从落盘 profile 恢复登录态。 - 点击“清除登录态”,确认 profile 被删除并重新打开后需要重新登录。 - 切换两个不同远程浏览器页面,确认各自登录态互不串。 - 回归检查远程浏览器的后退、前进、刷新、复制选中文本、一键刷新上游凭证仍可用。 ## Assumptions - 本次只处理“自定义页面查看”的远程浏览器登录态,不改“上游认证提取弹窗”的临时 profile 行为。 - 登录态保留依赖目标站点 cookie/localStorage 本身有效;如果目标站点设置极短过期或服务端强制失效,SmartUp 无法绕过。 - 后续可再加 idle TTL 清理后台浏览器进程,但本次优先解决频繁重开导致的重复登录体验。 开始优化 - 远程浏览器复制按钮同步到本机剪贴板 ## Summary - 根因:远程浏览器里的网页复制按钮只操作服务端 Chromium 的剪贴板,当前 SmartUp 只把鼠标/键盘事件发到远端,没有把远端 clipboard 同步回本机浏览器。 - 修复目标:用户点击远程页面里的复制按钮后,SmartUp 自动尝试读取远端剪贴板并写入本机剪贴板;失败时提供明确提示和手动复制兜底。 ## Key Changes - 后端新增远程剪贴板读取能力: - 在 Playwright context 创建后授予当前 origin clipboard-read / clipboard-write 权限。 - 增加 GET /api/browser-sessions/{session_id}/clipboard,通过 navigator.clipboard.readText() 读取远端页面剪贴板。 - 读取失败时返回可解释错误,不影响现有远程浏览器会话。 - 前端 PageViewer.vue 增加自动同步: - 在远程鼠标 mouseup 后短延迟读取远端剪贴板。 - 如果读到非空文本且和上次同步内容不同,调用 navigator.clipboard.writeText() 写入本机剪贴板,并提示“已同步到本机剪贴板”。 - 保留现有“复制远程选中文本”按钮,作为选中文本场景的手动路径。 - 兜底体验: - 如果本机浏览器阻止自动写剪贴板,显示一个可复制文本提示框或通知,让用户手动复制。 - 只同步文本,不处理图片/富文本剪贴板。 ## Test Plan - 在远程浏览器打开包含 API Key 复制按钮的页面,点击页面内复制图标,回到本机输入框粘贴,确认拿到 sk-...。 - 点击普通按钮或空白区域,确认不会反复弹出无意义复制提示。 - 点击远程页面后如果 clipboard 为空,确认不报错、不打断操作。 - 测试 HTTPS / localhost 下本机 navigator.clipboard.writeText 成功路径。 - 测试浏览器禁用剪贴板权限时,确认出现手动复制兜底提示。 - 回归 Ctrl+C 选中文本复制、Ctrl+V 粘贴到远程输入框、远程浏览器登录态保留。 ## Assumptions - 目标主要是文本类密钥、token、API key。 - 自动同步只在用户远程点击后触发,不做持续轮询,避免误复制和资源浪费。 - 若目标站点自身复制按钮没有真正写入远端 clipboard,SmartUp 无法凭空知道按钮背后的文本,只能提示未检测到远程剪贴板内容。 修复 - 远程浏览器登录态持久化与二次访问转圈修复方案 ## Summary 当前后端已经用 launch_persistent_context(profile_dir) 做了登录态持久化,问题不在“没有持久化”,而在“长期复用活跃 session/page”。第二次访问一直转圈,优先按“复用了不可用的旧 page,前端等不到首帧”处理。 默认策略改为:登录态靠持久化 profile 保存,页面运行态不长期依赖复用;离开远程浏览器页面时关闭 runtime session,但不删除 profile。再次进入时新建 Chromium context,从 profile 恢复 cookies/localStorage/sessionStorage。 ## Key Changes - 前端生命周期调整: - 页面切换、组件卸载、active=false 时调用现有 DELETE /api/browser-sessions/{id} 关闭远程浏览器 runtime session。 - 不调用“清除登录态”接口,不删除 profile 目录。 - 清空 remoteSession、remoteScreenshotUrl、loading/error/caret 等运行态,避免旧截图和旧 session 干扰。 - 后端保持持久化 profile: - close(session_id) 只关闭 context,不删除普通 page-* profile。 - 仅 clearProfile(custom_page_id) 删除登录态目录。 - auth-capture-* 临时 profile 仍按现有逻辑清理。 - 保留同一页面内的短期复用,但加健康兜底: - 如果 create() 命中现有 session,先做轻量健康检查:page 未关闭、能取 state、能在短超时内截图。 - 健康检查失败时关闭旧 session,用同一 profile 重新启动 context。 - 首帧失败兜底: - 后端 screenshot() 加明确超时,避免 WebSocket push loop 无限等待。 - 前端连接 WebSocket 后增加首帧 watchdog,例如 8-10 秒没有收到第一张截图就提示“远程浏览器无响应”,并提供“重建浏览器”按钮。 - “重建浏览器”只关闭 runtime session 后重开,不删除登录态。 - 文案区分两个动作: - “刷新/重建浏览器”:关闭当前 runtime,用持久化 profile 重开,登录态保留。 - “清除登录态”:关闭 runtime 并删除 profile,下次需要重新登录。 ## API / Behavior - 不新增必须 API,复用现有: - DELETE /api/browser-sessions/{session_id} = 关闭运行中的浏览器,不清登录态。 - DELETE /api/browser-sessions/profiles/{custom_page_id} = 清除登录态。 - 如需更清晰,可前端按钮文案改为“重建浏览器”,内部调用 close session 后 startRemoteBrowser()。 ## Test Plan - PackyAPI 首次登录成功后,离开页面再回来,应直接进入已登录状态,不要求重新登录。 - 关闭浏览器页面、刷新 SmartUp 页面、重新进入远程浏览器,应从持久化 profile 恢复登录态。 - 点击“重建浏览器”后,应保留登录态并恢复可操作画面。 - 点击“清除登录态”后,应删除 profile,下次进入需要重新登录。 - 模拟旧 session 截图失败或 WebSocket 无首帧,前端不能无限转圈,应显示可恢复错误和重建入口。 - 多个远程浏览器页面之间切换,profile 不串,登录态按页面隔离。 ## Assumptions - 用户更看重“再次访问不用重新登录”和“不要卡死转圈”,不要求保留上一次离开时的精确 DOM/滚动/弹窗运行状态。 - profile 目录所在 /app/data/browser-profiles 是持久卷;如果部署环境没有挂载持久卷,需要先修部署卷配置。 - 普通关闭 session 不删除 profile,因此用它替代长期活跃复用更可靠。 - 二次访问远程浏览器白屏修复方案 ## Summary 结合 Docker 日志看,第一次进入有完整链路:POST /api/browser-sessions、WebSocket 建连、随后离开时 DELETE /api/browser-sessions/{id}。但第二次白屏期间日志里没有新的 POST /api/browser-sessions,说明问题不是后端 Chromium 已启动但截图失败,而是前端关闭 session 后,重新激活页面时没有重新创建远程浏览器 session。 根因在 PageViewer.vue 的 props.active watcher:active=false 时关闭了 session;active=true 时只在 remoteSession 仍存在时重连 WebSocket,remoteSession=null 时没有调用 startRemoteBrowser()。 ## Key Changes - 修复 props.active watcher: - active=false:继续 stopRemoteWs() + await closeRemoteSession(),保留 profile。 - active=true: - 如果 remoteSession.value 存在,按现有逻辑 connectRemoteWs()。 - 如果 remoteSession.value 不存在,调用 await startRemoteBrowser(),从持久化 profile 新建 runtime session。 - 激活后再 nextTick(() => remoteFrameRef.value?.focus())。 - 修复白屏显示状态: - closeRemoteSession() 清空 remoteSession 和截图后,对远程浏览器页面设置 iframeLoading.value = true,避免没有 session、没有截图、也没有 loading 的空白区域。 - startRemoteBrowser() 成功建连后继续由首帧 onRemoteImageLoad() 关闭 loading。 - 防重复创建: - 复用现有 startRemoteBrowserPromises,确保 active=true、路由 watcher、重建按钮同时触发时不会并发创建多个 session。 - 保持 profile 持久化语义: - 普通关闭 session 只释放 context,不删除 page-* profile。 - “清除登录态”仍是唯一删除 profile 的入口。 ## Test Plan - 首次打开 98 远程浏览器,确认 Docker 日志出现 POST /api/browser-sessions 和 WebSocket。 - 切到“网站管理”等普通页面,确认日志出现 DELETE /api/browser-sessions/{id}。 - 再次点击 98,应重新出现 POST /api/browser-sessions,页面不白屏,并从 profile 恢复登录态。 - 在自定义页面 tab 间切换:旧 tab 关闭 runtime,新 tab 激活时能重新创建 session。 - 点击“重建浏览器”:应关闭当前 session 后重新创建,登录态保留。 - 点击“清除登录态”:下次进入需要重新登录。 - 若首帧 10 秒未到,应显示“远程浏览器无响应”,不能无限白屏或转圈。 ## Assumptions - Docker 日志中第二次没有 POST /api/browser-sessions 是本次白屏的主要证据。 - 白屏不是目标站点自身渲染白页,而是 SmartUp 前端没有重新启动远程浏览器 session。 - 当前持久化 profile 目录 /app/data/browser-profiles 已正确挂载,问题不需要改 Docker volume。 - 系统页高亮按钮统一方案 ## Summary 问题不是所有 primary 都错,而是行内/列表内的频繁操作按钮被做成了实心橙色,和表格、绑定列表这种低强调区域不搭。统一规则:页面级主操作保留橙色 primary;表格行内、列表行内、辅助面板里的操作全部降级为 text/ghost 样式。 ## Key Changes - Websites.vue: - “我的网站分组”表格里的“绑定”去掉 type="primary",改为 text 低强调按钮,建议加 class="inline-action". - “分组绑定”列表里的“同步”去掉 type="primary",改为 text 低强调按钮,建议加 class="inline-action". - “分组绑定”面板头部的“新增绑定”如果觉得仍抢眼,改为 text 或 plain,只保留页面顶部“新增网站”为 primary。 - 删除按钮继续保留 danger 语义,但通过局部 CSS 保持透明背景,只在 hover 时浅红反馈。 - 局部样式: - 新增 .inline-action:小尺寸、透明背景、浅边框/低饱和文字,hover 时轻微橙色底,不使用实心渐变。 - 新增 .panel-head .el-button--primary.is-plain 或专用 .panel-action,让面板级新增按钮不等同页面级主 CTA。 - 设计层级: - 页面级创建:新增网站、抽屉里的 保存 保留 primary。 - 面板级辅助:拉取分组、新增绑定 使用 default/plain/text。 - 行内操作:绑定、同步、编辑、删除全部使用 text/ghost。 ## Test Plan - 网站管理页不再出现截图里那种橙色实心小按钮。 - 页面顶部“新增网站”仍是唯一明显主 CTA。 - “绑定”“同步”在 hover 时有反馈,但默认状态不抢视觉。 - 删除按钮默认不实心,hover 浅红。 - npm --prefix frontend run build 通过。 ## Assumptions - 用户要统一的是系统页里的行内高亮按钮,不是取消所有页面级 primary 按钮。 - 优化 make up 依赖缓存 ## Summary 当前 make up 固定执行 docker compose up -d --build,Dockerfile 里又有 npm ci、pip install --no-cache-dir、apt-get install、playwright install chromium。如果 Docker 层缓存失效或 BuildKit cache 不启用,就会反复下载依赖。优化目标是:普通启动不重新构建;需要构建时复用 npm/pip/apt/playwright 缓存。 ## Key Changes - 调整 Makefile: - make up 改为只执行 docker compose up -d,不默认 --build - 新增 make build 执行 DOCKER_BUILDKIT=1 docker compose build - 新增 make up-build 执行 DOCKER_BUILDKIT=1 docker compose up -d --build - 保留访问地址输出逻辑 - 调整 Dockerfile 依赖安装缓存: - frontend 阶段给 npm ci 加 BuildKit cache mount:--mount=type=cache,target=/root/.npm - backend 阶段取消 pip install --no-cache-dir,改为 BuildKit cache mount:--mount=type=cache,target=/root/.cache/pip - apt-get update/install 加 cache mount:/var/cache/apt、/var/lib/apt/lists - playwright install chromium 加缓存目录,例如 PLAYWRIGHT_BROWSERS_PATH=/ms-playwright 并用 --mount=type=cache,target=/ms-playwright - 保持 Docker 层缓存友好顺序: - frontend/package*.json 先 COPY,再 npm ci - backend/requirements.txt 先 COPY,再 pip install - 源码 COPY 放在依赖安装之后,避免业务代码改动导致依赖层重建 - 可选补充: - 在 README 或 Makefile 注释里说明日常使用 make up,依赖变更后用 make up-build ## Test Plan - 第一次运行: - make up-build - 预期会下载依赖并成功启动 - 第二次运行: - make up-build - 预期 npm/pip/apt/playwright 依赖明显走缓存,构建时间显著下降 - 普通重启: - make down && make up - 预期不触发镜像构建,也不下载依赖 - 修改前端/后端业务代码后: - make up-build - 预期只重跑源码 COPY 后的构建步骤,不重新下载 npm/pip 依赖 - 修改 frontend/package-lock.json 或 backend/requirements.txt 后: - make up-build - 预期只对应依赖层重新安装 ## Assumptions - 本机 Docker 支持 BuildKit cache mount。 - 日常 make up 的语义应是“启动服务”,不是“每次强制构建”。 - 需要更新镜像时,显式使用 make build 或 make up-build。 - 优化网站分组接口错误提示 ## Summary 当前错误来自 httpx.HTTPStatusError 的原始字符串,包含完整 URL、MDN 链接和两个 fallback endpoint,展示很丑。应在后端 Sub2ApiWebsiteClient 里把常见 HTTP 错误归一化成中文业务错误,前端继续显示 detail 即可。 ## Key Changes - 在 backend/app/services/website_client.py 增加错误格式化: - 捕获 httpx.HTTPStatusError - 401 显示:目标网站认证失败,请检查 Admin API Key / JWT 是否正确 - 403 显示:目标网站权限不足,请检查当前凭证是否有分组管理权限 - 404 显示:目标网站接口不存在,请检查 API Prefix 和分组接口路径 - 5xx 显示:目标网站服务异常,请稍后重试 - 网络/超时显示:无法连接目标网站 或 目标网站请求超时 - 优化 get_groups() fallback 错误聚合: - 如果 /groups 和 /groups/all 都是同一类认证错误,只返回一条友好提示 - 不再把完整 URL 和 MDN 链接暴露给用户 - 可保留简短接口路径信息,例如:尝试接口:/groups、/groups/all - 保持后端日志可排查: - 用户看到中文友好错误 - logger 里仍记录原始异常和 URL,便于排查 - 前端 Websites.vue 不需要大改: - 继续使用 e.response?.data?.detail || '拉取分组失败' - 展示出来会变成后端清洗后的中文错误 ## Test Plan - 目标网站 Admin API Key 配错: - 点击“拉取分组”或打开导入弹窗 - 预期提示:目标网站认证失败,请检查 Admin API Key / JWT 是否正确 - 不再出现完整 URL、MDN 链接、/groups/all 原始异常串 - API Prefix 配错: - 预期提示接口不存在或路径配置错误 - 目标网站关闭或地址不可达: - 预期提示无法连接/请求超时 - 正常凭证: - 拉取分组仍正常 - 补充单测: - 覆盖 401 双 fallback 聚合 - 覆盖 404、超时、网络错误的友好消息 ## Assumptions - 这类错误都属于目标 Sub2API 网站调用失败,不应触发 SmartUp 自身退出登录。 - 用户界面优先展示可操作建议;原始 HTTP 细节只保留在日志中。 - 修复网站管理编辑入口不明显 ## Summary 编辑功能已经存在,但当前是低对比度 icon-only 圆按钮,和多个操作图标挤在一起,用户很难识别。改为把“编辑”作为显性文字按钮放到操作列第一位,同时保留其他低强调图标按钮。 ## Key Changes - 调整网站列表操作列: - 把“编辑”移到第一个 - 改成文字按钮:编辑 - 可带图标: 编辑 - 其他操作继续 icon-only: - 查看分组、连接测试、新增绑定、导入分组、导入账号、删除 - 操作列宽度从 232 增加到 280 或改为 min-width="280" - .action-row 间距从 2px 增加到 6px - 给编辑按钮加稳定样式: - 不用 type="primary" - 使用 text/透明背景 - hover 时有轻微背景 - 不和删除按钮一样用 danger - tooltip 保留: - 编辑按钮仍可 tooltip “编辑网站配置” ## Test Plan - 打开网站管理: - 每行操作列第一个能直接看到“编辑” - 点击“编辑”打开网站抽屉 - 可以修改 Base URL、API Prefix、Admin API Key/JWT、超时等字段 - 检查视觉: - 编辑按钮不被其他按钮遮挡 - 操作按钮不重叠 - hover tooltip 正常 - 窄屏检查: - 表格横向滚动时操作列仍可用 - 构建: - npm --prefix frontend run build ## Assumptions - 问题不是点击事件失效,而是入口不可见/不可识别。 - 行内按钮仍遵循之前统一过的低强调风格,不恢复高亮实心按钮。 - 修复网站管理操作列按钮被裁切 ## Summary 截图显示不是层级问题,而是操作列宽度仍不够。当前操作列里放了“编辑”文字按钮 + 6 个圆形图标按钮,width=280 放不下,左侧按钮被裁切,只露出右侧几个图标和 hover 的一部分。需要减少行内按钮数量,把次要操作收进“更多”菜单,保证“编辑”稳定可见。 ## Key Changes - 操作列改为固定少量主操作: - 编辑:文字按钮,永远显示,排第一个 - 查看分组:文字或图标按钮,永远显示 - 更多:下拉菜单 - 删除:保留 icon-only danger,或放入更多菜单底部 - 将低频操作移入 el-dropdown: - 连接测试 - 新增绑定 - 导入上游分组 - 导入为账号管理账号 - 操作列宽度调整为稳定值: - 建议 width="240" 或 min-width="240" - 因为只显示 3-4 个入口,不再需要 280+ - .action-row 保持 flex-wrap: nowrap,但不再有 7 个按钮挤在一列 - 给 更多 菜单项加清晰文字: - 连接测试 - 新增绑定 - 导入上游分组 - 导入为账号管理账号 - 保留现有低强调风格: - 行内不使用实心 primary/success - 删除仍是 danger 低强调 hover ## Suggested UI 操作列展示为: [编辑] [查看分组] [更多 ▾] [删除] 更多菜单: 连接测试 新增绑定 导入上游分组 导入为账号管理账号 ## Test Plan - 打开网站管理: - “编辑”在每行操作列左侧完整可见 - “查看分组”完整可见 - “更多”点击后能看到 4 个次要操作 - 删除按钮可见且仍为低强调 danger - 点击验证: - 编辑打开网站抽屉 - 查看分组正常加载 - 更多里的连接测试、新增绑定、导入分组、导入账号都能触发原函数 - 宽屏和窄屏: - 操作列不再裁切左侧按钮 - 表格横向滚动时操作列内容仍完整 - 构建: - npm --prefix frontend run build ## Assumptions - 这个问题的根因是操作列内容过多导致裁切,不是按钮点击事件失效。 - 高频操作是“编辑”和“查看分组”;导入、绑定、测试属于低频操作,可以收进更多菜单。 - 上游 SmartUp 分组 Key 唯一化与同步清理 ## Summary 修复“创建分组 Key 可重复点击导致远端/本地重复”的问题。目标是每个上游分组最多只有一个 SmartUp 前缀 Key,本地数据库也只保留一条;如果远端 Key 被删除,本地同步删除;Key 的本地更新时间与上游分组检测时间保持一致。 ## Key Changes - 后端 Key 命名规则改为稳定名称: - 不再在 key_name 里追加时间戳 - 固定为:{name_prefix}-{upstream.name}-{group_name} - 默认前缀仍是 SmartUp - 创建前先同步远端已有 Key: - 调用 Sub2API GET /api/v1/keys,用 search={prefix}、group_id={group_id}、status=active 查远端 Key - 匹配规则:同一上游分组下,name 以 SmartUp 或用户填写前缀开头,且名称等于稳定 Key 名 - 如果远端已存在:不创建新 Key,只 upsert 本地记录为该远端 Key - 如果远端不存在:创建一个新 Key,并 upsert 本地记录 - 本地数据库唯一化: - upstream_generated_keys 增加唯一维度:upstream_id + group_id + key_name - SQLite 迁移时先清理历史重复: - 同一 upstream_id/group_id/key_name 只保留最新一条 - 旧的重复记录删除 - 之后创建唯一索引,防止并发/重复点击再次插入多条 - 同步远端删除: - 在“上游检测成功”时同步该上游的 SmartUp Key 状态 - 拉取远端 Key 列表后,如果本地记录的 key_id 在远端不存在,则删除本地记录 - 如果上游分组本身在最新检测快照里不存在,也删除该分组对应的本地 SmartUp Key 记录 - 检测时间一致: - UpstreamGeneratedKey 增加 updated_at 或 last_checked_at - 每次上游检测成功并同步 Key 后,把相关 Key 的更新时间设置为本次快照 captured_at - 详情抽屉展示这个更新时间,语义为“随上游检测同步于” - 前端防重复点击: - 生成 Key 按钮在 generatingKeys 时禁用,避免连续点击 - 创建结果区对“已存在,已复用”和“新创建”做区分 - 文案改为“确保每个分组有一个 SmartUp Key”,不再暗示每次都新建 - 上游列表操作按钮: - 仍保留“创建分组 Key” - 点击后执行“确保存在/同步”语义,不执行盲目创建 ## API / Client Changes - UpstreamClient 增加: - list_api_keys(search, group_id, status, endpoint="/keys") - delete_api_key(key_id, endpoint="/keys/{id}") 可选备用 - find_smartup_group_key(group_id, key_name, prefix) - POST /api/upstreams/{id}/keys/generate-by-groups 行为变更: - 从“总是创建”改为“存在则复用,不存在才创建” - 返回 item status: - created:本次新建 - exists:远端已存在,本地已同步 - failed:失败 - GET /api/upstreams/{id}/generated-keys: - 只返回当前本地有效记录 - 不再返回远端已删除或分组已不存在的残留记录 ## Test Plan - 单测:重复创建 - 同一上游同一分组连续调用两次 generate - 预期远端 create 只调用一次 - 本地 upstream_generated_keys 只有一条 - 第二次返回 exists - 单测:历史重复清理 - 预置同一 upstream_id/group_id/key_name 多条记录 - 运行迁移/清理函数 - 预期只保留最新一条 - 单测:远端删除同步 - 本地有 Key,远端 GET /keys 不返回该 key_id - 运行上游检测同步 - 预期本地记录被删除 - 单测:分组删除同步 - 最新分组快照不包含某 group_id - 预期该 group_id 的 SmartUp Key 本地记录被删除 - 单测:检测时间同步 - 上游检测成功写入 snapshot captured_at - 预期相关 Key 的 updated_at/last_checked_at 等于该检测时间 - 前端验证: - 快速多次点击创建按钮不会产生多条 - 已存在 Key 时显示“已存在,已同步” - 详情抽屉不再显示远端已删除的 Key - 回归: - npm --prefix frontend run build - env PYTHONPATH=backend backend/venv/bin/pytest backend/test_upstream_key_account_import.py backend/test_upstream.py -v ## Assumptions - 只管理 SmartUp 或用户在弹窗中填写的前缀创建的 Key,不清理用户手动创建的普通 Key。 - “每个上游分组只能有一个 SmartUp 前缀 Key”按 upstream_id + group_id + stable key_name 判断。 - 远端 Sub2API Key 列表接口为 GET /api/v1/keys,删除接口为 DELETE /api/v1/keys/{id}。 - 远端 Key 被删除后,本地应物理删除,不保留 deleted 状态记录。