Compare commits
No commits in common. "8cf08c0f23d4b411e53955785a2a5a1059d6059f" and "137d27acddb6b5df8cea18edec36b81eb06a052e" have entirely different histories.
8cf08c0f23
...
137d27acdd
|
|
@ -5,10 +5,6 @@
|
|||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||
|
||||
## 0.60.28 — 2026-08-04
|
||||
|
||||
- 对话中的 HTML 产物预览现在会充分利用可用宽度,图片和视频预览也适度放大;文件附件改为更清晰的偏方形卡片,可直接辨认文件类型、名称和预览入口。
|
||||
|
||||
## 0.60.27 — 2026-08-03
|
||||
|
||||
- 加强服务端命令执行、第三方依赖和文件处理链路的安全防护,并补齐持续类型检查,降低已知依赖漏洞与边界类型错误导致服务异常的风险。
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-08-04(对话产物预览与文件 chip 优化,bump 0.60.28)
|
||||
最后更新:2026-08-03(工程类型门禁 + 安全审计清零,bump 0.60.27)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -21,10 +21,6 @@
|
|||
|
||||
## 已完成关键能力
|
||||
|
||||
### 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。
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.60.28"
|
||||
__version__ = "0.60.27"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ class AuditCheck:
|
|||
weight: float
|
||||
module: str
|
||||
arguments: tuple[str, ...]
|
||||
direct_network: bool = False
|
||||
|
||||
|
||||
ENGINEERING_CHECKS = (
|
||||
|
|
@ -70,21 +69,12 @@ SECURITY_CHECKS = (
|
|||
"pip_audit",
|
||||
(
|
||||
"-m", "pip_audit", "--local",
|
||||
"--cache-dir", "evaluation/.cache/pip-audit",
|
||||
"--progress-spinner", "off",
|
||||
),
|
||||
direct_network=True,
|
||||
),
|
||||
)
|
||||
|
||||
_PROXY_ENV_KEYS = (
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
)
|
||||
|
||||
|
||||
def _case_result(
|
||||
*,
|
||||
|
|
@ -129,32 +119,23 @@ def _tail(text: str, limit: int = 1200) -> str:
|
|||
return compact[-limit:] if compact else ""
|
||||
|
||||
|
||||
def _subprocess_env(*, direct_network: bool) -> dict[str, str]:
|
||||
env = {
|
||||
**os.environ,
|
||||
"PYTHONUTF8": "1",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
}
|
||||
if direct_network:
|
||||
for key in _PROXY_ENV_KEYS:
|
||||
env.pop(key, None)
|
||||
return env
|
||||
|
||||
|
||||
def _run(
|
||||
arguments: tuple[str, ...],
|
||||
*,
|
||||
repo_root: Path,
|
||||
timeout_s: float,
|
||||
evidence_path: Path | None = None,
|
||||
direct_network: bool = False,
|
||||
) -> tuple[bool, str, float]:
|
||||
started = time.monotonic()
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
[sys.executable, *arguments],
|
||||
cwd=repo_root,
|
||||
env=_subprocess_env(direct_network=direct_network),
|
||||
env={
|
||||
**os.environ,
|
||||
"PYTHONUTF8": "1",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
|
|
@ -324,7 +305,6 @@ def audit_security_repository(
|
|||
evidence_path=(
|
||||
evidence_dir / f"{check.id}.txt" if write_evidence else None
|
||||
),
|
||||
direct_network=check.direct_network,
|
||||
)
|
||||
total_duration += duration
|
||||
assertions.append(
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ 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/);
|
||||
|
|
@ -69,12 +67,3 @@ 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/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,14 +6,9 @@ import unittest
|
|||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from evaluation.audit import (
|
||||
_subprocess_env,
|
||||
audit_repository,
|
||||
audit_security_repository,
|
||||
)
|
||||
from evaluation.client import EvalClientError, require_safe_base_url
|
||||
from evaluation.audit import audit_repository, audit_security_repository
|
||||
from evaluation.combine import combine_reports
|
||||
from evaluation.inspect_bridge import export_inspect_jsonl
|
||||
from evaluation.models import AssertionSpec, EvalConfigError, RunObservation
|
||||
|
|
@ -140,33 +135,6 @@ class ClientSafetyTests(unittest.TestCase):
|
|||
|
||||
|
||||
class ReportTests(unittest.TestCase):
|
||||
def test_direct_audit_environment_ignores_proxy(self):
|
||||
for key in (
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
):
|
||||
with self.subTest(key=key), patch.dict(
|
||||
"evaluation.audit.os.environ",
|
||||
{key: "http://proxy.example"},
|
||||
clear=True,
|
||||
):
|
||||
direct_env = _subprocess_env(direct_network=True)
|
||||
inherited_env = _subprocess_env(direct_network=False)
|
||||
|
||||
direct_keys = {item.casefold() for item in direct_env}
|
||||
inherited_by_key = {
|
||||
item.casefold(): value for item, value in inherited_env.items()
|
||||
}
|
||||
self.assertNotIn(key.casefold(), direct_keys)
|
||||
self.assertEqual(
|
||||
inherited_by_key[key.casefold()], "http://proxy.example"
|
||||
)
|
||||
self.assertEqual(direct_env["PYTHONUTF8"], "1")
|
||||
|
||||
def test_combine_existing_dimension_reports(self):
|
||||
engineering = {
|
||||
"suite": {"name": "engineering", "version": "1"},
|
||||
|
|
|
|||
|
|
@ -1031,39 +1031,29 @@
|
|||
.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: 6px; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.artifact-bar { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 4px; font-family: var(--mono); }
|
||||
.art-chip {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
.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; }
|
||||
.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); }
|
||||
.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: 280px;
|
||||
max-width: 230px;
|
||||
}
|
||||
.paste-chip-del {
|
||||
border: 1px solid var(--border); border-left: 0; background: #fff; color: var(--muted);
|
||||
border-radius: 0 5px 5px 0; padding: 2px 7px; line-height: 1.4;
|
||||
border-radius: 0 999px 999px 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); }
|
||||
|
|
@ -1078,12 +1068,12 @@
|
|||
}
|
||||
.art-media .art-media-error { color: #b34a4a; }
|
||||
.art-media img {
|
||||
display: block; max-width: 560px; max-height: 420px;
|
||||
display: block; max-width: 360px; max-height: 280px;
|
||||
width: auto; height: auto; cursor: zoom-in;
|
||||
}
|
||||
.art-media-video { flex: 0 1 720px; width: min(100%, 720px); }
|
||||
.art-media video {
|
||||
display: block; width: 100%; height: auto; max-height: 405px; background: #000;
|
||||
display: block; max-width: 480px; max-height: 320px;
|
||||
width: auto; height: auto; background: #000;
|
||||
}
|
||||
|
||||
#chat-form {
|
||||
|
|
@ -1262,7 +1252,7 @@
|
|||
text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.art-html {
|
||||
flex: 1 1 100%; width: 100%; min-width: 0; max-width: none;
|
||||
flex: 1 1 100%; min-width: 0; max-width: 760px;
|
||||
border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden;
|
||||
background: #fff; font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
|
@ -1283,9 +1273,6 @@
|
|||
.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);
|
||||
|
|
|
|||
|
|
@ -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, renderArtifactChipContent, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js";
|
||||
import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, 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",
|
||||
`<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>`);
|
||||
`<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>`);
|
||||
}
|
||||
tray.classList.toggle("show", attachCount() > 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,17 +167,6 @@ 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 `<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。
|
||||
// 产物工具传 true;assistant 正文传 "html",避免重复内联图片/视频但让通用工具生成的
|
||||
// HTML 有展示位;普通工具结果传 false,引用到的文件仍走 chip,避免把旧产物铺满消息。
|
||||
|
|
@ -202,7 +191,7 @@ export function renderArtifactBarHtml(rels, inlineMode = true, taskId = "", lega
|
|||
// 不在这里发请求避免 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 `<button type="button" class="art-chip" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr} title="${escapeHtml(rel)} · 点击预览(可下载)">${renderArtifactChipContent(name, rel)}</button>`;
|
||||
return `<button type="button" class="art-chip" data-rel="${escapeHtml(rel)}"${taskAttr}${legacyAttr} title="${escapeHtml(rel)} · 点击预览(可下载)">${escapeHtml(name)}</button>`;
|
||||
}).join("");
|
||||
return `<div class="artifact-bar">${items}</div>`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue