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>
This commit is contained in:
caoqianming 2026-06-11 12:00:30 +08:00
parent 15d69b3372
commit dd797a91e2
7 changed files with 34 additions and 3 deletions

View File

@ -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 类内容)

View File

@ -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,不在热路径。

2
RUN.md
View File

@ -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":"<zcbot 版本>"}` | 豁免 |
| `GET /` | 302 → `/static/dev.html` dev SPA | 豁免 |
| `GET /docs` `/openapi.json` | Swagger UI / OpenAPI schema | 豁免 |
| `GET /static/*` | dev.html 等静态文件 | 豁免 |

View File

@ -0,0 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.8.0"

View File

@ -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)):

View File

@ -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 @@
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1.5"></rect><rect x="14" y="3" width="7" height="7" rx="1.5"></rect><rect x="3" y="14" width="7" height="7" rx="1.5"></rect><rect x="14" y="14" width="7" height="7" rx="1.5"></rect></svg>
<span>技能</span>
</button>
<span id="app-version" title="zcbot 版本"></span>
</div>
</div>
<div id="split-left" class="splitter" role="separator" aria-orientation="vertical" title="拖拽调整任务栏宽度"></div>

View File

@ -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) {