Commit Graph

112 Commits

Author SHA1 Message Date
caoqianming f12df1bd82 feat(admin): 后台目录导航 + 按模型/各用户用量时间筛选排序 + 存储分页 + 导出 PDF + bump 0.11.0
- 左侧目录(sticky,点击平滑滚动 + scrollspy 高亮,窄屏转横向 chip);各区 scroll-margin-top 避开顶栏
- 按模型 / 各用户用量拆为独立端点,带 range(all/7d/30d)+ sort(cost/tokens);
  各用户用量含零用量用户(时间条件放 JOIN ON,避免被 cutoff 挤掉)
- 存储分页(/v1/admin/storage/users);各用户用量分页;overview 瘦身为固定指标(runtime/tasks/users/总用量+近7d),独立表自管 range/sort/page
- 导出 PDF:客户端 window.print()(零依赖),填充隐藏报告 DOM + @media print 版式;列表取前 10
- 文档同步 DESIGN §7.3 / PROGRESS / RUN

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:44:47 +08:00
caoqianming ef611b0666 feat(admin): 角色化管理后台 + 分页各用户用量 + bump 0.9.0
- users 加 role 列(user/admin,migration 0009);make_require_admin 按 DB role gate(不进 JWT,改完即时生效)
- /v1/admin/overview 监控总览:runtime(并发/线程池/SSE/RSS)+ tasks + users + usage 总用量 + storage
- /v1/admin/usage/users 分页各用户 token 用量(全表 LEFT JOIN 含零用量,cost desc,稳定排序)
- /v1/me 返 role;登录/建用户响应带 role;main.py user role / user add --role;建用户弹框加角色下拉
- 独立页 web/static/admin.html + js/admin.js(阈值/热力色差、响应式、10s 轮询、独立翻页);dev SPA admin 才显"管理"入口
- 文档同步:DESIGN §7.3/§7.4、PROGRESS、RUN

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:02:20 +08:00
caoqianming 44be5753f7 fix(version): 版本号挪到右侧存储条最左,垂直居中 + bump 0.8.1
- dev.html: #app-version 从左栏 #rail-resources 移入 #storage-foot 最左,
  带细分隔线,垂直居中(align-items:center);左栏底部腾给后续按钮
- core.__version__ 0.8.0 → 0.8.1(纯 UI 位置微调,patch)
- PROGRESS 同步新位置描述

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:55:35 +08:00
caoqianming dd797a91e2 feat(version): 版本号单一事实源(core.__version__)+ web 左栏底部展示
- core/__init__.py 新增 __version__ = "0.8.0",作唯一来源
- web/app.py: FastAPI version 与 /healthz 返回都引它(不再写死两份)
- dev.html: 左栏「我的资源」技能按钮旁加 #app-version 小灰字(纯展示)
- main.js: boot 时无条件 fetch /healthz 填版本号(auth 豁免,embed/未登录皆可)
- 放左栏底部而非顶栏:embed 模式桌面端 header 被 CSS 隐藏,顶栏点不到
- CLAUDE.md「文档维护」加规矩:每次 commit/push bump __version__(patch/minor/major 分类)
- RUN.md / PROGRESS.md 同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:00:30 +08:00
caoqianming 15d69b3372 feat(ops): 并发/线程池轻量监控 + 接管默认 executor
已上生产后线程池排队此前无观测手段:
- lifespan 显式建 ThreadPoolExecutor(尺寸复刻 Python 默认 min(32,cpu+4),
  env ZCBOT_RUN_MAX_WORKERS 可调) + set_default_executor 接管 —— 行为不变
  (匿名池换显式同尺寸池),但 max_workers 可读、成调并发的旋钮
- _stats_logger 每 60s 采样:active_runs(含排队)逼近 max_workers 即排队,
  刷新峰值/有负载打 [stats],空闲静默不刷屏
- broker.total_subscribers() 全局 SSE 订阅数;RSS 用 stdlib resource
  (Unix 峰值;Windows dev 降级),零新依赖

不做监控界面:运维健康是少数标量日志够,业务分析走 SQL。DESIGN 8.4 记
取舍 + 界面阶梯;无感换版(gunicorn/Redis/蓝绿)成本不抵当前收益,搁置。

查看: journalctl -u zcbot | grep [stats]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:09:44 +08:00
caoqianming f614046438 feat(web): 技能弹框平台/我的拆成独立两栏(三栏 master-detail)
平台 skill 多、我的是用户核心区,混一栏易被淹。改三栏:平台列表 |
我的列表 | 正文,我的获得与平台对等版面。modal 加宽到 1000px;窄屏
(<=760px)三栏自动上下堆叠。load_errors 归到「我的」栏底。纯前端。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:22:06 +08:00
caoqianming d89ebad272 feat(web): 技能按钮换扁平 SVG 图标 + 弹框改两栏 master-detail
- 左侧 rail「技能」按钮 emoji 🧩 换成扁平 inline SVG(2x2 grid,
  描边 currentColor 跟随主题);modal 标题同步。
- modal 从"列表→点开换正文+返回"改两栏:左栏平台/我的两组列表(可选中
  高亮),右栏展示选中 skill 完整 SKILL.md;删除按钮挪到右栏正文头部
  (只对我的),左列表保持干净只显名+描述。窄屏(<=640px)两栏自动改上下堆叠。
- 纯前端,后端/测试无影响。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 10:08:26 +08:00
caoqianming 958678aa12 feat(skills): 用户私有 skill(.skills)+ 创作工具 + skill-creator + Web 查看页
每用户可在私有 .skills/ 下造/改 skill,只对自己生效。

- SkillRegistry 改多来源(SkillSource 列表:内置 + 用户 .skills),后扫同名
  覆盖先扫 → user wins;user_overrides 记覆盖关系、discovery 显式标注;
  Skill 加 source;from_dir 区分"非 skill 目录(静默)"与"格式错(SkillLoadError)",
  坏的用户 skill 收进 load_errors 注入 prompt,不崩整次扫描。容器路径改写下沉
  到 registry.container_dir(按 source 给 /sandbox/skills 或 /workspace/.skills),
  LoadSkillTool 去掉 container_skills_dir 参数。
- 新增 host-side 工具 save_skill / fork_skill(tools/skill_authoring.py):
  fs 的 base_dir 锚 cwd/容器 wd 够不到 user_root/.skills,故用 host-side typed
  tool(与 seedream/document_* 同范式)。save_skill 写时校验 frontmatter;
  fork_skill copytree 整目录(带脚本)+ 自动对齐 frontmatter name。
- 新增 skill-creator 引导 skill(重点教写好 description + fork 语义)。
- Web:左侧 rail 底部「技能」按钮 → modal 分平台/我的两组,点开看完整
  SKILL.md,我的可删;后端加 GET /v1/skills/{name}(正文)+ DELETE
  /v1/skills/{name}(只删 user 源 + 防穿越);/v1/skills 带 source/overrides/
  load_errors;新 web/static/js/skills.js。创建/改/fork 仍走对话。
- .skills 是 dotfile(文件面板隐藏,与 .memory 一致;validate_task_name 已禁
  . 起头 working_dir,天然不撞)。
- 测试:test_user_skills.py(20 例)+ 改写 test_load_skill.py;全 121 过。
- 文档:DESIGN §3.5 / PROGRESS / RUN(布局+端点)/ SKILL_LIST 同步。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 09:46:39 +08:00
caoqianming e45705c672 docs(auth): 邮箱密码定为长期保留,OIDC 降级为选做 + 拆出 CORS 收紧
已接入真实用户且邮箱密码长期保留,故:
- DESIGN §7.0/§7.3/§7.7 三处「邮箱密码同步下线」改「与 OIDC 并存长期保留」
- 原「真 OIDC 发布前必做」拆成:CORS 收紧(现做)+ OIDC(选做,信任模型可接受则延后)
- PROGRESS 下一步候选 #1 同步拆分;auth.py docstring 同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:47:27 +08:00
caoqianming 815aeb81a9 feat(web): dev SPA 加 9 处克制入场微动效(纯 CSS + 一处极小 JS)
保留现有红主色 / VS Code 三栏审美不改风格,只补低风险微动效:
- 消息气泡 .msg 淡入+上滑(批量加载退化为柔和集体淡入)
- 4 个 .modal 卡片 scale 弹入 + 遮罩淡入
- 全局 button:active 下压 1px
- 进度 dock / 上传 toast 顶部滑下淡入
- 下拉操作菜单 #floating-menu 从右上锚点弹出
- 拖拽 overlay #file-droparea 快淡入
- 拖拽文件放下 → 落点 pane-right 一次 drop-pulse 轻回弹(files.js
  加 .drop-pulse + animationend once 自摘 + reflow 保证可重放)
- 全部纳入 prefers-reduced-motion 守卫(spinner/blink 等功能动画保留)

刻意未做:进度块「打勾」逐步动画(dock.innerHTML 每 tick 全量重渲染,
keyframe 会逐 tick 重放);复制 ✓ 闪(当前 SPA 无剪贴板复制功能,无触发点)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:15:21 +08:00
caoqianming 4f6e879050 feat(web): systemctl restart 优雅 drain in-flight run,不再误标 error
此前 restart 硬杀 BG run 线程,下次启动 reaper 把所有 running/cancelling
标 error: server restarted before run finished —— 用户一多就不能随便重启。

单实例止血,零 DB 改动:
- lifespan 加 draining(Event) + inflight 登记表(顺手修 create_task 不留引用
  可能被 GC 的旧坑);finally 先拒新 run → await 收尾 → 超 drain_timeout 转
  协作式 cancel(= 用户按停止,标 idle 不报 error、可重发)→ 超 cancel_grace
  仍没退的留给 SIGKILL(最坏退化 = 改前)
- POST /messages:draining 期返 503 + Retry-After;起 run 登记 inflight
- main.py uvicorn 加 timeout_graceful_shutdown=5(否则长连 SSE 挡在 drain 前)
- config/agent.yaml 加 shutdown 段(drain 30s / grace 15s,偏短更安全)
- dev SPA chat.js 发送包退避重试(503 背压 + 交接拒连都重试 ~26s)

部署强耦合:unit TimeoutStopSec 10→90(必须 > drain+grace+sandbox 清扫余量),
已写进 RUN.md unit + 故障兜底。B 蓝绿(零 503 窗口)留作触发信号后再做。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 10:54:43 +08:00
caoqianming c02d20b005 style(web): favicon 换成登录页 logo 样式(红底白 Z),与 brand 统一
原内联蓝色机器人 SVG 与登录卡/顶栏 brand(红色圆角方块 + 白 Z,
渐变 #c0392b → #8e2a20)不一致;改为同款,浏览器标签页图标对齐品牌。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:32:20 +08:00
caoqianming b70b993257 feat(preview): pptx 在线预览 —— LibreOffice→PDF + 复用 PDF iframe(DESIGN §8.3 Stage 1)
文件区点 .pptx 不再只能下载。后端转 PDF,前端复用现成 PDF iframe。

- web/pptx_render.py: pptx_to_pdf() 调 soffice,独立临时 profile 绕单 profile
  锁、60s 超时 kill;缓存 .preview/<stem>.<hash>.pdf(hash=mtime+size,源改即
  失效,prune 旧 hash);soffice 缺失抛 SofficeNotFoundError
- web/app.py: GET /v1/files/preview_pdf —— _safe_join 防穿越 + 仅 .ppt(x) +
  per-path asyncio.Lock 防并发重转 + run_in_executor 不堵事件循环;缺失 501/失败 500
- preview.js: ppt 组 + main/mini 共用 _showPptAsPdf(spinner loading + 失败回退下载)
- dev.html: .preview-spinner(复用 @keyframes spin)
- 转换跑 web host 进程不进沙盒;部署 host 装 libreoffice-impress + fonts-noto-cjk
  (sandbox Dockerfile 不动)
- tests/test_pptx_render.py: 10 例(缓存命中跳 soffice/源变失效+prune/缺失降级/越界拒绝)
- 文档:RUN.md(host 装 + 故障兜底 2 行)、PROGRESS.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:04:02 +08:00
caoqianming 824f746571 fix(progress): 停压 task_progress 参数修进度还原 + 进度区移到对话区顶部
问题1(进度不对): 上下文压缩把旧 task_progress tool_call 参数换成
{"_compacted":true,"step_id":"sX"} 这种像合法调用的标记, 既毒化模型让它
照抄出残废 update_step(丢 step.status)入库, 又让前端 applyProgressAction
读不到 args.step → 步骤永停 pending。修复: task_progress 参数一律不压缩。

问题2(没像 codex 在顶部): 删掉每条消息卡内联进度块, 进度统一只在对话区
顶部单一 dock 实时显示(钉顶不滚); 全部完成时折叠成一行摘要。prompt/tool
描述改为跑完标 completed 而非 clear, 留住全绿收尾。

校验: unittest test_context_compaction/test_task_progress_tool 12 过;
node --test frontend_task_progress 2 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:52:44 +08:00
caoqianming 2136fdd306 fix(web): 修登录无反应(newtask.js 漏 import $)+ 补 favicon
newtask.js 用了 dom.js 的 $ 简写却漏 import,模块加载即抛
ReferenceError: $ is not defined,中断 newtask 绑定及 auth/chat
链路 → 点登录无反应。补 import 与其余模块对齐。

另在 dev.html head 加内联 SVG favicon,消掉根 /favicon.ico 404。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:22:59 +08:00
caoqianming c898ff863d Disable static asset caching 2026-06-08 09:16:31 +08:00
caoqianming 4ee09976ee Show task progress above composer 2026-06-08 09:04:43 +08:00
caoqianming 8616ba2b56 Add task progress tool 2026-06-08 08:44:16 +08:00
caoqianming 15ecf45c93 refactor(dev): 前端模块化 Step 2 收官 — 抽出 chat.js,main 缩成 75 行入口
最后也最缠的一块:任务列表(浏览/筛选/滚动)+ selectTask 切换 +
renderChatMeta/模型下拉 + renderMessages + live-run 助手 + sendMessage/cancel +
fetchSse/handleSseEvent + 润色/粘贴文件 + 完成/废弃/删除/导出/清空
(原 main 连续区 64–1132)→ chat.js(1086 行)。

决策:合一个 chat.js 而非强拆 tasks.js+stream.js —— 二者共享
state.liveRuns + chat-stream DOM + run 生命周期,live-run 助手被
selectTask 与 SSE 机器两边调用、骑墙;强拆会制造双向各 ~4-5 个 import
且边界不自然。

- 导出 loadTaskList / loadModels / selectTask;embed/files/newtask 对这
  三个的 import 从 ./main.js 改指 ./chat.js;formatUploadProgress 加 export。
- chat 不调 enterApp → 与 main 无环。
- main.js 仅留 enterApp(编排)+ loadStorage + Esc 关栈 + boot = 75 行入口,
  import 精简到 11 行(layout/markdown/media 不再被 main 直接引用,但经
  chat 仍在依赖图、副作用照常)。

校验升级:node 全检 + import/export 一致性 + 从 main BFS 的模块可达性
(14/14 可达,确保副作用模块不掉出图)。

dev.html 4087 行单文件 → 14 个零构建 ES module + 纯 HTML;main 2719→75。
路径 1(拆文件)完成。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:09:53 +08:00
caoqianming 36dbdb2dda refactor(dev): 前端模块化 Step 2 — 抽出 embed.js(iframe 模式)
父页面经 postMessage 推 token 进入应用 + 401 重签(原 main 1147–1209 +
顶层 _embedInitialTaskHandled 一次性标志)→ embed.js(75 行)。

- 导出 embedInit(boot 调)+ embedPostToParent / embedShowWaiting
  (auth 的 logout 在 embed 下通知父页面/显示等待态)—— 后两个从 main
  迁出后,auth.js 对它们的 import 从 ./main.js 改指 ./embed.js
  (auth 仍从 main import enterApp)。
- 反向 import main glue enterApp / loadTaskList / selectTask。
  main↔embed、auth↔embed 均运行时调用环,安全。

main.js 删至 1154 行(2719 起,已搬出约 58%)。node 全检过、
import/export 一致性过、静态测试 2 过。剩 main 内:enterApp glue +
tasks(列表/选择/渲染消息)+ stream(发送/SSE)+ boot + Esc 关栈。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:35:01 +08:00
caoqianming 40fefdffef refactor(dev): 前端模块化 Step 2 — 抽出 newtask.js(新建任务弹框)
任务名 / 工作目录(新建 sentinel 或复用已有 + 二级 input 联动)/ 描述 /
skill / 模型 select,提交 POST /v1/tasks(原 main 1146–1320)→
newtask.js(186 行)。

- 顶层自绑 hd-new 打开 / nt-go 提交 / 各 input 联动;唯一对外导出
  loadFolderSuggestions(供 main enterApp 初始化顶部 filter-wd、files
  复制/移动后刷目录)—— 它从 main 迁来后,files.js 对它的 import 从
  ./main.js 改指 ./newtask.js。
- 反向 import main glue loadModels(加 export)/ loadTaskList / selectTask
  + logout(auth)。

main.js 删至 1220 行。node 全检过、import/export 一致性校验过、
私有符号清零。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:24:29 +08:00
caoqianming e5940266ca refactor(dev): 前端模块化 Step 2 — 抽出 media.js + 收敛 downloadFile 反向依赖
对话内工具活动标签 + artifact(产物)抽取/渲染:toolActivityLabel /
extractArtifactRels / extractMediaBanner / renderArtifactBarHtml /
upgradeMediaArtifacts / downloadFile → media.js(237 行,原 main 1134–1359)。

收敛点:downloadFile 移入 media 后,preview.js / files.js 对它的 import
从 ./main.js 改指 ./media.js —— 把这条反向依赖从 main 挪开。media 导入极少
(escapeHtml / _categorize(preview)/ state / logout),与 preview 成
media↔preview 环(均运行时调用,安全)。

两次险漏靠校验抓回:
- 共享 const ARTIFACT_PRODUCING_TOOLS(main renderMessages/SSE 用 4 处,
  .has() 访问非函数调用,"被调标识符"法漏掉)
- 内部函数 _flushMediaArtifactCache(selectTask 切任务清缓存用)
两者经残留符号检查发现后补 export。

新增全模块 import/export 一致性校验脚本(每个 import{X} 必在目标 export),
11 模块全过。main.js 删至 1393 行。node --check 11 模块全过、静态测试 2 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:05:48 +08:00
caoqianming 71bac870ed refactor(dev): 前端模块化 Step 2 — 抽出 files.js(文件面板 + 选入 + 拖拽上传)
右栏文件列表浏览/导航/删除/重命名 + 刷新 + "选入"弹框(跨目录勾选
复制/移动)+ 拖拽 overlay + 上传(XHR 带进度)+ 上传状态条。

代码原分散在 main.js 两段非连续区(1133–1459 文件列表/选入/拖拽 +
1697–1786 上传 helper,中间夹着 media 段)→ 合并进 files.js(433 行)。

- 导出 loadFiles / scheduleFilesRefresh(SSE 文件事件刷新)/
  closeSrcPicker(main Esc 关栈)/ uploadFiles(聊天区粘贴/拖拽复用);
  其余入口模块顶层自绑。
- 反向 import openFilePreview(preview)、logout(auth)、main glue
  downloadFile / selectTask / loadTaskList / loadFolderSuggestions
  (后三个加 export,后续随 tasks/newtask 模块化再迁)。
- 依赖分析用"段内被调标识符 − 段内定义 − 叶子/全局"全量提取,补回固定
  清单漏掉的 loadFolderSuggestions / loadTaskList。

main.js 删至 1619 行。node --check 双过、main 残留 files 私有符号清零、
files 无未导入 glue、静态测试 2 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:08:55 +08:00
caoqianming 9394e065f1 refactor(dev): 前端模块化 Step 2 — 抽出 preview.js(文件预览 + mini 预览)
文件预览主弹框(图/视频/PDF/文本/markdown/docx/xlsx,大文件降级下载,
docx/xlsx 走 loadScript 懒加载 vendor)+ 同时再开的小窗预览
(原 main.js 1687–2048)→ preview.js(379 行)。

- 导出 openFilePreview / openPasteFilePreview / closeFilePreview /
  closeMiniPreview / _categorize(媒体段判图/视频用)。
- 反向 import downloadFile(main 媒体段,加 export)、logout(auth)。
- Esc 关弹窗栈处理器留 main(跨模块协调 chpw/选入/文件预览/小预览)。
- 一处去耦:deletePastedFile(留 main)原直接读 preview 私有
  _fpCurrentRel/_mpCurrentRel 判断要不要关预览 → 改为 preview 导出封装
  closePreviewIfShowing(rel),行为不变但不泄漏内部状态(唯一非纯剪切微调)。

main.js 删至 2034 行。node --check 双过、preview 私有符号在 main 清零、
无未导入 glue 引用、静态测试 2 过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:57:43 +08:00
caoqianming 16dc1719eb refactor(dev): 前端模块化 Step 2 — 抽出 auth.js(首个 main↔模块 ES 环)
登录(邮箱密码 / UUID+PLATFORM_KEY 两 tab)+ 管理员加用户 + 改密码
三节(原 main.js 21–227)→ auth.js(218 行)。

- 各入口在模块顶层自绑 onclick;只导出 logout(供全局 20 处 401 处理)
  / closeChpwModal(供 main 的 Esc 统一关弹窗栈)。
- 反向 import main 的 glue:enterApp / embedPostToParent / embedShowWaiting
  (main 给这三个加 export)——首次引入 main↔auth 循环依赖。三者皆 hoisted
  函数声明,模块实例化即就绪,且只在运行时(点击/401)调用,绝不在顶层
  求值时触发 → ES live binding 下安全。后续 features↔glue 环同理。
- main.js 删至 2397 行。node --check 双过、auth 私有符号在 main 清零、
  静态测试 2 过。逻辑零改动。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:39:06 +08:00
caoqianming cbb16b896f refactor(dev): 前端模块化 Step 2(起)— 抽出 layout.js
三栏布局(pane 折叠 rail + 拖拽 splitter + 手机单列视图)是 main.js 里
唯一对其他功能节零出边的干净段,用它打样增量剥离。

- layout.js(121 行):import $ + 4 个 LS_*_COLLAPSED/WIDTH,只导出
  mqPhone / setMobileView(后者供 selectTask 在手机宽下选中任务自动切
  对话面板,是唯一跨模块调用)。折叠/splitter/mobile-tab 顶层事件绑定
  原样保留(ES module 默认 defer,import 时 DOM 已就绪)。
- main.js:删 114 行 → 2606 行,加 layout import 并清掉随之不再用的
  4 个 LS_* import。逻辑零改动,纯剪切 + 连线;node --check 过,
  main 残留 layout 私有符号清零。

顺手修 Step 1 遗留测试失败:test_static_vendor 第二用例原只 grep
dev.html 找 formatContextStats / context_original_chars / cache_hit_tokens,
模块化后这些搬进 js/*.js → 改为扫 dev.html + js/*.js 合并源。2 测试全过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:27:51 +08:00
caoqianming 72ae41e122 refactor(dev): 前端模块化 Step 1 — dev.html 拆零构建 ES module(叶子优先)
dev.html 原 4087 行单文件原生 JS。本步只做"拆文件"(路径 1),
不引框架、不改逻辑——纯剪切 + import 连线。

抽出 4 个无依赖叶子到 web/static/js/:
- state.js   state 单例 + LS_* + EMBED*
- format.js  escapeHtml / humanSize / fmtTokens / usage 系列等纯格式化
- dom.js     $ + 浮层菜单 showMenu/hideMenu(import escapeHtml)
- api.js     api() Bearer 封装(import state)
- markdown.js renderMd / highlightIn(依赖 vendor 全局)

剩余主体(login→boot)落 main.js 并 import 叶子;dev.html 内联大
<script> 换成一行 <script type="module" src="js/main.js">,降到 1121 行。

app.py 加 mimetypes.add_type("text/javascript", ".js") 兜底,防 Windows
把 .js 判成 text/plain 致 module 被浏览器拒执行(本机实测本就 OK,纯防御)。

校验:6 模块 node --check 全过 + 无私有符号泄漏到 main。
后续 Step 2+ 再从 main.js 把 login/tasks/stream/files/preview 逐个剥成独立模块。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:55:20 +08:00
caoqianming 6f9ca26e3f fix(dev): 改密码弹框复用选入文件的头/体/脚分隔布局
#chpw-modal 原先无专属 CSS,.card 只继承公共骨架,缺 padding/width/
表单整形,卡片被撑近全宽且无内边距。改为复用 #src-picker-modal 的
头/体/脚分隔布局(标题/按钮区带分隔线、表单包进 .body),.card 收到
400px + flex column,input focus 高亮。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:01:48 +08:00
caoqianming f2b3675337 feat(dev): 顶栏自助改密码 + 选入按钮文字改图标(防换行)
改密码:web/auth.py::change_password(验旧密码 + bcrypt 重哈希,错误归一到
UserCreateError code);POST /v1/auth/change_password 挂 require_user,
user_id 取自 JWT 不信前端(旧密码错/无密码 403、弱密码 400)。前端顶栏
「退出登录」左侧加「改密码」按钮(并入 embed 隐藏规则)+ 复用 .modal 弹框
(旧/新/确认,前端先验长度与一致性,成功不登出,401 走 logout)。

选入:#btn-src-pick 文字「选入…」→ 单字符 ⊕(同 ⬆ ↻ › 风格,title 保留
语义),修窄面板偶发换行。

文档:PROGRESS / RUN(API 表 + 用户管理 + 两条兜底)/ DESIGN(auth API 清单)同步。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:07:21 +08:00
caoqianming f81dd2def6 fix(usage): 缓存命中率同源(修 822% 怪值)+ 列表花费改 hover + backfill 加 --assume-cache-hit-rate
- 命中率怪值修复:原以 tasks.tokens_prompt 作分母、usage_events.cache_hit 作分子,
  跨源(tokens_prompt 会被"清空对话"重置、usage_events 不重置)→ 算出 822%。
  _usage_aggregates 改为同时返回 chat tokens_in/out,_task_dict 的 token 总量优先
  取 usage_events 聚合(与 cache_hit 同源,命中率恒 ≤100%)
- 前端:任务列表行不再内联 ¥,花费/缓存命中率藏进 hover tooltip(taskUsageTooltip,
  多行:输入/输出拆分·命中+命中率·真实花费);顶栏保留内联简版 + 同款 tooltip
- backfill: 加 --assume-cache-hit-rate RATE,对无 cache_hit_tokens 字段的老事件按
  估算命中率折价(DeepSeek 当时缓存了只是没记,全价偏高);已记真实值的不受影响

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:41:51 +08:00
caoqianming af2ad3cef1 feat(usage): 记账给前缀缓存命中折价 + 前端体现缓存命中/真实成本
排查"rust→PPT"task(flash,34 轮)发现累计 tokens_in 69.9 万里 88.6% 是缓存
命中,但 _fallback_chat_cost_cny 把命中段也按 input 全价算,记账虚高 2-3x。

- capabilities: 加 cache_hit_cny_per_mtoken(deepseek flash 0.1 / pro 0.2;
  0=不区分按全价兜底,绝不少记)
- usage: 成本公式拆三段「命中×缓存价 + (input−命中)×input价 + output×output价」;
  loop 把 cache_hit_tokens + 缓存单价透传进 record_chat_usage
- web: 不加 DB 列。app.py 加 _usage_aggregates(单查询 GROUP BY usage_events,
  复用列表 msg_counts 批量范式,无 N+1)on-the-fly 算每 task 真实成本 + 缓存命中,
  _task_dict 带出;dev.html 列表行显 ¥、顶栏 formatTaskUsage 显「tok·缓存命中%·¥」
- scripts: backfill_chat_cost_cache_discount.py 按 units 已存 token 重算历史
  cost_cny(只改成本列,默认 dry-run,--apply 落库)

折价只对新 chat 事件即时生效;历史走 backfill 脚本(部署后跑)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 08:26:09 +08:00
caoqianming 1c30a9e54e Reduce chat context token usage 2026-06-04 16:41:14 +08:00
caoqianming af97dd7c62 feat(web): 文件面板底部展示用户已用存储 + 配额
后端已有 user_disk_usage 表(后台 15min 扫描落库)但无对外查询口,
加 GET /v1/user/storage(require_user)返 {bytes_used, file_count,
limit_bytes, scanned_at};limit_bytes 由 parse_bytes(quotas.
disk_bytes_per_user) 得,≤0/None=不限。disk_quota.get_user_usage
扩为返 (bytes,count,scanned_at) 三元组(复用而非新开函数,顺手改唯一
调用方 check_disk_quota 解包)。

前端 dev.html 右侧文件面板底部钉一条进度条+文字:#pane-right 改 flex
列让 file-list 独占滚动、存储条钉底;loadStorage() 在 enterApp 拉一次;
不限额时只显已用、隐进度条;超额变红;hover 显文件数+统计时间。样式用
class 选择器压低特异性,让折叠/手机隐藏规则能盖住它。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:00:36 +08:00
caoqianming 33bea27d85 fix(web): embed 模式登录页一闪而过 — body 首行同步隐藏 #login
#login 默认 display:flex(带 login-in 动画),加 body.embed-mode 隐藏它的
embedInit() 在 body 末尾才执行;单文件 3800+ 行,浏览器常在解析到底部脚本前
就先把登录卡画出来,造成"一闪而过"。改法:在 <body> 第一行加一段同步内联脚本,
?embed=1 时立即 add embed-mode,赶在 #login 解析/绘制之前隐藏它。

只是绘制闸门,底部 embedInit(postMessage 握手 / embed-waiting / token 分支)
完全不动,embed-mode 幂等。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:23:32 +08:00
caoqianming e6eebbc9ad feat(web): tool_call 标题行改显中文活动描述 + 修 args 字段 bug
dev.html 实时流读 ev.data.arguments 但后端 emit 的是 args,字段名对不上
导致参数永远为空、连带 artifact 路径提取失效。新增 toolActivityLabel 按
12 个工具的关键参数套中文动词(执行命令/运行 Python/读写编辑文件/查找搜索
/联网搜索/抓取网页/加载技能/生成图像视频),实时流与历史回放两处同步;完整
参数仍在折叠 <pre> 里。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:24:54 +08:00
caoqianming c4229bede4 skills: /v1/skills 每次现扫,加/删 skill 目录无需重启
lifespan 静态快照改 per-request 扫描,SkillRegistry 构造 ~3ms / 次,
e2e ~20ms 在 JWT + DB 噪声里完全可忽略。agent_builder 早已每次新建
registry,本次只补前端下拉这一处。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:47:47 +08:00
caoqianming da4e47139f dev SPA: embed+task_id 模型下拉 + 导出/清空键 disable 修复
- loadModels 收尾若有 task 选中补一次 renderChatMeta,修 embed+task_id 模型下拉
  在 models 接口 resolve 前 selectTask 已跑过的竞态(同步覆盖生图/生视频下拉)
- btn-export/btn-clear-msgs 不再按 n_messages 门禁,只要选中 task 就常亮;清空
  保留 running 期禁用。修历史 bug:清空后两键 disable,新对话进来 taskMeta 不
  重渲一直灰

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:42:19 +08:00
caoqianming eaf7f3ea1e Stage C 收尾包:资源 yaml + 磁盘配额 + 网络放开 + 容器内源持久化
dogfood + 信任同事白名单阶段 Step 4 完整 egress proxy 暂不做(沉淀为升级触发
信号:任一陌生用户注册 / 模型异常 outbound / 信任白名单出现非密切相识者 → 必上)。
本批 3 件:

(A) 容器资源 yaml 化(可调不重 build):
- agent.yaml 加 sandbox 段(memory/cpus/pids_limit)
- SandboxPool ctor 加三字段,优先级 env > yaml > 默(2g/1.0/256)
- setup_pool/init_pool 透传 sandbox_cfg
- sandbox check 输出加 [info] 4 行给运维一眼对账

(B) 应用层磁盘配额(§7.5 #4 软配额):
- migration 0008 user_disk_usage 单行 per user
- core/storage/disk_quota.py:parse_bytes("5gb"/int)+ scan_user_dir
  (os.scandir 跳顶层 .zcbot_tmp / .memory)+ upsert ON CONFLICT
  + check_disk_quota + scan_all_users 串行
- lifespan _disk_scanner 后台 task(启动跑一次 + 默 15min 周期)
- DockerExecutor write/edit 起手 gate 超额 [Error] 不调容器
- /v1/files/upload 同款 gate 超额 HTTP 413
- yaml `quotas.disk_bytes_per_user: 5gb` + `disk_scan_interval_seconds: 900`
- race 接受:扫描间隙写入轻微突破(image/video 配额同款 race-tolerant);
  外部用户开放前 OS 层 xfs prjquota 兜底
- 11 测试 covered parse_bytes / scan / 跳 dotfile

(C) 网络放开 + 容器内源持久化:
- network.py 去 --internal flag,容器走 docker bridge default 有 NAT outbound
- 已存在 internal network 不自动 rm 仅 warn,RUN.md 给迁移命令(避免破现有容器)
- iptables 红线段不动(169.254/127/10/172.16/192.168/100.64/PG_IP DROP),
  挡 cloud metadata + 内网扫描 + loopback,基线不依赖 proxy
- Dockerfile 加 /etc/pip.conf(global index-url + timeout 60) + /etc/npmrc
  (global registry),让运行时模型 `pip install foo` / `npm install bar`
  也走 mirror(此前 --build-arg 只 build 时生效)

unittest discover 46/46 PASS(原 35 + 新 11)。
DESIGN 不动(延后决策仍在 §7.7 Stage C 阶段语义内,触发信号沉淀进
PROGRESS / RUN);RUN.md 加 env 列表 + 网络迁移 + 配额 + 故障兜底 3 行。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:35:53 +08:00
caoqianming 23ff996d38 Stage C Step 3d: fs 工具进容器 + DESIGN §7.5 #6 重写(物理边界替代代码护栏)
Ubuntu dogfood 暴露 host 工具漏底:base_dir=Path.cwd() 无 user_root 校验,
模型 glob "*" 列出 host /home/lighthouse/zcbot/.git/.venv/... zcbot 源码自身。
DESIGN §7.5 #6 原写"host 工具走 paths.py::resolve_user_path 校验"是假命题
(代码里没那函数),绝对路径完全不挡。

修法:fs 工具(read/write/edit/glob/grep)也走 docker exec,物理边界替代
代码护栏(Phase B path validator 那条不做 ── 脆弱)。

- core/sandbox/tool_runner.py 新增:容器内 helper,stdin 接 JSON args,
  调 tools/fs.py 的 Tool 子类;base_dir=cwd,user_root=/workspace
- DockerExecutor 加 FS_TOOLS 信任域 + _exec_fs_tool:docker exec -i ...
  python /sandbox/tool_runner.py <name>,stdin 喂 JSON args(CJK / 引号
  透明传不被 shell metachar 切)
- _run_subprocess 加 stdin 参数 + is_fs_tool 分支返 stdout 直透(原 Tool
  返回串语义保持),exit≠0 stderr 当 ToolResult content
- SandboxPool 加 repo_root 字段 + <repo>/skills:/sandbox/skills:ro mount
  让容器内 read SKILL references 能解析
- Dockerfile COPY tools/ /sandbox/tools/ + tool_runner.py(build-time COPY
  而非 mount ── 容器内代码不应跟随 host repo 改动)
- web/app.py 透传 ROOT 给 init_pool
- 留 host 的工具:load_skill(SkillRegistry 内存查找)/ web_search /
  web_fetch / seedream / seedance(持 Bocha/ARK key 不入容器)
- DESIGN §7.5 #6 重写:"几乎所有工具进容器,host 只留持 key + 跨 user 的",
  原假命题溯源标注 2026-05-26 修正

代价:每 fs tool call +~200ms docker exec overhead,对话级 N≤15 总 1-3s,
LLM 推理 5-30s 下噪声。升级触发(§7.9 升级表)docker exec → unix socket RPC
仍按原信号(overhead/total > 30% 持续 / 长驻服务工作流)。

测试:test_executor_docker 加 4 fs 路径测试(argv 形态 / CJK stdin JSON /
exit≠0 stderr 透传 / timeout);改原 read 直通测试 → load_skill 直通
(read 现在进容器)。unittest discover 35/35 PASS。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:56:41 +08:00
caoqianming 1a950dedb5 Stage C Step 5: main.py sandbox check + lifespan fs quota WARN
- main.py sandbox check 子命令:5 项独立探测 + 汇总 exit code
  ① docker daemon 可达 ② zcbot-sandbox:latest 镜像存在
  ③ zcbot-sandbox-net network 存在(warn 不 err) ④ 镜像 zcbot uid 与 host
  uid 对齐 ⑤ workspace/users 所在 fs 类型可 quota
- core/sandbox/check.py:detect_fs_quota(path) -> (level, msg) 抽出来给
  lifespan 与 CLI 共用;识别 xfs+prjquota/ext4+project/zfs/btrfs/tmpfs/其他
- web/app.py lifespan docker backend 启用时调 detect_fs_quota 打 WARN
  到 stdout(不阻塞启动,应用层周期扫描仍生效)
- err vs warn 分界:err = docker backend fail-fast 根因(daemon/镜像/uid),
  warn = 不阻塞启动但外部开放前要清(network 缺/fs 不可 quota)
- run_sandbox_check 用 module-level getattr 而非固化 CHECKS 元组,让
  unittest patch core.sandbox.check.check_xxx 生效
- tests/test_sandbox_check.py 19 测试覆盖各分支 + exit code 汇总;
  unittest discover 31/31 PASS
- RUN.md 加"部署前置对账"小节 + "配额硬化"重写(fs 状态→处理映射表 +
  xfs 升级 4 步) + 故障兜底 3 行;DESIGN 不动

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:41:16 +08:00
caoqianming dfac0acfa6 Stage C Step 3: DockerExecutor 集成 AgentLoop + web lifespan reaper
- core/executor_docker.py 新增 DockerExecutor:组合 HostExecutor+SandboxPool,
  shell/run_python 走 docker exec(setsid + --user 1000:1000 + --workdir),
  其他工具直通 host(§7.5 #6 信任域二分)
- run_python tmp .py 落 <user_root>/.zcbot_tmp/<task_id>/(dotfile,/v1/files
  天然过滤),容器内对应 /workspace/.zcbot_tmp/...,跑完 unlink
- ZCBOT_SANDBOX_BACKEND=host|docker env 切 backend,默 host(Windows dogfood
  零变化);docker 路径 pool 未 init → fail-fast 不静默退化
- web/app.py lifespan:docker backend 启动时 init_pool + shutdown_all 清孤儿 +
  60s 后台 reaper(run_in_executor 调 sync reap_idle);关闭时 cancel + 兜底清
- pool.py 顺手清 Step 2 债:asyncio.Lock → threading.Lock,ensure 改同步
  (主使用方是 BG 线程 tool call,ephemeral loop 会让 asyncio.Lock 跨锁失效)
- Cancel limitation 接受:Popen.kill() 仅杀 docker CLI 客户端,容器内进程靠
  idle 5min reaper 兜底;升级到 PGID 协议(§7.5 #3)等用户反馈触发
- tests/test_executor_docker.py 11 测试覆盖关键路径(host 直通/argv 形态/
  tmp 清理/timeout/cancel/未知工具/enable_run_python=False)
- DESIGN.md 不动(纯按 §7.5 #5 #6 既有协议实施)
- RUN.md 加 ZCBOT_SANDBOX_BACKEND env 段 + 切 docker 的前置条件 + 集成验证路径
- unittest discover 12/12 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:13:16 +08:00
caoqianming ade7f3d1e1 Vendor markdown frontend assets 2026-05-25 09:31:36 +08:00
caoqianming ea89c5f209 Persist chat loading state across task switches 2026-05-25 09:16:20 +08:00
caoqianming 7a0d03fb29 Show upload progress 2026-05-25 08:54:06 +08:00
caoqianming 6e33f07bfb Show pasted upload chips 2026-05-25 08:43:07 +08:00
caoqianming f157a4e050 Add resizable web panes 2026-05-25 08:31:31 +08:00
caoqianming f3017480d0 Show full file names on hover 2026-05-25 08:18:20 +08:00
caoqianming 3525399041 Fix task list scroll container 2026-05-25 08:06:33 +08:00
caoqianming ae6b8630e2 ui: 手机端对话面板顶栏 + chat-meta 紧凑化 (5 按钮换行不竖排 + 模型 label 转 emoji)
- #pane-mid .pane-head 加 flex-wrap + 按钮 white-space:nowrap,
  消除 "完\n成" / "废\n弃" 这种内部断行;藏 "对话" label / spacer
- #chat-meta 手机端 gap 8→6px,藏 .tid,.desc 限宽 60vw ellipsis
- 三个模型下拉 label 加 .mdl-text / .mdl-icon 双 span,
  桌面文字 / 手机 emoji (💬/🖼/🎬) CSS 切换,桌面零变化

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:00:52 +08:00
caoqianming 72ae0ded0a feat(web): embed 模式接受 ?task_id=<uuid> URL 参数自动定位 task
首次签发 token / 已有缓存 token 两条进入路径都覆盖;
401 重签不重置选择,尊重用户中间 UI 切过的 task。
EMBED.md URL 参数表 + 故障兜底同步更新。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:27:19 +08:00