codebee 0.1.66 → 0.1.67
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 +13 -1
- package/app/core/acceptance.py +100 -0
- package/app/core/assets.py +110 -0
- package/app/core/attachments.py +92 -1
- package/app/core/bookmeta.py +160 -26
- package/app/core/defectretro.py +43 -0
- package/app/core/flows.py +47 -1
- package/app/core/jobs.py +20 -4
- package/app/core/knowledge.py +50 -3
- package/app/core/manager.py +7 -0
- package/app/core/market_remote.py +124 -26
- package/app/core/modelhub.py +29 -0
- package/app/core/operations.py +228 -0
- package/app/core/paihang.py +58 -29
- package/app/core/pipeline.py +205 -11
- package/app/core/planner.py +4 -1
- package/app/core/publish/browser.py +6 -0
- package/app/core/publish/fanqie.py +31 -0
- package/app/core/publish/flow.py +9 -1
- package/app/core/publish/ledger.py +7 -2
- package/app/core/publish/manager.py +91 -22
- package/app/core/revisions.py +51 -0
- package/app/core/runner.py +27 -12
- package/app/core/skills.py +94 -1
- package/app/core/store.py +207 -11
- package/app/core/volumes.py +34 -0
- package/app/core/zentao.py +21 -3
- package/app/main.py +140 -21
- package/app/ui/app.js +407 -17
- package/app/ui/artdiff.js +74 -0
- package/app/ui/i18n.js +51 -1
- package/app/ui/index.html +6 -0
- package/app/ui/style.css +96 -24
- package/package.json +1 -1
package/app/ui/app.js
CHANGED
|
@@ -4315,6 +4315,7 @@ function drawTaskDetail(key, runs, appendFrom) {
|
|
|
4315
4315
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
4316
4316
|
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
4317
4317
|
(String(s.model || "").trim() ? '<span class="st-model" title="' + esc(t("实际派发模型")) + '">' + esc(String(s.model).trim()) + "</span>" : "") +
|
|
4318
|
+
(String(s.provider || "").trim() ? '<span class="st-prov" title="' + esc(t("实际用上游(厂商@网关)")) + '">' + esc(String(s.provider).trim()) + "</span>" : "") +
|
|
4318
4319
|
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
4319
4320
|
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
4320
4321
|
statusChip(s.status) +
|
|
@@ -4757,6 +4758,7 @@ function renderRunDetailSteps(run, token) {
|
|
|
4757
4758
|
'<span class="role">' + esc(s.role) + "</span>" +
|
|
4758
4759
|
'<span class="who">' + esc(t(s.agent_label || s.agent)) + "</span>" +
|
|
4759
4760
|
(s.model ? '<span class="st-model" title="' + esc(t("实际派发模型")) + '">' + esc(s.model) + "</span>" : "") +
|
|
4761
|
+
(String(s.provider || "").trim() ? '<span class="st-prov" title="' + esc(t("实际用上游(厂商@网关)")) + '">' + esc(String(s.provider).trim()) + "</span>" : "") +
|
|
4760
4762
|
'<span class="sum">' + esc((s.note ? "◆ " + s.note + " — " : "") + (s.summary || "")) + "</span>" +
|
|
4761
4763
|
'<span class="dur">' + (s.duration_s != null ? s.duration_s + "s" : "") + "</span>" +
|
|
4762
4764
|
statusChip(s.status) +
|
|
@@ -4818,6 +4820,7 @@ async function renderRunDetail() {
|
|
|
4818
4820
|
(run.error ? '<span class="stat err">' + errTag(run.error) + esc(run.error.slice(0, 200)) + "</span>" : "") +
|
|
4819
4821
|
'<span class="stat tasksum hidden" id="rd-meta-task"></span>';
|
|
4820
4822
|
if (S.detailSide) fillMetaTask(S.detailSide.stats || {}); // 缓存命中:轮询重画不闪丢累计组
|
|
4823
|
+
renderTimeoutRow(rcTask, run);
|
|
4821
4824
|
renderPlan(run);
|
|
4822
4825
|
renderRouting(run);
|
|
4823
4826
|
renderChapterScores(run);
|
|
@@ -6189,9 +6192,174 @@ const FP_IMG = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "svg"
|
|
|
6189
6192
|
const FP_TXT = new Set(["md", "txt", "log", "csv", "yml", "yaml", "ini", "toml", "json",
|
|
6190
6193
|
"py", "js", "ts", "jsx", "tsx", "html", "htm", "css", "svg", "bat", "sh", "ps1",
|
|
6191
6194
|
"c", "h", "cpp", "hpp", "java", "go", "rs", "xml", "sql", "mqtt", "proto"]);
|
|
6195
|
+
const FP_DOCX = new Set(["docx"]); // 只认 OOXML;legacy .doc 二进制不在此列
|
|
6196
|
+
const FP_PDF = new Set(["pdf"]);
|
|
6197
|
+
const FP_XLSX = new Set(["xlsx"]); // 只认 OOXML;legacy .xls 二进制不在此列
|
|
6192
6198
|
|
|
6193
6199
|
function _fpExt(name) { return (String(name).split(".").pop() || "").toLowerCase(); }
|
|
6194
6200
|
|
|
6201
|
+
/* docx 只读预览(试探功能):渲染器走 jsdelivr 懒加载(docx-preview 依赖
|
|
6202
|
+
* JSZip,两者只在首次打开 docx 时加载,平时零请求零开销);加载/渲染失败
|
|
6203
|
+
* 优雅回落到下载提示。渲染内容是用户自己工作目录的文件,信任面与网页成品
|
|
6204
|
+
* 预览页签一致。版本钉死,升级要走一次人工验证。 */
|
|
6205
|
+
let _docxLibs = null;
|
|
6206
|
+
function _loadDocxLibs() {
|
|
6207
|
+
if (_docxLibs) return _docxLibs;
|
|
6208
|
+
const load = (src) => new Promise((res, rej) => {
|
|
6209
|
+
const s = document.createElement("script");
|
|
6210
|
+
s.src = src; s.onload = () => res(); s.onerror = () => rej(new Error("load fail: " + src));
|
|
6211
|
+
document.head.appendChild(s);
|
|
6212
|
+
});
|
|
6213
|
+
_docxLibs = Promise.all([
|
|
6214
|
+
load("https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"),
|
|
6215
|
+
load("https://cdn.jsdelivr.net/npm/docx-preview@0.4.1/dist/docx-preview.min.js"),
|
|
6216
|
+
]).then(() => {
|
|
6217
|
+
if (!window.docx || !window.JSZip) throw new Error("docx 渲染器未就绪");
|
|
6218
|
+
});
|
|
6219
|
+
_docxLibs.catch(() => { _docxLibs = null; }); // 失败不缓存,下次打开可重试
|
|
6220
|
+
return _docxLibs;
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
async function _fpRenderDocx(url) {
|
|
6224
|
+
const el = _fpEnsure();
|
|
6225
|
+
try {
|
|
6226
|
+
const [r] = await Promise.all([fetch(url), _loadDocxLibs()]);
|
|
6227
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
6228
|
+
if (!filePopIsOpen()) return;
|
|
6229
|
+
_fpSetBody('<div class="fp-docx"><div class="fp-docx-load">' + esc(t("渲染中…")) + "</div></div>");
|
|
6230
|
+
const box = el.querySelector(".fp-docx");
|
|
6231
|
+
await window.docx.renderAsync(await r.arrayBuffer(), box, null, { inWrapper: true });
|
|
6232
|
+
if (!filePopIsOpen()) return;
|
|
6233
|
+
const loader = box.querySelector(".fp-docx-load");
|
|
6234
|
+
if (loader) loader.remove();
|
|
6235
|
+
} catch (e) {
|
|
6236
|
+
if (filePopIsOpen())
|
|
6237
|
+
_fpSetBody('<div class="fp-hint">' + esc(t("docx 预览不可用(渲染器需联网首次加载);可下载后查看。")) + "</div>");
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
/* PDF 只读预览(WorkDSH「真任务以可读产物收尾」切片):pdf.js 走 jsdelivr
|
|
6242
|
+
* 懒加载,钉 3.11.174(最后带 UMD 构建的版本,4.x 转 ESM 不适用 script 注入);
|
|
6243
|
+
* 渲染进 canvas,按弹窗宽度适配;大 PDF 封顶 20 页防渲染爆内存,超页给下载
|
|
6244
|
+
* 提示。加载/渲染失败优雅回落下载提示,与 docx 同款。 */
|
|
6245
|
+
const FP_PDF_MAX_PAGES = 20;
|
|
6246
|
+
let _pdfLib = null;
|
|
6247
|
+
function _loadPdfLib() {
|
|
6248
|
+
if (_pdfLib) return _pdfLib;
|
|
6249
|
+
const base = "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/legacy/build/";
|
|
6250
|
+
const load = (src) => new Promise((res, rej) => {
|
|
6251
|
+
const s = document.createElement("script");
|
|
6252
|
+
s.src = src; s.onload = () => res(); s.onerror = () => rej(new Error("load fail: " + src));
|
|
6253
|
+
document.head.appendChild(s);
|
|
6254
|
+
});
|
|
6255
|
+
_pdfLib = load(base + "pdf.min.js").then(() => {
|
|
6256
|
+
if (!window.pdfjsLib) throw new Error("pdf 渲染器未就绪");
|
|
6257
|
+
window.pdfjsLib.GlobalWorkerOptions.workerSrc = base + "pdf.worker.min.js";
|
|
6258
|
+
});
|
|
6259
|
+
_pdfLib.catch(() => { _pdfLib = null; }); // 失败不缓存,下次打开可重试
|
|
6260
|
+
return _pdfLib;
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
async function _fpRenderPdf(url) {
|
|
6264
|
+
const el = _fpEnsure();
|
|
6265
|
+
try {
|
|
6266
|
+
const [r] = await Promise.all([fetch(url), _loadPdfLib()]);
|
|
6267
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
6268
|
+
if (!filePopIsOpen()) return;
|
|
6269
|
+
_fpSetBody('<div class="fp-pdf"><div class="fp-docx-load">' + esc(t("渲染中…")) + "</div></div>");
|
|
6270
|
+
const box = el.querySelector(".fp-pdf");
|
|
6271
|
+
const doc = await window.pdfjsLib.getDocument({ data: await r.arrayBuffer() }).promise;
|
|
6272
|
+
if (!filePopIsOpen()) return;
|
|
6273
|
+
const loader = box.querySelector(".fp-docx-load");
|
|
6274
|
+
if (loader) loader.remove();
|
|
6275
|
+
const total = doc.numPages;
|
|
6276
|
+
const width = Math.max(320, (box.clientWidth || 800) - 8);
|
|
6277
|
+
for (let i = 1; i <= Math.min(total, FP_PDF_MAX_PAGES); i++) {
|
|
6278
|
+
const page = await doc.getPage(i);
|
|
6279
|
+
if (!filePopIsOpen()) return;
|
|
6280
|
+
const vp1 = page.getViewport({ scale: width / page.getViewport({ scale: 1 }).width });
|
|
6281
|
+
const canvas = document.createElement("canvas");
|
|
6282
|
+
canvas.className = "fp-pdf-page";
|
|
6283
|
+
canvas.width = Math.floor(vp1.width); canvas.height = Math.floor(vp1.height);
|
|
6284
|
+
await page.render({ canvasContext: canvas.getContext("2d"), viewport: vp1 }).promise;
|
|
6285
|
+
box.appendChild(canvas);
|
|
6286
|
+
}
|
|
6287
|
+
if (total > FP_PDF_MAX_PAGES)
|
|
6288
|
+
box.insertAdjacentHTML("beforeend", '<div class="fp-hint">' +
|
|
6289
|
+
esc(t("仅渲染前 {0} 页(共 {1} 页),完整内容请下载。")
|
|
6290
|
+
.replace("{0}", FP_PDF_MAX_PAGES).replace("{1}", total)) + "</div>");
|
|
6291
|
+
} catch (e) {
|
|
6292
|
+
if (filePopIsOpen())
|
|
6293
|
+
_fpSetBody('<div class="fp-hint">' + esc(t("PDF 预览不可用(渲染器需联网首次加载);可下载后查看。")) + "</div>");
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
|
|
6297
|
+
/* xlsx 只读预览:SheetJS 走 jsdelivr 懒加载(钉 0.18.5,npm 末版);多工作表
|
|
6298
|
+
* 给页签切换;单元格按文本渲染(raw:false,日期/数字走显示值),200 行 ×
|
|
6299
|
+
* 40 列封顶,超限给下载提示。只认 OOXML 的 .xlsx——legacy .xls 仍走下载。 */
|
|
6300
|
+
const FP_XLS_MAX_ROWS = 200, FP_XLS_MAX_COLS = 40;
|
|
6301
|
+
let _xlsxLib = null;
|
|
6302
|
+
function _loadXlsxLib() {
|
|
6303
|
+
if (_xlsxLib) return _xlsxLib;
|
|
6304
|
+
const load = (src) => new Promise((res, rej) => {
|
|
6305
|
+
const s = document.createElement("script");
|
|
6306
|
+
s.src = src; s.onload = () => res(); s.onerror = () => rej(new Error("load fail: " + src));
|
|
6307
|
+
document.head.appendChild(s);
|
|
6308
|
+
});
|
|
6309
|
+
_xlsxLib = load("https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js")
|
|
6310
|
+
.then(() => { if (!window.XLSX) throw new Error("xlsx 渲染器未就绪"); });
|
|
6311
|
+
_xlsxLib.catch(() => { _xlsxLib = null; });
|
|
6312
|
+
return _xlsxLib;
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
async function _fpRenderXlsx(url) {
|
|
6316
|
+
const el = _fpEnsure();
|
|
6317
|
+
try {
|
|
6318
|
+
const [r] = await Promise.all([fetch(url), _loadXlsxLib()]);
|
|
6319
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
6320
|
+
if (!filePopIsOpen()) return;
|
|
6321
|
+
_fpSetBody('<div class="fp-xlsx"><div class="fp-docx-load">' + esc(t("渲染中…")) + "</div></div>");
|
|
6322
|
+
const box = el.querySelector(".fp-xlsx");
|
|
6323
|
+
const wb = window.XLSX.read(await r.arrayBuffer(), { type: "array" });
|
|
6324
|
+
if (!filePopIsOpen()) return;
|
|
6325
|
+
box.innerHTML = "";
|
|
6326
|
+
const tabs = document.createElement("div");
|
|
6327
|
+
tabs.className = "fp-xls-tabs";
|
|
6328
|
+
const pane = document.createElement("div");
|
|
6329
|
+
box.appendChild(tabs); box.appendChild(pane);
|
|
6330
|
+
const drawSheet = (idx) => {
|
|
6331
|
+
const ws = wb.Sheets[wb.SheetNames[idx]];
|
|
6332
|
+
const rows = window.XLSX.utils.sheet_to_json(ws, { header: 1, defval: "", raw: false });
|
|
6333
|
+
let html = '<table class="fp-xls"><tbody>';
|
|
6334
|
+
rows.slice(0, FP_XLS_MAX_ROWS).forEach((row, ri) => {
|
|
6335
|
+
const tag = ri === 0 ? "th" : "td";
|
|
6336
|
+
html += "<tr>" + row.slice(0, FP_XLS_MAX_COLS).map((c) =>
|
|
6337
|
+
"<" + tag + ">" + esc(String(c == null ? "" : c)) + "</" + tag + ">").join("") + "</tr>";
|
|
6338
|
+
});
|
|
6339
|
+
html += "</tbody></table>";
|
|
6340
|
+
if (rows.length > FP_XLS_MAX_ROWS)
|
|
6341
|
+
html += '<div class="fp-hint">' +
|
|
6342
|
+
esc(t("已截断:仅显示前 {0} 行 × {1} 列;完整内容请下载。")
|
|
6343
|
+
.replace("{0}", FP_XLS_MAX_ROWS).replace("{1}", FP_XLS_MAX_COLS)) + "</div>";
|
|
6344
|
+
pane.innerHTML = html;
|
|
6345
|
+
tabs.querySelectorAll(".fp-xls-tab").forEach((b, i) =>
|
|
6346
|
+
b.classList.toggle("active", i === idx));
|
|
6347
|
+
};
|
|
6348
|
+
wb.SheetNames.forEach((sn, i) => {
|
|
6349
|
+
const b = document.createElement("button");
|
|
6350
|
+
b.className = "ghost fp-xls-tab" + (i === 0 ? " active" : "");
|
|
6351
|
+
b.type = "button"; b.textContent = sn;
|
|
6352
|
+
b.addEventListener("click", () => drawSheet(i));
|
|
6353
|
+
tabs.appendChild(b);
|
|
6354
|
+
});
|
|
6355
|
+
if (!wb.SheetNames.length) throw new Error("empty workbook");
|
|
6356
|
+
drawSheet(0);
|
|
6357
|
+
} catch (e) {
|
|
6358
|
+
if (filePopIsOpen())
|
|
6359
|
+
_fpSetBody('<div class="fp-hint">' + esc(t("表格预览不可用(渲染器需联网首次加载);可下载后查看。")) + "</div>");
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
|
|
6195
6363
|
/* 按 URL 弹窗预览:图片 blob 直显、md 渲染、文本/代码等宽原文、json 美化、
|
|
6196
6364
|
* 其余二进制只给下载。成品弹窗与目录文件弹窗共用;目录文件弹窗带 save
|
|
6197
6365
|
* 上下文 → 额外给「编辑」,改完保存回工作目录(POST /api/dir/save)。 */
|
|
@@ -6239,6 +6407,9 @@ async function _fpPreviewUrl(url, name, size, opts) {
|
|
|
6239
6407
|
el.querySelector(".fp-acts").insertAdjacentHTML("afterbegin", btns.join(""));
|
|
6240
6408
|
return;
|
|
6241
6409
|
}
|
|
6410
|
+
if (FP_DOCX.has(ext)) { await _fpRenderDocx(url); return; }
|
|
6411
|
+
if (FP_PDF.has(ext)) { await _fpRenderPdf(url); return; }
|
|
6412
|
+
if (FP_XLSX.has(ext)) { await _fpRenderXlsx(url); return; }
|
|
6242
6413
|
_fpSetBody('<div class="fp-hint">' + esc(t("二进制文件不预览,可下载查看。")) + "</div>");
|
|
6243
6414
|
} catch (e) {
|
|
6244
6415
|
_fpSetBody('<div class="fp-hint">' + esc(t("内容读取失败:") + (e.message || e)) + "</div>");
|
|
@@ -6411,22 +6582,82 @@ function fmtSize(n) {
|
|
|
6411
6582
|
/* 成品文件行(检查器「成品文件」与主栏「成果」分区共用一套标记):
|
|
6412
6583
|
* fc-row:文件行 + 弹窗预览按钮同行排布,避免按钮独占一行参差不齐。
|
|
6413
6584
|
* 点文件名与点「预览」同款 artPopup 弹窗,行为一致不 surprise。 */
|
|
6414
|
-
function artifactsChips(runId, files) {
|
|
6585
|
+
function artifactsChips(runId, files, prevRun) {
|
|
6415
6586
|
return files.map((f) => {
|
|
6416
6587
|
const isTxt = FP_TXT.has(_fpExt(f.name)); // 文本/代码都有弹窗预览(按代码格式展示)
|
|
6588
|
+
// 与上一版对比(OpenCreator「版本化对比」借鉴):同名文本文件在上一版
|
|
6589
|
+
// run 也存在时给 diff 入口;prevRun=null(无历史/清单拉取失败)静默不给
|
|
6590
|
+
const diffable = isTxt && prevRun && prevRun.names.has(f.name);
|
|
6417
6591
|
return '<span class="fc-row">' +
|
|
6418
6592
|
'<a class="file-chip artifact-file-open" href="' + urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" +
|
|
6419
6593
|
encodeURIComponent(f.name)) + '" target="_blank" rel="noopener" ' +
|
|
6420
6594
|
'title="' + esc(f.name + " · " + fmtSize(f.size)) + '" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
6421
6595
|
'<i class="fx">' + esc(_fpExt(f.name).slice(0, 4) || "file") + "</i>" +
|
|
6422
|
-
'<span class="p">' + esc(f.name) +
|
|
6596
|
+
'<span class="p">' + esc(f.name) + '</span><i>' + fmtSize(f.size) + "</i></a>" +
|
|
6423
6597
|
(isTxt ? '<a class="file-chip prev artifact-file-preview" title="' + esc(t("查看内容")) +
|
|
6424
6598
|
'" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
6425
6599
|
'<svg class="ico" aria-hidden="true"><use href="#i-book"/></svg>' + t("预览") + "</a>" : "") +
|
|
6600
|
+
(diffable ? '<a class="file-chip prev artifact-file-diff" title="' + esc(t("与上一版对比")) +
|
|
6601
|
+
'" data-file-run="' + esc(runId) + '" data-prev-run="' + esc(prevRun.id) +
|
|
6602
|
+
'" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
|
|
6603
|
+
'<svg class="ico" aria-hidden="true"><use href="#i-git-branch"/></svg>' + t("对比") + "</a>" : "") +
|
|
6426
6604
|
"</span>";
|
|
6427
6605
|
}).join("");
|
|
6428
6606
|
}
|
|
6429
6607
|
|
|
6608
|
+
/* 两版 diff 弹窗正文(纯函数在 artdiff.js,node 直测):统一 diff 行 + 长
|
|
6609
|
+
* 相同段折叠;中段超限诚实给整块计数(不装作逐行对齐)。 */
|
|
6610
|
+
function artifactDiffBody(a, b) {
|
|
6611
|
+
const d = ArtifactDiff.diff(a, b);
|
|
6612
|
+
if (d.identical) return '<div class="fp-hint">' + esc(t("两版内容一致。")) + "</div>";
|
|
6613
|
+
const gap = (n) => '<div class="dl-gap">… ' + esc(t("{0} 行相同(已折叠)").replace("{0}", n)) + " …</div>";
|
|
6614
|
+
const ln = (cls, s, mark) => '<div class="dl-ln dl-' + cls + '"><i>' + mark + "</i>" + esc(s || "") + "</div>";
|
|
6615
|
+
let html = "";
|
|
6616
|
+
if (d.prefix) html += gap(d.prefix);
|
|
6617
|
+
if (d.oversized) {
|
|
6618
|
+
html += '<div class="dl-gap">' +
|
|
6619
|
+
esc(t("变更过大(旧 {0} 行 → 新 {1} 行),不逐行对齐;可分别打开两版查看。")
|
|
6620
|
+
.replace("{0}", d.oversized.a).replace("{1}", d.oversized.b)) + "</div>";
|
|
6621
|
+
} else {
|
|
6622
|
+
const rows = d.rows || [];
|
|
6623
|
+
let run = 0, runAt = 0; // 连续相同行折叠:≥6 行合并成一个 gap
|
|
6624
|
+
rows.forEach((r, idx) => {
|
|
6625
|
+
if (r.t === "=") { if (!run) runAt = idx; run++; return; }
|
|
6626
|
+
if (run < 6) for (let k = runAt; k < runAt + run; k++) html += ln("eq", rows[k].s, " ");
|
|
6627
|
+
else html += gap(run);
|
|
6628
|
+
run = 0;
|
|
6629
|
+
html += r.t === "+" ? ln("add", r.s, "+") : ln("del", r.s, "−");
|
|
6630
|
+
});
|
|
6631
|
+
if (run < 6) for (let k = runAt; k < runAt + run; k++) html += ln("eq", rows[k].s, " ");
|
|
6632
|
+
else if (run) html += gap(run);
|
|
6633
|
+
}
|
|
6634
|
+
if (d.suffix) html += gap(d.suffix);
|
|
6635
|
+
return '<pre class="art-diff">' + html + "</pre>";
|
|
6636
|
+
}
|
|
6637
|
+
|
|
6638
|
+
/* 对比上一版弹窗:取两版文本 → diff 渲染(复用成品弹窗骨架)。 */
|
|
6639
|
+
window.artDiffPopup = async function (runId, prevRunId, name) {
|
|
6640
|
+
_fpSaveCtx = null; _fpRawText = ""; _fpDirty = false; _fpCopyText = null;
|
|
6641
|
+
const nm = String(name).split("/").pop();
|
|
6642
|
+
_fpOpen(nm + " · " + t("与上一版对比"),
|
|
6643
|
+
'<i>' + esc(String(prevRunId).slice(0, 8)) + " → " + esc(String(runId).slice(0, 8)) + "</i>", "");
|
|
6644
|
+
_fpSetBody('<div class="fp-hint">' + esc(t("正在取两版内容…")) + "</div>");
|
|
6645
|
+
const u1 = urlAuth("/api/runs/" + encodeURIComponent(prevRunId) + "/file?name=" + encodeURIComponent(name));
|
|
6646
|
+
const u2 = urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" + encodeURIComponent(name));
|
|
6647
|
+
try {
|
|
6648
|
+
const [r1, r2] = await Promise.all([fetch(u1), fetch(u2)]);
|
|
6649
|
+
if (!r1.ok || !r2.ok) throw new Error("HTTP " + (r1.ok ? r2.status : r1.status));
|
|
6650
|
+
const [a, b] = await Promise.all([r1.text(), r2.text()]);
|
|
6651
|
+
if (!filePopIsOpen()) return;
|
|
6652
|
+
if (a.length + b.length > 4 * 1024 * 1024)
|
|
6653
|
+
_fpSetBody('<div class="fp-hint">' + esc(t("文件过大,不在线对比;可分别打开两版查看。")) + "</div>");
|
|
6654
|
+
else _fpSetBody(artifactDiffBody(a, b));
|
|
6655
|
+
} catch (e) {
|
|
6656
|
+
if (filePopIsOpen())
|
|
6657
|
+
_fpSetBody('<div class="fp-hint">' + esc(t("内容读取失败:") + (e.message || e)) + "</div>");
|
|
6658
|
+
}
|
|
6659
|
+
};
|
|
6660
|
+
|
|
6430
6661
|
async function loadArtifacts(runId) {
|
|
6431
6662
|
// 双入口同源渲染:检查器「成品文件」TAB + 主栏详情「成果」分区。
|
|
6432
6663
|
let d;
|
|
@@ -6457,7 +6688,20 @@ async function loadArtifacts(runId) {
|
|
|
6457
6688
|
}
|
|
6458
6689
|
const head = '<div class="files-head"><span class="sec-title">' + t("成品文件") + '</span>' +
|
|
6459
6690
|
'<span class="wd" title="' + esc(t("点击复制")) + '" onclick="copyText(this.textContent)">' + esc(d.workdir) + "</span></div>";
|
|
6460
|
-
|
|
6691
|
+
// 上一版 run 的文件清单(详情上下文才有):同名文本文件给「对比」入口;
|
|
6692
|
+
// 拉不到/无历史静默降级——对比是增强不是闸门
|
|
6693
|
+
let prevRun = null;
|
|
6694
|
+
if (detailOwns && S.detailTaskKey) {
|
|
6695
|
+
const prev = (_taskRunsCache.get(S.detailTaskKey) || []).find((r) => r.id !== runId);
|
|
6696
|
+
if (prev) {
|
|
6697
|
+
try {
|
|
6698
|
+
const pd = await api("/api/runs/" + encodeURIComponent(prev.id) + "/files");
|
|
6699
|
+
if (pd && (pd.files || []).length)
|
|
6700
|
+
prevRun = { id: prev.id, names: new Set(pd.files.map((f2) => f2.name)) };
|
|
6701
|
+
} catch (e) { /* 上一版清单不可用就不给对比 */ }
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
const chips = artifactsChips(runId, files, prevRun);
|
|
6461
6705
|
if (box) box.innerHTML = head + '<div class="file-chips">' + chips + "</div>";
|
|
6462
6706
|
if (mainBox) { mainBox.classList.remove("hidden");
|
|
6463
6707
|
mainBox.innerHTML = head + '<div class="file-chips">' + chips + "</div>"; }
|
|
@@ -8038,6 +8282,56 @@ window.toggleRdModelMenu = toggleRdModelMenu;
|
|
|
8038
8282
|
});
|
|
8039
8283
|
})();
|
|
8040
8284
|
|
|
8285
|
+
// 任务总时限(秒)选项:任务信息弹层里的空闲可改项(2026-09-24 戍边骑奴案:
|
|
8286
|
+
// 12 章连载默认 1 小时在慢链天跑不完)。值不入轮询重建的 rd-meta,防 select 弹回。
|
|
8287
|
+
const TASK_TIMEOUT_CHOICES = [
|
|
8288
|
+
[1800, "30 分钟"], [3600, "1 小时"], [7200, "2 小时"], [10800, "3 小时"],
|
|
8289
|
+
[14400, "4 小时"], [21600, "6 小时"], [43200, "12 小时"], [86400, "24 小时"],
|
|
8290
|
+
];
|
|
8291
|
+
|
|
8292
|
+
function timeoutLabel(s) {
|
|
8293
|
+
const hit = TASK_TIMEOUT_CHOICES.find((c) => c[0] === Number(s));
|
|
8294
|
+
if (hit) return hit[1];
|
|
8295
|
+
const m = Math.round(Number(s) / 60);
|
|
8296
|
+
return (m >= 60 ? (Math.round(m / 60 * 10) / 10) + " 小时" : m + " 分钟");
|
|
8297
|
+
}
|
|
8298
|
+
|
|
8299
|
+
function renderTimeoutRow(rcTask, run) {
|
|
8300
|
+
const row = $("rd-timeout-row"), sel = $("rd-timeout");
|
|
8301
|
+
if (!row || !sel) return;
|
|
8302
|
+
if (!rcTask) { row.classList.add("hidden"); return; }
|
|
8303
|
+
row.classList.remove("hidden");
|
|
8304
|
+
const cur = Number(rcTask.timeout_s || 3600);
|
|
8305
|
+
// 选项一次性构建;当前值不在档内时补一项,避免选中丢失
|
|
8306
|
+
const want = TASK_TIMEOUT_CHOICES.map((c) => c[0]).concat(
|
|
8307
|
+
TASK_TIMEOUT_CHOICES.some((c) => c[0] === cur) ? [] : [cur]).join(",");
|
|
8308
|
+
if (sel.dataset.choices !== want) {
|
|
8309
|
+
sel.dataset.choices = want;
|
|
8310
|
+
sel.innerHTML = TASK_TIMEOUT_CHOICES
|
|
8311
|
+
.map((c) => '<option value="' + c[0] + '">' + esc(t(c[1])) + "</option>").join("") +
|
|
8312
|
+
(TASK_TIMEOUT_CHOICES.some((c) => c[0] === cur)
|
|
8313
|
+
? "" : '<option value="' + cur + '">' + esc(t("自定义 ") + timeoutLabel(cur)) + "</option>");
|
|
8314
|
+
}
|
|
8315
|
+
const busy = ["queued", "running"].indexOf(rcTask.status) >= 0;
|
|
8316
|
+
sel.disabled = busy;
|
|
8317
|
+
// 用户正操作(含失焦瞬间)不回写 value,防轮询把刚选的值弹回旧值
|
|
8318
|
+
if (document.activeElement !== sel) sel.value = String(cur);
|
|
8319
|
+
if (!sel.dataset.bound) {
|
|
8320
|
+
sel.dataset.bound = "1";
|
|
8321
|
+
sel.addEventListener("change", async () => {
|
|
8322
|
+
const task = (S.state && (S.state.tasks || []).find((x) => x.id === rcTask.id)) || rcTask;
|
|
8323
|
+
const patch = { timeout_s: Number(sel.value) || 3600 };
|
|
8324
|
+
if (task.rev != null) patch.expected_rev = task.rev;
|
|
8325
|
+
try {
|
|
8326
|
+
const data = await api("/api/tasks/" + encodeURIComponent(rcTask.id) + "/params", {
|
|
8327
|
+
method: "POST", operation: false, body: JSON.stringify(patch) });
|
|
8328
|
+
if (data && data.rev != null) task.rev = data.rev;
|
|
8329
|
+
task.timeout_s = patch.timeout_s; // 即时回显,不等轮询
|
|
8330
|
+
} catch (e) { toast(e && e.message ? e.message : "保存失败", true); }
|
|
8331
|
+
});
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
|
|
8041
8335
|
async function rdPrefsPush(what) {
|
|
8042
8336
|
// what: mode|thinking|model(变更源);写任务参数(空闲态才允许)
|
|
8043
8337
|
const task = (S.lastRun && S.lastRun.task_id && S.state &&
|
|
@@ -8051,16 +8345,29 @@ async function rdPrefsPush(what) {
|
|
|
8051
8345
|
patch.direct_model = rdModelMv;
|
|
8052
8346
|
patch.direct_agent = rdModelAgent;
|
|
8053
8347
|
}
|
|
8348
|
+
// 修订 CAS(借鉴 WorkDSH 契约纪律):带上页面所见的任务修订号,别的窗口
|
|
8349
|
+
// 先改过则后端拒绝过期写入;本地提示后由轮询回填最新值——「我改成功了」
|
|
8350
|
+
// 的假象(实际已被人覆盖)就此消失。
|
|
8351
|
+
if (task.rev != null) patch.expected_rev = task.rev;
|
|
8054
8352
|
try {
|
|
8055
|
-
await api("/api/tasks/" + encodeURIComponent(task.id) + "/params", {
|
|
8353
|
+
const data = await api("/api/tasks/" + encodeURIComponent(task.id) + "/params", {
|
|
8056
8354
|
method: "POST", operation: false, body: JSON.stringify(patch) });
|
|
8057
|
-
|
|
8355
|
+
if (data && data.rev != null) task.rev = data.rev; // 续用凭据:连推时后几次不再过期
|
|
8356
|
+
} catch (e) {
|
|
8357
|
+
if (String((e && e.message) || "").indexOf("已被其他窗口修改") >= 0) {
|
|
8358
|
+
toast(e.message, true); // 冲突要可见:静默吞掉=又一种假成功
|
|
8359
|
+
}
|
|
8360
|
+
/* 运行中/网络失败不打断对话,发送前还有兜底 */
|
|
8361
|
+
}
|
|
8058
8362
|
}
|
|
8059
8363
|
|
|
8060
8364
|
async function rdPrefsPushAll() {
|
|
8061
|
-
// 发送前兜底:三个一起推(/chat
|
|
8062
|
-
//
|
|
8063
|
-
|
|
8365
|
+
// 发送前兜底:三个一起推(/chat 开新一轮前调用)。改为顺序推——三次都带
|
|
8366
|
+
// expected_rev,并发推会在第一次成功后让后两次全成过期写入被拒;
|
|
8367
|
+
// 值已在前端,失败静默同前,下一轮 poll 的回填也不会覆盖用户刚选的值。
|
|
8368
|
+
await rdPrefsPush("mode");
|
|
8369
|
+
await rdPrefsPush("thinking");
|
|
8370
|
+
await rdPrefsPush("model");
|
|
8064
8371
|
}
|
|
8065
8372
|
|
|
8066
8373
|
function drawChatAtts() {
|
|
@@ -10753,7 +11060,7 @@ function mkSetView(v) {
|
|
|
10753
11060
|
function mkrFilterKey() {
|
|
10754
11061
|
const q = (($("mkr-search") || {}).value || "").trim().toLowerCase();
|
|
10755
11062
|
const srcSel = $("mkr-source");
|
|
10756
|
-
return q + "|" + (srcSel ? srcSel.value : "");
|
|
11063
|
+
return q + "|" + (srcSel ? srcSel.value : "") + "|" + (S.mkrInst ? "inst" : "");
|
|
10757
11064
|
}
|
|
10758
11065
|
|
|
10759
11066
|
/* 搜索防抖:过滤在服务端对全量做,停 350ms 再发请求;只跟当前过滤键变化时重拉首页 */
|
|
@@ -10778,6 +11085,7 @@ async function mkrLoadPage(reset) {
|
|
|
10778
11085
|
let url = "/api/market/remote?offset=" + (S.mkrOffset || 0) + "&limit=" + MKR_PAGE;
|
|
10779
11086
|
if (srcSel && srcSel.value) url += "&source=" + encodeURIComponent(srcSel.value);
|
|
10780
11087
|
if (q) url += "&q=" + encodeURIComponent(q);
|
|
11088
|
+
if (S.mkrInst) url += "&installed=1";
|
|
10781
11089
|
S.mkrLoading = true;
|
|
10782
11090
|
let data = null;
|
|
10783
11091
|
try { data = await api(url); } catch (e) { data = null; }
|
|
@@ -10829,11 +11137,20 @@ function renderMarketRemote() {
|
|
|
10829
11137
|
const times = (data.sources || []).map((s) => s.fetched_at).filter(Boolean);
|
|
10830
11138
|
const meta = $("mkr-meta");
|
|
10831
11139
|
if (meta) meta.textContent = times.length ? t("目录更新于 ") + times.join(" / ") : "";
|
|
11140
|
+
// 「已安装 N」入口:计数是全量已装(未过滤),点击切换只看已安装
|
|
11141
|
+
const instBtn = $("mkr-installed");
|
|
11142
|
+
if (instBtn) {
|
|
11143
|
+
instBtn.textContent = t("已安装 ") + (Number(data.installed_total) || 0);
|
|
11144
|
+
instBtn.classList.toggle("active", !!S.mkrInst);
|
|
11145
|
+
instBtn.setAttribute("aria-pressed", S.mkrInst ? "true" : "false");
|
|
11146
|
+
}
|
|
10832
11147
|
const items = S.mkrAll || [];
|
|
10833
11148
|
mkCount(t("已加载 ") + items.length + t(" / 共 ") + data.total + t(" 个"));
|
|
10834
11149
|
if (!items.length) {
|
|
10835
11150
|
grid.innerHTML = '<div class="empty">' + (data.total === 0 && !S.mkrOffset && !(($("mkr-search")||{}).value||"").trim() && !(srcSel||{}).value
|
|
10836
|
-
?
|
|
11151
|
+
? (S.mkrInst
|
|
11152
|
+
? t("还没有从外部目录安装过插件;退出过滤后逛逛目录,安装前会先给你看包内容。")
|
|
11153
|
+
: t("外部目录还是空的,点「拉取更新」从公开生态获取。"))
|
|
10837
11154
|
: t("没有符合条件插件")) + "</div>";
|
|
10838
11155
|
const more = $("mkr-more");
|
|
10839
11156
|
if (more) more.classList.add("hidden");
|
|
@@ -10889,15 +11206,73 @@ async function mkrRefresh() {
|
|
|
10889
11206
|
btn.textContent = old;
|
|
10890
11207
|
}
|
|
10891
11208
|
|
|
11209
|
+
/* 两阶段安装(先看后装):① 预览——服务端下载+剥离式检查但不落盘,弹框展示
|
|
11210
|
+
* 包内容(文件清单/体量/剥离项/完整性);② 确认——带 token 安装那份已检内容,
|
|
11211
|
+
* 不重复下载。token 一次性,安装失败弹框保留可重试。 */
|
|
10892
11212
|
async function mkrInstall(id) {
|
|
10893
|
-
// 安装要现场下载插件包(zip/tarball,实测 10~60s),必须立刻给反馈,
|
|
10894
|
-
// 否则用户以为点了没反应;按钮按 data-mk 定位,失败时由重拉页面复位
|
|
10895
11213
|
const btn = document.querySelector('#mkr-grid button[data-mk="' + id + '"]');
|
|
10896
|
-
if (btn) { btn.disabled = true; btn.textContent = t("
|
|
10897
|
-
toast(t("
|
|
11214
|
+
if (btn) { btn.disabled = true; btn.textContent = t("检查中…"); }
|
|
11215
|
+
toast(t("正在下载插件包做安装前检查,包较大时需要约一分钟…"));
|
|
11216
|
+
let p;
|
|
10898
11217
|
try {
|
|
10899
|
-
|
|
11218
|
+
p = await api("/api/market/remote/preview", { method: "POST",
|
|
10900
11219
|
body: JSON.stringify({ id }), timeout: 180000 });
|
|
11220
|
+
} catch (e) {
|
|
11221
|
+
toast(e.message, true);
|
|
11222
|
+
mkrLoadPage(true); // 失败重拉复位按钮
|
|
11223
|
+
return;
|
|
11224
|
+
}
|
|
11225
|
+
mkrShowInstallPreview(p);
|
|
11226
|
+
}
|
|
11227
|
+
|
|
11228
|
+
function _mkrIntegrityText(k) {
|
|
11229
|
+
const map = {
|
|
11230
|
+
verified: t("sha256 已校验"),
|
|
11231
|
+
unverified: t("无哈希(HTTPS 传输 + 纯技能白名单兜底)"),
|
|
11232
|
+
"git-ref": t("按 git ref 锁定"),
|
|
11233
|
+
"git-head": t("跟随仓库 HEAD"),
|
|
11234
|
+
};
|
|
11235
|
+
return map[k] || k || "";
|
|
11236
|
+
}
|
|
11237
|
+
|
|
11238
|
+
function mkrShowInstallPreview(p) {
|
|
11239
|
+
p = p || {};
|
|
11240
|
+
const rows = (p.file_list || []).map((f) =>
|
|
11241
|
+
'<tr><td title="' + esc(f.path) + '">' + esc(f.path) + '</td><td class="mkpv-chars">' + esc(mkCharsText(f.chars)) + "</td></tr>").join("");
|
|
11242
|
+
const moreF = p.files_total > (p.file_list || []).length
|
|
11243
|
+
? '<tr><td colspan="2" class="mkpv-more">' + t("…等共 ") + p.files_total + t(" 个文件") + "</td></tr>" : "";
|
|
11244
|
+
const stripped = p.stripped || [];
|
|
11245
|
+
const strippedHtml = stripped.length
|
|
11246
|
+
? '<div class="mkpv-sec">' + t("将自动剥离(不写入、不执行):") + "</div>" +
|
|
11247
|
+
'<ul class="mkpv-strip">' + stripped.map((s) => "<li>" + esc(s) + "</li>").join("") +
|
|
11248
|
+
(p.stripped_total > stripped.length
|
|
11249
|
+
? '<li class="mkpv-more">' + t("…等 ") + p.stripped_total + t(" 个") + "</li>" : "") + "</ul>"
|
|
11250
|
+
: "";
|
|
11251
|
+
const skills = (p.skills || []).map((s) => '<span class="tag">' + esc(s) + "</span>").join("");
|
|
11252
|
+
openModal(t("安装前确认"),
|
|
11253
|
+
'<div class="mkpv">' +
|
|
11254
|
+
'<div class="mkpv-name">' + esc(p.title || p.id || "") + "</div>" +
|
|
11255
|
+
'<div class="mkpv-meta"><span>' + esc(t("来源:") + (p.source || "")) + "</span>" +
|
|
11256
|
+
(p.version ? "<span>v" + esc(p.version) + "</span>" : "") +
|
|
11257
|
+
"<span>" + esc(_mkrIntegrityText(p.integrity)) + "</span></div>" +
|
|
11258
|
+
(skills ? '<div class="mkpv-skills">' + t("包含技能:") + skills + "</div>" : "") +
|
|
11259
|
+
'<div class="mkpv-sum">' + t("共 ") + (p.files_total || 0) + t(" 个文件 · ") + esc(mkCharsText(p.total_chars || 0)) + "</div>" +
|
|
11260
|
+
(rows ? '<table class="mkpv-files"><thead><tr><th>' + t("文件") + "</th><th>" + t("体量") +
|
|
11261
|
+
"</th></tr></thead><tbody>" + rows + moreF + "</tbody></table>" : "") +
|
|
11262
|
+
strippedHtml +
|
|
11263
|
+
'<p class="hint">' + t("安装后运行任务时自动注入提示词;可到「经验库」启停或卸载。") + "</p>" +
|
|
11264
|
+
"</div>",
|
|
11265
|
+
'<button class="ghost" onclick="closeModal()">' + t("取消") + "</button>" +
|
|
11266
|
+
'<button class="primary" id="mkpv-ok" onclick="mkrInstallConfirm(\'' + esc(p.token || "") + '\',\'' + esc(p.id || "") + '\')">' + t("确认安装") + "</button>");
|
|
11267
|
+
}
|
|
11268
|
+
|
|
11269
|
+
async function mkrInstallConfirm(token, id) {
|
|
11270
|
+
const ok = $("mkpv-ok");
|
|
11271
|
+
if (ok) { ok.disabled = true; ok.textContent = t("安装中…"); }
|
|
11272
|
+
try {
|
|
11273
|
+
const r = await api("/api/market/remote/install", { method: "POST",
|
|
11274
|
+
body: JSON.stringify({ id, token }), timeout: 180000 });
|
|
11275
|
+
closeModal();
|
|
10901
11276
|
if (r && r.already) toast(t("该插件已安装过"));
|
|
10902
11277
|
else {
|
|
10903
11278
|
// 剥离式安装:脚本/钩子/MCP 文件被剔除但技能装上了,如实告知剔除了什么
|
|
@@ -10907,8 +11282,13 @@ async function mkrInstall(id) {
|
|
|
10907
11282
|
+ (stripped.length > 3 ? t(" 等 ") + stripped.length + t(" 个文件") : "")
|
|
10908
11283
|
: ""));
|
|
10909
11284
|
}
|
|
10910
|
-
} catch (e) {
|
|
10911
|
-
|
|
11285
|
+
} catch (e) {
|
|
11286
|
+
toast(e.message, true);
|
|
11287
|
+
const ok2 = $("mkpv-ok");
|
|
11288
|
+
if (ok2) { ok2.disabled = false; ok2.textContent = t("确认安装"); }
|
|
11289
|
+
return; // 弹框保留:可重试或取消(token 未消费)
|
|
11290
|
+
}
|
|
11291
|
+
mkrLoadPage(true); // 已安装徽章要翻牌
|
|
10912
11292
|
}
|
|
10913
11293
|
|
|
10914
11294
|
|
|
@@ -13119,6 +13499,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
13119
13499
|
const bindArtifactClicks = (root) => {
|
|
13120
13500
|
if (!root) return;
|
|
13121
13501
|
root.addEventListener("click", (e) => {
|
|
13502
|
+
const diff = e.target.closest(".artifact-file-diff[data-prev-run]");
|
|
13503
|
+
if (diff) { // 对比上一版:先于通用预览分支(chip 同样带 data-file-run/name)
|
|
13504
|
+
e.preventDefault();
|
|
13505
|
+
artDiffPopup(diff.dataset.fileRun, diff.dataset.prevRun, diff.dataset.fileName || "");
|
|
13506
|
+
return;
|
|
13507
|
+
}
|
|
13122
13508
|
const file = e.target.closest("[data-file-run][data-file-name]");
|
|
13123
13509
|
if (!file) return;
|
|
13124
13510
|
e.preventDefault();
|
|
@@ -13541,6 +13927,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
13541
13927
|
mkrLoadPage(true);
|
|
13542
13928
|
});
|
|
13543
13929
|
$("mkr-source").addEventListener("change", () => mkrLoadPage(true));
|
|
13930
|
+
$("mkr-installed").addEventListener("click", () => {
|
|
13931
|
+
S.mkrInst = !S.mkrInst;
|
|
13932
|
+
mkrLoadPage(true);
|
|
13933
|
+
});
|
|
13544
13934
|
$("mkr-refresh").addEventListener("click", mkrRefresh);
|
|
13545
13935
|
$("mkr-more-btn").addEventListener("click", () => mkrLoadPage(false));
|
|
13546
13936
|
mkrBindSentinel();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* 成品两版对比的纯 diff 计算(OpenCreator「版本化对比」借鉴)。
|
|
2
|
+
* 独立文件无 DOM 依赖:app.js 用 window.ArtifactDiff 渲染,tests 用
|
|
3
|
+
* module.exports 直测。算法:公共前后缀修剪 + 中段 LCS(上限保护,
|
|
4
|
+
* 超限诚实降级为整块替换计数,不装作对齐了)。 */
|
|
5
|
+
(function (root, factory) {
|
|
6
|
+
var api = factory();
|
|
7
|
+
if (typeof window !== "undefined") window.ArtifactDiff = api;
|
|
8
|
+
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
|
9
|
+
})(typeof self !== "undefined" ? self : this, function () {
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
var MID_CAP = 400; // 中段行数单边上限
|
|
13
|
+
var MID_PRODUCT_CAP = 160000; // LCS DP 格子上限(400×400)
|
|
14
|
+
|
|
15
|
+
/* LCS 回溯产出统一 diff 行:[{t:"="|"-"/"+", s:行文本}](只对中段)。 */
|
|
16
|
+
function lcsRows(A, B) {
|
|
17
|
+
var n = A.length, m = B.length;
|
|
18
|
+
var dp = [];
|
|
19
|
+
for (var k = 0; k <= n; k++) dp.push(new Uint32Array(m + 1));
|
|
20
|
+
for (var i = n - 1; i >= 0; i--)
|
|
21
|
+
for (var j = m - 1; j >= 0; j--)
|
|
22
|
+
dp[i][j] = A[i] === B[j] ? dp[i + 1][j + 1] + 1
|
|
23
|
+
: (dp[i + 1][j] >= dp[i][j + 1] ? dp[i + 1][j] : dp[i][j + 1]);
|
|
24
|
+
var rows = [];
|
|
25
|
+
i = 0; j = 0;
|
|
26
|
+
while (i < n && j < m) {
|
|
27
|
+
if (A[i] === B[j]) { rows.push({ t: "=", s: A[i] }); i++; j++; }
|
|
28
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) { rows.push({ t: "-", s: A[i] }); i++; }
|
|
29
|
+
else { rows.push({ t: "+", s: B[j] }); j++; }
|
|
30
|
+
}
|
|
31
|
+
while (i < n) { rows.push({ t: "-", s: A[i] }); i++; }
|
|
32
|
+
while (j < m) { rows.push({ t: "+", s: B[j] }); j++; }
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* 对外主口:旧版 a、新版 b → {
|
|
37
|
+
* prefix/suffix: 相同行数(渲染端折叠展示),
|
|
38
|
+
* identical: 布尔,两版一致,
|
|
39
|
+
* rows: 中段统一 diff 行(超限时为 null),
|
|
40
|
+
* oversized: {a, b} 超限时中段行数(诚实整块计数) } */
|
|
41
|
+
function diff(a, b) {
|
|
42
|
+
a = String(a == null ? "" : a);
|
|
43
|
+
b = String(b == null ? "" : b);
|
|
44
|
+
var A = a.split("\n"), B = b.split("\n");
|
|
45
|
+
var p = 0;
|
|
46
|
+
while (p < A.length && p < B.length && A[p] === B[p]) p++;
|
|
47
|
+
var s = 0;
|
|
48
|
+
while (s < A.length - p && s < B.length - p &&
|
|
49
|
+
A[A.length - 1 - s] === B[B.length - 1 - s]) s++;
|
|
50
|
+
var midA = A.slice(p, A.length - s), midB = B.slice(p, B.length - s);
|
|
51
|
+
var out = { prefix: p, suffix: s, identical: !midA.length && !midB.length,
|
|
52
|
+
rows: null, oversized: null };
|
|
53
|
+
if (out.identical) return out;
|
|
54
|
+
if (midA.length > MID_CAP || midB.length > MID_CAP ||
|
|
55
|
+
midA.length * midB.length > MID_PRODUCT_CAP) {
|
|
56
|
+
out.oversized = { a: midA.length, b: midB.length };
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
out.rows = lcsRows(midA, midB);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* rows → 摘要计数(+x/-y,渲染端徽章用)。 */
|
|
64
|
+
function counts(d) {
|
|
65
|
+
var add = 0, del = 0;
|
|
66
|
+
if (d.oversized) return { add: d.oversized.b, del: d.oversized.a };
|
|
67
|
+
(d.rows || []).forEach(function (r) {
|
|
68
|
+
if (r.t === "+") add++; else if (r.t === "-") del++;
|
|
69
|
+
});
|
|
70
|
+
return { add: add, del: del };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { diff: diff, counts: counts, lcsRows: lcsRows };
|
|
74
|
+
});
|