Files
SmartUp/REASONIX.md
T
2026-05-29 17:51:12 +08:00

541 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 watcheractive=false 时关闭了 sessionactive=true 时只在 remoteSession 仍存在时重连 WebSocketremoteSession=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 --buildDockerfile 里又有 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
- 调整网站列表操作列:
- 把“编辑”移到第一个
- 改成文字按钮:编辑
- 可带图标:<Edit /> 编辑
- 其他操作继续 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 状态记录。