codebee 0.1.104 → 0.1.106
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 +17 -0
- package/README.md +10 -4
- package/app/core/activity.py +36 -0
- package/app/core/builtin_agent.py +6 -1
- package/app/core/checkpoints.py +48 -5
- package/app/core/jobs.py +7 -0
- package/app/core/manager.py +1 -0
- package/app/core/market_remote.py +19 -7
- package/app/core/mcp_client.py +102 -63
- package/app/core/openapi.py +29 -0
- package/app/core/pipeline.py +40 -14
- package/app/core/project_memory.py +47 -1
- package/app/core/publish/auto.py +74 -3
- package/app/core/publish/ledger.py +5 -0
- package/app/core/publish/manager.py +5 -1
- package/app/core/registry.py +35 -0
- package/app/core/review_requests.py +119 -0
- package/app/core/runner.py +5 -1
- package/app/core/runtime_lock.py +75 -0
- package/app/core/selfupdate.py +8 -0
- package/app/core/store.py +32 -2
- package/app/core/usage.py +31 -26
- package/app/core/webhooks.py +17 -13
- package/app/desktop_host.py +190 -0
- package/app/main.py +131 -4
- package/app/pet.py +1 -1
- package/app/ui/activity.css +34 -0
- package/app/ui/activity.js +199 -0
- package/app/ui/app.js +813 -66
- package/app/ui/hive3d.js +214 -72
- package/app/ui/i18n.js +162 -2
- package/app/ui/index.html +155 -75
- package/app/ui/style.css +583 -26
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -17,6 +17,8 @@ const BROWSER_STORAGE_KEY = "orch.browser.workspace.v1";
|
|
|
17
17
|
const BROWSER_HOME = "__codebee_home__";
|
|
18
18
|
const BROWSER_SEARCH = "https://www.google.com/search?igu=1&q=";
|
|
19
19
|
const BROWSER_FRAME_BLOCKED_HOSTS = new Set(["github.com", "www.github.com", "x.com", "twitter.com", "www.twitter.com", "linkedin.com", "www.linkedin.com"]);
|
|
20
|
+
const BROWSER_HISTORY_LIMIT = 500;
|
|
21
|
+
const BROWSER_PERMISSION_FEATURES = ["camera", "microphone", "geolocation", "clipboard-read"];
|
|
20
22
|
const BROWSER_TASK_STORAGE_KEY = "orch.browser.task.v1";
|
|
21
23
|
let browserState = browserLoadState();
|
|
22
24
|
let browserTaskId = "";
|
|
@@ -30,12 +32,27 @@ function browserLoadState() {
|
|
|
30
32
|
const raw = JSON.parse(localStorage.getItem(BROWSER_STORAGE_KEY) || "{}");
|
|
31
33
|
const tabs = Array.isArray(raw.tabs) && raw.tabs.length ? raw.tabs : [{ id: "home", url: BROWSER_HOME, title: "CodeBee 浏览器", history: [BROWSER_HOME], index: 0 }];
|
|
32
34
|
const kept = tabs.slice(0, 20).map(browserNormalizeTab);
|
|
33
|
-
|
|
35
|
+
const migratedHistory = kept.flatMap((tab) => tab.history.slice(0, tab.index + 1).map((url, index) => ({
|
|
36
|
+
url, title: browserTitle(url), visitedAt: Date.now() - (tab.history.length - index),
|
|
37
|
+
})));
|
|
38
|
+
const history = Array.isArray(raw.history) ? raw.history.filter(browserValidHistoryEntry) : migratedHistory;
|
|
39
|
+
return {
|
|
40
|
+
tabs: kept,
|
|
41
|
+
active: Math.max(0, Math.min(Number(raw.active) || 0, kept.length - 1)),
|
|
42
|
+
history: history.slice(-BROWSER_HISTORY_LIMIT),
|
|
43
|
+
permissions: raw.permissions && typeof raw.permissions === "object" && !Array.isArray(raw.permissions) ? raw.permissions : {},
|
|
44
|
+
updatedAt: Number(raw.updatedAt) || 0,
|
|
45
|
+
};
|
|
34
46
|
} catch (e) {
|
|
35
|
-
return { tabs: [{ id: "home", url: BROWSER_HOME, title: "CodeBee 浏览器", history: [BROWSER_HOME], index: 0 }], active: 0 };
|
|
47
|
+
return { tabs: [{ id: "home", url: BROWSER_HOME, title: "CodeBee 浏览器", history: [BROWSER_HOME], index: 0 }], active: 0, history: [], permissions: {}, updatedAt: 0 };
|
|
36
48
|
}
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
function browserValidHistoryEntry(entry) {
|
|
52
|
+
return !!entry && typeof entry.url === "string" && entry.url !== BROWSER_HOME &&
|
|
53
|
+
/^https?:\/\//i.test(entry.url) && Number.isFinite(Number(entry.visitedAt));
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
function browserNormalizeTab(tab, i) {
|
|
40
57
|
const history = Array.isArray(tab.history) && tab.history.length ? tab.history.slice(-20) : [tab.url || BROWSER_HOME];
|
|
41
58
|
const index = Math.max(0, Math.min(Number(tab.index) || history.length - 1, history.length - 1));
|
|
@@ -44,14 +61,43 @@ function browserNormalizeTab(tab, i) {
|
|
|
44
61
|
|
|
45
62
|
function browserSaveState() {
|
|
46
63
|
try {
|
|
64
|
+
const stored = JSON.parse(localStorage.getItem(BROWSER_STORAGE_KEY) || "null");
|
|
65
|
+
if (stored && Number(stored.updatedAt) > Number(browserState.updatedAt || 0)) {
|
|
66
|
+
browserState = browserLoadState();
|
|
67
|
+
browserDraw(true);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
47
70
|
browserState.tabs = browserState.tabs.slice(0, 20).map(browserNormalizeTab);
|
|
48
71
|
browserState.active = Math.max(0, Math.min(browserState.active, browserState.tabs.length - 1));
|
|
72
|
+
browserState.history = (browserState.history || []).filter(browserValidHistoryEntry).slice(-BROWSER_HISTORY_LIMIT);
|
|
73
|
+
browserState.updatedAt = Math.max(Date.now(), Number(browserState.updatedAt || 0) + 1);
|
|
49
74
|
localStorage.setItem(BROWSER_STORAGE_KEY, JSON.stringify(browserState));
|
|
50
75
|
} catch (e) { /* private browsing/storage quota: browser remains usable in memory */ }
|
|
51
76
|
const count = $("browser-history-count");
|
|
52
77
|
if (count) count.textContent = t("{0} 个标签页", browserState.tabs.length);
|
|
53
78
|
}
|
|
54
79
|
|
|
80
|
+
window.addEventListener("storage", (event) => {
|
|
81
|
+
if (event.key !== BROWSER_STORAGE_KEY) return;
|
|
82
|
+
if (!event.newValue) {
|
|
83
|
+
browserState = browserLoadState();
|
|
84
|
+
browserDraw(true);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const raw = JSON.parse(event.newValue);
|
|
89
|
+
const tabs = Array.isArray(raw.tabs) && raw.tabs.length ? raw.tabs.slice(0, 20).map(browserNormalizeTab) : [];
|
|
90
|
+
browserState = tabs.length ? {
|
|
91
|
+
tabs,
|
|
92
|
+
active: Math.max(0, Math.min(Number(raw.active) || 0, tabs.length - 1)),
|
|
93
|
+
history: Array.isArray(raw.history) ? raw.history.filter(browserValidHistoryEntry).slice(-BROWSER_HISTORY_LIMIT) : [],
|
|
94
|
+
permissions: raw.permissions && typeof raw.permissions === "object" && !Array.isArray(raw.permissions) ? raw.permissions : {},
|
|
95
|
+
updatedAt: Number(raw.updatedAt) || 0,
|
|
96
|
+
} : browserLoadState();
|
|
97
|
+
browserDraw(true);
|
|
98
|
+
} catch (e) { /* Ignore a partially written value; local state remains usable. */ }
|
|
99
|
+
});
|
|
100
|
+
|
|
55
101
|
function browserTaskList() {
|
|
56
102
|
return ((S.state || {}).tasks || []).filter((task) => task && task.id);
|
|
57
103
|
}
|
|
@@ -93,7 +139,7 @@ function browserRenderHomeTask(task, run, status, verifyPass) {
|
|
|
93
139
|
});
|
|
94
140
|
if (!task) {
|
|
95
141
|
title.textContent = t("还没有任务");
|
|
96
|
-
meta.textContent = t("
|
|
142
|
+
meta.textContent = t("当前任务");
|
|
97
143
|
runLine.textContent = t("等待任务进入工作台");
|
|
98
144
|
Object.values(buttons).forEach((button) => { button.disabled = true; });
|
|
99
145
|
return;
|
|
@@ -280,6 +326,72 @@ function browserSetFrameError(show, message) {
|
|
|
280
326
|
if (text && message) text.textContent = message;
|
|
281
327
|
}
|
|
282
328
|
|
|
329
|
+
function browserRecordVisit(url) {
|
|
330
|
+
if (!url || url === BROWSER_HOME) return;
|
|
331
|
+
const visitedAt = Date.now();
|
|
332
|
+
const entry = { url, title: browserTitle(url), visitedAt };
|
|
333
|
+
browserState.history = (browserState.history || []).filter((item) => item.url !== url);
|
|
334
|
+
browserState.history.push(entry);
|
|
335
|
+
browserState.history = browserState.history.slice(-BROWSER_HISTORY_LIMIT);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function browserCurrentOrigin() {
|
|
339
|
+
const tab = browserActiveTab();
|
|
340
|
+
try { return tab && tab.url !== BROWSER_HOME ? new URL(tab.url, location.origin).origin : ""; } catch (e) { return ""; }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function browserSitePermissions(origin) {
|
|
344
|
+
const saved = browserState.permissions && browserState.permissions[origin];
|
|
345
|
+
return new Set(Array.isArray(saved) ? saved.filter((feature) => BROWSER_PERMISSION_FEATURES.includes(feature)) : []);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function browserApplyFramePermissions() {
|
|
349
|
+
const frame = $("browser-frame");
|
|
350
|
+
if (!frame) return;
|
|
351
|
+
const origin = browserCurrentOrigin();
|
|
352
|
+
const allowed = browserSitePermissions(origin);
|
|
353
|
+
frame.setAttribute("allow", BROWSER_PERMISSION_FEATURES.filter((feature) => allowed.has(feature))
|
|
354
|
+
.map((feature) => feature + " 'src'").join("; "));
|
|
355
|
+
const summary = $("browser-site-origin");
|
|
356
|
+
if (summary) summary.textContent = origin || t("当前没有打开网站");
|
|
357
|
+
const controls = $("browser-permission-list");
|
|
358
|
+
if (controls) {
|
|
359
|
+
controls.querySelectorAll("input[data-browser-permission]").forEach((input) => {
|
|
360
|
+
input.checked = allowed.has(input.dataset.browserPermission);
|
|
361
|
+
input.disabled = !origin || !origin.startsWith("https://");
|
|
362
|
+
});
|
|
363
|
+
const note = $("browser-permission-note");
|
|
364
|
+
if (note) note.textContent = origin && !origin.startsWith("https://")
|
|
365
|
+
? t("网站权限仅对 HTTPS 网站开放。")
|
|
366
|
+
: t("只允许此网站请求所选权限;浏览器仍会在网站实际使用时显示系统授权提示。");
|
|
367
|
+
}
|
|
368
|
+
const siteButton = $("browser-site-permissions");
|
|
369
|
+
if (siteButton) siteButton.disabled = !origin;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function browserRenderHistory() {
|
|
373
|
+
const list = $("browser-history-list");
|
|
374
|
+
if (!list) return;
|
|
375
|
+
const query = (($ ("browser-history-search") || {}).value || "").trim().toLocaleLowerCase();
|
|
376
|
+
const entries = (browserState.history || []).slice().reverse().filter((item) =>
|
|
377
|
+
!query || (item.title + " " + item.url).toLocaleLowerCase().includes(query));
|
|
378
|
+
if (!entries.length) {
|
|
379
|
+
list.innerHTML = '<div class="browser-dialog-empty">' + esc(t(query ? "没有匹配的浏览记录" : "暂无浏览记录")) + "</div>";
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
list.innerHTML = entries.map((item) => '<button type="button" class="browser-history-entry" data-browser-history-url="' + esc(item.url) + '">' +
|
|
383
|
+
'<span class="browser-history-entry-mark"><svg class="ico" aria-hidden="true"><use href="#i-history"></use></svg></span>' +
|
|
384
|
+
'<span class="browser-history-entry-copy"><b>' + esc(item.title || browserTitle(item.url)) + '</b><small>' + esc(item.url) + '</small></span>' +
|
|
385
|
+
'<time>' + esc(new Date(item.visitedAt).toLocaleString()) + '</time></button>').join("");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function browserOpenDialog(id) {
|
|
389
|
+
const dialog = $(id);
|
|
390
|
+
if (!dialog) return;
|
|
391
|
+
if (typeof dialog.showModal === "function") dialog.showModal();
|
|
392
|
+
else dialog.setAttribute("open", "");
|
|
393
|
+
}
|
|
394
|
+
|
|
283
395
|
function browserWatchFrame(url) {
|
|
284
396
|
window.clearTimeout(browserFrameWatch);
|
|
285
397
|
browserSetFrameError(false);
|
|
@@ -315,12 +427,13 @@ function browserDrawTabs() {
|
|
|
315
427
|
});
|
|
316
428
|
}
|
|
317
429
|
|
|
318
|
-
function browserDraw() {
|
|
430
|
+
function browserDraw(skipSave) {
|
|
319
431
|
const tab = browserActiveTab();
|
|
320
432
|
if (!tab) return;
|
|
321
433
|
browserRenderTaskContext();
|
|
322
434
|
browserDrawTabs();
|
|
323
435
|
const address = $("browser-address"), frame = $("browser-frame"), empty = $("browser-empty");
|
|
436
|
+
browserApplyFramePermissions();
|
|
324
437
|
if (address) address.value = tab.url === BROWSER_HOME ? "" : tab.url;
|
|
325
438
|
const canBack = tab.index > 0, canForward = tab.index < tab.history.length - 1;
|
|
326
439
|
["browser-back", "browser-forward"].forEach((id, i) => { const b = $(id); if (b) b.disabled = i ? !canForward : !canBack; });
|
|
@@ -338,7 +451,7 @@ function browserDraw() {
|
|
|
338
451
|
}
|
|
339
452
|
}
|
|
340
453
|
browserRenderHint(tab.url);
|
|
341
|
-
browserSaveState();
|
|
454
|
+
if (!skipSave) browserSaveState();
|
|
342
455
|
}
|
|
343
456
|
|
|
344
457
|
function browserNavigate(value, replace) {
|
|
@@ -347,6 +460,7 @@ function browserNavigate(value, replace) {
|
|
|
347
460
|
const tab = browserActiveTab();
|
|
348
461
|
if (replace) tab.history[tab.index] = url;
|
|
349
462
|
else { tab.history = tab.history.slice(0, tab.index + 1).concat(url).slice(-20); tab.index = tab.history.length - 1; }
|
|
463
|
+
browserRecordVisit(url);
|
|
350
464
|
tab.url = url; tab.title = browserTitle(url); browserDraw(); return true;
|
|
351
465
|
}
|
|
352
466
|
|
|
@@ -356,14 +470,71 @@ function browserOpenPreview() {
|
|
|
356
470
|
|
|
357
471
|
function browserBind() {
|
|
358
472
|
if (!$("browser-frame")) return;
|
|
473
|
+
const contextToggle = $("browser-context-toggle");
|
|
474
|
+
document.body.classList.add("browser-context-collapsed");
|
|
475
|
+
if (contextToggle) contextToggle.setAttribute("aria-expanded", "false");
|
|
476
|
+
if (contextToggle) contextToggle.addEventListener("click", () => {
|
|
477
|
+
const collapsed = document.body.classList.toggle("browser-context-collapsed");
|
|
478
|
+
contextToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
479
|
+
});
|
|
480
|
+
const homeSearch = $("browser-home-search");
|
|
481
|
+
if (homeSearch) homeSearch.addEventListener("submit", (e) => {
|
|
482
|
+
e.preventDefault();
|
|
483
|
+
const input = homeSearch.querySelector("input");
|
|
484
|
+
browserNavigate(input && input.value);
|
|
485
|
+
if (input) input.value = "";
|
|
486
|
+
});
|
|
359
487
|
$("browser-nav-form").addEventListener("submit", (e) => { e.preventDefault(); browserNavigate($("browser-address").value); });
|
|
360
488
|
$("browser-back").addEventListener("click", () => { const t0 = browserActiveTab(); if (t0.index > 0) { t0.index--; t0.url = t0.history[t0.index]; browserDraw(); } });
|
|
361
489
|
$("browser-forward").addEventListener("click", () => { const t0 = browserActiveTab(); if (t0.index < t0.history.length - 1) { t0.index++; t0.url = t0.history[t0.index]; browserDraw(); } });
|
|
362
490
|
$("browser-reload").addEventListener("click", () => { const f = $("browser-frame"); if (f && !f.classList.contains("hidden")) f.src = f.src; });
|
|
363
491
|
$("browser-home").addEventListener("click", () => browserNavigate(BROWSER_HOME));
|
|
364
|
-
$("browser-new-tab").addEventListener("click", () => {
|
|
492
|
+
$("browser-new-tab").addEventListener("click", () => {
|
|
493
|
+
browserState.tabs.push({ id: "tab-" + Date.now(), url: BROWSER_HOME, title: "CodeBee 浏览器", history: [BROWSER_HOME], index: 0 });
|
|
494
|
+
browserState.active = browserState.tabs.length - 1;
|
|
495
|
+
browserDraw();
|
|
496
|
+
$("browser-address").focus();
|
|
497
|
+
});
|
|
365
498
|
$("browser-open-external").addEventListener("click", () => { const t0 = browserActiveTab(); if (t0.url !== BROWSER_HOME) window.open(browserUrlAuth(t0.url), "_blank", "noopener"); });
|
|
366
499
|
$("browser-frame-error-open").addEventListener("click", () => { const t0 = browserActiveTab(); if (t0.url !== BROWSER_HOME) window.open(browserUrlAuth(t0.url), "_blank", "noopener"); });
|
|
500
|
+
$("browser-history-open").addEventListener("click", () => { browserRenderHistory(); browserOpenDialog("browser-history-dialog"); });
|
|
501
|
+
$("browser-history-search").addEventListener("input", browserRenderHistory);
|
|
502
|
+
$("browser-history-list").addEventListener("click", (e) => {
|
|
503
|
+
const entry = e.target.closest("[data-browser-history-url]");
|
|
504
|
+
if (!entry) return;
|
|
505
|
+
$("browser-history-dialog").close();
|
|
506
|
+
browserNavigate(entry.dataset.browserHistoryUrl);
|
|
507
|
+
});
|
|
508
|
+
$("browser-clear-data").addEventListener("click", () => {
|
|
509
|
+
const confirm = $("browser-clear-confirm");
|
|
510
|
+
if (typeof confirm.showModal === "function") confirm.showModal(); else confirm.setAttribute("open", "");
|
|
511
|
+
});
|
|
512
|
+
$("browser-clear-confirm-yes").addEventListener("click", () => {
|
|
513
|
+
browserState.history = [];
|
|
514
|
+
browserState.permissions = {};
|
|
515
|
+
browserState.tabs = [{ id: "home", url: BROWSER_HOME, title: "CodeBee 浏览器", history: [BROWSER_HOME], index: 0 }];
|
|
516
|
+
browserState.active = 0;
|
|
517
|
+
browserSaveState();
|
|
518
|
+
$("browser-clear-confirm").close();
|
|
519
|
+
$("browser-history-dialog").close();
|
|
520
|
+
browserDraw(true);
|
|
521
|
+
});
|
|
522
|
+
$("browser-site-permissions").addEventListener("click", () => {
|
|
523
|
+
browserApplyFramePermissions();
|
|
524
|
+
browserOpenDialog("browser-permissions-dialog");
|
|
525
|
+
});
|
|
526
|
+
$("browser-permission-list").addEventListener("change", (e) => {
|
|
527
|
+
const input = e.target.closest("input[data-browser-permission]");
|
|
528
|
+
const origin = browserCurrentOrigin();
|
|
529
|
+
if (!input || !origin || !origin.startsWith("https://")) return;
|
|
530
|
+
const allowed = browserSitePermissions(origin);
|
|
531
|
+
if (input.checked) allowed.add(input.dataset.browserPermission); else allowed.delete(input.dataset.browserPermission);
|
|
532
|
+
if (allowed.size) browserState.permissions[origin] = Array.from(allowed);
|
|
533
|
+
else delete browserState.permissions[origin];
|
|
534
|
+
browserSaveState();
|
|
535
|
+
browserApplyFramePermissions();
|
|
536
|
+
});
|
|
537
|
+
document.querySelectorAll("[data-browser-dialog-close]").forEach((button) => button.addEventListener("click", () => button.closest("dialog").close()));
|
|
367
538
|
$("browser-empty").addEventListener("click", (e) => {
|
|
368
539
|
const button = e.target.closest("[data-browser-home-action]");
|
|
369
540
|
if (!button || button.disabled) return;
|
|
@@ -434,7 +605,7 @@ function flowIconHtml(f) {
|
|
|
434
605
|
function flowDesc(f) {
|
|
435
606
|
if (f.id === "rank_scan") return t("抓四平台榜 → AI 选题洞察·附证据分级");
|
|
436
607
|
if (f.engine === "direct") return t("单智能体直达(快)");
|
|
437
|
-
if (f.engine === "code") return t("实现 → 验证
|
|
608
|
+
if (f.engine === "code") return t("实现 → 验证 →(复杂任务进)评审");
|
|
438
609
|
// 连载与单稿件同引擎,描述必须区分:连载强调逐章与断点续跑
|
|
439
610
|
if (f.serial) return t("大纲 → 逐章起草评审 → 合并(可续跑)");
|
|
440
611
|
return t("起草 → 多维评审 → 门禁");
|
|
@@ -1519,7 +1690,11 @@ function bindableProvs(provs) {
|
|
|
1519
1690
|
}
|
|
1520
1691
|
|
|
1521
1692
|
/* ---------------------------------------------------------- 通用弹框 */
|
|
1693
|
+
let _pbReturnFocus = null;
|
|
1522
1694
|
function openModal(title, bodyHtml, footHtml) {
|
|
1695
|
+
_pbReturnFocus = null;
|
|
1696
|
+
delete $("modal").dataset.pbKey;
|
|
1697
|
+
$("modal").classList.remove("pb-selection-modal");
|
|
1523
1698
|
$("modal-title").textContent = title;
|
|
1524
1699
|
$("modal-body").innerHTML = bodyHtml || "";
|
|
1525
1700
|
$("modal-foot").innerHTML = footHtml || "";
|
|
@@ -1531,9 +1706,165 @@ function closeModal() {
|
|
|
1531
1706
|
const m = $("modal");
|
|
1532
1707
|
if (!m) return;
|
|
1533
1708
|
m.classList.add("hidden");
|
|
1709
|
+
delete m.dataset.pbKey;
|
|
1710
|
+
m.classList.remove("pb-selection-modal");
|
|
1534
1711
|
$("modal-body").innerHTML = "";
|
|
1535
1712
|
$("modal-foot").innerHTML = "";
|
|
1536
1713
|
document.body.classList.remove("modal-open");
|
|
1714
|
+
if (_pbReturnFocus && _pbReturnFocus.isConnected) _pbReturnFocus.focus();
|
|
1715
|
+
_pbReturnFocus = null;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/* ---------------------------------------------------------- 人工评审队列 */
|
|
1719
|
+
const REVIEW_QUEUE = { rows: [], loaded: false, loading: 0, saving: false };
|
|
1720
|
+
const REVIEW_LABELS = { requested: "待分配", assigned: "评审中", approved: "已批准",
|
|
1721
|
+
changes_requested: "要求修改", dismissed: "已关闭", expired: "已过期" };
|
|
1722
|
+
|
|
1723
|
+
function reviewActions(status) {
|
|
1724
|
+
if (status === "requested") return ["assign", "dismiss", "expire"];
|
|
1725
|
+
if (status === "assigned") return ["approve", "changes_requested", "dismiss", "expire"];
|
|
1726
|
+
if (status === "changes_requested") return ["dismiss", "expire"];
|
|
1727
|
+
return [];
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function reviewActionLabel(action) {
|
|
1731
|
+
return t(({ assign: "分配评审人", approve: "批准评审", changes_requested: "要求修改",
|
|
1732
|
+
dismiss: "关闭请求", expire: "标记过期" })[action] || action);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function renderReviewQueue() {
|
|
1736
|
+
const list = $("review-queue-list");
|
|
1737
|
+
if (!list) return;
|
|
1738
|
+
const filter = $("review-queue-filter").value;
|
|
1739
|
+
const rows = REVIEW_QUEUE.rows.filter((row) => filter === "all" ||
|
|
1740
|
+
(filter === "active" ? reviewActions(row.status).length > 0 : row.status === filter))
|
|
1741
|
+
.sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
|
|
1742
|
+
$("review-queue-count").textContent = t("显示 {0} / {1} 条", rows.length, REVIEW_QUEUE.rows.length);
|
|
1743
|
+
list.innerHTML = rows.map((row) => {
|
|
1744
|
+
const stamp = new Date(Number(row.updated_at || 0) * 1000);
|
|
1745
|
+
const time = Number.isFinite(stamp.getTime()) ? stamp.toLocaleString(getLang() === "en" ? "en-US" : "zh-CN") : "";
|
|
1746
|
+
return '<article class="review-request"><div class="review-request-head"><strong>' +
|
|
1747
|
+
esc(row.summary || t("未填写评审摘要")) + '</strong><span class="tag ' +
|
|
1748
|
+
(row.status === "approved" ? "ok" : row.status === "changes_requested" ? "warn" : "") + '">' +
|
|
1749
|
+
esc(t(REVIEW_LABELS[row.status] || row.status)) + '</span></div><div class="review-request-meta">' +
|
|
1750
|
+
'<span>' + esc(t("运行 ID")) + ': <code>' + esc(row.run_id) + '</code></span>' +
|
|
1751
|
+
(row.channel ? '<span>' + esc(t("评审渠道")) + ': ' + esc(row.channel) + '</span>' : "") +
|
|
1752
|
+
(row.reviewer ? '<span>' + esc(t("评审人")) + ': ' + esc(row.reviewer) + '</span>' : "") +
|
|
1753
|
+
'<time>' + esc(time) + '</time></div>' +
|
|
1754
|
+
(Array.isArray(row.paths) && row.paths.length ? '<div class="review-request-paths">' +
|
|
1755
|
+
row.paths.map((path) => '<code>' + esc(path) + '</code>').join("") + '</div>' : "") +
|
|
1756
|
+
(row.reason ? '<p class="review-request-reason">' + esc(row.reason) + '</p>' : "") +
|
|
1757
|
+
'<div class="actions">' + reviewActions(row.status).map((action) =>
|
|
1758
|
+
'<button type="button" class="ghost small" data-review-id="' + esc(row.id) +
|
|
1759
|
+
'" data-review-action="' + action + '">' + esc(reviewActionLabel(action)) + '</button>').join("") +
|
|
1760
|
+
'</div></article>';
|
|
1761
|
+
}).join("") || '<p class="hint">' + esc(t(REVIEW_QUEUE.loaded ? "暂无匹配的评审请求" : "正在加载评审请求…")) + '</p>';
|
|
1762
|
+
list.onclick = (event) => {
|
|
1763
|
+
const button = event.target.closest("[data-review-action]");
|
|
1764
|
+
if (button && list.contains(button)) openReviewTransition(button.dataset.reviewId, button.dataset.reviewAction);
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
async function loadReviewQueue() {
|
|
1769
|
+
const list = $("review-queue-list"), message = $("review-queue-message");
|
|
1770
|
+
if (!list || !message) return;
|
|
1771
|
+
const token = ++REVIEW_QUEUE.loading;
|
|
1772
|
+
list.setAttribute("aria-busy", "true");
|
|
1773
|
+
message.textContent = t("正在加载评审请求…");
|
|
1774
|
+
if (!REVIEW_QUEUE.loaded) renderReviewQueue();
|
|
1775
|
+
try {
|
|
1776
|
+
const data = await api("/api/reviews", { timeout: 15000 });
|
|
1777
|
+
if (token !== REVIEW_QUEUE.loading) return;
|
|
1778
|
+
if (!Array.isArray(data.reviews)) throw new Error(t("评审数据格式不正确"));
|
|
1779
|
+
REVIEW_QUEUE.rows = data.reviews;
|
|
1780
|
+
REVIEW_QUEUE.loaded = true;
|
|
1781
|
+
message.textContent = "";
|
|
1782
|
+
renderReviewQueue();
|
|
1783
|
+
} catch (e) {
|
|
1784
|
+
if (token === REVIEW_QUEUE.loading) message.textContent = t("评审队列加载失败:") + e.message;
|
|
1785
|
+
} finally {
|
|
1786
|
+
if (token === REVIEW_QUEUE.loading) list.setAttribute("aria-busy", "false");
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function openReviewRequest() {
|
|
1791
|
+
if (REVIEW_QUEUE.saving) return;
|
|
1792
|
+
const runs = ((S.state || {}).runs || []).slice(0, 100);
|
|
1793
|
+
openModal(t("新建评审请求"), '<form id="review-request-form" class="form">' +
|
|
1794
|
+
'<div class="field"><label for="review-run-id">' + esc(t("运行 ID")) + ' *</label>' +
|
|
1795
|
+
'<input id="review-run-id" list="review-run-options" required maxlength="100" autocomplete="off">' +
|
|
1796
|
+
'<datalist id="review-run-options">' + runs.map((run) => '<option value="' + esc(run.id) + '">' +
|
|
1797
|
+
esc(run.goal || run.task_id || run.id) + '</option>').join("") + '</datalist></div>' +
|
|
1798
|
+
'<div class="field"><label for="review-summary">' + esc(t("评审摘要")) + '</label>' +
|
|
1799
|
+
'<textarea id="review-summary" rows="3" maxlength="1000"></textarea></div>' +
|
|
1800
|
+
'<div class="field"><label for="review-paths">' + esc(t("待评审文件(每行一个路径)")) + '</label>' +
|
|
1801
|
+
'<textarea id="review-paths" rows="3"></textarea></div>' +
|
|
1802
|
+
'<div class="field"><label for="review-channel">' + esc(t("评审渠道")) + '</label>' +
|
|
1803
|
+
'<input id="review-channel" maxlength="200"></div>' +
|
|
1804
|
+
'<p id="review-form-message" class="hint" role="status" aria-live="polite"></p></form>',
|
|
1805
|
+
'<button type="button" class="ghost" onclick="closeModal()">' + esc(t("取消")) + '</button>' +
|
|
1806
|
+
'<button id="review-save" type="submit" form="review-request-form" class="primary">' + esc(t("创建请求")) + '</button>');
|
|
1807
|
+
$("review-request-form").onsubmit = (event) => { event.preventDefault(); saveReviewRequest(); };
|
|
1808
|
+
$("review-run-id").value = S.detailRunId || "";
|
|
1809
|
+
$("review-run-id").focus();
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function openReviewTransition(id, action) {
|
|
1813
|
+
if (REVIEW_QUEUE.saving) return;
|
|
1814
|
+
const row = REVIEW_QUEUE.rows.find((item) => item.id === id);
|
|
1815
|
+
if (!row || !reviewActions(row.status).includes(action)) return;
|
|
1816
|
+
const assigning = action === "assign", needsReason = action === "changes_requested";
|
|
1817
|
+
openModal(reviewActionLabel(action), '<form id="review-transition-form" class="form">' +
|
|
1818
|
+
'<p class="hint">' + esc(row.summary || row.run_id) + '</p>' +
|
|
1819
|
+
'<div class="field"><label for="review-reviewer">' + esc(t("评审人")) + (assigning ? ' *' : '') + '</label>' +
|
|
1820
|
+
'<input id="review-reviewer" maxlength="200" value="' + esc(row.reviewer || "") + '"' +
|
|
1821
|
+
(assigning ? ' required' : '') + '></div>' +
|
|
1822
|
+
'<div class="field"><label for="review-reason">' + esc(t("处理意见")) + (needsReason ? ' *' : '') + '</label>' +
|
|
1823
|
+
'<textarea id="review-reason" rows="4" maxlength="1000"' + (needsReason ? ' required' : '') + '></textarea></div>' +
|
|
1824
|
+
'<p id="review-form-message" class="hint" role="status" aria-live="polite"></p></form>',
|
|
1825
|
+
'<button type="button" class="ghost" onclick="closeModal()">' + esc(t("取消")) + '</button>' +
|
|
1826
|
+
'<button id="review-save" type="submit" form="review-transition-form" class="primary">' + esc(reviewActionLabel(action)) + '</button>');
|
|
1827
|
+
$("review-transition-form").onsubmit = (event) => { event.preventDefault(); saveReviewTransition(id, action); };
|
|
1828
|
+
$(assigning ? "review-reviewer" : "review-reason").focus();
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
async function reviewQueueSave(path, body) {
|
|
1832
|
+
if (REVIEW_QUEUE.saving) return;
|
|
1833
|
+
const button = $("review-save"), form = $("review-request-form") || $("review-transition-form");
|
|
1834
|
+
const message = $("review-form-message");
|
|
1835
|
+
REVIEW_QUEUE.saving = true;
|
|
1836
|
+
button.disabled = true;
|
|
1837
|
+
message.textContent = t("正在保存…");
|
|
1838
|
+
try {
|
|
1839
|
+
await api(path, { method: "POST", body: JSON.stringify(body), timeout: 15000 });
|
|
1840
|
+
if (form.isConnected) closeModal();
|
|
1841
|
+
toast(t("评审请求已保存"));
|
|
1842
|
+
await loadReviewQueue();
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
if (message.isConnected) message.textContent = t("评审请求保存失败:") + e.message;
|
|
1845
|
+
else toast(t("评审请求保存失败:") + e.message, true);
|
|
1846
|
+
} finally {
|
|
1847
|
+
REVIEW_QUEUE.saving = false;
|
|
1848
|
+
button.disabled = false;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
async function saveReviewRequest() {
|
|
1853
|
+
const runId = $("review-run-id").value.trim();
|
|
1854
|
+
const paths = $("review-paths").value.split(/\r?\n/).map((path) => path.trim()).filter(Boolean);
|
|
1855
|
+
if (!runId) { $("review-form-message").textContent = t("请填写运行 ID"); return; }
|
|
1856
|
+
if (paths.length > 200 || paths.some((path) => path.length > 500)) {
|
|
1857
|
+
$("review-form-message").textContent = t("最多 200 个文件,每个路径最多 500 字符"); return;
|
|
1858
|
+
}
|
|
1859
|
+
await reviewQueueSave("/api/reviews", { run_id: runId, paths,
|
|
1860
|
+
summary: $("review-summary").value.trim(), channel: $("review-channel").value.trim() });
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
async function saveReviewTransition(id, action) {
|
|
1864
|
+
const reviewer = $("review-reviewer").value.trim(), reason = $("review-reason").value.trim();
|
|
1865
|
+
if (action === "assign" && !reviewer) { $("review-form-message").textContent = t("请填写评审人"); return; }
|
|
1866
|
+
if (action === "changes_requested" && !reason) { $("review-form-message").textContent = t("请填写修改意见"); return; }
|
|
1867
|
+
await reviewQueueSave("/api/reviews/" + encodeURIComponent(id) + "/transition", { action, reviewer, reason });
|
|
1537
1868
|
}
|
|
1538
1869
|
|
|
1539
1870
|
/* catalog 里 model 可能是字符串,也可能是 {default:...} 之类的对象 */
|
|
@@ -1927,6 +2258,7 @@ function render() {
|
|
|
1927
2258
|
renderImplSelects();
|
|
1928
2259
|
renderRunList();
|
|
1929
2260
|
renderSideTasks();
|
|
2261
|
+
CodeBeeActivity.render();
|
|
1930
2262
|
renderRunDetail();
|
|
1931
2263
|
renderCatalog();
|
|
1932
2264
|
renderModels();
|
|
@@ -4358,11 +4690,14 @@ window.toggleNotifySound = function () {
|
|
|
4358
4690
|
};
|
|
4359
4691
|
|
|
4360
4692
|
function paintNotifyToggle() {
|
|
4361
|
-
|
|
4693
|
+
CodeBeeActivity.paintToggle();
|
|
4694
|
+
const b = $("activity-sound");
|
|
4362
4695
|
if (!b) return;
|
|
4363
4696
|
const on = soundEnabled();
|
|
4364
4697
|
b.classList.toggle("off", !on);
|
|
4365
4698
|
b.title = on ? t("提示音已开(点击关闭)") : t("提示音已关(点击开启)");
|
|
4699
|
+
b.setAttribute("aria-label", b.title);
|
|
4700
|
+
b.setAttribute("aria-pressed", String(on));
|
|
4366
4701
|
}
|
|
4367
4702
|
|
|
4368
4703
|
// 「展开全部」钮:文件夹全开时箭头翻成收起朝向,提示文案跟着换(renderSideTasks 末尾与点击后各同步一次)
|
|
@@ -4386,6 +4721,43 @@ function paintArchToggle() {
|
|
|
4386
4721
|
b.setAttribute("aria-label", tip);
|
|
4387
4722
|
}
|
|
4388
4723
|
|
|
4724
|
+
/* 手机首页与侧栏共用目录/任务真源,保留独立的卡片展开态。 */
|
|
4725
|
+
function renderMobileWorkspaces(dirs) {
|
|
4726
|
+
const box = $("mobile-workspace-list");
|
|
4727
|
+
if (!box) return;
|
|
4728
|
+
const painted = box.querySelector("details");
|
|
4729
|
+
const open = new Set(Array.from(box.querySelectorAll("details[open]")).map((d) => d.dataset.dir));
|
|
4730
|
+
$("sub-tasks").classList.toggle("mobile-has-workspaces", dirs.length > 0);
|
|
4731
|
+
$("mobile-workspaces-count").textContent = t("{0} 个工作区 · {1} 个任务", dirs.length,
|
|
4732
|
+
dirs.reduce((n, d) => n + d.groups.length, 0));
|
|
4733
|
+
box.innerHTML = dirs.map((d, i) => {
|
|
4734
|
+
const label = d.dir === ORPHAN_DIR ? t("其他") : dirName(d.dir);
|
|
4735
|
+
const expanded = painted ? open.has(d.dir) : d.active || i === 0;
|
|
4736
|
+
return '<article class="mobile-workspace-card"><details class="mobile-workspace" data-dir="' + esc(d.dir) + '"' + (expanded ? ' open' : '') + '>' +
|
|
4737
|
+
'<summary><svg class="ico mobile-folder" aria-hidden="true"><use href="#i-folder"/></svg>' +
|
|
4738
|
+
'<span class="mobile-workspace-copy"><b>' + esc(label) + '</b><span title="' + esc(d.dir) + '">' + esc(d.dir === ORPHAN_DIR ? t("其他") : d.dir) + '</span>' +
|
|
4739
|
+
'<small>' + esc(relTime(d.time)) + '</small></span><span class="mobile-workspace-total">' + d.groups.length + '</span>' +
|
|
4740
|
+
'<svg class="ico mobile-workspace-chevron" aria-hidden="true"><use href="#i-chevron-r"/></svg></summary>' +
|
|
4741
|
+
'<div class="mobile-workspace-tasks">' + d.groups.map((g) =>
|
|
4742
|
+
'<button type="button" class="mobile-task" data-task="' + esc(g.taskId) + '" data-run="' + esc(g.runIds[0] || '') + '">' +
|
|
4743
|
+
'<span class="mobile-task-copy"><b>' + esc(g.title) + '</b><small>' + esc(relTime(g.time)) + '</small></span>' +
|
|
4744
|
+
(g.verdict ? '<span class="sbadge">' + t("待裁决") + '</span>' : '') +
|
|
4745
|
+
(g.archived ? '<span class="sbadge">' + t("已归档") + '</span>' : statusChip(g.active ? 'running' : g.status || t("未运行"))) + '</button>'
|
|
4746
|
+
).join('') + '</div></details>' +
|
|
4747
|
+
(d.dir === ORPHAN_DIR ? '' : '<button type="button" class="ghost mobile-workspace-new" data-new-dir="' + esc(d.dir) + '" title="' + esc(t("在此工作区新建任务")) + '" aria-label="' + esc(t("在此工作区新建任务")) + '"><svg class="ico" aria-hidden="true"><use href="#i-plus"/></svg></button>') + '</article>';
|
|
4748
|
+
}).join('');
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
function mobileNewTask(dir) {
|
|
4752
|
+
exitSettings();
|
|
4753
|
+
$("sub-tasks").classList.add("mobile-composing");
|
|
4754
|
+
if (dir) {
|
|
4755
|
+
$("f-workdir").value = dir;
|
|
4756
|
+
$("f-workdir").dispatchEvent(new Event("input", { bubbles: true }));
|
|
4757
|
+
}
|
|
4758
|
+
document.querySelector("main").scrollTop = 0;
|
|
4759
|
+
}
|
|
4760
|
+
|
|
4389
4761
|
function renderSideTasks() {
|
|
4390
4762
|
const box = $("side-tasks");
|
|
4391
4763
|
if (!box) return;
|
|
@@ -4504,6 +4876,7 @@ function renderSideTasks() {
|
|
|
4504
4876
|
'<span class="cnt">' + d.groups.length + "</span></summary>" +
|
|
4505
4877
|
'<div class="dirbody">' + inner + "</div></details>";
|
|
4506
4878
|
}).join("") || ('<div class="side-empty">' + (q ? t("无匹配任务") : t("暂无任务")) + "</div>");
|
|
4879
|
+
renderMobileWorkspaces(dirs);
|
|
4507
4880
|
scanAttention(groups);
|
|
4508
4881
|
paintNotifyToggle();
|
|
4509
4882
|
syncSideExpandBtn();
|
|
@@ -4579,7 +4952,7 @@ function sideRowActivate(det) {
|
|
|
4579
4952
|
/* 打开单条运行详情(右键菜单「打开详情」/检查器步骤条):不跳设置页——
|
|
4580
4953
|
* 留在任务树主视图,主栏直接展示运行详情。
|
|
4581
4954
|
* 带步骤号 n 时,详情渲染完自动定位到该步:滚动 + 高亮 + 展开它的日志。 */
|
|
4582
|
-
window.sideOpenRun = function (id, n) {
|
|
4955
|
+
window.sideOpenRun = function (id, n, pinTab) {
|
|
4583
4956
|
_taskStepRenderToken++;
|
|
4584
4957
|
const earlierRunsButton = $("rd-load-earlier");
|
|
4585
4958
|
if (earlierRunsButton) earlierRunsButton.hidden = true;
|
|
@@ -4588,17 +4961,18 @@ window.sideOpenRun = function (id, n) {
|
|
|
4588
4961
|
S.detailTaskKey = null;
|
|
4589
4962
|
if (!S.histJump && typeof histPush === "function") histPush({ m: "main", tab: "run-detail" });
|
|
4590
4963
|
showDetailInMain();
|
|
4591
|
-
openRun(id, n ? "steps" : null); //
|
|
4964
|
+
openRun(id, pinTab || (n ? "steps" : null)); // 活动视图可直达对应分区
|
|
4592
4965
|
};
|
|
4593
4966
|
|
|
4594
4967
|
/* 打开任务级详情:任务可能被续跑/重试过多次,步骤分散在多条 run 里。
|
|
4595
4968
|
* 按时间顺序列出该任务全部 run 的全部步骤,run 之间加分隔条。 */
|
|
4596
|
-
window.sideOpenTask = function (key) {
|
|
4969
|
+
window.sideOpenTask = function (key, pinTab) {
|
|
4597
4970
|
_taskStepRenderToken++;
|
|
4598
4971
|
chatJumpLatest(); // 首开任务详情:对话时间线落最新(默认落蜂巢时切到对话页签也带到底)
|
|
4599
4972
|
$("rd-steps").innerHTML = '<div class="hint" role="status">' + esc(t("正在加载任务详情…")) + "</div>";
|
|
4600
4973
|
S.lastRun = null;
|
|
4601
4974
|
S.detailTaskKey = key;
|
|
4975
|
+
S.activityTarget = pinTab ? { key, tab: pinTab } : null;
|
|
4602
4976
|
S.detailRunId = null;
|
|
4603
4977
|
S.focusStep = 0;
|
|
4604
4978
|
S.taskSig = "";
|
|
@@ -4612,8 +4986,9 @@ window.sideOpenTask = function (key) {
|
|
|
4612
4986
|
detailSideReset(); // 换详情目标:任务级 side 缓存作废,等首拉
|
|
4613
4987
|
const olderRunsButton = $("rd-load-earlier");
|
|
4614
4988
|
if (olderRunsButton) olderRunsButton.hidden = true;
|
|
4615
|
-
S.rdHiveFirst =
|
|
4616
|
-
S.rdTab = null;
|
|
4989
|
+
S.rdHiveFirst = !pinTab; // 活动入口钉住目标页签;项目入口默认落蜂巢
|
|
4990
|
+
S.rdTab = pinTab || null;
|
|
4991
|
+
S.rdTabPin = !!pinTab;
|
|
4617
4992
|
S.rdTabSig = "";
|
|
4618
4993
|
applyRdTabs();
|
|
4619
4994
|
syncInspectorVis(); // 详情已铺开:检查器让位(选中保留,返回列表自动滑回)
|
|
@@ -4796,6 +5171,20 @@ window.loadProjectMemory = async function (taskId) {
|
|
|
4796
5171
|
} catch (error) { box.innerHTML = '<span class="hint">' + esc(t("加载项目记忆失败:")) + esc(error.message) + "</span>"; }
|
|
4797
5172
|
};
|
|
4798
5173
|
|
|
5174
|
+
window.previewProjectMemory = async function (taskId) {
|
|
5175
|
+
taskId = taskId || S.detailTaskKey;
|
|
5176
|
+
if (!taskId) return;
|
|
5177
|
+
const box = $("rd-project-memory-body");
|
|
5178
|
+
if (!box) return;
|
|
5179
|
+
try {
|
|
5180
|
+
const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory/preview?cap=4000");
|
|
5181
|
+
const entries = (data.entries || []).map((entry) =>
|
|
5182
|
+
'<span class="chip">' + esc(entry.title || t("项目记忆")) + ' · v' + esc(String(entry.version || 1)) + '</span>').join(" ");
|
|
5183
|
+
const text = data.text ? '<pre>' + esc(data.text) + '</pre>' : '<span class="hint">' + esc(t("暂无可注入记忆")) + '</span>';
|
|
5184
|
+
box.insertAdjacentHTML("afterbegin", '<div class="rd-memory-preview"><strong>' + esc(t("本次注入预览")) + '</strong><span class="hint">' + esc(t("已使用 {0}/{1} 字符", data.budget?.used || 0, data.budget?.cap || 0)) + '</span><div>' + entries + '</div>' + text + '</div>');
|
|
5185
|
+
} catch (error) { toast(t("项目记忆预览失败:") + error.message, true); }
|
|
5186
|
+
};
|
|
5187
|
+
|
|
4799
5188
|
window.decideProjectMemory = async function (taskId, id, action, version) {
|
|
4800
5189
|
try {
|
|
4801
5190
|
await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory", {
|
|
@@ -4841,7 +5230,7 @@ window.loadRunCheckpoints = async function (requestedRunId) {
|
|
|
4841
5230
|
const checkpoint = data.checkpoint || {};
|
|
4842
5231
|
const steps = Array.isArray(checkpoint.steps) ? checkpoint.steps : [];
|
|
4843
5232
|
const stepSummary = steps.length ? '<div class="rd-checkpoint-steps"><strong>' + esc(t("步骤回放预览")) + '</strong>' +
|
|
4844
|
-
steps.map((step) => '<div class="rd-checkpoint-step"><span>#' + esc(String(step.step ?? "")) + ' · ' + esc(step.role || t("未命名步骤")) + '</span><span class="chip">' + esc(step.status || step.event || "unknown") + (step.attempt ? ' · ' + esc(t("第"
|
|
5233
|
+
steps.map((step) => '<div class="rd-checkpoint-step"><span>#' + esc(String(step.step ?? "")) + ' · ' + esc(step.role || t("未命名步骤")) + '</span><span class="chip">' + esc(step.status || step.event || "unknown") + (step.attempt ? ' · ' + esc(t("第{0}次", step.attempt)) : "") + '</span></div>').join("") +
|
|
4845
5234
|
(checkpoint.truncated ? '<p class="hint">' + esc(t("仅显示最近 200 个步骤,共 ") + (checkpoint.total_steps || steps.length)) + '</p>' : '') +
|
|
4846
5235
|
(checkpoint.replayable ? '<p class="hint">' + esc(t("所有步骤均已完成,可人工复核后继续")) + '</p>' : '<p class="hint">' + esc(t("无法安全自动回放:") + (checkpoint.blocked_reason || t("需要人工核对"))) + '</p>') + '</div>' : '';
|
|
4847
5236
|
body.innerHTML = (data.files && data.files.warning ? '<p class="hint">' + esc(data.files.warning) + "</p>" : "") + stepSummary +
|
|
@@ -5473,6 +5862,13 @@ function rdTabBadges() {
|
|
|
5473
5862
|
* renderPreview 等收尾调用——预览可用性是异步翻过来的,翻过来这次必须重选卡)。 */
|
|
5474
5863
|
function rdTabsSync(ctx) {
|
|
5475
5864
|
if (ctx) S._rdCtx = ctx;
|
|
5865
|
+
const target = S.activityTarget;
|
|
5866
|
+
if (target && target.key === S.detailTaskKey && rdTabAvail()[target.tab]) {
|
|
5867
|
+
S.rdTab = target.tab;
|
|
5868
|
+
S.rdTabPin = true;
|
|
5869
|
+
S.rdHiveFirst = false;
|
|
5870
|
+
S.activityTarget = null;
|
|
5871
|
+
}
|
|
5476
5872
|
const c = ctx || S._rdCtx || {};
|
|
5477
5873
|
if (!S.rdTabPin) {
|
|
5478
5874
|
const chatAvail = $("rd-chat") && !$("rd-chat").classList.contains("hidden");
|
|
@@ -6050,6 +6446,7 @@ function renderBookMetaPanel(task) {
|
|
|
6050
6446
|
box.classList.remove("hidden");
|
|
6051
6447
|
box.innerHTML = html;
|
|
6052
6448
|
box.dataset.taskId = task.id; // 跨任务守卫标记:展开态/输入保护只对同一任务生效
|
|
6449
|
+
pbRefreshSelectionDialog(task);
|
|
6053
6450
|
// TAB 徽章:已生成平台数(生成中显示 ●,随下次轮询刷新)
|
|
6054
6451
|
const badge = document.querySelector('#rd-tabs .rd-tab[data-tab="bookmeta"] .rd-badge');
|
|
6055
6452
|
if (badge) {
|
|
@@ -6151,7 +6548,7 @@ const PB_ST = {
|
|
|
6151
6548
|
error: ["出错", "pb-st-err"],
|
|
6152
6549
|
};
|
|
6153
6550
|
|
|
6154
|
-
function pbBlock(task, platform) {
|
|
6551
|
+
function pbBlock(task, platform, selectionOnly = false) {
|
|
6155
6552
|
const ps = (S.pubState && S.pubState.platforms && S.pubState.platforms[platform]) || {};
|
|
6156
6553
|
const st = ps.status || "none";
|
|
6157
6554
|
const [label, cls] = PB_ST[st] || PB_ST.none;
|
|
@@ -6172,6 +6569,8 @@ function pbBlock(task, platform) {
|
|
|
6172
6569
|
const remoteN = hasRemote ? ((typeof book.remote_total === "number")
|
|
6173
6570
|
? book.remote_total : book.remote_published) : null;
|
|
6174
6571
|
const nCh = hasRemote ? remoteN : localN;
|
|
6572
|
+
const publishedLatest = Math.max(Number(book && book.confirmed_published_through) || 0,
|
|
6573
|
+
((book && book.remote_published_nos) || []).reduce((max, n) => Math.max(max, Number(n) || 0), 0));
|
|
6175
6574
|
let chTitle = "";
|
|
6176
6575
|
if (hasRemote) {
|
|
6177
6576
|
const parts = [];
|
|
@@ -6203,7 +6602,8 @@ function pbBlock(task, platform) {
|
|
|
6203
6602
|
btns += ' <button class="primary" ' + (busy ? "disabled" : "") +
|
|
6204
6603
|
' onclick="pbUploadChapter(\'' + esc(task.id) + "', '" + platform + '\')" title="' +
|
|
6205
6604
|
esc(chTitle) + '">' +
|
|
6206
|
-
t("发一章") + (
|
|
6605
|
+
t("发一章") + (publishedLatest ? t("(已发布至 {0} 章)", publishedLatest)
|
|
6606
|
+
: nCh ? t("(已发 ") + nCh + t(")") : "") + "</button>";
|
|
6207
6607
|
btns += ' <button class="ghost pb-tool" ' + (busy ? "disabled" : "") +
|
|
6208
6608
|
' onclick="pbSyncPublished(\'' + esc(task.id) + "', '" + platform + '\')" title="' +
|
|
6209
6609
|
esc(t("按平台实况纠正已发章数")) + '">' + t("校准") + "</button>";
|
|
@@ -6237,7 +6637,8 @@ function pbBlock(task, platform) {
|
|
|
6237
6637
|
// 「发布所选」人工模式 / 「直发所选」逐章自动提交(护栏照常生效)。
|
|
6238
6638
|
// 分卷由服务端按卷计划自动选/建(volumes.py),界面上不用管卷。
|
|
6239
6639
|
const its = (au.items && au.items.length ? au.items : [])
|
|
6240
|
-
.map((x) => ({ n: Number(x.chapter_no), v: x.volume || ""
|
|
6640
|
+
.map((x) => ({ n: Number(x.chapter_no), v: x.volume || "", review: x.review_status || "pending",
|
|
6641
|
+
reason: x.review_reason || "", ready: x.publish_ready === true }))
|
|
6241
6642
|
.filter((x) => x.n > 0);
|
|
6242
6643
|
if (its.length && !run) {
|
|
6243
6644
|
const key = task.id + ":" + platform;
|
|
@@ -6252,49 +6653,79 @@ function pbBlock(task, platform) {
|
|
|
6252
6653
|
}
|
|
6253
6654
|
if (!S.pbSel[key] || S.pbSel[key].sig !== sig)
|
|
6254
6655
|
S.pbSel[key] = { sig, sel: new Set(its.map((x) => x.n)), groups };
|
|
6656
|
+
S.pbSel[key].groups = groups;
|
|
6657
|
+
S.pbSel[key].ready = new Set(its.filter((x) => x.ready).map((x) => x.n));
|
|
6255
6658
|
const sel = S.pbSel[key].sel;
|
|
6256
6659
|
let chips = "";
|
|
6257
6660
|
for (let gi = 0; gi < groups.length; gi++) {
|
|
6258
6661
|
const g = groups[gi];
|
|
6662
|
+
chips += '<section class="pb-pend-volume" data-volume="' + gi + '">';
|
|
6259
6663
|
if (groups.length > 1 || g.v) {
|
|
6260
6664
|
// 卷头=整卷开关:全选打勾、部分选中显示 已选/总(2026-10-10 按卷选章)
|
|
6261
6665
|
const cnt = g.ns.filter((n) => sel.has(n)).length;
|
|
6262
6666
|
const allOn = cnt === g.ns.length;
|
|
6263
|
-
chips += '<div class="pb-vol"><label class="pb-vol-sel' + (allOn ? " on" : "") +
|
|
6667
|
+
chips += '<div class="pb-vol"><label class="pb-vol-sel' + (allOn ? " on" : cnt ? " partial" : "") +
|
|
6264
6668
|
'" title="' + esc(t("勾选卷头=整卷进所选,再点取消")) + '">' +
|
|
6265
6669
|
'<input type="checkbox" ' + (allOn ? "checked" : "") +
|
|
6266
6670
|
' onchange="pbSelVolume(\'' + esc(task.id) + "','" + platform + "'," + gi + ',this.checked)">' +
|
|
6267
|
-
|
|
6671
|
+
'<span class="pb-vol-name">' + esc(g.v || t("未分卷")) + '</span>' +
|
|
6672
|
+
'<span class="pb-vol-count">' + esc(t("已选 {0}/{1} 章", cnt, g.ns.length)) + "</span></label></div>";
|
|
6268
6673
|
}
|
|
6269
|
-
|
|
6270
|
-
|
|
6674
|
+
chips += '<div class="pb-chapter-grid">';
|
|
6675
|
+
for (const n of g.ns) {
|
|
6676
|
+
const item = its.find((x) => x.n === n);
|
|
6677
|
+
const states = { ready: "已过审 · 可直发", passed_blocked: "已过审 · 发布受限",
|
|
6678
|
+
failed: "审稿未通过", stale: "已修改 · 需重审", pending: "待审或评审不足" };
|
|
6679
|
+
const review = Object.hasOwn(states, item.review) ? item.review : "pending";
|
|
6680
|
+
chips += '<label class="pb-chip review-' + review + (sel.has(n) ? " on" : "") + '" data-chapter="' + n + '" title="' +
|
|
6681
|
+
esc(t("第 {0} 章", n) + (item.reason ? " · " + item.reason : "")) + '">' +
|
|
6271
6682
|
'<input type="checkbox" ' + (sel.has(n) ? "checked" : "") +
|
|
6272
6683
|
' onchange="pbSelToggle(\'' + esc(task.id) + "','" + platform + "'," + n + ',this.checked)">' +
|
|
6273
|
-
n + "</label>";
|
|
6684
|
+
'<span class="pb-chip-number">' + n + '</span><small class="pb-chip-review">' + esc(t(states[review])) + "</small></label>";
|
|
6685
|
+
}
|
|
6686
|
+
chips += "</div></section>";
|
|
6274
6687
|
}
|
|
6275
6688
|
pendHtml += '<div class="pb-pend" data-key="' + esc(key) + '">' +
|
|
6276
6689
|
'<div class="pb-pend-head">' +
|
|
6277
|
-
'<
|
|
6690
|
+
'<div class="pb-pend-heading"><strong>' + esc(t("待发 ")) + its.length + esc(t(" 章")) + '</strong>' +
|
|
6691
|
+
'<span>' + esc(t("按卷计划自动分卷")) + "</span></div>" +
|
|
6692
|
+
'<button class="ghost pb-ready-select" ' + (S.pbSel[key].ready.size ? "" : "disabled") +
|
|
6693
|
+
' title="' + esc(t("仅选择单章审稿和发布门禁均已通过的章节")) + '" onclick="pbSelReady(\'' + esc(task.id) + "','" + platform + '\')">' + esc(t("只选可直发")) + '</button>' +
|
|
6278
6694
|
'<a href="javascript:void(0)" onclick="pbSelAll(\'' + esc(task.id) + "','" + platform + "',true)" + '">' + esc(t("全选")) + "</a>" +
|
|
6279
6695
|
'<a href="javascript:void(0)" onclick="pbSelAll(\'' + esc(task.id) + "','" + platform + "',false)" + '">' + esc(t("清空")) + "</a>" +
|
|
6280
6696
|
"</div>" +
|
|
6697
|
+
'<div class="pb-review-legend" aria-label="' + esc(t("章节审核状态说明")) + '">' +
|
|
6698
|
+
'<span class="review-ready"><i aria-hidden="true"></i>' + esc(t("已过审 · 可直发")) + '</span>' +
|
|
6699
|
+
'<span class="review-passed_blocked"><i aria-hidden="true"></i>' + esc(t("已过审 · 发布受限")) + '</span>' +
|
|
6700
|
+
'<span class="review-failed"><i aria-hidden="true"></i>' + esc(t("审稿未通过")) + '</span>' +
|
|
6701
|
+
'<span class="review-pending"><i aria-hidden="true"></i>' + esc(t("待审或评审不足")) + '</span>' +
|
|
6702
|
+
"</div>" +
|
|
6703
|
+
'<div class="pb-pend-progress">' +
|
|
6704
|
+
(au.published_latest ? '<strong>' + esc(t("已发布至第 {0} 章", au.published_latest)) + '</strong>' : '') +
|
|
6705
|
+
(au.next_chapter ? '<span>' + esc(t("下一章:第 {0} 章", au.next_chapter)) + '</span>' : '') +
|
|
6706
|
+
'<span>' + esc(t("可直发 {0} 章 · 单章过审 {1} 章", au.ready_count || 0, au.review_passed_count || 0)) + '</span></div>' +
|
|
6281
6707
|
'<div class="pb-pend-chips">' + chips + "</div>" +
|
|
6708
|
+
(au.drafted_nos && au.drafted_nos.length ? '<details class="pb-drafted-list"><summary>' +
|
|
6709
|
+
esc(t("已存草稿 {0} 章(未发布)", au.drafted_nos.length)) + '</summary><p>' +
|
|
6710
|
+
esc(au.drafted_nos.map((n) => t("第 {0} 章", n)).join(" · ")) + '</p></details>' : '') +
|
|
6711
|
+
'<div class="pb-pend-foot"><span class="pb-pend-count" aria-live="polite">' + esc(t("已选 {0}/{1} 章", sel.size, its.length)) + '</span>' +
|
|
6712
|
+
'<span class="pb-pend-mode-hint">' + esc(t("发布需逐章确认,直发自动提交,草稿不上线。")) + '</span>' +
|
|
6282
6713
|
'<div class="pb-pend-btns">' +
|
|
6283
|
-
'<button class="
|
|
6714
|
+
'<button class="primary" data-selection-action data-locked="' + busy + '" ' + (busy || !sel.size ? "disabled" : "") +
|
|
6284
6715
|
' title="' + esc(au.guard_ok ? "" : t("护栏提示:") + (au.guard_reason || "")) + '"' +
|
|
6285
6716
|
' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',false)">' +
|
|
6286
6717
|
t("发布所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6287
|
-
'<button class="ghost" ' + (busy || !sel.size ? "disabled" : "") +
|
|
6718
|
+
'<button class="ghost" data-selection-action data-locked="' + busy + '" ' + (busy || !sel.size ? "disabled" : "") +
|
|
6288
6719
|
' title="' + esc(t("所选章节逐章自动提交(无人值守),护栏每日上限/连败退避照常生效") +
|
|
6289
6720
|
(au.guard_ok ? "" : ";" + t("护栏提示:") + (au.guard_reason || ""))) + '"' +
|
|
6290
6721
|
' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',true)">' +
|
|
6291
6722
|
t("直发所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6292
|
-
'<button class="ghost" ' + (busy || !sel.size || au.draft_ok === false ? "disabled" : "") +
|
|
6723
|
+
'<button class="ghost" data-selection-action data-locked="' + (busy || au.draft_ok === false) + '" ' + (busy || !sel.size || au.draft_ok === false ? "disabled" : "") +
|
|
6293
6724
|
' title="' + esc(t("所选章节逐章自动存草稿(不上线、不受质量门禁拦截),完成后到平台检查并提交发布") +
|
|
6294
6725
|
(au.draft_ok !== false ? "" : ";" + t("护栏提示:") + (au.draft_reason || ""))) + '"' +
|
|
6295
6726
|
' onclick="pbDraftSelected(\'' + esc(task.id) + "','" + platform + '\')">' +
|
|
6296
6727
|
t("存草稿所选") + t("(") + sel.size + t(")") + "</button>" +
|
|
6297
|
-
"</div></div>";
|
|
6728
|
+
"</div></div></div>";
|
|
6298
6729
|
}
|
|
6299
6730
|
if (au.pending > 0 && au.draft_ok === false) {
|
|
6300
6731
|
// 硬护栏(建书未确认/每日上限/连败退避):存草稿也走不了,红色示警
|
|
@@ -6332,6 +6763,15 @@ function pbBlock(task, platform) {
|
|
|
6332
6763
|
btns += '<button class="ghost" onclick="pbRegToggle(\'' + esc(task.id) + "','" + platform + '\')" title="' +
|
|
6333
6764
|
esc(t("作品已在平台建好(建书流程没走通或手工建的)?补登记后直接发章,不会在平台新建")) + '">' + t("登记已有作品") + "</button>";
|
|
6334
6765
|
}
|
|
6766
|
+
if (selectionOnly) return pendHtml;
|
|
6767
|
+
if (pendHtml) {
|
|
6768
|
+
const pending = (((S.pubAuto || {}).books || []).find((b) => b.platform === platform) || {}).pending || 0;
|
|
6769
|
+
const view = (((S.pubAuto || {}).books || []).find((b) => b.platform === platform) || {});
|
|
6770
|
+
pendHtml = '<div class="pb-pend-summary"><span>' + esc(t("待发 ")) + pending + esc(t(" 章")) +
|
|
6771
|
+
(view.next_chapter ? ' · ' + esc(t("下一章:第 {0} 章", view.next_chapter)) : '') + '</span>' +
|
|
6772
|
+
'<button class="primary" onclick="pbOpenSelection(\'' + esc(task.id) + "','" + platform + '\')">' +
|
|
6773
|
+
esc(t("选择章节并发布")) + '</button></div>';
|
|
6774
|
+
}
|
|
6335
6775
|
let extra = "";
|
|
6336
6776
|
extra += pendHtml;
|
|
6337
6777
|
if (st === "error" && !bookReady && ps.last_action === "create_book") {
|
|
@@ -6361,6 +6801,53 @@ function pbBlock(task, platform) {
|
|
|
6361
6801
|
}
|
|
6362
6802
|
|
|
6363
6803
|
let _pbTimer = 0;
|
|
6804
|
+
window.pbOpenSelection = function (taskId, platform) {
|
|
6805
|
+
const task = ((S.state || {}).tasks || []).find((x) => x.id === taskId);
|
|
6806
|
+
if (!task) return;
|
|
6807
|
+
const html = pbBlock(task, platform, true);
|
|
6808
|
+
if (!html) { toast(t("当前没有可选择的待发章节")); return; }
|
|
6809
|
+
const title = t("选择章节并发布") + " · " + t(platform === "fanqie" ? "番茄" : "七猫");
|
|
6810
|
+
const returnFocus = document.activeElement;
|
|
6811
|
+
openModal(title, "", "");
|
|
6812
|
+
_pbReturnFocus = returnFocus;
|
|
6813
|
+
$("modal").dataset.pbKey = taskId + ":" + platform;
|
|
6814
|
+
$("modal").classList.add("pb-selection-modal");
|
|
6815
|
+
pbRefreshSelectionDialog(task);
|
|
6816
|
+
const first = $("modal-body").querySelector("input");
|
|
6817
|
+
if (first) first.focus({ preventScroll: true });
|
|
6818
|
+
};
|
|
6819
|
+
|
|
6820
|
+
function pbRefreshSelectionDialog(task) {
|
|
6821
|
+
const modal = $("modal");
|
|
6822
|
+
if (!modal || modal.classList.contains("hidden") || !modal.dataset.pbKey) return;
|
|
6823
|
+
const platform = ["fanqie", "qimao"].find((p) => modal.dataset.pbKey === task.id + ":" + p);
|
|
6824
|
+
if (!platform) { closeModal(); return; }
|
|
6825
|
+
const html = pbBlock(task, platform, true);
|
|
6826
|
+
if (!html) { closeModal(); return; }
|
|
6827
|
+
const shell = document.createElement("div");
|
|
6828
|
+
shell.innerHTML = html;
|
|
6829
|
+
const foot = shell.querySelector(".pb-pend-foot");
|
|
6830
|
+
foot.remove();
|
|
6831
|
+
const body = $("modal-body");
|
|
6832
|
+
const scrollTop = body.scrollTop;
|
|
6833
|
+
const active = document.activeElement;
|
|
6834
|
+
const hadFocus = modal.contains(active);
|
|
6835
|
+
const chip = active && active.closest(".pb-chip");
|
|
6836
|
+
const volume = active && active.closest(".pb-pend-volume");
|
|
6837
|
+
const action = active && active.getAttribute("onclick");
|
|
6838
|
+
const chapter = chip ? chip.dataset.chapter : "";
|
|
6839
|
+
const volumeIndex = volume ? volume.dataset.volume : "";
|
|
6840
|
+
body.innerHTML = shell.innerHTML;
|
|
6841
|
+
body.scrollTop = scrollTop;
|
|
6842
|
+
$("modal-foot").innerHTML = foot.outerHTML;
|
|
6843
|
+
pbSelCountSync(modal.dataset.pbKey);
|
|
6844
|
+
let target = null;
|
|
6845
|
+
if (chapter) target = [...body.querySelectorAll(".pb-chip")].find((c) => c.dataset.chapter === chapter)?.querySelector("input");
|
|
6846
|
+
else if (volumeIndex) target = body.querySelector('[data-volume="' + volumeIndex + '"] .pb-vol-sel input');
|
|
6847
|
+
else if (action && hadFocus) target = [...modal.querySelectorAll("[onclick]")].find((el) => el.getAttribute("onclick") === action);
|
|
6848
|
+
if (target) target.focus({ preventScroll: true });
|
|
6849
|
+
}
|
|
6850
|
+
|
|
6364
6851
|
function pbSyncState(task) {
|
|
6365
6852
|
clearTimeout(_pbTimer);
|
|
6366
6853
|
// 5s 节流折进调度间隔:距上一拉未满 5s 就等满再拉(pbKick 置 0 可立即拉)
|
|
@@ -6511,15 +6998,15 @@ window.pbUploadChapter = async function (taskId, platform) {
|
|
|
6511
6998
|
if (!wd) { box.innerHTML = '<div class="pb-err">' + esc(t("找不到任务工作目录")) + "</div>"; return; }
|
|
6512
6999
|
let files = [];
|
|
6513
7000
|
try {
|
|
6514
|
-
const r = await api("/api/
|
|
6515
|
-
|
|
6516
|
-
|
|
7001
|
+
const r = await api("/api/publish/task/" + encodeURIComponent(taskId) + "/pending");
|
|
7002
|
+
const book = (r.books || []).find((b) => b.platform === platform);
|
|
7003
|
+
files = ((book && book.items) || []).map((f) => ({ name: f.file, chapter_no: f.chapter_no }));
|
|
6517
7004
|
} catch (e) {
|
|
6518
7005
|
box.innerHTML = '<div class="pb-err">' + esc(t("列文件失败:") + e.message) + "</div>";
|
|
6519
7006
|
return;
|
|
6520
7007
|
}
|
|
6521
|
-
if (!files.length) { box.innerHTML = '<div class="pb-
|
|
6522
|
-
files.sort((a, b) =>
|
|
7008
|
+
if (!files.length) { box.innerHTML = '<div class="pb-hint">' + esc(t("暂无待发章节,已发布和已存草稿的章节已排除")) + "</div>"; return; }
|
|
7009
|
+
files.sort((a, b) => a.chapter_no - b.chapter_no);
|
|
6523
7010
|
const item = (f) =>
|
|
6524
7011
|
'<button class="ghost pb-ch-item" onclick="pbSendChapter(\'' + esc(taskId) + "', '" + platform +
|
|
6525
7012
|
'\',\'' + esc(f.name) + '\')" title="' + esc(f.name) + '">' + esc(f.name.split("/").pop()) + "</button>";
|
|
@@ -6585,18 +7072,37 @@ function pbSelCountSync(key) {
|
|
|
6585
7072
|
// 就地对账:按钮计数/禁用态与全部 chip 勾选态向状态层看齐——重绘链一旦
|
|
6586
7073
|
// 断掉(poll 失败/详情上下文切换),勾选与计数也不许脱节(2026-10-08 实案
|
|
6587
7074
|
// 「勾了 18 计数 48」即重绘未跟上)。
|
|
6588
|
-
const
|
|
6589
|
-
|
|
7075
|
+
const modal = $("modal");
|
|
7076
|
+
const box = modal && modal.dataset.pbKey === key ? modal
|
|
7077
|
+
: [...document.querySelectorAll(".pb-pend")].find((el) => el.dataset.key === key);
|
|
6590
7078
|
const ent = (S.pbSel || {})[key];
|
|
6591
7079
|
if (!box || !ent) return;
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
7080
|
+
const count = box.querySelector(".pb-pend-count");
|
|
7081
|
+
if (count) count.textContent = t("已选 {0}/{1} 章", ent.sel.size, ent.sig.split(",").length);
|
|
7082
|
+
for (const b of box.querySelectorAll("[data-selection-action]")) {
|
|
7083
|
+
const action = b.getAttribute("onclick") || "";
|
|
7084
|
+
const label = action.includes("pbDraftSelected") ? "存草稿所选"
|
|
7085
|
+
: action.includes(",true)") ? "直发所选" : "发布所选";
|
|
7086
|
+
b.textContent = t(label) + t("(") + ent.sel.size + t(")");
|
|
7087
|
+
b.disabled = ent.sel.size === 0 || b.dataset.locked === "true";
|
|
7088
|
+
if (!b.hasAttribute("data-ready-title")) b.dataset.readyTitle = b.title;
|
|
7089
|
+
b.title = ent.sel.size === 0 ? t("请先选择章节") : b.dataset.readyTitle;
|
|
7090
|
+
}
|
|
7091
|
+
for (const section of box.querySelectorAll(".pb-pend-volume")) {
|
|
7092
|
+
const g = ent.groups[Number(section.dataset.volume)];
|
|
7093
|
+
const label = section.querySelector(".pb-vol-sel");
|
|
7094
|
+
if (!g || !label) continue;
|
|
7095
|
+
const cnt = g.ns.filter((n) => ent.sel.has(n)).length;
|
|
7096
|
+
const input = label.querySelector("input");
|
|
7097
|
+
input.checked = cnt === g.ns.length;
|
|
7098
|
+
input.indeterminate = cnt > 0 && cnt < g.ns.length;
|
|
7099
|
+
label.classList.toggle("on", input.checked);
|
|
7100
|
+
label.classList.toggle("partial", input.indeterminate);
|
|
7101
|
+
label.querySelector(".pb-vol-count").textContent = t("已选 {0}/{1} 章", cnt, g.ns.length);
|
|
6596
7102
|
}
|
|
6597
7103
|
for (const c of box.querySelectorAll(".pb-chip")) {
|
|
6598
7104
|
const inp = c.querySelector("input");
|
|
6599
|
-
const num = Number(
|
|
7105
|
+
const num = Number(c.dataset.chapter);
|
|
6600
7106
|
if (!inp || !num) continue;
|
|
6601
7107
|
const on = ent.sel.has(num);
|
|
6602
7108
|
if (inp.checked !== on) inp.checked = on;
|
|
@@ -6609,8 +7115,7 @@ window.pbSelToggle = function (taskId, platform, no, on) {
|
|
|
6609
7115
|
const ent = (S.pbSel || {})[key];
|
|
6610
7116
|
if (!ent) return;
|
|
6611
7117
|
if (on) ent.sel.add(Number(no)); else ent.sel.delete(Number(no));
|
|
6612
|
-
pbSelCountSync(key); //
|
|
6613
|
-
S._pbSig = ""; pbKick();
|
|
7118
|
+
pbSelCountSync(key); // 选择只在本地同步,避免重绘打断滚动和键盘焦点
|
|
6614
7119
|
};
|
|
6615
7120
|
|
|
6616
7121
|
window.pbSelAll = function (taskId, platform, all) {
|
|
@@ -6620,7 +7125,14 @@ window.pbSelAll = function (taskId, platform, all) {
|
|
|
6620
7125
|
if (all) for (const n of ent.sig.split(",")) ent.sel.add(Number(n));
|
|
6621
7126
|
else ent.sel.clear();
|
|
6622
7127
|
pbSelCountSync(key);
|
|
6623
|
-
|
|
7128
|
+
};
|
|
7129
|
+
|
|
7130
|
+
window.pbSelReady = function (taskId, platform) {
|
|
7131
|
+
const key = taskId + ":" + platform;
|
|
7132
|
+
const ent = (S.pbSel || {})[key];
|
|
7133
|
+
if (!ent) return;
|
|
7134
|
+
ent.sel = new Set(ent.ready || []);
|
|
7135
|
+
pbSelCountSync(key);
|
|
6624
7136
|
};
|
|
6625
7137
|
|
|
6626
7138
|
window.pbSelVolume = function (taskId, platform, gi, on) {
|
|
@@ -6631,7 +7143,6 @@ window.pbSelVolume = function (taskId, platform, gi, on) {
|
|
|
6631
7143
|
if (on) for (const n of g.ns) ent.sel.add(n);
|
|
6632
7144
|
else for (const n of g.ns) ent.sel.delete(n);
|
|
6633
7145
|
pbSelCountSync(key);
|
|
6634
|
-
S._pbSig = ""; pbKick();
|
|
6635
7146
|
};
|
|
6636
7147
|
|
|
6637
7148
|
window.pbPublishSelected = async function (taskId, platform, autoSubmit) {
|
|
@@ -6649,6 +7160,7 @@ window.pbPublishSelected = async function (taskId, platform, autoSubmit) {
|
|
|
6649
7160
|
"/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
6650
7161
|
{ platform, chapters, auto_submit: !!autoSubmit },
|
|
6651
7162
|
"批量发布前的质量门禁未通过", taskId);
|
|
7163
|
+
if (r && r.started && $("modal").dataset.pbKey === key) closeModal();
|
|
6652
7164
|
if (!(r && r.gate_reviewed))
|
|
6653
7165
|
toast(autoSubmit ? t("直发已启动,进度见发布台")
|
|
6654
7166
|
: t("正在填第一章稿——填好后请在浏览器窗口里确认提交,再重新校准"));
|
|
@@ -6698,6 +7210,7 @@ window.pbDraftSelected = async function (taskId, platform) {
|
|
|
6698
7210
|
try {
|
|
6699
7211
|
await api("/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
|
|
6700
7212
|
{ method: "POST", body: JSON.stringify({ platform, chapters, as_draft: true }) });
|
|
7213
|
+
if ($("modal").dataset.pbKey === key) closeModal();
|
|
6701
7214
|
toast(t("存草稿已启动:逐章自动填稿并保存草稿,进度见发布台"));
|
|
6702
7215
|
} catch (e) { toast(t("存草稿启动失败:") + e.message, true); }
|
|
6703
7216
|
S._pbSig = ""; pbKick();
|
|
@@ -8632,7 +9145,7 @@ function hiveThinkLine(lines) {
|
|
|
8632
9145
|
* localStorage 持久化/WebGL 不可用降级,以及把 renderHive 的泳道数据翻译给场景。 —— */
|
|
8633
9146
|
let hiveScene = null; // Hive3D 场景实例(懒创建)
|
|
8634
9147
|
let hiveSceneDead = false; // WebGL 不可用/上下文丢失——本次页面周期内不再尝试
|
|
8635
|
-
let hiveMode = "
|
|
9148
|
+
let hiveMode = "reference"; // high-fidelity artwork is the public default; live3d stays internal for QA
|
|
8636
9149
|
let hiveRunId = ""; // 最近一次 renderHive 的 run.id(尾巴/思考缓存键前缀)
|
|
8637
9150
|
const hiveLiveMeta = {}; // rel -> { started_at }(3D 芯片秒表用)
|
|
8638
9151
|
let hiveDialogPoll = null;
|
|
@@ -8640,6 +9153,11 @@ let hiveDialogKey = "";
|
|
|
8640
9153
|
|
|
8641
9154
|
function setHiveSceneButtons(mode) {
|
|
8642
9155
|
document.querySelectorAll("[data-hive-view]").forEach((button) => {
|
|
9156
|
+
// 自由 3D 尚未达到发布质量:保留引擎给内部 QA,但不把入口暴露给用户。
|
|
9157
|
+
// hidden/aria-hidden 双保险,避免旧缓存或浏览器恢复状态重新显示它。
|
|
9158
|
+
const internalOnly = button.dataset.hiveView === "live3d";
|
|
9159
|
+
button.hidden = internalOnly;
|
|
9160
|
+
button.setAttribute("aria-hidden", internalOnly ? "true" : "false");
|
|
8643
9161
|
const active = button.dataset.hiveView === mode;
|
|
8644
9162
|
button.classList.toggle("active", active);
|
|
8645
9163
|
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
@@ -8711,6 +9229,8 @@ function closeHiveMonitorLog() {
|
|
|
8711
9229
|
function setHiveSceneMode(mode, opts) {
|
|
8712
9230
|
const viewport = $("rd-hive-viewport");
|
|
8713
9231
|
if (!viewport) return;
|
|
9232
|
+
// Public mode selection is deliberately limited to the polished artwork and 2D list.
|
|
9233
|
+
// live3d remains callable by internal QA, but can never be restored from user state.
|
|
8714
9234
|
let selected = ["live3d", "2d"].includes(mode) ? mode : "reference";
|
|
8715
9235
|
if (selected !== "2d") {
|
|
8716
9236
|
const sc = ensureHiveScene();
|
|
@@ -8802,18 +9322,18 @@ function setupHiveSceneControls() {
|
|
|
8802
9322
|
else hiveScene.zoomAt(button.dataset.hiveSceneAction === "zoom-in" ? 1 / 1.18 : 1.18);
|
|
8803
9323
|
});
|
|
8804
9324
|
});
|
|
8805
|
-
let saved = "
|
|
9325
|
+
let saved = "reference";
|
|
8806
9326
|
try {
|
|
8807
9327
|
saved = localStorage.getItem("orch.hiveView.v2") || "";
|
|
8808
9328
|
if (!saved) {
|
|
8809
|
-
// Migrate an explicitly selected 2D/live3D preference; the old default "reference"
|
|
8810
|
-
// was auto-persisted for every new visitor, so it is not a reliable user preference.
|
|
8811
9329
|
const legacy = localStorage.getItem("orch.hiveView");
|
|
8812
|
-
saved = legacy === "2d"
|
|
9330
|
+
saved = legacy === "2d" ? "2d" : "reference";
|
|
8813
9331
|
localStorage.setItem("orch.hiveView.v2", saved);
|
|
8814
9332
|
}
|
|
8815
|
-
} catch (e) { /* ignore storage restrictions and keep the
|
|
8816
|
-
|
|
9333
|
+
} catch (e) { /* ignore storage restrictions and keep the high-fidelity default */ }
|
|
9334
|
+
// A previously saved free-3D preference must not reopen the unreleased mode.
|
|
9335
|
+
// The engine remains callable by internal QA through setHiveSceneMode("live3d").
|
|
9336
|
+
setHiveSceneMode(saved === "2d" ? "2d" : "reference", { silent: true });
|
|
8817
9337
|
}
|
|
8818
9338
|
|
|
8819
9339
|
/* renderHive → 场景数据翻译:泳道=蜂巢塔的一层,格子按状态映射高度/颜色。
|
|
@@ -9477,21 +9997,63 @@ function scheduleChatLive(runId) {
|
|
|
9477
9997
|
}, 1200);
|
|
9478
9998
|
}
|
|
9479
9999
|
|
|
9480
|
-
/*
|
|
9481
|
-
*
|
|
9482
|
-
*
|
|
10000
|
+
/* 对话正文:保留 Markdown 的段落/标题/列表层级,``` 围栏复用代码块组件。
|
|
10001
|
+
* 模型文本先转义;链接只允许 http(s)/mailto,避免把回答内容当 HTML 或脚本执行。
|
|
10002
|
+
* 未闭合围栏按代码块收尾,流式输出中途也不撒裸反引号。 */
|
|
9483
10003
|
function chatBodyHTML(text) {
|
|
9484
10004
|
const lines = String(text || "").split(/\r?\n/);
|
|
9485
10005
|
const out = [];
|
|
9486
10006
|
let txt = [], code = null;
|
|
10007
|
+
const inline = (raw) => {
|
|
10008
|
+
let h = esc(raw);
|
|
10009
|
+
h = h.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
10010
|
+
h = h.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
10011
|
+
h = h.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
|
|
10012
|
+
h = h.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g,
|
|
10013
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
10014
|
+
return h;
|
|
10015
|
+
};
|
|
9487
10016
|
const flushTxt = () => {
|
|
9488
10017
|
if (!txt.length) return;
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
}
|
|
10018
|
+
const blocks = [];
|
|
10019
|
+
let paragraph = [], list = null;
|
|
10020
|
+
const closeParagraph = () => {
|
|
10021
|
+
if (paragraph.length) blocks.push("<p>" + paragraph.map(inline).join("<br>") + "</p>");
|
|
10022
|
+
paragraph = [];
|
|
10023
|
+
};
|
|
10024
|
+
const closeList = () => {
|
|
10025
|
+
if (list) { blocks.push("</" + list + ">"); list = null; }
|
|
10026
|
+
};
|
|
10027
|
+
for (const rawLine of txt) {
|
|
10028
|
+
const line = rawLine.trimEnd();
|
|
10029
|
+
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
10030
|
+
const bullet = line.match(/^\s{0,3}[-+*]\s+(.+)$/);
|
|
10031
|
+
const numbered = line.match(/^\s{0,3}\d+[.)]\s+(.+)$/);
|
|
10032
|
+
if (!line.trim()) { closeParagraph(); closeList(); continue; }
|
|
10033
|
+
if (heading) {
|
|
10034
|
+
closeParagraph(); closeList();
|
|
10035
|
+
const level = heading[1].length;
|
|
10036
|
+
blocks.push("<h" + level + ">" + inline(heading[2]) + "</h" + level + ">");
|
|
10037
|
+
continue;
|
|
10038
|
+
}
|
|
10039
|
+
if (/^\s{0,3}(---+|___+|\*\*\*+)\s*$/.test(line)) {
|
|
10040
|
+
closeParagraph(); closeList(); blocks.push("<hr>"); continue;
|
|
10041
|
+
}
|
|
10042
|
+
if (bullet || numbered) {
|
|
10043
|
+
closeParagraph();
|
|
10044
|
+
const kind = bullet ? "ul" : "ol";
|
|
10045
|
+
if (list !== kind) { closeList(); list = kind; blocks.push("<" + list + ">"); }
|
|
10046
|
+
blocks.push("<li>" + inline((bullet || numbered)[1]) + "</li>");
|
|
10047
|
+
continue;
|
|
10048
|
+
}
|
|
10049
|
+
closeList();
|
|
10050
|
+
const quote = line.match(/^\s{0,3}>\s?(.*)$/);
|
|
10051
|
+
if (quote) {
|
|
10052
|
+
closeParagraph(); blocks.push("<blockquote><p>" + inline(quote[1]) + "</p></blockquote>");
|
|
10053
|
+
} else paragraph.push(line);
|
|
10054
|
+
}
|
|
10055
|
+
closeParagraph(); closeList();
|
|
10056
|
+
out.push('<div class="chat-body chat-markdown">' + blocks.join("") + "</div>");
|
|
9495
10057
|
txt = [];
|
|
9496
10058
|
};
|
|
9497
10059
|
for (const raw of lines) {
|
|
@@ -9827,6 +10389,7 @@ function renderRunOutcome(run, runs, aggregate) {
|
|
|
9827
10389
|
function renderDetailOverview(run, runs, task, aggregate) {
|
|
9828
10390
|
const box = $("rd-overview");
|
|
9829
10391
|
if (!box || !run) return;
|
|
10392
|
+
CodeBeeActivity.markViewed(run);
|
|
9830
10393
|
const direct = chatEngineIsDirect(run);
|
|
9831
10394
|
box.classList.toggle("direct-overview", direct);
|
|
9832
10395
|
const all = runs && runs.length ? runs : [run];
|
|
@@ -10259,6 +10822,17 @@ function bindChat() {
|
|
|
10259
10822
|
ta.style.height = "auto";
|
|
10260
10823
|
ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
|
|
10261
10824
|
});
|
|
10825
|
+
// 移动端软键盘会缩短 visual viewport;把输入条主动带回可视区,
|
|
10826
|
+
// 避免窄屏详情里键盘弹出后发送按钮落在屏幕下方(尤其是 Android Chrome)。
|
|
10827
|
+
const keepChatInputVisible = () => {
|
|
10828
|
+
if (document.activeElement !== ta || window.innerWidth > 720) return;
|
|
10829
|
+
requestAnimationFrame(() => {
|
|
10830
|
+
try { ta.scrollIntoView({ block: "nearest", inline: "nearest" }); }
|
|
10831
|
+
catch (e) { ta.scrollIntoView(); }
|
|
10832
|
+
});
|
|
10833
|
+
};
|
|
10834
|
+
ta.addEventListener("focus", () => setTimeout(keepChatInputVisible, 60));
|
|
10835
|
+
if (window.visualViewport) window.visualViewport.addEventListener("resize", keepChatInputVisible);
|
|
10262
10836
|
ta.addEventListener("keydown", (e) => {
|
|
10263
10837
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); window.chatSend(); }
|
|
10264
10838
|
});
|
|
@@ -11144,6 +11718,10 @@ function card(c) {
|
|
|
11144
11718
|
? '<label class="toggle"><input type="checkbox" ' + (c.orch_enabled ? "checked" : "") +
|
|
11145
11719
|
' onchange="toggleOrch(\'' + esc(c.id) + '\', this.checked)">' + t(" 参与编排") + '</label>'
|
|
11146
11720
|
: '<span class="tag">' + t("仅管理") + '</span>';
|
|
11721
|
+
const capability = c.capabilities || {};
|
|
11722
|
+
const capTags = [capability.transport, capability.resume ? t("支持续会话") : "",
|
|
11723
|
+
capability.mcp ? "MCP" : "", capability.sandbox].filter(Boolean)
|
|
11724
|
+
.map((x) => '<span class="tag">' + esc(t(x)) + '</span>').join("");
|
|
11147
11725
|
const dis = mgmtBusy(c.id) ? " disabled" : "";
|
|
11148
11726
|
const hasModels = modelGroups().length > 0;
|
|
11149
11727
|
const modelBox = c.config_writable
|
|
@@ -11172,6 +11750,7 @@ function card(c) {
|
|
|
11172
11750
|
(c.installed && c.uninstall_cmd ? '<button class="danger small"' + dis + ' onclick="mgmt(\'' + esc(c.id) + '\', \'uninstall\')">' + t("卸载") + '</button>' : "") +
|
|
11173
11751
|
"</div>" +
|
|
11174
11752
|
'<div class="note">' + esc(t(c.note || "")) + "</div>" +
|
|
11753
|
+
(capTags ? '<div class="facts capability-tags">' + capTags + '</div>' : "") +
|
|
11175
11754
|
'<div class="facts">' + t("版本 ") + '<b>' + esc(c.version || "-") + "</b>" + t(" 模型 ") + "<b>" + esc(fmtModel(c.model) || "-") + "</b>" +
|
|
11176
11755
|
((c.detail || c.config_path) ? "<br>" + esc(c.detail || c.config_path) : "") + "</div>" +
|
|
11177
11756
|
mgmtPanel(c.id) +
|
|
@@ -14909,6 +15488,7 @@ function setLangBtn(lang) {
|
|
|
14909
15488
|
renderCodePreviews(); // 预览徽章是动态文案:语言切换时若停在皮肤页要跟着换
|
|
14910
15489
|
// 数据与备份页的清理预估/状态是 JS 动态生成的:停在那一页时也要跟着换语言
|
|
14911
15490
|
if (S.tab === "data") loadDataPage();
|
|
15491
|
+
if (S.tab === "agents") renderReviewQueue();
|
|
14912
15492
|
// 帮助中心若开着:徽章/标题/目录/正文跟着换语言重画
|
|
14913
15493
|
if (!$("welcome").classList.contains("hidden")) {
|
|
14914
15494
|
paintHelpChrome();
|
|
@@ -15464,12 +16044,103 @@ function ovDateText() {
|
|
|
15464
16044
|
function renderOverview() {
|
|
15465
16045
|
ovRenderHead();
|
|
15466
16046
|
ovRenderKpis();
|
|
16047
|
+
goalRender();
|
|
15467
16048
|
syncOvRanges();
|
|
15468
16049
|
renderOverviewTrend();
|
|
15469
16050
|
ovRenderQuick();
|
|
15470
16051
|
ovRenderRecent();
|
|
15471
16052
|
}
|
|
15472
16053
|
|
|
16054
|
+
let _goalCache = null;
|
|
16055
|
+
let _goalBusy = false;
|
|
16056
|
+
|
|
16057
|
+
async function goalLoad() {
|
|
16058
|
+
if (_goalBusy) return _goalCache;
|
|
16059
|
+
_goalBusy = true;
|
|
16060
|
+
try {
|
|
16061
|
+
_goalCache = await api("/api/goals");
|
|
16062
|
+
goalRender();
|
|
16063
|
+
} catch (e) {
|
|
16064
|
+
const error = $("ov-goal-error");
|
|
16065
|
+
if (error) error.textContent = e.message || t("无法读取长目标");
|
|
16066
|
+
} finally { _goalBusy = false; }
|
|
16067
|
+
return _goalCache;
|
|
16068
|
+
}
|
|
16069
|
+
|
|
16070
|
+
function goalRender() {
|
|
16071
|
+
const card = $("ov-goal");
|
|
16072
|
+
if (!card) return;
|
|
16073
|
+
const current = _goalCache && _goalCache.current;
|
|
16074
|
+
const empty = $("ov-goal-empty"), active = $("ov-goal-active"), phase = $("ov-goal-phase");
|
|
16075
|
+
empty.classList.toggle("hidden", !!current);
|
|
16076
|
+
active.classList.toggle("hidden", !current);
|
|
16077
|
+
phase.classList.toggle("hidden", !current);
|
|
16078
|
+
if (!current) return;
|
|
16079
|
+
const paused = current.phase === "paused";
|
|
16080
|
+
const rounds = Math.max(0, Number(current.rounds_started) || 0);
|
|
16081
|
+
const maximum = Math.max(1, Number(current.rounds_max) || 1);
|
|
16082
|
+
const percent = Math.min(100, Math.round(rounds * 100 / maximum));
|
|
16083
|
+
$("ov-goal-name").textContent = current.title || "";
|
|
16084
|
+
$("ov-goal-description-text").textContent = current.description || "";
|
|
16085
|
+
$("ov-goal-progress").textContent = t(paused ? "已暂停" : "目标进度");
|
|
16086
|
+
$("ov-goal-rounds").textContent = rounds + " / " + maximum + t(" 轮");
|
|
16087
|
+
$("ov-goal-progress-fill").style.width = percent + "%";
|
|
16088
|
+
phase.textContent = t(paused ? "已暂停" : "进行中");
|
|
16089
|
+
phase.className = "chip " + (paused ? "warn" : "running");
|
|
16090
|
+
$("ov-goal-error").textContent = "";
|
|
16091
|
+
let actions;
|
|
16092
|
+
if (paused) actions = '<button type="button" class="primary small" data-goal-phase="active">' + esc(t("恢复目标")) + '</button>';
|
|
16093
|
+
else actions = '<button type="button" class="primary small" data-goal-round' + (rounds >= maximum ? ' disabled' : '') + '>' + esc(t("开始一轮")) + '</button>' +
|
|
16094
|
+
'<button type="button" class="ghost small" data-goal-phase="paused">' + esc(t("暂停")) + '</button>';
|
|
16095
|
+
$("ov-goal-actions").innerHTML = actions + '<button type="button" class="ghost small" data-goal-phase="complete">' + esc(t("标记完成")) + '</button>';
|
|
16096
|
+
}
|
|
16097
|
+
|
|
16098
|
+
async function goalCreate() {
|
|
16099
|
+
const title = $("ov-goal-title").value.trim();
|
|
16100
|
+
const description = $("ov-goal-description").value.trim();
|
|
16101
|
+
if (!title) { $("ov-goal-title").focus(); return; }
|
|
16102
|
+
const button = $("ov-goal-create"); button.disabled = true;
|
|
16103
|
+
try {
|
|
16104
|
+
const data = await api("/api/goals", { method: "POST", body: JSON.stringify({ title, description }) });
|
|
16105
|
+
_goalCache = { current: data.goal, goals: [data.goal] };
|
|
16106
|
+
$("ov-goal-title").value = ""; $("ov-goal-description").value = "";
|
|
16107
|
+
goalRender();
|
|
16108
|
+
} catch (e) { $("ov-goal-error").textContent = e.message || t("创建目标失败"); }
|
|
16109
|
+
finally { button.disabled = false; }
|
|
16110
|
+
}
|
|
16111
|
+
|
|
16112
|
+
async function goalTransition(phase) {
|
|
16113
|
+
const current = _goalCache && _goalCache.current;
|
|
16114
|
+
if (!current) return;
|
|
16115
|
+
const actions = $("ov-goal-actions");
|
|
16116
|
+
actions.querySelectorAll("button").forEach((button) => { button.disabled = true; });
|
|
16117
|
+
try {
|
|
16118
|
+
const data = await api("/api/goals/" + encodeURIComponent(current.goal_id), {
|
|
16119
|
+
method: "POST", body: JSON.stringify({ revision: current.revision, phase }) });
|
|
16120
|
+
_goalCache = { current: ["active", "paused"].includes(data.goal.phase) ? data.goal : null, goals: (_goalCache.goals || []).map((g) =>
|
|
16121
|
+
g.goal_id === data.goal.goal_id ? data.goal : g) };
|
|
16122
|
+
goalRender();
|
|
16123
|
+
} catch (e) {
|
|
16124
|
+
$("ov-goal-error").textContent = e.message || t("更新目标失败");
|
|
16125
|
+
goalLoad();
|
|
16126
|
+
}
|
|
16127
|
+
}
|
|
16128
|
+
|
|
16129
|
+
async function goalStartRound() {
|
|
16130
|
+
const current = _goalCache && _goalCache.current;
|
|
16131
|
+
if (!current) return;
|
|
16132
|
+
try {
|
|
16133
|
+
const data = await api("/api/goals/" + encodeURIComponent(current.goal_id) + "/round", {
|
|
16134
|
+
method: "POST", body: JSON.stringify({ revision: current.revision }) });
|
|
16135
|
+
_goalCache = { current: data.goal, goals: (_goalCache.goals || []).map((g) =>
|
|
16136
|
+
g.goal_id === data.goal.goal_id ? data.goal : g) };
|
|
16137
|
+
goalRender();
|
|
16138
|
+
} catch (e) {
|
|
16139
|
+
$("ov-goal-error").textContent = e.message || t("开始新一轮失败");
|
|
16140
|
+
goalLoad();
|
|
16141
|
+
}
|
|
16142
|
+
}
|
|
16143
|
+
|
|
15473
16144
|
function ovRenderHead() {
|
|
15474
16145
|
const dateEl = $("ov-date");
|
|
15475
16146
|
if (dateEl) dateEl.textContent = ovDateText();
|
|
@@ -16012,7 +16683,7 @@ window.saveCleanupSettings = saveCleanupSettings;
|
|
|
16012
16683
|
|
|
16013
16684
|
/* 手机端(<900px)抽屉收起:点导航/任何侧栏可点项后都应收回,别挡内容 */
|
|
16014
16685
|
function collapseDrawerIfMobile() {
|
|
16015
|
-
if (window.innerWidth
|
|
16686
|
+
if (window.innerWidth <= 900) document.body.classList.add("side-collapsed");
|
|
16016
16687
|
}
|
|
16017
16688
|
|
|
16018
16689
|
/* 切页:主栏换成目标子页。shell 决定左栏形态——"main" 保持任务树、"settings" 换成
|
|
@@ -16046,7 +16717,7 @@ function switchTab(name, shell) {
|
|
|
16046
16717
|
document.querySelectorAll("#page-settings .subpage").forEach((d) => d.classList.toggle("hidden", d.id !== "sub-" + name));
|
|
16047
16718
|
renderPageCrumb(); // 页名 + 面包屑组名一起刷新(组名从左栏导航现读)
|
|
16048
16719
|
if (name === "runs" && !S.detailRunId) closeRun();
|
|
16049
|
-
if (name === "agents") autoCheckUpdates();
|
|
16720
|
+
if (name === "agents") { autoCheckUpdates(); loadReviewQueue(); }
|
|
16050
16721
|
if (name === "orch") { loadOrchestrator(); loadSettings(); } // 进编排设置页拉取配置
|
|
16051
16722
|
if (name === "evalbench") loadEvalBench(); // 进模型评测页:榜单 + 候选清单
|
|
16052
16723
|
if (name === "skills") loadSkills(); // 进经验库页拉取沉淀
|
|
@@ -16060,6 +16731,7 @@ function switchTab(name, shell) {
|
|
|
16060
16731
|
if (name === "data") loadDataPage(); // 进数据与备份页:清理配置/状态/可清理预估
|
|
16061
16732
|
if (name === "usage") { syncUsageRange(); loadUsage(); } // 进用量页:对齐范围选中态并拉取
|
|
16062
16733
|
if (name === "overview") { ovDays(); loadOverview(); } // 进概览页:对齐趋势范围并拉两期台账
|
|
16734
|
+
if (name === "overview") goalLoad();
|
|
16063
16735
|
if (name === "browser") browserDraw();
|
|
16064
16736
|
if (name === "appearance") renderAppearance(); // 进皮肤页:按当前皮肤/明暗重画卡片
|
|
16065
16737
|
if (name === "appearance") renderCodeSettings(); // 代码设置行 + 双主题预览卡同步当前值
|
|
@@ -16072,6 +16744,7 @@ function switchTab(name, shell) {
|
|
|
16072
16744
|
function exitSettings() {
|
|
16073
16745
|
if (S.detailRunId || S.detailTaskKey) closeRun(); // 设置运行列表开着详情就回表单:一并关掉,防 chat-fill 锁滚动与标题残留
|
|
16074
16746
|
S.tab = "tasks";
|
|
16747
|
+
$("sub-tasks").classList.remove("mobile-composing");
|
|
16075
16748
|
S.mainPage = ""; // 主栏回到表单:不再是快捷导航页
|
|
16076
16749
|
stopAutoPoll(); // 离开设置视图:自动化页轮询一并停掉
|
|
16077
16750
|
document.querySelectorAll(".page").forEach((p) => p.classList.toggle("hidden", p.id !== "page-settings"));
|
|
@@ -16211,6 +16884,51 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
16211
16884
|
history.replaceState(null, "", location.pathname);
|
|
16212
16885
|
}
|
|
16213
16886
|
document.querySelectorAll(".set-item").forEach((b) => b.addEventListener("click", () => switchTab(b.dataset.sub)));
|
|
16887
|
+
const settingsSearch = $("settings-search");
|
|
16888
|
+
if (settingsSearch) {
|
|
16889
|
+
const filterSettings = () => {
|
|
16890
|
+
const query = settingsSearch.value.trim().toLocaleLowerCase();
|
|
16891
|
+
let matches = 0;
|
|
16892
|
+
document.querySelectorAll(".side-settings .set-item").forEach((item) => {
|
|
16893
|
+
const haystack = [item.innerText, item.dataset.search || ""].join(" ").toLocaleLowerCase();
|
|
16894
|
+
const visible = !query || haystack.includes(query);
|
|
16895
|
+
item.classList.toggle("settings-search-hidden", !visible);
|
|
16896
|
+
if (visible) matches++;
|
|
16897
|
+
});
|
|
16898
|
+
document.querySelectorAll(".side-settings .set-group").forEach((group) => {
|
|
16899
|
+
let next = group.nextElementSibling, hasVisibleItem = false;
|
|
16900
|
+
while (next && !next.classList.contains("set-group")) {
|
|
16901
|
+
if (next.classList.contains("set-item") && !next.classList.contains("settings-search-hidden")) hasVisibleItem = true;
|
|
16902
|
+
next = next.nextElementSibling;
|
|
16903
|
+
}
|
|
16904
|
+
group.classList.toggle("settings-search-hidden", !!query && !hasVisibleItem);
|
|
16905
|
+
});
|
|
16906
|
+
const empty = $("settings-search-empty");
|
|
16907
|
+
if (empty) empty.classList.toggle("hidden", !query || matches > 0);
|
|
16908
|
+
};
|
|
16909
|
+
settingsSearch.addEventListener("input", filterSettings);
|
|
16910
|
+
settingsSearch.addEventListener("keydown", (event) => {
|
|
16911
|
+
if (event.key === "Escape" && settingsSearch.value) {
|
|
16912
|
+
event.preventDefault();
|
|
16913
|
+
settingsSearch.value = "";
|
|
16914
|
+
filterSettings();
|
|
16915
|
+
}
|
|
16916
|
+
if (event.key === "Enter") {
|
|
16917
|
+
const first = document.querySelector(".side-settings .set-item:not(.settings-search-hidden)");
|
|
16918
|
+
if (first && settingsSearch.value.trim()) {
|
|
16919
|
+
event.preventDefault();
|
|
16920
|
+
first.click();
|
|
16921
|
+
}
|
|
16922
|
+
}
|
|
16923
|
+
});
|
|
16924
|
+
document.addEventListener("keydown", (event) => {
|
|
16925
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "f" && document.body.classList.contains("settings-mode")) {
|
|
16926
|
+
event.preventDefault();
|
|
16927
|
+
settingsSearch.focus();
|
|
16928
|
+
settingsSearch.select();
|
|
16929
|
+
}
|
|
16930
|
+
});
|
|
16931
|
+
}
|
|
16214
16932
|
$("btn-set-back").addEventListener("click", exitSettings);
|
|
16215
16933
|
$("btn-files-back").addEventListener("click", closeFolderFiles); // 文件浏览页返回任务树
|
|
16216
16934
|
// 文件浏览页点文件行:中央弹窗预览(行是委托绑定,懒加载子目录里的行同样生效;
|
|
@@ -16425,7 +17143,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
16425
17143
|
"_blank", "noopener");
|
|
16426
17144
|
});
|
|
16427
17145
|
$("btn-theme").addEventListener("click", toggleTheme);
|
|
16428
|
-
$("btn-notify-toggle").addEventListener("click", toggleNotifySound);
|
|
16429
17146
|
// 命令面板:侧栏搜索行 / Ctrl+K 唤起(函数若尚未落地,跳过而不炸整个初始化)
|
|
16430
17147
|
if (typeof bindCmdK === "function") bindCmdK();
|
|
16431
17148
|
// 侧栏常驻用量条:首屏先取一次台账(之后由 applyState 的节流刷新接手)
|
|
@@ -16485,7 +17202,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
16485
17202
|
const root = document.documentElement;
|
|
16486
17203
|
const apply = (w) => root.style.setProperty("--side-w", w + "px");
|
|
16487
17204
|
const saved = parseInt(localStorage.getItem(KEY), 10);
|
|
16488
|
-
if (saved >= MIN && saved <= MAX && window.innerWidth
|
|
17205
|
+
if (saved >= MIN && saved <= MAX && window.innerWidth > 900) apply(saved);
|
|
16489
17206
|
const el = $("side-resizer");
|
|
16490
17207
|
let active = false, startX = 0, startW = 0;
|
|
16491
17208
|
const onMove = (e) => {
|
|
@@ -16499,7 +17216,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
16499
17216
|
localStorage.setItem(KEY, String(Math.round($("sidebar").getBoundingClientRect().width)));
|
|
16500
17217
|
};
|
|
16501
17218
|
el.addEventListener("pointerdown", (e) => {
|
|
16502
|
-
if (window.innerWidth
|
|
17219
|
+
if (window.innerWidth <= 900) return;
|
|
16503
17220
|
active = true; startX = e.clientX;
|
|
16504
17221
|
startW = $("sidebar").getBoundingClientRect().width;
|
|
16505
17222
|
document.body.classList.add("side-resizing");
|
|
@@ -16520,17 +17237,47 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
16520
17237
|
$("sidebar").addEventListener("click", (e) => {
|
|
16521
17238
|
if (e.target.closest("button, summary, .stask")) collapseDrawerIfMobile();
|
|
16522
17239
|
});
|
|
16523
|
-
$("btn-new-task").addEventListener("click",
|
|
17240
|
+
$("btn-new-task").addEventListener("click", () => mobileNewTask());
|
|
17241
|
+
$("btn-mobile-new").addEventListener("click", () => mobileNewTask());
|
|
17242
|
+
$("btn-mobile-workspaces").addEventListener("click", () => {
|
|
17243
|
+
$("sub-tasks").classList.remove("mobile-composing");
|
|
17244
|
+
document.querySelector("main").scrollTop = 0;
|
|
17245
|
+
});
|
|
17246
|
+
$("mobile-workspace-list").addEventListener("click", (e) => {
|
|
17247
|
+
const add = e.target.closest("[data-new-dir]");
|
|
17248
|
+
if (add) { mobileNewTask(add.dataset.newDir); return; }
|
|
17249
|
+
const task = e.target.closest(".mobile-task");
|
|
17250
|
+
if (task) sideRowActivate(task);
|
|
17251
|
+
});
|
|
17252
|
+
// 旋转/缩窄桌面窗口时也收起导航,断点与 CSS 保持一致。
|
|
17253
|
+
const mobileLayout = window.matchMedia("(max-width: 900px)");
|
|
17254
|
+
mobileLayout.addEventListener("change", (e) => {
|
|
17255
|
+
if (e.matches) document.body.classList.add("side-collapsed");
|
|
17256
|
+
});
|
|
16524
17257
|
// 侧栏快捷导航:主栏换成目标页,左栏任务树不动(shell="main")。
|
|
16525
17258
|
// 页面清单以 index.html .side-quick 的 data-page 为准,这里逐项绑 id。
|
|
16526
17259
|
$("btn-q-overview").addEventListener("click", () => switchTab("overview", "main"));
|
|
17260
|
+
$("ov-goal-create").addEventListener("click", goalCreate);
|
|
17261
|
+
$("ov-goal-title").addEventListener("keydown", (e) => {
|
|
17262
|
+
if (e.key === "Enter" && !e.isComposing) { e.preventDefault(); goalCreate(); }
|
|
17263
|
+
});
|
|
17264
|
+
$("ov-goal-actions").addEventListener("click", (e) => {
|
|
17265
|
+
if (e.target.closest("[data-goal-round]")) { goalStartRound(); return; }
|
|
17266
|
+
const button = e.target.closest("[data-goal-phase]");
|
|
17267
|
+
if (!button) return;
|
|
17268
|
+
const phase = button.dataset.goalPhase;
|
|
17269
|
+
if (phase === "complete") {
|
|
17270
|
+
uiConfirm(t("确定将这个长目标标记为完成吗?"), { danger: false,
|
|
17271
|
+
ok: t("标记完成") }).then((ok) => { if (ok) goalTransition(phase); });
|
|
17272
|
+
} else goalTransition(phase);
|
|
17273
|
+
});
|
|
16527
17274
|
$("btn-q-runs").addEventListener("click", () => switchTab("runs", "main"));
|
|
16528
17275
|
$("btn-q-automation").addEventListener("click", () => switchTab("automation", "main"));
|
|
16529
17276
|
$("btn-q-browser").addEventListener("click", () => switchTab("browser", "main"));
|
|
16530
17277
|
browserBind();
|
|
16531
17278
|
bindCtxMenus();
|
|
16532
17279
|
bindInspector();
|
|
16533
|
-
if (window.innerWidth
|
|
17280
|
+
if (window.innerWidth <= 900) document.body.classList.add("side-collapsed");
|
|
16534
17281
|
applyAppearance();
|
|
16535
17282
|
$("btn-create").addEventListener("click", createTask);
|
|
16536
17283
|
/* 侧栏「显示已归档」开关(原「最近任务」面板移除后,归档找回的唯一入口)。
|