diff --git a/CHANGELOG.md b/CHANGELOG.md index be8c41d..da6d71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 工程口径的完整记录见 `PROGRESS.md` / git log。 +## 0.60.28 — 2026-08-04 + +- 对话中的 HTML 产物预览现在会充分利用可用宽度,图片和视频预览也适度放大;文件附件改为更清晰的偏方形卡片,可直接辨认文件类型、名称和预览入口。 + ## 0.60.27 — 2026-08-03 - 加强服务端命令执行、第三方依赖和文件处理链路的安全防护,并补齐持续类型检查,降低已知依赖漏洞与边界类型错误导致服务异常的风险。 diff --git a/PROGRESS.md b/PROGRESS.md index cedb341..50c3b79 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-08-03(工程类型门禁 + 安全审计清零,bump 0.60.27) +最后更新:2026-08-04(对话产物预览与文件 chip 优化,bump 0.60.28) --- @@ -21,6 +21,10 @@ ## 已完成关键能力 +### 2026-08-04 + +- **08-04 / 0.60.28 / 对话产物宽版预览 + 文件 chip 优化**:包含 HTML 产物的助手消息改用接近占满对话区的宽版布局,HTML 画布取消 760px 上限;内联视频上限提升至 720px,图片上限提升至 560px 并保持原比例。文件 chip 从小胶囊调整为偏方形附件卡,直接展示扩展名类型块、文件名和预览入口,粘贴附件同步统一样式并保留键盘焦点反馈。Node 前端 15 项、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API 或依赖变化。 + ### 2026-08-03 - **08-03 / 0.60.27 / 工程类型门禁 + 安全审计清零**:修复 141 个源文件中的 203 项 mypy 错误,新增按第三方无类型依赖与动态工具签名精确收口的 mypy 配置;外部协议规定的弱哈希显式标注非本地密码用途,host shell 前后台执行均改用明确解释器 argv,移除隐式 `shell=True`。提高 aiohttp、Pillow、cryptography、Starlette、python-multipart、pydantic-settings 与 pip 的安全版本下限,`pip check` 无冲突、`pip-audit` 无已知漏洞;新增显式 shell 回归测试,完整审计 448 项 unittest 全绿(17 skip),Ruff/mypy/Bandit/pip-audit 通过,engineering 80/100 PASS、security 100/100 PASS。无 schema/migration/API 变化,未连接生产 DB。 diff --git a/core/__init__.py b/core/__init__.py index 39ccda7..a3afb0f 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.60.27" +__version__ = "0.60.28" diff --git a/tests/frontend_preview.test.mjs b/tests/frontend_preview.test.mjs index bdcbe8a..4054833 100644 --- a/tests/frontend_preview.test.mjs +++ b/tests/frontend_preview.test.mjs @@ -56,6 +56,8 @@ test("assistant HTML artifacts render inline with lazy loading and an expand act assert.match(mediaJs, /new IntersectionObserver/); assert.match(mediaJs, /configureHtmlPreviewFrame\(frame, source/); assert.match(pageHtml, /\.art-html-frame/); + assert.match(pageHtml, /\.msg\.assistant:has\(\.art-html\)/); + assert.match(pageHtml, /\.art-html\s*\{[^}]*width:\s*100%[^}]*max-width:\s*none/s); assert.match(chatJs, /renderArtifactBarHtml\(extractArtifactRels\(p\.content, wd\), "html", state\.taskId/); assert.match(chatJs, /Array\.isArray\(m\.artifact_refs\)/); assert.match(chatJs, /renderArtifactBarHtml\(m\.artifact_refs, true, state\.taskId/); @@ -67,3 +69,12 @@ test("assistant HTML artifacts render inline with lazy loading and an expand act const sendMessage = chatJs.indexOf("async function sendMessage"); assert.ok(clickHandler >= 0 && expandHandler > clickHandler && expandHandler < sendMessage); }); + +test("artifact chips expose a compact file type and preview affordance", () => { + const mediaJs = readFileSync(new URL("../web/static/js/media.js", import.meta.url), "utf8"); + assert.match(mediaJs, /class="art-chip-icon"/); + assert.match(mediaJs, /class="art-chip-name"/); + assert.match(mediaJs, /class="art-chip-open">预览/); + assert.match(pageHtml, /\.art-chip-icon/); + assert.match(pageHtml, /\.art-chip:focus-visible/); +}); diff --git a/web/static/dev.html b/web/static/dev.html index 87bccd1..680ed6e 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -1031,29 +1031,39 @@ .tool-banner .kv.cost { color: #b34a4a; border-color: #e0c4c4; } .tool-banner .kv.model { color: var(--accent); border-color: #e0c4c4; } /* ───── artifact chips(对话内点产物预览/下载) ───── */ - .artifact-bar { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 4px; font-family: var(--mono); } + .artifact-bar { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px; } .art-chip { - font: inherit; font-size: 11px; line-height: 1.4; - padding: 2px 8px 2px 6px; border: 1px solid var(--border); - background: #fff; color: #555; border-radius: 999px; cursor: pointer; - max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - display: inline-flex; align-items: center; gap: 4px; - transition: var(--t); + min-width: 0; max-width: 340px; height: 38px; padding: 3px 9px 3px 4px; + border: 1px solid var(--border); border-radius: 5px; cursor: pointer; + background: linear-gradient(180deg, #fff, #fafafa); color: var(--text); + display: inline-flex; align-items: center; gap: 8px; + font: inherit; font-size: 12px; line-height: 1.3; text-align: left; + box-shadow: 0 1px 2px rgba(0,0,0,.04); transition: var(--t); } - .art-chip::before { content: "📄"; font-size: 11px; } - .art-chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); } - #chat-hint .art-chip { margin: 0 2px; vertical-align: middle; font-family: var(--mono); } + .art-chip-icon { + flex: 0 0 32px; height: 30px; display: inline-flex; align-items: center; justify-content: center; + border-radius: 3px; background: var(--accent-soft); color: var(--accent); + font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: -.2px; + } + .art-chip-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .art-chip-open { flex: 0 0 auto; color: var(--muted); font-size: 11px; } + .art-chip:hover, .art-chip:focus-visible { + background: var(--accent-soft); border-color: rgba(192,57,43,.45); color: var(--accent); + box-shadow: 0 2px 8px rgba(192,57,43,.10); transform: translateY(-1px); + } + .art-chip:hover .art-chip-open, .art-chip:focus-visible .art-chip-open { color: var(--accent); } + #chat-hint .art-chip { margin: 0 2px; vertical-align: middle; } .paste-chip-wrap { display: inline-flex; align-items: center; max-width: 280px; margin: 0 2px; vertical-align: middle; } .paste-chip-wrap .art-chip { margin: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; - max-width: 230px; + max-width: 280px; } .paste-chip-del { border: 1px solid var(--border); border-left: 0; background: #fff; color: var(--muted); - border-radius: 0 999px 999px 0; padding: 2px 7px; line-height: 1.4; + border-radius: 0 5px 5px 0; padding: 2px 7px; line-height: 1.4; font-size: 11px; cursor: pointer; transition: var(--t); } .paste-chip-del:hover { background: var(--c-red-bg); border-color: var(--c-red-bd); color: var(--c-red); } @@ -1068,12 +1078,12 @@ } .art-media .art-media-error { color: #b34a4a; } .art-media img { - display: block; max-width: 360px; max-height: 280px; + display: block; max-width: 560px; max-height: 420px; width: auto; height: auto; cursor: zoom-in; } + .art-media-video { flex: 0 1 720px; width: min(100%, 720px); } .art-media video { - display: block; max-width: 480px; max-height: 320px; - width: auto; height: auto; background: #000; + display: block; width: 100%; height: auto; max-height: 405px; background: #000; } #chat-form { @@ -1252,7 +1262,7 @@ text-overflow: ellipsis; white-space: nowrap; } .art-html { - flex: 1 1 100%; min-width: 0; max-width: 760px; + flex: 1 1 100%; width: 100%; min-width: 0; max-width: none; border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; background: #fff; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; } @@ -1273,6 +1283,9 @@ .art-html-viewport .art-media-loading, .art-html-viewport .art-media-error { padding: 10px; color: var(--muted); font-size: 11px; } .art-html-viewport .art-media-error { color: #b34a4a; } + /* HTML 是小型内容画布:只让含 HTML 预览的助手消息使用宽版布局, + 普通文本消息继续维持易读行长。 */ + .msg.assistant:has(.art-html) { width: calc(100% - 16px); max-width: none; } .preview-modes { display: inline-flex; align-items: center; gap: 2px; padding: 2px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--panel-muted); diff --git a/web/static/js/chat.js b/web/static/js/chat.js index cb785ac..77d038b 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -27,7 +27,7 @@ import { logout } from "./auth.js"; import { openWechatModal } from "./wechat.js"; import { openFilePreview, openPasteFilePreview, closePreviewIfShowing } from "./preview.js"; import { loadFiles, scheduleFilesRefresh, uploadFiles, formatUploadProgress } from "./files.js"; -import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js"; +import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, renderArtifactChipContent, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js"; import { applyProgressAction, cloneProgressSteps, progressActionsFromToolCalls } from "./progress.js"; import { refreshProcs, decorateBgprocCard, hasRunningProc, killTaskProcs } from "./procs.js"; @@ -1919,7 +1919,7 @@ function addAttachChips(saved) { existing.add(rel); const name = f.name || (rel.split("/").pop() || rel); tray.insertAdjacentHTML("beforeend", - ``); + ``); } tray.classList.toggle("show", attachCount() > 0); } diff --git a/web/static/js/media.js b/web/static/js/media.js index e01a0d3..3db2d03 100644 --- a/web/static/js/media.js +++ b/web/static/js/media.js @@ -167,6 +167,17 @@ export function extractArtifactRels(text, workingDir) { return out; } +// 文件 chip 的内容结构保持纯前端生成:扩展名充当轻量类型图标,不额外请求 +// 文件元数据;长文件名仍由 CSS 单行省略,完整路径保留在 button title 中。 +export function renderArtifactChipContent(name, rel) { + const rawExt = String(rel || "").split("/").pop().split(".").pop(); + const hasExt = rawExt && rawExt !== String(rel || "").split("/").pop(); + const ext = hasExt ? rawExt.toUpperCase().slice(0, 4) : "FILE"; + return `` + + `${escapeHtml(name)}` + + `预览`; +} + // inlineMode 控制升级范围:true=图片/视频/HTML,"html"=仅 HTML,false=全走 chip。 // 产物工具传 true;assistant 正文传 "html",避免重复内联图片/视频但让通用工具生成的 // HTML 有展示位;普通工具结果传 false,引用到的文件仍走 chip,避免把旧产物铺满消息。 @@ -191,7 +202,7 @@ export function renderArtifactBarHtml(rels, inlineMode = true, taskId = "", lega // 不在这里发请求避免 string-build 阶段失控的并发;upgrade 走 DOM walk 一次。 return `${escapeHtml(name)} 加载中…`; } - return ``; + return ``; }).join(""); return `
`; }