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

31 KiB
Raw Blame History

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

    • 调整网站列表操作列:
      • 把“编辑”移到第一个
      • 改成文字按钮:编辑
      • 可带图标: 编辑
    • 其他操作继续 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 状态记录。