codebee 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +20 -3
- package/app/core/aiflavor.py +63 -9
- package/app/core/attachments.py +117 -4
- package/app/core/dispatch.py +34 -5
- package/app/core/dispatch_log.py +113 -0
- package/app/core/errorlog.py +1 -43
- package/app/core/jobs.py +30 -4
- package/app/core/modelhub.py +2 -0
- package/app/core/paths.py +2 -2
- package/app/core/pipeline.py +252 -12
- package/app/core/portguard.py +111 -0
- package/app/core/portscan.py +188 -0
- package/app/core/redact.py +38 -0
- package/app/core/router.py +58 -14
- package/app/core/runner.py +15 -8
- package/app/core/selfupdate.py +86 -27
- package/app/core/skills.py +37 -19
- package/app/core/store.py +2 -4
- package/app/core/task_compile.py +5 -1
- package/app/core/usage.py +226 -24
- package/app/main.py +82 -9
- package/app/pet.py +34 -11
- package/app/ui/app.js +177 -1
- package/app/ui/i18n.js +36 -0
- package/app/ui/index.html +1144 -1137
- package/app/ui/style.css +34 -0
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -3315,6 +3315,24 @@ function setEditRetry(taskId, status, taskExists) {
|
|
|
3315
3315
|
if (bn) bn.classList.toggle("hidden", !taskExists || failedish);
|
|
3316
3316
|
}
|
|
3317
3317
|
|
|
3318
|
+
/* 详情页归档按钮:非运行中显示;已归档任务显示「取消归档」。
|
|
3319
|
+
* 点击走 archiveTask(与右键菜单同一链路:取消归档会自动恢复侧栏目录显示)。 */
|
|
3320
|
+
function syncArchBtn(task, active) {
|
|
3321
|
+
const b = $("btn-arch");
|
|
3322
|
+
if (!b) return;
|
|
3323
|
+
const show = !!task && !active;
|
|
3324
|
+
b.classList.toggle("hidden", !show);
|
|
3325
|
+
if (!show) return;
|
|
3326
|
+
const archived = !!task.archived;
|
|
3327
|
+
b.textContent = archived ? t("取消归档") : t("归档任务");
|
|
3328
|
+
b.onclick = async () => {
|
|
3329
|
+
try {
|
|
3330
|
+
await archiveTask(task.id, !archived);
|
|
3331
|
+
b.textContent = archived ? t("归档任务") : t("取消归档");
|
|
3332
|
+
} catch (e) { /* archiveTask 内部已 toast */ }
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3318
3336
|
/* 重试按钮三态:失败/取消 → 「↻ 继续任务」(断点续跑语义);连载任务完整跑完
|
|
3319
3337
|
* 但质量未达标(verdict.publishable=false)→ 「↻ 重写未达标章」——后端 retry
|
|
3320
3338
|
* 会把未过线章从继承清单剔除,只重写重评这几章(store.retry_task 未达标分支);
|
|
@@ -3346,6 +3364,7 @@ function renderTaskDetail() {
|
|
|
3346
3364
|
if (bc) bc.classList.toggle("hidden",
|
|
3347
3365
|
!(tk && tk.serial && tk.status !== "running" && tk.status !== "queued"));
|
|
3348
3366
|
setEditRetry(tk ? tk.id : "", tk ? tk.status : "", !!tk);
|
|
3367
|
+
syncArchBtn(tk, tk && (tk.status === "running" || tk.status === "queued"));
|
|
3349
3368
|
const isTask = ((S.state || {}).tasks || []).some((t2) => t2.id === key);
|
|
3350
3369
|
if (isTask) {
|
|
3351
3370
|
api("/api/tasks/" + encodeURIComponent(key) + "/runs")
|
|
@@ -3420,6 +3439,7 @@ function drawTaskDetail(key, runs) {
|
|
|
3420
3439
|
'<span class="stat">tokens <b>' + sum("tokens") + "</b></span>" +
|
|
3421
3440
|
(latest.error ? '<span class="stat err">' + errTag(latest.error) + esc(latest.error.slice(0, 200)) + "</span>" : "");
|
|
3422
3441
|
$("rd-plan").classList.add("hidden");
|
|
3442
|
+
renderRouting(latest);
|
|
3423
3443
|
let html = "";
|
|
3424
3444
|
ordered.forEach((r, i) => {
|
|
3425
3445
|
const runNo = runs.length - i;
|
|
@@ -3776,6 +3796,9 @@ async function renderRunDetail() {
|
|
|
3776
3796
|
S.cancelTargetRunId = active ? run.id : null;
|
|
3777
3797
|
$("btn-delete").classList.toggle("hidden", active);
|
|
3778
3798
|
$("btn-share").classList.toggle("hidden", active); // 分享页:结束后可生成自包含 HTML
|
|
3799
|
+
// 归档按钮:非运行中任务可归档/取消归档(此前只有侧栏右键菜单入口,
|
|
3800
|
+
// 用户反馈「归档按钮不见了」——补显式入口,文案随状态切换)
|
|
3801
|
+
syncArchBtn(rcTask, active);
|
|
3779
3802
|
$("btn-talk").classList.toggle("hidden", !(run.task_id && !chatEngineIsDirect(run)));
|
|
3780
3803
|
const rcTask = ((S.state || {}).tasks || []).find((x) => x.id === run.task_id);
|
|
3781
3804
|
setRetryBtn(run, rcTask);
|
|
@@ -3800,6 +3823,7 @@ async function renderRunDetail() {
|
|
|
3800
3823
|
'<span class="stat tasksum hidden" id="rd-meta-task"></span>';
|
|
3801
3824
|
if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
|
|
3802
3825
|
renderPlan(run);
|
|
3826
|
+
renderRouting(run);
|
|
3803
3827
|
// 详情页优先展示最近一步,排查运行中的任务时无需滚到底部;
|
|
3804
3828
|
// 蜂巢泳道仍按原始流程顺序呈现,避免破坏阶段语义。
|
|
3805
3829
|
$("rd-steps").innerHTML = (run.steps || []).slice().reverse().map((s) =>
|
|
@@ -5443,6 +5467,75 @@ function renderPlan(run) {
|
|
|
5443
5467
|
'<span class="role">' + esc(s.title || "") + "</span>" +
|
|
5444
5468
|
'<span class="sum">' + esc(s.detail || "") + "</span></div>").join("") + "</div>" + routeHtml;
|
|
5445
5469
|
}
|
|
5470
|
+
|
|
5471
|
+
/* 运行详情的统一调度审计:任务画像和实际选路来自同一次编译结果。
|
|
5472
|
+
* 旧运行没有 task_spec/route_plan 时保持隐藏,避免把历史数据伪装成当前决策。 */
|
|
5473
|
+
function renderRouting(run) {
|
|
5474
|
+
let box = $("rd-routing");
|
|
5475
|
+
if (!box) {
|
|
5476
|
+
const steps = $("rd-steps");
|
|
5477
|
+
if (!steps || !steps.parentNode) return;
|
|
5478
|
+
box = document.createElement("div");
|
|
5479
|
+
box.id = "rd-routing";
|
|
5480
|
+
box.className = "rd-routing hidden";
|
|
5481
|
+
steps.parentNode.insertBefore(box, steps);
|
|
5482
|
+
}
|
|
5483
|
+
const spec = run && run.task_spec;
|
|
5484
|
+
const plan = run && run.route_plan;
|
|
5485
|
+
if (!spec || !plan) { box.classList.add("hidden"); box.innerHTML = ""; return; }
|
|
5486
|
+
const type = spec.type || "direct";
|
|
5487
|
+
const difficulty = spec.difficulty || "default";
|
|
5488
|
+
const dimension = spec.dimension || "reasoning";
|
|
5489
|
+
const caps = Array.isArray(spec.capabilities) ? spec.capabilities.filter(Boolean) : [];
|
|
5490
|
+
const typeLabel = ((flowById(type) || {}).name) || type;
|
|
5491
|
+
const difficultyLabel = t(({ easy: "简单", default: "标准", hard: "困难" })[difficulty] || difficulty);
|
|
5492
|
+
const dimensionLabel = t(({ coding: "编码", writing: "写作", reasoning: "推理", vision: "视觉" })[dimension] || dimension);
|
|
5493
|
+
const capLabels = {
|
|
5494
|
+
coding: "编码", writing: "写作", reasoning: "推理", vision: "视觉",
|
|
5495
|
+
filesystem: "文件读写", verification: "验证", long_context: "长上下文",
|
|
5496
|
+
continuity: "连续性", attachments: "附件",
|
|
5497
|
+
};
|
|
5498
|
+
const capsText = caps.length ? caps.map((x) => t(capLabels[x] || x)).join(t("、")) : t("通用能力");
|
|
5499
|
+
const selectedLabel = (entry) => entry && (entry.label || entry.agent_id || "") || t("未选定");
|
|
5500
|
+
const roleBlock = (key, title) => {
|
|
5501
|
+
const route = plan[key] || {};
|
|
5502
|
+
const candidates = Array.isArray(route.candidates) ? route.candidates : [];
|
|
5503
|
+
if (!route.selected && !candidates.length) return "";
|
|
5504
|
+
const selected = candidates.find((x) => x.agent_id === route.selected) || candidates[0];
|
|
5505
|
+
const participants = Array.isArray(route.participants) ? route.participants : [];
|
|
5506
|
+
const activeIds = participants.length ? participants.slice() : (route.selected ? [route.selected] : []);
|
|
5507
|
+
if (route.selected && activeIds.includes(route.selected)) {
|
|
5508
|
+
activeIds.splice(activeIds.indexOf(route.selected), 1);
|
|
5509
|
+
activeIds.unshift(route.selected);
|
|
5510
|
+
}
|
|
5511
|
+
const activeLabels = activeIds.map((id) => {
|
|
5512
|
+
const item = candidates.find((x) => x.agent_id === id);
|
|
5513
|
+
return selectedLabel(item || { agent_id: id });
|
|
5514
|
+
}).filter(Boolean);
|
|
5515
|
+
const fallback = Array.isArray(route.fallback) ? route.fallback : [];
|
|
5516
|
+
const fallbackLabels = fallback.map((id) => {
|
|
5517
|
+
const item = candidates.find((x) => x.agent_id === id);
|
|
5518
|
+
return selectedLabel(item || { agent_id: id });
|
|
5519
|
+
}).filter(Boolean);
|
|
5520
|
+
const rows = candidates.map((x) =>
|
|
5521
|
+
'<div class="rd-route-row"><span class="rd-route-score">' + esc(Number(x.score || 0).toFixed(1)) +
|
|
5522
|
+
'</span><span class="rd-route-agent">' + esc(selectedLabel(x)) + '</span><span class="rd-route-reason">' +
|
|
5523
|
+
esc(x.reason || "") + '</span></div>').join("");
|
|
5524
|
+
return '<div class="rd-route-role"><div class="rd-route-role-head"><b>' + esc(title) +
|
|
5525
|
+
'</b><span>' + esc(activeLabels.join(" + ") || selectedLabel(selected)) + '</span></div>' +
|
|
5526
|
+
(route.selection_reason ? '<div class="rd-route-selection">' + esc(route.selection_reason) + '</div>' : "") +
|
|
5527
|
+
(fallbackLabels.length ? '<div class="rd-route-fallback">' + esc(t("降级:")) + esc(fallbackLabels.join(" → ")) + '</div>' : "") +
|
|
5528
|
+
(rows ? '<details class="rd-route-candidates"><summary>' + esc(t("查看候选评分")) + '</summary>' + rows + '</details>' : "") +
|
|
5529
|
+
'</div>';
|
|
5530
|
+
};
|
|
5531
|
+
const roles = [roleBlock("implement", t("执行")), roleBlock("review", t("评审"))].filter(Boolean).join("");
|
|
5532
|
+
if (!roles) { box.classList.add("hidden"); box.innerHTML = ""; return; }
|
|
5533
|
+
box.classList.remove("hidden");
|
|
5534
|
+
box.innerHTML = '<div class="rd-routing-head"><span class="rd-routing-title">' + esc(t("任务画像与调度")) +
|
|
5535
|
+
'</span><span class="rd-routing-spec">' + esc(t("{0} · {1} · {2}", t(typeLabel), difficultyLabel, dimensionLabel)) + '</span></div>' +
|
|
5536
|
+
'<div class="rd-routing-caps"><span>' + esc(t("需要:")) + '</span>' + esc(capsText) + '</div>' +
|
|
5537
|
+
'<div class="rd-route-roles">' + roles + '</div>';
|
|
5538
|
+
}
|
|
5446
5539
|
/* 日志框顶部状态条:运行中 → ● 实时(呼吸点 + 最后刷新时间);结束 → 灰字已结束。
|
|
5447
5540
|
* 没有它,用户分不清"日志在刷但 CLI 暂无输出"和"刷新坏了"——两者长得一模一样。 */
|
|
5448
5541
|
function logLiveBadge(live) {
|
|
@@ -9216,7 +9309,7 @@ function renderSu() {
|
|
|
9216
9309
|
t(" ") + "<a href=\"#\" onclick=\"event.preventDefault();suApply()\">" + t("立即升级") + "</a></b></p>";
|
|
9217
9310
|
if (SU.notes) {
|
|
9218
9311
|
html += "<div class=\"hint\"><b>" + t("新版本更新内容") + t(":") + "</b>" +
|
|
9219
|
-
|
|
9312
|
+
renderRelnotes(SU.notes) + "</div>";
|
|
9220
9313
|
}
|
|
9221
9314
|
} else if (SU.mode === "npm" && !SU.note) {
|
|
9222
9315
|
html += "<p class=\"hint\">" + t("已是最新版。") + "</p>";
|
|
@@ -9232,6 +9325,31 @@ function renderSu() {
|
|
|
9232
9325
|
renderUpdPill();
|
|
9233
9326
|
}
|
|
9234
9327
|
|
|
9328
|
+
/* 更新说明分类渲染:### 行渲染为小节标题,- 行渲染为条目(对照主流产品的
|
|
9329
|
+
* 更新日志样式:问题修复/新功能分组,一眼看懂)。全部 esc 后拼接。 */
|
|
9330
|
+
function renderRelnotes(txt) {
|
|
9331
|
+
const lines = String(txt || "").replace(/\r/g, "").split("\n");
|
|
9332
|
+
let html = "", inList = false;
|
|
9333
|
+
const closeList = () => { if (inList) { html += "</div>"; inList = false; } };
|
|
9334
|
+
for (const raw of lines) {
|
|
9335
|
+
const line = raw.trim();
|
|
9336
|
+
if (!line) continue;
|
|
9337
|
+
if (/^#{2,4}\s+/.test(line)) {
|
|
9338
|
+
closeList();
|
|
9339
|
+
html += "<p style=\"margin:6px 0 2px\"><b>" + esc(line.replace(/^#{2,4}\s+/, "")) + "</b></p>";
|
|
9340
|
+
} else if (/^[-*]\s+/.test(line)) {
|
|
9341
|
+
if (!inList) { html += "<div style=\"margin:2px 0 2px 4px\">"; inList = true; }
|
|
9342
|
+
html += "<div style=\"display:flex;gap:6px\"><span style=\"opacity:.6\">·</span>" +
|
|
9343
|
+
"<span style=\"flex:1\">" + esc(line.replace(/^[-*]\s+/, "")) + "</span></div>";
|
|
9344
|
+
} else {
|
|
9345
|
+
closeList();
|
|
9346
|
+
html += "<p style=\"margin:2px 0\">" + esc(line) + "</p>";
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
closeList();
|
|
9350
|
+
return html || ("<pre class=\"su-notes\">" + esc(txt) + "</pre>");
|
|
9351
|
+
}
|
|
9352
|
+
|
|
9235
9353
|
/* 顶栏左上角常驻更新胶囊:有新版才出现(同版本被忽略后不再出现),
|
|
9236
9354
|
* 点击直达「关于与更新」,× 忽略本版本(与 su.seen 记账键同一套语义) */
|
|
9237
9355
|
function renderUpdPill() {
|
|
@@ -9348,6 +9466,64 @@ async function reportIssue() {
|
|
|
9348
9466
|
}
|
|
9349
9467
|
}
|
|
9350
9468
|
|
|
9469
|
+
/* 端口占用诊断(借鉴 leftopen):谁占着端口、属于哪个项目;可温和关闭 */
|
|
9470
|
+
async function scanPorts() {
|
|
9471
|
+
const box = $("ports-list");
|
|
9472
|
+
if (!box) return;
|
|
9473
|
+
const b = $("ports-scan");
|
|
9474
|
+
if (b) b.disabled = true;
|
|
9475
|
+
try {
|
|
9476
|
+
const r = await api("/api/ports");
|
|
9477
|
+
const ports = r.ports || [];
|
|
9478
|
+
if (!ports.length) { box.innerHTML = '<span class="hint">' + esc(t("没有发现监听端口")) + "</span>"; return; }
|
|
9479
|
+
box.innerHTML = "";
|
|
9480
|
+
const tbl = document.createElement("table");
|
|
9481
|
+
tbl.style.width = "100%";
|
|
9482
|
+
tbl.style.fontSize = "12px";
|
|
9483
|
+
tbl.style.borderCollapse = "collapse";
|
|
9484
|
+
const th = (s) => '<th style="text-align:left;padding:4px 8px;opacity:.7">' + esc(s) + "</th>";
|
|
9485
|
+
tbl.innerHTML = "<thead><tr>" + th(t("端口")) + th(t("进程")) + th(t("项目")) + th("") + "</tr></thead>";
|
|
9486
|
+
const tb = document.createElement("tbody");
|
|
9487
|
+
for (const p of ports) {
|
|
9488
|
+
const tr = document.createElement("tr");
|
|
9489
|
+
const td = (s) => '<td style="padding:4px 8px;border-top:1px solid var(--border)">' + s + "</td>";
|
|
9490
|
+
tr.innerHTML =
|
|
9491
|
+
td(p.port + (p.local_only ? ' <span class="pill">' + esc(t("仅本机")) + "</span>" : "") + (p.self ? ' <span class="pill">' + esc(t("本服务")) + "</span>" : "")) +
|
|
9492
|
+
td(esc((p.process || "?") + (p.pid ? " · pid " + p.pid : ""))) +
|
|
9493
|
+
td(p.project ? esc(p.project) : "-") +
|
|
9494
|
+
"<td></td>";
|
|
9495
|
+
const act = tr.lastElementChild;
|
|
9496
|
+
if (!p.self && p.pid) {
|
|
9497
|
+
const btn = document.createElement("button");
|
|
9498
|
+
btn.className = "ghost small";
|
|
9499
|
+
btn.textContent = t("关闭");
|
|
9500
|
+
btn.onclick = () => closePort(p.port);
|
|
9501
|
+
act.appendChild(btn);
|
|
9502
|
+
}
|
|
9503
|
+
tb.appendChild(tr);
|
|
9504
|
+
}
|
|
9505
|
+
tbl.appendChild(tb);
|
|
9506
|
+
box.appendChild(tbl);
|
|
9507
|
+
} catch (e) {
|
|
9508
|
+
box.innerHTML = '<span class="hint">' + esc(t("扫描失败:") + (e.message || e)) + "</span>";
|
|
9509
|
+
} finally {
|
|
9510
|
+
if (b) b.disabled = false;
|
|
9511
|
+
}
|
|
9512
|
+
}
|
|
9513
|
+
|
|
9514
|
+
async function closePort(port) {
|
|
9515
|
+
if (!confirm(t("向占用端口 %1 的进程发送温和关闭信号?(系统进程会被拒绝)").replace("%1", port))) return;
|
|
9516
|
+
try {
|
|
9517
|
+
const r = await api("/api/ports/close", { method: "POST", body: JSON.stringify({ port }) });
|
|
9518
|
+
toast(r.message || (r.ok ? t("已发送关闭信号") : t("未能关闭")), !r.ok);
|
|
9519
|
+
scanPorts();
|
|
9520
|
+
} catch (e) {
|
|
9521
|
+
toast(e.message || t("关闭失败"), true);
|
|
9522
|
+
}
|
|
9523
|
+
}
|
|
9524
|
+
|
|
9525
|
+
|
|
9526
|
+
|
|
9351
9527
|
async function suCheck() {
|
|
9352
9528
|
const b = $("su-check");
|
|
9353
9529
|
if (b) b.disabled = true;
|
package/app/ui/i18n.js
CHANGED
|
@@ -584,6 +584,8 @@
|
|
|
584
584
|
"勾选可批量删除": "Tick to bulk delete",
|
|
585
585
|
"取消归档": "Unarchive",
|
|
586
586
|
"归档": "Archive",
|
|
587
|
+
"归档任务": "Archive Task",
|
|
588
|
+
"归档后任务从侧栏收起,可随时用左下角时钟图标找回并取消归档": "Archiving hides the task from the sidebar; use the clock icon at the bottom-left to find it back and unarchive anytime",
|
|
587
589
|
"重命名任务": "Rename task",
|
|
588
590
|
"↻ 继续任务": "↻ Resume task",
|
|
589
591
|
"↻ 重写未达标章": "↻ Rewrite unqualified chapters",
|
|
@@ -1867,6 +1869,23 @@
|
|
|
1867
1869
|
"一键反馈 Issue": "Report Issue",
|
|
1868
1870
|
"已打开 GitHub 反馈页,内容已预填,可直接提交(也可附上诊断包 zip)": "GitHub issue page opened with the report pre-filled — submit it, and feel free to attach the diagnostics zip",
|
|
1869
1871
|
"反馈摘要生成失败:": "Failed to build issue summary: ",
|
|
1872
|
+
"端口占用": "Port Usage",
|
|
1873
|
+
"扫描本机端口": "Scan local ports",
|
|
1874
|
+
"谁占着端口、属于哪个项目一目了然;「仅本机」的端口外网不可达。关闭只发温和信号,系统进程与本服务自身拒绝关闭。": "See at a glance which process holds each port and which project it belongs to; \"local only\" ports are unreachable from outside. Close sends a gentle termination signal only — system processes and this service itself are refused.",
|
|
1875
|
+
"端口": "Port",
|
|
1876
|
+
"进程": "Process",
|
|
1877
|
+
"项目": "Project",
|
|
1878
|
+
"仅本机": "local only",
|
|
1879
|
+
"本服务": "this service",
|
|
1880
|
+
"关闭": "Close",
|
|
1881
|
+
"没有发现监听端口": "No listening ports found",
|
|
1882
|
+
"扫描失败:": "Scan failed: ",
|
|
1883
|
+
"向占用端口 %1 的进程发送温和关闭信号?(系统进程会被拒绝)": "Send a gentle close signal to the process holding port %1? (system processes are refused)",
|
|
1884
|
+
"已发送关闭信号": "Close signal sent",
|
|
1885
|
+
"未能关闭": "Could not close",
|
|
1886
|
+
"关闭失败": "Close failed",
|
|
1887
|
+
|
|
1888
|
+
|
|
1870
1889
|
|
|
1871
1890
|
// —— 禅道 Bug 自动修复 ——
|
|
1872
1891
|
"禅道": "ZenTao",
|
|
@@ -2261,6 +2280,23 @@
|
|
|
2261
2280
|
"旧版浏览器缓存残留": "Legacy browser cache remains",
|
|
2262
2281
|
"历史备份包": "Old backup archives",
|
|
2263
2282
|
"服务日志瘦身": "Service log trim",
|
|
2283
|
+
"任务画像与调度": "Task profile & routing",
|
|
2284
|
+
"通用能力": "general capabilities",
|
|
2285
|
+
"未选定": "Not selected",
|
|
2286
|
+
"执行": "Implement",
|
|
2287
|
+
"降级:": "Fallback: ",
|
|
2288
|
+
"查看候选评分": "View candidate scores",
|
|
2289
|
+
"需要:": "Needs: ",
|
|
2290
|
+
"简单": "Easy",
|
|
2291
|
+
"标准": "Standard",
|
|
2292
|
+
"困难": "Hard",
|
|
2293
|
+
"编码": "Coding",
|
|
2294
|
+
"写作": "Writing",
|
|
2295
|
+
"推理": "Reasoning",
|
|
2296
|
+
"视觉": "Vision",
|
|
2297
|
+
"文件读写": "File access",
|
|
2298
|
+
"长上下文": "Long context",
|
|
2299
|
+
"连续性": "Continuity",
|
|
2264
2300
|
"发布浏览器缓存": "Publishing browser cache",
|
|
2265
2301
|
"保留运行记录与结果报告,只删过程中的原始日志": "Keeps run records and result reports; only raw process logs are deleted",
|
|
2266
2302
|
"发布过程留档截图,超期即清": "Screenshots kept as publishing evidence, removed once expired",
|