31 KiB
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。
- 修复 props.active watcher:
-
系统页高亮按钮统一方案
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 按钮。
- Websites.vue:
-
优化 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。
- 调整 Makefile:
-
优化网站分组接口错误提示
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 细节只保留在日志中。
- 在 backend/app/services/website_client.py 增加错误格式化:
-
修复网站管理编辑入口不明显
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 状态记录。
- 后端 Key 命名规则改为稳定名称: