@aiyiran/myclaw 1.1.34 → 1.1.35

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 (21) hide show
  1. package/assets/myclaw-artifacts.js +150 -100
  2. package/package.json +1 -1
  3. package/patches/patch-manifest.json +2 -1
  4. package/patches/patch-skill.js +18 -8
  5. package/server/sync_workspace.py +92 -0
  6. package/skills/yiran-course-template-pipeline/SKILL.md +1 -2
  7. package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2651-demo/347/224/237/346/210/220.md +0 -7
  8. package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2652-student/347/224/237/346/210/220.md +35 -0
  9. package/skills/yiran-course-template-pipeline/prompts//351/230/266/346/256/2653-teacher/347/224/237/346/210/220.md +6 -5
  10. 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 +26 -21
  11. package/skills/yiran-course-template-pipeline/references/teacher-fields.md +5 -5
  12. package/skills/yiran-course-template-pipeline/references/teacher-scaffold.json +1 -1
  13. package/skills/yiran-course-template-pipeline/scripts/build_template_index.py +14 -2
  14. package/skills/yiran-course-template-pipeline/scripts/move_template_task.py +43 -7
  15. package/skills/yiran-course-template-pipeline/scripts/publish_template.py +44 -120
  16. package/skills/yiran-course-template-pipeline/template-index.json +269 -0
  17. package/skills/yiran-course-template-pipeline/template-index.md +52 -0
  18. package/skills/yiran-course-template-pipeline//345/272/237/345/274/203/rewrite_html_refs.py +102 -0
  19. package/skills/yiran-playground-template-use/SKILL.md +2 -12
  20. package/skills/yiran-course-template-pipeline/cdn-convention.md +0 -77
  21. package/skills/yiran-playground-template-use/scripts/build_template_index.py +0 -103
@@ -4,7 +4,7 @@
4
4
  指导 agent 在已有 `__demo__.html`、`__student__.json`、`__teacher__.json` 的前提下,先生成页面,再移动文件夹。
5
5
 
6
6
  注意:
7
- 第4阶段现在拆成两个独立脚本,不再混在一个脚本里。
7
+ 第4阶段拆成三个独立脚本,不再混在一个脚本里。
8
8
 
9
9
  ---
10
10
 
@@ -29,8 +29,8 @@
29
29
  - `index.html`
30
30
 
31
31
  其中 `index.html` 使用固定脚本规则:
32
- - 左边展示“评价目标”
33
- - 右边展示“步骤”
32
+ - 左边展示"评价目标"
33
+ - 右边展示"步骤"
34
34
  - 不显示任务标题
35
35
  - 不显示一句话说明
36
36
  - 这部分不再靠临时 prompt 自由发挥,而是固定走脚本渲染
@@ -50,10 +50,7 @@ python3 scripts/build_template_views.py 任务文件夹
50
50
  ## 脚本2:move_template_task.py
51
51
 
52
52
  目标:
53
- 在页面已经生成好的前提下,把整个文件夹直接移动到:
54
- skill 目录下的 `templates/`
55
-
56
- 同时按规则改名,例如:
53
+ 在页面已经生成好的前提下,把整个文件夹直接移动到 skill 目录下的 `templates/`,同时按规则改名,例如:
57
54
  `a100_给依然老师设计一个AI画像`
58
55
 
59
56
  调用方式:
@@ -68,32 +65,40 @@ python3 scripts/move_template_task.py 任务文件夹
68
65
 
69
66
  ---
70
67
 
71
- ---
72
-
73
68
  ## 脚本3:publish_template.py
74
69
 
75
70
  目标:
76
- 把模板文件夹内所有资源上传到七牛 CDN,并重写 HTML 中的资源引用为 CDN 绝对地址。
71
+ 把模板文件夹内所有文件上传到七牛 CDN,完成后重建并上传 `template-index.json`。
77
72
 
78
73
  处理逻辑:
79
- - 扫描文件夹内所有 HTML 文件的资源引用(`src`、`href`、`url()`)
80
- - 对每个引用,尝试在本地找到对应文件(相对路径直接拼接,外部 URL 按文件名查找)
81
- - 本地能找到的文件:上传到七牛 → 重写引用为新 CDN URL
82
- - 本地找不到的引用:保持原样不动
74
+ - 遍历文件夹内所有文件,逐一上传(强制覆盖)
75
+ - 相对路径在 CDN 上天然可用,无需重写引用
83
76
 
84
- CDN 路径规则(见 `cdn-convention.md`):
77
+ **CDN 配置:**
78
+
79
+ | 项 | 值 |
80
+ |---|---|
81
+ | Bucket | `yiran1` |
82
+ | CDN 域名 | `https://cdn.yiranlaoshi.com` |
83
+
84
+ **CDN 路径规则:**
85
85
  ```
86
86
  https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/templates/{文件夹名}/{文件名}
87
87
  ```
88
88
 
89
+ 七牛 key:
90
+ ```
91
+ myclaw/live/yiran/skills/yiran-playground-template-use/templates/{文件夹名}/{文件名}
92
+ ```
93
+
89
94
  调用方式:
90
95
  ```bash
91
- python3 scripts/publish_template.py templates下的目标文件夹路径
92
- ```
96
+ # 发布单个模板
97
+ python3 scripts/publish_template.py templates/a103_做一个介绍页面
93
98
 
94
- 适用场景:
95
- - move 完成后,准备正式对外使用模板
96
- - 需要保证所有资源链接在任意环境下均可访问
99
+ # 全量发布所有模板
100
+ python3 scripts/publish_template.py --all
101
+ ```
97
102
 
98
103
  ---
99
104
 
@@ -103,5 +108,5 @@ python3 scripts/publish_template.py templates下的目标文件夹路径
103
108
  2. 先 build,再 move,再 publish
104
109
  3. build 只在原目录生成页面,不移动
105
110
  4. move 只负责移动和改名,并自动触发索引构建
106
- 5. publish 负责资源上传和链接重写,是最终对外发布的步骤
111
+ 5. publish 负责全量上传到 CDN,是最终对外发布的步骤
107
112
  6. 文件命名固定,不临时改名
@@ -111,7 +111,7 @@ __teacher__.json 中有两组固定标签:
111
111
 
112
112
  主能力标签只能从下面 4 个里选 1 个:
113
113
 
114
- ### 1. 构思能力
114
+ ### 第一能力:构思能力
115
115
  适合:
116
116
  - 重点在想主题
117
117
  - 重点在想设定
@@ -122,7 +122,7 @@ __teacher__.json 中有两组固定标签:
122
122
  - 先想一个故事
123
123
  - 先想一个主题
124
124
 
125
- ### 2. 表达构建
125
+ ### 第二能力:表达构建
126
126
  适合:
127
127
  - 重点在把内容组织出来
128
128
  - 重点在把想法做成页面或作品
@@ -133,7 +133,7 @@ __teacher__.json 中有两组固定标签:
133
133
  - 做展示页
134
134
  - 做图文页面
135
135
 
136
- ### 3. 问题判断
136
+ ### 第三能力:问题判断
137
137
  适合:
138
138
  - 重点在发现问题
139
139
  - 重点在判断哪里不合适
@@ -144,7 +144,7 @@ __teacher__.json 中有两组固定标签:
144
144
  - 找出页面问题
145
145
  - 判断哪里需要调整
146
146
 
147
- ### 4. 迭代推进
147
+ ### 第四能力:迭代推进
148
148
  适合:
149
149
  - 重点在继续修改
150
150
  - 重点在一版一版优化
@@ -243,7 +243,7 @@ __teacher__.json 中有两组固定标签:
243
243
 
244
244
  ### 4)主能力标签
245
245
  - 只能选 1 个
246
- - 固定集合:1.构思能力 / 2.表达构建 / 3.问题判断 / 4.迭代推进
246
+ - 固定集合:第一能力:构思能力 / 第二能力:表达构建 / 第三能力:问题判断 / 第四能力:迭代推进
247
247
 
248
248
  ### 5)任务类型标签
249
249
  - 只能选 1 个
@@ -2,7 +2,7 @@
2
2
  "任务类别": "请填写任务类别,只能使用 A、B、C。其中 A=启动类,B=结构类,C=迭代类",
3
3
  "任务编号": "请填写三位编号字符串,编号范围为 100 到 999。每个类别单独从 100 开始,后续按顺序递增,例如 100、101、102",
4
4
  "任务名称": "请填写任务名称,应和 __student__.json 中的任务标题一致或高度一致",
5
- "主能力标签": "请从固定标签中只选一个。固定集合:1.构思能力 / 2.表达构建 / 3.问题判断 / 4.迭代推进。它回答的是这个任务主要训练什么能力",
5
+ "主能力标签": "请从固定标签中只选一个。固定集合:第一能力:构思能力 / 第二能力:表达构建 / 第三能力:问题判断 / 第四能力:迭代推进。它回答的是这个任务主要训练什么能力",
6
6
  "任务类型标签": "请从固定标签中只选一个。固定集合:游戏机制 / 角色交互 / 视听表达 / 改编优化。它回答的是这个任务最终属于什么作品类型",
7
7
  "适合学生类型": [
8
8
  "请填写一个适合学生类型标签,例如:初次接触"
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  import json
3
3
  import re
4
+ from datetime import datetime, timezone
4
5
  from pathlib import Path
6
+ from typing import Optional
5
7
 
6
8
  ROOT = Path(__file__).resolve().parent.parent
7
9
  TEMPLATES_DIR = ROOT / 'templates'
@@ -23,7 +25,7 @@ def parse_folder_name(name: str):
23
25
  return None, None, None
24
26
 
25
27
 
26
- def get_entry_file(folder: Path) -> Path | None:
28
+ def get_entry_file(folder: Path) -> Optional[Path]:
27
29
  for name in ['index.html', '__demo__.html', '__student-view__.html']:
28
30
  if (folder / name).exists():
29
31
  return folder / name
@@ -61,6 +63,12 @@ def main():
61
63
  ])))
62
64
 
63
65
  entry = get_entry_file(folder)
66
+
67
+ # 文件夹内所有文件列表 + 最新 mtime
68
+ files = sorted(f.name for f in folder.iterdir() if f.is_file())
69
+ folder_mtime = max((f.stat().st_mtime for f in folder.iterdir() if f.is_file()), default=0)
70
+ updated_at = datetime.fromtimestamp(folder_mtime, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
71
+
64
72
  record = {
65
73
  '系列': series,
66
74
  '编号': number,
@@ -74,6 +82,8 @@ def main():
74
82
  '主能力标签': 能力标签,
75
83
  '任务类型标签': 类型标签,
76
84
  '关键词': keywords,
85
+ 'files': files,
86
+ 'updated_at': updated_at,
77
87
  }
78
88
 
79
89
  if series not in index:
@@ -81,7 +91,9 @@ def main():
81
91
  index[series][number] = record
82
92
  items_for_md.append(record)
83
93
 
84
- OUTPUT_JSON.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding='utf-8')
94
+ index_updated_at = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
95
+ output = {'index_updated_at': index_updated_at, 'templates': index}
96
+ OUTPUT_JSON.write_text(json.dumps(output, ensure_ascii=False, indent=2), encoding='utf-8')
85
97
 
86
98
  lines = ['# 模板索引', '', '每次 templates 内模板有变化后,都应重新运行本脚本。', '']
87
99
  for item in items_for_md:
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  import json
3
+ import re
3
4
  import shutil
4
5
  import subprocess
5
6
  import sys
@@ -16,16 +17,46 @@ def load_json(path):
16
17
  return json.load(f)
17
18
 
18
19
 
19
- def task_folder_name(teacher):
20
+ def save_json(path, data):
21
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
22
+
23
+
24
+ def normalize_category(teacher):
20
25
  category = str(teacher['任务类别']).upper()
21
26
  if category not in {'A', 'B', 'C'}:
22
27
  raise ValueError('任务类别只能是 A、B、C')
28
+ return category
29
+
30
+
31
+ def sanitize_name(name):
32
+ return str(name).replace('/', '_').replace(' ', '_')
33
+
23
34
 
24
- number = str(teacher['任务编号']).zfill(3)
25
- if not number.isdigit() or not (100 <= int(number) <= 999):
26
- raise ValueError('任务编号必须是 100 到 999 的三位数字字符串')
35
+ def find_next_available_number(category, target_root):
36
+ used = set()
37
+ pattern = re.compile(r'^([a-zA-Z])(\d{3})_')
27
38
 
28
- name = str(teacher['任务名称']).replace('/', '_').replace(' ', '_')
39
+ for folder in target_root.iterdir() if target_root.exists() else []:
40
+ if not folder.is_dir():
41
+ continue
42
+ m = pattern.match(folder.name)
43
+ if not m:
44
+ continue
45
+ if m.group(1).upper() != category:
46
+ continue
47
+ used.add(int(m.group(2)))
48
+
49
+ for num in range(100, 1000):
50
+ if num not in used:
51
+ return f'{num:03d}'
52
+ raise RuntimeError(f'{category} 类编号已用尽')
53
+
54
+
55
+ def task_folder_name(teacher, target_root):
56
+ category = normalize_category(teacher)
57
+ number = find_next_available_number(category, target_root)
58
+ teacher['任务编号'] = number
59
+ name = sanitize_name(teacher['任务名称'])
29
60
  return f'{category.lower()}{number}_{name}'
30
61
 
31
62
 
@@ -41,14 +72,19 @@ def main():
41
72
  if not (source_dir / filename).exists():
42
73
  raise FileNotFoundError(f'缺少必需文件: {filename}')
43
74
 
44
- teacher = load_json(source_dir / '__teacher__.json')
75
+ teacher_path = source_dir / '__teacher__.json'
76
+ teacher = load_json(teacher_path)
45
77
  target_root.mkdir(parents=True, exist_ok=True)
46
- target_dir = target_root / task_folder_name(teacher)
78
+ folder_name = task_folder_name(teacher, target_root)
79
+ save_json(teacher_path, teacher)
80
+
81
+ target_dir = target_root / folder_name
47
82
  if target_dir.exists():
48
83
  raise FileExistsError(f'目标目录已存在: {target_dir}')
49
84
 
50
85
  shutil.move(str(source_dir), str(target_dir))
51
86
  print(f'已移动并改名到: {target_dir}')
87
+ print(f'已自动分配编号: {teacher["任务编号"]}')
52
88
 
53
89
  subprocess.run(['python3', str(INDEX_SCRIPT)], check=True)
54
90
  print('已自动重建 templates 索引')
@@ -5,25 +5,22 @@ publish_template.py
5
5
  发布模板文件夹到七牛 CDN,完成后重建并上传 template-index.json。
6
6
 
7
7
  流程:
8
- 1. 扫描文件夹内所有 HTML 文件的资源引用(src / href / url())
9
- 2. 对每个引用,找到本地对应文件(相对路径 or 同名本地文件)
10
- 3. 上传本地文件到七牛,key = myclaw/live/yiran/skills/yiran-playground-template-use/templates/{文件夹名}/{文件名}
11
- 4. 重写 HTML 中的原引用为新 CDN URL
12
- 5. 上传修改后的 HTML
13
- 6. 重建 template-index.json 并上传到 CDN
8
+ 1. 把文件夹内所有文件无脑上传到七牛(强制覆盖)
9
+ 2. 重建 template-index.json 并上传到 CDN
14
10
 
15
11
  用法:
16
12
  python3 scripts/publish_template.py <模板文件夹路径>
13
+ python3 scripts/publish_template.py --all
17
14
 
18
15
  例:
19
16
  python3 scripts/publish_template.py templates/a103_做一个介绍页面
17
+ python3 scripts/publish_template.py --all
20
18
  """
21
19
 
22
- import re
20
+ import argparse
23
21
  import subprocess
24
22
  import sys
25
23
  from pathlib import Path
26
- from urllib.parse import urlparse, unquote
27
24
 
28
25
  from qiniu import Auth, put_file_v2, CdnManager
29
26
 
@@ -37,119 +34,34 @@ CDN_PREFIX = "myclaw/live/yiran/skills/yiran-playground-template-use/templates
37
34
  q = Auth(QINIU_KEY, QINIU_SECRET)
38
35
  cdn_manager = CdnManager(q)
39
36
 
40
- # ── 资源引用正则 ────────────────────────────────────────────────────────────
41
- # 匹配 src="..." href="..." url(...) 三种形式
42
- ATTR_RE = re.compile(r'(src|href)=["\']([^"\']+)["\']', re.IGNORECASE)
43
- URL_RE = re.compile(r'url\(["\']?([^)"\']+)["\']?\)', re.IGNORECASE)
44
37
 
45
- # 不处理的协议前缀
46
- SKIP_PREFIXES = ('data:', '#', 'mailto:', 'javascript:')
47
-
48
-
49
- def is_external(url: str) -> bool:
50
- return url.startswith('http://') or url.startswith('https://') or url.startswith('//')
51
-
52
-
53
- def find_local_file(ref: str, base_dir: Path) -> Path | None:
54
- """
55
- 给定一个资源引用,尝试在 base_dir 中找到对应的本地文件。
56
- - 相对路径 → 直接拼接
57
- - 外部 URL → 取 path 最后一段文件名,在 base_dir 中查找
58
- """
59
- if any(ref.startswith(p) for p in SKIP_PREFIXES):
60
- return None
61
-
62
- if is_external(ref):
63
- filename = Path(unquote(urlparse(ref).path)).name
64
- candidate = base_dir / filename
65
- return candidate if candidate.is_file() else None
66
- else:
67
- candidate = (base_dir / ref).resolve()
68
- # 必须在 base_dir 内,防止路径穿越
69
- if candidate.is_file() and str(candidate).startswith(str(base_dir.resolve())):
70
- return candidate
71
- return None
72
-
73
-
74
- def upload_file(local_path: Path, folder_name: str) -> str:
75
- """上传单个文件到七牛,返回 CDN URL。"""
76
- key = f"{CDN_PREFIX}/{folder_name}/{local_path.name}"
77
- token = q.upload_token(QINIU_BUCKET, key, 3600)
38
+ def upload_file(local_path, folder_name):
39
+ """上传单个文件到七牛(强制覆盖),返回 CDN URL。"""
40
+ key = "{}/{}/{}".format(CDN_PREFIX, folder_name, local_path.name)
41
+ token = q.upload_token(QINIU_BUCKET, key, 3600, policy={'insertOnly': 0})
78
42
  ret, info = put_file_v2(token, key, str(local_path))
79
- if info.status_code not in (200, 614): # 614 = 已存在
80
- raise RuntimeError(f"上传失败 {local_path.name}: {info}")
81
- cdn_url = f"{CDN_DOMAIN}/{key}"
43
+ if info.status_code != 200:
44
+ raise RuntimeError("上传失败 {}: {}".format(local_path.name, info))
45
+ cdn_url = "{}/{}".format(CDN_DOMAIN, key)
82
46
  cdn_manager.refresh_urls([cdn_url])
83
47
  return cdn_url
84
48
 
85
49
 
86
- def rewrite_html(html: str, ref_map: dict[str, str]) -> str:
87
- """将 HTML 中的旧引用替换为新 CDN URL。"""
88
- def replace_attr(m):
89
- attr, val = m.group(1), m.group(2)
90
- new_val = ref_map.get(val, val)
91
- return f'{attr}="{new_val}"'
92
-
93
- def replace_url(m):
94
- val = m.group(1)
95
- new_val = ref_map.get(val, val)
96
- return f'url("{new_val}")'
97
-
98
- html = ATTR_RE.sub(replace_attr, html)
99
- html = URL_RE.sub(replace_url, html)
100
- return html
101
-
102
-
103
- def collect_refs(html: str) -> set[str]:
104
- refs = set()
105
- for m in ATTR_RE.finditer(html):
106
- refs.add(m.group(2))
107
- for m in URL_RE.finditer(html):
108
- refs.add(m.group(1))
109
- return refs
110
-
111
-
112
- def publish(folder_path: Path):
50
+ def publish(folder_path):
113
51
  folder_name = folder_path.name
114
- html_files = list(folder_path.glob('*.html'))
52
+ files = [f for f in folder_path.iterdir() if f.is_file()]
115
53
 
116
- if not html_files:
117
- print(f"⚠️ 未找到 HTML 文件:{folder_path}")
54
+ if not files:
55
+ print("⚠️ 文件夹为空:{}".format(folder_path))
118
56
  return
119
57
 
120
- print(f"📂 发布文件夹:{folder_name}")
121
-
122
- # ── 收集所有 HTML 中的资源引用 ──────────────────────────────────────────
123
- all_refs: dict[str, str] = {} # 原始引用 CDN URL
124
-
125
- for html_file in html_files:
126
- html = html_file.read_text(encoding='utf-8')
127
- for ref in collect_refs(html):
128
- if ref in all_refs:
129
- continue
130
- local = find_local_file(ref, folder_path)
131
- if local:
132
- print(f" ⬆️ 上传资源: {local.name} ← {ref[:60]}")
133
- cdn_url = upload_file(local, folder_name)
134
- all_refs[ref] = cdn_url
135
- print(f" → {cdn_url}")
136
- else:
137
- if not any(ref.startswith(p) for p in SKIP_PREFIXES):
138
- print(f" ⏭️ 跳过(本地未找到): {ref[:80]}")
58
+ print("📂 发布文件夹:{}".format(folder_name))
59
+ for f in sorted(files):
60
+ cdn_url = upload_file(f, folder_name)
61
+ print(" ⬆️ {} → {}".format(f.name, cdn_url))
139
62
 
140
- # ── 重写并上传 HTML 文件 ─────────────────────────────────────────────────
141
- for html_file in html_files:
142
- html = html_file.read_text(encoding='utf-8')
143
- new_html = rewrite_html(html, all_refs)
63
+ print("\n✅ 发布完成:{}".format(folder_name))
144
64
 
145
- # 写回本地(覆盖)
146
- html_file.write_text(new_html, encoding='utf-8')
147
-
148
- print(f" ⬆️ 上传 HTML: {html_file.name}")
149
- cdn_url = upload_file(html_file, folder_name)
150
- print(f" → {cdn_url}")
151
-
152
- print(f"\n✅ 发布完成:{folder_name}")
153
65
 
154
66
 
155
67
  def publish_index(pipeline_root: Path):
@@ -164,9 +76,9 @@ def publish_index(pipeline_root: Path):
164
76
  return
165
77
 
166
78
  key = f"{CDN_PREFIX.rsplit('/templates', 1)[0]}/template-index.json"
167
- token = q.upload_token(QINIU_BUCKET, key, 3600)
79
+ token = q.upload_token(QINIU_BUCKET, key, 3600, policy={'insertOnly': 0})
168
80
  ret, info = put_file_v2(token, key, str(index_file))
169
- if info.status_code not in (200, 614):
81
+ if info.status_code != 200:
170
82
  raise RuntimeError(f"index 上传失败: {info}")
171
83
  cdn_url = f"{CDN_DOMAIN}/{key}"
172
84
  cdn_manager.refresh_urls([cdn_url])
@@ -174,18 +86,30 @@ def publish_index(pipeline_root: Path):
174
86
 
175
87
 
176
88
  def main():
177
- if len(sys.argv) < 2:
178
- print("用法: python3 publish_template.py <模板文件夹路径>")
179
- sys.exit(1)
89
+ parser = argparse.ArgumentParser(description='发布模板到七牛 CDN')
90
+ group = parser.add_mutually_exclusive_group(required=True)
91
+ group.add_argument('folder', nargs='?', help='单个模板文件夹路径')
92
+ group.add_argument('--all', action='store_true', help='发布 templates/ 下所有模板')
93
+ args = parser.parse_args()
180
94
 
181
- folder = Path(sys.argv[1]).expanduser().resolve()
182
- if not folder.is_dir():
183
- print(f"❌ 路径不存在或不是文件夹: {folder}")
184
- sys.exit(1)
95
+ pipeline_root = Path(__file__).resolve().parent.parent
185
96
 
186
- publish(folder)
97
+ if args.all:
98
+ templates_dir = pipeline_root / 'templates'
99
+ folders = sorted(d for d in templates_dir.iterdir() if d.is_dir())
100
+ if not folders:
101
+ print("⚠️ templates/ 下没有找到任何模板文件夹")
102
+ sys.exit(0)
103
+ print(f"📦 共 {len(folders)} 个模板,开始全量发布...\n")
104
+ for folder in folders:
105
+ publish(folder)
106
+ else:
107
+ folder = Path(args.folder).expanduser().resolve()
108
+ if not folder.is_dir():
109
+ print(f"❌ 路径不存在或不是文件夹: {folder}")
110
+ sys.exit(1)
111
+ publish(folder)
187
112
 
188
- pipeline_root = Path(__file__).resolve().parent.parent
189
113
  publish_index(pipeline_root)
190
114
 
191
115