feat(web): widen artifact previews and refine file chips
This commit is contained in:
parent
0bc8c28a02
commit
8cf08c0f23
|
|
@ -5,6 +5,10 @@
|
||||||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||||
|
|
||||||
|
## 0.60.28 — 2026-08-04
|
||||||
|
|
||||||
|
- 对话中的 HTML 产物预览现在会充分利用可用宽度,图片和视频预览也适度放大;文件附件改为更清晰的偏方形卡片,可直接辨认文件类型、名称和预览入口。
|
||||||
|
|
||||||
## 0.60.27 — 2026-08-03
|
## 0.60.27 — 2026-08-03
|
||||||
|
|
||||||
- 加强服务端命令执行、第三方依赖和文件处理链路的安全防护,并补齐持续类型检查,降低已知依赖漏洞与边界类型错误导致服务异常的风险。
|
- 加强服务端命令执行、第三方依赖和文件处理链路的安全防护,并补齐持续类型检查,降低已知依赖漏洞与边界类型错误导致服务异常的风险。
|
||||||
|
|
|
||||||
|
|
@ -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-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
|
### 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。
|
- **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。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.60.27"
|
__version__ = "0.60.28"
|
||||||
|
|
|
||||||
|
|
@ -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, /new IntersectionObserver/);
|
||||||
assert.match(mediaJs, /configureHtmlPreviewFrame\(frame, source/);
|
assert.match(mediaJs, /configureHtmlPreviewFrame\(frame, source/);
|
||||||
assert.match(pageHtml, /\.art-html-frame/);
|
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, /renderArtifactBarHtml\(extractArtifactRels\(p\.content, wd\), "html", state\.taskId/);
|
||||||
assert.match(chatJs, /Array\.isArray\(m\.artifact_refs\)/);
|
assert.match(chatJs, /Array\.isArray\(m\.artifact_refs\)/);
|
||||||
assert.match(chatJs, /renderArtifactBarHtml\(m\.artifact_refs, true, state\.taskId/);
|
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");
|
const sendMessage = chatJs.indexOf("async function sendMessage");
|
||||||
assert.ok(clickHandler >= 0 && expandHandler > clickHandler && expandHandler < 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/);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1031,29 +1031,39 @@
|
||||||
.tool-banner .kv.cost { color: #b34a4a; border-color: #e0c4c4; }
|
.tool-banner .kv.cost { color: #b34a4a; border-color: #e0c4c4; }
|
||||||
.tool-banner .kv.model { color: var(--accent); border-color: #e0c4c4; }
|
.tool-banner .kv.model { color: var(--accent); border-color: #e0c4c4; }
|
||||||
/* ───── artifact chips(对话内点产物预览/下载) ───── */
|
/* ───── 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 {
|
.art-chip {
|
||||||
font: inherit; font-size: 11px; line-height: 1.4;
|
min-width: 0; max-width: 340px; height: 38px; padding: 3px 9px 3px 4px;
|
||||||
padding: 2px 8px 2px 6px; border: 1px solid var(--border);
|
border: 1px solid var(--border); border-radius: 5px; cursor: pointer;
|
||||||
background: #fff; color: #555; border-radius: 999px; cursor: pointer;
|
background: linear-gradient(180deg, #fff, #fafafa); color: var(--text);
|
||||||
max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
display: inline-flex; align-items: center; gap: 8px;
|
||||||
display: inline-flex; align-items: center; gap: 4px;
|
font: inherit; font-size: 12px; line-height: 1.3; text-align: left;
|
||||||
transition: var(--t);
|
box-shadow: 0 1px 2px rgba(0,0,0,.04); transition: var(--t);
|
||||||
}
|
}
|
||||||
.art-chip::before { content: "📄"; font-size: 11px; }
|
.art-chip-icon {
|
||||||
.art-chip:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
|
flex: 0 0 32px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
|
||||||
#chat-hint .art-chip { margin: 0 2px; vertical-align: middle; font-family: var(--mono); }
|
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 {
|
.paste-chip-wrap {
|
||||||
display: inline-flex; align-items: center; max-width: 280px; margin: 0 2px;
|
display: inline-flex; align-items: center; max-width: 280px; margin: 0 2px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.paste-chip-wrap .art-chip {
|
.paste-chip-wrap .art-chip {
|
||||||
margin: 0; border-top-right-radius: 0; border-bottom-right-radius: 0;
|
margin: 0; border-top-right-radius: 0; border-bottom-right-radius: 0;
|
||||||
max-width: 230px;
|
max-width: 280px;
|
||||||
}
|
}
|
||||||
.paste-chip-del {
|
.paste-chip-del {
|
||||||
border: 1px solid var(--border); border-left: 0; background: #fff; color: var(--muted);
|
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);
|
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); }
|
.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 .art-media-error { color: #b34a4a; }
|
||||||
.art-media img {
|
.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;
|
width: auto; height: auto; cursor: zoom-in;
|
||||||
}
|
}
|
||||||
|
.art-media-video { flex: 0 1 720px; width: min(100%, 720px); }
|
||||||
.art-media video {
|
.art-media video {
|
||||||
display: block; max-width: 480px; max-height: 320px;
|
display: block; width: 100%; height: auto; max-height: 405px; background: #000;
|
||||||
width: auto; height: auto; background: #000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-form {
|
#chat-form {
|
||||||
|
|
@ -1252,7 +1262,7 @@
|
||||||
text-overflow: ellipsis; white-space: nowrap;
|
text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.art-html {
|
.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;
|
border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden;
|
||||||
background: #fff; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
|
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-loading,
|
||||||
.art-html-viewport .art-media-error { padding: 10px; color: var(--muted); font-size: 11px; }
|
.art-html-viewport .art-media-error { padding: 10px; color: var(--muted); font-size: 11px; }
|
||||||
.art-html-viewport .art-media-error { color: #b34a4a; }
|
.art-html-viewport .art-media-error { color: #b34a4a; }
|
||||||
|
/* HTML 是小型内容画布:只让含 HTML 预览的助手消息使用宽版布局,
|
||||||
|
普通文本消息继续维持易读行长。 */
|
||||||
|
.msg.assistant:has(.art-html) { width: calc(100% - 16px); max-width: none; }
|
||||||
.preview-modes {
|
.preview-modes {
|
||||||
display: inline-flex; align-items: center; gap: 2px; padding: 2px;
|
display: inline-flex; align-items: center; gap: 2px; padding: 2px;
|
||||||
border: 1px solid var(--border); border-radius: var(--r-md); background: var(--panel-muted);
|
border: 1px solid var(--border); border-radius: var(--r-md); background: var(--panel-muted);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import { logout } from "./auth.js";
|
||||||
import { openWechatModal } from "./wechat.js";
|
import { openWechatModal } from "./wechat.js";
|
||||||
import { openFilePreview, openPasteFilePreview, closePreviewIfShowing } from "./preview.js";
|
import { openFilePreview, openPasteFilePreview, closePreviewIfShowing } from "./preview.js";
|
||||||
import { loadFiles, scheduleFilesRefresh, uploadFiles, formatUploadProgress } from "./files.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 { applyProgressAction, cloneProgressSteps, progressActionsFromToolCalls } from "./progress.js";
|
||||||
import { refreshProcs, decorateBgprocCard, hasRunningProc, killTaskProcs } from "./procs.js";
|
import { refreshProcs, decorateBgprocCard, hasRunningProc, killTaskProcs } from "./procs.js";
|
||||||
|
|
||||||
|
|
@ -1919,7 +1919,7 @@ function addAttachChips(saved) {
|
||||||
existing.add(rel);
|
existing.add(rel);
|
||||||
const name = f.name || (rel.split("/").pop() || rel);
|
const name = f.name || (rel.split("/").pop() || rel);
|
||||||
tray.insertAdjacentHTML("beforeend",
|
tray.insertAdjacentHTML("beforeend",
|
||||||
`<span class="paste-chip-wrap" data-rel="${escapeHtml(rel)}" data-kind="${f.attachmentKind === "image" ? "image" : "file"}"><button type="button" class="art-chip paste-chip" data-rel="${escapeHtml(rel)}" title="${escapeHtml(rel)} · 点击预览">${escapeHtml(name)}</button><button type="button" class="paste-chip-del" data-rel="${escapeHtml(rel)}" title="删除该文件">×</button></span>`);
|
`<span class="paste-chip-wrap" data-rel="${escapeHtml(rel)}" data-kind="${f.attachmentKind === "image" ? "image" : "file"}"><button type="button" class="art-chip paste-chip" data-rel="${escapeHtml(rel)}" title="${escapeHtml(rel)} · 点击预览">${renderArtifactChipContent(name, rel)}</button><button type="button" class="paste-chip-del" data-rel="${escapeHtml(rel)}" title="删除该文件">×</button></span>`);
|
||||||
}
|
}
|
||||||
tray.classList.toggle("show", attachCount() > 0);
|
tray.classList.toggle("show", attachCount() > 0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,17 @@ export function extractArtifactRels(text, workingDir) {
|
||||||
return out;
|
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 `<span class="art-chip-icon" aria-hidden="true">${escapeHtml(ext)}</span>`
|
||||||
|
+ `<span class="art-chip-name">${escapeHtml(name)}</span>`
|
||||||
|
+ `<span class="art-chip-open">预览</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
// inlineMode 控制升级范围:true=图片/视频/HTML,"html"=仅 HTML,false=全走 chip。
|
// inlineMode 控制升级范围:true=图片/视频/HTML,"html"=仅 HTML,false=全走 chip。
|
||||||
// 产物工具传 true;assistant 正文传 "html",避免重复内联图片/视频但让通用工具生成的
|
// 产物工具传 true;assistant 正文传 "html",避免重复内联图片/视频但让通用工具生成的
|
||||||
// HTML 有展示位;普通工具结果传 false,引用到的文件仍走 chip,避免把旧产物铺满消息。
|
// HTML 有展示位;普通工具结果传 false,引用到的文件仍走 chip,避免把旧产物铺满消息。
|
||||||
|
|
@ -191,7 +202,7 @@ export function renderArtifactBarHtml(rels, inlineMode = true, taskId = "", lega
|
||||||
// 不在这里发请求避免 string-build 阶段失控的并发;upgrade 走 DOM walk 一次。
|
// 不在这里发请求避免 string-build 阶段失控的并发;upgrade 走 DOM walk 一次。
|
||||||
return `<span class="art-media art-media-${cat}" data-rel="${escapeHtml(rel)}" data-cat="${cat}"${taskAttr}${legacyAttr} title="${escapeHtml(rel)}"><span class="art-media-loading">${escapeHtml(name)} 加载中…</span></span>`;
|
return `<span class="art-media art-media-${cat}" data-rel="${escapeHtml(rel)}" data-cat="${cat}"${taskAttr}${legacyAttr} title="${escapeHtml(rel)}"><span class="art-media-loading">${escapeHtml(name)} 加载中…</span></span>`;
|
||||||
}
|
}
|
||||||
return `<button type="button" class="art-chip" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr} title="${escapeHtml(rel)} · 点击预览(可下载)">${escapeHtml(name)}</button>`;
|
return `<button type="button" class="art-chip" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr} title="${escapeHtml(rel)} · 点击预览(可下载)">${renderArtifactChipContent(name, rel)}</button>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
return `<div class="artifact-bar">${items}</div>`;
|
return `<div class="artifact-bar">${items}</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue