diff --git a/CLAUDE.md b/CLAUDE.md index 54665de..ab52aa9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,6 +42,12 @@ PowerShell here-string `@'...'@` **只在 PowerShell 工具里有效**;用 Bash - 状态表(§7 B Step 几 / Phase 几)若变化跟着改 - 文件清单若新增 / 删除模块跟着改 +**每次 commit / push 必须 bump 版本号** —— 单一事实源是 `core/__init__.py` 的 `__version__`(web/app.py 的 FastAPI version、`/healthz` 返回、前端左栏底部展示都引这里,改版本只动这一行): +- patch(`0.8.x`):bug 修复 / 重构 / 调参 / 新加 skill / 样式 +- minor(`0.x.0`):成批新功能 / 明显的对外行为变化 +- major(`x.0.0`):1.0 正式发版 / 不兼容大重构 +- 当前 `0.x` 开发期,未正式发版前不进 1.0 + **只有以下情况才动 `DESIGN.md`**(避免把工程笔记沉淀成设计): - 架构 / 心智模型变化(如 §7.1 task-primary 重写) - 取舍决策推翻或新增(§5 / §7.9 类内容) diff --git a/PROGRESS.md b/PROGRESS.md index cbbdaf2..afa84b2 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -23,6 +23,7 @@ ### 2026-06-11 +- **版本号机制(单一事实源 + 前端展示)**:此前只有 `web/app.py` 写死 `version="0.8"`(仅进 OpenAPI 文档,前端拿不到)。改为 `core/__init__.py` 的 `__version__`(当前 `0.8.0`)作唯一来源 → FastAPI `version`、`/healthz` 返回 `{"status":"ok","version":..}`、前端左栏底部展示全引它,**改版本只动这一行**。前端 `main.js` boot 时无条件 fetch `/healthz`(auth 豁免,embed/未登录都拿得到)填进 `#rail-resources` 的 `#app-version`(技能按钮旁,纯展示不可点;rail 折叠/手机时随整栏隐藏)。**放左栏底部而非顶栏**:embed 模式桌面端整层 header 被 CSS 隐藏,顶栏点不到。CLAUDE.md「文档维护」段已加规矩:每次 commit/push bump `__version__`(patch=修复/重构/调参/skill、minor=成批新功能/对外行为变化、major=1.0 发版)。 - **并发/线程池轻量监控 + 接管默认 executor(§8.4 落地第 1 步)**:已上生产后线程池排队此前无观测手段。lifespan 显式建 `ThreadPoolExecutor`(尺寸复刻 Python 默认 `min(32, cpu+4)`,env `ZCBOT_RUN_MAX_WORKERS` 可调大)+ `set_default_executor` 接管——run 走 `asyncio.to_thread` 即用它,这样既能读 `max_workers` 判断排队、也成了日后调并发的旋钮(**行为不变**,只从匿名默认池换成显式同尺寸池;run 与 disk scan/pptx/reaper 仍共享此池,同原默认)。加 `_stats_logger` 后台 task 每 60s 采样:`active_runs`(=`len(inflight)`,含排队中)逼近 `max_workers` 即排队、新 run 的 SSE 会卡着不吐 token;**刷新峰值**时打 `[stats] new peak active_runs=N max_workers=M`(≥max_workers 带 `[WARN 已在排队]`),**有负载**时打 `[stats] active_runs=.. max_workers=.. sse_subs=.. rss_peak=..MB`,**空闲静默不刷屏**。RSS 用 stdlib `resource`(Unix 峰值/high-water;Windows dev 降级跳过),零新依赖;新 `broker.total_subscribers()` 给全局 SSE 订阅数。查看:`journalctl -u zcbot | grep '\[stats\]'`。**不做监控界面**(运维健康是少数标量、日志够诊断;业务分析数据已落 DB 走 SQL)——界面阶梯见 DESIGN §8.4。 - **dev SPA「技能」查看 modal(左侧 rail 底部入口)**:因 `.skills` 在文件面板隐藏,加左侧 rail 底部「我的资源」分组(`#rail-resources`,留位给后续「记忆」)+「技能」按钮 → 弹 modal 分「平台 skill / 我的 skill」两组列表,点任一项展开**完整 SKILL.md**(`GET /v1/skills/{name}` + 现有 markdown 渲染),「我的」每项带删除(二次确认 → `DELETE /v1/skills/{name}`,只删 user 源 + 防穿越);覆盖标 `已覆盖平台同名`,`load_errors` 提示未加载的。创建/改/fork 仍走对话。新 `web/static/js/skills.js`(零构建 ES module,main.js import + Esc 栈接入);`/v1/skills` 已带 source/overrides/load_errors。**纯查看 + 删除,不在 UI 做创建/编辑**(编辑天然对话式)。 - **用户私有 skill(每用户 `.skills/`,可从零写或 fork 内置再改)**:`SkillRegistry` 从单目录改**多来源**(`SkillSource` 列表:内置 `ROOT/skills` + 用户 `user_root/.skills`),后扫同名覆盖先扫 → **user wins**;覆盖关系记进 `user_overrides`,discovery 显式标 `[你的·已覆盖内置]`(不静默)。`Skill` 加 `source` 字段;`from_dir` 区分"无 SKILL.md(静默跳过)"与"有但格式错(抛 `SkillLoadError`)",`_scan` 捕获用户来源的错收进 `load_errors`、注入 system prompt 提示用户修(一个坏 skill 不再崩整次扫描)。容器路径改写从 LoadSkillTool 下沉到 registry(`container_dir` 按 `source` 给 `/sandbox/skills` 或 `/workspace/.skills`),LoadSkillTool 去掉 `container_skills_dir` 参数。**关键判断**:写 skill 用 host-side typed tool(`save_skill`/`fork_skill`,`tools/skill_authoring.py`)而非 fs/shell —— 因 fs 的 base_dir 锚 cwd(host)/ 容器 wd(docker),都够不到 `user_root/.skills`,跨 backend 不可靠;host-side 工具知道 user_root 一个落点两模式通吃(与 seedream/DocumentDownload 一致范式)。`save_skill` 写时校验 frontmatter(名合法 / YAML 合法 / 有 description / name 一致),`fork_skill` copytree 整目录(带脚本)+ 自动把 frontmatter name 对齐新名(否则 fork ppt 仍叫 ppt 会反覆盖内置)。`.skills` 是 dotfile(文件面板隐藏,与 `.memory` 一致;`validate_task_name` 已禁 `.` 起头 working_dir,天然不撞)。`/v1/skills` 带上用户 skill + `source`/`overrides_builtin`/`load_errors`。新增 `skill-creator` 引导 skill。+`test_user_skills.py`(20 例)+ 改写 `test_load_skill.py`。性能:多扫一目录,没 `.skills` 的用户一次 `exists()` 跳过;有 skill 仅每 run +1-3ms,不在热路径。 diff --git a/RUN.md b/RUN.md index 025f5f5..2293704 100644 --- a/RUN.md +++ b/RUN.md @@ -142,7 +142,7 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta | 方法 + 路径 | 用途 | Auth | |---|---|---| -| `GET /healthz` | `{"status":"ok"}` | 豁免 | +| `GET /healthz` | `{"status":"ok","version":""}` | 豁免 | | `GET /` | 302 → `/static/dev.html` dev SPA | 豁免 | | `GET /docs` `/openapi.json` | Swagger UI / OpenAPI schema | 豁免 | | `GET /static/*` | dev.html 等静态文件 | 豁免 | diff --git a/core/__init__.py b/core/__init__.py index e69de29..44dd7b9 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -0,0 +1,3 @@ +# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 +# 改版本只动这一行。 +__version__ = "0.8.0" diff --git a/web/app.py b/web/app.py index 4c5a391..3f27fa7 100644 --- a/web/app.py +++ b/web/app.py @@ -35,6 +35,7 @@ from pydantic import BaseModel from sqlalchemy import BigInteger, cast, func, select, update from starlette.background import BackgroundTask +from core import __version__ from core.paths import to_db_path from core.storage import ( NoSubtaskError, @@ -784,7 +785,7 @@ def create_app() -> FastAPI: app = FastAPI( title="zcbot api", - version="0.8", + version=__version__, description=( "zcbot 后端 — /v1 JSON API + SSE。Auth: PLATFORM_KEY → JWT(§7 D' 过渡)。" "本地 dev SPA: /static/dev.html。" @@ -814,7 +815,7 @@ def create_app() -> FastAPI: @app.get("/healthz", tags=["misc"]) def healthz(): - return {"status": "ok"} + return {"status": "ok", "version": __version__} @app.get("/v1/models", tags=["misc"]) def list_models(user_id: UUID = Depends(require_user)): diff --git a/web/static/dev.html b/web/static/dev.html index d74a0d6..e0facee 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -210,6 +210,11 @@ display: inline-flex; align-items: center; justify-content: center; gap: 6px; } #rail-resources > button svg { flex-shrink: 0; opacity: .85; } + /* 版本号:贴在「我的资源」栏右侧,纯展示(低优先信息,rail 折叠/手机时随整栏隐藏) */ + #app-version { + flex-shrink: 0; font-size: 11px; color: var(--muted); + font-family: var(--mono); white-space: nowrap; cursor: default; + } /* ───── 技能查看 modal(两栏 master-detail)───── */ #skills-modal { z-index: 112; } @@ -1163,6 +1168,7 @@ 技能 + diff --git a/web/static/js/main.js b/web/static/js/main.js index 000d878..baf8640 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -54,6 +54,19 @@ async function loadStorage() { } +// 版本号:/healthz 是 auth 豁免接口,embed / 未登录都拿得到 → boot 时无条件拉一次填进左栏底部。 +// 失败静默(版本号是装饰性信息,不该因网络抖动报错)。 +async function loadVersion() { + const el = $("app-version"); + if (!el) return; + try { + const r = await fetch("/healthz"); + const d = await r.json(); + if (d && d.version) el.textContent = "v" + d.version; + } catch (e) {} +} + + // ───── Esc 关弹窗栈(跨模块协调:chpw/选入/文件预览/小预览)───── document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; @@ -67,6 +80,7 @@ document.addEventListener("keydown", (e) => { // ───── boot ───── +loadVersion(); // 与登录态无关,立即拉 if (EMBED) { embedInit(); } else if (state.token) {