# 模板系统设计经验总结

## 一、版本化目录结构

### 问题
CDN 有缓存，更新模板后旧内容仍被访问。

### 方案
```
templates/
  a104_做一个会出招的神奇按钮/
    v1/   ← 每次更新递增版本号，CDN 路径变了，缓存自然失效
    v2/
```

### 经验
- **本地只保留一个版本**：下载新版前先删旧版目录，避免就绪状态歧义
- **目录存在即就绪**：不需要额外标记文件（`.sync-done`），目录本身就是状态
- **原子性替换**：下载到 `.tmp/`，完成后 `rename .tmp/ → v1/`，不会出现"半完成"状态

---

## 二、稳定 ID 设计

### 问题
模板可能改编号（A100 → A105）、改名，如果用路径/编号做唯一标识会丢失对应关系。

### 方案
```json
// __teacher__.json
{
  "_id": "696de592",   // 8位hex，颁发一次永不变
  "_version": 1        // 每次更新递增
}
```

### 经验
- ID 在 `move` 阶段颁发，之后不再改变
- CDN index 以 `_id` 为 key（平铺字典），而不是 `{系列: {编号: record}}` 嵌套结构
- 本地/CDN 比对：用 `_id` 找记录，再比较 `version` 字段判断是否需要更新

---

## 三、非阻塞同步 + 轮询进度

### 问题
下载几十个文件要几分钟，HTTP handler 不能阻塞。

### 方案
```python
# 立刻写 index（前端列表立刻出现），然后后台下载
with open(TEMPLATE_LOCAL_INDEX, 'w') as f:
    json.dump(cdn_index, f)

t = threading.Thread(target=_do_sync_templates, args=(...), daemon=True)
t.start()
self._send_json({'syncing': True, 'total': len(pending)})
```
前端每 3 秒轮询 `/api/sync-status`。

### 经验
- **先写 index 再下载**：用户立刻看到列表，下载在后台进行
- **守卫变量防重复轮询**：`if (_pollTimer) return`
- **轮询 vs WebSocket**：简单场景轮询够用，不要过度设计

---

## 四、前端就绪状态简化

### 反面教材（复杂版）
维护 `_readyFolders` Set + `_rowMap` + `updateActionBtns()` + 开屏预取状态，
按钮 disabled/opacity 联动，约 40 行状态管理代码。

### 正确做法（简单版）
- 按钮永远可点
- 点击后服务端检查就绪状态，未就绪返回 `{code: 'not_ready'}`
- 前端只需在 `.then()` 里判断 `res.code === 'not_ready'` 显示提示

```js
if (res.code === 'not_ready') {
  setFooterStatus('⟳ 模板文件下载中，完成后即可使用', '#f59e0b');
}
```

### 经验
**让服务端做校验，前端做展示**，不要在前端维护服务端状态的镜像。

---

## 五、同一文件存在两个副本

### 问题
`deploy_to_workspace.py` 存在于两个路径：
- `.openclaw/skills/.../deploy_to_workspace.py` ← 服务端实际运行
- `.openclaw/workspace-yiranclaw/myclaw/skills/.../deploy_to_workspace.py` ← 源文件

`mc patch` 会用源文件覆盖服务端文件，导致修改被还原。

### 经验
- **改脚本必须改两处**，或者建立符号链接
- 发现改动被还原时，先查是哪个路径在被覆盖，再两处同步修改

---

## 六、Pipeline 职责拆分

### 原则：每个脚本只做一件事

| 脚本 | 职责 |
|------|------|
| `identify_template.py` | 颁发 `_id`、校准编号、设置 `_version` |
| `move_template_task.py` | 调用 identify、创建 `v{N}/` 子目录、复制文件 |
| `build_template_index.py` | 扫描本地目录、生成 JSON index |
| `publish_template.py` | 上传到 CDN（带版本路径）、上传 index |
| `migrate_templates.py` | 一次性迁移旧结构到新结构 |

### 经验
- `move` 不再删除原文件夹（改为复制），避免误操作丢失源文件
- 版本号在 `move` 阶段确定，`publish` 不决定版本号
- 迁移脚本加 `--dry-run` 参数，先验证再执行

---

## 七、API 设计：参数复用代替接口重复

### 问题
"复制到 playgrounds" 和 "创建新 workspace" 流程完全相同，只是目标 workspace 名不同。

### 反面做法
新增 `/api/template/new-workspace` 接口，重复了 90% 的代码。

### 正确做法
```python
# POST /api/template/deploy
# body: {"folder": "...", "agent": "playgrounds"}  ← 已有 workspace
# body: {"folder": "..."}                           ← 不传则自动生成唯一名
agent_name = body.get('agent') or '{}-{}-{}'.format(series, version, hhmm)
```

### 经验
**加参数，不加接口**。两个行为只有一个变量不同时，用参数区分而不是新建接口。

---

## 八、mc 命令扩展

### 方法
在 `index.js` 里添加两处：

```js
// 1. 路由
} else if (command === 'sync-templates') {
  runSyncTemplates();

// 2. 函数（调本地 API，轮询进度）
function runSyncTemplates() {
  const http = require('http');
  const apiPort = process.platform === 'linux' ? '8080' : '18800';
  // apiGet → /api/sync-templates → 轮询 /api/sync-status
}
```

### 经验
- `mc` 命令本质是 Node.js 脚本，扩展成本极低
- 本地 API 端口：Mac=18800，Linux=8080
- 轮询用 `setTimeout` 递归，而不是 `setInterval`（更容易控制退出）

---

## 九、命名规范

### 实例目录命名
```
{业务名}_{HH:MM}
做一个会出招的神奇按钮_1911
```

### 新建 workspace 命名
```
{作品编号}-{版本号}-{小时分钟}
a104-v1-1911
→ workspace-a104-v1-1911
```

### 原则
- 去掉冗余信息（日期、系列前缀 `a104` 在实例名里不需要）
- 保留区分信息（时间保证唯一，版本号便于追溯）
- 用 `-` 分隔 workspace 名的各段，用 `_` 分隔实例名各段

---

## 十、window.open vs window.location.href

- `window.open('url', '_blank')` 会被浏览器弹窗拦截器阻止（非用户直接触发的点击）
- setTimeout 内的跳转用 `window.location.href` 更可靠
- 如果必须新标签，需要在用户点击的同步代码里调用 `window.open`
