@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.
Files changed (83) hide show
  1. package/.claude/settings.local.json +25 -1
  2. package/assets/myclaw-artifacts.js +1070 -126
  3. package/assets/myclaw-inject.js +913 -121
  4. package/delete_agents.js +268 -0
  5. package/index.js +361 -20
  6. package/package.json +1 -1
  7. package/patches/patch-manifest.json +10 -0
  8. package/server/sync_workspace.py +444 -14
  9. package/skills/yiran-course-template-pipeline/README.md +127 -0
  10. package/skills/yiran-course-template-pipeline/SKILL.md +65 -0
  11. package/skills/yiran-course-template-pipeline/assets/a100-teacher.example.html +66 -0
  12. package/skills/yiran-course-template-pipeline/assets/student-template.html +64 -0
  13. package/skills/yiran-course-template-pipeline/assets/teacher-portrait-demo.html +105 -0
  14. package/skills/yiran-course-template-pipeline/assets/teacher-task-view.html +110 -0
  15. package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2651-demo/347/224/237/346/210/220.md +92 -0
  16. package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2652-student/347/224/237/346/210/220.md +115 -0
  17. package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2653-teacher/347/224/237/346/210/220.md +131 -0
  18. 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
  19. package/skills/yiran-course-template-pipeline/references/student-example.json +38 -0
  20. package/skills/yiran-course-template-pipeline/references/student-fields.md +195 -0
  21. package/skills/yiran-course-template-pipeline/references/student-scaffold.json +34 -0
  22. package/skills/yiran-course-template-pipeline/references/teacher-fields.md +265 -0
  23. package/skills/yiran-course-template-pipeline/references/teacher-scaffold.json +25 -0
  24. package/skills/yiran-course-template-pipeline/scripts/build_template_views.py +125 -0
  25. package/skills/yiran-course-template-pipeline/scripts/move_template_task.py +59 -0
  26. package/skills/yiran-course-template-pipeline/scripts/render_student_page.py +52 -0
  27. package/skills/yiran-course-template-pipeline/scripts/render_teacher_view.py +108 -0
  28. package/skills/yiran-playground-template-use/SKILL.md +105 -0
  29. package/skills/yiran-playground-template-use/prompts/remix-handoff.txt +11 -0
  30. package/skills/yiran-playground-template-use/scripts/build_template_index.py +103 -0
  31. package/skills/yiran-playground-template-use/scripts/deploy_template.py +34 -0
  32. package/skills/yiran-playground-template-use/scripts/deploy_to_workspace.py +211 -0
  33. package/skills/yiran-playground-template-use/scripts/prepare_playgrounds.py +39 -0
  34. package/skills/yiran-playground-template-use/scripts/query_template.py +171 -0
  35. package/skills/yiran-playground-template-use/scripts/run_playgrounds_flow.py +44 -0
  36. package/skills/yiran-playground-template-use/scripts/start_tui_handoff.py +77 -0
  37. package/skills/yiran-playground-template-use/search-agent-prompt.md +39 -0
  38. package/skills/yiran-playground-template-use/template-index.json +136 -0
  39. package/skills/yiran-playground-template-use/template-index.md +38 -0
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. package/skills/yiran-skill-media/SKILL.md +6 -15
  72. package/skills/yiran-skill-media/scripts/generate.py +47 -18
  73. package/skills/yiran-skill-media/scripts/generation_log.json +1 -56
  74. package/skills/yiran-skill-media/scripts/providers/__pycache__/__init__.cpython-311.pyc +0 -0
  75. package/skills/yiran-skill-media/scripts/providers/__pycache__/__init__.cpython-37.pyc +0 -0
  76. package/skills/yiran-skill-media/scripts/providers/__pycache__/jimeng_image.cpython-37.pyc +0 -0
  77. package/skills/yiran-skill-media/scripts/providers/__pycache__/jimeng_video.cpython-311.pyc +0 -0
  78. package/skills/yiran-skill-media/scripts/providers/__pycache__/jimeng_video.cpython-37.pyc +0 -0
  79. package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_image.cpython-37.pyc +0 -0
  80. package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_music.cpython-37.pyc +0 -0
  81. package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_video.cpython-311.pyc +0 -0
  82. package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_video.cpython-37.pyc +0 -0
  83. package/skills/yiran-skill-media/scripts/providers/__pycache__/vapi_image.cpython-37.pyc +0 -0
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: yiran-playground-template-use
3
+ description: AI Playground 课程模板库。存储模板、维护索引、提供搜索,并对外暴露两个脚本入口:playgrounds 一条龙,以及指定 workspace 的 deploy / deploy+tui。适用于"我要用 A100""帮我打开模板103""把 A100 放到 playgrounds"这类场景。
4
+ ---
5
+
6
+ # yiran-playground-template-use
7
+
8
+ 这个 skill 负责模板的**存储、搜索和使用**,是模板生命周期的后半段。
9
+
10
+ 模板的生产由 `yiran-course-template-pipeline` 完成,生产完成后移入本 skill 的 `templates/`。
11
+
12
+ ## 能做什么
13
+
14
+ 1. **搜索模板**:按系列/编号或关键词找到合适的模板
15
+ 2. **playgrounds 一条龙**:自动准备 playgrounds、复制模板、并发起 tui
16
+ 3. **指定 workspace 投放**:支持只 deploy,或 deploy 后继续 tui
17
+
18
+ ## 如何搜索
19
+
20
+ 两种方式:
21
+
22
+ - AI 驱动:使用 `search-agent-prompt.md` + `template-index.json`
23
+ - 脚本驱动:`python3 scripts/query_template.py --series A --number 100`
24
+
25
+ 说明:
26
+ - `query_template.py` 是唯一查询主入口
27
+ - 先按系列 + 编号查
28
+ - 没有明确编号时,再按关键词、标签和索引内容匹配
29
+
30
+ ## 对外脚本入口
31
+
32
+ ### 参数总表
33
+
34
+ | 脚本 | 模式 | 调用方式 | 必填参数 | 可选参数 | 是否自动 tui |
35
+ |---|---|---|---|---|---|
36
+ | `run_playgrounds_flow.py` | A + B + C | `python3 scripts/run_playgrounds_flow.py <template_path>` | `template_path` | 无 | 是 |
37
+ | `deploy_to_workspace.py` | 单独 B | `python3 scripts/deploy_to_workspace.py <template_path> <workspace_path>` | `template_path` `workspace_path` | 无 | 否 |
38
+ | `deploy_to_workspace.py` | B + C | `python3 scripts/deploy_to_workspace.py <template_path> <workspace_path> --with-tui` | `template_path` `workspace_path` | `--with-tui` | 是 |
39
+
40
+ ### 入口 1,playgrounds 一条龙
41
+ ```bash
42
+ python3 scripts/run_playgrounds_flow.py <template_path>
43
+ ```
44
+
45
+ 特点:
46
+ - 只收 1 个参数:模板路径
47
+ - 内部自动执行 A + B + C
48
+ - 固定使用 `playgrounds`
49
+ - 自动执行 `mc new playgrounds`
50
+ - 自动复制到 `workspace-playgrounds/remix/实例名`
51
+ - 自动发起 `mc tui playgrounds`
52
+
53
+ ### 入口 2,指定 workspace 投放
54
+ ```bash
55
+ python3 scripts/deploy_to_workspace.py <template_path> <workspace_path>
56
+ python3 scripts/deploy_to_workspace.py <template_path> <workspace_path> --with-tui
57
+ ```
58
+
59
+ 特点:
60
+ - 只 deploy 时,不进入 tui
61
+ - 加 `--with-tui` 时,deploy 后继续发起 tui
62
+ - 不暴露 `--agent`
63
+ - 不暴露 `--session`
64
+ - agent 从 `workspace-xxx` 自动推导
65
+
66
+ ## 统一规则
67
+
68
+ - `实例名 = 目标文件夹名 = session 名`
69
+ - 实例名在 deploy 阶段生成
70
+ - 命名规则默认是:`月日_原模板名_时分`
71
+ - 如果同一分钟同名冲突,会自动加 `_2`、`_3`
72
+ - 后续要修改的文件优先使用 `__demo__.html`
73
+
74
+ ## 内部脚本分层
75
+
76
+ - `scripts/prepare_playgrounds.py`:内部 A 阶段,playgrounds 准备
77
+ - `scripts/deploy_to_workspace.py`:内部 B / C 阶段,指定 workspace deploy / optional tui
78
+ - `scripts/run_playgrounds_flow.py`:对外 A+B+C 一条龙入口
79
+ - `scripts/deploy_template.py`:兼容转发到默认 playgrounds workspace
80
+
81
+ ## 目录结构
82
+
83
+ - `SKILL.md`:本说明
84
+ - `search-agent-prompt.md`:AI 搜索指引
85
+ - `template-index.json`:模板索引(自动生成,不手改)
86
+ - `template-index.md`:模板索引可读版本(自动生成,不手改)
87
+ - `templates/`:模板库,每个子文件夹是一个独立模板
88
+ - `scripts/build_template_index.py`:重建索引
89
+ - `scripts/query_template.py`:查询主脚本
90
+ - `scripts/prepare_playgrounds.py`:内部准备脚本
91
+ - `scripts/deploy_to_workspace.py`:对外 workspace 投放脚本
92
+ - `scripts/run_playgrounds_flow.py`:对外 playgrounds 一条龙脚本
93
+ - `prompts/remix-handoff.txt`:交接提示词模板
94
+
95
+ ## 索引维护
96
+
97
+ `yiran-course-template-pipeline` 打包新模板后会自动触发索引重建。
98
+
99
+ `template-index.json` 与 `template-index.md` 都视为自动产物,不手动编辑。
100
+
101
+ 如需手动重建:
102
+
103
+ ```bash
104
+ python3 scripts/build_template_index.py
105
+ ```
@@ -0,0 +1,11 @@
1
+ 下面是这次要继续处理的作品材料,请先按路径查看相关文件,并为后续继续引导学生完成作品做准备。
2
+
3
+ 老师给到的作品文件夹路径:
4
+ {{FOLDER_REL_PATH}}
5
+
6
+ 后续我们要修改的文件路径:
7
+ {{ENTRY_REL_PATH}}
8
+
9
+ 请先查看这个作品文件夹中的所有现有文件,并重点查看后续要修改的网页文件。
10
+ 后续我们会基于这些现有内容继续修改作品,并继续引导学生完成任务。
11
+ 所以你现在先完成阅读和理解,暂时不要开始改动。
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parent.parent
7
+ TEMPLATES_DIR = ROOT / 'templates'
8
+ OUTPUT_JSON = ROOT / 'template-index.json'
9
+ OUTPUT_MD = ROOT / 'template-index.md'
10
+
11
+
12
+ def read_json(path: Path):
13
+ try:
14
+ return json.loads(path.read_text(encoding='utf-8'))
15
+ except Exception:
16
+ return {}
17
+
18
+
19
+ def parse_folder_name(name: str):
20
+ m = re.match(r'^([a-zA-Z]+)(\d+)_(.+)$', name)
21
+ if m:
22
+ return m.group(1).upper(), str(int(m.group(2))), m.group(3)
23
+ return None, None, None
24
+
25
+
26
+ def get_entry_file(folder: Path) -> Path | None:
27
+ for name in ['index.html', '__demo__.html', '__student-view__.html']:
28
+ if (folder / name).exists():
29
+ return folder / name
30
+ return None
31
+
32
+
33
+ def main():
34
+ index = {}
35
+ items_for_md = []
36
+
37
+ for folder in sorted(TEMPLATES_DIR.iterdir() if TEMPLATES_DIR.exists() else []):
38
+ if not folder.is_dir():
39
+ continue
40
+
41
+ series, number, _ = parse_folder_name(folder.name)
42
+ if not series:
43
+ continue
44
+
45
+ teacher = read_json(folder / '__teacher__.json')
46
+ student = read_json(folder / '__student__.json')
47
+
48
+ name = teacher.get('任务名称') or student.get('任务标题', '')
49
+ 一句话 = student.get('一句话说明', '')
50
+ 能力标签 = teacher.get('主能力标签', '')
51
+ 类型标签 = teacher.get('任务类型标签', '')
52
+
53
+ keywords = list(dict.fromkeys(filter(None, [
54
+ folder.name,
55
+ 一句话,
56
+ *teacher.get('适合学生类型', []),
57
+ *teacher.get('训练重点', []),
58
+ name,
59
+ 能力标签,
60
+ 类型标签,
61
+ ])))
62
+
63
+ entry = get_entry_file(folder)
64
+ record = {
65
+ '系列': series,
66
+ '编号': number,
67
+ '名称': name,
68
+ '文件夹名': folder.name,
69
+ '路径': str(folder.relative_to(ROOT)),
70
+ 'student_json': str((folder / '__student__.json').relative_to(ROOT)) if (folder / '__student__.json').exists() else '',
71
+ 'teacher_json': str((folder / '__teacher__.json').relative_to(ROOT)) if (folder / '__teacher__.json').exists() else '',
72
+ '入口文件': str(entry.relative_to(ROOT)) if entry else '',
73
+ '一句话说明': 一句话,
74
+ '主能力标签': 能力标签,
75
+ '任务类型标签': 类型标签,
76
+ '关键词': keywords,
77
+ }
78
+
79
+ if series not in index:
80
+ index[series] = {}
81
+ index[series][number] = record
82
+ items_for_md.append(record)
83
+
84
+ OUTPUT_JSON.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding='utf-8')
85
+
86
+ lines = ['# 模板索引', '', '每次 templates 内模板有变化后,都应重新运行本脚本。', '']
87
+ for item in items_for_md:
88
+ lines.append(f"## {item['系列']}{item['编号']} {item['名称']}")
89
+ lines.append(f"- 路径:{item['路径']}")
90
+ lines.append(f"- 入口文件:{item['入口文件']}")
91
+ lines.append(f"- 主能力标签:{item['主能力标签']}")
92
+ lines.append(f"- 任务类型标签:{item['任务类型标签']}")
93
+ if item['关键词']:
94
+ lines.append(f"- 关键词:{', '.join(item['关键词'])}")
95
+ lines.append('')
96
+
97
+ OUTPUT_MD.write_text('\n'.join(lines), encoding='utf-8')
98
+ print(f'已生成: {OUTPUT_JSON}')
99
+ print(f'已生成: {OUTPUT_MD}')
100
+
101
+
102
+ if __name__ == '__main__':
103
+ main()
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ TARGET_SCRIPT = Path(__file__).resolve().parent / 'deploy_to_workspace.py'
8
+ DEFAULT_WORKSPACE = Path.home() / '.openclaw' / 'workspace-playgrounds'
9
+
10
+
11
+ def fail(payload, code=1):
12
+ print(json.dumps(payload, ensure_ascii=False))
13
+ raise SystemExit(code)
14
+
15
+
16
+ def main():
17
+ if len(sys.argv) not in (2, 3):
18
+ fail({'ok': False, 'error': 'usage', 'message': '请改用 python3 scripts/deploy_to_workspace.py <template_path> <workspace_path> [--with-tui]'})
19
+
20
+ template_path = sys.argv[1]
21
+ args = ['python3', str(TARGET_SCRIPT), template_path, str(DEFAULT_WORKSPACE)]
22
+ if len(sys.argv) == 3:
23
+ if sys.argv[2] != '--with-tui':
24
+ fail({'ok': False, 'error': 'unknown_argument', 'message': f'未知参数: {sys.argv[2]}'})
25
+ args.append('--with-tui')
26
+
27
+ result = subprocess.run(args, capture_output=True, text=True)
28
+ sys.stdout.write(result.stdout)
29
+ sys.stderr.write(result.stderr)
30
+ raise SystemExit(result.returncode)
31
+
32
+
33
+ if __name__ == '__main__':
34
+ main()
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from datetime import datetime, timezone, timedelta
7
+ from pathlib import Path
8
+
9
+ ARTIFACTS_FILENAME = '__MY_ARTIFACTS__.json'
10
+ IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'}
11
+ # 最后注册的排 UI 最前,顺序:其他 → 图片 → student-view → index
12
+ ARTIFACTS_TAIL_ORDER = ['__student-view__.html', 'index.html']
13
+
14
+
15
+ def _now_iso():
16
+ tz = timezone(timedelta(hours=8))
17
+ return datetime.now(tz).isoformat()
18
+
19
+
20
+ def _get_file_type(path: str) -> str:
21
+ ext = Path(path).suffix.lower()
22
+ return {'.html': 'html', '.htm': 'html', '.json': 'json', '.js': 'js',
23
+ '.css': 'css', '.png': 'image', '.jpg': 'image', '.jpeg': 'image',
24
+ '.gif': 'image', '.svg': 'image', '.mp3': 'audio', '.mp4': 'video'}.get(ext, 'file')
25
+
26
+
27
+ def _gen_id():
28
+ import random, string
29
+ return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
30
+
31
+
32
+ def _register_file(artifacts_path: Path, workspace_id: str, rel_path: str):
33
+ """向 __MY_ARTIFACTS__.json 注册/更新一个文件,更新 updated_at"""
34
+ now = _now_iso()
35
+ if artifacts_path.exists():
36
+ try:
37
+ data = json.loads(artifacts_path.read_text(encoding='utf-8'))
38
+ except Exception:
39
+ data = None
40
+ else:
41
+ data = None
42
+
43
+ if data is None:
44
+ data = {
45
+ 'workspace_id': workspace_id,
46
+ 'title': '我的资源库',
47
+ 'release_version': 1,
48
+ 'created_at': now,
49
+ 'updated_at': now,
50
+ 'version': 1,
51
+ 'base_url': '',
52
+ 'preview_path': 'index.html',
53
+ 'assets': [],
54
+ }
55
+
56
+ found = False
57
+ for asset in data.get('assets', []):
58
+ if asset.get('path') == rel_path:
59
+ asset.setdefault('created_at', now)
60
+ asset['updated_at'] = now
61
+ asset['type'] = _get_file_type(rel_path)
62
+ found = True
63
+ break
64
+
65
+ if not found:
66
+ data['assets'].append({
67
+ 'id': _gen_id(),
68
+ 'type': _get_file_type(rel_path),
69
+ 'path': rel_path,
70
+ 'created_at': now,
71
+ 'updated_at': now,
72
+ })
73
+
74
+ data['updated_at'] = now
75
+ artifacts_path.parent.mkdir(parents=True, exist_ok=True)
76
+ artifacts_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
77
+
78
+
79
+ def register_instance_files(workspace: Path, target_dir: Path):
80
+ """B 阶段结束后,按顺序注册所有文件到 __MY_ARTIFACTS__.json。
81
+ 注册顺序(后注册 = updated_at 最新 = UI 排前):
82
+ 其他资源 → 图片资源 → __student-view__.html → index.html
83
+ """
84
+ workspace_id = workspace.name
85
+ artifacts_path = workspace / '.myclaw' / ARTIFACTS_FILENAME
86
+
87
+ all_files = sorted(f for f in target_dir.rglob('*') if f.is_file())
88
+ rel_paths = [f.as_posix().replace(workspace.as_posix() + '/', '') for f in all_files]
89
+
90
+ def is_tail(p):
91
+ return any(p.endswith('/' + name) or p == name for name in ARTIFACTS_TAIL_ORDER)
92
+
93
+ def is_image(p):
94
+ return Path(p).suffix.lower() in IMAGE_EXTS
95
+
96
+ others = [p for p in rel_paths if not is_tail(p) and not is_image(p)]
97
+ images = [p for p in rel_paths if is_image(p)]
98
+ tail = [p for name in ARTIFACTS_TAIL_ORDER
99
+ for p in rel_paths if p.endswith('/' + name) or p == name]
100
+
101
+ for rel in others + images + tail:
102
+ _register_file(artifacts_path, workspace_id, rel)
103
+
104
+ PROMPT_TEMPLATE_PATH = Path(__file__).resolve().parent.parent / 'prompts' / 'remix-handoff.txt'
105
+
106
+
107
+ def fail(payload, code=1):
108
+ print(json.dumps(payload, ensure_ascii=False))
109
+ raise SystemExit(code)
110
+
111
+
112
+ def parse_args(argv):
113
+ if len(argv) < 2:
114
+ fail({'ok': False, 'error': 'usage', 'message': '用法: python3 scripts/deploy_to_workspace.py <template_path> <workspace_path> [--with-tui]'})
115
+
116
+ source_dir = Path(argv[0])
117
+ workspace = Path(argv[1])
118
+ do_tui = False
119
+
120
+ extra = argv[2:]
121
+ if extra:
122
+ if extra == ['--with-tui']:
123
+ do_tui = True
124
+ else:
125
+ fail({'ok': False, 'error': 'unknown_argument', 'message': f'未知参数: {extra}'})
126
+
127
+ ws_name = workspace.name
128
+ agent = ws_name[len('workspace-'):] if ws_name.startswith('workspace-') else ''
129
+ return source_dir, workspace, agent, do_tui
130
+
131
+
132
+ def run(cmd):
133
+ return subprocess.run(cmd, capture_output=True, text=True)
134
+
135
+
136
+ def choose_entry_file(target_dir: Path):
137
+ for preferred in ['index.html', '__demo__.html', '__student-view__.html']:
138
+ candidate = target_dir / preferred
139
+ if candidate.exists():
140
+ return candidate
141
+ html_files = sorted(list(target_dir.rglob('*.html')) + list(target_dir.rglob('*.htm')))
142
+ return html_files[0] if html_files else None
143
+
144
+
145
+ def build_instance_name(source_dir: Path, remix_dir: Path):
146
+ base = f"{datetime.now().strftime('%m%d')}_{source_dir.name}_{datetime.now().strftime('%H%M')}"
147
+ candidate = base
148
+ suffix = 2
149
+ while (remix_dir / candidate).exists():
150
+ candidate = f"{base}_{suffix}"
151
+ suffix += 1
152
+ return candidate
153
+
154
+
155
+ def build_message(folder_rel_path: str, entry_rel_path: str):
156
+ if not PROMPT_TEMPLATE_PATH.exists():
157
+ fail({'ok': False, 'error': 'prompt_template_not_found', 'path': str(PROMPT_TEMPLATE_PATH)})
158
+ prompt_template = PROMPT_TEMPLATE_PATH.read_text(encoding='utf-8')
159
+ return prompt_template.replace('{{FOLDER_REL_PATH}}', folder_rel_path).replace('{{ENTRY_REL_PATH}}', entry_rel_path)
160
+
161
+
162
+ def main():
163
+ source_dir, workspace, agent, do_tui = parse_args(sys.argv[1:])
164
+ if not source_dir.exists() or not source_dir.is_dir():
165
+ fail({'ok': False, 'error': 'source_dir_not_found', 'source_dir': str(source_dir)})
166
+
167
+ workspace.mkdir(parents=True, exist_ok=True)
168
+ remix_dir = workspace / 'remix'
169
+ remix_dir.mkdir(parents=True, exist_ok=True)
170
+
171
+ instance_name = build_instance_name(source_dir, remix_dir)
172
+ target_dir = remix_dir / instance_name
173
+ shutil.copytree(source_dir, target_dir)
174
+
175
+ register_instance_files(workspace, target_dir)
176
+
177
+ entry_file = choose_entry_file(target_dir)
178
+ if not entry_file:
179
+ fail({'ok': False, 'error': 'entry_html_not_found', 'target_dir': str(target_dir)})
180
+
181
+ folder_rel_path = f'remix/{instance_name}'
182
+ entry_rel_path = entry_file.as_posix().replace(workspace.as_posix() + '/', '')
183
+
184
+ message = ''
185
+ tui_started = False
186
+ if do_tui:
187
+ if not agent:
188
+ fail({'ok': False, 'error': 'workspace_agent_not_inferable', 'message': 'workspace 路径必须形如 workspace-xxx,才能启用 --with-tui'})
189
+ message = build_message(folder_rel_path, entry_rel_path)
190
+ result = run(['mc', 'tui', agent, '--session', instance_name, '--message', message])
191
+ if result.returncode != 0:
192
+ fail({'ok': False, 'error': 'mc_tui_failed', 'agent': agent, 'session': instance_name, 'details': result.stderr or result.stdout})
193
+ tui_started = True
194
+
195
+ print(json.dumps({
196
+ 'ok': True,
197
+ 'mode': 'B+C' if do_tui else 'B',
198
+ 'workspace': str(workspace),
199
+ 'agent': agent,
200
+ 'instance_name': instance_name,
201
+ 'target_dir': str(target_dir),
202
+ 'folder_rel_path': folder_rel_path,
203
+ 'entry_rel_path': entry_rel_path,
204
+ 'tui_started': tui_started,
205
+ 'session': instance_name,
206
+ 'message': message,
207
+ }, ensure_ascii=False))
208
+
209
+
210
+ if __name__ == '__main__':
211
+ main()
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ OPENCLAW_ROOT = Path.home() / '.openclaw'
7
+ AGENT = 'playgrounds'
8
+ WORKSPACE = OPENCLAW_ROOT / 'workspace-playgrounds'
9
+ REMIX_DIR = WORKSPACE / 'remix'
10
+
11
+
12
+ def fail(payload, code=1):
13
+ print(json.dumps(payload, ensure_ascii=False))
14
+ raise SystemExit(code)
15
+
16
+
17
+ def run(cmd):
18
+ return subprocess.run(cmd, capture_output=True, text=True)
19
+
20
+
21
+ def main():
22
+ result = run(['mc', 'new', AGENT])
23
+ output = (result.stdout or '') + (result.stderr or '')
24
+ if result.returncode != 0 and '已存在' not in output and 'exists' not in output.lower():
25
+ fail({'prepared': False, 'error': 'mc_new_failed', 'agent': AGENT, 'details': output.strip()})
26
+
27
+ WORKSPACE.mkdir(parents=True, exist_ok=True)
28
+ REMIX_DIR.mkdir(parents=True, exist_ok=True)
29
+
30
+ print(json.dumps({
31
+ 'prepared': True,
32
+ 'agent': AGENT,
33
+ 'workspace': str(WORKSPACE),
34
+ 'remix_dir': str(REMIX_DIR),
35
+ }, ensure_ascii=False))
36
+
37
+
38
+ if __name__ == '__main__':
39
+ main()
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import re
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ SKILL_ROOT = Path(__file__).resolve().parent.parent
8
+ INDEX_PATH = SKILL_ROOT / 'template-index.json'
9
+ TEMPLATES_ROOT = SKILL_ROOT / 'templates'
10
+
11
+
12
+ def fail(payload, code=1):
13
+ print(json.dumps(payload, ensure_ascii=False))
14
+ raise SystemExit(code)
15
+
16
+
17
+ def normalize(text: str) -> str:
18
+ return ''.join(str(text).lower().replace('-', '').replace('_', '').split())
19
+
20
+
21
+ def parse_args(argv):
22
+ query = ''
23
+ series = ''
24
+ number = ''
25
+ i = 0
26
+ while i < len(argv):
27
+ arg = argv[i]
28
+ if arg == '--query' and i + 1 < len(argv):
29
+ query = argv[i + 1]
30
+ i += 2
31
+ elif arg == '--series' and i + 1 < len(argv):
32
+ series = argv[i + 1]
33
+ i += 2
34
+ elif arg == '--number' and i + 1 < len(argv):
35
+ number = argv[i + 1]
36
+ i += 2
37
+ else:
38
+ fail({'found': False, 'error': 'unknown_argument', 'message': f'未知参数: {arg}'})
39
+ return query.strip(), series.strip(), number.strip()
40
+
41
+
42
+ def flatten_entries(index):
43
+ entries = []
44
+ if isinstance(index, dict):
45
+ for s, numbers in index.items():
46
+ for n, entry in numbers.items():
47
+ e = dict(entry)
48
+ e.setdefault('系列', s)
49
+ e.setdefault('编号', n)
50
+ entries.append(e)
51
+ elif isinstance(index, list):
52
+ for entry in index:
53
+ folder = str(entry.get('文件夹', ''))
54
+ m = re.match(r'([A-Za-z])(\d+)_', folder)
55
+ e = dict(entry)
56
+ if m:
57
+ e.setdefault('系列', m.group(1).upper())
58
+ e.setdefault('编号', m.group(2))
59
+ e.setdefault('名称', entry.get('任务名称', folder))
60
+ path = TEMPLATES_ROOT / folder
61
+ e.setdefault('路径', str(path))
62
+ e.setdefault('入口文件', str(path / 'index.html'))
63
+ entries.append(e)
64
+ return entries
65
+
66
+
67
+ def build_candidate(entry):
68
+ return {
69
+ 'series': entry.get('系列', ''),
70
+ 'number': entry.get('编号', ''),
71
+ 'name': entry.get('名称', entry.get('任务名称', '')),
72
+ 'template_path': entry.get('路径', ''),
73
+ 'entry_file': entry.get('入口文件', ''),
74
+ }
75
+
76
+
77
+ def score_entry(entry, query):
78
+ qn = normalize(query)
79
+ stop_words = set('的了要我想用帮开始那个做一个模板作品网页页面老师相关查找查询系列里面帮我和')
80
+ filtered = ''.join(ch for ch in query if ch not in stop_words and not ch.isspace())
81
+
82
+ name = str(entry.get('名称', entry.get('任务名称', '')))
83
+ title = str(entry.get('一句话说明', ''))
84
+ folder = str(entry.get('文件夹名', entry.get('文件夹', '')))
85
+ keywords = [str(x) for x in entry.get('关键词', [])]
86
+ keywords += [str(x) for x in entry.get('适合学生类型', [])]
87
+ keywords += [str(x) for x in entry.get('训练重点', [])]
88
+ if entry.get('主能力标签'):
89
+ keywords.append(str(entry.get('主能力标签')))
90
+ if entry.get('任务类型标签'):
91
+ keywords.append(str(entry.get('任务类型标签')))
92
+
93
+ haystacks = [name, title, folder] + keywords
94
+ hay_norm = [normalize(x) for x in haystacks]
95
+
96
+ score = 0
97
+ if any(qn and qn in h for h in hay_norm):
98
+ score += 100
99
+ if filtered:
100
+ hit_chars = 0
101
+ for ch in filtered:
102
+ if any(ch in h for h in haystacks):
103
+ hit_chars += 1
104
+ score += hit_chars * 10
105
+ if query and any(query in x for x in haystacks):
106
+ score += 30
107
+ return score
108
+
109
+
110
+ def main():
111
+ query, series, number = parse_args(sys.argv[1:])
112
+
113
+ if not query and not series and not number:
114
+ fail({'found': False, 'error': 'missing_query', 'message': '请提供 --query 或 --series/--number'})
115
+ if not INDEX_PATH.exists():
116
+ fail({'found': False, 'error': 'index_not_found', 'index_path': str(INDEX_PATH)})
117
+
118
+ index = json.loads(INDEX_PATH.read_text(encoding='utf-8'))
119
+ entries = flatten_entries(index)
120
+ results = []
121
+
122
+ if series and number:
123
+ for entry in entries:
124
+ if str(entry.get('系列', '')).upper() == series.upper() and str(entry.get('编号', '')) == number:
125
+ results = [entry]
126
+ break
127
+ elif number:
128
+ results = [e for e in entries if str(e.get('编号', '')) == number]
129
+ elif query:
130
+ scored = []
131
+ for entry in entries:
132
+ s = score_entry(entry, query)
133
+ if s > 0:
134
+ scored.append((s, entry))
135
+ scored.sort(key=lambda x: (-x[0], x[1].get('系列', ''), x[1].get('编号', '')))
136
+ results = [entry for _, entry in scored]
137
+
138
+ if not results:
139
+ print(json.dumps({
140
+ 'found': False,
141
+ 'candidate_count': 0,
142
+ 'candidates': [],
143
+ 'message': '没有找到匹配模板,请补充系列、编号或更完整关键词'
144
+ }, ensure_ascii=False))
145
+ return
146
+
147
+ uniq = []
148
+ seen = set()
149
+ for entry in results:
150
+ key = (entry.get('系列', ''), entry.get('编号', ''), entry.get('路径', ''))
151
+ if key in seen:
152
+ continue
153
+ seen.add(key)
154
+ uniq.append(entry)
155
+
156
+ if len(uniq) == 1:
157
+ print(json.dumps({
158
+ 'found': True,
159
+ 'candidate_count': 1,
160
+ 'template': build_candidate(uniq[0])
161
+ }, ensure_ascii=False))
162
+ else:
163
+ print(json.dumps({
164
+ 'found': False,
165
+ 'candidate_count': len(uniq),
166
+ 'candidates': [build_candidate(e) for e in uniq]
167
+ }, ensure_ascii=False))
168
+
169
+
170
+ if __name__ == '__main__':
171
+ main()