@aiyiran/myclaw 1.1.44 → 1.1.46

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.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ migrate_templates.py
4
+
5
+ 将旧版平铺结构迁移到版本化结构:
6
+ templates/{folder}/*.html → templates/{folder}/v1/*.html
7
+
8
+ 同时为没有 _id 的模板颁发 ID,写入 __teacher__.json。
9
+
10
+ 用法:
11
+ python3 scripts/migrate_templates.py [--dry-run]
12
+ """
13
+ import json
14
+ import re
15
+ import secrets
16
+ import shutil
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ ROOT = Path(__file__).resolve().parent.parent
21
+ TEMPLATES_DIR = ROOT / 'templates'
22
+ INDEX_SCRIPT = ROOT / 'scripts' / 'build_template_index.py'
23
+
24
+
25
+ def load_json(path: Path) -> dict:
26
+ try:
27
+ return json.loads(path.read_text(encoding='utf-8'))
28
+ except Exception:
29
+ return {}
30
+
31
+
32
+ def save_json(path: Path, data: dict):
33
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
34
+
35
+
36
+ def gen_id() -> str:
37
+ return secrets.token_hex(4)
38
+
39
+
40
+ def has_version_subdir(folder: Path) -> bool:
41
+ for d in folder.iterdir():
42
+ if d.is_dir() and re.match(r'^v\d+$', d.name):
43
+ return True
44
+ return False
45
+
46
+
47
+ def main():
48
+ dry_run = '--dry-run' in sys.argv
49
+ if dry_run:
50
+ print('🔍 DRY RUN 模式,不实际修改文件\n')
51
+
52
+ if not TEMPLATES_DIR.exists():
53
+ print('❌ templates/ 目录不存在')
54
+ sys.exit(1)
55
+
56
+ folders = sorted(d for d in TEMPLATES_DIR.iterdir() if d.is_dir())
57
+ migrated = 0
58
+
59
+ for folder in folders:
60
+ if has_version_subdir(folder):
61
+ print(f'✓ 跳过(已版本化): {folder.name}')
62
+ continue
63
+
64
+ # 列出所有直接子文件(排除目录)
65
+ files = [f for f in folder.iterdir() if f.is_file()]
66
+ if not files:
67
+ print(f'⚠️ 空目录,跳过: {folder.name}')
68
+ continue
69
+
70
+ teacher_path = folder / '__teacher__.json'
71
+ teacher = load_json(teacher_path)
72
+
73
+ # 补充 _id
74
+ if not teacher.get('_id'):
75
+ teacher['_id'] = gen_id()
76
+ print(f'🆔 颁发 ID {teacher["_id"]}: {folder.name}')
77
+
78
+ # 补充 _version
79
+ if not teacher.get('_version'):
80
+ teacher['_version'] = 1
81
+
82
+ if not dry_run:
83
+ # 先写回 __teacher__.json
84
+ save_json(teacher_path, teacher)
85
+
86
+ # 创建 v1/ 并移动所有文件
87
+ v1_dir = folder / 'v1'
88
+ v1_dir.mkdir(exist_ok=True)
89
+ for f in files:
90
+ shutil.move(str(f), str(v1_dir / f.name))
91
+
92
+ print(f'📦 迁移: {folder.name} → {folder.name}/v1/')
93
+ migrated += 1
94
+
95
+ print(f'\n{"[DRY RUN] " if dry_run else ""}共迁移 {migrated} 个模板')
96
+
97
+ if not dry_run and migrated > 0:
98
+ import subprocess
99
+ subprocess.run(['python3', str(INDEX_SCRIPT)], check=True)
100
+ print('✅ 索引已重建')
101
+
102
+
103
+ if __name__ == '__main__':
104
+ main()
@@ -1,4 +1,19 @@
1
1
  #!/usr/bin/env python3
2
+ """
3
+ move_template_task.py
4
+
5
+ 将模板源目录纳入 pipeline(复制,不删除原目录)。
6
+
7
+ 流程:
8
+ 1. 校验必需文件
9
+ 2. 调用 identify_template.py(颁发/校准身份,写回源目录)
10
+ 3. 读取更新后的 __teacher__.json,拼出目标路径
11
+ 4. 复制到 templates/{folder_name}/v{N}/
12
+ 5. 重建 template-index.json
13
+
14
+ 用法:
15
+ python3 move_template_task.py <模板目录> [目标根目录]
16
+ """
2
17
  import json
3
18
  import re
4
19
  import shutil
@@ -8,86 +23,71 @@ from pathlib import Path
8
23
 
9
24
  ROOT = Path(__file__).resolve().parent.parent
10
25
  DEFAULT_TARGET = ROOT / 'templates'
26
+ IDENTIFY_SCRIPT = ROOT / 'scripts' / 'identify_template.py'
11
27
  INDEX_SCRIPT = ROOT / 'scripts' / 'build_template_index.py'
12
- REQUIRED_FILES = ['__demo__.html', '__student__.json', '__teacher__.json', '__student-view__.html', '__teacher-view__.html']
13
-
14
-
15
- def load_json(path):
16
- with path.open('r', encoding='utf-8') as f:
17
- return json.load(f)
18
-
19
-
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):
25
- category = str(teacher['任务类别']).upper()
26
- if category not in {'A', 'B', 'C'}:
27
- raise ValueError('任务类别只能是 A、B、C')
28
- return category
29
-
28
+ REQUIRED_FILES = [
29
+ '__demo__.html', '__student__.json', '__teacher__.json',
30
+ '__student-view__.html', '__teacher-view__.html',
31
+ ]
30
32
 
31
- def sanitize_name(name):
32
- return str(name).replace('/', '_').replace(' ', '_')
33
33
 
34
+ def load_json(path: Path) -> dict:
35
+ try:
36
+ return json.loads(path.read_text(encoding='utf-8'))
37
+ except Exception:
38
+ return {}
34
39
 
35
- def find_next_available_number(category, target_root):
36
- used = set()
37
- pattern = re.compile(r'^([a-zA-Z])(\d{3})_')
38
40
 
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['任务名称'])
60
- return f'{category.lower()}{number}_{name}'
41
+ def sanitize_name(name: str) -> str:
42
+ return re.sub(r'[/\\ ]', '_', str(name).strip())
61
43
 
62
44
 
63
45
  def main():
64
46
  if len(sys.argv) < 2:
65
- print('用法: python3 move_template_task.py <已有文件夹> [目标目录]')
47
+ print('用法: python3 move_template_task.py <模板目录> [目标根目录]')
66
48
  sys.exit(1)
67
49
 
68
50
  source_dir = Path(sys.argv[1]).expanduser().resolve()
69
51
  target_root = Path(sys.argv[2]).expanduser().resolve() if len(sys.argv) > 2 else DEFAULT_TARGET
70
52
 
53
+ if not source_dir.is_dir():
54
+ print(f'❌ 目录不存在: {source_dir}')
55
+ sys.exit(1)
56
+
57
+ # 1. 校验必需文件
71
58
  for filename in REQUIRED_FILES:
72
59
  if not (source_dir / filename).exists():
73
- raise FileNotFoundError(f'缺少必需文件: {filename}')
60
+ print(f'缺少必需文件: {filename}')
61
+ sys.exit(1)
74
62
 
75
- teacher_path = source_dir / '__teacher__.json'
76
- teacher = load_json(teacher_path)
77
- target_root.mkdir(parents=True, exist_ok=True)
78
- folder_name = task_folder_name(teacher, target_root)
79
- save_json(teacher_path, teacher)
63
+ # 2. 颁发/校准身份(写回源目录的 __teacher__.json
64
+ print('\n── 身份认证 ────────────────────────────────')
65
+ subprocess.run([sys.executable, str(IDENTIFY_SCRIPT), str(source_dir)], check=True)
66
+
67
+ # 3. 读取更新后的身份信息
68
+ teacher = load_json(source_dir / '__teacher__.json')
69
+ category = teacher.get('任务类别', '').upper()
70
+ number = teacher.get('任务编号', '100')
71
+ name = sanitize_name(teacher.get('任务名称', 'untitled'))
72
+ version = teacher.get('_version', 1)
73
+
74
+ folder_name = f'{category.lower()}{number}_{name}'
75
+ version_str = f'v{version}'
76
+ target_dir = target_root / folder_name / version_str
80
77
 
81
- target_dir = target_root / folder_name
82
78
  if target_dir.exists():
83
- raise FileExistsError(f'目标目录已存在: {target_dir}')
79
+ print(f' 目标目录已存在(重复操作?): {target_dir}')
80
+ sys.exit(1)
84
81
 
85
- shutil.move(str(source_dir), str(target_dir))
86
- print(f'已移动并改名到: {target_dir}')
87
- print(f'已自动分配编号: {teacher["任务编号"]}')
82
+ # 4. 复制到 templates/{folder}/v{N}/
83
+ print(f'\n── 复制文件 ────────────────────────────────')
84
+ target_root.mkdir(parents=True, exist_ok=True)
85
+ shutil.copytree(str(source_dir), str(target_dir))
86
+ print(f'✅ 已复制到: {target_dir}')
88
87
 
89
- subprocess.run(['python3', str(INDEX_SCRIPT)], check=True)
90
- print('已自动重建 templates 索引')
88
+ # 5. 重建 index
89
+ print(f'\n── 重建索引 ────────────────────────────────')
90
+ subprocess.run([sys.executable, str(INDEX_SCRIPT)], check=True)
91
91
 
92
92
 
93
93
  if __name__ == '__main__':
@@ -4,85 +4,99 @@ publish_template.py
4
4
 
5
5
  发布模板文件夹到七牛 CDN,完成后重建并上传 template-index.json。
6
6
 
7
- 流程:
8
- 1. 把文件夹内所有文件无脑上传到七牛(强制覆盖)
9
- 2. 重建 template-index.json 并上传到 CDN
7
+ CDN 路径:{CDN_PREFIX}/{folder_name}/{version}/{filename}
8
+ 版本号从 templates/{folder}/v{N}/ 子目录读取(以本地最新版本为准)。
10
9
 
11
10
  用法:
12
11
  python3 scripts/publish_template.py <模板文件夹路径>
13
12
  python3 scripts/publish_template.py --all
14
-
15
- 例:
16
- python3 scripts/publish_template.py templates/a103_做一个介绍页面
17
- python3 scripts/publish_template.py --all
18
13
  """
19
-
20
14
  import argparse
15
+ import re
21
16
  import subprocess
22
17
  import sys
23
18
  from pathlib import Path
24
19
 
25
20
  from qiniu import Auth, put_file_v2, CdnManager
26
21
 
27
- # ── 七牛配置(与 server 保持一致)──────────────────────────────────────────
28
22
  QINIU_KEY = "T3tgxM7EMx1j4VESw4m4PIfFXoOvBo-wQEOQewXX"
29
23
  QINIU_SECRET = "PVZvlKVOjX2RqlV2ILMg-QwpNMssOlpVbaEzypz0"
30
24
  QINIU_BUCKET = "yiran1"
31
25
  CDN_DOMAIN = "https://cdn.yiranlaoshi.com"
32
- CDN_PREFIX = "myclaw/live/yiran/skills/yiran-playground-template-use/templates"
26
+ CDN_INDEX_KEY = "myclaw/live/yiran/skills/yiran-playground-template-use/template-index.json"
27
+ CDN_PREFIX = "myclaw/live/yiran/skills/yiran-playground-template-use/templates"
33
28
 
34
29
  q = Auth(QINIU_KEY, QINIU_SECRET)
35
30
  cdn_manager = CdnManager(q)
36
31
 
37
32
 
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})
42
- ret, info = put_file_v2(token, key, str(local_path))
33
+ def get_latest_version_dir(folder: Path):
34
+ """返回最新 v{N} 子目录和版本号,没有则返回 (None, 0)"""
35
+ max_v = 0
36
+ result = None
37
+ for d in folder.iterdir():
38
+ if d.is_dir():
39
+ m = re.match(r'^v(\d+)$', d.name)
40
+ if m:
41
+ v = int(m.group(1))
42
+ if v > max_v:
43
+ max_v = v
44
+ result = d
45
+ return result, max_v
46
+
47
+
48
+ def upload_file(local_path: Path, cdn_key: str) -> str:
49
+ token = q.upload_token(QINIU_BUCKET, cdn_key, 3600, policy={'insertOnly': 0})
50
+ ret, info = put_file_v2(token, cdn_key, str(local_path))
43
51
  if info.status_code != 200:
44
- raise RuntimeError("上传失败 {}: {}".format(local_path.name, info))
45
- cdn_url = "{}/{}".format(CDN_DOMAIN, key)
52
+ raise RuntimeError('上传失败 {}: {}'.format(local_path.name, info))
53
+ cdn_url = '{}/{}'.format(CDN_DOMAIN, cdn_key)
46
54
  cdn_manager.refresh_urls([cdn_url])
47
55
  return cdn_url
48
56
 
49
57
 
50
- def publish(folder_path):
58
+ def publish(folder_path: Path):
51
59
  folder_name = folder_path.name
52
- files = [f for f in folder_path.iterdir() if f.is_file()]
60
+ version_dir, version_num = get_latest_version_dir(folder_path)
61
+
62
+ if not version_dir:
63
+ print('⚠️ {} 没有版本子目录,跳过'.format(folder_name))
64
+ return
65
+
66
+ version_str = 'v{}'.format(version_num)
67
+ files = [f for f in version_dir.rglob('*') if f.is_file() and f.name != '.sync-done']
53
68
 
54
69
  if not files:
55
- print("⚠️ 文件夹为空:{}".format(folder_path))
70
+ print('⚠️ 文件夹为空: {}/{}'.format(folder_name, version_str))
56
71
  return
57
72
 
58
- print("📂 发布文件夹:{}".format(folder_name))
73
+ print('📂 发布: {}/{}'.format(folder_name, version_str))
59
74
  for f in sorted(files):
60
- cdn_url = upload_file(f, folder_name)
61
- print(" ⬆️ {} {}".format(f.name, cdn_url))
62
-
63
- print("\n✅ 发布完成:{}".format(folder_name))
75
+ rel = f.relative_to(version_dir)
76
+ cdn_key = '{}/{}/{}/{}'.format(CDN_PREFIX, folder_name, version_str, rel.as_posix())
77
+ cdn_url = upload_file(f, cdn_key)
78
+ print(' ⬆️ {} {}'.format(rel, cdn_url))
64
79
 
80
+ print('✅ 完成: {}/{}\n'.format(folder_name, version_str))
65
81
 
66
82
 
67
83
  def publish_index(pipeline_root: Path):
68
- """重建 template-index.json 并上传到 CDN。"""
69
84
  index_script = pipeline_root / 'scripts' / 'build_template_index.py'
70
- print("\n🔄 重建 template-index.json ...")
85
+ print('🔄 重建 template-index.json ...')
71
86
  subprocess.run(['python3', str(index_script)], check=True)
72
87
 
73
88
  index_file = pipeline_root / 'template-index.json'
74
89
  if not index_file.is_file():
75
- print("⚠️ template-index.json 未生成,跳过上传")
90
+ print('⚠️ template-index.json 未生成,跳过上传')
76
91
  return
77
92
 
78
- key = f"{CDN_PREFIX.rsplit('/templates', 1)[0]}/template-index.json"
79
- token = q.upload_token(QINIU_BUCKET, key, 3600, policy={'insertOnly': 0})
80
- ret, info = put_file_v2(token, key, str(index_file))
93
+ token = q.upload_token(QINIU_BUCKET, CDN_INDEX_KEY, 3600, policy={'insertOnly': 0})
94
+ ret, info = put_file_v2(token, CDN_INDEX_KEY, str(index_file))
81
95
  if info.status_code != 200:
82
- raise RuntimeError(f"index 上传失败: {info}")
83
- cdn_url = f"{CDN_DOMAIN}/{key}"
96
+ raise RuntimeError('index 上传失败: {}'.format(info))
97
+ cdn_url = '{}/{}'.format(CDN_DOMAIN, CDN_INDEX_KEY)
84
98
  cdn_manager.refresh_urls([cdn_url])
85
- print(f" ⬆️ 已上传索引 → {cdn_url}")
99
+ print(' ⬆️ 已上传索引 → {}'.format(cdn_url))
86
100
 
87
101
 
88
102
  def main():
@@ -98,15 +112,15 @@ def main():
98
112
  templates_dir = pipeline_root / 'templates'
99
113
  folders = sorted(d for d in templates_dir.iterdir() if d.is_dir())
100
114
  if not folders:
101
- print("⚠️ templates/ 下没有找到任何模板文件夹")
115
+ print('⚠️ templates/ 下没有找到任何模板文件夹')
102
116
  sys.exit(0)
103
- print(f"📦 共 {len(folders)} 个模板,开始全量发布...\n")
117
+ print('📦 共 {} 个模板,开始全量发布...\n'.format(len(folders)))
104
118
  for folder in folders:
105
119
  publish(folder)
106
120
  else:
107
121
  folder = Path(args.folder).expanduser().resolve()
108
122
  if not folder.is_dir():
109
- print(f"❌ 路径不存在或不是文件夹: {folder}")
123
+ print('❌ 路径不存在或不是文件夹: {}'.format(folder))
110
124
  sys.exit(1)
111
125
  publish(folder)
112
126