Compare commits

..

No commits in common. "20c5bffa7bbfb6f96ddd657d2d2b3a705cbbad60" and "8cf08c0f23d4b411e53955785a2a5a1059d6059f" have entirely different histories.

12 changed files with 131 additions and 154 deletions

View File

@ -5,10 +5,6 @@
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
> 工程口径的完整记录见 `PROGRESS.md` / git log。 > 工程口径的完整记录见 `PROGRESS.md` / git log。
## 0.60.29 — 2026-08-04
- 生成 PPT 时,如果没有说明所属机构,模板候选会固定包含中国建材总院红模板;提到总院、中国建材、中建材、建材集团或 CNBM 模板时会直接选用该模板。明确属于其他机构或已经指定其他模板时不会强制加入,同时移除了容易混淆的通用商务红模板。
## 0.60.28 — 2026-08-04 ## 0.60.28 — 2026-08-04
- 对话中的 HTML 产物预览现在会充分利用可用宽度,图片和视频预览也适度放大;文件附件改为更清晰的偏方形卡片,可直接辨认文件类型、名称和预览入口。 - 对话中的 HTML 产物预览现在会充分利用可用宽度,图片和视频预览也适度放大;文件附件改为更清晰的偏方形卡片,可直接辨认文件类型、名称和预览入口。

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-08-04(PPT 总院红候选与别名路由收敛,bump 0.60.29) 最后更新:2026-08-04(对话产物预览与文件 chip 优化,bump 0.60.28)
--- ---
@ -23,8 +23,6 @@
### 2026-08-04 ### 2026-08-04
- **08-04 / 0.60.29 / PPT 总院红候选硬门 + 别名直达**:新建 PPT 未说明所属机构时,模板 / 品牌 / visual_style 候选必须包含 `zongyuan_red`;明确外部机构或其他唯一模板时不强制加入。总院、中国建材、中建材、建材集团、CNBM 等“机构词 × 模板/风格词”表达直接归一到总院红,不再追问具体模板;删除易混淆的通用 `business-red` 品牌预设,普通商务红仅保留为即时配色候选。新增路由回归测试,相关 5 项 unittest、品牌索引 JSON 校验及 diff 检查通过;无 schema、migration、HTTP API、依赖或运行方式变化。
- **08-04 / 0.60.28 / 对话产物宽版预览 + 文件 chip 优化**:包含 HTML 产物的助手消息改用接近占满对话区的宽版布局HTML 画布取消 760px 上限;内联视频上限提升至 720px图片上限提升至 560px 并保持原比例。文件 chip 从小胶囊调整为偏方形附件卡直接展示扩展名类型块、文件名和预览入口粘贴附件同步统一样式并保留键盘焦点反馈。Node 前端 15 项、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API 或依赖变化。 - **08-04 / 0.60.28 / 对话产物宽版预览 + 文件 chip 优化**:包含 HTML 产物的助手消息改用接近占满对话区的宽版布局HTML 画布取消 760px 上限;内联视频上限提升至 720px图片上限提升至 560px 并保持原比例。文件 chip 从小胶囊调整为偏方形附件卡直接展示扩展名类型块、文件名和预览入口粘贴附件同步统一样式并保留键盘焦点反馈。Node 前端 15 项、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API 或依赖变化。
### 2026-08-03 ### 2026-08-03

View File

@ -1,7 +1,7 @@
# zcbot Skill 清单 # zcbot Skill 清单
服务对象:中国建筑材料科学研究总院 —— 无机非金属材料 R&D(水泥 / 混凝土 / 玻璃 / 陶瓷 / 耐火 / 新型建材) 服务对象:中国建筑材料科学研究总院 —— 无机非金属材料 R&D(水泥 / 混凝土 / 玻璃 / 陶瓷 / 耐火 / 新型建材)
最后更新:2026-08-04(ppt 默认机构候选包含总院红,明确外部机构除外,建材集团别名直达) 最后更新:2026-07-27(videogen 新增 Seedance 单图首帧与最多 9 张多图参考输入)
Skill 总数:18 Skill 总数:18
zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` + 配套 templates / scripts / Python helper),模型在识别用户意图后挂载对应 skill,按其内置的阶段化流程产出可交付物。本文档面向**使用方 / 协作方**,按"做什么、什么时候用、什么时候别用、典型产物"组织。 zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` + 配套 templates / scripts / Python helper),模型在识别用户意图后挂载对应 skill,按其内置的阶段化流程产出可交付物。本文档面向**使用方 / 协作方**,按"做什么、什么时候用、什么时候别用、典型产物"组织。
@ -20,7 +20,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
| 科研写作 | [patent](#patent) | 写发明专利技术交底书(供代理师转写) | | 科研写作 | [patent](#patent) | 写发明专利技术交底书(供代理师转写) |
| 科研写作 | [review](#review) | 审稿 / 润色 / 校对(中英文,长文档分段深审;含英文 SCI 润色子模式) | | 科研写作 | [review](#review) | 审稿 / 润色 / 校对(中英文,长文档分段深审;含英文 SCI 润色子模式) |
| 科研写作 | [rebuttal](#rebuttal) | 回复审稿意见(修回):逐点回复信 + 稿件修改清单 + 修回 cover letter | | 科研写作 | [rebuttal](#rebuttal) | 回复审稿意见(修回):逐点回复信 + 稿件修改清单 + 修回 cover letter |
| 演示出图 | [ppt](#ppt) | 生成可编辑 PowerPoint(SVG-first:逐页手写 SVG → 原生 DrawingML;总院红品牌模板可选) | | 演示出图 | [ppt](#ppt) | 生成可编辑 PowerPoint(SVG-first:逐页手写 SVG → 转原生 DrawingML;19 种视觉风格 + 模板库) |
| 演示出图 | [plot_pub](#plot_pub) | 出版级 matplotlib 学术图(中文 + viridis + 矢量 + 投稿级复合图设计纪律) | | 演示出图 | [plot_pub](#plot_pub) | 出版级 matplotlib 学术图(中文 + viridis + 矢量 + 投稿级复合图设计纪律) |
| 文献检索 | [research](#research) | 查 paper_server(OpenAlex 元数据 + Sci-Hub 下载) | | 文献检索 | [research](#research) | 查 paper_server(OpenAlex 元数据 + Sci-Hub 下载) |
| 文献检索 | [documents](#documents) | 查内部 7 学科材料知识库(100W+ 论文,跨语言检索;host-side tool 持 key) | | 文献检索 | [documents](#documents) | 查内部 7 学科材料知识库(100W+ 论文,跨语言检索;host-side tool 持 key) |
@ -209,7 +209,7 @@ zcbot 的"skill"是一份可加载的工作流脚本(`skills/<name>/SKILL.md` +
**定向模型**:frontmatter 定向 `glm.pro52`(GLM-5.2)—— 对话中 load 本 skill 时自动切换到该模型(不限档位,不自动切回;后续消息沿用,可在顶栏下拉手动切走)。 **定向模型**:frontmatter 定向 `glm.pro52`(GLM-5.2)—— 对话中 load 本 skill 时自动切换到该模型(不限档位,不自动切回;后续消息沿用,可在顶栏下拉手动切走)。
**默认主题 —— 自由设计 + 总院红条件必选候选**:用户未说明所属机构时,新建 PPT 的候选列表必须包含 `templates/layouts/zongyuan_red/`;用户明确说明属于其他机构 / 品牌或提供其他唯一模板时不强制加入。用户明确说“总院模板、建材集团模板、中国建材模板、中建材模板、CNBM 模板”等同义表达时,直接视为已选中总院红。普通商务红只保留为即时配色方向,不再提供独立品牌模板 **默认主题 —— 自由设计**(content-driven):按内容+受众+选定 visual_style 派生配色版式,spec 阶段给 ≥3 套候选挑;商务红/品牌色作为候选之一,用户点名或素材有 brand guideline 才锁定
**八条对齐**(spec 阶段定稿,ah):画布 / 页数 / 受众+核心信息+投递目的 / mode+visual_style / 配色 / 图标库 / 字体+字号 / 配图。确认后产出两份引擎契约:`design_spec.md`(人读叙事)+ `spec_lock.md`(机读执行锁,executor 每页重读、抗长 deck 漂移)。 **八条对齐**(spec 阶段定稿,ah):画布 / 页数 / 受众+核心信息+投递目的 / mode+visual_style / 配色 / 图标库 / 字体+字号 / 配图。确认后产出两份引擎契约:`design_spec.md`(人读叙事)+ `spec_lock.md`(机读执行锁,executor 每页重读、抗长 deck 漂移)。

View File

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

View File

@ -65,12 +65,11 @@ model: glm.pro52
## 默认主题 — 自由设计(content-driven) ## 默认主题 — 自由设计(content-driven)
**默认不锁死配色**:策略阶段根据**内容 + 受众 + 选定的 visual_style**派生一套协调配色与版式(在 spec 阶段给用户 ≥3 个配色/风格候选挑)。模板是地板也是天花板 —— 默认自由设计让版面跟着内容走,而非被固定语汇框死。 **默认不锁死配色**:策略阶段根据**内容 + 受众 + 选定的 visual_style** 派生一套协调配色与版式(在 spec 阶段给用户 ≥3 个配色/风格候选挑)。模板是地板也是天花板 —— 默认自由设计让版面跟着内容走,而非被固定语汇框死。
- 商务红 `#C00000` 可作为普通中文政企 / 集团 / 科研商务汇报的**配色候选之一**,但它只是一套即时派生的配色方案,不对应内置品牌模板。用户点名其他颜色或素材里有 brand guideline → 按其锁定。 - 商务红 `#C00000` / 中建材等品牌色,作为**候选之一**;**中文政企/集团/科研商务汇报默认就把商务红列进 ≥3 配色候选**(见 strategist.md §e)。用户点名("做成蓝色 / 用我们公司紫色")或素材里有 brand guideline → 按其锁定。
- 其它品牌 / 模板仍按明确路径 opt-in(见 strategist.md 模板分发),不主动猜、不模糊匹配。 - 现成一款 **`business-red` 商务红品牌预设**(`templates/brands/business-red/`:#C00000 全色表 + 宋体标题 + 实心图标);用户说"红色 / 商务红 / 中建材风"→ 指给他按路径 opt-in,或直接锁其配色。其它品牌/模板同理:**用户给 `templates/` 下明确路径才触发**(见 strategist.md 模板分发),不主动猜、不模糊匹配。
- **候选硬门**:zcbot 默认服务于中国建筑材料科学研究总院。用户未说明所属机构时,只要新建 PPT 进入模板 / 品牌 / visual_style 候选流程,候选列表中**必须包含** `templates/layouts/zongyuan_red/`,并以“中国建材总院红模板”单独列出,与其他候选一起等用户确认;不要静默锁定。用户明确说明 PPT 属于其他机构 / 品牌,或提供现有 PPT / 明确指定其他唯一模板时,不强制加入 `zongyuan_red`。模板锁定的是品牌身份 + 页面骨架,正文仍按内容扩展,不机械限制为 5 页。 - **例外(主动提示):中国建材总院系汇报** —— 受众/素材/用户机构指向 **中国建筑材料科学研究总院 · 中国建材(CNBM)系**(工作汇报/立项/项目评审/**职称评审**/品牌宣讲)时,策略阶段**主动**把整套品牌模板 `templates/layouts/zongyuan_red/`(八边形 logo + 品牌红 `#D7000E` + 总部大楼实景铺底,5 页齐)作为候选点名给用户,用户点头再按明确路径套入(见 strategist.md §e "中国建材总院" 提示)。这是唯一鼓励主动提模板的场景;其余仍等明确路径。
- **总院模板别名直接归一**:机构词“总院 / 中国建筑材料科学研究总院 / 中国建材 / 中建材 / CNBM / 中国建材集团 / 建材集团”,与意图词“模板 / 红模板 / 品牌模板 / 风格 / 品牌风”任意组合,都视为用户已选中 `templates/layouts/zongyuan_red/`。包括但不限于“总院红、中国建材模板、中建材模板、CNBM 模板、中国建材集团模板、建材集团模板、集团红模板、中国建材风、中建材风、按集团模板、用建材集团风格”;直接锁定该明确路径,不再追问“是哪一套模板”。在本项目总院语境下,单说“集团模板 / 集团红模板”也按此处理。用户提供的现有 PPT / 其他明确模板仍然优先。
--- ---

View File

@ -119,11 +119,11 @@ Proactively provide a color scheme (HEX values) based on content characteristics
| Technology / Internet | `#1565C0` Bright Blue | Innovative, energetic | | Technology / Internet | `#1565C0` Bright Blue | Innovative, energetic |
| Healthcare / Health | `#00796B` Teal Green | Professional, reassuring | | Healthcare / Health | `#00796B` Teal Green | Professional, reassuring |
| Government / Public Sector | `#C41E3A` Red | Authoritative, dignified | | Government / Public Sector | `#C41E3A` Red | Authoritative, dignified |
| 中文政企 / 集团 / 研究院所 商务汇报 | `#C00000` 商务红 | 庄重、权威、结论先行 | | 中文政企 / 集团 / 研究院所 商务汇报 | `#C00000` 商务红 | 庄重、权威、结论先行(中建材系商务红) |
> **商务红 default color candidate.** For Chinese state-enterprise / group / research-institute business decks (工作汇报 / 述职 / 方案评审 / 年度总结 / 科技成果), offer **商务红 `#C00000`** as one of the ≥3 color candidates by default — it is a color direction, not a built-in brand template. Anchor a red-dominant scheme on it: pair with a 鎏金 `#BF9B5F` *or* 沉靛蓝 `#2B4C7E` accent (red-gold or red-blue, pick one as the point accent), warm off-white surface (`#FBF5F5`), and reserve solid `#C00000` for headings / key data / one color block — never a full-canvas red. > **商务红 default candidate.** For Chinese state-enterprise / group / research-institute business decks (工作汇报 / 述职 / 方案评审 / 年度总结 / 科技成果), offer **商务红 `#C00000`** as one of the ≥3 color candidates by default — it is the conventional, expected business color in this context. Anchor a red-dominant scheme on it: pair with a 鎏金 `#BF9B5F` *or* 沉靛蓝 `#2B4C7E` accent (red-gold or red-blue, pick one as the point accent), warm off-white surface (`#FBF5F5`), and reserve solid `#C00000` for headings / key data / one color block — never a full-canvas red. A ready-made bundle of exactly this (palette + serif-title typography + filled-icon style) ships as the **`business-red` brand preset** (`templates/brands/business-red/`); when the user wants "红色 / 商务红 / 中建材风" point them at it (apply by explicit path per Step 3) or lock its palette inline.
> **候选硬门 + 别名直达 `zongyuan_red`.** zcbot 默认服务于中国建筑材料科学研究总院。用户未说明所属机构时,只要新建 PPT 进入模板 / 品牌 / visual_style 候选流程,候选列表中必须包含 `templates/layouts/zongyuan_red/`,以“中国建材总院红模板”单独列出。用户明确说明 PPT 属于其他机构 / 品牌,或提供现有 deck / 明确指定其他唯一模板时,不强制加入 `zongyuan_red`。别名按“机构词 × 意图词”归一:机构词包括“总院 / 中国建筑材料科学研究总院 / 中国建材 / 中建材 / CNBM / 中国建材集团 / 建材集团”,意图词包括“模板 / 红模板 / 品牌模板 / 风格 / 品牌风”;任意组合都表示用户已明确选中 `zongyuan_red`,包括但不限于“总院红、中国建材模板、中建材模板、CNBM 模板、中国建材集团模板、建材集团模板、集团红模板、中国建材风、中建材风、按集团模板、用建材集团风格”。在本项目总院语境下,单说“集团模板 / 集团红模板”也按此处理。此时直接按路径锁定 identity + structure,不再追问具体模板。`zongyuan_red` 是中国建材相关表述唯一对应的内置红色品牌模板,带真实 logo、总部实景和封面/目录/章节/内容/尾页 5 类骨架。模板锁定后保留品牌恒定色,补锁 12 个内容辅助色并按实际页数扩展正文版式 > **中国建材总院 → 主动提 `zongyuan_red` 整套模板.** 当受众 / 素材 / 用户机构指向 **中国建筑材料科学研究总院 · 中国建材(CNBM)系**(工作汇报 / 述职 / 立项 / 项目评审 / **职称评审** / 品牌宣讲)时,除商务红候选外,**主动在 ah 预览里把 `templates/layouts/zongyuan_red/` 作为一个候选点名给用户**(一句话:"你们总院有现成品牌模板 `zongyuan_red`(八边形 logo + 品牌红 `#D7000E` + 总部大楼实景铺底,封面/目录/章节/内容/尾页 5 页齐),要不要直接套?")。与 `business-red` 的区别:business-red 只是**配色+字体身份预设**(自由设计上皮用);zongyuan_red 是**带真实 logo/实景照片的整套 5 页版式**,要品牌一致、省事就用它。仍是 opt-in —— 用户点头后按 Step 3 以**明确路径**套入锁定其 identity+structure;用户要另起风格 / 换配色就走自由设计,不强推
**Color rules**: 60-30-10 rule (primary 60%, secondary 30%, accent 10%); text contrast ratio >= 4.5:1; no more than 4 colors per page. **Color rules**: 60-30-10 rule (primary 60%, secondary 30%, accent 10%); text contrast ratio >= 4.5:1; no more than 4 colors per page.

View File

@ -6,5 +6,9 @@
"google": { "google": {
"summary": "Google brand identity — multi-product corporate decks, developer events (Google I/O style), education and training in the Google ecosystem", "summary": "Google brand identity — multi-product corporate decks, developer events (Google I/O style), education and training in the Google ecosystem",
"primary_color": "#4285F4" "primary_color": "#4285F4"
},
"business-red": {
"summary": "商务红 (Business Red) — 中文政企/集团/研究院所商务汇报、述职、方案评审、年度总结;商务红 #C00000(中建材系同色),庄重结论先行,红金/红蓝配,实心图标,宋体标题",
"primary_color": "#C00000"
} }
} }

View File

@ -0,0 +1,80 @@
---
brand_id: business-red
kind: brand
summary: 商务红 (Business Red) identity — 中文政企/科研/集团商务汇报,商务红 #C00000(中建材系商务红同色);庄重、结论先行、红金/红蓝配
primary_color: "#C00000"
---
# 商务红 Business-Red Brand Specification
> Identity-only preset (no SVG page roster — pages are composed freely under these constraints).
> A **generic** Chinese business / state-enterprise / research-institute identity anchored on
> 商务红 `#C00000` (the PowerPoint "Dark Red", also 中建材系商务红). Not tied to one organization
> — reuse for 总院 / 集团 / 政企 / 科研 decks. To bind a real organization, drop its logo into this
> directory and fill §IV (see Logo note).
## I. Brand Overview
| Property | Value |
|---|---|
| Brand Name | 商务红 (Business Red) |
| Use Cases | 政企 / 集团 / 研究院所 工作汇报、述职、方案评审、年度总结、科技成果汇报、对外宣讲 |
| Tone | 庄重、严谨、结论先行、克制;权威而不浮夸(红为主、金/蓝点缀,避免大面积纯红刺眼) |
## II. Color Scheme
| Role | HEX | Provenance | Notes |
|---|---|---|---|
| primary | `#C00000` | fact | 商务红 — 主色(= PowerPoint 标准"深红",中建材系商务红同色)。标题强调、主色块、主图表系列 |
| primary-deep | `#8A0000` | approx | 深绛红 — 主红暗阶。表头、深色页底、强调描边、悬停/选中态 |
| neutral-dark | `#1A1A1A` | approx | 近黑 — 正文、深色面、图表基线/坐标轴 |
| bg | `#FFFFFF` | fact | 纯白页面背景 |
| accent (gold) | `#BF9B5F` | approx | 鎏金/古铜 — 点缀、金线分隔、奖项/关键数据高亮。红金为政企/官方经典搭配,显厚重 |
| accent (info) | `#2B4C7E` | approx | 沉靛蓝 — 流程/链接/可交互/第二数据系列。冷色平衡暖红,红蓝是另一套稳重搭配 |
| accent (positive) | `#2E7D32` | approx | 墨绿 — 推荐项、正向、达标、增长 |
| accent (alert) | `#E08A1E` | approx | 琥珀橙 — 风险/提醒/预警(刻意避开与主红撞色,不要再用一种红表示警示) |
| surface | `#FBF5F5` | approx | 暖调微红的卡片底/面板,比纯白略沉,承托内容 |
| border | `#ECDCDC` | approx | 暖浅灰 — 卡片边、分隔线、表格栏线 |
| muted-text | `#6E6260` | approx | 暖灰 — 次要文字、注释、图表标签、页码/出处 |
第 14 行(primary / primary-deep / neutral-dark / bg)是身份骨架gold / info / positive / alert /
surface / border / muted 是其上的演示约定色,按政企商务气质派生。语义三元组(info=流程 / positive=
推荐 / alert=风险)用于区分内容类型。**纪律**:红为主、金或蓝二选一作点缀主调(不要红金蓝同台喧宾);
大面积底色用 bg/surface,纯 `#C00000` 只压在标题条、关键数据、主色块等"少而重"的位置 —— 避免满屏
红的廉价感。Strategist 可按页轮换主色支配比例。
## III. Typography
| Role | Family | Weight |
|---|---|---|
| title | `"Source Han Serif SC", "Noto Serif CJK SC", "Songti SC", "SimSun", serif` | 600700 |
| body | `"Source Han Sans SC", "Noto Sans CJK SC", "Microsoft YaHei", "PingFang SC", sans-serif` | 400 |
> 宋体系标题给政企/官方的庄重感,黑体正文保清晰。两条字体栈尾都收在 **预装字体**(`SimSun` /
> `Microsoft YaHei`,见 shared-standards §字体),无运行时回退也不会掉成系统默认。若部署机装了思源
> 宋体/黑体(沙箱镜像的 `fonts-noto-cjk` 即 Noto CJK),会优先吃到更优字形。纯数字/西文标号可另配
> 一款衬线数字体(如 `Georgia`)增强财务/数据页质感,非必须。
## IV. Logo
**本预设不绑定具体机构 logo**(通用商务红身份)。默认用机构中文全称作**文字 wordmark**:
- 封面:机构全称用 title 衬线体,`primary`(`#C00000`)或 `neutral-dark`,压在左下/居中;副标题用 body。
- 页眉/页脚:可放机构简称小字 + 一条 `primary` 细色条作版式锚点;不要每页堆砌。
- 要绑真实机构:把其 logo 存成 `./<org>_logo.svg` 放进本目录,在此表补一行(File / Form / Usage),
并按 anthropic 预设的 Logo 段写清留白(≥0.5× logo 高)、用色(单色优先 `primary` 或随页文字色)、
封面 vs 页内的取用规则。
## V. Voice & Tone
- 正式度:正式、书面、政企汇报口吻;结论先行。
- 人称:我们 / 各位(中文);单位自称用机构简称。
- Emoji:避免(政企/官方场合不用)。
- 缩略语 / 术语:首次出现给中文全称(必要时附英文/缩写),后文再用简称。
## VI. Icon Style
- Preference: filled(实心)
> 政企/官方汇报偏稳重厚实,实心图标比线性更压得住场。用 `templates/icons/` 时优先 `tabler-filled` /
> `chunk-filled` 实心库,不要 outline 线性库(太轻,与庄重调性不符)。图标用色走 `primary` /
> `primary-deep` / `neutral-dark`,点缀可用 `gold`;一套库到底,不混库。

View File

@ -78,14 +78,3 @@ test("artifact chips expose a compact file type and preview affordance", () => {
assert.match(pageHtml, /\.art-chip-icon/); assert.match(pageHtml, /\.art-chip-icon/);
assert.match(pageHtml, /\.art-chip:focus-visible/); assert.match(pageHtml, /\.art-chip:focus-visible/);
}); });
test("message actions copy raw content and revise by appending a new message", () => {
assert.match(chatJs, /class="msg-action msg-copy"[^>]+aria-label="复制消息"/);
assert.match(chatJs, /class="msg-action msg-revise"[^>]+aria-label="引用并修改消息"/);
assert.match(chatJs, /state\.loadedMessages\.find/);
assert.match(chatJs, /navigator\.clipboard\.writeText\(text\)/);
assert.match(chatJs, /function loadMessageIntoComposer\(text\)/);
assert.match(chatJs, /修改后发送将作为一条新消息追加/);
assert.match(pageHtml, /\.msg:hover \.msg-actions, \.msg:focus-within \.msg-actions/);
assert.match(pageHtml, /\.msg-action:focus-visible/);
});

View File

@ -1,47 +0,0 @@
"""PPT skill 的总院品牌候选与别名路由回归测试。"""
import json
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
class TestPptSkillBrandPolicy(unittest.TestCase):
def test_zongyuan_red_is_candidate_with_alias_direct_route(self):
skill = (ROOT / "skills" / "ppt" / "SKILL.md").read_text(encoding="utf-8")
strategist = (
ROOT / "skills" / "ppt" / "references" / "strategist.md"
).read_text(encoding="utf-8")
self.assertIn("候选硬门", skill)
self.assertIn("候选列表中**必须包含**", skill)
self.assertIn("用户未说明所属机构", skill)
self.assertIn("用户明确说明 PPT 属于其他机构 / 品牌", skill)
self.assertIn("总院模板别名直接归一", skill)
self.assertIn("建材集团模板", skill)
self.assertIn("机构词", skill)
self.assertIn("中国建材风", skill)
self.assertIn("候选硬门 + 别名直达", strategist)
self.assertIn("用户已明确选中 `zongyuan_red`", strategist)
self.assertNotIn("唯一默认入口", skill)
self.assertNotIn("business-red", skill)
self.assertNotIn("business-red", strategist)
self.assertFalse(
(ROOT / "skills" / "ppt" / "templates" / "brands" / "business-red").exists()
)
brands_index = json.loads(
(
ROOT
/ "skills"
/ "ppt"
/ "templates"
/ "brands"
/ "brands_index.json"
).read_text(encoding="utf-8")
)
self.assertNotIn("business-red", brands_index)
if __name__ == "__main__":
unittest.main()

View File

@ -886,26 +886,18 @@
.msg .body a, .md-render a { color: var(--accent); } .msg .body a, .md-render a { color: var(--accent); }
.msg .body img, .md-render img { max-width: 100%; } .msg .body img, .md-render img { max-width: 100%; }
.msg .body hr, .md-render hr { border: none; border-top: 1px solid var(--border); margin: 0.8em 0; } .msg .body hr, .md-render hr { border: none; border-top: 1px solid var(--border); margin: 0.8em 0; }
/* 消息快捷操作:桌面悬停/键盘聚焦时显示;触屏无 hover 时保留淡态可发现性。 */ /* 卡片底部操作栏(仿 GPT/DeepSeek):默认隐,悬停/聚焦现;触屏无 hover → 常显淡态 */
.msg-actions { .msg-actions { display: flex; gap: 6px; margin-top: 6px; opacity: 0; transition: opacity .12s; }
display: flex; align-items: center; gap: 2px; min-height: 26px; margin-top: 4px;
opacity: 0; transition: opacity .12s;
}
.msg:hover .msg-actions, .msg:focus-within .msg-actions { opacity: 1; } .msg:hover .msg-actions, .msg:focus-within .msg-actions { opacity: 1; }
.msg.user .msg-actions { justify-content: flex-end; } .msg.user .msg-actions { justify-content: flex-end; }
.msg-action { .msg-actions button {
display: inline-flex; align-items: center; justify-content: center; font: inherit; font-size: 11px; line-height: 1; color: var(--muted);
width: 26px; height: 26px; padding: 4px; border: 0; border-radius: var(--r-sm); background: transparent; border: 1px solid var(--border); border-radius: var(--r-sm);
color: var(--muted); background: transparent; cursor: pointer; padding: 3px 8px; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
transition: color .12s, background .12s; transition: background .12s, color .12s, border-color .12s;
}
.msg-action:hover { color: var(--text); background: var(--code-bg); }
.msg-action:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
.msg-action.copied { color: #16a34a; }
.msg-action svg {
width: 16px; height: 16px; fill: none; stroke: currentColor;
stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
} }
.msg-actions button:hover { color: var(--text); background: var(--code-bg); border-color: var(--muted); }
.msg-actions button.copied { color: #16a34a; border-color: #16a34a; background: transparent; }
@media (hover: none) { .msg-actions { opacity: .55; } } @media (hover: none) { .msg-actions { opacity: .55; } }
.tool-call { margin-top: 6px; font-family: var(--mono); font-size: 12px; } .tool-call { margin-top: 6px; font-family: var(--mono); font-size: 12px; }
.tool-call summary { .tool-call summary {

View File

@ -1411,21 +1411,15 @@ function setTaskProgress(taskId, steps) {
if (state.taskId === taskId) renderTaskProgressDock(normalized); if (state.taskId === taskId) renderTaskProgressDock(normalized);
} }
const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="11" height="11" rx="2"></rect><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path></svg>`; // 卡片底部「复制」按钮 HTML。复制的是正文原文(Markdown 源),点击时经事件委托据
const REVISE_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20h9"></path><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4Z"></path></svg>`; // 卡片 data-idx 回 state.loadedMessages 取 payload.content —— 不从渲染后的 .body HTML 反推。
const CHECK_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6"></path></svg>`; // 直播卡不带此栏:流结束后 loadMessages 整屏重渲成持久卡,自然带上。
function msgActionsHtml() {
// 消息操作保持轻量:复制取 DB payload 中的 Markdown 原文;用户的“修改”只把旧消息 return `<div class="msg-actions"><button type="button" class="msg-copy" title="复制正文">复制</button></div>`;
// 载入 composer,后续发送仍是 append-only 新消息,不改写历史、不重跑旧工具链。
function msgActionsHtml(role) {
const copy = `<button type="button" class="msg-action msg-copy" aria-label="复制消息" title="复制">${COPY_ICON}</button>`;
const canRevise = role === "user" && !channelCfg(state.taskMeta && state.taskMeta.channel);
const revise = canRevise
? `<button type="button" class="msg-action msg-revise" aria-label="引用并修改消息" title="修改后作为新消息发送">${REVISE_ICON}</button>`
: "";
return `<div class="msg-actions">${copy}${revise}</div>`;
} }
// 复制文本到剪贴板:优先 async Clipboard API(需 https/localhost),失败降级 execCommand。
// 按钮态短暂反馈「✓ 已复制 / 复制失败」后复位。
async function copyTextToClipboard(text, btn) { async function copyTextToClipboard(text, btn) {
if (!text) return; if (!text) return;
let ok = false; let ok = false;
@ -1446,37 +1440,16 @@ async function copyTextToClipboard(text, btn) {
ta.remove(); ta.remove();
} catch (e) { ok = false; } } catch (e) { ok = false; }
} }
if (!ok) { if (btn) {
message("复制失败,请手动选择正文复制", "error"); if (!btn.dataset.label) btn.dataset.label = btn.textContent;
return; btn.textContent = ok ? "✓ 已复制" : "复制失败";
btn.classList.toggle("copied", ok);
clearTimeout(btn._copyTimer);
btn._copyTimer = setTimeout(() => {
btn.textContent = btn.dataset.label;
btn.classList.remove("copied");
}, 1400);
} }
if (!btn) return;
if (!btn.dataset.originalHtml) btn.dataset.originalHtml = btn.innerHTML;
btn.innerHTML = CHECK_ICON;
btn.classList.add("copied");
btn.setAttribute("aria-label", "已复制");
clearTimeout(btn._copyTimer);
btn._copyTimer = setTimeout(() => {
btn.innerHTML = btn.dataset.originalHtml;
btn.classList.remove("copied");
btn.setAttribute("aria-label", "复制消息");
}, 1400);
}
function loadMessageIntoComposer(text) {
const input = $("chat-input");
if (!text || !input || input.readOnly) return;
input.focus();
input.select();
// 与提示词润色/语音转写共用浏览器原生 undo 栈,覆盖已有草稿后仍可 Ctrl+Z 撤销。
let inserted = false;
try { inserted = document.execCommand("insertText", false, text); } catch (e) { /* fallback */ }
if (!inserted) {
input.value = text;
input.setSelectionRange(text.length, text.length);
}
syncOptimizeBtn();
$("chat-hint").textContent = "已载入旧消息;修改后发送将作为一条新消息追加";
} }
// stickBottom:渲染完是否滚到底。首屏/发送等"看最新"路径保持默认 true; // stickBottom:渲染完是否滚到底。首屏/发送等"看最新"路径保持默认 true;
@ -1673,10 +1646,10 @@ function renderMessages(msgs, { stickBottom = true } = {}) {
} }
// 进度不再内联进消息卡 —— 累积值在循环末统一喂顶部 dock(见 setTaskProgress) // 进度不再内联进消息卡 —— 累积值在循环末统一喂顶部 dock(见 setTaskProgress)
} }
if ((role === "user" || role === "assistant") // 复制栏:user 恒有正文;assistant 仅在有正文文本时挂(纯 tool_calls 轮无正文可复制)
&& typeof p.content === "string" && p.content.trim()) { const hasCopy = role === "user"
html += msgActionsHtml(role); || (role === "assistant" && typeof p.content === "string" && p.content.trim());
} if (hasCopy) html += msgActionsHtml();
// task_progress-only assistant rows feed the persistent progress dock. // task_progress-only assistant rows feed the persistent progress dock.
// Once that tool call is filtered, do not leave an empty assistant shell. // Once that tool call is filtered, do not leave an empty assistant shell.
if (role === "assistant" && !hasVisibleAssistantContent) continue; if (role === "assistant" && !hasVisibleAssistantContent) continue;
@ -2409,20 +2382,13 @@ document.addEventListener("keydown", (e) => {
// 视频走原生 <video controls>:点击=播放/暂停,全屏走浏览器自带按钮,不进 modal — // 视频走原生 <video controls>:点击=播放/暂停,全屏走浏览器自带按钮,不进 modal —
// 弹个 modal 反而打断播放,不如交给浏览器。 // 弹个 modal 反而打断播放,不如交给浏览器。
$("chat-stream").addEventListener("click", (e) => { $("chat-stream").addEventListener("click", (e) => {
const messageAction = e.target.closest && e.target.closest(".msg-action"); const copyBtn = e.target.closest && e.target.closest(".msg-copy");
if (messageAction) { if (copyBtn) {
const card = messageAction.closest(".msg[data-idx]"); const card = copyBtn.closest(".msg[data-idx]");
const idx = card && card.dataset.idx; const idx = card && card.dataset.idx;
const stored = idx != null const m = idx != null && state.loadedMessages.find((x) => String(x.idx) === String(idx));
&& state.loadedMessages.find((x) => String(x.idx) === String(idx)); const text = m && m.payload && typeof m.payload.content === "string" ? m.payload.content : "";
const text = stored && stored.payload && typeof stored.payload.content === "string" copyTextToClipboard(text, copyBtn);
? stored.payload.content
: "";
if (messageAction.classList.contains("msg-copy")) {
void copyTextToClipboard(text, messageAction);
} else if (messageAction.classList.contains("msg-revise")) {
loadMessageIntoComposer(text);
}
return; return;
} }
const askBtn = e.target.closest && e.target.closest(".ask-option"); const askBtn = e.target.closest && e.target.closest(".ask-option");