diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fa208fd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,108 @@ +# 更新日志 + +> 面向用户的更新说明,由 `GET /v1/changelog` 解析后在前端「点版本号」弹层展示。 +> 维护口径:只记**用户可感知**的变化("能干什么了"而不是"改了哪个模块"),纯内部重构 / 修复不记, +> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 +> 工程口径的完整记录见 `PROGRESS.md` / git log。 + +## 0.55.0 — 2026-07-09 + +- 点击右下角版本号可查看更新日志(本弹层);有新版本未看时版本号旁会亮红点提示。 + +## 0.54.1 — 2026-07-09 + +- 对话顶部新增上下文用量指示环:随时看到当前对话"装了多少",接近上限会变色提醒。 +- 超长对话自动整理:早期内容压缩为摘要,目标、结论和关键数据保留,长对话不再因过长而失效。 + +## 0.53.1 — 2026-07-08 + +- 新增「审稿回复」能力:上传决定信 / 审稿意见,协助逐条起草回复信、稿件修改清单和修回 cover letter(中英双语)。 +- 论文能力增强:新增图表设计规范与投稿前全文审计;英文 SCI 润色新增分层诊断模式。 + +## 0.53.0 — 2026-07-08 + +- 支持上传录音文件转文字:单个文件最长 5 小时,自动区分说话人,适合会议 / 访谈记录整理与总结。 + +## 0.52.0 — 2026-07-08 + +- 微信 / 企业微信里可以直接发语音消息对话:自动识别为文字并回显「已识别」,再进入对话。 + +## 0.49.0 — 2026-07-07 + +- 语音输入升级为流式识别:边说边出字,松开即得到完整文本,不再等待。 + +## 0.48.0 — 2026-07-07 + +- 语音按钮支持「按住说话」:按住录音、松开转写、上滑取消(手机 / 电脑通用);快速点一下则进入免持连续录音。 + +## 0.47.0 — 2026-07-07 + +- 网页端新增语音输入:点麦克风按钮说话,识别结果插入输入框(不自动发送,可先编辑)。 + +## 0.46.2 — 2026-07-07 + +- 历史消息浏览优化:翻页与目录跳转按提问轮次对齐,不再出现半截的工具消息。 + +## 0.46.0 — 2026-07-07 + +- 企业微信新成员未绑定账号时发消息,会自动收到绑定指引,不再无响应。 + +## 0.45.0 — 2026-07-07 + +- 手机端显示优化:弹出菜单不再超出屏幕、按钮触控目标加大、管理入口位置统一。 + +## 0.44.0 — 2026-07-07 + +- 企业微信内点开应用即自动登录进入,无需再输账号密码。 + +## 0.43.0 — 2026-07-07 + +- 对话过程可视化:显示模型的思考过程(可折叠),各阶段实时计时,工具执行显示进度与耗时,不再长时间"静止的思考中"。 + +## 0.41.0 — 2026-07-06 + +- 服务更新升级不再中断使用:更新期间正在进行的对话继续完成,无需重来。 + +## 0.38.4 — 2026-07-03 + +- 任务列表显示运行状态:运行中的任务带绿色脉冲点,在别处(手机 / 定时任务)启动的运行也实时反映,无需刷新页面。 + +## 0.37.2 — 2026-07-03 + +- 对话直播中的文字按轮次分段展示,工具连续执行时不再把正文挤出视野。 + +## 0.37.0 — 2026-07-03 + +- PPT 生成质量强化(0.36–0.38 系列):自动质检对齐网格、图表退化、图标缺失、文字重叠、内容越界等问题,发现即自动返工。 + +## 0.35.0 — 2026-07-01 + +- 新增快捷指令:在「记忆」中定义触发词(如"日报"),发送时自动展开为完整指令。 + +## 0.34.0 — 2026-07-01 + +- PPT 工作目录精简:中间构建产物收进隐藏目录,文件面板里只看到源文件和交付物。 + +## 0.33.5 — 2026-06-30 + +- PPT 新增「商务红」政企风配色预设;素材没有配图时会主动提议 AI 配图(确认后才生成)。 + +## 0.33.0 — 2026-06-29 + +- PPT 能力全面重构:逐页设计生成,导出为**原生可编辑**的 PPT(文字、图形、图标均可在 Office 中直接修改),支持模板与品牌配色。 + +## 0.32.0 — 2026-06-29 + +- 微信 / 企业微信长会话自动分段:间隔超过 6 小时自动开启新话题(历史仍保留),也可发送「新话题」手动重置。 + +## 0.31.3 — 2026-06-26 + +- 消息输入框支持直接拖拽文件;多个附件并列展示,不再互相顶掉。 + +## 0.31.0 — 2026-06-26 + +- 模型访问按账户档位开放:不同账户可用的模型清单由管理员按档位配置。 + +## 0.30.0 — 2026-06-26 + +- 新增可选模型:豆包 Seed 2.1(turbo / pro / evolving)与 GLM 5.2。 diff --git a/CLAUDE.md b/CLAUDE.md index 38574e1..7207d4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,8 @@ PowerShell here-string `@'...'@` **只在 PowerShell 工具里有效**;用 Bash - major(`x.0.0`):1.0 正式发版 / 不兼容大重构 - 当前 `0.x` **公测期**,1.0 留给"对外冻结行为 / 正式 GA"那一刻;公测中保持 `0.x` 迭代,minor 走新功能、patch 走修复 +**bump 版本时若改动用户可感知 → 顺手在 `CHANGELOG.md` 顶部加条目**(格式 `## <版本> — <日期>` + bullet):口径面向用户("能干什么了"而不是"改了哪个模块"),纯内部重构 / 修复不记——不是每个版本都要有条目。这份由 `GET /v1/changelog` 解析、前端点版本号展示,别写内部模块名 / env 变量 / 部署细节。 + **只有以下情况才动 `DESIGN.md`**(避免把工程笔记沉淀成设计): - 架构 / 心智模型变化(如 §7.1 task-primary 重写) - 取舍决策推翻或新增(§5 / §7.9 类内容) diff --git a/PROGRESS.md b/PROGRESS.md index 2f337d0..6e017aa 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-07-09(清理 deepseek-chat/reasoner 下线残留描述,bump 0.54.2) +最后更新:2026-07-09(用户版更新日志:CHANGELOG.md + /v1/changelog + 点版本号弹层,bump 0.55.0) --- @@ -23,6 +23,7 @@ ### 2026-07 +- **07-09 / 0.55.0**:**用户版更新日志**(点版本号看"更新了什么"):新增仓库根 `CHANGELOG.md`(面向用户口径,只记可感知变化,回填至 0.30;与工程笔记 PROGRESS 分离——PROGRESS 满是内部模块/env/部署细节不宜外露);`GET /v1/changelog`(公开无鉴权,`## <版本> — <日期>` 正则切段,mtime 缓存热改即生效,无 DB 零 migration);前端右栏底部版本号变可点 → 更新日志弹层(`changelog.js`,复用 marked+purify 渲染),localStorage 记 last-seen 版本、与 /healthz 版本不一致时版本号旁亮红点、打开即清(公测用户发现更新的唯一入口)。CLAUDE.md 加维护约定:bump 版本若用户可感知顺手补 CHANGELOG 条目。 - **07-09 / 0.54.2**:清理 `deepseek-chat/reasoner` 下线残留描述(旧模型 2026-07 已下线):删 `config/models/deepseek_v4.yaml` 里"可退回 deepseek-chat"的误导注释(照做会打不通),DESIGN 附录迁移提醒改为已完成陈述。全库 grep 确认代码/配置本体无旧 model id 引用。 - **07-09 / 0.54.1**:**对话头部压缩指示环**(用户提出:金额撤下明面,换编辑器式上下文环)——头部用量区改为 `N tok + 环形指示器`:环 = 窗口体量 / 模型容量(`reliable_context×2.5` chars),色阶映射三段机制(绿 <50% 未压缩 / 琥珀 50-85% 压缩区 / 红 ≥85% 达折叠阈值),折叠过附 `已整理×N`;金额/缓存命中率挪 hover tooltip(taskUsageTooltip 本就有,信息不丢)。数据链:① `GET /v1/tasks/{id}` 详情端点(列表不加,免 100 行×聚合)新增 `context_window_chars/limit_chars/pressure/folds` 四个 additive 字段(窗口体量 = `sum(length(payload::text)) where idx>=base` 单聚合;容量按 task 模型档案折算,读不出置 None 前端画灰环);② 折叠计费 kind `"chat"→"context_fold"`(`record_chat_usage` 加 kind 参数,prompt_optimize 同范式;折叠次数 = count 该 kind,零 migration,admin 按 kind 对账折叠成本)——0.54.0 未部署无存量,改名零成本;③ `llm_start` 事件补 `context_limit_chars`,run 中每轮实时刷环,`context_fold` done 事件折叠计数 +1 + 体量骤降环回绿;④ PATCH 模型/清空对话两处 `taskMeta` 整体覆盖点改 merge,保住环数据(清空顺带归零)。`format.js` 纯 SVG donut(14px,stroke-dasharray)无依赖。**§8.8 Phase 2 长会话中段折叠摘要**(补齐 IM 常驻会话"单段超长"缺口;Phase 1 gap 分段只砍跨话题累积)——run 起点窗口体量达 `reliable_context×85%`(chars×2.5 折算,与 50% 压缩门槛同源常量,挪 `core/context.py`)→ 新模块 `core/context_fold.py` 把窗口中段(保留尾部 ~30%,切点必落 user 消息不劈 tool 配对)折叠为固定模板结构化摘要(目标/约束决定/进展/待办/关键路径·ID·数值原文保留),写 `tasks.context_summary`(migration 0021)+ 推进 `context_base_idx`,`Session.load` 在 system 后注入仅内存的「[前情摘要]」消息(`_n_head/_base_idx` 元数据维护 idx 映射不变量)。**前缀缓存友好**:摘要调用复用会话自身 prepare 后的消息前缀 + 末尾追加指令(与上一轮 chat 缓存字节一致,近全程 cache hit;切点跨列表用"第 k 条 user"对齐);增量更新 = 旧摘要本在被折前缀里,指令要求合并。失败零阻塞(warn + 跳过);计费入 usage_events(message_id=NULL);「新话题」硬重置/web 清空对话顺带清摘要,gap 软重置保留。前端:新 SSE `context_fold` 事件(start 状态行提示 / done 流内分隔条),顺手补 `warn` 事件渲染(此前 loop 熔断/重复拦截提示在 web 端被静默丢弃)。单测 `tests/test_context_fold.py` 12 项(切点/映射/apply_fold/maybe_fold 打桩),全量 198 过。 - **07-08 / 0.53.1**:**新增 rebuttal skill(审稿意见回复 / 修回)+ paper·review 增强**——调研 GitHub nature skill 生态(Yuan1z0825/nature-skills 26.9k★ Apache-2.0、Boom5426/Nature-Paper-Skills MIT)后移植三块:① 新 skill `rebuttal`(填投稿后环节空白):决定信 / 审稿意见 → 逐点回复信 + 稿件修改清单 + 修回 cover letter,拆条分诊(E.x/Rn.m 编号 → 8 类 × 4 档严重度 × 12 动作标签 tracker)→ 逐条策略确认 → **先改稿后写信** → QA 渲染(复用 --profile paper,中英双语);零编造铁律(不虚构实验/行号/图号/"已改"声明,含糊腹稿一律 AUTHOR_INPUT_NEEDED 追问,中文备注九种含糊转换表)、语气纪律(不同意五步窄口径、拒实验只用范围边界不用时间经费)、疑难案例库(两审稿人打架/统计大修/合规 blocking/appeal 识别)、readiness 四档诚实标注;② paper 补 `figure_discipline.md`(一图一论断/panel 角色/锚 panel/主图 vs 补充材料/图例四规则,阶段三必读)+ `submission_audit.md`(六步通读审计抓 quality_check 机检不到的跨章漂移 + 数据可用性声明模式含企业受限场景,阶段六必读);③ review 补 `sci_polish.md` 英文 SCI 润色子模式(四层递进诊断:全文策略→章节职责→段落逻辑→句子润色,章节职责错了不做句子润色;claim/evidence/boundary;中译英先抽命题不逐句翻)。未装:nature 系文献检索/下载类(自有 research/documents/brief 后端对不上)、paper2ppt/paper-to-patent/proposal-writer(已有国内定制版更强)。 diff --git a/core/__init__.py b/core/__init__.py index 07c35c0..584c11a 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.54.2" +__version__ = "0.55.0" diff --git a/tests/test_changelog.py b/tests/test_changelog.py new file mode 100644 index 0000000..d14d708 --- /dev/null +++ b/tests/test_changelog.py @@ -0,0 +1,50 @@ +"""用户版更新日志:CHANGELOG.md 解析器(web/app.py::_parse_changelog)。""" +import unittest + +from web.app import _CHANGELOG_PATH, _parse_changelog + + +SAMPLE = """# 更新日志 + +> 导语在首个条目前,应被丢弃。 + +## 0.55.0 — 2026-07-09 + +- 条目 A + +## v0.54.1 - 2026-07-09 + +- 条目 B +- 条目 C + +## 0.53.0 + +- 无日期条目 +""" + + +class ChangelogParseTests(unittest.TestCase): + def test_sample_parses_versions_dates_and_bodies(self) -> None: + es = _parse_changelog(SAMPLE) + self.assertEqual([e["version"] for e in es], ["0.55.0", "0.54.1", "0.53.0"]) + self.assertEqual(es[0]["date"], "2026-07-09") + self.assertEqual(es[1]["date"], "2026-07-09") # v 前缀 + 单横线也认 + self.assertIsNone(es[2]["date"]) # 日期可省 + self.assertEqual(es[0]["body_md"], "- 条目 A") + self.assertIn("条目 C", es[1]["body_md"]) + + def test_preamble_before_first_entry_is_dropped(self) -> None: + es = _parse_changelog(SAMPLE) + self.assertNotIn("导语", es[0]["body_md"]) + + def test_real_changelog_file_parses(self) -> None: + # 仓库里的真文件:格式坏了(条目头打错 / 空正文)在 CI 就翻车,不等用户点开才发现 + es = _parse_changelog(_CHANGELOG_PATH.read_text(encoding="utf-8")) + self.assertGreater(len(es), 0) + for e in es: + self.assertRegex(e["version"], r"^\d+\.\d+") + self.assertTrue(e["body_md"], f"empty body for {e['version']}") + + +if __name__ == "__main__": + unittest.main() diff --git a/web/app.py b/web/app.py index 18d2360..c506b87 100644 --- a/web/app.py +++ b/web/app.py @@ -15,6 +15,7 @@ import asyncio import json import mimetypes import os +import re import tempfile from concurrent.futures import ThreadPoolExecutor from contextlib import asynccontextmanager @@ -46,7 +47,7 @@ from starlette.background import BackgroundTask from core import __version__ from core.branding import brand_name -from core.paths import from_db_path, to_db_path +from core.paths import ROOT, from_db_path, to_db_path from core.storage import ( NoSubtaskError, check_no_subtask, @@ -106,6 +107,29 @@ def _pptx_lock_for(abs_path: str) -> asyncio.Lock: # ─────────────────────────── helpers ─────────────────────────── +# 用户版更新日志:仓库根 CHANGELOG.md(面向用户的口径,区别于工程笔记 PROGRESS.md)。 +# 按文件 mtime 缓存解析结果 —— 热改文件即生效,不用重启。条目头格式 `## <版本> — <日期>`。 +_CHANGELOG_PATH = ROOT / "CHANGELOG.md" +_CHANGELOG_HEAD = re.compile(r"^##\s+v?(\d[\w.\-]*)\s*(?:[—–-]+\s*(\S+))?\s*$") +_changelog_cache: dict[str, Any] = {"mtime": None, "entries": []} + + +def _parse_changelog(text: str) -> list[dict[str, Any]]: + """CHANGELOG.md → [{version, date, body_md}](文件序,即新在前);首个 `## ` 前的导语丢弃。""" + entries: list[dict[str, Any]] = [] + body: list[str] = [] + for line in text.splitlines(): + m = _CHANGELOG_HEAD.match(line) + if m: + body = [] + entries.append({"version": m.group(1), "date": m.group(2), "body_md": body}) + elif entries: + body.append(line) + for e in entries: + e["body_md"] = "\n".join(e["body_md"]).strip() + return entries + + def _norm_path(p: str) -> str: """跨 OS 显示归一:backslash → forward slash。""" return (p or "").replace("\\", "/") @@ -1334,13 +1358,31 @@ def create_app() -> FastAPI: return {"status": "ok", "version": __version__, "instance": INSTANCE or None, "brand": BRAND} + @app.get("/v1/changelog", tags=["misc"]) + def changelog(limit: int = 20): + """用户版更新日志(仓库根 CHANGELOG.md 解析,新在前)。 + + 公开端点(无鉴权):内容本就是写给用户看的,登录页脚也展示版本号。 + 只记用户可感知的变化,当前版本可能没有专属条目(纯内部改动不记)。 + 前端:点左栏底部版本号弹层展示,配 localStorage 红点提示新版本。 + """ + limit = max(1, min(limit, 100)) + try: + mtime = _CHANGELOG_PATH.stat().st_mtime + except OSError: + return {"current": __version__, "entries": []} + if _changelog_cache["mtime"] != mtime: + _changelog_cache["entries"] = _parse_changelog( + _CHANGELOG_PATH.read_text(encoding="utf-8")) + _changelog_cache["mtime"] = mtime + return {"current": __version__, "entries": _changelog_cache["entries"][:limit]} + @app.get("/WW_verify_{token}.txt", include_in_schema=False) def wecom_domain_verify(token: str): """企业微信「网页授权可信域名」归属校验文件(公开,无需登录)。 把企业微信下载的 WW_verify_.txt 放到 ZCBOT_WECOM_VERIFY_DIR (默认 repo 根)下,本路由按文件名在域名根 serve。""" from fastapi.responses import PlainTextResponse - from core.paths import ROOT if not token.isalnum(): # 防路径穿越 raise HTTPException(404, "not found") vdir = Path(os.getenv("ZCBOT_WECOM_VERIFY_DIR", "").strip() or str(ROOT)) diff --git a/web/static/dev.html b/web/static/dev.html index b2013c0..229d0a8 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -210,12 +210,50 @@ display: inline-flex; align-items: center; justify-content: center; gap: 6px; } #rail-resources > button svg { flex-shrink: 0; opacity: .85; } - /* 版本号:钉在右侧文件面板底部存储条最左,带细分隔线,纯展示 + /* 版本号:钉在右侧文件面板底部存储条最左,带细分隔线;点击弹更新日志, + 有新版本未看时右上亮红点(changelog.js 维护 .hasnew) (垂直居中由 .storage-foot 的 align-items:center 提供;随存储条一起显隐) */ #app-version { flex-shrink: 0; font-size: 11px; color: var(--muted); - font-family: var(--mono); white-space: nowrap; cursor: default; + font-family: var(--mono); white-space: nowrap; cursor: pointer; padding-right: 8px; border-right: 1px solid var(--border-soft); + position: relative; + } + #app-version:hover { color: var(--text); text-decoration: underline; } + #app-version.hasnew::after { + content: ""; position: absolute; top: -3px; right: 3px; + width: 6px; height: 6px; border-radius: 50%; background: #e5484d; + } + + /* ───── 更新日志 modal(点版本号弹出)───── */ + #changelog-modal { z-index: 112; } + #changelog-modal .card { + width: 640px; max-width: 94vw; max-height: 80vh; + display: flex; flex-direction: column; + } + #changelog-modal h3 { + margin: 0; padding: 12px 16px; font-size: 16px; + border-bottom: 1px solid var(--border); + display: flex; align-items: center; gap: 8px; + } + #changelog-modal h3 .spacer { flex: 1; } + #changelog-modal h3 svg { opacity: .85; } + #cl-cur { font-size: 12px; color: var(--muted); font-weight: normal; font-family: var(--mono); } + #cl-body { flex: 1; min-height: 0; overflow: auto; padding: 4px 20px 16px; } + .cl-entry { padding: 12px 0; border-bottom: 1px solid var(--border-soft); } + .cl-entry:last-child { border-bottom: none; } + .cl-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px; } + .cl-ver { font-family: var(--mono); font-weight: 600; font-size: 14px; } + .cl-badge { + font-size: 11px; color: #fff; background: #2f8a4c; + border-radius: 8px; padding: 1px 7px; + } + .cl-date { font-size: 12px; color: var(--muted); } + .cl-md { font-size: 13px; } + .cl-md ul { margin: 4px 0; padding-left: 18px; } + .cl-md li { margin: 2px 0; } + @media (max-width: 760px) { + #changelog-modal .card { width: 96vw; max-height: 88vh; } } /* ───── 技能查看 modal(两栏 master-detail)───── */ @@ -1459,6 +1497,20 @@ + + +