codebee 0.1.74 → 0.1.77

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.
Files changed (119) hide show
  1. package/CHANGELOG.md +628 -607
  2. package/LICENSE +21 -21
  3. package/README.md +507 -508
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/aiflavor.py +197 -197
  13. package/app/core/analytics.py +38 -38
  14. package/app/core/artifacts.py +148 -148
  15. package/app/core/assets.py +110 -110
  16. package/app/core/attachments.py +623 -623
  17. package/app/core/audit.py +30 -30
  18. package/app/core/backup.py +470 -470
  19. package/app/core/beekeeper.py +183 -183
  20. package/app/core/benchstore.py +95 -95
  21. package/app/core/board.py +209 -209
  22. package/app/core/bookmeta.py +1036 -921
  23. package/app/core/bookmeta_catalog.py +180 -180
  24. package/app/core/branching.py +137 -137
  25. package/app/core/builtin_agent.py +1680 -1680
  26. package/app/core/capability.py +144 -144
  27. package/app/core/chaptersafe.py +32 -32
  28. package/app/core/cleanup.py +303 -303
  29. package/app/core/compaction.py +201 -201
  30. package/app/core/contracts.py +255 -255
  31. package/app/core/covergen.py +356 -356
  32. package/app/core/defectretro.py +43 -43
  33. package/app/core/diagnostics.py +114 -114
  34. package/app/core/dispatch.py +190 -190
  35. package/app/core/dispatch_log.py +128 -128
  36. package/app/core/env_scrub.py +85 -85
  37. package/app/core/errorlog.py +137 -137
  38. package/app/core/evaluation.py +174 -174
  39. package/app/core/failure_notify.py +290 -290
  40. package/app/core/goal_service.py +167 -167
  41. package/app/core/health.py +389 -387
  42. package/app/core/history.py +38 -38
  43. package/app/core/hooks.py +162 -162
  44. package/app/core/jobs.py +22 -0
  45. package/app/core/manager.py +2232 -2232
  46. package/app/core/mocks.py +64 -64
  47. package/app/core/notify.py +201 -201
  48. package/app/core/novel_quality.py +189 -189
  49. package/app/core/openapi.py +154 -154
  50. package/app/core/operations.py +228 -228
  51. package/app/core/paihang.py +145 -145
  52. package/app/core/pipeline.py +189 -18
  53. package/app/core/planner.py +88 -0
  54. package/app/core/portguard.py +111 -111
  55. package/app/core/presence.py +77 -77
  56. package/app/core/preview.py +293 -293
  57. package/app/core/prompt_guard.py +78 -78
  58. package/app/core/publish/__init__.py +7 -7
  59. package/app/core/publish/browser.py +655 -655
  60. package/app/core/publish/fanqie.py +165 -165
  61. package/app/core/publish/flow.py +640 -640
  62. package/app/core/publish/flows-fanqie-calibrated.json +336 -336
  63. package/app/core/publish/manager.py +1118 -1118
  64. package/app/core/publish/qimao.py +176 -176
  65. package/app/core/publish/ws.py +139 -139
  66. package/app/core/quality_gate.py +194 -194
  67. package/app/core/redact.py +38 -38
  68. package/app/core/repeat_guard.py +129 -129
  69. package/app/core/retrieval.py +137 -137
  70. package/app/core/router.py +398 -398
  71. package/app/core/runner.py +2238 -2225
  72. package/app/core/selfupdate.py +384 -384
  73. package/app/core/session_log.py +161 -161
  74. package/app/core/sessions.py +312 -312
  75. package/app/core/settings_schema.py +270 -270
  76. package/app/core/share_page.py +114 -114
  77. package/app/core/skill_scan.py +139 -139
  78. package/app/core/skillpacks/market/character-bible.md +66 -66
  79. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  80. package/app/core/skillpacks/market/git-workflow.md +57 -57
  81. package/app/core/skillpacks/market/release-notes.md +72 -72
  82. package/app/core/skillpacks/market/weekly-report.md +71 -71
  83. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  84. package/app/core/skills.py +1 -1
  85. package/app/core/step_runner.py +132 -132
  86. package/app/core/story_tracking.py +275 -241
  87. package/app/core/task_compile.py +199 -199
  88. package/app/core/telemetry.py +292 -292
  89. package/app/core/tlsctx.py +64 -64
  90. package/app/core/token_meter.py +147 -147
  91. package/app/core/tracing.py +144 -144
  92. package/app/core/upstream.py +41 -41
  93. package/app/core/usage.py +1035 -1035
  94. package/app/core/volumes.py +389 -389
  95. package/app/core/wxdigest.py +858 -858
  96. package/app/core/zentao.py +2692 -2692
  97. package/app/main.py +59 -0
  98. package/app/mcp_server.py +244 -244
  99. package/app/output/_createbook_body.json +1 -0
  100. package/app/output/_probe_fq_click.py +50 -0
  101. package/app/output/_probe_fq_form.py +42 -0
  102. package/app/output/_probe_fq_modal.py +55 -0
  103. package/app/output/_probe_fq_points.py +71 -0
  104. package/app/output/_probe_fq_submit.py +34 -0
  105. package/app/output/_probe_fq_watch.py +62 -0
  106. package/app/pick_dialog.py +110 -110
  107. package/app/ui/app.js +108 -12
  108. package/app/ui/artdiff.js +74 -74
  109. package/app/ui/board.html +162 -162
  110. package/app/ui/board.js +305 -305
  111. package/app/ui/i18n.js +15 -0
  112. package/app/ui/icons/bee.svg +79 -79
  113. package/app/ui/index.html +4 -0
  114. package/app/ui/manifest.json +16 -16
  115. package/app/ui/qrcode.js +2297 -2297
  116. package/app/ui/style.css +7086 -7070
  117. package/bin/tutti.js +147 -147
  118. package/package.json +48 -48
  119. package/tools/wx_reader.py +261 -261
package/app/ui/app.js CHANGED
@@ -4458,6 +4458,14 @@ function renderSideTasks() {
4458
4458
  vb.classList.toggle("hidden", !vcount);
4459
4459
  vb.title = vcount ? vcount + t(" 个任务待裁决") : "";
4460
4460
  }
4461
+ // 急停钮现身条件同源侧栏真相(全量组,不受搜索过滤):有排队/运行中任务
4462
+ // 才显示。点击走后端全量扫描,窗口外/侧栏外的活跃运行也一并取消。
4463
+ const acount = groups.filter((g) => g.status === "running" || g.status === "queued").length;
4464
+ const kb = $("btn-kill-all");
4465
+ if (kb) {
4466
+ kb.classList.toggle("hidden", !acount);
4467
+ kb.title = acount ? t("急停:取消全部进行中(排队/运行)任务") + "(" + acount + ")" : "";
4468
+ }
4461
4469
  }
4462
4470
 
4463
4471
  /* 主视图打开详情面板的公共部分:留在任务树,主栏切到运行/任务详情 */
@@ -5061,6 +5069,26 @@ async function clearRuns() {
5061
5069
  poll();
5062
5070
  }
5063
5071
 
5072
+ /* 急停(kill switch):取消全部排队/运行中任务。危险操作——确认框点名
5073
+ * 数量;后端全量扫,侧栏窗口外的活跃运行也一并取消。防双击:busy 期间
5074
+ * 直接吞掉重复点击。 */
5075
+ async function cancelAllRuns() {
5076
+ const btn = $("btn-kill-all");
5077
+ if (!btn || btn.classList.contains("busy")) return;
5078
+ if (!await uiConfirm(t("确认急停:取消全部进行中(排队/运行)任务?"), { ok: t("急停"), danger: true })) return;
5079
+ btn.classList.add("busy");
5080
+ try {
5081
+ const d = await api("/api/runs/cancel_all", { method: "POST", timeout: 30000 });
5082
+ toast(t("已取消 ") + ((d && d.count) || 0) + t(" 个进行中任务"));
5083
+ } catch (e) {
5084
+ toast(t("急停失败:") + e.message, true);
5085
+ } finally {
5086
+ btn.classList.remove("busy");
5087
+ }
5088
+ poll();
5089
+ }
5090
+ window.cancelAllRuns = cancelAllRuns;
5091
+
5064
5092
  async function openRun(id, pinTab) {
5065
5093
  _taskStepRenderToken++;
5066
5094
  S.detailRunId = id;
@@ -5706,10 +5734,26 @@ function renderBookMetaPanel(task) {
5706
5734
  const v = tags
5707
5735
  ? '<span class="bm-chips">' + raw.map((x) => '<span class="bm-chip">' + esc(String(x)) + "</span>").join("") + "</span>"
5708
5736
  : '<span class="bm-f-v' + (long ? " bm-f--scroll" : "") + '" title="' + esc(val) + '">' + esc(val) + "</span>";
5737
+ // 书名行加「换名」(2026-10-02 撞名实案):只换书名一个字段,其余
5738
+ // 已核对过的分类/标签/简介不被重摇覆盖;换名中禁用态由 S.bmRenaming
5739
+ // 跨重绘保持(轮询整块 innerHTML 重建会把按钮态复位)
5740
+ let rename = "";
5741
+ if (key === "book_name") {
5742
+ const renaming = S.bmRenaming === task.id + "|" + p.id;
5743
+ rename = '<button class="bm-rename" data-task="' + esc(task.id) +
5744
+ '" data-plat="' + p.id + '" data-old="' + esc(val) + '"' + (renaming ? " disabled" : "") +
5745
+ ' title="' + esc(t("书名与本站已有作品重名时,点这里换一个新书名(其余字段不动)")) + '"' +
5746
+ ' onclick="bmRename(this)">' +
5747
+ (renaming
5748
+ ? '<svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名中…"))
5749
+ : '<svg class="ico" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名"))) +
5750
+ "</button>";
5751
+ }
5709
5752
  return '<div class="bm-f' + (long || tags ? " bm-f--full" : "") + '">' +
5710
5753
  '<span class="bm-f-k">' + esc(t(label)) + "</span>" + v +
5711
5754
  '<button class="bm-copy" data-text="' + esc(val) + '" data-label="' + esc(t(label)) +
5712
- '" title="' + esc(t("点击复制")) + '" onclick="bmCopyBtn(this)"><svg class="ico" aria-hidden="true"><use href="#i-file-text"/></svg>' + t("复制") + "</button></div>";
5755
+ '" title="' + esc(t("点击复制")) + '" onclick="bmCopyBtn(this)"><svg class="ico" aria-hidden="true"><use href="#i-file-text"/></svg>' + t("复制") + "</button>" +
5756
+ rename + "</div>";
5713
5757
  }).join("") + "</div>";
5714
5758
  } else if (st === "failed") {
5715
5759
  body = '<div class="bm-errhint">' + esc(entry.error || t("生成失败")) + "</div>";
@@ -5794,6 +5838,34 @@ window.bmGen = async function (taskId, platform) {
5794
5838
  } catch (e) { toast(t("生成失败:") + e.message, true); }
5795
5839
  };
5796
5840
 
5841
+ /* 换名(书名撞名专用):同步接口单次轻模型调用,成功后新书名随 SSE/轮询
5842
+ * 回填;失败 toast 报错,现有字段原样保留。期间 S.bmRenaming 让按钮禁用态
5843
+ * 跨轮询重绘保持,防连点打两个换名请求。 */
5844
+ window.bmRename = async function (btn) {
5845
+ if (btn.disabled) return;
5846
+ // 立即禁用+换文案(防 await 期间双击发第二个请求);S.bmRenaming 让禁用态
5847
+ // 跨轮询重绘保持(轮询整块 innerHTML 重建会把按钮态复位,渲染端按它还原)
5848
+ btn.disabled = true;
5849
+ btn.innerHTML = '<svg class="ico spin" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名中…"));
5850
+ const taskId = btn.dataset.task, platform = btn.dataset.plat;
5851
+ S.bmRenaming = taskId + "|" + platform;
5852
+ try {
5853
+ const r = await api("/api/tasks/" + encodeURIComponent(taskId) + "/book-meta/rename",
5854
+ { method: "POST", body: JSON.stringify({ platform }) });
5855
+ toast(t("已换名:") + "《" + (r.old || btn.dataset.old || "") + "》→《" + (r.new || "") + "》");
5856
+ } catch (e) {
5857
+ toast(t("换名失败:") + e.message, true);
5858
+ } finally {
5859
+ S.bmRenaming = "";
5860
+ // 就地恢复按钮:render 的详情签名守卫在服务端数据没变时(换名失败正是)
5861
+ // 会跳过重绘,按钮不能指望 render 归位;若 render 真重建了整卡,btn 已
5862
+ // detach,改它是无害空操作
5863
+ btn.disabled = false;
5864
+ btn.innerHTML = '<svg class="ico" aria-hidden="true"><use href="#i-refresh"/></svg>' + esc(t("换名"));
5865
+ try { await refreshState(); render(); } catch (e) { /* 拉状态失败不影响上面的恢复 */ }
5866
+ }
5867
+ };
5868
+
5797
5869
  /* 封面图生成(covergen):curl 子进程直接落盘运行目录,Python 不经手图像字节 */
5798
5870
  window.coverGen = async function (taskId) {
5799
5871
  try {
@@ -7434,8 +7506,10 @@ function artifactsChips(runId, files, prevRun, hasPlan) {
7434
7506
  (isTxt ? '<a class="file-chip prev artifact-file-preview" title="' + esc(t("查看内容")) +
7435
7507
  '" data-file-run="' + esc(runId) + '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
7436
7508
  '<svg class="ico" aria-hidden="true"><use href="#i-book"/></svg>' + t("预览") + "</a>" : "") +
7437
- (diffable ? '<a class="file-chip prev artifact-file-diff" title="' + esc(t("与上一版对比")) +
7509
+ (diffable ? '<a class="file-chip prev artifact-file-diff" title="' +
7510
+ esc(prevRun.ver ? t("与第 {0} 版对比").replace("{0}", prevRun.ver) : t("与上一版对比")) +
7438
7511
  '" data-file-run="' + esc(runId) + '" data-prev-run="' + esc(prevRun.id) +
7512
+ '" data-prev-ver="' + (prevRun.ver || "") +
7439
7513
  '" data-file-name="' + esc(f.name) + '" data-file-size="' + (Number(f.size) || 0) + '">' +
7440
7514
  '<svg class="ico" aria-hidden="true"><use href="#i-git-branch"/></svg>' + t("对比") + "</a>" : "") +
7441
7515
  "</span>";
@@ -7472,12 +7546,17 @@ function artifactDiffBody(a, b) {
7472
7546
  return '<pre class="art-diff">' + html + "</pre>";
7473
7547
  }
7474
7548
 
7475
- /* 对比上一版弹窗:取两版文本 → diff 渲染(复用成品弹窗骨架)。 */
7476
- window.artDiffPopup = async function (runId, prevRunId, name) {
7549
+ /* 对比上一版弹窗:取两版文本 → diff 渲染(复用成品弹窗骨架)。
7550
+ * prevVer 在场时副标用「第 N 版 → 当前」(序号比 run_id 前 8 位对用户可读);
7551
+ * 缺号退回原 run_id 摘要,审计语义不丢。 */
7552
+ window.artDiffPopup = async function (runId, prevRunId, name, prevVer) {
7477
7553
  _fpSaveCtx = null; _fpRawText = ""; _fpDirty = false; _fpCopyText = null;
7478
7554
  const nm = String(name).split("/").pop();
7479
- _fpOpen(nm + " · " + t("与上一版对比"),
7480
- '<i>' + esc(String(prevRunId).slice(0, 8)) + " → " + esc(String(runId).slice(0, 8)) + "</i>", "");
7555
+ const sub = prevVer
7556
+ ? '<i>' + esc(t("第 {0} 版 → 当前").replace("{0}", prevVer)) +
7557
+ " · " + esc(String(prevRunId).slice(0, 8)) + " → " + esc(String(runId).slice(0, 8)) + "</i>"
7558
+ : '<i>' + esc(String(prevRunId).slice(0, 8)) + " → " + esc(String(runId).slice(0, 8)) + "</i>";
7559
+ _fpOpen(nm + " · " + t("与上一版对比"), sub, "");
7481
7560
  _fpSetBody('<div class="fp-hint">' + esc(t("正在取两版内容…")) + "</div>");
7482
7561
  const u1 = urlAuth("/api/runs/" + encodeURIComponent(prevRunId) + "/file?name=" + encodeURIComponent(name));
7483
7562
  const u2 = urlAuth("/api/runs/" + encodeURIComponent(runId) + "/file?name=" + encodeURIComponent(name));
@@ -7525,20 +7604,35 @@ async function loadArtifacts(runId) {
7525
7604
  return;
7526
7605
  }
7527
7606
  const head = '<div class="files-head"><span class="sec-title">' + t("成品文件") + '</span>' +
7607
+ verBadge +
7528
7608
  '<span class="wd" title="' + esc(t("点击复制")) + '" onclick="copyText(this.textContent)">' + esc(d.workdir) + "</span></div>";
7529
7609
  // 上一版 run 的文件清单(详情上下文才有):同名文本文件给「对比」入口;
7530
- // 拉不到/无历史静默降级——对比是增强不是闸门
7531
- let prevRun = null;
7610
+ // 拉不到/无历史静默降级——对比是增强不是闸门。
7611
+ // 版本序号(OpenCreator「版本化」剩余面):任务 run 列表最新在前,序号 =
7612
+ // run 总数 - 列表内序位;total 缺失(分页未拉过)就不编版本号,徽章/对比
7613
+ // 标题退回原样——序号是可读性增强不是闸门
7614
+ let prevRun = null, curVer = 0;
7532
7615
  if (detailOwns && S.detailTaskKey) {
7533
- const prev = (_taskRunsCache.get(S.detailTaskKey) || []).find((r) => r.id !== runId);
7616
+ const runs = _taskRunsCache.get(S.detailTaskKey) || [];
7617
+ const total = ((_taskRunsPaging.get(S.detailTaskKey) || {}).total) || 0;
7618
+ const idx = runs.findIndex((r) => r.id === runId);
7619
+ if (total && idx >= 0) curVer = total - idx;
7620
+ const prev = runs.find((r) => r.id !== runId);
7534
7621
  if (prev) {
7535
7622
  try {
7536
7623
  const pd = await api("/api/runs/" + encodeURIComponent(prev.id) + "/files");
7537
- if (pd && (pd.files || []).length)
7538
- prevRun = { id: prev.id, names: new Set(pd.files.map((f2) => f2.name)) };
7624
+ if (pd && (pd.files || []).length) {
7625
+ const pIdx = runs.indexOf(prev);
7626
+ prevRun = { id: prev.id, names: new Set(pd.files.map((f2) => f2.name)),
7627
+ ver: (total && pIdx >= 0) ? total - pIdx : 0 };
7628
+ }
7539
7629
  } catch (e) { /* 上一版清单不可用就不给对比 */ }
7540
7630
  }
7541
7631
  }
7632
+ const verBadge = curVer > 1
7633
+ ? '<span class="ver-badge" title="' + esc(t("该任务第 N 次运行的产出,序号含修订与重试")) + '">' +
7634
+ esc(t("第 {0} 版").replace("{0}", curVer)) + "</span>"
7635
+ : "";
7542
7636
  const chips = artifactsChips(runId, files, prevRun, d.has_plan);
7543
7637
  if (box) box.innerHTML = head + '<div class="file-chips">' + chips + "</div>";
7544
7638
  if (mainBox) { mainBox.classList.remove("hidden");
@@ -15258,6 +15352,7 @@ document.addEventListener("DOMContentLoaded", () => {
15258
15352
  b.addEventListener("click", () => setUsageHeatMode(b.dataset.heat)));
15259
15353
  $("btn-back").addEventListener("click", closeRun);
15260
15354
  $("btn-cancel").addEventListener("click", cancelRun);
15355
+ $("btn-kill-all").addEventListener("click", cancelAllRuns);
15261
15356
  bindDirector();
15262
15357
  bindChat();
15263
15358
  const bindArtifactClicks = (root) => {
@@ -15266,7 +15361,8 @@ document.addEventListener("DOMContentLoaded", () => {
15266
15361
  const diff = e.target.closest(".artifact-file-diff[data-prev-run]");
15267
15362
  if (diff) { // 对比上一版:先于通用预览分支(chip 同样带 data-file-run/name)
15268
15363
  e.preventDefault();
15269
- artDiffPopup(diff.dataset.fileRun, diff.dataset.prevRun, diff.dataset.fileName || "");
15364
+ artDiffPopup(diff.dataset.fileRun, diff.dataset.prevRun, diff.dataset.fileName || "",
15365
+ Number(diff.dataset.prevVer) || 0);
15270
15366
  return;
15271
15367
  }
15272
15368
  const file = e.target.closest("[data-file-run][data-file-name]");
package/app/ui/artdiff.js CHANGED
@@ -1,74 +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
- });
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
+ });