From 629f1cd74e47160513653be3aa810c0e72d55c2d Mon Sep 17 00:00:00 2001 From: xiaosheng <73678111+xiaoshengbao@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:49:41 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E5=9B=BA=E5=AE=9A=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=E6=A3=80=E7=B4=A2=E6=B6=88=E6=81=AF=E6=80=BB=E9=87=8F?= =?UTF-8?q?=E5=B9=B6=E5=AE=8C=E5=96=84=20AI=20=E8=BF=87=E7=A8=8B=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design-qa.md | 137 ++++++++++++ docs/local-semantic-search.md | 4 + frontend/assets/css/agent.css | 41 ++-- frontend/components/LocalSearchSettings.vue | 107 +++++++-- frontend/components/chat/AgentRun.vue | 48 +---- frontend/components/chat/AgentSubtasks.vue | 4 +- frontend/components/chat/AgentToolCall.vue | 37 +++- frontend/components/chat/ChatAgentPanel.vue | 10 +- frontend/tests/agent-timeline.test.js | 45 ++-- frontend/tests/agent-tool-errors.test.js | 27 +++ frontend/tests/chat-agent.test.js | 36 ++++ .../fixtures/AgentModelPickerPreview.vue | 2 +- .../tests/fixtures/AgentProgressPreview.vue | 12 ++ .../tests/fixtures/LocalSearchPreview.vue | 11 +- frontend/tests/fixtures/local-search.js | 18 +- frontend/tests/local-search.test.js | 119 +++++++++- src/wechat_decrypt_tool/ai/deep_runtime.py | 9 +- .../local_search/frozen.py | 109 ++++++++++ .../local_search/progressive.py | 4 +- .../local_search/service.py | 44 +++- .../local_search/totals.py | 75 +++++++ tests/test_ai_deepagents.py | 8 +- tests/test_local_search.py | 9 +- tests/test_local_search_totals.py | 203 ++++++++++++++++++ 24 files changed, 962 insertions(+), 157 deletions(-) create mode 100644 src/wechat_decrypt_tool/local_search/frozen.py create mode 100644 src/wechat_decrypt_tool/local_search/totals.py create mode 100644 tests/test_local_search_totals.py diff --git a/design-qa.md b/design-qa.md index ff280245..fc8b41d1 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,3 +1,110 @@ +# 本地检索固定总量验收 · 2026-09-15 + +- final result: passed +- 本节为最新验收;下方记录保留历史方案,原「预计总量、完成校准」已由固定本轮清单取代。 +- source visual truth path: `output/local-search-progress/total-desktop.png`(上一版面板)。 +- implementation screenshot path: `output/local-search-progress/fixed-total.png`、`fixed-counting.png`、`fixed-narrow-dark.png`(均在同目录)。 +- viewport: 1280×720 CSS px;比较截图均为 1280×720 像素,未额外缩放。宽容器 1000px,窄容器 480px。 +- state: 整理中、统计中;浅色宽屏、深色窄屏;正式组件使用明确标注的虚构数据。 +- full-view comparison evidence: 在同一输入中查看上一版总量面板、新固定总量面板和统计阶段截图;保留阶段、进度条、四项数字与处理详情的层级。 +- focused region comparison evidence: 完整截图中统计区域清晰可读,无需裁切;窄屏另行检查两列数字、分母和提示文字,无截断。 + +## 五项核验 + +- 字体:主数字和次要分母维持原有字号、字重;「本轮总量」取代「预计总量」。 +- 间距:宽屏四列、窄屏两列;保存进度与固定分母同组,长提示自然换行。 +- 颜色:沿用绿色保存进度和蓝色累计索引,深色数字清晰。 +- 图像:无新增位图,沿用现有状态图标和原生进度条。 +- 文案:统计时明确说明统计完成后开始整理;不显示中途增长的统计值或虚假百分比。整理时说明本轮固定、新消息下一轮。 + +## 行为与验证 + +- 先分页准备完整消息清单、去重并确定总量,然后开始模型整理;进度条按已保存消息 / 固定总量计算。 +- 暂停、重启沿用同一清单;源消息删除、新增或补入旧消息不会改变已冻结的总量。未完成任务不因后台发现新会话而扩大范围。 +- 清单暂存在本地 SQLite,完成后清理;首次统计增加等待时间和临时磁盘占用。清单丢失时明确报错,保留已有索引,不静默重算分母。 +- 83 项后端测试、54 项前端测试通过;覆盖完整准备先于编码、固定总量、源数据变动、暂停续接、统计失败重试、零值及旧任务兼容。 +- 浏览器 console error 为空;机械扫描无发现;目标文件 diff 检查通过。最终无 P0/P1/P2 视觉问题。 +- 当前桌面后端尚未重启,新后端逻辑需重启项目后加载。验收使用测试与隔离预览,未对真实聊天触发额外整理。 + +final result: passed + +--- + +# 本地检索消息总量补充验收 · 2026-09-15 + +- final result: passed +- source visual truth path: `output/local-search-progress/desktop.png`(已接受的面板),本轮要求在已读取消息旁增加总量。 +- implementation screenshot path: `output/local-search-progress/total-desktop.png`、`output/local-search-progress/total-narrow.png`。 +- viewport: 1280×720 CSS px;源与实现截图均为 1280×720 像素,工具已归一为 CSS 像素,未额外缩放。 +- state: 浅色、整理中、虚构数据;宽容器 1000px,窄容器 480px。 +- full-view comparison evidence: 同一输入并列查看原面板、新宽屏、新窄屏三张截图。所有主体布局保留,首项增加小字号分母 58,735,显示为 26,286 / 58,735。 +- focused region comparison evidence: 数字、标签与分母在完整截图中均可读,未额外裁切。 + +## 五项核验 + +- 字体:现有 24px 主数字保留;分母 14px、普通字重,清晰区分当前数与总量。 +- 间距:分母同行基线对齐,宽度不足可换行;窄窗口各统计项 clientWidth / scrollWidth 均为 182px,无溢出。 +- 颜色:分母使用现有次要文字令牌,继承深色主题;绿色与蓝色统计保持原样。 +- 图像:未新增图片或替换图标。 +- 文案:标明「预计总量」,本轮范围与去重校准规则放在处理详情;未知值不伪造分母。 + +## 证据与边界 + +- 浏览器无 console error;宽窄截图无 P0/P1/P2 问题,无需视觉修正。 +- 52 项前端测试通过;77 项后端测试通过。覆盖当前任务绑定、零值、未知/失效总量、范围合并、取消、失败降级、完成校准和禁止正文回退扫描。 +- 机械扫描无发现,修改文件 diff 检查通过。 +- 后端统计为新代码;现有运行服务需重启后加载。本轮未重启正在运行的桌面服务,也未对真实聊天触发额外索引。 + +final result: passed + +--- + +# 本地语义检索进度面板验收 · 2026-09-15 + +- final result: passed +- source visual truth path: `C:/Users/123/AppData/Local/Temp/codex-clipboard-6525eb58-700d-4983-a2a4-2eb62d433f8a.png` +- 改造对象参考:`C:/Users/123/AppData/Local/Temp/codex-clipboard-271309b8-7457-4ccd-8051-81f66f0c1b58.png` +- implementation screenshot path: `output/local-search-progress/desktop.png` +- 补充截图:`output/local-search-progress/narrow-dark.png`、`initial.png`、`error.png`(均在同目录)。 +- 本地预览:`http://127.0.0.1:4173/tests/fixtures/local-search.html?progress=running` +- 预览挂载正式 LocalSearchSettings 组件,数据为明确标注的虚构快照;未操作真实索引。 + +## 对比依据与范围 + +图一是语音任务的样式参考,图二是语义检索的改造对象,并非要求复制语音转换页面。参考图一 1800×1200、图二 1474×1104;最终浏览器截图 1280×720(工具输出按 CSS 像素归一)。浏览器视口 1280×720 CSS px;宽容器约 1000px,窄窗口容器 480px,状态卡约 417px。未拉伸图像,未把原图系统缩放或不同内容产生的尺寸差认定为视觉缺陷。 + +已在同一图像比较输入中打开图一和 desktop.png,重点对比进度区的结构:阶段与状态标签、通栏绿色进度条、数字优先的四列统计、下方辅助说明。两张完整截图中的进度区均能直接读清,不需要额外裁切。语义检索继续使用自己的消息/片段/索引统计,未引入语音任务的失败数或成功数。 + +## 五项视觉核验 + +- 字体:保留现有系统中文字体;阶段 14px,主要数字最高 24px、600 字重,启用等宽数字。标签与说明降低层级,数字不再混在整句中。 +- 布局:白色面板、12px 圆角、20px 内边距;四列统计平铺,累计索引有细分隔线。容器小于 520px 改为两列,DOM 检查各项 scrollWidth 等于 clientWidth,无横向溢出。 +- 颜色:沿用产品绿色。绿色表示已保存,蓝色区分累计索引,错误用明确图标与文字表达。深色数字使用 #64d49c 和 #9aafd7。 +- 图标与图像:使用项目已有 Font Awesome 图标,无新增位图或占位图。参考中的纯 UI 元素用原生 progress 和组件样式实现。 +- 内容:百分比按任务已完成会话时间段计算,未知总数为不定进度。实时生成与事务保存数量分开;运行中可见暂停入口;批量、复用、索引模式移至处理详情,错误和警告保持展开。 + +## 检查历史 + +1. 首轮宽屏对比通过;窄窗口深色截图发现 [P2] 统计绿色/蓝色过暗,scoped CSS 的全局主题选择器未命中状态卡。 +2. 修正完整全局选择器;再次截图 `narrow-dark.png`,确认绿色变量为 #64d49c,绿色/蓝色数字清晰;两列无截断。无其他 P0/P1/P2 问题。 + +## 交互与验证 + +- 浏览器检查详情展开/收起、暂停、首次零进度、错误提示、深浅色、窄窗口。 +- 暂停后实际显示已保存片段 4,180,替换生成中片段 4,260;保留 43% 进度并提供继续整理入口。 +- 浏览器 console error 列表为空。 +- `vitest run tests/local-search.test.js --root frontend`:46 项通过,覆盖进度口径、未知/空范围、零值到完成、错误外显和原有事件/暂停语义。 +- Impeccable 机械扫描无发现;目标文件 `git diff --check` 通过。 +- 验证边界:组件与虚构任务状态验证;未重新运行真实模型或索引流程,后端未修改。 + +## 结论 + +final result: passed + +--- + +以下保留此前任务的验收记录: + # AI 助手改版验收 - 日期:2026-09-08 @@ -471,3 +578,33 @@ final result: passed - 浏览器已检查强度和列表两态,回到强度态保留预览。10 项既有组件回归通过,git diff --check 通过。前端热更新已生效,无后端改动。 final result: passed + +## 2026-09-15 读取小节点详情面板 + +- 范围:AgentToolCall 的逐次读取节点;保留独立运行详情入口和已有执行过程结构。 +- 视觉来源:C:/Users/123/AppData/Local/Temp/codex-clipboard-1a0a8fe1-af94-4454-999b-6998fb7bc653.png(979×270),C:/Users/123/AppData/Local/Temp/codex-clipboard-54baee6a-21f0-4a21-888f-5a96545bed0a.png(735×1048)。前者定义展开面板,后者定义挂载位置。 +- 实现截图:tmp/agent-node-details-20260915/light-expanded.png、dark-expanded.png。 +- 预览:http://127.0.0.1:3057/tests/fixtures/agent-progress.html?nodes=1;仅合成数据。 +- 视口:1280×720 CSS px,浏览器 DPR 1.75,截图工具输出 1265×712 px。参考图未提供缩放比例;按节点与面板内容区域比较结构与效果,不以原图字号像素推断应用字号。 +- 状态:10 次读取,首节点展开,其余折叠;另测第二节点键盘展开、运行态、浅色和深色主题。 +- 对比证据:命令详情参考与实现截图在同批图像输入中展示,随后将节点列表参考与同张实现截图同批展示;聚焦首节点、浅灰面板、输入参数、结果和右下角状态,宽面板及窄侧栏均清晰可读。 + +### Findings + +无待处理 P0/P1/P2。一次视觉比较通过;修复过一项旧测试对保存节点提示文字的断言,保留其原有提示。没有据截图追加视觉调整。 + +- 字体:沿用现有系统字体、12px 详情与 11px 状态;参数使用现有等宽字体回退。保持应用密度,不复制参考截图的放大比例。 +- 间距:每个小节点独立箭头,展开区域紧随该节点;6px 圆角、10px/12px 内边距,竖线随面板高度延伸。窄面板约 231 CSS px,无横向溢出,日期自动换行。 +- 颜色:复用 ag-soft、ag-muted 等明暗主题变量;浅灰背景,完成状态右对齐;失败状态保留文字和图标,运行态使用既有 shimmer。 +- 图片与图标:无新增位图需求,全部使用现有 Font Awesome 图标,未将参考中的命令内容或批注复制为图片。 +- 文案与数据:本次会话、查询条件、分页位置、返回数量和状态来自已有节点数据;显示零分页位置,不伪造 Shell 命令。运行详情结构保持不变。 + +### Implementation Checklist + +- 鼠标和 Enter 均可展开单个节点;节点互不干扰。 +- 进度更新保留展开选择;组件测试覆盖外部保存状态与本地状态,以及重新挂载。 +- 81 项相关测试已通过(22 项工具详情、26 项时间线、33 项聊天面板)。 +- 浏览器错误日志为空;改动文件 diff --check 通过。 +- 限制:浏览器使用合成数据,未调用真实模型。沿用现有全部结果文案与错误分支,未更改后端。 + +final result: passed diff --git a/docs/local-semantic-search.md b/docs/local-semantic-search.md index 852f48f6..b190a497 100644 --- a/docs/local-semantic-search.md +++ b/docs/local-semantic-search.md @@ -90,6 +90,10 @@ GPU 进程故障、超时、缺库或显存不足时销毁故障进程,在独 - 默认自动批量(`read_batch_size=0`)。总内存不超过 4 GiB 或可用内存不足 1 GiB 时最多 100 条;总内存不超过 8 GiB 或可用内存不足 3 GiB 时最多 500 条;其余情况自动使用 1,000 条。高级设置可选 100/500/1,000/2,000 条,手动选择仍受内存保护,每批重新检查。不依赖 NVIDIA 显卡是否存在。 - 单批累计文本达到 128,000 字符时提前处理,避免少量长文本积累过多片段;保留完整单条消息,因此此阈值不是进程内存的硬上限。推理仍按最多 8 个片段执行,不随读取批量扩大显存需求。低内存策略通过模拟测试,其他硬件、系统和磁盘速度仍需实机验证。 - 读取数量约每 250 ms(有新消息时)上报一次,页尾立即上报;SSE 每秒传递事件,1.5 秒轮询兜底。界面直接更新读取数、片段数、实际批量和已保存进度,较旧事件或轮询响应不得让进度倒退。设备状态不变时不重复写入相同事件。 +- 整理采用固定轮次:先显示「正在统计消息总量」,按任务开始时的聊天目录、截止时间和本轮范围准备完整清单并去重,再显示「已读取 / 本轮总量」。统计中的临时数量不会作为分母展示;统计失败则保留断点并等待重试,不提前进入推理。 +- 本轮总量发布后不再增长、隐藏或在完成时校准,进度条按「已保存消息数 / 本轮总量」计算。后台发现的新会话不会打断未完成轮次;统计完成后收到或补入的消息留到下一轮。 +- 为避免统计与实际读取之间的数据变化,准备清单时会分页暂存本轮消息,之后直接从本地暂存数据整理,不再第二次读取微信正文。内存按分页条数和字符数限制;首次整理需等待全部统计完成,并临时占用额外磁盘空间。暂存文件位于本地检索数据目录的 `plans/` 下,按账号与任务隔离。 +- 暂停或重启保留统计断点及已固定的消息清单;旧任务迁移时将已保存数量纳入本轮总量。完成后自动清理临时清单,调整配置、清理索引或删除账号时也会清理。已固定清单丢失时提示从头整理,不静默重新统计成另一个总量。 - 最小化继续处理;退出停止。重启恢复此前运行任务,主动暂停保持暂停。数据和断点仍在同一事务提交,读取中的数量不用于恢复;失败或暂停只保留已提交批次,大批写入期间每 100 条检查取消。恢复时会重读尚未保存的一批。 - `members(source,chunk)` 索引保证按原消息查找关联片段时不扫描整张关联表,适用于新建及已有索引。批量去重和关联查询在后台线程执行。 - 关键词及向量分别最多召回 200 条,按来源去重、RRF(常数 60)融合。查询票据固定账号、权限版本、范围及候选顺序,有效十分钟,最多缓存 32 次查询。检索文本返回前再次按原消息权限和过滤条件约束,不返回片段内越权相邻原文。 diff --git a/frontend/assets/css/agent.css b/frontend/assets/css/agent.css index 1c3bb7d5..b9466838 100644 --- a/frontend/assets/css/agent.css +++ b/frontend/assets/css/agent.css @@ -166,11 +166,25 @@ html[data-theme="dark"] .agent-thread-management .is-destructive, html[data-them .agent-tool-timeline .agent-tool-attempt { position:relative; padding:0 0 8px 20px; margin:0; } .agent-tool-timeline .agent-tool-attempt:not(:last-child)::before { content:''; position:absolute; left:0; top:10px; bottom:-10px; width:1px; background:color-mix(in srgb,var(--ag-muted) 30%,transparent); } .agent-tool-timeline .agent-tool-attempt:last-child { padding-bottom:0; } -.agent-tool-attempt-row { display:flex; align-items:baseline; flex-wrap:wrap; gap:2px 18px; list-style:none; cursor:pointer; min-height:24px; line-height:1.8; } +.agent-tool-attempt-row { display:flex; align-items:baseline; flex-wrap:wrap; gap:2px 12px; list-style:none; cursor:pointer; min-height:28px; line-height:1.8; border-radius:4px; } .agent-tool-attempt-row strong { font-weight:400; min-width:72px; } .agent-tool-attempt-row .agent-tool-node { position:absolute; top:8px; left:-4px; font-size:6px; color:color-mix(in srgb,var(--ag-muted) 55%,var(--ag-bg)); } .agent-tool-inspection[open] > summary { margin-bottom:8px; } .agent-tool-inspection[open] { padding-bottom:4px; } +/* 小节点下方展示本次操作,浅色面板与右下角状态呼应命令详情的阅读方式。 */ +.agent-tool-attempt-row:hover { color:var(--app-text-primary); } +.agent-attempt-chevron { margin-left:auto; flex:none; font-size:10px; align-self:center; transition:transform .15s ease; } +.agent-tool-inspection[open] > .agent-tool-attempt-row .agent-attempt-chevron { transform:rotate(90deg); } +.agent-tool-inspection-panel { min-width:0; padding:10px 12px; border-radius:6px; background:var(--ag-soft); color:var(--ag-muted); line-height:1.75; } +.agent-tool-inspection-panel > header { margin:0 0 8px; } +.agent-tool-inspection-panel > header strong { color:inherit; font-weight:400; } +.agent-tool-request { font-family:var(--font-mono,ui-monospace,SFMono-Regular,Consolas,monospace); font-size:12px; } +.agent-tool-request:empty,.agent-tool-response:empty { display:none; } +.agent-tool-request + .agent-tool-response:not(:empty) { margin-top:10px; } +.agent-tool-inspection-status { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:12px; font-size:11px; font-variant-numeric:tabular-nums; } +.agent-tool-inspection-status > span:last-child { display:inline-flex; align-items:center; gap:5px; } +.agent-tool-inspection-panel.is-failed .agent-tool-inspection-status > span:last-child { color:var(--danger-color,#b95039); } +@media(prefers-reduced-motion:reduce) { .agent-attempt-chevron { transition:none; } } .agent-progress-note { line-height:1.7; margin:16px 0 8px; color:var(--app-text-primary,#25352d); } .agent-progress-note p { margin:0; } .agent-progress-note small, .agent-progress-note.is-superseded { color:var(--ag-muted); font-size:10px; } @@ -194,8 +208,8 @@ html[data-theme="dark"] .agent-thread-management .is-destructive, html[data-them .agent-error { font-size:11px; color:#b95039; background:#b9503910; padding:9px 12px; border-radius:7px; margin:10px 16px; } .agent-run .agent-error { margin:10px 0; } .agent-composer { position:relative; flex-shrink:0; padding:6px 16px 10px; } -.agent-input-box { border:1px solid var(--ag-border); border-radius:24px; padding:10px; background:var(--ag-bg); box-shadow:0 2px 8px #00000003; } -.agent-input-box:focus-within { border-color:var(--ag-muted); box-shadow:0 0 0 1px var(--ag-border); } +.agent-input-box { border:1px solid var(--ag-border); border-radius:12px; padding:10px; background:var(--ag-bg); box-shadow:0 2px 8px #00000003; } +.agent-input-box:focus-within { border-color:color-mix(in srgb,var(--ag-muted) 45%,var(--ag-border)); } .agent-input-box textarea { display:block; width:100%; min-height:28px; max-height:144px; resize:none; overflow-y:auto; border:0; outline:0; font:inherit; font-size:13px; line-height:1.6; background:transparent; color:inherit; padding:0; box-sizing:border-box; } .agent-input-box textarea::placeholder { color:var(--ag-muted); } .agent-input-actions { display:flex; gap:4px; align-items:center; margin-top:10px; min-width:0; } @@ -205,7 +219,6 @@ html[data-theme="dark"] .agent-thread-management .is-destructive, html[data-them .agent-input-actions > button { flex-shrink:0; width:28px; height:28px; padding:0; color:var(--ag-muted); font-size:12px; } .agent-input-actions .agent-send { margin-left:auto; color:#fff; background:#171717; width:28px; height:28px; border-radius:50%; padding:3px; font-size:14px; } .agent-input-actions .agent-send:hover { background:var(--app-text-secondary,#5f5f5f); } -.agent-disclaimer { display:block; text-align:center; color:var(--ag-muted); font-size:10px; margin-top:6px; } .agent-composer-settings { position:absolute; bottom:calc(100% - 4px); left:12px; right:12px; padding:12px; background:var(--ag-bg); border:1px solid var(--ag-border); border-radius:10px; box-shadow:0 -6px 28px #0001; z-index:5; font-size:12px; } .agent-composer-settings header { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; } .agent-composer-settings label { display:block; color:var(--ag-muted); } @@ -274,7 +287,10 @@ html[data-theme=dark] .agent-panel .agent-composer-settings > button { color:#70 html[data-theme=dark] .agent-panel .agent-markdown .agent-ref[aria-expanded=true] { color:#fff; background:#087c44; } /* 执行记录沿用正文阅读轴;当前状态独立显示,折叠历史时也能看到进展。 */ -.agent-process-toggle .agent-shimmer { animation:none; background:none; color:inherit; -webkit-text-fill-color:currentColor; } +/* 用文字扫光表示 AI 正在处理,减少持续旋转图标的干扰。 */ +.agent-shimmer { background:linear-gradient(100deg,currentColor 35%,color-mix(in srgb,currentColor 25%,var(--ag-bg)) 50%,currentColor 65%); background-size:250% 100%; background-clip:text; -webkit-background-clip:text; -webkit-text-fill-color:transparent; animation:agent-shimmer 2s linear infinite; } +@keyframes agent-shimmer { from { background-position:100% 0; } to { background-position:0 0; } } +@media(prefers-reduced-motion:reduce), (forced-colors:active) { .agent-shimmer { animation:none; background:none; -webkit-text-fill-color:currentColor; } } .agent-stream-status { display:flex; align-items:center; gap:8px; margin:14px 0 18px; color:var(--ag-muted); font-size:13px; } .agent-stream-status time { font-size:10px; opacity:.7; font-variant-numeric:tabular-nums; } .agent-status-dot { width:6px; height:6px; flex:none; border-radius:50%; background:currentColor; animation:agent-status-pulse 1.6s ease-in-out infinite; } @@ -325,13 +341,11 @@ html[data-theme=dark] .agent-panel .agent-markdown .agent-ref[aria-expanded=true .agent-process-body { margin:6px 0 0 7px; border-left:1px solid var(--ag-border); padding:2px 0 4px 10px; background:transparent; } .agent-process-body.is-collapsed { margin:2px 0 0; padding:0; border:0; } .agent-live-step { display:flex; align-items:baseline; gap:10px; margin:6px 8px; padding:5px 0; color:var(--ag-muted); font-size:12px; line-height:1.65; } -.agent-live-step > i { width:14px; flex:none; color:#079b57; } .agent-live-step > div { min-width:0; } .agent-live-caption { font-size:11px; margin-right:8px; color:var(--ag-muted); } .agent-live-step .agent-stream-status { display:inline; margin:0; color:var(--app-text-primary); font-size:13px; overflow-wrap:anywhere; } .agent-live-hint { margin:3px 0 0; font-size:12px; } .agent-live-step > time { flex:none; font-size:11px; font-variant-numeric:tabular-nums; } -html[data-theme=dark] .agent-live-step > i { color:#63d99a; } .agent-process-body .agent-tool { margin:2px 0; padding:0 8px; font-size:13px; } .agent-process-body .agent-tool-title { color:var(--ag-muted); } .agent-process-body .agent-tool[open] { background:var(--ag-bg); } @@ -348,26 +362,17 @@ html[data-theme=dark] .agent-live-step > i { color:#63d99a; } .agent-stage-row > span { overflow-wrap:anywhere; } .agent-stage-row > small { font-size:11px; font-variant-numeric:tabular-nums; } .agent-stage-row .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } -.agent-stage-row.is-running > i { color:#079b57; } .agent-process-body .agent-process:empty { display:none; } .agent-process-body:has(.agent-process:empty) .agent-run-metadata { margin-top:0; padding-top:0; border-top:0; } .agent-process-body .agent-run-metadata { margin:12px 8px 0; padding-top:10px; border-top:1px solid var(--ag-border); } -/* 摘要入口与实时进度分层,诊断信息按需展开。 */ +/* 执行过程底部直接显示 Token 用量,来源结果仍按需展开。 */ .agent-metadata-actions { display:flex; flex-wrap:wrap; align-items:center; gap:4px 20px; } .agent-metadata-actions > button { display:inline-flex; align-items:center; gap:8px; min-height:32px; padding:4px 0; margin:0; font-size:12px; color:var(--ag-muted); text-decoration:none; } .agent-metadata-actions > button i { flex:none; width:10px; text-align:center; font-size:10px; } .agent-metadata-actions > button:hover { color:var(--app-text-primary); } .agent-run-metadata button:focus-visible { outline:2px solid var(--ag-muted); outline-offset:3px; border-radius:4px; } .agent-coverage-summary { margin:0 0 4px; line-height:1.7; } -.agent-run-details { padding:12px 0 4px; line-height:1.7; overflow-wrap:anywhere; } -.agent-detail-facts { display:grid; gap:8px; margin:0; } -.agent-detail-facts > div { display:grid; grid-template-columns:5em minmax(0,1fr); gap:12px; align-items:baseline; } -.agent-detail-facts dt, .agent-detail-facts dd { margin:0; } -.agent-detail-facts dd { color:var(--app-text-primary); font-variant-numeric:tabular-nums; } -.agent-run-details .agent-usage { margin:12px 0 0; font-size:12px; } -.agent-run-details .agent-coverage-note { margin-top:8px; } -.agent-run-details .agent-index-note { margin-top:12px; } -.agent-run-details button { min-height:32px; padding:4px 6px; font-size:12px; text-decoration:underline; text-underline-offset:3px; } +.agent-run-metadata > .agent-usage { margin:6px 0; font-size:12px; font-variant-numeric:tabular-nums; } .agent-answer-heading { margin:0 0 12px; font-size:14px; line-height:1.5; font-weight:600; color:var(--app-text-primary); } .agent-panel .agent-final-answer .agent-markdown { font-size:15px; line-height:1.85; } @container (max-width:480px) { diff --git a/frontend/components/LocalSearchSettings.vue b/frontend/components/LocalSearchSettings.vue index 1885dd5c..19d3f6b3 100644 --- a/frontend/components/LocalSearchSettings.vue +++ b/frontend/components/LocalSearchSettings.vue @@ -7,23 +7,40 @@
请先在聊天页面选择账号。现在也可以下载模型,之后再开始整理。
-
-
-
{{ job.status==='done' ? completedTitle : stage(job) }}
- 用时 {{ elapsed(job) }} +
+
+
{{ job.status==='done' ? completedTitle : stage(job) }}
+
{{ actualDevice }}用时 {{ elapsed(job) }}
-

{{ job.status==='done' ? '本次检查' : '已读取' }} {{ job.read_count ?? job.processed ?? 0 }} 条消息 · {{ running ? '本次生成' : '本次已保存' }} {{ running ? (job.embedded_count ?? job.embedded ?? 0) : (job.embedded ?? 0) }} 个片段 · {{ actualDevice }}

-

当前索引:{{ indexStats.messages }} 条消息 · {{ indexStats.chunks }} 个片段

-

{{ indexMode(job.mode) }}

-

已复用 {{ job.unchanged }} 条未变化消息,无需重复生成片段。

-

内容没有变化,已复用现有搜索数据,无需重复生成片段。

-

已保存 {{ job.processed || 0 }} 条消息的进度

-

当前每批最多 {{ job.read_batch_size_effective }} 条 · 根据可用内存调整

-

可以离开这个页面,整理会在后台继续。

-

在聊天搜索中切换到「智能搜索」,或直接向 AI 助手提问。

-

请调整聊天或时间范围;只有图片等尚未提取文字的内容无法生成搜索片段。

-

{{ job.warning }}

{{ job.error }}

-
+
+ {{ progress.total ? (progress.unit==='消息' ? '已保存消息进度' : `${progress.unit}进度`) : job.status==='done' ? '本轮整理进度' : job.stage==='counting' ? '统计完成后显示总量' : '正在确认整理范围' }}{{ number(progress.completed) }} / {{ number(progress.total) }} 已完成 + {{ progress.percent === null ? '—' : `${progress.percent}%` }} +
+ +
+
{{ job.status==='done' ? '本次检查消息' : '已读取消息' }}
{{ number(job.read_count ?? job.processed ?? 0) }}/ {{ number(messageTotal.value) }}{{ messageTotal.value !== null ? '条消息' : messageTotal.hint }}
+
已保存进度
{{ number(job.processed ?? 0) }}条消息
+
{{ running ? '本次生成片段' : '本次已保存片段' }}
{{ number(running ? (job.embedded_count ?? job.embedded ?? 0) : (job.embedded ?? 0)) }}个片段
+
当前索引片段
{{ indexStats ? number(indexStats.chunks) : '—' }}{{ indexStats ? `覆盖 ${number(indexStats.messages)} 条消息` : '等待索引统计' }}
+
+

{{ job.warning }}

+

{{ job.error }}

+ +
+ 处理详情 +
+

{{ indexMode(job.mode) }}

+

已复用 {{ number(job.unchanged) }} 条未变化消息,无需重复生成片段。

+

内容没有变化,已复用现有搜索数据,无需重复生成片段。

+

已读取 {{ number(job.read_count ?? job.processed ?? 0) }} 条消息,已保存 {{ number(job.processed ?? 0) }} 条消息的进度。生成中的片段以保存后结果为准。

+

开始整理前先统计完整消息总量,本轮总量固定不变;新增消息留到下一轮。暂停或重启后继续使用同一总量。

+

当前每批最多 {{ number(job.read_batch_size_effective) }} 条 · 根据可用内存调整

+

进度按已保存的{{ progress.unit }}计算,不代表剩余用时。

+
+
@@ -217,6 +234,38 @@ const selectedModel=computed(()=>state.value.models?.find(m=>m.id===form.model)) const displayModel=computed(()=>selectedModel.value || state.value.models?.find(m=>m.recommended) || state.value.models?.[0]) const modelReady=computed(()=>!!selectedModel.value?.downloaded) const running=computed(()=>['running','queued'].includes(job.value?.status)) +const number=value=>Number(value ?? 0).toLocaleString('zh-CN') +const messageTotal=computed(()=>{ + const current=job.value, total=state.value.message_total + // 固定总量从统计快照读取,完成、暂停和实时读取事件都不能改写分母。 + if(total?.job_id===current?.id && total?.status==='ready' && total.fixed===true && typeof total.value==='number' && Number.isFinite(total.value) && total.value>=0){ + return {value:total.value} + } + if(current?.status==='done')return {value:current.processed ?? 0} + const status=total?.job_id===current?.id ? total?.status : null + return {value:null,hint:status==='unavailable' ? '总量统计未完成' : !running.value ? '总量未统计完' : '总量统计中…'} +}) +const progress=computed(()=>{ + const current=job.value, total=messageTotal.value.value + if(total!==null){ + const completed=Math.min(total,Math.max(0,Number(current?.processed) || 0)) + return {total,completed,unit:'消息',percent:total ? Math.floor(completed/total*100) : current?.status==='done' ? 100 : 0} + } + if(current?.stage==='counting')return {total:0,completed:0,unit:'消息',percent:null} + // 旧任务尚未迁移固定消息清单时,继续展示原来已有的会话进度。 + const segments=current?.segments?.length ?? current?.config?.usernames?.length ?? 0 + const completed=Math.min(segments,Math.max(0,Number(current?.chat_index) || 0)) + return {total:segments,completed,unit:current?.segments ? '会话时间段' : '聊天',percent:segments ? Math.floor(completed/segments*100) : null} +}) +const statusHint=computed(()=>{ + if(running.value && job.value?.stage==='counting')return '正在统计本轮消息总量,统计完成后开始整理。' + if(running.value)return '本轮范围已固定,新增消息留到下一轮;可以离开此页面。' + if(job.value?.status==='paused')return '进度已保存,可继续整理。' + if(job.value?.status==='error')return '已保存的进度会保留,可在下方重试整理。' + if(job.value?.status==='done' && hasSearchData.value)return '本轮整理已完成。在聊天搜索中切换到「智能搜索」,或直接向 AI 助手提问。' + if(job.value?.status==='done' && indexStats.value)return '请调整聊天或时间范围;只有图片等尚未提取文字的内容无法生成搜索片段。' + return '本轮整理已结束。' +}) const selectedChatNames=computed(()=>form.usernames.map(id=>chats.value.find(c=>c.username===id)?.name).filter(Boolean).slice(0,3).join('、')+(form.usernames.length>3?' 等':'')) const dialogTitle=computed(()=>({models:'选择检索模型',scope:'选择聊天',import:'离线导入'}[dialog.value] || '')) const invalidDates=computed(()=>period.value==='custom' && (!startDate.value || !endDate.value || startDate.value>endDate.value)) @@ -230,7 +279,7 @@ const actualDevice=computed(()=>state.value.device?.actual_device==='cuda' ? (st const bytes=n=>!n ? '0 B' : n<1024**2 ? `${(n/1024).toFixed(0)} KB` : n<1024**3 ? `${(n/1024**2).toFixed(1)} MB` : `${(n/1024**3).toFixed(2)} GB` const date=n=>n ? new Date(n*1000).toLocaleString() : '—' const elapsed=j=>`${Math.max(0,Math.floor((j.finished || (['paused','done','error'].includes(j.status) ? j.updated : now.value) || now.value)-j.started))} 秒` -const stage=j=>!j ? '未下载' : ({importing:'导入中',pausing:'正在暂停',queued:'等待整理',connecting:'连接中',downloading:'下载中',retry_wait:'等待重试',paused:'已暂停',verifying:'校验中',verified:'校验完成',loading:'测试模型',done:'已完成',error:'处理失败',reading:'读取聊天记录',organizing:'整理消息片段',embedding:'正在理解聊天内容',saving:'正在保存搜索数据',installing:'安装加速组件'}[j.status==='error' || j.status==='paused' ? j.status : j.stage] || '处理中') +const stage=j=>!j ? '未下载' : ({counting:'正在统计消息总量',importing:'导入中',pausing:'正在暂停',queued:'等待整理',connecting:'连接中',downloading:'下载中',retry_wait:'等待重试',paused:'已暂停',verifying:'校验中',verified:'校验完成',loading:'测试模型',done:'已完成',error:'处理失败',reading:'读取聊天记录',organizing:'整理消息片段',embedding:'正在理解聊天内容',saving:'正在保存搜索数据',installing:'安装加速组件'}[j.status==='error' || j.status==='paused' ? j.status : j.stage] || '处理中') const request=(path,options={},scoped=false)=>api.request(`/local-search${path}${scoped && account.value ? `${path.includes('?')?'&':'?'}account=${encodeURIComponent(account.value)}`:''}`,options) let refreshDone=Promise.resolve() let timer, loading=false, version=0, gpuVersion=0, needsReset=false, previousFocus, stopEvents @@ -381,10 +430,28 @@ onBeforeUnmount(()=>{clearInterval(timer);stopEvents?.()}) .lss-start p{font-size:11px}.lss-start strong{font-size:12px} .lss-start-actions{display:flex;gap:8px;flex-wrap:wrap;flex-shrink:0} @container (max-width:600px){.lss-start-actions button{flex:1}} -.lss-status{border:1px solid var(--app-border,#e7e9ed);border-radius:8px;background:var(--app-surface-soft,#f7f8fa);padding:14px 16px;margin-bottom:14px} -.lss-status-title{display:flex;align-items:center;gap:8px}.lss-status-title>i{color:#079b57} -.lss-live-count{font-variant-numeric:tabular-nums} +.lss-status{--lss-green:#07834b;--lss-blue:#526d9e;border:1px solid color-mix(in srgb,var(--app-border,#e7e9ed),#079b57 20%);border-radius:12px;background:var(--app-surface-bg,#fff);padding:20px;margin-bottom:16px;container-type:inline-size;font-variant-numeric:tabular-nums} +.lss-status-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap} +.lss-status-title{display:flex;align-items:center;gap:8px;font-size:14px;min-width:0}.lss-status-title>i{color:var(--lss-green);flex-shrink:0} +.lss-status-meta{display:flex;align-items:center;gap:12px;min-width:0;margin-left:auto}.lss-status-meta>.lss-note{white-space:nowrap} +.lss-device-badge{display:inline-flex;align-items:center;gap:6px;min-width:0;font-size:11px;padding:4px 9px;border-radius:6px;background:var(--app-accent-soft,#edf8f1);color:var(--lss-green);overflow-wrap:anywhere} +.lss-progress-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:20px 0 8px;color:var(--app-text-secondary,#687582);font-size:11px} +.lss-progress-heading strong{color:var(--lss-blue);font-size:16px;flex-shrink:0}.lss-progress-count{margin-left:10px} +.local-search-settings .lss-index-progress{display:block;width:100%;height:8px;appearance:none;border:0;border-radius:8px;overflow:hidden;background:var(--app-accent-soft,#e6f0eb);accent-color:#07bf63} +.lss-index-progress::-webkit-progress-bar{background:var(--app-accent-soft,#e6f0eb)}.lss-index-progress::-webkit-progress-value{background:#07bf63;border-radius:8px}.lss-index-progress::-moz-progress-bar{background:#07bf63;border-radius:8px} +.lss-index-progress:indeterminate{opacity:.6}.is-paused .lss-index-progress{accent-color:var(--app-text-secondary,#687582)}.is-paused .lss-index-progress::-webkit-progress-value{background:var(--app-text-secondary,#687582)}.is-paused .lss-index-progress::-moz-progress-bar{background:var(--app-text-secondary,#687582)} +.lss-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px;margin:24px 0} +.lss-metric{display:flex;flex-direction:column;align-items:center;text-align:center;min-width:0}.lss-metric dt{order:2;margin-top:5px;font-size:11px;font-weight:400;color:var(--app-text-secondary,#687582)} +.lss-metric dd{display:contents;margin:0}.lss-metric dd>strong{font-size:clamp(18px,3.4cqw,24px);font-weight:600;overflow-wrap:anywhere;max-width:100%}.lss-metric{font-size:24px;font-weight:600;line-height:1.3}.lss-metric dd>span{order:3;margin-top:5px;font-size:10px;font-weight:400;color:var(--app-text-secondary,#687582)} +.lss-metric-saved{color:var(--lss-green)}.lss-metric-index{color:var(--lss-blue);border-left:1px solid var(--app-border,#e7e9ed)} +.lss-read-total{display:flex;align-items:baseline;justify-content:center;flex-wrap:wrap;gap:4px}.lss-total-denominator{font-size:14px;font-weight:400;white-space:nowrap;color:var(--app-text-secondary,#687582)} +.lss-status-footer{display:flex;align-items:center;justify-content:space-between;gap:16px}.lss-status-footer>.lss-note{margin:0;font-size:11px}.lss-status-footer button{flex-shrink:0} +.lss-status-details{border-top:1px solid var(--app-border,#e7e9ed);margin-top:16px;padding-top:12px}.lss-status-details summary{display:flex;align-items:center;gap:8px;width:fit-content;min-height:28px;font-size:11px;color:var(--app-text-secondary,#687582);list-style:none}.lss-status-details summary::-webkit-details-marker{display:none}.lss-status-details summary>i{font-size:9px}.lss-status-details[open] summary>i{transform:rotate(180deg)}.lss-status-details summary:hover{color:var(--lss-green)}.lss-status-details>div{padding-top:4px} +.lss-status :is(.lss-error,.lss-status-warning){overflow-wrap:anywhere;margin-bottom:12px}.lss-status-warning{display:flex;align-items:baseline;gap:7px} .lss-status.is-error{border-color:var(--danger-color,#cb4b43)} +.lss-status.is-error .lss-status-title>i{color:var(--danger-color,#cb4b43)} +:global([data-theme=dark] .lss-status){--lss-green:#64d49c;--lss-blue:#9aafd7} +@container(max-width:520px){.lss-metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 12px}.lss-metric-index{border-left:0}.lss-progress-count{display:block;margin:4px 0 0}.lss-status-footer{align-items:flex-start;flex-wrap:wrap}.lss-status-footer button{margin-left:auto}.lss-metric{font-size:22px}} .lss-bottom-note{display:flex;align-items:flex-start;gap:7px;font-size:10px;color:var(--app-text-secondary,#687582);padding:12px 1px;line-height:1.7} .lss-bottom-note>i{margin-top:3px} .lss-advanced{border:1px solid var(--app-border,#e7e9ed);border-radius:9px;margin-top:14px;background:var(--app-surface-soft,#f7f7f7);overflow:hidden} diff --git a/frontend/components/chat/AgentRun.vue b/frontend/components/chat/AgentRun.vue index dd7ac4d5..984b39f7 100644 --- a/frontend/components/chat/AgentRun.vue +++ b/frontend/components/chat/AgentRun.vue @@ -18,42 +18,22 @@

{{ item.text }} · 第 {{ item.attempt }} 次尝试

旧答案已根据补充要求调整
-