@aiyiran/myclaw 1.1.170 → 1.1.172
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/CLAUDE.md +7 -7
- package/assets/myclaw-artifacts.js +1 -0
- package/assets/myclaw-inject.js +99 -48
- package/create_agent.js +65 -11
- package/index.js +37 -21
- package/package.json +1 -1
- package/server/debug_record.py +15 -15
- package/server/sync_workspace.py +2 -2
package/CLAUDE.md
CHANGED
|
@@ -132,8 +132,8 @@ voice-output.js → voice-input.js → myclaw-tts.js
|
|
|
132
132
|
▼ ▼
|
|
133
133
|
服务端(sync_workspace.py,本机 18800 / 远程经 nginx /sync)
|
|
134
134
|
├─ 按 workspace 自行算目录,写两个文件:
|
|
135
|
-
│
|
|
136
|
-
│
|
|
135
|
+
│ __debug-data__.js ← 每次覆盖,window.__DEBUG_DATA__ = {规范化记录}(数据,喂渲染也给 AI 读)
|
|
136
|
+
│ 调试日志.html ← 恒定壳,含 CSS + 前端渲染器,运行时加载 __debug-data__.js 渲染
|
|
137
137
|
▼
|
|
138
138
|
同步循环 → 上传 CDN → 可展示给学生
|
|
139
139
|
```
|
|
@@ -144,7 +144,7 @@ voice-output.js → voice-input.js → myclaw-tts.js
|
|
|
144
144
|
|------|------|------|--------|
|
|
145
145
|
| **probe**(采集侧) | `assets/debug-probe.js` | 在 iframe 内采集现场,`postMessage` 上报 | traceId、workspace、上传、判因 |
|
|
146
146
|
| **collector**(接收侧) | `assets/debug-collector.js` | 父页面收事件、攒记录、关闭时提交 | probe 注入、AI 修复、HTML 模板 |
|
|
147
|
-
| **server**(落库侧) | `server/debug_record.py` | 算目录、校验、normalize、写
|
|
147
|
+
| **server**(落库侧) | `server/debug_record.py` | 算目录、校验、normalize、写 __debug-data__.js + 恒定 HTML 壳 | 采集、CDN 上传(交给同步循环) |
|
|
148
148
|
| **renderer**(渲染侧) | HTML 壳内联 JS(`render_shell_html()` 里) | 读 `window.__DEBUG_DATA__`,渲染小结/线索/回放 | 数据清洗(服务端做) |
|
|
149
149
|
|
|
150
150
|
### 关键设计约束(改之前先读)
|
|
@@ -161,9 +161,9 @@ voice-output.js → voice-input.js → myclaw-tts.js
|
|
|
161
161
|
5. **前端不传文件路径**。前端只传逻辑字段(claw / workspace / entryFile),**服务端自己算目录**。`claw` 不是路径层级,只用于跟本机 `CLAW_NAME` 校验防串写;真实路径是 `{openclaw_root}/{workspace}/debug/`。
|
|
162
162
|
|
|
163
163
|
6. **数据/渲染分离,落盘两个文件**(固定文件名,每次覆盖):
|
|
164
|
-
- `
|
|
165
|
-
-
|
|
166
|
-
- ⚠️ 渲染逻辑(事件转写 / 错误解释 / 线索提取)现在在**前端 JS**(`render_shell_html()` 里),不在 Python。改文案/样式改这里,且因为壳恒定,AI 修 bug 应读 `
|
|
164
|
+
- `__debug-data__.js`:`window.__DEBUG_DATA__ = {...}`,唯一数据源(喂前端渲染 + 给 AI 直接读)。
|
|
165
|
+
- `调试日志.html`:**恒定壳**,内容不随数据变(哈希不变 → 同步不会反复当成更新)。壳内联 CSS + 渲染器 JS,运行时用 `__debug-data__.js?t=<时间戳>` 动态加载绕 CDN 缓存。
|
|
166
|
+
- ⚠️ 渲染逻辑(事件转写 / 错误解释 / 线索提取)现在在**前端 JS**(`render_shell_html()` 里),不在 Python。改文案/样式改这里,且因为壳恒定,AI 修 bug 应读 `__debug-data__.js` 而非 HTML。
|
|
167
167
|
|
|
168
168
|
### 接口
|
|
169
169
|
|
|
@@ -192,4 +192,4 @@ voice-output.js → voice-input.js → myclaw-tts.js
|
|
|
192
192
|
### 已知待办
|
|
193
193
|
|
|
194
194
|
- 整链路端到端实测(单测已过,未在真实 gateway + 同步进程跑通)。
|
|
195
|
-
- `{workspace}/debug
|
|
195
|
+
- `{workspace}/debug/调试日志.html` 会进 `__MY_ARTIFACTS__.json` 作品列表,是否需要从列表过滤、改走专门入口,待定。
|
package/assets/myclaw-inject.js
CHANGED
|
@@ -325,34 +325,20 @@
|
|
|
325
325
|
|
|
326
326
|
// ═══ 3. Debug 按钮 ═══
|
|
327
327
|
|
|
328
|
-
function buildTestDonePrompt() {
|
|
329
|
-
var rec = window.myclawLastDebugRecord;
|
|
328
|
+
function buildTestDonePrompt(keepLogs) {
|
|
330
329
|
var lines = ["我刚才已经完成了测试。"];
|
|
331
330
|
|
|
332
|
-
|
|
333
|
-
// 数据文件即可,不要读同目录的 debug-record.html(那只是给人看的渲染壳)。
|
|
334
|
-
var dataFile = null;
|
|
335
|
-
if (rec) {
|
|
336
|
-
if (rec.debugRecordPath) {
|
|
337
|
-
dataFile = rec.debugRecordPath.replace(/debug-record\.html$/, "debug-data.js");
|
|
338
|
-
} else if (rec.workspace) {
|
|
339
|
-
dataFile = rec.workspace + "/debug/debug-data.js";
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (dataFile) {
|
|
343
|
-
lines.push("请你读取调试数据文件:" + dataFile);
|
|
344
|
-
} else {
|
|
345
|
-
lines.push("请你读取本次测试的调试数据文件 debug/debug-data.js。");
|
|
346
|
-
}
|
|
347
|
-
lines.push("它是一行 `window.__DEBUG_DATA__ = {...}` 的赋值,里面的 events 数组就是完整运行现场;不要去读同目录的 debug-record.html。");
|
|
348
|
-
if (rec && rec.entryFile) {
|
|
349
|
-
lines.push("我刚才测试的文件是:" + rec.entryFile);
|
|
350
|
-
}
|
|
331
|
+
lines.push("请你读取调试数据文件 debug/__debug-data__.js,它是一行 `window.__DEBUG_DATA__ = {...}` 的赋值,里面的 events 数组就是完整运行现场。");
|
|
351
332
|
|
|
352
333
|
lines.push("");
|
|
353
334
|
lines.push("请帮我判断:我们之前的问题是否已经修复。");
|
|
354
335
|
lines.push("如果已经修复,请简单说明你从 events 里看到了什么证据。");
|
|
355
336
|
lines.push("如果还没有修复,请指出 events 里最关键的问题,并建议下一步最小修改。");
|
|
337
|
+
if (keepLogs) {
|
|
338
|
+
lines.push("请保留现有调试日志,不要删除或精简这些 console.log,后面还要继续观察。");
|
|
339
|
+
} else {
|
|
340
|
+
lines.push("如果你确认问题已经修复,请把为了调试临时添加的 console.log / 运行提示清理掉;如果还没修复,请先保留必要日志。");
|
|
341
|
+
}
|
|
356
342
|
|
|
357
343
|
return lines.join("\n");
|
|
358
344
|
}
|
|
@@ -387,9 +373,7 @@
|
|
|
387
373
|
|
|
388
374
|
menu.appendChild(makeItem("查报错", showDebugModal));
|
|
389
375
|
menu.appendChild(makeItem("加日志", showLogModal));
|
|
390
|
-
menu.appendChild(makeItem("看结果",
|
|
391
|
-
sendGeneratedPrompt(buildTestDonePrompt());
|
|
392
|
-
}));
|
|
376
|
+
menu.appendChild(makeItem("看结果", showTestDoneModal));
|
|
393
377
|
|
|
394
378
|
wrap.appendChild(btn);
|
|
395
379
|
wrap.appendChild(menu);
|
|
@@ -417,6 +401,11 @@
|
|
|
417
401
|
if (modal) modal.remove();
|
|
418
402
|
}
|
|
419
403
|
|
|
404
|
+
function closeTestDoneModal() {
|
|
405
|
+
var modal = document.getElementById("myclaw-test-done-modal");
|
|
406
|
+
if (modal) modal.remove();
|
|
407
|
+
}
|
|
408
|
+
|
|
420
409
|
function makeDebugLabel(text) {
|
|
421
410
|
var label = document.createElement("div");
|
|
422
411
|
label.textContent = text;
|
|
@@ -571,30 +560,16 @@
|
|
|
571
560
|
var state = { description: "" };
|
|
572
561
|
|
|
573
562
|
function buildDebugPrompt() {
|
|
574
|
-
var rec = window.myclawLastDebugRecord;
|
|
575
563
|
var lines = [];
|
|
576
564
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
var dataFile = null;
|
|
580
|
-
if (rec) {
|
|
581
|
-
if (rec.debugRecordPath) {
|
|
582
|
-
dataFile = rec.debugRecordPath.replace(/debug-record\.html$/, "debug-data.js");
|
|
583
|
-
} else if (rec.workspace) {
|
|
584
|
-
dataFile = rec.workspace + "/debug/debug-data.js";
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
if (dataFile) {
|
|
588
|
-
lines.push("调试数据文件:" + dataFile);
|
|
589
|
-
lines.push("");
|
|
590
|
-
}
|
|
591
|
-
|
|
565
|
+
lines.push("调试数据文件:debug/__debug-data__.js");
|
|
566
|
+
lines.push("");
|
|
592
567
|
lines.push("学生描述:" + (state.description || "(未填写)"));
|
|
593
568
|
lines.push("");
|
|
594
569
|
lines.push("请从学生描述里提取:我做了什么、发生了什么、我的预期是什么。");
|
|
595
570
|
lines.push("");
|
|
596
571
|
lines.push("如何阅读调试数据(按下面抓重点,别东翻西找):");
|
|
597
|
-
lines.push("1.
|
|
572
|
+
lines.push("1. 读调试数据文件 debug/__debug-data__.js,它是一行 `window.__DEBUG_DATA__ = {...}` 的赋值,里面的 events 数组就是完整运行现场。");
|
|
598
573
|
lines.push("2. 先在 events 里找 runtime-error / unhandledrejection / 以及 payload.level 为 error 的 console 事件,它们是问题的起点。");
|
|
599
574
|
lines.push("3. 抓住报错事件的 message、stack、filename、lineno——直接定位到源码里出错的那个函数/那几行,只看相关代码段,不要通读整个作品文件。");
|
|
600
575
|
lines.push("4. 再倒着看报错之前的 page-enter / click / keydown / console,还原“做了什么 → 触发了什么”的因果链,确认重现路径。");
|
|
@@ -691,15 +666,13 @@
|
|
|
691
666
|
|
|
692
667
|
function buildLogPrompt() {
|
|
693
668
|
var lines = [];
|
|
694
|
-
lines.push("
|
|
695
|
-
lines.push("
|
|
669
|
+
lines.push("目前的信息如下:debug/__debug-data__.js");
|
|
670
|
+
lines.push("我要解决一个问题:" + state.problem);
|
|
696
671
|
lines.push("");
|
|
697
|
-
lines.push("这些调试信息后续会给到你,用来帮我继续解决问题。");
|
|
698
|
-
lines.push("");
|
|
699
|
-
lines.push("请只添加调试信息,不要顺手修改功能逻辑。");
|
|
700
672
|
lines.push("请优先添加 console.log,文字要清楚,适合小学生看懂。");
|
|
701
673
|
lines.push("请在关键步骤都加上信息,比如:有没有点到、进入了哪一步、重要变量现在是多少、为什么没有继续。");
|
|
702
|
-
lines.push("
|
|
674
|
+
lines.push("");
|
|
675
|
+
lines.push("另外,你帮我看一下,如果你能直接找到原因就先尝试修复一下。如果不容易找到,就仅仅加日志。但是,日志一定要加。");
|
|
703
676
|
return lines.join("\n");
|
|
704
677
|
}
|
|
705
678
|
|
|
@@ -787,6 +760,61 @@
|
|
|
787
760
|
problemField.focus();
|
|
788
761
|
}
|
|
789
762
|
|
|
763
|
+
function showTestDoneModal() {
|
|
764
|
+
if (document.getElementById("myclaw-test-done-modal")) return;
|
|
765
|
+
|
|
766
|
+
var overlay = document.createElement("div");
|
|
767
|
+
overlay.id = "myclaw-test-done-modal";
|
|
768
|
+
|
|
769
|
+
var box = document.createElement("div");
|
|
770
|
+
box.className = "myclaw-debug-box myclaw-test-done-box";
|
|
771
|
+
|
|
772
|
+
var header = document.createElement("div");
|
|
773
|
+
header.className = "myclaw-debug-header myclaw-test-done-header";
|
|
774
|
+
header.innerHTML = "<span>✅ 看测试结果</span>";
|
|
775
|
+
var closeX = document.createElement("span");
|
|
776
|
+
closeX.textContent = "✕";
|
|
777
|
+
closeX.className = "myclaw-debug-close";
|
|
778
|
+
closeX.onclick = function () { closeTestDoneModal(); };
|
|
779
|
+
header.appendChild(closeX);
|
|
780
|
+
|
|
781
|
+
var body = document.createElement("div");
|
|
782
|
+
body.className = "myclaw-test-done-body";
|
|
783
|
+
body.textContent = "让 AI 看刚才的测试日志,并判断问题有没有修好。";
|
|
784
|
+
|
|
785
|
+
var footer = document.createElement("div");
|
|
786
|
+
footer.className = "myclaw-test-done-actions";
|
|
787
|
+
|
|
788
|
+
function makeAction(text, className, keepLogs) {
|
|
789
|
+
var btn = document.createElement("button");
|
|
790
|
+
btn.type = "button";
|
|
791
|
+
btn.className = className;
|
|
792
|
+
btn.textContent = text;
|
|
793
|
+
btn.addEventListener("click", function () {
|
|
794
|
+
closeTestDoneModal();
|
|
795
|
+
sendGeneratedPrompt(buildTestDonePrompt(keepLogs));
|
|
796
|
+
});
|
|
797
|
+
return btn;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
var keepBtn = makeAction("检查并保留日志", "myclaw-test-done-keep", true);
|
|
801
|
+
var cleanBtn = makeAction("检查并去掉日志", "myclaw-test-done-clean", false);
|
|
802
|
+
var cancelBtn = document.createElement("button");
|
|
803
|
+
cancelBtn.type = "button";
|
|
804
|
+
cancelBtn.className = "myclaw-test-done-cancel";
|
|
805
|
+
cancelBtn.textContent = "取消";
|
|
806
|
+
cancelBtn.addEventListener("click", function () { closeTestDoneModal(); });
|
|
807
|
+
|
|
808
|
+
footer.appendChild(keepBtn);
|
|
809
|
+
footer.appendChild(cleanBtn);
|
|
810
|
+
footer.appendChild(cancelBtn);
|
|
811
|
+
box.appendChild(header);
|
|
812
|
+
box.appendChild(body);
|
|
813
|
+
box.appendChild(footer);
|
|
814
|
+
overlay.appendChild(box);
|
|
815
|
+
document.body.appendChild(overlay);
|
|
816
|
+
}
|
|
817
|
+
|
|
790
818
|
// ═══ 3.1 Prompt 小助手按钮(实现见 myclaw-iteration.js) ═══
|
|
791
819
|
|
|
792
820
|
function createPromptButton() {
|
|
@@ -1109,7 +1137,7 @@ btn.addEventListener("click", function () {
|
|
|
1109
1137
|
" to { opacity: 1; }",
|
|
1110
1138
|
"}",
|
|
1111
1139
|
/* Debug 提交弹框 */
|
|
1112
|
-
"#myclaw-debug-modal, #myclaw-log-modal {",
|
|
1140
|
+
"#myclaw-debug-modal, #myclaw-log-modal, #myclaw-test-done-modal {",
|
|
1113
1141
|
" position: fixed; inset: 0; z-index: 99999;",
|
|
1114
1142
|
" background: rgba(0,0,0,0.45);",
|
|
1115
1143
|
" display: flex; align-items: center; justify-content: center;",
|
|
@@ -1131,6 +1159,9 @@ btn.addEventListener("click", function () {
|
|
|
1131
1159
|
".myclaw-log-header {",
|
|
1132
1160
|
" background: linear-gradient(135deg,#0ea5e9,#6366f1);",
|
|
1133
1161
|
"}",
|
|
1162
|
+
".myclaw-test-done-header {",
|
|
1163
|
+
" background: linear-gradient(135deg,#10b981,#14b8a6);",
|
|
1164
|
+
"}",
|
|
1134
1165
|
".myclaw-debug-close {",
|
|
1135
1166
|
" cursor: pointer; padding: 2px 8px; border-radius: 4px; font-size: 15px;",
|
|
1136
1167
|
"}",
|
|
@@ -1173,6 +1204,26 @@ btn.addEventListener("click", function () {
|
|
|
1173
1204
|
".myclaw-debug-submit:disabled {",
|
|
1174
1205
|
" cursor: not-allowed;",
|
|
1175
1206
|
"}",
|
|
1207
|
+
".myclaw-test-done-box { width: 460px; }",
|
|
1208
|
+
".myclaw-test-done-body {",
|
|
1209
|
+
" padding: 20px; color: #cdd6f4; font-size: 14px;",
|
|
1210
|
+
" line-height: 1.7; font-family: -apple-system,sans-serif;",
|
|
1211
|
+
"}",
|
|
1212
|
+
".myclaw-test-done-actions {",
|
|
1213
|
+
" display: flex; flex-direction: column; gap: 10px;",
|
|
1214
|
+
" padding: 0 20px 20px;",
|
|
1215
|
+
"}",
|
|
1216
|
+
".myclaw-test-done-actions button {",
|
|
1217
|
+
" padding: 11px 12px; border: none; border-radius: 8px;",
|
|
1218
|
+
" color: #fff; font-size: 14px; font-weight: bold;",
|
|
1219
|
+
" font-family: -apple-system,sans-serif; cursor: pointer;",
|
|
1220
|
+
"}",
|
|
1221
|
+
".myclaw-test-done-keep { background: #10b981; }",
|
|
1222
|
+
".myclaw-test-done-clean { background: #6366f1; }",
|
|
1223
|
+
".myclaw-test-done-cancel { background: #3d3d5c; color: #cdd6f4 !important; }",
|
|
1224
|
+
".myclaw-test-done-keep:hover { background: #059669; }",
|
|
1225
|
+
".myclaw-test-done-clean:hover { background: #4f46e5; }",
|
|
1226
|
+
".myclaw-test-done-cancel:hover { background: #4b4b6a; }",
|
|
1176
1227
|
].join("\n");
|
|
1177
1228
|
document.head.appendChild(style);
|
|
1178
1229
|
}
|
package/create_agent.js
CHANGED
|
@@ -55,11 +55,17 @@ function fail(msg, code = 1) {
|
|
|
55
55
|
* - 非法字符替换为连字符
|
|
56
56
|
* - 去除多余连字符
|
|
57
57
|
*/
|
|
58
|
+
// 与 OpenClaw 本体的 normalizeAgentId 逐字对齐(dist 里的实现):
|
|
59
|
+
// 允许 a-z 0-9 _ - ;连续非法字符压成一个 - ;去首尾 - ;不再压缩内部重复 -
|
|
60
|
+
// 故意不像旧版那样压 `-{2,}`,以保证「标准化后 === 原输入」这个判据
|
|
61
|
+
// 与 OpenClaw 真实落库结果一致,从而准确判断输入是否「天生满足」。
|
|
58
62
|
function normalizeAgentId(raw) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
return (raw == null ? '' : String(raw))
|
|
64
|
+
.trim()
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
67
|
+
.replace(/^-+/g, '')
|
|
68
|
+
.replace(/-+$/g, '');
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
@@ -72,11 +78,47 @@ function validateAgentId(agentId) {
|
|
|
72
78
|
if (agentId.length > 64) {
|
|
73
79
|
fail('Agent ID 太长,请保持在 64 个字符以内。');
|
|
74
80
|
}
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// 放宽到与 OpenClaw 一致:允许下划线,首尾必须是字母/数字
|
|
82
|
+
if (!/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/.test(agentId)) {
|
|
83
|
+
fail('Agent ID 只能包含小写字母、数字、连字符和下划线(不能以连字符/下划线开头或结尾)。');
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
// ── 中文/非法输入时的兜底 key 生成 ──────────────────────────────
|
|
88
|
+
// 格式:MMDD_<三个随机英文单词>_MMSS (北京时间),如 0630_brave-lunar-otter_1207
|
|
89
|
+
// 全小写 + 连字符 + 下划线,天然满足 validateAgentId;带日期/分秒 + 随机词,几乎不可能撞名。
|
|
90
|
+
const FALLBACK_WORDS = [
|
|
91
|
+
'brave', 'lunar', 'otter', 'maple', 'pixel', 'comet', 'river', 'ember',
|
|
92
|
+
'tiger', 'cloud', 'quartz', 'mango', 'panda', 'fox', 'willow', 'cedar',
|
|
93
|
+
'orbit', 'frost', 'sunny', 'cocoa', 'pearl', 'raven', 'lotus', 'noble',
|
|
94
|
+
'jade', 'amber', 'breeze', 'crisp', 'dawn', 'echo', 'flint', 'glow',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
98
|
+
|
|
99
|
+
function generateFallbackKey() {
|
|
100
|
+
// 北京时间(UTC+8),与 buildBirthMessage 保持一致
|
|
101
|
+
const bj = new Date(Date.now() + 8 * 60 * 60 * 1000);
|
|
102
|
+
const MMDD = pad2(bj.getUTCMonth() + 1) + pad2(bj.getUTCDate());
|
|
103
|
+
const MMSS = pad2(bj.getUTCMinutes()) + pad2(bj.getUTCSeconds());
|
|
104
|
+
const pick = () => FALLBACK_WORDS[Math.floor(Math.random() * FALLBACK_WORDS.length)];
|
|
105
|
+
const words = pick() + '-' + pick() + '-' + pick();
|
|
106
|
+
return MMDD + '_' + words + '_' + MMSS;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 命名决策:判断输入是否「天生满足」key 格式 ───────────────────
|
|
110
|
+
// 天生满足(标准化后与原输入完全一致)→ 直接用作 id,不另设显示名。
|
|
111
|
+
// 不满足(含中文/空格/大写等)→ 生成兜底 key,原始输入作为 identity.name(中文显示)。
|
|
112
|
+
function resolveAgentNaming(rawName) {
|
|
113
|
+
const trimmed = (rawName == null ? '' : String(rawName)).trim();
|
|
114
|
+
const normalized = normalizeAgentId(trimmed);
|
|
115
|
+
const isNative = !!normalized && normalized === trimmed;
|
|
116
|
+
if (isNative) {
|
|
117
|
+
return { agentId: normalized, displayName: null, isNative: true };
|
|
118
|
+
}
|
|
119
|
+
return { agentId: generateFallbackKey(), displayName: trimmed, isNative: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
80
122
|
/**
|
|
81
123
|
* 生成北京时间出生消息
|
|
82
124
|
*/
|
|
@@ -138,8 +180,13 @@ function defaultWorkspaceFiles(agentId) {
|
|
|
138
180
|
*/
|
|
139
181
|
function createAgent(rawName, opts) {
|
|
140
182
|
opts = opts || {};
|
|
141
|
-
//
|
|
142
|
-
|
|
183
|
+
// 命名决策:优先用调用方(index.js)已解析好的结果,避免重复生成导致 id 漂移;
|
|
184
|
+
// 否则在此内部解析。
|
|
185
|
+
const naming = opts.agentId
|
|
186
|
+
? { agentId: opts.agentId, displayName: opts.displayName != null ? opts.displayName : null }
|
|
187
|
+
: resolveAgentNaming(rawName);
|
|
188
|
+
const agentId = naming.agentId;
|
|
189
|
+
const displayName = naming.displayName; // 非 null 时写入 identity.name(中文显示名)
|
|
143
190
|
validateAgentId(agentId);
|
|
144
191
|
|
|
145
192
|
// 确定 OpenClaw 配置目录
|
|
@@ -208,12 +255,18 @@ function createAgent(rawName, opts) {
|
|
|
208
255
|
fs.copyFileSync(configPath, backupPath);
|
|
209
256
|
|
|
210
257
|
// 添加 Agent 条目
|
|
211
|
-
|
|
258
|
+
// 顶层 name 保持 ascii id(OpenClaw 不拿它当 UI 显示名);
|
|
259
|
+
// 中文/非法输入时,把原始输入写进 identity.name —— 这才是 UI 真正显示的名字。
|
|
260
|
+
const entry = {
|
|
212
261
|
id: agentId,
|
|
213
262
|
name: agentId,
|
|
214
263
|
workspace: workspaceDir,
|
|
215
264
|
agentDir: agentDir,
|
|
216
|
-
}
|
|
265
|
+
};
|
|
266
|
+
if (displayName) {
|
|
267
|
+
entry.identity = { name: displayName };
|
|
268
|
+
}
|
|
269
|
+
configData.agents.list.push(entry);
|
|
217
270
|
|
|
218
271
|
// 写入配置
|
|
219
272
|
try {
|
|
@@ -242,6 +295,7 @@ function createAgent(rawName, opts) {
|
|
|
242
295
|
return {
|
|
243
296
|
ok: true,
|
|
244
297
|
agentId: agentId,
|
|
298
|
+
displayName: displayName,
|
|
245
299
|
sessionKey: 'agent:' + agentId + ':main',
|
|
246
300
|
workspace: normalizePath(workspaceDir),
|
|
247
301
|
agentDir: normalizePath(agentDir),
|
|
@@ -251,4 +305,4 @@ function createAgent(rawName, opts) {
|
|
|
251
305
|
};
|
|
252
306
|
}
|
|
253
307
|
|
|
254
|
-
module.exports = { createAgent, normalizeAgentId, validateAgentId };
|
|
308
|
+
module.exports = { createAgent, normalizeAgentId, validateAgentId, resolveAgentNaming, generateFallbackKey };
|
package/index.js
CHANGED
|
@@ -335,7 +335,7 @@ function runNew() {
|
|
|
335
335
|
|
|
336
336
|
function askName() {
|
|
337
337
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
338
|
-
rl.question(B + '请输入 Agent
|
|
338
|
+
rl.question(B + '请输入 Agent 名字(中文 / 英文均可): ' + NC, function (answer) {
|
|
339
339
|
rl.close();
|
|
340
340
|
const name = (answer || '').trim();
|
|
341
341
|
if (!name) {
|
|
@@ -343,30 +343,27 @@ function runNew() {
|
|
|
343
343
|
askName();
|
|
344
344
|
return;
|
|
345
345
|
}
|
|
346
|
-
// 预检:必须包含英文字母或数字
|
|
347
|
-
if (!/[a-zA-Z0-9]/.test(name)) {
|
|
348
|
-
console.log(R + '名字必须包含英文或数字,请重新输入(如 my-agent)。' + NC);
|
|
349
|
-
askName();
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
346
|
doCreate(name);
|
|
353
347
|
});
|
|
354
348
|
}
|
|
355
349
|
|
|
356
350
|
function doCreate(input) {
|
|
357
|
-
const {
|
|
351
|
+
const { resolveAgentNaming } = require('./create_agent');
|
|
352
|
+
|
|
353
|
+
// 命名决策:天生满足 key 格式 → 直接用作 id;否则生成 ascii key + 中文显示名
|
|
354
|
+
var naming = resolveAgentNaming(input);
|
|
355
|
+
var agentId = naming.agentId;
|
|
356
|
+
var displayName = naming.displayName;
|
|
358
357
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (!agentId || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agentId)) {
|
|
362
|
-
console.log(R + '名字必须是英文、数字或连字符(如 my-agent),请重新输入。' + NC);
|
|
358
|
+
if (!agentId) {
|
|
359
|
+
console.log(R + '名字无效,请重新输入。' + NC);
|
|
363
360
|
console.log('');
|
|
364
361
|
if (rawName) { process.exit(1); return; }
|
|
365
362
|
askName();
|
|
366
363
|
return;
|
|
367
364
|
}
|
|
368
365
|
|
|
369
|
-
//
|
|
366
|
+
// 重名检查(不依赖 createAgent,避免它内部 process.exit)
|
|
370
367
|
const fs = require('fs');
|
|
371
368
|
const path = require('path');
|
|
372
369
|
const os = require('os');
|
|
@@ -375,23 +372,36 @@ function runNew() {
|
|
|
375
372
|
var configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
376
373
|
var list = (configData.agents && configData.agents.list) || [];
|
|
377
374
|
if (list.some(function (a) { return a && a.id === agentId; })) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
375
|
+
if (naming.isNative) {
|
|
376
|
+
// 用户直接输入的 ascii 名撞了 → 友好提示并重试
|
|
377
|
+
console.log(R + 'Agent "' + agentId + '" 已存在,请换一个名字。' + NC);
|
|
378
|
+
console.log('');
|
|
379
|
+
if (rawName) { process.exit(1); return; }
|
|
380
|
+
askName();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// 生成的兜底 key 撞名(极罕见)→ 重新生成一次
|
|
384
|
+
naming = resolveAgentNaming(input);
|
|
385
|
+
agentId = naming.agentId;
|
|
386
|
+
displayName = naming.displayName;
|
|
383
387
|
}
|
|
384
388
|
} catch (e) {
|
|
385
389
|
// 配置文件读不到就算了,交给 createAgent 处理
|
|
386
390
|
}
|
|
387
391
|
|
|
388
392
|
// 校验通过,开始创建
|
|
389
|
-
|
|
393
|
+
if (displayName) {
|
|
394
|
+
console.log(Y + '正在创建 "' + displayName + '"(ID: ' + agentId + ')...' + NC);
|
|
395
|
+
} else {
|
|
396
|
+
console.log(Y + '正在创建 Agent "' + agentId + '"...' + NC);
|
|
397
|
+
}
|
|
390
398
|
console.log('');
|
|
391
399
|
|
|
392
400
|
var result;
|
|
393
401
|
try {
|
|
394
402
|
const createOpts = customFirstMessage ? { firstMessage: customFirstMessage } : {};
|
|
403
|
+
createOpts.agentId = agentId;
|
|
404
|
+
createOpts.displayName = displayName;
|
|
395
405
|
result = require('./create_agent').createAgent(input, createOpts);
|
|
396
406
|
} catch (err) {
|
|
397
407
|
console.log(R + '创建失败: ' + err.message + NC);
|
|
@@ -399,9 +409,15 @@ function runNew() {
|
|
|
399
409
|
}
|
|
400
410
|
|
|
401
411
|
// 打印结果
|
|
402
|
-
|
|
412
|
+
var shownName = displayName ? (displayName + '(' + agentId + ')') : agentId;
|
|
413
|
+
console.log(G + '✅' + NC + ' Agent "' + shownName + '" 创建完成');
|
|
403
414
|
console.log('');
|
|
404
|
-
|
|
415
|
+
if (displayName) {
|
|
416
|
+
console.log(' ' + G + '✅' + NC + ' 显示名 ' + displayName);
|
|
417
|
+
console.log(' ' + G + '✅' + NC + ' 生成 ID ' + agentId);
|
|
418
|
+
} else {
|
|
419
|
+
console.log(' ' + G + '✅' + NC + ' 名称标准化 ' + agentId);
|
|
420
|
+
}
|
|
405
421
|
console.log(' ' + G + '✅' + NC + ' 环境检查 无冲突');
|
|
406
422
|
console.log(' ' + G + '✅' + NC + ' 创建工作空间 已就绪');
|
|
407
423
|
console.log(' ' + G + '✅' + NC + ' 注册配置 已写入');
|
package/package.json
CHANGED
package/server/debug_record.py
CHANGED
|
@@ -5,20 +5,20 @@ debug_record.py
|
|
|
5
5
|
把前端收集到的 currentDebugRecord 落盘成两个文件,供学生/老师/AI 查看。
|
|
6
6
|
|
|
7
7
|
【架构:数据与渲染分离】
|
|
8
|
-
-
|
|
8
|
+
- __debug-data__.js —— 每次覆盖写。内容是 `window.__DEBUG_DATA__ = {规范化后的记录}`。
|
|
9
9
|
这是**唯一数据源**:既喂前端渲染,也给 AI 直接读。
|
|
10
|
-
-
|
|
11
|
-
|
|
10
|
+
- 调试日志.html —— 恒定的“壳”。包含 CSS + 前端渲染器 JS,运行时动态加载
|
|
11
|
+
__debug-data__.js 并渲染。内容不随数据变化,因此同步循环不会
|
|
12
12
|
反复把它当成“更新”重传。
|
|
13
13
|
|
|
14
14
|
为什么这么拆:
|
|
15
15
|
1. HTML 壳恒定 → 哈希不变 → 不再被同步误判为更新(根除循环上传问题)。
|
|
16
|
-
2. AI 读的是纯数据
|
|
16
|
+
2. AI 读的是纯数据 __debug-data__.js,信噪比高,不掺样式。
|
|
17
17
|
3. 渲染逻辑在前端,改样式不用动 Python、不用重启同步进程。
|
|
18
18
|
|
|
19
19
|
服务端职责(本文件):
|
|
20
20
|
- 接收 / 校验 / normalize 记录
|
|
21
|
-
- 写出
|
|
21
|
+
- 写出 __debug-data__.js 和 调试日志.html
|
|
22
22
|
渲染职责(前端,在 HTML 壳的内联 JS 里):
|
|
23
23
|
- 读 window.__DEBUG_DATA__,渲染小结 / 线索 / 运行回放 / 原始数据
|
|
24
24
|
|
|
@@ -65,7 +65,7 @@ ERROR_EVENT_TYPES = {
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
# =============================================================================
|
|
68
|
-
# 2. 保存层:写
|
|
68
|
+
# 2. 保存层:写 __debug-data__.js + 恒定的 调试日志.html 壳
|
|
69
69
|
# =============================================================================
|
|
70
70
|
|
|
71
71
|
|
|
@@ -74,8 +74,8 @@ def save_debug_record(record: Dict[str, Any], debug_dir: Path) -> Dict[str, str]
|
|
|
74
74
|
落盘调试记录。
|
|
75
75
|
|
|
76
76
|
写两个文件:
|
|
77
|
-
-
|
|
78
|
-
-
|
|
77
|
+
- __debug-data__.js 每次覆盖,内容 = window.__DEBUG_DATA__ = {normalized record}
|
|
78
|
+
- 调试日志.html 恒定壳(内容不变),负责加载并渲染上面的数据
|
|
79
79
|
"""
|
|
80
80
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
81
81
|
|
|
@@ -83,12 +83,12 @@ def save_debug_record(record: Dict[str, Any], debug_dir: Path) -> Dict[str, str]
|
|
|
83
83
|
|
|
84
84
|
# 数据文件:每次覆盖
|
|
85
85
|
data_js = "window.__DEBUG_DATA__ = " + safe_json_for_js(normalized_record) + ";\n"
|
|
86
|
-
data_path = debug_dir / "
|
|
86
|
+
data_path = debug_dir / "__debug-data__.js"
|
|
87
87
|
write_text_atomic(data_path, data_js)
|
|
88
88
|
|
|
89
89
|
# HTML 壳:内容恒定(不含数据)。每次都写没关系——内容一致,
|
|
90
90
|
# 基于内容哈希的同步不会把它当成变更重传。
|
|
91
|
-
html_path = debug_dir / "
|
|
91
|
+
html_path = debug_dir / "调试日志.html"
|
|
92
92
|
write_text_atomic(html_path, render_shell_html())
|
|
93
93
|
|
|
94
94
|
return {"html_path": str(html_path), "data_path": str(data_path)}
|
|
@@ -181,7 +181,7 @@ def handle_save_request(body: Dict[str, Any], openclaw_root: str, server_claw: s
|
|
|
181
181
|
paths = save_debug_record(body, debug_dir)
|
|
182
182
|
|
|
183
183
|
cdn_base = f"https://cdn.yiranlaoshi.com/{claw}"
|
|
184
|
-
debug_record_url = f"{cdn_base}/{workspace}/debug
|
|
184
|
+
debug_record_url = f"{cdn_base}/{workspace}/debug/调试日志.html"
|
|
185
185
|
|
|
186
186
|
record = normalize_record(body)
|
|
187
187
|
|
|
@@ -206,7 +206,7 @@ def assert_safe_id(value: str, field_name: str) -> None:
|
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
# =============================================================================
|
|
209
|
-
# 4. record / event 规范化(数据清洗留在服务端,输出进
|
|
209
|
+
# 4. record / event 规范化(数据清洗留在服务端,输出进 __debug-data__.js)
|
|
210
210
|
# =============================================================================
|
|
211
211
|
|
|
212
212
|
|
|
@@ -391,7 +391,7 @@ def safe_json_for_js(value: Any) -> str:
|
|
|
391
391
|
# 这个函数返回的内容**不随数据变化**,是纯静态的“壳”:
|
|
392
392
|
# - <style> 页面样式
|
|
393
393
|
# - 前端渲染器 JS(事件转写 / 错误解释 / 线索提取 / DOM 渲染)
|
|
394
|
-
# - 运行时动态加载
|
|
394
|
+
# - 运行时动态加载 __debug-data__.js?t=<时间戳> 绕开 CDN 缓存
|
|
395
395
|
# 渲染逻辑是从原 Python 函数(format_replay_event / simplify_error_message /
|
|
396
396
|
# extract_clues / render_*)逐一移植过来的 JS 版本。
|
|
397
397
|
#
|
|
@@ -719,13 +719,13 @@ def render_shell_html() -> str:
|
|
|
719
719
|
|
|
720
720
|
// 动态加载数据,带时间戳绕开 CDN 缓存(壳恒定,数据每次新)
|
|
721
721
|
var s = document.createElement("script");
|
|
722
|
-
s.src = "
|
|
722
|
+
s.src = "__debug-data__.js?t=" + Date.now();
|
|
723
723
|
s.onload = function () {
|
|
724
724
|
try { render(window.__DEBUG_DATA__); }
|
|
725
725
|
catch (e) { document.getElementById("dynamic").textContent = "渲染失败:" + (e && e.message); }
|
|
726
726
|
};
|
|
727
727
|
s.onerror = function () {
|
|
728
|
-
document.getElementById("dynamic").textContent = "调试数据加载失败(
|
|
728
|
+
document.getElementById("dynamic").textContent = "调试数据加载失败(__debug-data__.js)";
|
|
729
729
|
};
|
|
730
730
|
document.head.appendChild(s);
|
|
731
731
|
})();
|
package/server/sync_workspace.py
CHANGED
|
@@ -477,8 +477,8 @@ def _put_file_to_qiniu(token, key, abs_path):
|
|
|
477
477
|
"""统一上传入口。HTML 文件注入 probe 后以字节上传;其余走原始文件上传。"""
|
|
478
478
|
ext = os.path.splitext(abs_path)[1].lower()
|
|
479
479
|
if ext in (".html", ".htm"):
|
|
480
|
-
#
|
|
481
|
-
if os.path.basename(abs_path) != "
|
|
480
|
+
# 调试日志.html 是展示壳,不是学生作品,跳过 probe 注入
|
|
481
|
+
if os.path.basename(abs_path) != "调试日志.html":
|
|
482
482
|
with open(abs_path, "rb") as f:
|
|
483
483
|
data = inject_probe_into_html(f.read())
|
|
484
484
|
return put_data(token, key, data, mime_type="text/html")
|