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 +8 -0
- package/README.md +5 -2
- package/app/core/checkpoints.py +48 -5
- package/app/core/manager.py +1 -0
- package/app/core/openapi.py +29 -0
- package/app/core/pipeline.py +26 -8
- package/app/core/project_memory.py +47 -1
- package/app/core/registry.py +35 -0
- package/app/core/review_requests.py +119 -0
- package/app/core/runner.py +5 -1
- package/app/main.py +43 -0
- package/app/ui/app.js +21 -2
- package/app/ui/hive3d.js +54 -31
- package/app/ui/i18n.js +32 -1
- package/app/ui/index.html +1 -0
- package/package.json +1 -1
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.
|
|
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
|
|
package/app/core/checkpoints.py
CHANGED
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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"
|
package/app/core/manager.py
CHANGED
|
@@ -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")),
|
package/app/core/openapi.py
CHANGED
|
@@ -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}},
|
package/app/core/pipeline.py
CHANGED
|
@@ -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
|
+
}
|
package/app/core/registry.py
CHANGED
|
@@ -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
|
package/app/core/runner.py
CHANGED
|
@@ -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
|
-
|
|
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("第"
|
|
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
|
|
95
|
-
[.38,.13,.
|
|
96
|
-
[-.12,-.255,.
|
|
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*.
|
|
104
|
-
for(const p of outline){
|
|
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,-.
|
|
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,
|
|
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
|
|
283
|
-
this.add(rb,x,.50,z+.91,.
|
|
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,.
|
|
287
|
-
for(const ribY of [.34,.48,.62])this.add(rb,x,ribY,z+.93,.
|
|
288
|
-
for(const side of [-1,1])this.add(rb,x+side*.
|
|
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-.
|
|
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=.
|
|
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
|
-
//
|
|
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*.
|
|
321
|
-
this.addLink(shoulder,elbow,.
|
|
322
|
-
this.addLink(elbow,wrist,.
|
|
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],.
|
|
325
|
-
this.add(s,elbow[0],elbow[1],elbow[2],.
|
|
326
|
-
this.add(s,wrist[0],wrist[1],wrist[2],.052,.052,.052,[.
|
|
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,
|
|
329
|
-
for(let finger=0;finger<3;finger++)this.add(b,
|
|
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
|
-
//
|
|
335
|
-
|
|
336
|
-
this.add(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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=
|
|
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
|
-
"实现 → 验证
|
|
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