codebee 0.1.95 → 0.1.96

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 (138) hide show
  1. package/CHANGELOG.md +725 -720
  2. package/LICENSE +21 -21
  3. package/README.md +515 -509
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/agent_context.py +100 -0
  6. package/app/core/aiflavor.py +197 -197
  7. package/app/core/analytics.py +38 -38
  8. package/app/core/artifacts.py +148 -148
  9. package/app/core/assets.py +110 -110
  10. package/app/core/attachments.py +623 -623
  11. package/app/core/audit.py +30 -30
  12. package/app/core/backup.py +470 -470
  13. package/app/core/beekeeper.py +183 -183
  14. package/app/core/benchstore.py +102 -95
  15. package/app/core/board.py +209 -209
  16. package/app/core/bookmeta.py +1036 -1036
  17. package/app/core/bookmeta_catalog.py +180 -180
  18. package/app/core/branching.py +137 -137
  19. package/app/core/builtin_agent.py +1680 -1680
  20. package/app/core/capability.py +144 -144
  21. package/app/core/chaptersafe.py +32 -32
  22. package/app/core/checkpoints.py +143 -0
  23. package/app/core/cleanup.py +303 -303
  24. package/app/core/compaction.py +201 -201
  25. package/app/core/contracts.py +255 -255
  26. package/app/core/covergen.py +356 -356
  27. package/app/core/defectretro.py +43 -43
  28. package/app/core/diagnostics.py +114 -114
  29. package/app/core/dispatch.py +198 -191
  30. package/app/core/dispatch_log.py +128 -128
  31. package/app/core/env_scrub.py +85 -85
  32. package/app/core/errorlog.py +137 -137
  33. package/app/core/eval_matrix.py +82 -0
  34. package/app/core/evalbench.py +184 -91
  35. package/app/core/evaluation.py +174 -174
  36. package/app/core/failure_notify.py +290 -290
  37. package/app/core/flow_graph.py +33 -0
  38. package/app/core/goal_service.py +167 -167
  39. package/app/core/health.py +389 -389
  40. package/app/core/history.py +38 -38
  41. package/app/core/hooks.py +162 -162
  42. package/app/core/jobs.py +25 -4
  43. package/app/core/knowledge_pipeline.py +86 -0
  44. package/app/core/manager.py +2232 -2232
  45. package/app/core/mocks.py +64 -64
  46. package/app/core/notify.py +201 -201
  47. package/app/core/novel_quality.py +181 -189
  48. package/app/core/openapi.py +177 -154
  49. package/app/core/operations.py +228 -228
  50. package/app/core/paihang.py +145 -145
  51. package/app/core/pipeline.py +43 -19
  52. package/app/core/planner.py +17 -11
  53. package/app/core/policy.py +52 -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/auto.py +26 -3
  60. package/app/core/publish/browser.py +655 -655
  61. package/app/core/publish/chaprows.py +138 -0
  62. package/app/core/publish/fanqie.py +210 -165
  63. package/app/core/publish/flow.py +753 -640
  64. package/app/core/publish/flows-fanqie-calibrated.json +349 -336
  65. package/app/core/publish/ledger.py +13 -1
  66. package/app/core/publish/manager.py +1150 -1118
  67. package/app/core/publish/qimao.py +176 -176
  68. package/app/core/publish/volumes.py +108 -0
  69. package/app/core/publish/ws.py +139 -139
  70. package/app/core/quality_gate.py +194 -194
  71. package/app/core/redact.py +38 -38
  72. package/app/core/repeat_guard.py +129 -129
  73. package/app/core/retrieval.py +137 -137
  74. package/app/core/router.py +398 -398
  75. package/app/core/runner.py +2238 -2238
  76. package/app/core/selfupdate.py +384 -384
  77. package/app/core/session_log.py +161 -161
  78. package/app/core/sessions.py +312 -312
  79. package/app/core/settings.py +20 -0
  80. package/app/core/settings_schema.py +270 -270
  81. package/app/core/share_page.py +114 -114
  82. package/app/core/skill_scan.py +139 -139
  83. package/app/core/skillpacks/fanqie-novel.md +8 -6
  84. package/app/core/skillpacks/market/character-bible.md +66 -66
  85. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  86. package/app/core/skillpacks/market/git-workflow.md +57 -57
  87. package/app/core/skillpacks/market/release-notes.md +72 -72
  88. package/app/core/skillpacks/market/weekly-report.md +71 -71
  89. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  90. package/app/core/skillpacks/qimao-signing.md +14 -11
  91. package/app/core/skills.py +32 -21
  92. package/app/core/step_runner.py +132 -132
  93. package/app/core/store.py +9 -0
  94. package/app/core/story_tracking.py +275 -275
  95. package/app/core/task_compile.py +199 -199
  96. package/app/core/telemetry.py +292 -292
  97. package/app/core/tlsctx.py +64 -64
  98. package/app/core/token_meter.py +147 -147
  99. package/app/core/tracing.py +207 -144
  100. package/app/core/upstream.py +41 -41
  101. package/app/core/usage.py +1038 -1038
  102. package/app/core/volumes.py +389 -389
  103. package/app/core/webhooks.py +89 -0
  104. package/app/core/wxdigest.py +858 -858
  105. package/app/core/zentao.py +2692 -2692
  106. package/app/main.py +104 -9
  107. package/app/mcp_server.py +244 -244
  108. package/app/pick_dialog.py +110 -110
  109. package/app/ui/app.js +286 -18
  110. package/app/ui/artdiff.js +74 -74
  111. package/app/ui/assets/community-qr.png +0 -0
  112. package/app/ui/assets/donate-alipay.jpg +0 -0
  113. package/app/ui/assets/donate-qr.jpg +0 -0
  114. package/app/ui/board.html +162 -162
  115. package/app/ui/board.js +305 -305
  116. package/app/ui/i18n.js +33 -0
  117. package/app/ui/icons/bee.svg +79 -79
  118. package/app/ui/index.html +32 -3
  119. package/app/ui/manifest.json +16 -16
  120. package/app/ui/qrcode.js +2297 -2297
  121. package/app/ui/style.css +196 -2
  122. package/bin/tutti.js +147 -147
  123. package/package.json +48 -48
  124. package/tools/wx_reader.py +261 -261
  125. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  126. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  127. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  128. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  129. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  130. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  131. package/app/.playwright-cli/run-detail-after.png +0 -0
  132. package/app/output/_createbook_body.json +0 -1
  133. package/app/output/_probe_fq_click.py +0 -50
  134. package/app/output/_probe_fq_form.py +0 -42
  135. package/app/output/_probe_fq_modal.py +0 -55
  136. package/app/output/_probe_fq_points.py +0 -71
  137. package/app/output/_probe_fq_submit.py +0 -34
  138. package/app/output/_probe_fq_watch.py +0 -62
package/app/ui/app.js CHANGED
@@ -840,8 +840,12 @@ function recommendTaskType(goal) {
840
840
  // 专属词在前(简历/PPT/标书的目标里常带技术词,放 code 之后会被截胡,
841
841
  // 「优化我的简历,投后端岗」实测命中 code——2026-10-07 巡检):
842
842
  ["resume", /(简历|求职|应聘)/i],
843
- ["presentation", /(演示文稿|ppt|幻灯片|答辩)/i],
843
+ ["presentation", /(演示文稿|ppt|幻灯片|答辩|演示场合)/i],
844
844
  ["bid_doc", /(标书|投标|招标应答)/i],
845
+ // 缺陷复盘同为专属词(「复盘这些bug」会被 code 的 bug 词截胡):要求复盘
846
+ // 与 bug/缺陷 共现(或命中漏测),纯 debug(「分析这个bug」)仍归 code。
847
+ ["defect_retro",
848
+ /(缺陷复盘|漏测|(bug|缺陷)[^,。;]{0,10}复盘|复盘[^,。;]{0,10}(bug|缺陷))/i],
845
849
  ["code", /(代码|bug|报错|修复|重构|接口|数据库|前端|后端|python|javascript|typescript|java|go\b|git)/i],
846
850
  ["translation", /(翻译|译成|translate|translation|中译英|英译中)/i],
847
851
  ["email", /(邮件|email|e-mail|回信|邀约信|商务函)/i],
@@ -849,11 +853,18 @@ function recommendTaskType(goal) {
849
853
  ["video_script", /(短视频|口播|分镜|抖音|b站|视频号|视频脚本)/i],
850
854
  ["speech", /(演讲稿|发言稿|致辞|演讲)/i],
851
855
  ["tech_proposal", /(技术方案|架构方案|选型方案|实施方案)/i],
852
- ["research", /(调研报告|竞品调研|市场调研|深度研究)/i],
853
- ["serial_novel", /(连载|续写.*章|网文|长篇小说)/i],
856
+ ["research", /(调研|深度研究)/i],
857
+ // goal_hint 引导词对齐(2026-10-08 第二轮巡检):serial_novel 例句
858
+ // 「都市女频/可签约平台」、article 提示「头条/知乎」——照提示输入
859
+ // 此前不弹类型建议(同 video_script 补词第 3 例缝隙)。
860
+ ["serial_novel", /(连载|续写.*章|网文|长篇小说|女频|男频|签约平台)/i],
854
861
  ["novel", /(小说|故事|短篇|章节)/i],
855
- ["article", /(公众号|自媒体|文章|博客|小红书)/i],
862
+ ["article", /(公众号|自媒体|文章|博客|小红书|头条|知乎)/i],
856
863
  ["doc", /(文档|操作手册|说明书)/i],
864
+ // 宽词放末位兜底(「电影排行榜文章」由前面的 article 先接住);词形对齐
865
+ // rank_scan 的 goal_hint(「默认分析总榜热门题材」)。至此 direct 之外
866
+ // 17 型推荐规则全覆盖(2026-10-08 巡检补 rank_scan/defect_retro 两型)。
867
+ ["rank_scan", /(扫榜|热门题材|榜单分析|排行榜)/i],
857
868
  ];
858
869
  const hit = rules.find((x) => x[1].test(s));
859
870
  return hit && flowById(hit[0]) ? hit[0] : "";
@@ -3117,9 +3128,13 @@ async function createTask() {
3117
3128
  if (suggested) {
3118
3129
  const flow = flowById(suggested) || {};
3119
3130
  resetSubmit();
3131
+ // 快档目标(rank_scan/defect_retro)没有评审门禁,卖点如实换成各自的
3132
+ // 专用数据链路/产出模板;review/code 目标维持门禁口径。
3133
+ const tail = flow.engine === "direct"
3134
+ ? t("这个需求更适合「{0}」流程,能使用对应的专用数据链路与产出模板。是否切换后执行?")
3135
+ : t("这个需求更适合「{0}」流程,能使用对应的规划与质量门禁。是否切换后执行?");
3120
3136
  const yes = await uiConfirm(
3121
- t("这个需求更适合「{0}」流程,能使用对应的规划与质量门禁。是否切换后执行?")
3122
- .replace("{0}", t(flow.name || suggested)),
3137
+ tail.replace("{0}", t(flow.name || suggested)),
3123
3138
  { title: t("推荐任务类型"), ok: t("切换类型") });
3124
3139
  S.typeSuggestionDone = true;
3125
3140
  if (yes) pickType(suggested);
@@ -3847,6 +3862,7 @@ async function renameTask(id) {
3847
3862
  async function retryTask(id) {
3848
3863
  try {
3849
3864
  const r = await api("/api/tasks/" + encodeURIComponent(id) + "/retry", { method: "POST" });
3865
+ chatJumpLatest(); // 续跑副本开新一轮:时间线要跳到最新,别停在原地或历史中间
3850
3866
  jumpToRun(r.run_id);
3851
3867
  } catch (e) { toast(t("重试失败:") + e.message, true); return; }
3852
3868
  poll();
@@ -3873,6 +3889,7 @@ async function continueSerial(id) {
3873
3889
  r = await api("/api/tasks/" + encodeURIComponent(id) + "/continue",
3874
3890
  { method: "POST", body: JSON.stringify({ chapters: ch }) });
3875
3891
  } catch (e) { toast(t("创建续写任务失败:") + e.message, true); return; }
3892
+ chatJumpLatest(); // 续写批次开新一轮:对话时间线跳到最新
3876
3893
  jumpToRun(r.run_id);
3877
3894
  poll();
3878
3895
  }
@@ -4548,6 +4565,7 @@ window.sideOpenRun = function (id, n) {
4548
4565
  * 按时间顺序列出该任务全部 run 的全部步骤,run 之间加分隔条。 */
4549
4566
  window.sideOpenTask = function (key) {
4550
4567
  _taskStepRenderToken++;
4568
+ chatJumpLatest(); // 首开任务详情:对话时间线落最新(默认落蜂巢时切到对话页签也带到底)
4551
4569
  $("rd-steps").innerHTML = '<div class="hint" role="status">' + esc(t("正在加载任务详情…")) + "</div>";
4552
4570
  S.lastRun = null;
4553
4571
  S.detailTaskKey = key;
@@ -5101,6 +5119,7 @@ window.cancelAllRuns = cancelAllRuns;
5101
5119
 
5102
5120
  async function openRun(id, pinTab) {
5103
5121
  _taskStepRenderToken++;
5122
+ chatJumpLatest(); // 首开运行详情:对话时间线落最新(历史长时别停在顶部)
5104
5123
  S.detailRunId = id;
5105
5124
  S.detailTaskKey = null;
5106
5125
  S.runDetailSig = ""; // 新目标首帧必画;也兜住「任务态改过 DOM 后回到本运行」的串台
@@ -5185,6 +5204,10 @@ function rdTabAvail() {
5185
5204
 
5186
5205
  function applyRdTabs() {
5187
5206
  const avail = rdTabAvail();
5207
+ // 「切进对话页签」以 pane 入口前的真实显隐为准:rdChatNavGo/setRdTab 等
5208
+ // 入口都是先改 S.rdTab 再进来,函数内抓 S.rdTab 旧值抓不到过渡。
5209
+ const _chatPaneIn = document.querySelector('#run-detail .rd-pane[data-pane="chat"]');
5210
+ const chatWasHidden = !_chatPaneIn || _chatPaneIn.classList.contains("hidden");
5188
5211
  if (!S.rdTab || !avail[S.rdTab]) {
5189
5212
  S.rdTab = ["chat", "hive", "steps", "result", "preview", "git", "bible", "bookmeta"]
5190
5213
  .find((k) => avail[k]) || "steps";
@@ -5209,6 +5232,17 @@ function applyRdTabs() {
5209
5232
  }
5210
5233
  // 对话页签不放日志抽屉:会盖住贴底输入条(手动切来/自动选卡都覆盖)
5211
5234
  if (S.rdTab === "chat" && !$("rd-log").classList.contains("hidden")) window.rdLogClose();
5235
+ // 切进对话页签且带着跳最新标记(继续任务/首开详情置的):把时间线带到底。
5236
+ // 内容已与当前 run 对齐(drawChatFlow 盖的 run 戳)才就地落底并消费——内容
5237
+ // 还是上一轮的旧帧(续跑换 run、/timeline 在途)则保留标记,等 drawChatFlow
5238
+ // 重建完新内容再落底,避免停在旧帧尾、新一轮气泡压在折叠线下面。
5239
+ if (S.rdTab === "chat" && chatWasHidden && chatForceBottom) {
5240
+ const fl = $("rd-chat-flow");
5241
+ if (fl && fl.dataset.runId && fl.dataset.runId === chatRunId) {
5242
+ chatScrollBottomNow(fl);
5243
+ chatForceBottom = false;
5244
+ }
5245
+ }
5212
5246
  renderChatNav();
5213
5247
  }
5214
5248
 
@@ -5980,6 +6014,7 @@ function pbBlock(task, platform) {
5980
6014
  }
5981
6015
  const busy = st === "busy";
5982
6016
  let btns = "";
6017
+ let pendHtml = ""; // 批量选择 chips(bookReady 分支里填充,extra 段落拼接)
5983
6018
  if (st === "none" || st === "error" || st === "waiting_login") {
5984
6019
  btns += '<button class="ghost" onclick="pbConnect(\'' + platform + '\')">' +
5985
6020
  (st === "error" ? t("重连") : t("连接平台")) + "</button>";
@@ -6011,6 +6046,41 @@ function pbBlock(task, platform) {
6011
6046
  ' onclick="pbPublishAll(\'' + esc(task.id) + "', '" + platform + '\')">' +
6012
6047
  t("发布全部待发") + t("(") + au.pending + t(")") + "</button>";
6013
6048
  }
6049
+ // 批量选择:待发章号 chips(默认全选),「发布所选」人工模式 / 「直发所选」
6050
+ // 逐章自动提交(护栏每日上限/连败退避照常生效)。分卷由服务端按卷计划
6051
+ // 自动选/建(volumes.py),界面上不用管卷。
6052
+ const items = (au.items && au.items.length ? au.items : [])
6053
+ .map((x) => Number(x.chapter_no)).filter((n) => n > 0);
6054
+ if (items.length && !run) {
6055
+ const key = task.id + ":" + platform;
6056
+ const sig = items.join(",");
6057
+ S.pbSel = S.pbSel || {};
6058
+ if (!S.pbSel[key] || S.pbSel[key].sig !== sig)
6059
+ S.pbSel[key] = { sig, sel: new Set(items) };
6060
+ const sel = S.pbSel[key].sel;
6061
+ let chips = "";
6062
+ for (const n of items)
6063
+ chips += '<label class="pb-chip' + (sel.has(n) ? " on" : "") + '">' +
6064
+ '<input type="checkbox" ' + (sel.has(n) ? "checked" : "") +
6065
+ ' onchange="pbSelToggle(\'' + esc(task.id) + "','" + platform + "'," + n + ',this.checked)">' +
6066
+ n + "</label>";
6067
+ pendHtml += '<div class="pb-pend" data-key="' + esc(key) + '">' +
6068
+ '<div class="pb-pend-head">' +
6069
+ '<span>' + esc(t("待发 ")) + items.length + esc(t(" 章(按卷计划自动分卷)")) + "</span>" +
6070
+ '<a href="javascript:void(0)" onclick="pbSelAll(\'' + esc(task.id) + "','" + platform + "',true)" + '">' + esc(t("全选")) + "</a>" +
6071
+ '<a href="javascript:void(0)" onclick="pbSelAll(\'' + esc(task.id) + "','" + platform + "',false)" + '">' + esc(t("清空")) + "</a>" +
6072
+ "</div>" +
6073
+ '<div class="pb-pend-chips">' + chips + "</div>" +
6074
+ '<div class="pb-pend-btns">' +
6075
+ '<button class="ghost" ' + (busy || !au.guard_ok || !sel.size ? "disabled" : "") +
6076
+ ' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',false)">' +
6077
+ t("发布所选") + t("(") + sel.size + t(")") + "</button>" +
6078
+ '<button class="ghost" ' + (busy || !au.guard_ok || !sel.size ? "disabled" : "") +
6079
+ ' title="' + esc(t("所选章节逐章自动提交(无人值守),护栏每日上限/连败退避照常生效")) + '"' +
6080
+ ' onclick="pbPublishSelected(\'' + esc(task.id) + "','" + platform + '\',true)">' +
6081
+ t("直发所选") + t("(") + sel.size + t(")") + "</button>" +
6082
+ "</div></div>";
6083
+ }
6014
6084
  if (au.pending > 0 && !au.guard_ok) {
6015
6085
  btns += '<div class="pb-err">' + esc(au.guard_reason || t("护栏拦截")) + "</div>";
6016
6086
  }
@@ -6040,6 +6110,7 @@ function pbBlock(task, platform) {
6040
6110
  esc(t("作品已在平台建好(建书流程没走通或手工建的)?补登记后直接发章,不会在平台新建")) + '">' + t("登记已有作品") + "</button>";
6041
6111
  }
6042
6112
  let extra = "";
6113
+ extra += pendHtml;
6043
6114
  if (st === "error" && !bookReady && ps.last_action === "create_book") {
6044
6115
  extra += '<div class="pb-hint">' + esc(t("若作品其实已在平台建好,点「登记已有作品」补登记,勿重复创建")) + "</div>";
6045
6116
  }
@@ -6287,8 +6358,46 @@ window.pbConfirmChapter = async function (taskId, platform, chapterNo) {
6287
6358
  S._pbSig = ""; pbKick();
6288
6359
  };
6289
6360
 
6290
- window.pbPublishAll = async function (taskId, platform) {
6291
- const au = (((S.pubAuto && S.pubAuto.books) || [])
6361
+ window.pbSelToggle = function (taskId, platform, no, on) {
6362
+ const key = taskId + ":" + platform;
6363
+ const ent = (S.pbSel || {})[key];
6364
+ if (!ent) return;
6365
+ if (on) ent.sel.add(Number(no)); else ent.sel.delete(Number(no));
6366
+ S._pbSig = ""; pbKick(); // 重画刷新按钮计数(勾选态存 S.pbSel 不丢)
6367
+ };
6368
+
6369
+ window.pbSelAll = function (taskId, platform, all) {
6370
+ const key = taskId + ":" + platform;
6371
+ const ent = (S.pbSel || {})[key];
6372
+ if (!ent) return;
6373
+ if (all) for (const n of ent.sig.split(",")) ent.sel.add(Number(n));
6374
+ else ent.sel.clear();
6375
+ S._pbSig = ""; pbKick();
6376
+ };
6377
+
6378
+ window.pbPublishSelected = async function (taskId, platform, autoSubmit) {
6379
+ const key = taskId + ":" + platform;
6380
+ const ent = (S.pbSel || {})[key];
6381
+ const chapters = ent ? [...ent.sel].sort((a, b) => a - b) : [];
6382
+ if (!chapters.length) return;
6383
+ const msg = autoSubmit
6384
+ ? t("将对所选 {0} 章(第 {1} 章—第 {2} 章)逐章自动填稿并提交发布,无需人工确认;护栏(每日上限/连续失败暂停)生效。确定直发?")
6385
+ : t("将按章号顺序逐章填所选 {0} 章(第 {1} 章—第 {2} 章)。人工模式每轮填一章并停在表单页,由你在浏览器里确认提交。分卷会按卷计划自动选/建。继续?");
6386
+ if (!(await uiConfirm(t(msg, chapters.length, chapters[0], chapters[chapters.length - 1]),
6387
+ { ok: autoSubmit ? t("开始直发") : t("开始发布") }))) return;
6388
+ try {
6389
+ const r = await pbApiWithQualityOverride(
6390
+ "/api/publish/task/" + encodeURIComponent(taskId) + "/publish-all",
6391
+ { platform, chapters, auto_submit: !!autoSubmit },
6392
+ "批量发布前的质量门禁未通过", taskId);
6393
+ if (!(r && r.gate_reviewed))
6394
+ toast(autoSubmit ? t("直发已启动,进度见发布台")
6395
+ : t("正在填第一章稿——填好后请在浏览器窗口里确认提交,再重新校准"));
6396
+ } catch (e) { toast(t("自动发布失败:") + e.message, true); }
6397
+ S._pbSig = ""; pbKick();
6398
+ };
6399
+
6400
+ window.pbPublishAll = async function (taskId, platform) { const au = (((S.pubAuto && S.pubAuto.books) || [])
6292
6401
  .find((b) => b.platform === platform)) || {};
6293
6402
  if (!au.pending) return;
6294
6403
  if (!(await uiConfirm(t("将从最靠前的待发章节开始填稿(共 {0} 章待发)。本轮只填一章并停在表单页,由你在浏览器里确认提交;提交后点击“我已在平台提交”,再选择下一章。护栏(每日上限/连续失败暂停)生效。继续?", au.pending),
@@ -8228,12 +8337,95 @@ function hiveThinkLine(lines) {
8228
8337
  return "";
8229
8338
  }
8230
8339
 
8340
+ const hiveSceneTransform = { x: 0, y: 0, zoom: 1 };
8341
+
8342
+ function updateHiveSceneTransform() {
8343
+ const scene = $("rd-hive-cells");
8344
+ if (!scene) return;
8345
+ scene.style.setProperty("--scene-x", hiveSceneTransform.x + "px");
8346
+ scene.style.setProperty("--scene-y", hiveSceneTransform.y + "px");
8347
+ scene.style.setProperty("--scene-zoom", String(hiveSceneTransform.zoom));
8348
+ }
8349
+
8350
+ function setHiveSceneMode(mode) {
8351
+ const scene = $("rd-hive-cells");
8352
+ const viewport = $("rd-hive-viewport");
8353
+ const tools = document.querySelector(".hive-scene-tools");
8354
+ if (!scene || !viewport) return;
8355
+ const is3d = mode !== "2d";
8356
+ scene.classList.toggle("hive-3d", is3d);
8357
+ scene.classList.toggle("hive-2d", !is3d);
8358
+ viewport.classList.toggle("hive-2d", !is3d);
8359
+ if (tools) tools.classList.toggle("hive-2d", !is3d);
8360
+ document.querySelectorAll("[data-hive-view]").forEach((button) => {
8361
+ const active = button.dataset.hiveView === (is3d ? "3d" : "2d");
8362
+ button.classList.toggle("active", active);
8363
+ button.setAttribute("aria-pressed", active ? "true" : "false");
8364
+ });
8365
+ }
8366
+
8367
+ function setupHiveSceneControls() {
8368
+ const viewport = $("rd-hive-viewport");
8369
+ const scene = $("rd-hive-cells");
8370
+ if (!viewport || !scene || viewport.dataset.controlsReady) return;
8371
+ viewport.dataset.controlsReady = "true";
8372
+ updateHiveSceneTransform();
8373
+
8374
+ document.querySelectorAll("[data-hive-view]").forEach((button) => {
8375
+ button.addEventListener("click", () => setHiveSceneMode(button.dataset.hiveView));
8376
+ });
8377
+ document.querySelectorAll("[data-hive-scene-action]").forEach((button) => {
8378
+ button.addEventListener("click", () => {
8379
+ if (button.dataset.hiveSceneAction === "reset") {
8380
+ hiveSceneTransform.x = 0;
8381
+ hiveSceneTransform.y = 0;
8382
+ hiveSceneTransform.zoom = 1;
8383
+ } else {
8384
+ hiveSceneTransform.zoom = Math.max(.65, Math.min(1.55,
8385
+ hiveSceneTransform.zoom + (button.dataset.hiveSceneAction === "zoom-in" ? .1 : -.1)));
8386
+ }
8387
+ updateHiveSceneTransform();
8388
+ });
8389
+ });
8390
+
8391
+ let drag = null;
8392
+ viewport.addEventListener("pointerdown", (event) => {
8393
+ if (event.button !== 0 || event.target.closest(".hive-cell, button, a, input, textarea")) return;
8394
+ drag = { id: event.pointerId, x: event.clientX, y: event.clientY,
8395
+ originX: hiveSceneTransform.x, originY: hiveSceneTransform.y };
8396
+ viewport.classList.add("is-dragging");
8397
+ viewport.setPointerCapture(event.pointerId);
8398
+ });
8399
+ viewport.addEventListener("pointermove", (event) => {
8400
+ if (!drag || drag.id !== event.pointerId) return;
8401
+ hiveSceneTransform.x = drag.originX + event.clientX - drag.x;
8402
+ hiveSceneTransform.y = drag.originY + event.clientY - drag.y;
8403
+ updateHiveSceneTransform();
8404
+ });
8405
+ const endDrag = (event) => {
8406
+ if (!drag || (event && drag.id !== event.pointerId)) return;
8407
+ drag = null;
8408
+ viewport.classList.remove("is-dragging");
8409
+ };
8410
+ viewport.addEventListener("pointerup", endDrag);
8411
+ viewport.addEventListener("pointercancel", endDrag);
8412
+ viewport.addEventListener("lostpointercapture", endDrag);
8413
+ viewport.addEventListener("wheel", (event) => {
8414
+ if (!scene.classList.contains("hive-3d")) return;
8415
+ event.preventDefault();
8416
+ hiveSceneTransform.zoom = Math.max(.65, Math.min(1.55,
8417
+ hiveSceneTransform.zoom * (event.deltaY < 0 ? 1.08 : .92)));
8418
+ updateHiveSceneTransform();
8419
+ }, { passive: false });
8420
+ }
8421
+
8231
8422
  window.renderHive = function (run) {
8232
8423
  const box = $("rd-hive");
8233
8424
  if (!box) return;
8234
8425
  const steps = (run && run.steps) || [];
8235
8426
  if (!run || !steps.length) { box.classList.add("hidden"); stopHiveTick(); return; }
8236
8427
  box.classList.remove("hidden");
8428
+ setupHiveSceneControls();
8237
8429
  // 按步骤首次出现顺序分泳道(流水线天然有序)
8238
8430
  const lanes = [], byStage = {};
8239
8431
  steps.forEach((s) => {
@@ -8444,6 +8636,7 @@ async function resumeNow() {
8444
8636
  } catch (e) { toast(t("操作失败:") + (e && e.message ? e.message : t("网络异常")), true); return; }
8445
8637
  if (r && r.ok) {
8446
8638
  toast(t("已跳过等待,马上继续"));
8639
+ chatJumpLatest(); // 续跑副本接着跑:新步骤落时间线尾部,跳过去看最新
8447
8640
  refreshState(); // 后端 bump 也会推 SSE,这里主动刷一轮让按钮立刻消失
8448
8641
  } else {
8449
8642
  toast((r && r.error) || t("操作失败"), true);
@@ -8720,7 +8913,28 @@ let chatSig = "";
8720
8913
  let chatLiveSig = ""; // 时间线实时增量签名(思考/正文长度变才重建 DOM)
8721
8914
  let chatLiveTimer = null; // 运行中快轮询句柄(1.2s,见 scheduleChatLive)
8722
8915
  let chatLiveRunId = null;
8723
- let chatForceBottom = false; // 发送消息后强制贴底一次(不管当时滚到哪,都要看到自己刚发的内容与回复)
8916
+ let chatForceBottom = false; // 强制贴底一次(不管当时滚到哪,都要看到最新内容);发送消息、继续任务、打开详情共用
8917
+
8918
+ /* 瞬时落底:.rd-chat-flow 带 scroll-behavior:smooth,直接赋值 scrollTop 会变
8919
+ * 成跨全时间的平滑滚动动画(长对话要滑近一秒,测试也会量到半途)——跳最新
8920
+ * 这类大跨度移动一律压成瞬时。 */
8921
+ function chatScrollBottomNow(fl) {
8922
+ if (!fl) return;
8923
+ const prev = fl.style.scrollBehavior;
8924
+ fl.style.scrollBehavior = "auto";
8925
+ fl.scrollTop = fl.scrollHeight;
8926
+ fl.style.scrollBehavior = prev;
8927
+ }
8928
+
8929
+ /* 让对话时间线跳到最新:置强制贴底标记(drawChatFlow 下一次重建时消费),
8930
+ * 并就现有内容先即时贴底,视觉零等待。「继续任务/立即续跑/继续连载」点完
8931
+ * 新一轮内容落在时间线尾部,用户要看到的是最新进展,不是停在原地或历史中间;
8932
+ * 首次打开详情同理。页签当前不可见时标记保留(drawChatFlow 不消费),
8933
+ * 切进对话页签时由 applyRdTabs 带到底。 */
8934
+ function chatJumpLatest() {
8935
+ chatForceBottom = true;
8936
+ chatScrollBottomNow($("rd-chat-flow"));
8937
+ }
8724
8938
 
8725
8939
  function chatEngineIsDirect(run) {
8726
8940
  const st = S.state || {};
@@ -8870,6 +9084,7 @@ function drawChatFlow(run, data, active) {
8870
9084
  ":" + (it.consumed ? 1 : 0), "") + "|" + ((data && data.result) ? "r" : "");
8871
9085
  if (liveSig === chatLiveSig && flow.childElementCount) return;
8872
9086
  chatLiveSig = liveSig;
9087
+ flow.dataset.runId = run.id; // run 戳:applyRdTabs 判「内容是否已对齐当前对话」用
8873
9088
  // 时间显示统一收敛到 HH:MM(日期在 meta 条里,全量戳塞气泡就是噪音);
8874
9089
  // 附件兼容 字符串路径 / {name|path} 对象 两种形态(对象直接拼会成 [object Object])
8875
9090
  const chatTime = (v) => { const s = String(v || "");
@@ -8961,10 +9176,13 @@ function drawChatFlow(run, data, active) {
8961
9176
  ? t("运行中:新消息会排队,本轮回答完后依次送达")
8962
9177
  : t("已结束:发送后将自动开新一轮接着做");
8963
9178
  // 贴底跟随:用户滚到底部附近才自动滚到最新输出,回看历史不打扰;
8964
- // 刚发送过消息则强制贴底一次(用户要看到自己发的消息与正在来的回复)
8965
- const stick = chatForceBottom ||
9179
+ // 带强制贴底标记(发送/继续任务/首开详情置的)则无条件跳最新。
9180
+ // 页签当前不可见(display:none 时 scrollHeight 恒 0)贴底无处落:
9181
+ // 标记保留给 applyRdTabs,切进对话页签那一刻再带到底。
9182
+ const flowVisible = flow.offsetParent !== null;
9183
+ const stick = (chatForceBottom && flowVisible) ||
8966
9184
  flow.scrollHeight - flow.scrollTop - flow.clientHeight < 80;
8967
- chatForceBottom = false;
9185
+ if (chatForceBottom && flowVisible) chatForceBottom = false;
8968
9186
  // 思考过程面板(.ct-body 自带滚动条)逐帧整体重建会丢滚动位置——重绘前
8969
9187
  // 记住「贴底 or 用户上滑到哪」,重绘后恢复:贴底则一直跟最新(2026-09-22
8970
9188
  // 用户诉求),上滑回看历史思维链则保持位置不被打扰。
@@ -8974,7 +9192,7 @@ function drawChatFlow(run, data, active) {
8974
9192
  : true;
8975
9193
  const thinkTop = prevThink ? prevThink.scrollTop : 0;
8976
9194
  flow.innerHTML = html || '<div class="hint">' + esc(t("还没有对话内容")) + "</div>";
8977
- if (stick) flow.scrollTop = flow.scrollHeight;
9195
+ if (stick) chatScrollBottomNow(flow);
8978
9196
  const newThink = flow.querySelector('[data-think-live="1"] .ct-body');
8979
9197
  if (newThink) {
8980
9198
  if (thinkPin) newThink.scrollTop = newThink.scrollHeight;
@@ -9532,9 +9750,7 @@ window.chatSend = async function () {
9532
9750
  ta.style.height = "";
9533
9751
  chatAtts = []; drawChatAtts();
9534
9752
  chatSig = ""; // 强制重画时间线
9535
- chatForceBottom = true; // 发完滚到最新:看到自己刚发的消息与正在来的回复
9536
- const fl = $("rd-chat-flow");
9537
- if (fl) fl.scrollTop = fl.scrollHeight; // 重画前先即时贴底,视觉零跳动
9753
+ chatJumpLatest(); // 发完滚到最新:看到自己刚发的消息与正在来的回复
9538
9754
  if (active) {
9539
9755
  const d = await api("/api/runs/" + encodeURIComponent(chatRunId));
9540
9756
  if (d.run) renderChat(d.run, d.run.status === "running" || d.run.status === "queued");
@@ -9549,6 +9765,7 @@ function bindChat() {
9549
9765
  if (!btn || !file || !send || !ta) return;
9550
9766
  const focusBtn = $("rd-chat-focus");
9551
9767
  if (focusBtn) focusBtn.addEventListener("click", () => {
9768
+ chatJumpLatest(); // 继续对话:先看到最新一轮再开口
9552
9769
  S.rdTab = "chat";
9553
9770
  S.rdTabPin = true;
9554
9771
  applyRdTabs();
@@ -9697,6 +9914,7 @@ const SKINS = [
9697
9914
  { id: "ocean", name: "深海", desc: "藏青底色 + 天蓝强调,夜间长时间盯任务更沉静(默认)" },
9698
9915
  { id: "hermes", name: "墨金", desc: "暖墨底色 + 金色发丝线,Hermes 式古典优雅" },
9699
9916
  { id: "classic", name: "经典", desc: "黑白灰 + 蓝色强调,ChatGPT 式清爽配色" },
9917
+ { id: "paper", name: "简白", desc: "纯白画布 + 浅灰分区 + 克制蓝强调,Codex 式极简明亮" },
9700
9918
  { id: "forest", name: "森野", desc: "墨绿底色 + 青翠强调,偏自然的护眼配色" },
9701
9919
  { id: "amber", name: "暖阳", desc: "暖棕底色 + 琥珀强调,纸感暖调" },
9702
9920
  { id: "violet", name: "霓虹", desc: "暗紫底色 + 品红强调,霓虹感强" },
@@ -13090,10 +13308,25 @@ async function loadEvalBench() {
13090
13308
  const ae = $("eb-auto-enabled"), ad = $("eb-auto-days");
13091
13309
  if (ae && S.settings) ae.checked = S.settings.bench_auto_enabled === true;
13092
13310
  if (ad && S.settings) ad.value = S.settings.bench_auto_days || 7;
13311
+ const fbe = $("eb-feedback-enabled");
13312
+ if (fbe && S.settings) fbe.checked = S.settings.bench_feedback_enabled !== false;
13093
13313
  renderEvalBench();
13094
13314
  if (S.evalbench && S.evalbench.running) EB_TIMER = setInterval(pollEvalBench, 2500);
13095
13315
  }
13096
13316
 
13317
+ async function saveBenchFeedback() {
13318
+ const msg = $("eb-feedback-msg");
13319
+ try {
13320
+ const r = await api("/api/settings", { method: "POST", body: JSON.stringify({
13321
+ bench_feedback_enabled: !$("eb-feedback-enabled") || $("eb-feedback-enabled").checked }) });
13322
+ S.settings = r.settings;
13323
+ if (msg) { msg.textContent = t("已保存"); }
13324
+ pollEvalBench();
13325
+ } catch (e) {
13326
+ if (msg) { msg.textContent = e.message; }
13327
+ }
13328
+ }
13329
+
13097
13330
  async function saveBenchAuto() {
13098
13331
  const msg = $("eb-auto-msg");
13099
13332
  try {
@@ -13161,7 +13394,8 @@ function renderEvalBench() {
13161
13394
  : t("裁判未就绪:先在「编排设置」配置编排者供应商");
13162
13395
  const pr = eb.progress;
13163
13396
  $("eb-progress").textContent = eb.running && pr
13164
- ? t("评测中 ") + (pr.done || 0) + "/" + (pr.total || 0) + (pr.current ? t(" · ") + pr.current : "")
13397
+ ? t("评测中 ") + (pr.done || 0) + "/" + (pr.total || 0) + (pr.current ? t(" · ") + pr.current : "") +
13398
+ (pr.cancel ? t(" · 停止中…当前样题完成后即停") : "")
13165
13399
  : "";
13166
13400
  board.innerHTML = (eb.leaderboard || []).map((r) => {
13167
13401
  const dims = Object.keys(r.scores || {}).length
@@ -13194,6 +13428,26 @@ function renderEvalBench() {
13194
13428
  }
13195
13429
  const btn = $("eb-run-btn");
13196
13430
  if (btn) btn.disabled = !!eb.running;
13431
+ const stopBtn = $("eb-stop-btn");
13432
+ if (stopBtn) {
13433
+ const stopping = !!(eb.running && eb.progress && eb.progress.cancel);
13434
+ stopBtn.textContent = stopping ? t("强制停止") : t("停止");
13435
+ stopBtn.disabled = !eb.running; // 空闲置灰:第二次点击=强制脱离的入口只在停止中开放
13436
+ }
13437
+ const fbBox = $("eb-feedback-status");
13438
+ if (fbBox) {
13439
+ const fb = eb.feedback || {};
13440
+ if (!fb.enabled) {
13441
+ fbBox.textContent = t("反哺已关闭:评测只出榜单,不影响选路。");
13442
+ } else {
13443
+ const sig = Object.values(fb.signals || {});
13444
+ const down = sig.filter((x) => x.below).length;
13445
+ const up = sig.length - down;
13446
+ fbBox.textContent = sig.length
13447
+ ? t("反哺生效:") + up + t(" 个模型加权 · ") + down + t(" 个降权(据 14 天内实测分)")
13448
+ : t("反哺生效中:暂无 14 天内实测分,跑一轮评测后开始影响排序。");
13449
+ }
13450
+ }
13197
13451
  }
13198
13452
 
13199
13453
  /* 逐题对比:行=样题,列=候选模型(按榜单名次),格=最新一次得分 */
@@ -13241,7 +13495,21 @@ async function evalbenchRun() {
13241
13495
  }
13242
13496
 
13243
13497
  async function evalbenchCancel() {
13244
- try { await api("/api/evalbench/cancel", { method: "POST" }); } catch (e) { /* 空闲时 200 ok=false */ }
13498
+ // 两段式:第一击优雅停(等当前样题做完——生成调用最坏要等几分钟);
13499
+ // 服务端已打停止标记后再点=强制脱离(立即回到空闲,后台线程自行收尾)。
13500
+ const msg = $("eb-msg");
13501
+ const already = !!(S.evalbench && S.evalbench.progress && S.evalbench.progress.cancel);
13502
+ try {
13503
+ const r = await api("/api/evalbench/cancel",
13504
+ { method: "POST", body: JSON.stringify({ force: already }) });
13505
+ if (r && r.ok && r.force) {
13506
+ if (msg) { msg.className = "msg ok"; msg.textContent = t("已强制脱离:界面回到空闲,后台当前调用跑完即自行退出"); }
13507
+ } else if (r && r.ok) {
13508
+ if (msg) { msg.className = "msg"; msg.textContent = t("已请求停止:当前样题完成后即停(生成调用最长可能等几分钟);等不及再点一次「强制停止」"); }
13509
+ }
13510
+ } catch (e) {
13511
+ if (msg) { msg.className = "msg err"; msg.textContent = e.message; }
13512
+ }
13245
13513
  pollEvalBench();
13246
13514
  }
13247
13515
 
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
+ });
Binary file
Binary file
Binary file