@aiyiran/myclaw 1.1.24 → 1.1.26
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/settings.local.json +25 -1
- package/assets/myclaw-artifacts.js +1070 -126
- package/assets/myclaw-inject.js +913 -121
- package/delete_agents.js +268 -0
- package/index.js +361 -20
- package/package.json +1 -1
- package/patches/patch-manifest.json +10 -0
- package/server/sync_workspace.py +444 -14
- package/skills/yiran-course-template-pipeline/README.md +127 -0
- package/skills/yiran-course-template-pipeline/SKILL.md +65 -0
- package/skills/yiran-course-template-pipeline/assets/a100-teacher.example.html +66 -0
- package/skills/yiran-course-template-pipeline/assets/student-template.html +64 -0
- package/skills/yiran-course-template-pipeline/assets/teacher-portrait-demo.html +105 -0
- package/skills/yiran-course-template-pipeline/assets/teacher-task-view.html +110 -0
- package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2651-demo/347/224/237/346/210/220.md +92 -0
- package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2652-student/347/224/237/346/210/220.md +115 -0
- package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2653-teacher/347/224/237/346/210/220.md +131 -0
- package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2654-/346/211/223/345/214/205/350/220/275/347/233/230.md +77 -0
- package/skills/yiran-course-template-pipeline/references/student-example.json +38 -0
- package/skills/yiran-course-template-pipeline/references/student-fields.md +195 -0
- package/skills/yiran-course-template-pipeline/references/student-scaffold.json +34 -0
- package/skills/yiran-course-template-pipeline/references/teacher-fields.md +265 -0
- package/skills/yiran-course-template-pipeline/references/teacher-scaffold.json +25 -0
- package/skills/yiran-course-template-pipeline/scripts/build_template_views.py +125 -0
- package/skills/yiran-course-template-pipeline/scripts/move_template_task.py +59 -0
- package/skills/yiran-course-template-pipeline/scripts/render_student_page.py +52 -0
- package/skills/yiran-course-template-pipeline/scripts/render_teacher_view.py +108 -0
- package/skills/yiran-playground-template-use/SKILL.md +105 -0
- package/skills/yiran-playground-template-use/prompts/remix-handoff.txt +11 -0
- package/skills/yiran-playground-template-use/scripts/build_template_index.py +103 -0
- package/skills/yiran-playground-template-use/scripts/deploy_template.py +34 -0
- package/skills/yiran-playground-template-use/scripts/deploy_to_workspace.py +211 -0
- package/skills/yiran-playground-template-use/scripts/prepare_playgrounds.py +39 -0
- package/skills/yiran-playground-template-use/scripts/query_template.py +171 -0
- package/skills/yiran-playground-template-use/scripts/run_playgrounds_flow.py +44 -0
- package/skills/yiran-playground-template-use/scripts/start_tui_handoff.py +77 -0
- package/skills/yiran-playground-template-use/search-agent-prompt.md +39 -0
- package/skills/yiran-playground-template-use/template-index.json +136 -0
- package/skills/yiran-playground-template-use/template-index.md +38 -0
- package/skills/yiran-playground-template-use/templates/a100_/347/273/231/344/276/235/347/204/266/350/200/201/345/270/210/350/256/276/350/256/241/344/270/200/344/270/252AI/347/224/273/345/203/217/__demo__.html +140 -0
- package/skills/yiran-playground-template-use/templates/a100_/347/273/231/344/276/235/347/204/266/350/200/201/345/270/210/350/256/276/350/256/241/344/270/200/344/270/252AI/347/224/273/345/203/217/__student-view__.html +64 -0
- package/skills/yiran-playground-template-use/templates/a100_/347/273/231/344/276/235/347/204/266/350/200/201/345/270/210/350/256/276/350/256/241/344/270/200/344/270/252AI/347/224/273/345/203/217/__student__.json +38 -0
- package/skills/yiran-playground-template-use/templates/a100_/347/273/231/344/276/235/347/204/266/350/200/201/345/270/210/350/256/276/350/256/241/344/270/200/344/270/252AI/347/224/273/345/203/217/__teacher-view__.html +52 -0
- package/skills/yiran-playground-template-use/templates/a100_/347/273/231/344/276/235/347/204/266/350/200/201/345/270/210/350/256/276/350/256/241/344/270/200/344/270/252AI/347/224/273/345/203/217/__teacher__.json +36 -0
- package/skills/yiran-playground-template-use/templates/a100_/347/273/231/344/276/235/347/204/266/350/200/201/345/270/210/350/256/276/350/256/241/344/270/200/344/270/252AI/347/224/273/345/203/217/index.html +61 -0
- package/skills/yiran-playground-template-use/templates/a101_/345/201/2323/345/274/240/345/220/214/344/270/273/351/242/230/345/233/276/347/211/207/__demo__.html +131 -0
- package/skills/yiran-playground-template-use/templates/a101_/345/201/2323/345/274/240/345/220/214/344/270/273/351/242/230/345/233/276/347/211/207/__student-view__.html +64 -0
- package/skills/yiran-playground-template-use/templates/a101_/345/201/2323/345/274/240/345/220/214/344/270/273/351/242/230/345/233/276/347/211/207/__student__.json +34 -0
- package/skills/yiran-playground-template-use/templates/a101_/345/201/2323/345/274/240/345/220/214/344/270/273/351/242/230/345/233/276/347/211/207/__teacher-view__.html +52 -0
- package/skills/yiran-playground-template-use/templates/a101_/345/201/2323/345/274/240/345/220/214/344/270/273/351/242/230/345/233/276/347/211/207/__teacher__.json +34 -0
- package/skills/yiran-playground-template-use/templates/a103_/345/201/232/344/270/200/344/270/252/344/273/213/347/273/215/351/241/265/351/235/242/__demo__.html +77 -0
- package/skills/yiran-playground-template-use/templates/a103_/345/201/232/344/270/200/344/270/252/344/273/213/347/273/215/351/241/265/351/235/242/__student-view__.html +64 -0
- package/skills/yiran-playground-template-use/templates/a103_/345/201/232/344/270/200/344/270/252/344/273/213/347/273/215/351/241/265/351/235/242/__student__.json +38 -0
- package/skills/yiran-playground-template-use/templates/a103_/345/201/232/344/270/200/344/270/252/344/273/213/347/273/215/351/241/265/351/235/242/__teacher-view__.html +52 -0
- package/skills/yiran-playground-template-use/templates/a103_/345/201/232/344/270/200/344/270/252/344/273/213/347/273/215/351/241/265/351/235/242/__teacher__.json +34 -0
- package/skills/yiran-playground-template-use/templates/b100_/345/201/232/344/270/200/344/270/252/346/214/211/351/222/256/351/241/265/351/235/242/__demo__.html +162 -0
- package/skills/yiran-playground-template-use/templates/b100_/345/201/232/344/270/200/344/270/252/346/214/211/351/222/256/351/241/265/351/235/242/__student-view__.html +64 -0
- package/skills/yiran-playground-template-use/templates/b100_/345/201/232/344/270/200/344/270/252/346/214/211/351/222/256/351/241/265/351/235/242/__student__.json +34 -0
- package/skills/yiran-playground-template-use/templates/b100_/345/201/232/344/270/200/344/270/252/346/214/211/351/222/256/351/241/265/351/235/242/__teacher-view__.html +52 -0
- package/skills/yiran-playground-template-use/templates/b100_/345/201/232/344/270/200/344/270/252/346/214/211/351/222/256/351/241/265/351/235/242/__teacher__.json +34 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/__demo__.html +180 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/__student-view__.html +64 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/__student__.json +38 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/__teacher-view__.html +52 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/__teacher__.json +41 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/demo.html +180 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271/index.html +121 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271//345/260/217/347/273/204/345/220/211/347/245/245/347/211/251_26.png +0 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271//345/260/217/347/273/204/345/233/276/345/275/242/346/240/207/345/277/227_83.png +0 -0
- package/skills/yiran-playground-template-use/templates/c100_/347/273/231/345/260/217/347/273/204/344/275/234/345/223/201/345/201/232/344/270/200/346/254/241/345/260/217/344/277/256/346/224/271//347/217/255/347/272/247/345/260/217/347/273/204/345/276/275/347/253/240_47.png +0 -0
- package/skills/yiran-skill-media/SKILL.md +6 -15
- package/skills/yiran-skill-media/scripts/generate.py +47 -18
- package/skills/yiran-skill-media/scripts/generation_log.json +1 -56
- package/skills/yiran-skill-media/scripts/providers/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/__init__.cpython-37.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/jimeng_image.cpython-37.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/jimeng_video.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/jimeng_video.cpython-37.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_image.cpython-37.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_music.cpython-37.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_video.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_video.cpython-37.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/vapi_image.cpython-37.pyc +0 -0
package/server/sync_workspace.py
CHANGED
|
@@ -51,10 +51,136 @@ SYNC_ARTIFACTS_JSON_TO_OSS = False
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
# 忽略的目录名(路径中只要包含这些目录段就跳过)
|
|
54
|
-
IGNORE_DIR_PATTERNS = {'.git', 'node_modules', '__pycache__', '.myclaw'}
|
|
54
|
+
IGNORE_DIR_PATTERNS = {'.git', 'node_modules', '__pycache__', '.myclaw', '__history__', 'history'}
|
|
55
55
|
|
|
56
56
|
# 忽略的文件名
|
|
57
|
-
IGNORE_FILE_PATTERNS = {'.myclaw-sync.pid', '.DS_Store', '.gitignore', '.gitkeep'}
|
|
57
|
+
IGNORE_FILE_PATTERNS = {'.myclaw-sync.pid', '.DS_Store', '.gitignore', '.gitkeep', 'history.json'}
|
|
58
|
+
|
|
59
|
+
# 历史版本目录名和索引文件名
|
|
60
|
+
HISTORY_DIR = "__history__"
|
|
61
|
+
HISTORY_FILENAME = "history.json"
|
|
62
|
+
|
|
63
|
+
# 回滚压制表:{ abs_path: 写入时间戳 }
|
|
64
|
+
# 回滚时往 workspace 写文件会触发 on_modified,用此表跳过快照,避免产生多余版本
|
|
65
|
+
_rollback_suppressed: dict = {}
|
|
66
|
+
_ROLLBACK_SUPPRESS_TTL = 3.0 # 秒
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_rollback_suppressed(abs_path: str) -> bool:
|
|
70
|
+
ts = _rollback_suppressed.get(abs_path)
|
|
71
|
+
if ts and time.time() - ts < _ROLLBACK_SUPPRESS_TTL:
|
|
72
|
+
return True
|
|
73
|
+
_rollback_suppressed.pop(abs_path, None)
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load_history_json(history_json_path: str, workspace_id: str) -> dict:
|
|
78
|
+
if os.path.exists(history_json_path):
|
|
79
|
+
try:
|
|
80
|
+
with open(history_json_path, "r", encoding="utf-8") as f:
|
|
81
|
+
return json.load(f)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
return _empty_history(workspace_id)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def save_history_version(workspace_id: str, relative_path: str, file_path: str):
|
|
88
|
+
"""把当前文件内容作为新快照写入 __history__/vN/,并更新 history.json。"""
|
|
89
|
+
base_path = get_openclaw_path()
|
|
90
|
+
history_base = os.path.join(base_path, workspace_id, HISTORY_DIR)
|
|
91
|
+
history_json_path = os.path.join(history_base, HISTORY_FILENAME)
|
|
92
|
+
|
|
93
|
+
os.makedirs(history_base, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
history_data = _load_history_json(history_json_path, workspace_id)
|
|
96
|
+
|
|
97
|
+
next_ver = history_data.get("next_version", 1)
|
|
98
|
+
ver_dir_name = f"v{next_ver}"
|
|
99
|
+
|
|
100
|
+
# 复制当前文件内容到 __history__/vN/<relative_path>
|
|
101
|
+
rel_parts = relative_path.replace("\\", "/").split("/")
|
|
102
|
+
dest_path = os.path.join(history_base, ver_dir_name, *rel_parts)
|
|
103
|
+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
104
|
+
try:
|
|
105
|
+
with open(file_path, "rb") as src, open(dest_path, "wb") as dst:
|
|
106
|
+
dst.write(src.read())
|
|
107
|
+
except Exception as e:
|
|
108
|
+
print(f"[history] 快照写入失败: {e}")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# 更新 next_version、records、current_versions
|
|
112
|
+
history_data["next_version"] = next_ver + 1
|
|
113
|
+
history_data["records"].append({
|
|
114
|
+
"version": next_ver,
|
|
115
|
+
"version_dir": ver_dir_name,
|
|
116
|
+
"path": relative_path,
|
|
117
|
+
"snapshot_at": now_iso()
|
|
118
|
+
})
|
|
119
|
+
if "current_versions" not in history_data:
|
|
120
|
+
history_data["current_versions"] = {}
|
|
121
|
+
history_data["current_versions"][relative_path] = next_ver
|
|
122
|
+
|
|
123
|
+
with open(history_json_path, "w", encoding="utf-8") as f:
|
|
124
|
+
json.dump(history_data, f, ensure_ascii=False, indent=2)
|
|
125
|
+
|
|
126
|
+
print(f"[history] 已保存快照 {ver_dir_name}: {relative_path}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def rollback_to_version(workspace_id: str, relative_path: str, version_dir: str):
|
|
130
|
+
"""
|
|
131
|
+
将指定历史版本的文件复制回 workspace,更新 current_versions。
|
|
132
|
+
返回错误信息字符串,成功返回 None。
|
|
133
|
+
"""
|
|
134
|
+
base_path = get_openclaw_path()
|
|
135
|
+
history_base = os.path.join(base_path, workspace_id, HISTORY_DIR)
|
|
136
|
+
history_json_path = os.path.join(history_base, HISTORY_FILENAME)
|
|
137
|
+
|
|
138
|
+
# 安全检查
|
|
139
|
+
if ".." in version_dir or ".." in relative_path:
|
|
140
|
+
return "invalid path"
|
|
141
|
+
|
|
142
|
+
rel_parts = relative_path.replace("\\", "/").split("/")
|
|
143
|
+
src_path = os.path.join(history_base, version_dir, *rel_parts)
|
|
144
|
+
if not os.path.exists(src_path):
|
|
145
|
+
return f"历史文件不存在: {version_dir}/{relative_path}"
|
|
146
|
+
|
|
147
|
+
workspace_path = os.path.join(base_path, workspace_id)
|
|
148
|
+
dest_path = os.path.join(workspace_path, *rel_parts)
|
|
149
|
+
|
|
150
|
+
# 压制本次写盘触发的 on_modified 快照
|
|
151
|
+
_rollback_suppressed[dest_path] = time.time()
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
155
|
+
with open(src_path, "rb") as src, open(dest_path, "wb") as dst:
|
|
156
|
+
dst.write(src.read())
|
|
157
|
+
except Exception as e:
|
|
158
|
+
_rollback_suppressed.pop(dest_path, None)
|
|
159
|
+
return f"文件写入失败: {e}"
|
|
160
|
+
|
|
161
|
+
# 更新 current_versions(从 version_dir 解析版本号)
|
|
162
|
+
history_data = _load_history_json(history_json_path, workspace_id)
|
|
163
|
+
if "current_versions" not in history_data:
|
|
164
|
+
history_data["current_versions"] = {}
|
|
165
|
+
try:
|
|
166
|
+
ver_num = int(version_dir.lstrip("v"))
|
|
167
|
+
except ValueError:
|
|
168
|
+
ver_num = -1
|
|
169
|
+
history_data["current_versions"][relative_path] = ver_num
|
|
170
|
+
with open(history_json_path, "w", encoding="utf-8") as f:
|
|
171
|
+
json.dump(history_data, f, ensure_ascii=False, indent=2)
|
|
172
|
+
|
|
173
|
+
print(f"[history] 已回滚 {relative_path} → {version_dir}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _empty_history(workspace_id: str) -> dict:
|
|
178
|
+
return {
|
|
179
|
+
"workspace_id": workspace_id,
|
|
180
|
+
"next_version": 1,
|
|
181
|
+
"current_versions": {},
|
|
182
|
+
"records": []
|
|
183
|
+
}
|
|
58
184
|
|
|
59
185
|
|
|
60
186
|
def _path_has_ignored_dir(filepath):
|
|
@@ -217,7 +343,12 @@ def file_gen(path, method):
|
|
|
217
343
|
init_config(space_id, relative_path, method="delete")
|
|
218
344
|
print(f"🗑️ 已删除配置记录: {relative_path}")
|
|
219
345
|
else:
|
|
220
|
-
#
|
|
346
|
+
# ── 历史版本:把当前内容作为快照存档(仅覆盖行为,非配置文件)──
|
|
347
|
+
# 回滚写盘触发的 on_modified 跳过,避免产生多余版本
|
|
348
|
+
if os.path.basename(path) != ARTIFACTS_FILENAME and not _is_rollback_suppressed(path):
|
|
349
|
+
save_history_version(space_id, relative_path, path)
|
|
350
|
+
|
|
351
|
+
# ── 上传到 OSS ───────────────────────────────────────────
|
|
221
352
|
try:
|
|
222
353
|
token = q.upload_token(bucket_name, key, 3600)
|
|
223
354
|
# 直接使用 put_file_v2 上传文件路径,避免读取到空内容时导致 SDK 报错缺少 data 参数
|
|
@@ -440,28 +571,28 @@ def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=No
|
|
|
440
571
|
|
|
441
572
|
print(f"[Fork] 找到 {len(files)} 个文件,开始下载...")
|
|
442
573
|
success = 0
|
|
574
|
+
downloaded_rels = []
|
|
443
575
|
for item in files:
|
|
444
576
|
key = item['key']
|
|
445
|
-
# 去掉前缀,得到相对路径(CDN key 是 URL 编码过的,decode 成实际文件名)
|
|
446
577
|
rel_path = unquote(key[len(prefix):])
|
|
447
578
|
if not rel_path:
|
|
448
579
|
continue
|
|
449
580
|
local_path = os.path.join(target_dir, rel_path)
|
|
450
581
|
if _download_from_cdn(key, local_path):
|
|
451
582
|
success += 1
|
|
583
|
+
downloaded_rels.append(rel_path)
|
|
452
584
|
print(f" ✓ {rel_path}")
|
|
453
585
|
|
|
454
586
|
print(f"[Fork] 完成: 成功 {success}/{len(files)} 个文件")
|
|
455
587
|
|
|
456
|
-
#
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
print(f"[Fork] 警告: 入口文件不存在,跳过 mtime 更新: {entry_rel_path}")
|
|
588
|
+
# 写入 JSON 记录,入口文件最后写,确保其 updated_at 最新排第一
|
|
589
|
+
entry_decoded = unquote(entry_rel_path) if entry_rel_path else None
|
|
590
|
+
for rel in downloaded_rels:
|
|
591
|
+
if rel != entry_decoded:
|
|
592
|
+
init_config(new_workspace, rel, method="add")
|
|
593
|
+
if entry_decoded and entry_decoded in downloaded_rels:
|
|
594
|
+
init_config(new_workspace, entry_decoded, method="add")
|
|
595
|
+
print(f"[Fork] 入口文件已置顶: {entry_decoded}")
|
|
465
596
|
|
|
466
597
|
# 文件到位后再发分析消息,确保 AI 能读到 fork 的内容
|
|
467
598
|
fork_message = (
|
|
@@ -529,7 +660,7 @@ def start_fork(remote_url):
|
|
|
529
660
|
# 内嵌 HTTP API 服务
|
|
530
661
|
# ═══════════════════════════════════════════════════════════════
|
|
531
662
|
|
|
532
|
-
API_PORT = int(os.environ.get("MYCLAW_API_PORT", "
|
|
663
|
+
API_PORT = int(os.environ.get("MYCLAW_API_PORT", "18800"))
|
|
533
664
|
|
|
534
665
|
|
|
535
666
|
class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
@@ -588,6 +719,14 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
588
719
|
return self._handle_resync(params)
|
|
589
720
|
elif path == '/api/fork':
|
|
590
721
|
return self._handle_fork()
|
|
722
|
+
elif path == '/api/history/rollback':
|
|
723
|
+
return self._handle_rollback()
|
|
724
|
+
elif path == '/api/agents/trash':
|
|
725
|
+
return self._handle_agents_trash()
|
|
726
|
+
elif path == '/api/template/copy':
|
|
727
|
+
return self._handle_template_copy()
|
|
728
|
+
elif path == '/api/template/deploy':
|
|
729
|
+
return self._handle_template_deploy()
|
|
591
730
|
else:
|
|
592
731
|
self._send_json({"error": "not found"}, 404)
|
|
593
732
|
|
|
@@ -598,11 +737,67 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
598
737
|
return self._handle_config()
|
|
599
738
|
elif path == '/api/artifacts':
|
|
600
739
|
return self._handle_artifacts(params)
|
|
740
|
+
elif path == '/api/history':
|
|
741
|
+
return self._handle_history(params)
|
|
742
|
+
elif path == '/api/history/file':
|
|
743
|
+
return self._handle_history_file(params)
|
|
601
744
|
elif path == '/api/fork/status':
|
|
602
745
|
return self._handle_fork_status(params)
|
|
746
|
+
elif path == '/api/agents':
|
|
747
|
+
return self._handle_agents_list()
|
|
748
|
+
elif path == '/api/file':
|
|
749
|
+
return self._handle_file(params)
|
|
603
750
|
else:
|
|
604
751
|
self._send_json({"error": "not found"}, 404)
|
|
605
752
|
|
|
753
|
+
def _handle_agents_list(self):
|
|
754
|
+
"""GET /api/agents → 复用 mc list-agents --json"""
|
|
755
|
+
try:
|
|
756
|
+
ret = subprocess.run(
|
|
757
|
+
['mc', 'list-agents', '--json'],
|
|
758
|
+
capture_output=True, timeout=15, text=True
|
|
759
|
+
)
|
|
760
|
+
if ret.returncode == 0:
|
|
761
|
+
self._send_json_raw(ret.stdout.strip().encode('utf-8'))
|
|
762
|
+
else:
|
|
763
|
+
self._send_json({'error': ret.stderr.strip() or 'mc list-agents 执行失败'}, 500)
|
|
764
|
+
except FileNotFoundError:
|
|
765
|
+
self._send_json({'error': 'mc 命令未找到,请确认 MyClaw 已安装'}, 500)
|
|
766
|
+
except subprocess.TimeoutExpired:
|
|
767
|
+
self._send_json({'error': 'mc list-agents 超时'}, 500)
|
|
768
|
+
except Exception as e:
|
|
769
|
+
self._send_json({'error': str(e)}, 500)
|
|
770
|
+
|
|
771
|
+
def _handle_agents_trash(self):
|
|
772
|
+
"""POST /api/agents/trash Body: {"paths": [...]} → 复用 mc trash-paths"""
|
|
773
|
+
try:
|
|
774
|
+
length = int(self.headers.get('Content-Length', 0))
|
|
775
|
+
body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
|
|
776
|
+
except Exception:
|
|
777
|
+
self._send_json({'error': '请求体解析失败'}, 400)
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
paths = [p for p in body.get('paths', []) if p and isinstance(p, str)]
|
|
781
|
+
if not paths:
|
|
782
|
+
self._send_json({'error': '缺少有效的 paths 参数'}, 400)
|
|
783
|
+
return
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
ret = subprocess.run(
|
|
787
|
+
['mc', 'trash-paths', '--json'] + paths,
|
|
788
|
+
capture_output=True, timeout=30, text=True
|
|
789
|
+
)
|
|
790
|
+
if ret.returncode in (0, 1): # 1 = 部分失败,仍返回 JSON
|
|
791
|
+
self._send_json_raw(ret.stdout.strip().encode('utf-8'))
|
|
792
|
+
else:
|
|
793
|
+
self._send_json({'error': ret.stderr.strip() or 'mc trash-paths 执行失败'}, 500)
|
|
794
|
+
except FileNotFoundError:
|
|
795
|
+
self._send_json({'error': 'mc 命令未找到,请确认 MyClaw 已安装'}, 500)
|
|
796
|
+
except subprocess.TimeoutExpired:
|
|
797
|
+
self._send_json({'error': 'mc trash-paths 超时'}, 500)
|
|
798
|
+
except Exception as e:
|
|
799
|
+
self._send_json({'error': str(e)}, 500)
|
|
800
|
+
|
|
606
801
|
def _handle_config(self):
|
|
607
802
|
"""GET /api/config → 返回 claw 名称和 CDN base_url"""
|
|
608
803
|
self._send_json({"claw": claw, "base_url": BASE_URL})
|
|
@@ -628,6 +823,95 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
628
823
|
except Exception as e:
|
|
629
824
|
self._send_json({"error": str(e)}, 500)
|
|
630
825
|
|
|
826
|
+
def _handle_history(self, params):
|
|
827
|
+
"""GET /api/history?workspace=xxx → 返回 __history__/history.json"""
|
|
828
|
+
workspace = params.get('workspace', [''])[0]
|
|
829
|
+
if not workspace:
|
|
830
|
+
self._send_json({"error": "missing workspace param"}, 400)
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
base_path = get_openclaw_path()
|
|
834
|
+
json_path = os.path.join(base_path, workspace, HISTORY_DIR, HISTORY_FILENAME)
|
|
835
|
+
|
|
836
|
+
if not os.path.exists(json_path):
|
|
837
|
+
self._send_json({"workspace_id": workspace, "next_version": 1, "records": []})
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
with open(json_path, 'r', encoding='utf-8') as f:
|
|
842
|
+
content = f.read()
|
|
843
|
+
self._send_json_raw(content.encode('utf-8'))
|
|
844
|
+
except Exception as e:
|
|
845
|
+
self._send_json({"error": str(e)}, 500)
|
|
846
|
+
|
|
847
|
+
def _handle_history_file(self, params):
|
|
848
|
+
"""GET /api/history/file?workspace=xxx&version=v1&path=xxx → 返回历史版本文件内容"""
|
|
849
|
+
workspace = params.get('workspace', [''])[0]
|
|
850
|
+
version = params.get('version', [''])[0]
|
|
851
|
+
rel_path = params.get('path', [''])[0]
|
|
852
|
+
|
|
853
|
+
if not workspace or not version or not rel_path:
|
|
854
|
+
self._send_json({"error": "missing params"}, 400)
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
# 安全检查:防止路径穿越
|
|
858
|
+
if '..' in version or '..' in rel_path:
|
|
859
|
+
self._send_json({"error": "invalid path"}, 400)
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
base_path = get_openclaw_path()
|
|
863
|
+
rel_parts = rel_path.replace('\\', '/').split('/')
|
|
864
|
+
file_path = os.path.join(base_path, workspace, HISTORY_DIR, version, *rel_parts)
|
|
865
|
+
|
|
866
|
+
if not os.path.exists(file_path):
|
|
867
|
+
self._send_json({"error": "file not found"}, 404)
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
try:
|
|
871
|
+
with open(file_path, 'rb') as f:
|
|
872
|
+
content = f.read()
|
|
873
|
+
# 根据扩展名设置 Content-Type
|
|
874
|
+
ext = rel_path.rsplit('.', 1)[-1].lower() if '.' in rel_path else ''
|
|
875
|
+
ct_map = {
|
|
876
|
+
'html': 'text/html; charset=utf-8',
|
|
877
|
+
'htm': 'text/html; charset=utf-8',
|
|
878
|
+
'css': 'text/css; charset=utf-8',
|
|
879
|
+
'js': 'application/javascript; charset=utf-8',
|
|
880
|
+
'json': 'application/json; charset=utf-8',
|
|
881
|
+
}
|
|
882
|
+
content_type = ct_map.get(ext, 'text/plain; charset=utf-8')
|
|
883
|
+
self.send_response(200)
|
|
884
|
+
self._cors_headers()
|
|
885
|
+
self.send_header('Content-Type', content_type)
|
|
886
|
+
self.send_header('Content-Length', str(len(content)))
|
|
887
|
+
self.end_headers()
|
|
888
|
+
self.wfile.write(content)
|
|
889
|
+
except Exception as e:
|
|
890
|
+
self._send_json({"error": str(e)}, 500)
|
|
891
|
+
|
|
892
|
+
def _handle_rollback(self):
|
|
893
|
+
"""POST /api/history/rollback Body: {"workspace":"...","version":"v2","path":"..."}"""
|
|
894
|
+
try:
|
|
895
|
+
length = int(self.headers.get('Content-Length', 0))
|
|
896
|
+
body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
|
|
897
|
+
except Exception:
|
|
898
|
+
self._send_json({"error": "请求体解析失败"}, 400)
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
workspace = body.get('workspace', '')
|
|
902
|
+
version = body.get('version', '')
|
|
903
|
+
rel_path = body.get('path', '')
|
|
904
|
+
|
|
905
|
+
if not workspace or not version or not rel_path:
|
|
906
|
+
self._send_json({"error": "缺少参数"}, 400)
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
err = rollback_to_version(workspace, rel_path, version)
|
|
910
|
+
if err:
|
|
911
|
+
self._send_json({"ok": False, "error": err}, 400)
|
|
912
|
+
else:
|
|
913
|
+
self._send_json({"ok": True})
|
|
914
|
+
|
|
631
915
|
def _handle_resync(self, params):
|
|
632
916
|
"""POST /api/resync?workspace=xxx → 触发全量同步"""
|
|
633
917
|
workspace = params.get('workspace', [''])[0]
|
|
@@ -671,6 +955,152 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
|
|
|
671
955
|
return
|
|
672
956
|
self._send_json(_fork_jobs[job_id])
|
|
673
957
|
|
|
958
|
+
def _skill_scripts_root(self):
|
|
959
|
+
"""返回 yiran-playground-template-use/scripts 的绝对路径"""
|
|
960
|
+
return os.path.join(
|
|
961
|
+
get_openclaw_path(), 'workspace-ai-demo', 'skills',
|
|
962
|
+
'yiran-playground-template-use', 'scripts'
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
def _resolve_template_path(self, folder):
|
|
966
|
+
"""校验 folder 并返回模板绝对路径;非法时直接响应并返回 None"""
|
|
967
|
+
if not folder or '..' in folder or '/' in folder or '\\' in folder:
|
|
968
|
+
self._send_json({'error': 'invalid folder'}, 400)
|
|
969
|
+
return None
|
|
970
|
+
path = os.path.join(
|
|
971
|
+
get_openclaw_path(), 'workspace-ai-demo', 'skills',
|
|
972
|
+
'yiran-playground-template-use', 'templates', folder
|
|
973
|
+
)
|
|
974
|
+
if not os.path.isdir(path):
|
|
975
|
+
self._send_json({'error': f'模板目录不存在: {folder}'}, 404)
|
|
976
|
+
return None
|
|
977
|
+
return path
|
|
978
|
+
|
|
979
|
+
def _run_skill_script(self, cmd, timeout=60):
|
|
980
|
+
"""运行 skill 脚本,解析 stdout JSON 并返回 (data, err)"""
|
|
981
|
+
try:
|
|
982
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
983
|
+
output = (result.stdout or '').strip()
|
|
984
|
+
if output:
|
|
985
|
+
try:
|
|
986
|
+
return json.loads(output), None
|
|
987
|
+
except json.JSONDecodeError:
|
|
988
|
+
pass
|
|
989
|
+
return None, result.stderr.strip() or output or '未知错误'
|
|
990
|
+
except subprocess.TimeoutExpired:
|
|
991
|
+
return None, f'执行超时({timeout}s)'
|
|
992
|
+
except Exception as e:
|
|
993
|
+
return None, str(e)
|
|
994
|
+
|
|
995
|
+
def _handle_template_copy(self):
|
|
996
|
+
"""POST /api/template/copy Body: {"folder": "a100_...", "workspace": "workspace-xxx"}
|
|
997
|
+
→ deploy_to_workspace.py <template_path> <workspace_abs_path> (不加 --with-tui)"""
|
|
998
|
+
try:
|
|
999
|
+
length = int(self.headers.get('Content-Length', 0))
|
|
1000
|
+
body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
|
|
1001
|
+
except Exception:
|
|
1002
|
+
self._send_json({'error': '请求体解析失败'}, 400)
|
|
1003
|
+
return
|
|
1004
|
+
|
|
1005
|
+
folder = body.get('folder', '')
|
|
1006
|
+
workspace = body.get('workspace', '') # e.g. "workspace-xxx"
|
|
1007
|
+
if not workspace:
|
|
1008
|
+
self._send_json({'error': '缺少 workspace 参数'}, 400)
|
|
1009
|
+
return
|
|
1010
|
+
|
|
1011
|
+
template_path = self._resolve_template_path(folder)
|
|
1012
|
+
if template_path is None:
|
|
1013
|
+
return
|
|
1014
|
+
|
|
1015
|
+
workspace_abs = os.path.join(get_openclaw_path(), workspace)
|
|
1016
|
+
script = os.path.join(self._skill_scripts_root(), 'deploy_to_workspace.py')
|
|
1017
|
+
|
|
1018
|
+
data, err = self._run_skill_script(
|
|
1019
|
+
[sys.executable, script, template_path, workspace_abs],
|
|
1020
|
+
timeout=30
|
|
1021
|
+
)
|
|
1022
|
+
if err:
|
|
1023
|
+
self._send_json({'ok': False, 'error': err}, 500)
|
|
1024
|
+
else:
|
|
1025
|
+
self._send_json(data)
|
|
1026
|
+
|
|
1027
|
+
def _handle_template_deploy(self):
|
|
1028
|
+
"""POST /api/template/deploy Body: {"folder": "a100_..."}
|
|
1029
|
+
→ run_playgrounds_flow.py <template_path> (prepare + deploy + --with-tui)"""
|
|
1030
|
+
try:
|
|
1031
|
+
length = int(self.headers.get('Content-Length', 0))
|
|
1032
|
+
body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
|
|
1033
|
+
except Exception:
|
|
1034
|
+
self._send_json({'error': '请求体解析失败'}, 400)
|
|
1035
|
+
return
|
|
1036
|
+
|
|
1037
|
+
folder = body.get('folder', '')
|
|
1038
|
+
template_path = self._resolve_template_path(folder)
|
|
1039
|
+
if template_path is None:
|
|
1040
|
+
return
|
|
1041
|
+
|
|
1042
|
+
script = os.path.join(self._skill_scripts_root(), 'run_playgrounds_flow.py')
|
|
1043
|
+
|
|
1044
|
+
data, err = self._run_skill_script(
|
|
1045
|
+
[sys.executable, script, template_path],
|
|
1046
|
+
timeout=60
|
|
1047
|
+
)
|
|
1048
|
+
if err:
|
|
1049
|
+
self._send_json({'ok': False, 'error': err}, 500)
|
|
1050
|
+
else:
|
|
1051
|
+
self._send_json(data)
|
|
1052
|
+
|
|
1053
|
+
def _handle_file(self, params):
|
|
1054
|
+
"""GET /api/file?path=relative/path → 通用文件服务,路径相对于 .openclaw 根目录
|
|
1055
|
+
安全:os.path.realpath 解析后必须以 openclaw 根为前缀,防止路径穿越"""
|
|
1056
|
+
rel_path = params.get('path', [''])[0]
|
|
1057
|
+
if not rel_path:
|
|
1058
|
+
self._send_json({"error": "missing path param"}, 400)
|
|
1059
|
+
return
|
|
1060
|
+
|
|
1061
|
+
base_path = os.path.realpath(get_openclaw_path())
|
|
1062
|
+
abs_path = os.path.realpath(os.path.join(base_path, rel_path))
|
|
1063
|
+
|
|
1064
|
+
# 路径必须在 .openclaw 目录内
|
|
1065
|
+
if not abs_path.startswith(base_path + os.sep):
|
|
1066
|
+
self._send_json({"error": "forbidden"}, 403)
|
|
1067
|
+
return
|
|
1068
|
+
|
|
1069
|
+
if not os.path.isfile(abs_path):
|
|
1070
|
+
self._send_json({"error": "file not found"}, 404)
|
|
1071
|
+
return
|
|
1072
|
+
|
|
1073
|
+
try:
|
|
1074
|
+
with open(abs_path, 'rb') as f:
|
|
1075
|
+
content = f.read()
|
|
1076
|
+
|
|
1077
|
+
ext = abs_path.rsplit('.', 1)[-1].lower() if '.' in abs_path else ''
|
|
1078
|
+
ct_map = {
|
|
1079
|
+
'html': 'text/html; charset=utf-8',
|
|
1080
|
+
'htm': 'text/html; charset=utf-8',
|
|
1081
|
+
'css': 'text/css; charset=utf-8',
|
|
1082
|
+
'js': 'application/javascript; charset=utf-8',
|
|
1083
|
+
'json': 'application/json; charset=utf-8',
|
|
1084
|
+
'txt': 'text/plain; charset=utf-8',
|
|
1085
|
+
'md': 'text/plain; charset=utf-8',
|
|
1086
|
+
'png': 'image/png',
|
|
1087
|
+
'jpg': 'image/jpeg',
|
|
1088
|
+
'jpeg': 'image/jpeg',
|
|
1089
|
+
'gif': 'image/gif',
|
|
1090
|
+
'svg': 'image/svg+xml',
|
|
1091
|
+
'webp': 'image/webp',
|
|
1092
|
+
}
|
|
1093
|
+
content_type = ct_map.get(ext, 'application/octet-stream')
|
|
1094
|
+
|
|
1095
|
+
self.send_response(200)
|
|
1096
|
+
self._cors_headers()
|
|
1097
|
+
self.send_header('Content-Type', content_type)
|
|
1098
|
+
self.send_header('Content-Length', str(len(content)))
|
|
1099
|
+
self.end_headers()
|
|
1100
|
+
self.wfile.write(content)
|
|
1101
|
+
except Exception as e:
|
|
1102
|
+
self._send_json({"error": str(e)}, 500)
|
|
1103
|
+
|
|
674
1104
|
def log_message(self, format, *args):
|
|
675
1105
|
# 静默日志,避免轮询刷屏
|
|
676
1106
|
pass
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# 课程模板流水线 Skill
|
|
2
|
+
|
|
3
|
+
这个 skill 面向中文课堂的小专题课程设计流程。
|
|
4
|
+
|
|
5
|
+
它的目标不是只生成某一个页面,而是把一个“题目灵感 / 设计角度”逐步变成一整套可用的课程任务文件。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 当前工作流
|
|
10
|
+
|
|
11
|
+
## 阶段一:灵感 -> demo 页面
|
|
12
|
+
最开始输入的是:
|
|
13
|
+
- 题目灵感
|
|
14
|
+
- 设计角度
|
|
15
|
+
- 课堂上的切入想法
|
|
16
|
+
|
|
17
|
+
这一阶段最重要的事情是:
|
|
18
|
+
**先做出 __demo__.html(效果演示页)**
|
|
19
|
+
|
|
20
|
+
阶段一到阶段三的产物,统一放在:
|
|
21
|
+
**当前工作目录下新建的 `任务名称/` 文件夹里。**
|
|
22
|
+
|
|
23
|
+
也就是说:
|
|
24
|
+
- 不先放到 templates
|
|
25
|
+
- 不先放到 skill 目录根部
|
|
26
|
+
- 而是在当前工作目录里,先建立一个任务临时文件夹,再往里写 `__demo__.html`、`__student__.json`、`__teacher__.json`
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 阶段二:生成 __student__.json
|
|
31
|
+
这一阶段不要求一定已经有 __teacher__.json。
|
|
32
|
+
这一阶段甚至不要求一定已经有完整作品。
|
|
33
|
+
|
|
34
|
+
这一阶段的核心目标是:
|
|
35
|
+
**先把学生任务的步骤和评价结构 JSON 确立下来。**
|
|
36
|
+
|
|
37
|
+
对应输出:
|
|
38
|
+
`__student__.json`
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 阶段三:生成 __teacher__.json
|
|
43
|
+
在 __student__.json 已经明确后,再整理教师侧信息。
|
|
44
|
+
|
|
45
|
+
这一阶段的核心目标是:
|
|
46
|
+
**明确题目设计目的、训练重点、适合学生类型,以及老师上课时如何判断和引导。**
|
|
47
|
+
|
|
48
|
+
对应输出:
|
|
49
|
+
`__teacher__.json`
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 阶段四:JSON -> HTML,并按目录打包
|
|
54
|
+
第4阶段现在拆成两个脚本:
|
|
55
|
+
|
|
56
|
+
### 1)build_template_views.py
|
|
57
|
+
校验通过后,在原文件夹中直接生成:
|
|
58
|
+
- `__student-view__.html`
|
|
59
|
+
- `__teacher-view__.html`
|
|
60
|
+
- `index.html`
|
|
61
|
+
|
|
62
|
+
其中 `index.html` 采用固定脚本规则:
|
|
63
|
+
- 只展示两块内容
|
|
64
|
+
- 左边是“评价目标”
|
|
65
|
+
- 右边是“步骤”
|
|
66
|
+
- 不显示任务标题
|
|
67
|
+
- 不显示一句话说明
|
|
68
|
+
- 不再依赖临时提示词拼装
|
|
69
|
+
- 统一由脚本直接根据 `__student__.json` 渲染
|
|
70
|
+
|
|
71
|
+
### 2)move_template_task.py
|
|
72
|
+
在页面已经生成好的前提下,把整个文件夹移动到:
|
|
73
|
+
`yiran-playground-template-use` skill 的 `templates/`
|
|
74
|
+
|
|
75
|
+
同时按规则改名,例如:
|
|
76
|
+
`a100_给依然老师设计一个AI画像`
|
|
77
|
+
|
|
78
|
+
并且:
|
|
79
|
+
**move 成功后会自动触发 `yiran-playground-template-use` 的索引重建。**
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# 当前目录结构
|
|
84
|
+
|
|
85
|
+
- `SKILL.md`:技能总说明
|
|
86
|
+
- `prompts/阶段1-demo生成.md`:如何把灵感先做成 demo 页面
|
|
87
|
+
- `prompts/阶段2-student生成.md`:如何生成 __student__.json
|
|
88
|
+
- `prompts/阶段3-teacher生成.md`:如何生成 __teacher__.json
|
|
89
|
+
- `prompts/阶段4-打包落盘.md`:如何组织文件并调用第4阶段脚本
|
|
90
|
+
- `scripts/render_teacher_view.py`:把 __teacher__.json 渲染成教师查看页
|
|
91
|
+
- `scripts/render_student_page.py`:把 __student__.json 渲染成学生页
|
|
92
|
+
- `scripts/build_template_views.py`:在原目录生成 __student-view__.html 和 __teacher-view__.html
|
|
93
|
+
- `scripts/move_template_task.py`:移动整个任务文件夹到 templates 并改名,同时自动重建索引
|
|
94
|
+
- `references/teacher-fields.md`:教师侧字段设计与标签规则说明
|
|
95
|
+
- `references/student-fields.md`:学生侧字段说明
|
|
96
|
+
- `references/student-example.json`:学生侧 JSON 示例
|
|
97
|
+
- `references/student-scaffold.json`:学生侧 JSON 脚手架
|
|
98
|
+
- `references/teacher-scaffold.json`:教师侧 JSON 脚手架
|
|
99
|
+
- `assets/student-template.html`:独立学生页模板
|
|
100
|
+
- 模板库和搜索索引统一维护在 `yiran-playground-template-use` skill 中
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
# 模板索引
|
|
105
|
+
|
|
106
|
+
模板库和索引统一维护在 `yiran-playground-template-use` skill 中。
|
|
107
|
+
|
|
108
|
+
`move_template_task.py` 成功执行后会自动触发索引重建。如需手动重建,在 `yiran-playground-template-use` skill 中运行:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python3 scripts/build_template_index.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# 核心原则
|
|
117
|
+
|
|
118
|
+
1. 先有灵感,再做 demo,再定 __student__.json,再定 __teacher__.json
|
|
119
|
+
2. 阶段一到三的产物,统一放在当前工作目录下新建的任务文件夹里
|
|
120
|
+
3. __student__.json 先解决学生任务结构
|
|
121
|
+
4. __teacher__.json 再解决题目设计目的与教师判断
|
|
122
|
+
5. 第4阶段拆成 build 和 move 两个独立脚本
|
|
123
|
+
6. templates 作为 skill 的资产目录统一维护
|
|
124
|
+
7. 模板有变化后,要重新构建索引
|
|
125
|
+
8. demo 中的媒体资源优先使用 CDN 链接,不使用本地相对路径;当前固定前缀为 `https://cdn.yiranlaoshi.com/yiran/workspace-ai-demo`,后面拼接资源相对路径
|
|
126
|
+
9. 模板负责布局,提示词负责填内容,脚本负责稳定输出
|
|
127
|
+
10. 所有内容优先服务中文课堂和小学生阅读场景
|