@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
|
@@ -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,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()
|