@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
@@ -484,17 +484,44 @@
484
484
  return tb.localeCompare(ta);
485
485
  });
486
486
 
487
- // 黑名单:隐藏以下文件(__XXX__ 格式的内部文件),豁免 student-view.html
487
+ // 黑名单:隐藏以下文件(__XXX__ 格式的内部文件以及系统文件),豁免 student-view.html
488
488
  var ARTIFACT_BLACKLIST = [
489
489
  '__demo__.html',
490
490
  '__student__.json',
491
491
  '__teacher__.json',
492
492
  '__teacher-view__.html',
493
+ 'USER.md',
494
+ 'TOOLS.md',
495
+ 'SOUL.md',
496
+ 'MEMORY.md',
497
+ 'IDENTITY.md',
498
+ 'HEARTBEAT.md',
499
+ 'BOOTSTRAP.md',
500
+ 'AGENTS.md'
493
501
  ];
502
+
503
+ // 黑名单目录:隐藏特定的系统目录
504
+ var DIR_BLACKLIST = [
505
+ 'memory/',
506
+ '.openclaw/'
507
+ ];
508
+
494
509
  if (!showHiddenArtifacts) {
495
510
  sorted = sorted.filter(function (asset) {
496
- var basename = (asset.path || asset.name || '').split('/').pop();
497
- return ARTIFACT_BLACKLIST.indexOf(basename) === -1;
511
+ var path = asset.path || asset.name || '';
512
+ var basename = path.split('/').pop();
513
+
514
+ // 1. 检查文件名是否包含在黑名单中
515
+ if (ARTIFACT_BLACKLIST.indexOf(basename) !== -1) return false;
516
+
517
+ // 2. 检查路径是否以黑名单目录开头
518
+ for (var i = 0; i < DIR_BLACKLIST.length; i++) {
519
+ if (path.indexOf(DIR_BLACKLIST[i]) === 0) {
520
+ return false;
521
+ }
522
+ }
523
+
524
+ return true;
498
525
  });
499
526
  }
500
527
 
@@ -2170,16 +2197,27 @@
2170
2197
  // ── 头部 ──
2171
2198
  var head = document.createElement('div');
2172
2199
  head.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid rgba(255,255,255,0.1);flex-shrink:0;';
2200
+ var headLeft = document.createElement('span');
2201
+ headLeft.style.cssText = 'display:flex;align-items:center;gap:10px;';
2202
+
2173
2203
  var headTitle = document.createElement('span');
2174
2204
  headTitle.textContent = '📋 作品模板库';
2175
2205
  headTitle.style.cssText = 'font-size:14px;font-weight:bold;';
2206
+
2207
+ var syncStatus = document.createElement('span');
2208
+ syncStatus.style.cssText = 'font-size:11px;color:rgba(205,214,244,0.4);transition:opacity 0.3s;';
2209
+ syncStatus.textContent = '⟳ 检查更新...';
2210
+
2211
+ headLeft.appendChild(headTitle);
2212
+ headLeft.appendChild(syncStatus);
2213
+
2176
2214
  var headClose = document.createElement('span');
2177
2215
  headClose.textContent = '✕';
2178
2216
  headClose.style.cssText = 'cursor:pointer;font-size:14px;padding:2px 8px;border-radius:3px;transition:background 0.12s;';
2179
2217
  headClose.onmouseenter = function () { headClose.style.background = 'rgba(255,255,255,0.1)'; };
2180
2218
  headClose.onmouseleave = function () { headClose.style.background = 'none'; };
2181
2219
  headClose.onclick = function () { overlay.remove(); };
2182
- head.appendChild(headTitle);
2220
+ head.appendChild(headLeft);
2183
2221
  head.appendChild(headClose);
2184
2222
  box.appendChild(head);
2185
2223
 
@@ -2319,108 +2357,120 @@
2319
2357
  loadingEl.textContent = '加载中...';
2320
2358
  leftPane.appendChild(loadingEl);
2321
2359
 
2322
- fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2323
- .then(function (r) {
2324
- if (!r.ok) throw new Error('HTTP ' + r.status);
2325
- return r.json();
2326
- })
2327
- .then(function (indexData) {
2328
- // 按系列、编号升序展平
2329
- var templates = [];
2330
- Object.keys(indexData).sort().forEach(function (series) {
2331
- Object.keys(indexData[series]).sort(function (a, b) { return parseInt(a) - parseInt(b); }).forEach(function (num) {
2332
- templates.push(indexData[series][num]);
2333
- });
2360
+ function flattenTemplates(indexData) {
2361
+ // 兼容新格式 {index_updated_at, templates: {...}} 和旧格式 {A: {...}}
2362
+ var raw = indexData.templates || indexData;
2363
+ var list = [];
2364
+ Object.keys(raw).sort().forEach(function (series) {
2365
+ Object.keys(raw[series]).sort(function (a, b) { return parseInt(a) - parseInt(b); }).forEach(function (num) {
2366
+ list.push(raw[series][num]);
2334
2367
  });
2368
+ });
2369
+ return list;
2370
+ }
2335
2371
 
2336
- leftPane.textContent = '';
2337
- var activeRow = null;
2338
-
2339
- templates.forEach(function (tpl, idx) {
2340
- var row = document.createElement('div');
2341
- row.style.cssText = 'padding:10px;border-radius:5px;cursor:pointer;transition:background 0.12s;';
2342
-
2343
- function setActive() {
2344
- if (activeRow) activeRow.style.background = 'transparent';
2345
- activeRow = row;
2346
- row.style.background = 'rgba(100,149,237,0.15)';
2347
- currentTpl = tpl;
2348
- setFooterStatus('');
2349
-
2350
- // 更新右侧 header
2351
- rightHeader.innerHTML = '';
2352
-
2353
- var infoSpan = document.createElement('span');
2354
- infoSpan.style.cssText = 'font-size:12px;color:#cdd6f4;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
2355
- infoSpan.textContent = tpl['系列'] + tpl['编号'] + ' ' + tpl['名称'];
2356
-
2357
- var useBtn = document.createElement('button');
2358
- useBtn.textContent = '✨ 使用模板';
2359
- useBtn.style.cssText = 'flex-shrink:0;padding:5px 14px;background:#a78bfa;border:none;border-radius:4px;color:#fff;font-size:12px;font-family:monospace;cursor:pointer;transition:background 0.15s;white-space:nowrap;';
2360
- useBtn.onmouseenter = function () { useBtn.style.background = '#8b5cf6'; };
2361
- useBtn.onmouseleave = function () { useBtn.style.background = '#a78bfa'; };
2362
- useBtn.onclick = function () {
2363
- var promptText = '我要使用' + tpl['系列'] + tpl['编号'] + '模板:' + tpl['名称'] + '。' + tpl['一句话说明'];
2364
- navigator.clipboard.writeText(promptText).then(function () {
2365
- useBtn.textContent = '✓ 提示词已复制';
2366
- useBtn.style.background = '#10b981';
2367
- setTimeout(function () {
2368
- useBtn.textContent = '✨ 使用模板';
2369
- useBtn.style.background = '#a78bfa';
2370
- }, 2000);
2371
- });
2372
- };
2373
-
2374
- rightHeader.appendChild(infoSpan);
2375
- rightHeader.appendChild(useBtn);
2372
+ function renderTemplateList(templates) {
2373
+ leftPane.textContent = '';
2374
+ var activeRow = null;
2375
+
2376
+ templates.forEach(function (tpl) {
2377
+ var row = document.createElement('div');
2378
+ row.style.cssText = 'padding:10px;border-radius:5px;cursor:pointer;transition:background 0.12s;';
2379
+
2380
+ function setActive() {
2381
+ if (activeRow) activeRow.style.background = 'transparent';
2382
+ activeRow = row;
2383
+ row.style.background = 'rgba(100,149,237,0.15)';
2384
+ currentTpl = tpl;
2385
+ setFooterStatus('');
2386
+ rightHeader.innerHTML = '';
2387
+ var infoSpan = document.createElement('span');
2388
+ infoSpan.style.cssText = 'font-size:12px;color:#cdd6f4;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
2389
+ infoSpan.textContent = tpl['系列'] + tpl['编号'] + ' ' + tpl['名称'];
2390
+ var useBtn = document.createElement('button');
2391
+ useBtn.textContent = ' 使用模板';
2392
+ useBtn.style.cssText = 'flex-shrink:0;padding:5px 14px;background:#a78bfa;border:none;border-radius:4px;color:#fff;font-size:12px;font-family:monospace;cursor:pointer;transition:background 0.15s;white-space:nowrap;';
2393
+ useBtn.onmouseenter = function () { useBtn.style.background = '#8b5cf6'; };
2394
+ useBtn.onmouseleave = function () { useBtn.style.background = '#a78bfa'; };
2395
+ useBtn.onclick = function () {
2396
+ var promptText = '我要使用' + tpl['系列'] + tpl['编号'] + '模板:' + tpl['名称'] + '。' + tpl['一句话说明'];
2397
+ navigator.clipboard.writeText(promptText).then(function () {
2398
+ useBtn.textContent = '✓ 提示词已复制';
2399
+ useBtn.style.background = '#10b981';
2400
+ setTimeout(function () { useBtn.textContent = '✨ 使用模板'; useBtn.style.background = '#a78bfa'; }, 2000);
2401
+ });
2402
+ };
2403
+ rightHeader.appendChild(infoSpan);
2404
+ rightHeader.appendChild(useBtn);
2405
+ previewIframe.src = MYCLAW_API_BASE + '/api/file?path='
2406
+ + encodeURIComponent(TEMPLATE_ROOT + '/templates/' + tpl['文件夹名'] + '/__student-view__.html');
2407
+ }
2376
2408
 
2377
- // 加载 demo.html(text/html,server 直接返回,iframe 可正常渲染)
2378
- previewIframe.src = MYCLAW_API_BASE + '/api/file?path='
2379
- + encodeURIComponent(TEMPLATE_ROOT + '/templates/' + tpl['文件夹名'] + '/__student-view__.html');
2380
- }
2409
+ row.onclick = setActive;
2410
+ row.onmouseenter = function () { if (row !== activeRow) row.style.background = 'rgba(255,255,255,0.06)'; };
2411
+ row.onmouseleave = function () { if (row !== activeRow) row.style.background = 'transparent'; };
2412
+
2413
+ var topRow = document.createElement('div');
2414
+ topRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:4px;';
2415
+ var badge = document.createElement('span');
2416
+ badge.textContent = tpl['系列'] + tpl['编号'];
2417
+ badge.style.cssText = 'font-size:10px;font-weight:bold;background:#4a4a7a;color:#cdd6f4;padding:1px 7px;border-radius:8px;flex-shrink:0;';
2418
+ var nameEl = document.createElement('span');
2419
+ nameEl.textContent = tpl['名称'];
2420
+ nameEl.style.cssText = 'font-size:12px;color:#cdd6f4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
2421
+ topRow.appendChild(badge);
2422
+ topRow.appendChild(nameEl);
2423
+
2424
+ var descEl = document.createElement('div');
2425
+ descEl.textContent = tpl['一句话说明'];
2426
+ descEl.style.cssText = 'font-size:11px;color:rgba(205,214,244,0.5);line-height:1.4;margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;';
2427
+
2428
+ var tagsEl = document.createElement('div');
2429
+ tagsEl.style.cssText = 'display:flex;gap:4px;margin-top:5px;flex-wrap:wrap;';
2430
+ [tpl['主能力标签'], tpl['任务类型标签']].filter(Boolean).forEach(function (tag) {
2431
+ var tagEl = document.createElement('span');
2432
+ tagEl.textContent = tag;
2433
+ tagEl.style.cssText = 'font-size:9px;padding:1px 7px;border-radius:8px;background:rgba(167,139,250,0.15);color:#a78bfa;';
2434
+ tagsEl.appendChild(tagEl);
2435
+ });
2381
2436
 
2382
- row.onclick = setActive;
2383
- row.onmouseenter = function () { if (row !== activeRow) row.style.background = 'rgba(255,255,255,0.06)'; };
2384
- row.onmouseleave = function () { if (row !== activeRow) row.style.background = 'transparent'; };
2385
-
2386
- // 系列+编号徽章 + 名称
2387
- var topRow = document.createElement('div');
2388
- topRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:4px;';
2389
-
2390
- var badge = document.createElement('span');
2391
- badge.textContent = tpl['系列'] + tpl['编号'];
2392
- badge.style.cssText = 'font-size:10px;font-weight:bold;background:#4a4a7a;color:#cdd6f4;padding:1px 7px;border-radius:8px;flex-shrink:0;';
2393
-
2394
- var nameEl = document.createElement('span');
2395
- nameEl.textContent = tpl['名称'];
2396
- nameEl.style.cssText = 'font-size:12px;color:#cdd6f4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
2397
-
2398
- topRow.appendChild(badge);
2399
- topRow.appendChild(nameEl);
2400
-
2401
- // 一句话说明
2402
- var descEl = document.createElement('div');
2403
- descEl.textContent = tpl['一句话说明'];
2404
- descEl.style.cssText = 'font-size:11px;color:rgba(205,214,244,0.5);line-height:1.4;margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;';
2405
-
2406
- // 标签
2407
- var tagsEl = document.createElement('div');
2408
- tagsEl.style.cssText = 'display:flex;gap:4px;margin-top:5px;flex-wrap:wrap;';
2409
- [tpl['主能力标签'], tpl['任务类型标签']].filter(Boolean).forEach(function (tag) {
2410
- var tagEl = document.createElement('span');
2411
- tagEl.textContent = tag;
2412
- tagEl.style.cssText = 'font-size:9px;padding:1px 7px;border-radius:8px;background:rgba(167,139,250,0.15);color:#a78bfa;';
2413
- tagsEl.appendChild(tagEl);
2414
- });
2437
+ row.appendChild(topRow);
2438
+ row.appendChild(descEl);
2439
+ row.appendChild(tagsEl);
2440
+ leftPane.appendChild(row);
2441
+ });
2442
+ }
2415
2443
 
2416
- row.appendChild(topRow);
2417
- row.appendChild(descEl);
2418
- row.appendChild(tagsEl);
2419
- leftPane.appendChild(row);
2444
+ // ── 并行:加载本地 index + 检查 CDN 更新 ──────────────────────────────
2445
+ // 检查 CDN 更新(不阻塞列表加载)
2446
+ fetch(MYCLAW_API_BASE + '/api/sync-templates')
2447
+ .then(function (r) { return r.json(); })
2448
+ .then(function (sync) {
2449
+ if (!sync.changed) {
2450
+ syncStatus.textContent = '';
2451
+ return;
2452
+ }
2453
+ var msg = '';
2454
+ if (sync.added && sync.added.length) msg += '新增 ' + sync.added.length + ' 个';
2455
+ if (sync.updated && sync.updated.length) msg += (msg ? ',' : '') + '更新 ' + sync.updated.length + ' 个';
2456
+ syncStatus.textContent = '✓ ' + msg;
2457
+ syncStatus.style.color = '#10b981';
2458
+ // 重新加载列表
2459
+ fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2460
+ .then(function (r) { return r.json(); })
2461
+ .then(function (newIndex) { renderTemplateList(flattenTemplates(newIndex)); })
2462
+ .catch(function () {});
2463
+ setTimeout(function () { syncStatus.textContent = ''; }, 4000);
2464
+ })
2465
+ .catch(function () { syncStatus.textContent = ''; });
2420
2466
 
2421
- // 默认选中第一个
2422
- if (idx === 0) setActive();
2423
- });
2467
+ fetch(MYCLAW_API_BASE + '/api/file?path=' + encodeURIComponent(TEMPLATE_ROOT + '/template-index.json'))
2468
+ .then(function (r) {
2469
+ if (!r.ok) throw new Error('HTTP ' + r.status);
2470
+ return r.json();
2471
+ })
2472
+ .then(function (indexData) {
2473
+ renderTemplateList(flattenTemplates(indexData));
2424
2474
  })
2425
2475
  .catch(function (err) {
2426
2476
  leftPane.textContent = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiyiran/myclaw",
3
- "version": "1.1.34",
3
+ "version": "1.1.35",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -25,7 +25,8 @@
25
25
  },
26
26
  {
27
27
  "name": "yiran-course-template-pipeline",
28
- "strategy": "on",
28
+ "strategy": "delete",
29
+ "ignore": ["templates"],
29
30
  "description": "课程模板制作流水线(demo -> student/teacher JSON -> HTML 打包)"
30
31
  }
31
32
  ],
@@ -43,18 +43,27 @@ function findSkillsDir() {
43
43
  /**
44
44
  * 递归复制目录
45
45
  */
46
- function copyDirSync(src, dest) {
46
+ function copyDirSync(src, dest, ignores = []) {
47
47
  if (!fs.existsSync(dest)) {
48
48
  fs.mkdirSync(dest, { recursive: true });
49
49
  }
50
50
 
51
51
  const entries = fs.readdirSync(src, { withFileTypes: true });
52
52
  for (const entry of entries) {
53
+ // 全局通用忽略项
54
+ if (entry.name === '__pycache__' || entry.name === '.DS_Store') {
55
+ continue;
56
+ }
57
+ // 配置覆盖忽略项
58
+ if (ignores && ignores.indexOf(entry.name) !== -1) {
59
+ continue;
60
+ }
61
+
53
62
  const srcPath = path.join(src, entry.name);
54
63
  const destPath = path.join(dest, entry.name);
55
64
 
56
65
  if (entry.isDirectory()) {
57
- copyDirSync(srcPath, destPath);
66
+ copyDirSync(srcPath, destPath, ignores);
58
67
  } else {
59
68
  fs.copyFileSync(srcPath, destPath);
60
69
  }
@@ -96,16 +105,16 @@ function patchSkills() {
96
105
  const manifestSkills = manifest && Array.isArray(manifest.skills) ? manifest.skills : [];
97
106
 
98
107
  // 把 manifest 里的配置做成一个 map 方便查询
99
- const skillStrategyMap = {};
108
+ const skillConfigMap = {};
100
109
  for (const s of manifestSkills) {
101
- if (s.name) skillStrategyMap[s.name] = s.strategy || 'auto';
110
+ if (s.name) skillConfigMap[s.name] = s;
102
111
  }
103
112
 
104
113
  let count = 0;
105
114
 
106
115
  // 1. 处理 manifest 中明确被 delete 的 skill
107
- for (const [name, strategy] of Object.entries(skillStrategyMap)) {
108
- if (strategy === 'delete') {
116
+ for (const [name, config] of Object.entries(skillConfigMap)) {
117
+ if (config.strategy === 'delete') {
109
118
  const targetDir = path.join(skillsDir, name);
110
119
  if (fs.existsSync(targetDir)) {
111
120
  fs.rmSync(targetDir, { recursive: true, force: true });
@@ -120,7 +129,8 @@ function patchSkills() {
120
129
  for (const skillDir of skillDirs) {
121
130
  const srcDir = path.join(myclawSkillsDir, skillDir.name);
122
131
  const destDir = path.join(skillsDir, skillDir.name);
123
- const strategy = skillStrategyMap[skillDir.name];
132
+ const config = skillConfigMap[skillDir.name];
133
+ const strategy = config ? config.strategy : null;
124
134
 
125
135
  if (!strategy) {
126
136
  console.log('[myclaw-skill] ⊘ 跳过: ' + skillDir.name + ' [未配置,跳过]');
@@ -148,7 +158,7 @@ function patchSkills() {
148
158
  continue;
149
159
  }
150
160
 
151
- copyDirSync(srcDir, destDir);
161
+ copyDirSync(srcDir, destDir, config.ignore || []);
152
162
  count++;
153
163
  console.log('[myclaw-skill] ✅ 已注入: ' + skillDir.name + ' [' + strategy + ']');
154
164
  }
@@ -747,6 +747,8 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
747
747
  return self._handle_agents_list()
748
748
  elif path == '/api/file':
749
749
  return self._handle_file(params)
750
+ elif path == '/api/sync-templates':
751
+ return self._handle_sync_templates()
750
752
  else:
751
753
  self._send_json({"error": "not found"}, 404)
752
754
 
@@ -1101,6 +1103,96 @@ class MyclawAPIHandler(BaseHTTPRequestHandler):
1101
1103
  except Exception as e:
1102
1104
  self._send_json({"error": str(e)}, 500)
1103
1105
 
1106
+ def _handle_sync_templates(self):
1107
+ """GET /api/sync-templates
1108
+ 检查 CDN 上的 template-index.json 是否比本地新,若有变化则增量下载。
1109
+ 返回 {changed: false} 或 {changed: true, added: [...], updated: [...]}
1110
+ """
1111
+ CDN_INDEX_URL = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/template-index.json'
1112
+ CDN_TEMPLATE_BASE = 'https://cdn.yiranlaoshi.com/myclaw/live/yiran/skills/yiran-playground-template-use/templates'
1113
+
1114
+ local_templates_dir = os.path.join(
1115
+ get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'templates'
1116
+ )
1117
+ local_index_path = os.path.join(
1118
+ get_openclaw_path(), 'skills', 'yiran-playground-template-use', 'template-index.json'
1119
+ )
1120
+
1121
+ try:
1122
+ # 拉取 CDN index
1123
+ with urllib.request.urlopen(CDN_INDEX_URL, timeout=10) as resp:
1124
+ cdn_index = json.loads(resp.read().decode('utf-8'))
1125
+ except Exception as e:
1126
+ self._send_json({'error': 'CDN index 获取失败: ' + str(e)}, 502)
1127
+ return
1128
+
1129
+ # 读取本地 index
1130
+ local_index = {}
1131
+ if os.path.isfile(local_index_path):
1132
+ try:
1133
+ with open(local_index_path, 'r', encoding='utf-8') as f:
1134
+ local_index = json.load(f)
1135
+ except Exception:
1136
+ pass
1137
+
1138
+ cdn_updated_at = cdn_index.get('index_updated_at', '')
1139
+ local_updated_at = local_index.get('index_updated_at', '')
1140
+
1141
+ if cdn_updated_at and cdn_updated_at == local_updated_at:
1142
+ self._send_json({'changed': False})
1143
+ return
1144
+
1145
+ # 找出新增和变更的模板
1146
+ cdn_templates = cdn_index.get('templates', {})
1147
+ local_templates = local_index.get('templates', {})
1148
+ added = []
1149
+ updated = []
1150
+
1151
+ for series, nums in cdn_templates.items():
1152
+ for num, record in nums.items():
1153
+ folder_name = record.get('文件夹名', '')
1154
+ files = record.get('files', [])
1155
+ cdn_ua = record.get('updated_at', '')
1156
+ local_ua = local_templates.get(series, {}).get(num, {}).get('updated_at', '')
1157
+
1158
+ if not folder_name or not files:
1159
+ continue
1160
+
1161
+ is_new = series not in local_templates or num not in local_templates.get(series, {})
1162
+ is_changed = not is_new and cdn_ua != local_ua
1163
+
1164
+ if not is_new and not is_changed:
1165
+ continue
1166
+
1167
+ # 下载该文件夹的所有文件
1168
+ folder_dir = os.path.join(local_templates_dir, folder_name)
1169
+ os.makedirs(folder_dir, exist_ok=True)
1170
+ for filename in files:
1171
+ url = '{}/{}/{}'.format(CDN_TEMPLATE_BASE, folder_name, filename)
1172
+ dest = os.path.join(folder_dir, filename)
1173
+ try:
1174
+ with urllib.request.urlopen(url, timeout=15) as r:
1175
+ with open(dest, 'wb') as out:
1176
+ out.write(r.read())
1177
+ except Exception as e:
1178
+ print('[sync-templates] 下载失败 {}: {}'.format(url, e))
1179
+
1180
+ if is_new:
1181
+ added.append(folder_name)
1182
+ else:
1183
+ updated.append(folder_name)
1184
+
1185
+ # 覆盖本地 index
1186
+ try:
1187
+ os.makedirs(os.path.dirname(local_index_path), exist_ok=True)
1188
+ with open(local_index_path, 'w', encoding='utf-8') as f:
1189
+ json.dump(cdn_index, f, ensure_ascii=False, indent=2)
1190
+ except Exception as e:
1191
+ self._send_json({'error': 'index 写入失败: ' + str(e)}, 500)
1192
+ return
1193
+
1194
+ self._send_json({'changed': True, 'added': added, 'updated': updated})
1195
+
1104
1196
  def log_message(self, format, *args):
1105
1197
  # 静默日志,避免轮询刷屏
1106
1198
  pass
@@ -61,8 +61,7 @@ description: 面向中文课堂的小专题课程模板流水线。适用于把
61
61
  - 再定 __student__.json
62
62
  - 再定 __teacher__.json
63
63
  - 阶段一到三先在当前工作目录的任务文件夹里完成
64
- - 所有 HTML 中的外部资源(图片、字体、JS 库等)必须使用 CDN 链接,禁止本地相对路径;使用哪个 CDN 不限
65
- - publish 脚本会统一重新打包:无论原引用是相对路径还是其他 CDN,只要本地能找到对应文件,都会上传到指定 CDN 并重写引用(见 `cdn-convention.md`)
64
+ - publish 脚本把模板文件夹内所有文件上传到 CDN,CDN 路径规则见 `prompts/阶段4-打包落盘.md`
66
65
  - 第4阶段拆成 build、move、publish 三个独立脚本
67
66
  - templates 作为 skill 的资产目录统一维护
68
67
  - 模板有变化后,要重新构建索引
@@ -113,13 +113,6 @@
113
113
  - 例如:`班级小组徽章_47.png`
114
114
  - 这样可以减少浏览器缓存带来的旧图问题
115
115
 
116
- ### 资源路径方式
117
- - demo 网页中的所有媒体资源,不要使用本地相对路径
118
- - 必须使用 CDN 链接
119
- - CDN 固定前缀写死为:`https://cdn.yiranlaoshi.com/yiran/workspace-ai-demo`
120
- - 后面再拼接资源相对路径
121
- - 例如:`https://cdn.yiranlaoshi.com/yiran/workspace-ai-demo/c102_给小组作品做微调训练/班级小组徽章_47.png`
122
-
123
116
  ## demo 生成后的下一步
124
117
 
125
118
  生成 demo 页面后,不要立刻结束。
@@ -78,6 +78,41 @@
78
78
  - 每一步都包含:
79
79
  - 标题
80
80
  - 说明
81
+ - 默认优先从学生动作出发来写步骤,例如:先试一试、先看一看、先改一点、再加一个
82
+ - 但不要把动作顺序写死,不同任务按实际内容调整
83
+ - 文案要短,读完就能做,优先使用孩子能直接执行的动作词
84
+ - 少写抽象提问,少写老师视角的分析话术
85
+
86
+ 补充判断规则:
87
+ - 步骤标题要像“要去做的事”,不要像“要去理解的概念”
88
+ - 说明句尽量一句话说清,避免解释过长
89
+ - 默认优先写“小改动”,再写“自主新增”;不要一上来就要求学生从零设计完整新内容
90
+ - 如果一句步骤说明读完以后,学生还不知道手上下一步该做什么,这句就还不够好
91
+
92
+ 常见不推荐写法:
93
+ - 想一想,它影响了什么
94
+ - 分析这个功能作用于哪里
95
+ - 理解页面结构变化逻辑
96
+ - 总结这个页面的设计特点
97
+
98
+ 这些问题常见的坏处是:
99
+ - 太抽象
100
+ - 不贴近孩子动作
101
+ - 有时和页面真实任务也不完全对应
102
+
103
+ 因此 student 步骤里优先写这些动作词:
104
+ - 点
105
+ - 看
106
+ - 挑
107
+ - 改
108
+ - 加
109
+
110
+ 尽量少写这些老师视角词:
111
+ - 分析
112
+ - 理解
113
+ - 总结
114
+ - 归纳
115
+ - 识别
81
116
 
82
117
  ### 4)评价目标
83
118
  也是本阶段核心。
@@ -88,13 +88,14 @@
88
88
  ### 4)主能力标签
89
89
  这是最核心的教学能力标签,只能选一个。
90
90
 
91
- 固定集合:
92
- - 构思能力
93
- - 表达构建
94
- - 问题判断
95
- - 迭代推进
91
+ 固定集合(建议直接按完整写法输出):
92
+ - 第一能力:构思能力
93
+ - 第二能力:表达构建
94
+ - 第三能力:问题判断
95
+ - 第四能力:迭代推进
96
96
 
97
97
  它回答的是:这个任务主要训练什么能力。
98
+ 注意:为了减少模型漏写编号,输出时优先直接使用上面的完整写法,不要只写后半段名称。
98
99
 
99
100
  ### 5)任务类型标签
100
101
  说明作品本身属于什么类型,也只能选一个。