codebee 0.1.104 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
4
4
  `<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
5
5
  README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
6
6
 
7
+ ## v0.1.105(2026-10-12)
8
+
9
+ - **claude CLI 输出上限 16000→32000**:全局评审读 40 万字级书稿(约 28 万 token 输入)时按旧值反复续烧 23 分钟后报「response exceeded the 16000 output token maximum」三连挂(实案:单次 $3 零产出);32000=claude CLI 官方默认档,`setdefault` 语义保留——用户显式配置不覆盖
10
+ - **治理契约:外部人工评审请求通道**:新增 append-only 评审请求台账(requested→assigned→approved/changes_requested/dismissed/expired 幂等流转)与 `/api/reviews`、`/api/reviews/<id>/transition` 路由,外部智能体可按契约提交待人工评审的产物;配套 `/api/capabilities` 能力清单与 `/api/tasks/<id>/memory/preview` 任务记忆注入预览接口
11
+ - **项目记忆注入预算封顶 20000**:注入预算配置值超过 20000 时按 20000 收敛,防止过大预算挤占提示词(含回归测试锁定)
12
+ - **蜂巢 3D 蜜蜂翅膀精修**:翅膀更纤薄透明、减淡青雾、脉络几何强化,助手蜂手臂/键盘手部姿态与腕端高光对齐,视觉契约测试全程护航(约 40 笔打磨)
13
+ - 英文界面补齐「会话续跑」能力标签词条
14
+
7
15
  ## v0.1.104(2026-10-11)
8
16
 
9
17
  - **连载「一直重写不达标」根因防线(马甲指纹三道防线)**:「落笔入册」「整段连排」类跨章复用此前只能靠评审判死,而重写用的是同一作者模型、长出来的还是同族指纹——循环烧钱永不达标。现把防线前移到生成侧:①**章稿落盘确定性查重闸**:整段(≥40 字)/整句(≥12 字)/完整对白(≥6 字)逐字重复检测,阈值按真实事故章校准,白描短句复沓不误伤;带重复的章稿直接打回,同作者重试与换将的提示词自动追加消重指令(同提示重跑只会长出同族事故);②**起草负样例注入**:近 4 章实际开笔/收束摘录随提示词下发(`__REUSE_GUARD__`),跨章句式同款从评审判据前移成起草约束;③**大纲轮换纪律**:开笔方式与收束形态批内硬性轮换。查重证据自动进评审面可见。生效范围:发版后的新批连载
package/README.md CHANGED
@@ -25,9 +25,12 @@ Trae Agent、OpenClaw……),提供
25
25
  ---
26
26
 
27
27
  <!-- relnotes:start -->
28
- ### 最新版更新内容(v0.1.104)
28
+ ### 最新版更新内容(v0.1.105)
29
29
 
30
- - 连载新防线:跨章复用「马甲指纹」从评审判死前移到起草拦截——落盘查重闸(段落/整句/对白逐字重复直接打回)+ 近章开笔摘录作负样例注入 + 大纲开笔收束批内轮换,重写不再空转
30
+ - 修复 40 万字级书稿全局评审被 claude 输出上限 16000 顶爆三连挂:上限提到 32000(用户显式配置不覆盖)
31
+ - 新增治理契约:外部人工评审请求通道(append-only 台账+幂等流转)+ 能力清单/记忆注入预览接口
32
+ - 蜂巢 3D 蜜蜂翅膀精修:更纤薄透明、脉络清晰、手部姿态自然
33
+ - 项目记忆注入预算封顶 20000 防挤占
31
34
  <!-- relnotes:end -->
32
35
 
33
36
 
@@ -40,6 +40,20 @@ def _hash(value):
40
40
  return hashlib.sha256(raw.encode("utf-8")).hexdigest()
41
41
 
42
42
 
43
+ def _side_effects(value):
44
+ """Normalize side-effect labels without persisting arbitrary tool output."""
45
+ if isinstance(value, str):
46
+ value = [value]
47
+ if not isinstance(value, (list, tuple, set)):
48
+ return []
49
+ out = []
50
+ for item in value:
51
+ label = str(item or "").strip()[:80]
52
+ if label and label not in out:
53
+ out.append(label)
54
+ return out[:20]
55
+
56
+
43
57
  def _append(row):
44
58
  _DIR.mkdir(parents=True, exist_ok=True)
45
59
  with _file().open("a", encoding="utf-8") as handle:
@@ -48,7 +62,8 @@ def _append(row):
48
62
 
49
63
 
50
64
  def start(run_id, step, role, prompt="", *, input_data=None, attempt=1,
51
- idempotency_key=""):
65
+ idempotency_key="", tool_call=None, conversation_ref="",
66
+ side_effects=None):
52
67
  """Record a step start without persisting prompt contents."""
53
68
  now = time.time()
54
69
  try:
@@ -61,6 +76,14 @@ def start(run_id, step, role, prompt="", *, input_data=None, attempt=1,
61
76
  "attempt": attempt, "idempotency_key": str(idempotency_key or "")[:160],
62
77
  "prompt_sha256": _hash(prompt), "input_sha256": _hash(input_data),
63
78
  "created_at": now}
79
+ if tool_call is not None:
80
+ row["tool_call_digest"] = _hash(tool_call)
81
+ if conversation_ref:
82
+ row["conversation_ref"] = str(conversation_ref)[:200]
83
+ normalized_effects = _side_effects(side_effects)
84
+ if normalized_effects:
85
+ row["side_effects"] = normalized_effects
86
+ row["repropose_required"] = bool(tool_call is not None or normalized_effects)
64
87
  with _LOCK:
65
88
  try:
66
89
  _append(row)
@@ -73,13 +96,24 @@ def finish(run_id, step, status, *, error="", output=None, metadata=None):
73
96
  status = str(status or "unknown").strip().lower()
74
97
  if status not in _TERMINAL:
75
98
  status = "unknown"
99
+ safe_metadata = {}
100
+ for key, value in (metadata or {}).items():
101
+ name = str(key)[:40]
102
+ if name == "side_effects":
103
+ safe_metadata[name] = _side_effects(value)
104
+ elif name == "repropose_required":
105
+ safe_metadata[name] = bool(value)
106
+ elif isinstance(value, (str, int, float, bool)):
107
+ safe_metadata[name] = value
76
108
  row = {"event_id": "cp-" + secrets.token_hex(10), "event": "finish",
77
109
  "run_id": str(run_id or "")[:80], "step": int(step),
78
110
  "status": status, "error": scrub_text(error, limit=500),
79
- "output_sha256": _hash(output), "metadata": {
80
- str(k)[:40]: v for k, v in (metadata or {}).items()
81
- if isinstance(v, (str, int, float, bool))
82
- }, "created_at": time.time()}
111
+ "output_sha256": _hash(output), "metadata": safe_metadata,
112
+ "created_at": time.time()}
113
+ if "side_effects" in safe_metadata:
114
+ row["side_effects"] = safe_metadata["side_effects"]
115
+ if "repropose_required" in safe_metadata:
116
+ row["repropose_required"] = safe_metadata["repropose_required"]
83
117
  with _LOCK:
84
118
  try:
85
119
  _append(row)
@@ -135,6 +169,15 @@ def replay_preview(run_id):
135
169
  if key in starts:
136
170
  item.setdefault("prompt_sha256", starts[key].get("prompt_sha256"))
137
171
  item.setdefault("attempt", starts[key].get("attempt", 1))
172
+ metadata = item.get("metadata") or {}
173
+ for field in ("tool_call_digest", "conversation_ref", "side_effects",
174
+ "repropose_required"):
175
+ if field not in item and field in metadata:
176
+ item[field] = metadata[field]
177
+ item.setdefault("side_effects", [])
178
+ item["repropose_required"] = bool(
179
+ item.get("repropose_required") or item.get("side_effects")
180
+ or item.get("tool_call_digest"))
138
181
  steps.append(item)
139
182
  unknown = next((x for x in steps if x.get("status") in ("unknown", "running")
140
183
  or x.get("event") != "finish"
@@ -2374,6 +2374,7 @@ def catalog_view():
2374
2374
  "model": read_model(e),
2375
2375
  "orch_kind": (e.get("orch") or {}).get("kind"),
2376
2376
  "orch_enabled": orch_enabled,
2377
+ "capabilities": registry.capability_manifest(e),
2377
2378
  "update": update_info(e),
2378
2379
  "has_install": bool(e.get("install")),
2379
2380
  "has_upgrade": bool(e.get("upgrade")),
@@ -68,6 +68,13 @@ def document():
68
68
  "CommandAuth": {"type": "object", "properties": {
69
69
  "mode": {"type": "string", "enum": ["legacy", "restricted"]},
70
70
  "pending": {"type": "array"}, "rules": {"type": "array"}, "audit": {"type": "array"}}},
71
+ "ReviewRequest": {"type": "object", "required": ["id", "run_id", "status"],
72
+ "properties": {
73
+ "id": {"type": "string"}, "run_id": {"type": "string"},
74
+ "summary": {"type": "string"}, "paths": {"type": "array", "items": {"type": "string"}},
75
+ "channel": {"type": "string"},
76
+ "status": {"type": "string", "enum": ["requested", "assigned", "approved", "changes_requested", "dismissed", "expired"]},
77
+ "reviewer": {"type": "string"}, "reason": {"type": "string"}}},
71
78
  }
72
79
 
73
80
  def body(schema):
@@ -146,6 +153,28 @@ def document():
146
153
  "/api/command-auth/rules/{rule_id}/revoke": {
147
154
  "post": {"summary": "撤销命令授权规则", "security": security,
148
155
  "responses": {"200": ok, "400": errors["400"], "401": errors["401"], "404": errors["404"]}}},
156
+ "/api/reviews": {
157
+ "get": {"summary": "读取外部人工评审请求", "security": security,
158
+ "responses": {"200": response("评审请求列表", {"type": "object", "properties": {
159
+ "reviews": {"type": "array", "items": ref("ReviewRequest")}}}), "401": errors["401"]}},
160
+ "post": {"summary": "创建外部人工评审请求", "security": security,
161
+ "requestBody": body({"type": "object", "required": ["run_id"], "properties": {
162
+ "run_id": {"type": "string", "maxLength": 100}, "summary": {"type": "string", "maxLength": 1000},
163
+ "paths": {"oneOf": [{"type": "string"}, {"type": "array", "items": {"type": "string", "maxLength": 500}}]},
164
+ "channel": {"type": "string", "maxLength": 200}}}),
165
+ "responses": {"200": response("评审请求", {"type": "object", "properties": {"review": ref("ReviewRequest")}}), **errors}}},
166
+ "/api/reviews/{review_id}/transition": {
167
+ "post": {"summary": "推进外部人工评审状态", "security": security,
168
+ "requestBody": body({"type": "object", "required": ["action"], "properties": {
169
+ "action": {"type": "string", "enum": ["assign", "approve", "changes_requested", "dismiss", "expire"]},
170
+ "reviewer": {"type": "string", "maxLength": 200}, "reason": {"type": "string", "maxLength": 1000}}}),
171
+ "responses": {"200": response("评审请求", {"type": "object", "properties": {"review": ref("ReviewRequest")}}), **errors}}},
172
+ "/api/capabilities": {"get": {"summary": "读取智能体能力清单", "security": security,
173
+ "responses": {"200": response("能力清单", {"type": "object"}), "401": errors["401"]}}},
174
+ "/api/tasks/{task_id}/memory/preview": {"get": {"summary": "预览项目记忆注入内容", "security": security,
175
+ "parameters": [{"name": "q", "in": "query", "schema": {"type": "string", "maxLength": 500}},
176
+ {"name": "cap", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 20000}}],
177
+ "responses": {"200": response("记忆注入预览", {"type": "object"}), "401": errors["401"], "404": errors["404"], "409": errors["409"]}}},
149
178
  "/api/retrieval/search": {"get": {"summary": "本地检索", "security": security,
150
179
  "parameters": [{"name": "q", "in": "query", "required": True,
151
180
  "schema": {"type": "string", "maxLength": 500}},
@@ -445,11 +445,20 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
445
445
  model=agent.get("model") or "",
446
446
  provider=_binding_brief(agent))
447
447
  try:
448
+ checkpoint_effects = [] if readonly else ["workspace_files"]
449
+ if agent.get("mode") != "mock":
450
+ checkpoint_effects.append("external_agent")
451
+ if agent.get("kind") in ("acp", "mcp"):
452
+ checkpoint_effects.append("external_protocol")
448
453
  checkpoints.start(
449
454
  run_id, step["n"], role, prompt,
450
455
  input_data={"workdir": workdir, "readonly": readonly,
451
456
  "agent": agent.get("id")},
452
- attempt=len((store.get_run(run_id) or {}).get("steps") or []))
457
+ attempt=len((store.get_run(run_id) or {}).get("steps") or []),
458
+ tool_call={"name": "agent_step", "agent": agent.get("id"),
459
+ "role": role, "readonly": bool(readonly)},
460
+ conversation_ref=resume or "",
461
+ side_effects=checkpoint_effects)
453
462
  except Exception:
454
463
  pass
455
464
  start = time.time()
@@ -475,7 +484,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
475
484
  "tokens": 0, "usage": None, "error": sandbox_error,
476
485
  "raw": {"exit_code": None, "sandbox_blocked": True},
477
486
  "kind": agent.get("kind", "generic"), "model": agent.get("model")}
478
- _finish_step_result(run_id, step, res, role, agent, start)
487
+ _finish_step_result(run_id, step, res, role, agent, start,
488
+ readonly=readonly)
479
489
  return res
480
490
  if dead_binding:
481
491
  from .error_codes import ErrorCode
@@ -484,7 +494,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
484
494
  "error_code": ErrorCode.ENV_BLOCK,
485
495
  "raw": {"exit_code": None}, "kind": agent.get("kind", "generic"),
486
496
  "model": agent.get("model")}
487
- _finish_step_result(run_id, step, res, role, agent, start)
497
+ _finish_step_result(run_id, step, res, role, agent, start,
498
+ readonly=readonly)
488
499
  return res
489
500
  if agent.get("mode") == "mock":
490
501
  time.sleep(0.3)
@@ -507,7 +518,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
507
518
  "raw": {"exit_code": None, "repeat_stop": True},
508
519
  "kind": agent.get("kind", "generic"),
509
520
  "model": agent.get("model")}
510
- _finish_step_result(run_id, step, res, role, agent, start)
521
+ _finish_step_result(run_id, step, res, role, agent, start,
522
+ readonly=readonly)
511
523
  return res
512
524
  # 运行中指挥:drain 用户追加的指令/附件,注入本步(守门拦截时不 drain,
513
525
  # 消息留给下一个真实步骤,不空耗);role/step_n 作送达回执
@@ -529,7 +541,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
529
541
  "error": git_checkpoint.get("error") or "文件检查点准备失败",
530
542
  "raw": {"exit_code": None, "checkpoint_blocked": True},
531
543
  "kind": agent.get("kind", "generic"), "model": agent.get("model")}
532
- _finish_step_result(run_id, step, res, role, agent, start)
544
+ _finish_step_result(run_id, step, res, role, agent, start,
545
+ readonly=readonly)
533
546
  return res
534
547
  res = _spawn_step(session_run_id=run_id, role=role, agent=agent,
535
548
  prompt=effective_prompt, workdir=workdir, readonly=readonly,
@@ -561,7 +574,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
561
574
  pass
562
575
  # 先收尾再查取消:取消时进程已被 run_process 杀停,若先抛 Cancelled,
563
576
  # 步骤记录会永远停在「运行中」变僵尸(与 _run_verify 的顺序对齐)
564
- _finish_step_result(run_id, step, res, role, agent, start)
577
+ _finish_step_result(run_id, step, res, role, agent, start,
578
+ readonly=readonly)
565
579
  if (res.get("raw") or {}).get("deadline_exceeded"):
566
580
  store.update_run(run_id, expected_status="running", status="timeout",
567
581
  ended_at=_now(), error="任务总时限已到")
@@ -883,7 +897,7 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
883
897
  return res
884
898
 
885
899
 
886
- def _finish_step_result(run_id, step, res, role, agent, start):
900
+ def _finish_step_result(run_id, step, res, role, agent, start, *, readonly=None):
887
901
  """step 收尾:记录 + 用量 + 运行时断言(5F)。"""
888
902
  # 被取消杀停的步骤如实记「已取消」;超时被杀记「超时」——都只是步骤
889
903
  # 显示层的细分,run 级仍是 failed,善后路径(重试/自动续跑)不变
@@ -924,7 +938,10 @@ def _finish_step_result(run_id, step, res, role, agent, start):
924
938
  checkpoints.finish(
925
939
  run_id, step["n"], checkpoint_status,
926
940
  error=res.get("error") or "", output=res.get("text") or "",
927
- metadata={"role": role, "agent": agent.get("id") or ""})
941
+ metadata={"role": role, "agent": agent.get("id") or "",
942
+ "repropose_required": True,
943
+ "side_effects": ([] if readonly is True else ["workspace_files"]) +
944
+ (["external_agent"] if agent.get("mode") != "mock" else [])})
928
945
  except Exception:
929
946
  pass
930
947
  # 错误台账:失败/超时各记一条结构化记录(遥测与诊断包的数据源)。
@@ -1040,6 +1057,7 @@ __FILES__
1040
1057
  __CONTEXT__
1041
1058
 
1042
1059
  ## 要求
1060
+ - 思考先行:动手前先读懂相关代码、明确验收标准再改;不读懂就动手是大改与返工的主因。
1043
1061
  - 修改要最小化、聚焦任务目标;不要无关重构。
1044
1062
  - 反过度防御(借鉴 HERO:只约束你「提出」的方案,不约束你「检查」的范围)——
1045
1063
  不要给没要求的数据加哈希/校验和;不要为内部脚本铺层层边界分支;
@@ -179,7 +179,7 @@ def _relevance(row, terms):
179
179
  def active_text(workdir, *, cap=4000, query="", now=None):
180
180
  stamp = float(now if now is not None else time.time())
181
181
  try:
182
- cap = max(0, int(cap))
182
+ cap = min(20000, max(0, int(cap)))
183
183
  except (TypeError, ValueError):
184
184
  cap = 4000
185
185
  heading = "## 已批准项目记忆(内容已校验,过期条目不注入)\n\n"
@@ -204,3 +204,49 @@ def active_text(workdir, *, cap=4000, query="", now=None):
204
204
  used += cost
205
205
  text = "\n\n".join(selected)
206
206
  return (heading + text + "\n\n") if text else ""
207
+
208
+
209
+ def injection_preview(workdir, *, cap=4000, query="", now=None):
210
+ """Explain exactly which approved memory entries would be injected.
211
+
212
+ The returned text is the same bounded content as ``active_text``; metadata
213
+ is deliberately separate so callers can render provenance without putting
214
+ extra bookkeeping into the model context.
215
+ """
216
+ stamp = float(now if now is not None else time.time())
217
+ try:
218
+ cap = min(20000, max(0, int(cap)))
219
+ except (TypeError, ValueError):
220
+ cap = 4000
221
+ heading = "## 已批准项目记忆(内容已校验,过期条目不注入)\n\n"
222
+ body_cap = max(0, cap - len(heading) - 2)
223
+ terms = _query_terms(query)
224
+ candidates = []
225
+ for row in list_entries(workdir, now=stamp):
226
+ if row.get("status") != "approved" or row.get("expired") or not row.get("verified"):
227
+ continue
228
+ block = "### %s\n%s" % (row.get("title") or "项目记忆", row.get("content") or "")
229
+ candidates.append((row, block))
230
+ candidates.sort(key=lambda item: (_relevance(item[0], terms),
231
+ item[0].get("feedback_score", 0),
232
+ item[0].get("created_at", 0)), reverse=True)
233
+ selected, used = [], 0
234
+ for row, block in candidates:
235
+ cost = len(block) + (2 if selected else 0)
236
+ if used + cost > body_cap:
237
+ continue
238
+ selected.append((row, block))
239
+ used += cost
240
+ return {
241
+ "text": (heading + "\n\n".join(block for _row, block in selected) + "\n\n"
242
+ if selected else ""),
243
+ "entries": [{"id": row.get("id"), "title": row.get("title"),
244
+ "source_run": row.get("source_run", ""),
245
+ "version": int(row.get("version") or 1),
246
+ "content_sha256": row.get("content_sha256", ""),
247
+ "feedback_score": int(row.get("feedback_score") or 0)}
248
+ for row, _block in selected],
249
+ "budget": {"cap": cap, "used": len((heading + "\n\n".join(
250
+ block for _row, block in selected) + "\n\n") if selected else "")},
251
+ "query": str(query or "")[:500],
252
+ }
@@ -22,6 +22,41 @@ MOCK_AGENTS = [
22
22
  _LOCK = threading.RLock()
23
23
 
24
24
 
25
+ def capability_manifest(entry):
26
+ """Return one explicit, UI-safe capability contract for a backend.
27
+
28
+ The manifest is descriptive only: it never grants a backend extra access.
29
+ Execution remains governed by the task contract, sandbox policy and command
30
+ authorization gates.
31
+ """
32
+ entry = entry or {}
33
+ orch = entry.get("orch") or {}
34
+ kind = str(orch.get("kind") or "")
35
+ transport = "acp" if kind == "acp" or str(entry.get("id") or "").startswith("acp-") else "cli"
36
+ resume = bool(orch.get("resume_argv_template"))
37
+ if kind == "mock":
38
+ transport = "mock"
39
+ resume = False
40
+ platforms = entry.get("platforms")
41
+ if not isinstance(platforms, list) or not platforms:
42
+ platforms = ["windows", "linux", "macos"]
43
+ platforms = [str(x) for x in platforms if str(x) in ("windows", "linux", "macos")]
44
+ if not platforms:
45
+ platforms = ["windows", "linux", "macos"]
46
+ declared = entry.get("capabilities") if isinstance(entry.get("capabilities"), dict) else {}
47
+ return {
48
+ "id": str(entry.get("id") or ""),
49
+ "transport": transport,
50
+ "resume": resume,
51
+ "sandbox": str(declared.get("sandbox") or "task-policy"),
52
+ "approval": str(declared.get("approval") or "task-contract"),
53
+ "mcp": bool(declared.get("mcp", kind in ("acp", "mcp"))),
54
+ "platforms": platforms,
55
+ "model_tiers": [str(x) for x in (declared.get("model_tiers") or
56
+ ["budget", "standard", "premium"])[:8]],
57
+ }
58
+
59
+
25
60
  def load_enabled():
26
61
  try:
27
62
  return json.loads(paths.ENABLED_FILE.read_text(encoding="utf-8"))
@@ -0,0 +1,119 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Append-only external human-review requests with idempotent transitions."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import secrets
7
+ import threading
8
+ import time
9
+ from pathlib import Path
10
+
11
+ from . import paths
12
+
13
+ _LOCK = threading.RLock()
14
+ _FILE = paths.DATA_DIR / "review-requests.jsonl"
15
+ _STATUSES = {"requested", "assigned", "approved", "changes_requested", "dismissed", "expired"}
16
+ _TRANSITIONS = {
17
+ "assign": ("requested", "assigned"),
18
+ "approve": ("assigned", "approved"),
19
+ "changes_requested": ("assigned", "changes_requested"),
20
+ "dismiss": (("requested", "assigned", "changes_requested"), "dismissed"),
21
+ "expire": (("requested", "assigned", "changes_requested"), "expired"),
22
+ }
23
+
24
+
25
+ def _normalize_paths(value):
26
+ if isinstance(value, str):
27
+ value = [value]
28
+ if not isinstance(value, (list, tuple, set)):
29
+ return []
30
+ return [str(item).strip()[:500] for item in value
31
+ if str(item or "").strip()][:200]
32
+
33
+
34
+ def _read():
35
+ latest = {}
36
+ try:
37
+ lines = Path(_FILE).read_text(encoding="utf-8").splitlines()[-5000:]
38
+ except OSError:
39
+ return latest
40
+ for line in lines:
41
+ try:
42
+ row = json.loads(line)
43
+ except (TypeError, ValueError):
44
+ continue
45
+ if isinstance(row, dict) and row.get("id"):
46
+ latest[row["id"]] = row
47
+ return latest
48
+
49
+
50
+ def list_requests():
51
+ """Return the latest version of each request in append-only storage."""
52
+ with _LOCK:
53
+ return list(_read().values())
54
+
55
+
56
+ def _append(row):
57
+ path = Path(_FILE)
58
+ path.parent.mkdir(parents=True, exist_ok=True)
59
+ with path.open("a", encoding="utf-8") as handle:
60
+ handle.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
61
+ handle.flush()
62
+
63
+
64
+ def route_channel(paths_changed, rules=None, default_channel=""):
65
+ """Select the channel with the most matching changed files."""
66
+ paths_changed = _normalize_paths(paths_changed)
67
+ rules = [r for r in (rules or []) if isinstance(r, dict)]
68
+ best, best_count = "", 0
69
+ import fnmatch
70
+ for rule in rules:
71
+ channel = str(rule.get("channel") or "").strip()
72
+ patterns = _normalize_paths(rule.get("paths"))
73
+ count = sum(1 for path in paths_changed if any(fnmatch.fnmatchcase(path, str(p)) for p in patterns))
74
+ if channel and count > best_count:
75
+ best, best_count = channel, count
76
+ return best or str(default_channel or "")[:200]
77
+
78
+
79
+ def create(run_id, *, summary="", paths=None, channel="", now=None):
80
+ if not str(run_id or "").strip():
81
+ raise ValueError("run_id 不能为空")
82
+ stamp = float(now if now is not None else time.time())
83
+ row = {"id": "review-" + secrets.token_hex(10), "run_id": str(run_id or "")[:100],
84
+ "summary": str(summary or "")[:1000],
85
+ "paths": _normalize_paths(paths),
86
+ "channel": str(channel or "")[:200], "status": "requested",
87
+ "version": 1, "created_at": stamp, "updated_at": stamp}
88
+ with _LOCK:
89
+ _append(row)
90
+ return row
91
+
92
+
93
+ def get(request_id):
94
+ with _LOCK:
95
+ return _read().get(str(request_id or ""))
96
+
97
+
98
+ def transition(request_id, action, *, reviewer="", reason="", now=None):
99
+ with _LOCK:
100
+ row = _read().get(str(request_id or ""))
101
+ if not row:
102
+ raise KeyError("评审请求不存在")
103
+ action = str(action or "").strip().lower()
104
+ if action not in _TRANSITIONS:
105
+ raise ValueError("不支持的评审状态操作")
106
+ allowed, target = _TRANSITIONS[action]
107
+ if row.get("status") == target:
108
+ return row
109
+ if row.get("status") not in allowed:
110
+ raise ValueError("当前评审状态不允许此操作")
111
+ stamp = float(now if now is not None else time.time())
112
+ updated = dict(row, status=target, version=int(row.get("version") or 1) + 1,
113
+ updated_at=stamp)
114
+ if reviewer:
115
+ updated["reviewer"] = str(reviewer)[:200]
116
+ if reason:
117
+ updated["reason"] = str(reason)[:1000]
118
+ _append(updated)
119
+ return updated
@@ -1775,7 +1775,11 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
1775
1775
  bash = find_git_bash()
1776
1776
  if bash:
1777
1777
  base_env.setdefault("CLAUDE_CODE_GIT_BASH_PATH", bash)
1778
- base_env.setdefault("CLAUDE_CODE_MAX_OUTPUT_TOKENS", "16000")
1778
+ # 32000=claude CLI 官方默认(自定义网关模型常见上限):旧值 16000 在
1779
+ # 全局评审读 40 万字书稿时被输出顶爆——CLI 按 16000/轮反复续烧 23.5
1780
+ # 分钟、$3 后报 "response exceeded the 16000 output token maximum"
1781
+ # (2026-10-11 实案×3);setdefault 保留:用户显式配置不覆盖
1782
+ base_env.setdefault("CLAUDE_CODE_MAX_OUTPUT_TOKENS", "32000")
1779
1783
  sid = (resume or "").strip() if agent.get("mode") == "real" else ""
1780
1784
  if sid and kind == "generic" and not agent.get("resume_argv_template"):
1781
1785
  return {"ok": False, "text": "", "json": None, "cost_usd": 0.0, "tokens": 0,
package/app/main.py CHANGED
@@ -358,6 +358,22 @@ class Handler(BaseHTTPRequestHandler):
358
358
  return self._json(200, {"entries": project_memory.list_entries(task.get("workdir"))})
359
359
  except (OSError, ValueError) as exc:
360
360
  return self._json(409, {"error": str(exc)})
361
+ m = re.match(r"^/api/tasks/([^/]+)/memory/preview$", path)
362
+ if m:
363
+ task = store.get_task(m.group(1))
364
+ if not task:
365
+ return self._json(404, {"error": "not found"})
366
+ from core import project_memory
367
+ qs = parse_qs(urlparse(self.path).query)
368
+ try:
369
+ return self._json(200, project_memory.injection_preview(
370
+ task.get("workdir"), query=(qs.get("q") or [""])[0],
371
+ cap=(qs.get("cap") or [4000])[0]))
372
+ except (OSError, ValueError) as exc:
373
+ return self._json(409, {"error": str(exc)})
374
+ if path == "/api/reviews":
375
+ from core import review_requests
376
+ return self._json(200, {"reviews": review_requests.list_requests()})
361
377
  if path == "/api/backend-profiles":
362
378
  from core import backend_profiles
363
379
  try:
@@ -419,6 +435,10 @@ class Handler(BaseHTTPRequestHandler):
419
435
  if path == "/api/catalog":
420
436
  return self._json(200, {"catalog": manager.catalog_view(),
421
437
  "checking": manager.updates_checking()})
438
+ if path == "/api/capabilities":
439
+ from core import registry
440
+ return self._json(200, {"capabilities": [
441
+ registry.capability_manifest(e) for e in catalog.load()]})
422
442
  if path == "/api/sessions":
423
443
  from core import sessions
424
444
  return self._json(200, {"sessions": sessions.scan(
@@ -968,6 +988,29 @@ class Handler(BaseHTTPRequestHandler):
968
988
  return self._json(200, {"ok": ok, "output": out})
969
989
  if path == "/api/tasks/clarify":
970
990
  return self._api_task_clarify()
991
+ if path == "/api/reviews":
992
+ from core import review_requests
993
+ body = self._body() or {}
994
+ try:
995
+ row = review_requests.create(
996
+ body.get("run_id"), summary=body.get("summary"),
997
+ paths=body.get("paths"), channel=body.get("channel"))
998
+ return self._json(200, {"review": row})
999
+ except (OSError, TypeError, ValueError) as exc:
1000
+ return self._json(400, {"error": str(exc)})
1001
+ m = re.match(r"^/api/reviews/([^/]+)/transition$", path)
1002
+ if m:
1003
+ from core import review_requests
1004
+ body = self._body() or {}
1005
+ try:
1006
+ row = review_requests.transition(
1007
+ unquote(m.group(1)), body.get("action"),
1008
+ reviewer=body.get("reviewer"), reason=body.get("reason"))
1009
+ return self._json(200, {"review": row})
1010
+ except KeyError as exc:
1011
+ return self._json(404, {"error": str(exc)})
1012
+ except (OSError, TypeError, ValueError) as exc:
1013
+ return self._json(400, {"error": str(exc)})
971
1014
  m = re.match(r"^/api/tasks/([^/]+)/memory$", path)
972
1015
  if m:
973
1016
  task = store.get_task(m.group(1))
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/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;
package/app/ui/i18n.js CHANGED
@@ -1024,7 +1024,7 @@
1024
1024
  "代码引擎": "Code engine",
1025
1025
  "评审引擎": "Review engine",
1026
1026
  "直连引擎": "Direct engine",
1027
- "实现 → 验证 → 评审": "Implement → verify → review",
1027
+ "实现 → 验证 →(复杂任务进)评审": "Implement → verify → review (complex tasks)",
1028
1028
  "起草 → 多维评审 → 门禁": "Draft → multi-dim review → gate",
1029
1029
  "大纲 → 逐章起草评审 → 合并(可续跑)": "Outline → per-chapter draft & review → merge (resumable)",
1030
1030
  "≈{0} tokens · {1}次": "≈{0} tokens · {1} runs",
@@ -1484,6 +1484,7 @@
1484
1484
  "已写入:": "Saved: ",
1485
1485
  "仅管理": "Management only",
1486
1486
  "参与编排": "Join orchestration",
1487
+ "支持续会话": "Session resume",
1487
1488
  "版本 ": "Version ",
1488
1489
  " → <b>": " → <b>",
1489
1490
  "(已是最新版本,无需升级)": " (already up to date — no upgrade needed)",
@@ -3219,6 +3220,36 @@
3219
3220
  "网址": "Address",
3220
3221
  "在系统浏览器打开": "Open in system browser",
3221
3222
  "CodeBee 浏览器": "CodeBee Browser",
3223
+ // —— EN 词条收尾补全(2026-10-12 落地班 G 专项:t() 字面量对账复跑发现的
3224
+ // 22 键缺口——回放审批 15 + ACP 后端配置 7,功能入库时漏配,英文界面中文裸奔;
3225
+ // 「第{0}次」系占位复合键,替代「第/次」分字拼接,中文渲染不变)——
3226
+ "有用": "Helpful",
3227
+ "无用": "Not helpful",
3228
+ "项目记忆反馈失败:": "Project memory feedback failed: ",
3229
+ "步骤回放预览": "Step replay preview",
3230
+ "未命名步骤": "Unnamed step",
3231
+ "第{0}次": "attempt {0}",
3232
+ "仅显示最近 200 个步骤,共 ": "Showing last 200 steps of ",
3233
+ "所有步骤均已完成,可人工复核后继续": "All steps completed — review manually to continue",
3234
+ "无法安全自动回放:": "Cannot auto-replay safely: ",
3235
+ "需要人工核对": "manual review required",
3236
+ "批准一次": "Approve once",
3237
+ "暂无待批准命令": "No commands pending approval",
3238
+ "授权状态加载失败:": "Failed to load auth status: ",
3239
+ "授权策略保存失败:": "Failed to save auth policy: ",
3240
+ "命令授权操作失败:": "Command auth action failed: ",
3241
+ "ACP 活动": "ACP activity",
3242
+ "暂无 ACP 后端": "No ACP backends yet",
3243
+ "ACP 配置加载失败:": "Failed to load ACP config: ",
3244
+ "参数必须是 JSON 数组": "Argument must be a JSON array",
3245
+ "ACP 后端已保存": "ACP backend saved",
3246
+ "ACP 保存失败:": "Failed to save ACP backend: ",
3247
+ "删除 ACP 失败:": "Failed to delete ACP backend: ",
3248
+ "查看注入预览": "Preview injection",
3249
+ "本次注入预览": "Injection preview",
3250
+ "暂无可注入记忆": "No memory would be injected",
3251
+ "已使用 {0}/{1} 字符": "Using {0}/{1} characters",
3252
+ "项目记忆预览失败:": "Memory preview failed: ",
3222
3253
  };
3223
3254
 
3224
3255
  // ---------- 工具 ----------
package/app/ui/index.html CHANGED
@@ -744,6 +744,7 @@
744
744
  <div class="rd-checkpoints-head">
745
745
  <strong data-i18n="项目记忆治理">项目记忆治理</strong>
746
746
  <button id="rd-project-memory-refresh" type="button" class="ghost small" onclick="loadProjectMemory()" data-i18n="刷新记忆">刷新记忆</button>
747
+ <button id="rd-project-memory-preview" type="button" class="ghost small" onclick="previewProjectMemory()" data-i18n="查看注入预览">查看注入预览</button>
747
748
  </div>
748
749
  <div id="rd-project-memory-body" class="hint"></div>
749
750
  </section>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {