codebee 0.1.103 → 0.1.105

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 (128) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -21
  3. package/README.md +5 -3
  4. package/app/core/acceptance.py +100 -100
  5. package/app/core/acp_client.py +457 -457
  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/backend_profiles.py +188 -188
  13. package/app/core/backup.py +470 -470
  14. package/app/core/beekeeper.py +183 -183
  15. package/app/core/benchstore.py +102 -102
  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/capability.py +144 -144
  20. package/app/core/chaptersafe.py +32 -32
  21. package/app/core/checkpoints.py +48 -5
  22. package/app/core/cleanup.py +303 -303
  23. package/app/core/command_auth.py +302 -302
  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_log.py +128 -128
  30. package/app/core/env_scrub.py +85 -85
  31. package/app/core/errorlog.py +137 -137
  32. package/app/core/eval_matrix.py +91 -91
  33. package/app/core/evaluation.py +174 -174
  34. package/app/core/failure_notify.py +290 -290
  35. package/app/core/flow_graph.py +33 -33
  36. package/app/core/goal_service.py +167 -167
  37. package/app/core/health.py +389 -389
  38. package/app/core/history.py +38 -38
  39. package/app/core/hooks.py +162 -162
  40. package/app/core/knowledge_pipeline.py +86 -86
  41. package/app/core/manager.py +2386 -2385
  42. package/app/core/mocks.py +64 -64
  43. package/app/core/notify.py +201 -201
  44. package/app/core/novel_quality.py +285 -181
  45. package/app/core/openapi.py +246 -217
  46. package/app/core/operations.py +228 -228
  47. package/app/core/paihang.py +145 -145
  48. package/app/core/pipeline.py +81 -11
  49. package/app/core/planner.py +7 -0
  50. package/app/core/portguard.py +111 -111
  51. package/app/core/presence.py +77 -77
  52. package/app/core/preview.py +293 -293
  53. package/app/core/project_memory.py +252 -206
  54. package/app/core/prompt_guard.py +78 -78
  55. package/app/core/publish/__init__.py +7 -7
  56. package/app/core/publish/auto.py +659 -659
  57. package/app/core/publish/chaprows.py +138 -138
  58. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  59. package/app/core/publish/manager.py +1185 -1185
  60. package/app/core/publish/qimao.py +193 -193
  61. package/app/core/publish/volumes.py +108 -108
  62. package/app/core/publish/ws.py +139 -139
  63. package/app/core/quality_gate.py +201 -201
  64. package/app/core/redact.py +38 -38
  65. package/app/core/registry.py +35 -0
  66. package/app/core/repeat_guard.py +129 -129
  67. package/app/core/retrieval.py +137 -137
  68. package/app/core/review_requests.py +119 -0
  69. package/app/core/runner.py +5 -1
  70. package/app/core/sandbox_runtime.py +95 -95
  71. package/app/core/selfupdate.py +384 -384
  72. package/app/core/session_log.py +161 -161
  73. package/app/core/sessions.py +312 -312
  74. package/app/core/settings.py +215 -215
  75. package/app/core/settings_schema.py +282 -282
  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/step_runner.py +132 -132
  85. package/app/core/story_tracking.py +275 -275
  86. package/app/core/task_compile.py +199 -199
  87. package/app/core/telemetry.py +292 -292
  88. package/app/core/tlsctx.py +64 -64
  89. package/app/core/token_meter.py +147 -147
  90. package/app/core/tool_outputs.py +107 -107
  91. package/app/core/tracing.py +207 -207
  92. package/app/core/upstream.py +41 -41
  93. package/app/core/usage.py +1038 -1038
  94. package/app/core/volumes.py +389 -389
  95. package/app/core/webhooks.py +89 -89
  96. package/app/core/wxdigest.py +865 -865
  97. package/app/core/zentao.py +2692 -2692
  98. package/app/main.py +43 -0
  99. package/app/mcp_server.py +244 -244
  100. package/app/pick_dialog.py +110 -110
  101. package/app/ui/app.js +21 -2
  102. package/app/ui/artdiff.js +74 -74
  103. package/app/ui/hive3d.js +54 -31
  104. package/app/ui/i18n.js +32 -1
  105. package/app/ui/icons/bee.svg +79 -79
  106. package/app/ui/index.html +1 -0
  107. package/app/ui/manifest.json +16 -16
  108. package/app/ui/qrcode.js +2297 -2297
  109. package/bin/tutti.js +147 -147
  110. package/package.json +1 -1
  111. package/tools/wx_reader.py +261 -261
  112. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  113. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  114. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  115. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  116. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  117. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  118. package/app/.playwright-cli/run-detail-after.png +0 -0
  119. package/app/output/_createbook_body.json +0 -1
  120. package/app/output/_probe_fq_click.py +0 -50
  121. package/app/output/_probe_fq_form.py +0 -42
  122. package/app/output/_probe_fq_modal.py +0 -55
  123. package/app/output/_probe_fq_points.py +0 -71
  124. package/app/output/_probe_fq_submit.py +0 -34
  125. package/app/output/_probe_fq_watch.py +0 -62
  126. package/app/output/workbench-clean-preview.jpg +0 -0
  127. package/app/output/workbench-reference-render.png +0 -0
  128. package/app/output/workbench-shell.png +0 -0
@@ -1,110 +1,110 @@
1
- #!/usr/bin/env python3
2
- """原生「选择文件夹」对话框(main.py 的 /api/pick_folder 用)。
3
-
4
- Tk 必须活在自家进程的主线程里:HTTP 请求线程里建 root 在 macOS 上会崩,
5
- Windows 上反复建/销毁也不稳,所以隔离成独立进程。ask_directory() 是父端
6
- 入口,把本文件拉起为子进程,请求参数(JSON:initial 起始目录 / title 标题)
7
- 走 stdin,选中目录以 JSON({"path": ...})走 stdout,用户取消回空串;
8
- 对话框部分是文件尾的 main()。任何一步失败都以非零码退出,父端据此回
9
- fallback=true 让前端回落网页目录弹框。"""
10
- import json
11
- import os
12
- import subprocess
13
- import sys
14
- from pathlib import Path
15
-
16
- # 非 Windows 置 0:POSIX 的 Popen 对非零 creationflags 抛 ValueError(manager 同款守卫)
17
- CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
18
- _SELF = str(Path(__file__).resolve())
19
-
20
-
21
- def ask_directory(initial="", title="选择文件夹"):
22
- """父端入口:拉起子进程弹原生对话框,返回 (path, error, fallback)。
23
-
24
- 用户取消时 path 为空串;子进程起不来(机器无 tkinter 等)时 error
25
- 非空且 fallback=True,调用方原样转给前端回落网页目录弹框。"""
26
- if not Path(__file__).exists():
27
- return "", "pick_dialog.py 缺失", True
28
- req = json.dumps({"initial": initial, "title": title}).encode("utf-8")
29
- env = dict(os.environ, PYTHONIOENCODING="utf-8")
30
- try:
31
- if os.name == "nt":
32
- cp = subprocess.run([sys.executable, _SELF], input=req,
33
- capture_output=True, timeout=600, env=env,
34
- creationflags=CREATE_NO_WINDOW)
35
- else:
36
- cp = subprocess.run([sys.executable, _SELF], input=req,
37
- capture_output=True, timeout=600, env=env)
38
- except Exception as e:
39
- return "", str(e), True
40
- if cp.returncode != 0:
41
- err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
42
- return "", (err[-1] if err else "native picker unavailable"), True
43
- try:
44
- data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
45
- except Exception:
46
- return "", "对话框输出无法解析", True
47
- return str(data.get("path") or ""), "", False
48
-
49
-
50
- def ask_file(initial="", title="选择文件"):
51
- """父端入口:拉起子进程弹原生「选择文件」对话框(导入备份包用)。
52
- 返回 (path, error, fallback),语义与 ask_directory 一致。"""
53
- if not Path(__file__).exists():
54
- return "", "pick_dialog.py 缺失", True
55
- req = json.dumps({"mode": "file", "initial": initial, "title": title}).encode("utf-8")
56
- env = dict(os.environ, PYTHONIOENCODING="utf-8")
57
- try:
58
- if os.name == "nt":
59
- cp = subprocess.run([sys.executable, _SELF], input=req,
60
- capture_output=True, timeout=600, env=env,
61
- creationflags=CREATE_NO_WINDOW)
62
- else:
63
- cp = subprocess.run([sys.executable, _SELF], input=req,
64
- capture_output=True, timeout=600, env=env)
65
- except Exception as e:
66
- return "", str(e), True
67
- if cp.returncode != 0:
68
- err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
69
- return "", (err[-1] if err else "native picker unavailable"), True
70
- try:
71
- data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
72
- except Exception:
73
- return "", "对话框输出无法解析", True
74
- return str(data.get("path") or ""), "", False
75
-
76
-
77
- def main():
78
- req = {}
79
- try:
80
- req = json.loads(sys.stdin.read() or "{}")
81
- except Exception:
82
- pass
83
- import tkinter as tk
84
- from tkinter import filedialog
85
-
86
- root = tk.Tk()
87
- root.withdraw()
88
- try:
89
- root.attributes("-topmost", True) # 别被全屏浏览器盖住
90
- except Exception:
91
- pass
92
- is_file = req.get("mode") == "file"
93
- kw = {"title": req.get("title") or ("选择文件" if is_file else "选择文件夹")}
94
- init = req.get("initial") or ""
95
- if init and os.path.isdir(init):
96
- kw["initialdir"] = init
97
- try:
98
- if is_file:
99
- kw["filetypes"] = [("备份包", "*.zip"), ("全部文件", "*.*")]
100
- path = filedialog.askopenfilename(**kw) or ""
101
- else:
102
- path = filedialog.askdirectory(**kw) or ""
103
- except Exception:
104
- sys.exit(1)
105
- sys.stdout.write(json.dumps({"path": path}))
106
- root.destroy()
107
-
108
-
109
- if __name__ == "__main__":
110
- main()
1
+ #!/usr/bin/env python3
2
+ """原生「选择文件夹」对话框(main.py 的 /api/pick_folder 用)。
3
+
4
+ Tk 必须活在自家进程的主线程里:HTTP 请求线程里建 root 在 macOS 上会崩,
5
+ Windows 上反复建/销毁也不稳,所以隔离成独立进程。ask_directory() 是父端
6
+ 入口,把本文件拉起为子进程,请求参数(JSON:initial 起始目录 / title 标题)
7
+ 走 stdin,选中目录以 JSON({"path": ...})走 stdout,用户取消回空串;
8
+ 对话框部分是文件尾的 main()。任何一步失败都以非零码退出,父端据此回
9
+ fallback=true 让前端回落网页目录弹框。"""
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ # 非 Windows 置 0:POSIX 的 Popen 对非零 creationflags 抛 ValueError(manager 同款守卫)
17
+ CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
18
+ _SELF = str(Path(__file__).resolve())
19
+
20
+
21
+ def ask_directory(initial="", title="选择文件夹"):
22
+ """父端入口:拉起子进程弹原生对话框,返回 (path, error, fallback)。
23
+
24
+ 用户取消时 path 为空串;子进程起不来(机器无 tkinter 等)时 error
25
+ 非空且 fallback=True,调用方原样转给前端回落网页目录弹框。"""
26
+ if not Path(__file__).exists():
27
+ return "", "pick_dialog.py 缺失", True
28
+ req = json.dumps({"initial": initial, "title": title}).encode("utf-8")
29
+ env = dict(os.environ, PYTHONIOENCODING="utf-8")
30
+ try:
31
+ if os.name == "nt":
32
+ cp = subprocess.run([sys.executable, _SELF], input=req,
33
+ capture_output=True, timeout=600, env=env,
34
+ creationflags=CREATE_NO_WINDOW)
35
+ else:
36
+ cp = subprocess.run([sys.executable, _SELF], input=req,
37
+ capture_output=True, timeout=600, env=env)
38
+ except Exception as e:
39
+ return "", str(e), True
40
+ if cp.returncode != 0:
41
+ err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
42
+ return "", (err[-1] if err else "native picker unavailable"), True
43
+ try:
44
+ data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
45
+ except Exception:
46
+ return "", "对话框输出无法解析", True
47
+ return str(data.get("path") or ""), "", False
48
+
49
+
50
+ def ask_file(initial="", title="选择文件"):
51
+ """父端入口:拉起子进程弹原生「选择文件」对话框(导入备份包用)。
52
+ 返回 (path, error, fallback),语义与 ask_directory 一致。"""
53
+ if not Path(__file__).exists():
54
+ return "", "pick_dialog.py 缺失", True
55
+ req = json.dumps({"mode": "file", "initial": initial, "title": title}).encode("utf-8")
56
+ env = dict(os.environ, PYTHONIOENCODING="utf-8")
57
+ try:
58
+ if os.name == "nt":
59
+ cp = subprocess.run([sys.executable, _SELF], input=req,
60
+ capture_output=True, timeout=600, env=env,
61
+ creationflags=CREATE_NO_WINDOW)
62
+ else:
63
+ cp = subprocess.run([sys.executable, _SELF], input=req,
64
+ capture_output=True, timeout=600, env=env)
65
+ except Exception as e:
66
+ return "", str(e), True
67
+ if cp.returncode != 0:
68
+ err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
69
+ return "", (err[-1] if err else "native picker unavailable"), True
70
+ try:
71
+ data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
72
+ except Exception:
73
+ return "", "对话框输出无法解析", True
74
+ return str(data.get("path") or ""), "", False
75
+
76
+
77
+ def main():
78
+ req = {}
79
+ try:
80
+ req = json.loads(sys.stdin.read() or "{}")
81
+ except Exception:
82
+ pass
83
+ import tkinter as tk
84
+ from tkinter import filedialog
85
+
86
+ root = tk.Tk()
87
+ root.withdraw()
88
+ try:
89
+ root.attributes("-topmost", True) # 别被全屏浏览器盖住
90
+ except Exception:
91
+ pass
92
+ is_file = req.get("mode") == "file"
93
+ kw = {"title": req.get("title") or ("选择文件" if is_file else "选择文件夹")}
94
+ init = req.get("initial") or ""
95
+ if init and os.path.isdir(init):
96
+ kw["initialdir"] = init
97
+ try:
98
+ if is_file:
99
+ kw["filetypes"] = [("备份包", "*.zip"), ("全部文件", "*.*")]
100
+ path = filedialog.askopenfilename(**kw) or ""
101
+ else:
102
+ path = filedialog.askdirectory(**kw) or ""
103
+ except Exception:
104
+ sys.exit(1)
105
+ sys.stdout.write(json.dumps({"path": path}))
106
+ root.destroy()
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
package/app/ui/app.js CHANGED
@@ -434,7 +434,7 @@ function flowIconHtml(f) {
434
434
  function flowDesc(f) {
435
435
  if (f.id === "rank_scan") return t("抓四平台榜 → AI 选题洞察·附证据分级");
436
436
  if (f.engine === "direct") return t("单智能体直达(快)");
437
- if (f.engine === "code") return t("实现 → 验证 → 评审");
437
+ if (f.engine === "code") return t("实现 → 验证 →(复杂任务进)评审");
438
438
  // 连载与单稿件同引擎,描述必须区分:连载强调逐章与断点续跑
439
439
  if (f.serial) return t("大纲 → 逐章起草评审 → 合并(可续跑)");
440
440
  return t("起草 → 多维评审 → 门禁");
@@ -4796,6 +4796,20 @@ window.loadProjectMemory = async function (taskId) {
4796
4796
  } catch (error) { box.innerHTML = '<span class="hint">' + esc(t("加载项目记忆失败:")) + esc(error.message) + "</span>"; }
4797
4797
  };
4798
4798
 
4799
+ window.previewProjectMemory = async function (taskId) {
4800
+ taskId = taskId || S.detailTaskKey;
4801
+ if (!taskId) return;
4802
+ const box = $("rd-project-memory-body");
4803
+ if (!box) return;
4804
+ try {
4805
+ const data = await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory/preview?cap=4000");
4806
+ const entries = (data.entries || []).map((entry) =>
4807
+ '<span class="chip">' + esc(entry.title || t("项目记忆")) + ' · v' + esc(String(entry.version || 1)) + '</span>').join(" ");
4808
+ const text = data.text ? '<pre>' + esc(data.text) + '</pre>' : '<span class="hint">' + esc(t("暂无可注入记忆")) + '</span>';
4809
+ box.insertAdjacentHTML("afterbegin", '<div class="rd-memory-preview"><strong>' + esc(t("本次注入预览")) + '</strong><span class="hint">' + esc(t("已使用 {0}/{1} 字符", data.budget?.used || 0, data.budget?.cap || 0)) + '</span><div>' + entries + '</div>' + text + '</div>');
4810
+ } catch (error) { toast(t("项目记忆预览失败:") + error.message, true); }
4811
+ };
4812
+
4799
4813
  window.decideProjectMemory = async function (taskId, id, action, version) {
4800
4814
  try {
4801
4815
  await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory", {
@@ -4841,7 +4855,7 @@ window.loadRunCheckpoints = async function (requestedRunId) {
4841
4855
  const checkpoint = data.checkpoint || {};
4842
4856
  const steps = Array.isArray(checkpoint.steps) ? checkpoint.steps : [];
4843
4857
  const stepSummary = steps.length ? '<div class="rd-checkpoint-steps"><strong>' + esc(t("步骤回放预览")) + '</strong>' +
4844
- steps.map((step) => '<div class="rd-checkpoint-step"><span>#' + esc(String(step.step ?? "")) + ' · ' + esc(step.role || t("未命名步骤")) + '</span><span class="chip">' + esc(step.status || step.event || "unknown") + (step.attempt ? ' · ' + esc(t("第") + step.attempt + t("次")) : "") + '</span></div>').join("") +
4858
+ steps.map((step) => '<div class="rd-checkpoint-step"><span>#' + esc(String(step.step ?? "")) + ' · ' + esc(step.role || t("未命名步骤")) + '</span><span class="chip">' + esc(step.status || step.event || "unknown") + (step.attempt ? ' · ' + esc(t("第{0}次", step.attempt)) : "") + '</span></div>').join("") +
4845
4859
  (checkpoint.truncated ? '<p class="hint">' + esc(t("仅显示最近 200 个步骤,共 ") + (checkpoint.total_steps || steps.length)) + '</p>' : '') +
4846
4860
  (checkpoint.replayable ? '<p class="hint">' + esc(t("所有步骤均已完成,可人工复核后继续")) + '</p>' : '<p class="hint">' + esc(t("无法安全自动回放:") + (checkpoint.blocked_reason || t("需要人工核对"))) + '</p>') + '</div>' : '';
4847
4861
  body.innerHTML = (data.files && data.files.warning ? '<p class="hint">' + esc(data.files.warning) + "</p>" : "") + stepSummary +
@@ -11144,6 +11158,10 @@ function card(c) {
11144
11158
  ? '<label class="toggle"><input type="checkbox" ' + (c.orch_enabled ? "checked" : "") +
11145
11159
  ' onchange="toggleOrch(\'' + esc(c.id) + '\', this.checked)">' + t(" 参与编排") + '</label>'
11146
11160
  : '<span class="tag">' + t("仅管理") + '</span>';
11161
+ const capability = c.capabilities || {};
11162
+ const capTags = [capability.transport, capability.resume ? t("支持续会话") : "",
11163
+ capability.mcp ? "MCP" : "", capability.sandbox].filter(Boolean)
11164
+ .map((x) => '<span class="tag">' + esc(t(x)) + '</span>').join("");
11147
11165
  const dis = mgmtBusy(c.id) ? " disabled" : "";
11148
11166
  const hasModels = modelGroups().length > 0;
11149
11167
  const modelBox = c.config_writable
@@ -11172,6 +11190,7 @@ function card(c) {
11172
11190
  (c.installed && c.uninstall_cmd ? '<button class="danger small"' + dis + ' onclick="mgmt(\'' + esc(c.id) + '\', \'uninstall\')">' + t("卸载") + '</button>' : "") +
11173
11191
  "</div>" +
11174
11192
  '<div class="note">' + esc(t(c.note || "")) + "</div>" +
11193
+ (capTags ? '<div class="facts capability-tags">' + capTags + '</div>' : "") +
11175
11194
  '<div class="facts">' + t("版本 ") + '<b>' + esc(c.version || "-") + "</b>" + t(" 模型 ") + "<b>" + esc(fmtModel(c.model) || "-") + "</b>" +
11176
11195
  ((c.detail || c.config_path) ? "<br>" + esc(c.detail || c.config_path) : "") + "</div>" +
11177
11196
  mgmtPanel(c.id) +
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
+ });
package/app/ui/hive3d.js CHANGED
@@ -91,17 +91,21 @@ window.Hive3D = (function () {
91
91
  function organicGeometry(gl){
92
92
  // A polished, tapered leaf/wing silhouette with a gently domed center and two-sided normals.
93
93
  let outline=[
94
- [-.5,0,0],[-.34,.17,.018],[-.12,.255,.040],[.14,.225,.044],
95
- [.38,.13,.026],[.5,0,0],[.38,-.13,.026],[.14,-.225,.044],
96
- [-.12,-.255,.040],[-.34,-.17,.018]
94
+ [-.5,0,.006],[-.34,.17,.028],[-.12,.255,.064],[.14,.225,.078],
95
+ [.38,.13,.038],[.5,0,.006],[.38,-.13,.038],[.14,-.225,.078],
96
+ [-.12,-.255,.064],[-.34,-.17,.028]
97
97
  ];
98
98
  const area=outline.reduce((sum,p,i)=>{const q=outline[(i+1)%outline.length];return sum+p[0]*q[1]-q[0]*p[1];},0);
99
99
  if(area<0)outline=outline.reverse();
100
100
  const positions=[],normals=[],indices=[];
101
101
  const emitSide=(sign)=>{
102
102
  const centerIndex=positions.length/3;
103
- positions.push(0,0,sign*.045);normals.push(0,0,sign);
104
- for(const p of outline){positions.push(p[0],p[1],p[2]*sign);normals.push(0,0,sign);}
103
+ positions.push(0,0,sign*.105);normals.push(0,0,sign);
104
+ for(const p of outline){
105
+ positions.push(p[0],p[1],p[2]*sign);
106
+ const n=norm([-.20*p[0],-.28*p[1],sign]);
107
+ normals.push(...n);
108
+ }
105
109
  for(let i=0;i<outline.length;i++){
106
110
  const a=centerIndex+1+i,b=centerIndex+1+(i+1)%outline.length;
107
111
  if(sign>0)indices.push(centerIndex,a,b);else indices.push(centerIndex,b,a);
@@ -188,7 +192,7 @@ window.Hive3D = (function () {
188
192
  const vs="attribute vec3 aPosition; attribute vec3 aNormal; attribute vec4 aColor; uniform mat4 uViewProj; varying vec4 vColor; varying float vShade; varying vec3 vWorld; varying vec3 vNormal; void main(){gl_Position=uViewProj*vec4(aPosition,1.0);vColor=aColor;vWorld=aPosition;vNormal=normalize(aNormal);vec3 n=normalize(aNormal);float sky=clamp(n.y*0.5+0.5,0.0,1.0);float side=abs(n.x)*0.035;float heightShade=mix(0.94,1.0,smoothstep(-0.2,3.6,aPosition.y));vShade=(0.88+0.08*sky+side)*heightShade;}";
189
193
  const fs="precision mediump float; varying vec4 vColor; varying float vShade; varying vec3 vWorld; varying vec3 vNormal; uniform vec3 uEyePosition; void main(){vec3 n=normalize(vNormal);vec3 v=normalize(uEyePosition-vWorld);vec3 key=normalize(vec3(-0.48,0.86,0.42));vec3 fill=normalize(vec3(0.62,0.32,-0.72));vec3 warm=normalize(vec3(0.34,0.46,0.82));float ndl=max(dot(n,key),0.0);float fillN=max(dot(n,fill),0.0);float warmN=max(dot(n,warm),0.0);float hemi=clamp(n.y*0.5+0.5,0.0,1.0);vec3 base=vColor.rgb;float wallBlue=step(0.38,base.b)*step(0.35,base.g)*step(base.r*1.65,base.g)*step(1.7,vWorld.y);float wallGradient=0.86+0.14*smoothstep(1.7,4.5,vWorld.y)+0.055*exp(-pow((vWorld.x+2.1)*0.24,2.0));base=mix(base,base*wallGradient,wallBlue);vec3 h=normalize(key+v);vec3 hf=normalize(fill+v);float chroma=max(max(base.r,base.g),base.b)-min(min(base.r,base.g),base.b);float gloss=mix(0.10,0.34,smoothstep(0.10,0.82,chroma));float spec=pow(max(dot(n,h),0.0),mix(24.0,58.0,gloss))*gloss*0.42;float specFill=pow(max(dot(n,hf),0.0),36.0)*0.075;float fresnel=pow(1.0-max(dot(n,v),0.0),3.0);vec3 ambient=base*(0.55+0.13*hemi);vec3 direct=base*(0.34*ndl+0.16*fillN+0.08*warmN);vec3 highlight=vec3(1.0,0.91,0.78)*spec+vec3(0.52,0.84,1.0)*specFill+vec3(0.18,0.48,0.68)*fresnel*0.075;float cyan=max(0.0,min(base.g,base.b)-base.r)*0.65;vec3 emissive=base*cyan;vec3 lit=ambient+direct+highlight+emissive;lit*=vShade;gl_FragColor=vec4(min(lit,vec3(1.0)),vColor.a);}";
190
194
  const program=gl.createProgram();gl.attachShader(program,shader(gl,gl.VERTEX_SHADER,vs));gl.attachShader(program,shader(gl,gl.FRAGMENT_SHADER,fs));gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program));this.program=program;gl.useProgram(program);
191
- this.aPosition=gl.getAttribLocation(program,"aPosition");this.aNormal=gl.getAttribLocation(program,"aNormal");this.aColor=gl.getAttribLocation(program,"aColor");this.uViewProj=gl.getUniformLocation(program,"uViewProj");this.uEyePosition=gl.getUniformLocation(program,"uEyePosition");this.box=boxGeometry(gl);this.roundBox=roundedBoxGeometry(gl,6,.12);this.sphere=sphereGeometry(gl,16,24);this.cylinder=cylinderGeometry(gl,20);this.organic=organicGeometry(gl);this.grout=hexGroutGeometry(gl);this.bandGoldTop=sphereBandGeometry(gl,-.29,-.03);this.bandGoldBottom=sphereBandGeometry(gl,-.97,-.61);this.bandSeparator=sphereBandGeometry(gl,-.51,-.38);this.bandGoldTop.smooth=true;this.bandGoldBottom.smooth=true;this.bandSeparator.smooth=true;gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);gl.cullFace(gl.BACK);gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.clearColor(0,0,0,0);
195
+ this.aPosition=gl.getAttribLocation(program,"aPosition");this.aNormal=gl.getAttribLocation(program,"aNormal");this.aColor=gl.getAttribLocation(program,"aColor");this.uViewProj=gl.getUniformLocation(program,"uViewProj");this.uEyePosition=gl.getUniformLocation(program,"uEyePosition");this.box=boxGeometry(gl);this.roundBox=roundedBoxGeometry(gl,6,.12);this.sphere=sphereGeometry(gl,16,24);this.cylinder=cylinderGeometry(gl,20);this.organic=organicGeometry(gl);this.grout=hexGroutGeometry(gl);this.bandGoldTop=sphereBandGeometry(gl,-.08,.18);this.bandGoldBottom=sphereBandGeometry(gl,-.97,-.61);this.bandSeparator=sphereBandGeometry(gl,-.51,-.38);this.bandGoldTop.smooth=true;this.bandGoldBottom.smooth=true;this.bandSeparator.smooth=true;gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);gl.cullFace(gl.BACK);gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.clearColor(0,0,0,0);
192
196
  };
193
197
  Scene.prototype.listen=function(el,type,fn,opts){el.addEventListener(type,fn,opts);this.listeners.push(()=>el.removeEventListener(type,fn,opts));};
194
198
  Scene.prototype.bindSurface=function(){
@@ -204,10 +208,10 @@ window.Hive3D = (function () {
204
208
  Scene.prototype.addLink=function(a,b,radius,color){const dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2],length=Math.hypot(dx,dy,dz)||.001,rotX=Math.asin(clamp(dz/length,-1,1)),rotZ=Math.atan2(-dx,dy);this.add(this.cylinder,(a[0]+b[0])/2,(a[1]+b[1])/2,(a[2]+b[2])/2,radius,length,radius,color,0,rotZ,rotX);};
205
209
  Scene.prototype.buildWorld=function(){
206
210
  const b=this.box,rb=this.roundBox,s=this.sphere;this.objects=[];
207
- // Bright, calm blue-white studio with a continuous floor and a single clean feature wall.
211
+ // Bright, calm blue-white studio with a continuous floor and a single clean feature wall. The free camera is framed wide enough to keep all 14 stations visible at desktop aspect ratios.
208
212
  this.add(b,0,-.18,5.5,23,.28,34,[.97,.985,1,1]);
209
213
  this.add(b,0,2.70,-6.45,19,3.65,.24,[.10,.52,.89,1]);
210
- this.add(b,0,5.10,-6.30,19,.16,.18,[.98,.99,1,1]);
214
+ this.add(b,0,4.58,-6.30,19,.075,.12,[.985,.99,1,1]);
211
215
  // Wall seams, lower trim, and glass side windows.
212
216
  this.add(b,0,.90,-6.28,18.7,.12,.06,[.08,.42,.78,1]);
213
217
  this.add(b,0,.82,-6.285,18.7,.035,.035,[.25,.67,.94,1]);
@@ -265,6 +269,10 @@ window.Hive3D = (function () {
265
269
  this.add(this.cylinder,x,.78,z-.43,.072,.40,.072,[.32,.39,.45,1]);
266
270
  this.add(b,x,.49,z-.27,.54,.045,.34,[.16,.20,.24,1]);
267
271
  this.add(b,x,screenY-screenHeight*.42,z-.382,screenWidth*.84,.018,.014,[.10,.83,.94,1]);
272
+ // A tiny centered webcam and a restrained power indicator give the monitor a finished hardware read.
273
+ this.add(rb,x,screenY+screenHeight*.5+.064,z-.386,.070,.026,.018,[.025,.035,.045,1]);
274
+ this.add(s,x,screenY+screenHeight*.5+.064,z-.374,.012,.012,.008,[.14,.82,.98,1]);
275
+ this.add(s,x+screenWidth*.39,screenY-screenHeight*.5+.025,z-.375,.010,.010,.008,[.28,.94,.62,1]);
268
276
  this.add(rb,x-.18,.635,z+.25,.60,.035,.20,[.11,.14,.17,1]);
269
277
  this.add(s,x+.40,.648,z+.22,.060,.025,.075,[.055,.065,.075,1]);
270
278
  this.add(b,x+.40,.674,z+.20,.009,.005,.024,[.30,.66,.78,1]);
@@ -279,13 +287,13 @@ window.Hive3D = (function () {
279
287
  for(let k=0;k<3;k++)this.add(b,blueBinderStart+k*.095,.79,z+.34,.075,.35,.18,[[.08,.34,.80,1],[.10,.51,.91,1],[.06,.25,.65,1]][k]);
280
288
  this.add(this.cylinder,x,.08,z+1.10,.075,.28,.075,[.12,.15,.18,1]);
281
289
  this.add(rb,x,.30,z+1.10,.94,.12,.68,[.11,.14,.17,1]);
282
- this.add(rb,x,.50,z+.82,1.02,.60,.15,[.12,.17,.22,1]);
283
- this.add(rb,x,.50,z+.91,.76,.43,.024,[.20,.25,.29,1]);
290
+ this.add(rb,x,.50,z+.82,.88,.60,.15,[.12,.17,.22,1]);
291
+ this.add(rb,x,.50,z+.91,.66,.43,.024,[.20,.25,.29,1]);
284
292
  this.add(rb,x,.42,z+1.10,.48,.12,.12,[.18,.22,.25,1]);
285
293
  // Contrasting graphite upholstery separates the chair from the bee chassis.
286
- this.add(rb,x,.50,z+.90,.78,.43,.050,[.16,.20,.24,1]);
287
- for(const ribY of [.34,.48,.62])this.add(rb,x,ribY,z+.93,.58,.026,.020,[.34,.39,.43,1]);
288
- for(const side of [-1,1])this.add(rb,x+side*.49,.50,z+.93,.035,.48,.025,[.23,.30,.36,1]);
294
+ this.add(rb,x,.50,z+.90,.67,.43,.050,[.16,.20,.24,1]);
295
+ for(const ribY of [.34,.48,.62])this.add(rb,x,ribY,z+.93,.48,.026,.020,[.34,.39,.43,1]);
296
+ for(const side of [-1,1])this.add(rb,x+side*.43,.50,z+.93,.030,.48,.025,[.23,.30,.36,1]);
289
297
  for(const side of [-1,1]){
290
298
  this.add(rb,x+side*.39,.63,z+.96,.10,.075,.38,[.14,.18,.21,1]);
291
299
  this.add(b,x+side*.39,.48,z+.97,.035,.28,.045,[.10,.13,.16,1]);
@@ -293,14 +301,23 @@ window.Hive3D = (function () {
293
301
  for(let k=0;k<5;k++){const a=k*Math.PI*2/5;const wx=x+Math.cos(a)*.48,wz=z+1.10+Math.sin(a)*.40;this.add(b,x+Math.cos(a)*.38,.035,z+1.10+Math.sin(a)*.32,.30,.06,.075,[.08,.10,.12,1],a);this.add(s,wx,.005,wz,.085,.065,.075,[.035,.045,.055,1]);}
294
302
  const by=1.25,bz=z+.98;
295
303
  // Bee assistant: distinct head, dark chassis, striped abdomen, headset and translucent wings.
296
- this.add(s,x,by-.42,bz+.005,.31,.31,.235,[.075,.09,.11,1]);
304
+ this.add(s,x,by-.48,bz+.005,.345,.37,.255,[.075,.09,.11,1]);
297
305
  // Slightly enlarge the band ellipsoid to avoid depth-buffer z-fighting with the shell.
298
- const shellX=.315,shellY=.315,shellZ=.239,shellCenterY=by-.42,shellCenterZ=bz+.005;
306
+ const shellX=.350,shellY=.375,shellZ=.260,shellCenterY=by-.48,shellCenterZ=bz+.005;
299
307
  this.add(this.bandGoldTop,x,shellCenterY,shellCenterZ,shellX,shellY,shellZ,[1,.66,.035,1]);
300
308
  this.add(this.bandGoldBottom,x,shellCenterY,shellCenterZ,shellX,shellY,shellZ,[.99,.56,.018,1]);
301
309
  this.add(this.bandSeparator,x,shellCenterY,shellCenterZ,shellX,shellY,shellZ,[.045,.055,.065,1]);
302
310
  this.add(s,x,by,bz,.405,.37,.345,[1,.70,.045,1]);
303
- // The face points toward the monitor; the rear silhouette carries the headset details.
311
+ // A forward-facing graphite visor makes the assistant read as a character, not a plain yellow sphere.
312
+ // The workstation monitor is on -Z, so the faceplate and optical details sit on that side of the head.
313
+ this.add(rb,x,by+.012,bz-.166,.258,.142,.026,[.035,.055,.070,1]);
314
+ this.add(rb,x,by-.006,bz-.183,.214,.092,.012,[.055,.085,.105,1]);
315
+ for(const side of [-1,1]){
316
+ this.add(s,x+side*.066,by+.022,bz-.194,.045,.046,.020,[.20,.91,.98,1]);
317
+ this.add(s,x+side*.066,by+.029,bz-.205,.017,.018,.008,[.92,.99,1,1]);
318
+ }
319
+ this.add(rb,x,by-.043,bz-.197,.070,.010,.008,[.98,.61,.035,1]);
320
+ // The rear silhouette carries the headset details.
304
321
  for(const side of [-1,1]){
305
322
  this.add(s,x+side*.382,by+.015,bz+.035,.078,.118,.094,[.045,.055,.065,1]);
306
323
  this.add(s,x+side*.407,by+.015,bz+.052,.030,.052,.048,[.99,.68,.045,1]);
@@ -317,26 +334,32 @@ window.Hive3D = (function () {
317
334
  }
318
335
  // Shoulders, articulated elbows and forearms form continuous 3D links down to the keyboard.
319
336
  for(const side of [-1,1]){
320
- const shoulder=[x+side*.32,.99,z+.56],elbow=[x+side*.43,.83,z+.30],wrist=[x+side*.36,.675,z-.12],hand=[x+side*.35,.655,z-.20];
321
- this.addLink(shoulder,elbow,.039,[.19,.22,.25,1]);
322
- this.addLink(elbow,wrist,.034,[.20,.24,.27,1]);
337
+ const shoulder=[x+side*.40,.99,z+.86],elbow=[x+side*.49,.83,z+.63],wrist=[x+(side<0?-.34:.35),.675,z+.31],hand=[x+(side<0?-.30:.34),.655,z+.22];
338
+ this.addLink(shoulder,elbow,.042,[.12,.15,.18,1]);
339
+ this.addLink(elbow,wrist,.036,[.18,.22,.25,1]);
323
340
  this.addLink(wrist,hand,.030,[.96,.63,.035,1]);
324
- this.add(s,shoulder[0],shoulder[1],shoulder[2],.075,.075,.085,[.19,.22,.25,1]);
325
- this.add(s,elbow[0],elbow[1],elbow[2],.065,.065,.065,[.32,.36,.39,1]);
326
- this.add(s,wrist[0],wrist[1],wrist[2],.052,.052,.052,[.24,.29,.32,1]);
341
+ this.add(s,shoulder[0],shoulder[1],shoulder[2],.080,.080,.085,[.14,.17,.20,1]);
342
+ this.add(s,elbow[0],elbow[1],elbow[2],.068,.068,.068,[.26,.30,.33,1]);
343
+ this.add(s,wrist[0],wrist[1],wrist[2],.052,.052,.052,[.21,.25,.28,1]);
327
344
  this.add(rb,hand[0],hand[1],hand[2],.085,.045,.070,[.96,.63,.035,1]);
328
- this.add(s,x+side*.35,.655,z-.20,.070,.035,.055,[.98,.68,.045,1]);
329
- for(let finger=0;finger<3;finger++)this.add(b,x+side*.35+(finger-1)*.026,.638,z-.255,.012,.010,.046,[.22,.29,.35,1]);
345
+ this.add(s,hand[0],hand[1],hand[2],.070,.035,.055,[.98,.68,.045,1]);
346
+ for(let finger=0;finger<3;finger++)this.add(b,hand[0]+(finger-1)*.026,.638,hand[2]-.06,.012,.010,.046,[.22,.29,.35,1]);
330
347
  }
331
348
  // Small cyan service badge on the back of the chassis.
332
349
  this.add(rb,x,by-.585,bz+.205,.13,.068,.018,[.025,.10,.14,1]);
333
350
  this.add(rb,x,by-.585,bz+.218,.082,.016,.010,[.16,.88,.98,1]);
334
- // Smooth glassy oval wings sit at the shoulder line. Sphere geometry keeps their
335
- // silhouette rounded from every camera angle and lets the opaque body occlude them.
336
- this.add(s,x-.43,by-.245,bz+.12,.43,.105,.075,[.58,.84,1,.32],0,.10);
337
- this.add(s,x+.43,by-.245,bz+.12,.43,.105,.075,[.58,.84,1,.32],0,-.10);
338
- this.add(s,x-.40,by-.225,bz+.155,.28,.060,.040,[.93,.99,1,.18],0,.08);
339
- this.add(s,x+.40,by-.225,bz+.155,.28,.060,.040,[.93,.99,1,.18],0,-.08);
351
+ // Compact camera-facing wing panels meet the shoulder and read as distinct blue petals.
352
+ this.add(this.organic,x-.72,by+.39,bz+.32,.64,.62,.045,[.26,.64,.86,.46],0,-.45);
353
+ this.add(this.organic,x+.72,by+.39,bz+.32,.64,.62,.045,[.26,.64,.86,.46],0,.45);
354
+ // Fine cyan veins follow the smaller wing shape without spanning the workstation monitors.
355
+ for(const side of [-1,1]){
356
+ const root=[x+side*.38,by+.29,bz+.325];
357
+ const mid=[x+side*.68,by+.40,bz+.325];
358
+ const tip=[x+side*.98,by+.49,bz+.325];
359
+ this.addLink(root,mid,.0048,[.64,.88,1,.88]);
360
+ this.addLink(mid,tip,.0036,[.64,.88,1,.78]);
361
+ this.addLink([x+side*.64,by+.36,bz+.33],[x+side*.82,by+.46,bz+.33],.0028,[.82,.95,1,.66]);
362
+ }
340
363
 
341
364
  }
342
365
  // Planters and stylized leaves soften the room edges.
@@ -408,7 +431,7 @@ window.Hive3D = (function () {
408
431
  if(!this.gl||!this.active||this.displayMode!=="live3d")return;const gl=this.gl;const w=Math.max(1,this.cssW),h=Math.max(1,this.cssH);const dpr=Math.min(window.devicePixelRatio||1,1.6);
409
432
  const bw=Math.floor(w*dpr),bh=Math.floor(h*dpr);if(this.canvas.width!==bw||this.canvas.height!==bh){this.canvas.width=bw;this.canvas.height=bh;}
410
433
  gl.viewport(0,0,bw,bh);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);gl.useProgram(this.program);
411
- const radius=27.0/this.zoom;const eye=[Math.sin(this.yaw)*radius,3.1+Math.sin(this.pitch)*radius*.58,Math.cos(this.yaw)*radius-1.3];const view=mat4.lookAt(eye,[0,.62,0],[0,1,0]);const proj=mat4.perspective(.40,w/h,.1,70);const vp=mat4.multiply(mat4.multiply(proj,view),mat4.translate(this.panX,this.panY,0));this.viewProj=vp;
434
+ const radius=29.0/this.zoom;const eye=[Math.sin(this.yaw)*radius,3.1+Math.sin(this.pitch)*radius*.52,Math.cos(this.yaw)*radius-1.3];const view=mat4.lookAt(eye,[0,.62,0],[0,1,0]);const proj=mat4.perspective(.46,w/h,.1,80);const vp=mat4.multiply(mat4.multiply(proj,view),mat4.translate(this.panX,this.panY,0));this.viewProj=vp;
412
435
  gl.uniformMatrix4fv(this.uViewProj,false,vp);gl.uniform3f(this.uEyePosition,eye[0],eye[1],eye[2]);
413
436
  this.drawBatch(this.batchBuffers&&this.batchBuffers.opaque);
414
437
  const translucent=this.batchBuffers&&this.batchBuffers.transparent;