# PLAYGROUND 小组件制作与导入指导（完全小白版）

> 用途：把这份指导书下载后，直接发给任意 AI，再告诉 AI 你想制作的风格。默认情况下，AI 应该生成可以直接粘贴到 PLAYGROUND「HTML/CSS/JS 代码」框里的代码；只有用户明确需要桌面换图或桌面编辑字段时，才生成 `.widget.json` 文件。
>
> 这份指导书专门用于“桌面小组件”，不是聊天界面美化，也不是桌面主题。

---

## 0. 小白先看：你最终要做什么

你不需要自己手写复杂代码。

推荐流程（普通小白模式）：

1. 下载本指导书。
2. 把指导书文件发给任意 AI。
3. 再告诉 AI：
   - 想要什么风格；
   - 小组件显示什么内容；
   - 是否需要图片；
   - 哪些内容以后要自己修改。
4. 让 AI 只输出一段完整的 HTML/CSS/JS 混合代码。
5. 复制这段代码，粘贴到 PLAYGROUND「外观」→「创意工坊」→「小组件」里的「HTML/CSS/JS 代码」框。
6. 填写组件名称、占用列数和行数，点击保存。
7. 回到桌面，长按桌面进入编辑模式。
8. 点击空白位置，选择这个小组件并放到桌面。

普通代码模式可以直接制作和预览视觉效果，但不会自动生成桌面右上角的图片上传字段或文字编辑字段。

如果用户明确要求“导入后在桌面换图片”“点击铅笔修改文字/颜色”，才使用下面的高级 JSON 文件模式：

1. 让 AI 生成一个完整、合法的 `.widget.json` 文件内容。
2. 把 JSON 保存成 `你的名字.widget.json`，编码选择 UTF-8。
3. 在「外观」→「小组件管理」中点击「导入小组件」选择文件。
4. 回到桌面添加，并在编辑模式下使用铅笔按钮。

两种模式不要混用：JSON 不能粘贴到「HTML/CSS/JS 代码」框；HTML/CSS/JS 代码也不能当作 JSON 文件导入。

不要把 AI 输出的 Markdown 说明文字、```json 代码围栏或 ``` 符号一起粘贴到工坊代码框或保存到 JSON 文件。

错误示例：

```text
下面是你的组件：
```json
{ ... }
```
```

普通代码模式的正确做法：只复制 AI 输出的 HTML/CSS/JS 代码块，不要复制代码块外的说明。

高级 JSON 模式的正确做法：文件内容从第一个 `{` 开始，到最后一个 `}` 结束。

---

## 1. 小组件到底是什么

小组件是放在 PLAYGROUND 桌面上的一个独立视觉模块，例如：

- 时钟；
- 日期；
- 倒计时；
- 信件卡片；
- 唱片或照片卡；
- 角色状态卡；
- 天气视觉卡片；
- 票据、窗帘、相框等装饰组件。

小组件不是 App。

小组件不能：

- 打开聊天页面；
- 修改角色资料；
- 修改聊天记录；
- 修改 Memory；
- 修改世界书；
- 读取用户的 API Key；
- 修改桌面其他 App；
- 把内容放到手机外部；
- 覆盖整个手机页面。

小组件只负责：

> 在自己的桌面卡片区域里显示内容和处理自己的交互。

---

## 2. 最重要的理解：模板和用户配置是两回事

一个可编辑小组件由两部分组成：

```text
小组件模板
  ├── HTML 结构
  ├── CSS 样式
  ├── JavaScript 行为
  ├── 图片占位符
  └── 可编辑字段声明

当前桌面实例配置
  ├── 当前图片
  ├── 当前文字
  └── 当前颜色
```

导入文件保存的是“小组件模板”。

用户在桌面编辑模式下修改的是“当前桌面实例配置”。

因此：

- 修改图片不会改坏原来的小组件模板；
- 同一个小组件放在不同位置时，可以有不同配置；
- 修改文字不会影响角色数据和聊天数据；
- 删除桌面上的小组件，不会自动删除外观 App 里的小组件资源；
- 删除外观 App 里的小组件资源，才是删除这个小组件模板。

---

## 3. 高级模式：必须输出的 JSON 顶层结构

本节只适用于“导入 `.widget.json` 文件”的高级模式。普通小白模式不需要理解 JSON，也不要把本节内容粘贴到工坊代码框。

AI 生成的文件推荐使用以下结构：

```json
{
  "name": "小组件名称",
  "type": "custom_widget_template",
  "version": 2,
  "size": "small",
  "widthSpan": 1,
  "heightSpan": 1,
  "html": "<div class=\"widget-root\">...</div>",
  "css": ".widget-root{width:100%;height:100%;box-sizing:border-box;}",
  "images": {},
  "imageKeys": [],
  "userFields": []
}
```

### 每个字段是什么意思

| 字段 | 是否建议 | 作用 |
|---|---|---|
| `name` | 必须 | 小组件名称，显示在资源列表里 |
| `type` | 必须 | 必须写 `custom_widget_template` |
| `version` | 建议 | 可写 `2`，表示使用新版字段格式 |
| `size` | 建议 | `small`、`medium` 或 `large` |
| `widthSpan` | 强烈建议 | 横向占用 1 到 4 格 |
| `heightSpan` | 强烈建议 | 纵向占用 1 到 5 格 |
| `html` | 必须 | 小组件的 HTML 和必要的内联脚本 |
| `css` | 必须 | 小组件 CSS |
| `images` | 有图片时使用 | 保存默认图片资源 |
| `imageKeys` | 需要桌面换图时必须 | 声明哪些图片可以在桌面编辑 |
| `userFields` | 需要改文字或颜色时使用 | 声明桌面编辑面板中的字段 |

### 尺寸默认值

如果 AI 没有写 `widthSpan` 和 `heightSpan`，系统会根据 `size` 使用默认尺寸：

| `size` | 默认宽度 | 默认高度 |
|---|---:|---:|
| `small` | 1 格 | 1 格 |
| `medium` | 2 格 | 2 格 |
| `large` | 4 格 | 2 格 |

建议始终同时填写 `size`、`widthSpan`、`heightSpan`，这样尺寸不会因为 AI 的遗漏而改变。

---

## 4. JSON 最容易出错的地方

这是导入失败最常见的原因。

### 4.1 不能出现注释

错误：

```json
{
  "name": "照片卡",
  // 这里是图片
  "html": "..."
}
```

JSON 不支持 `//` 注释，也不支持 `/* ... */` 注释。

### 4.2 不能多写最后一个逗号

错误：

```json
{
  "name": "照片卡",
  "type": "custom_widget_template",
}
```

正确：

```json
{
  "name": "照片卡",
  "type": "custom_widget_template"
}
```

### 4.3 字符串里的双引号必须转义

错误：

```json
{
  "html": "<div title="照片">内容</div>"
}
```

正确：

```json
{
  "html": "<div title=\"照片\">内容</div>"
}
```

### 4.4 字符串里的换行必须写成 `\\n`

错误：把 HTML 或 CSS 的真实换行直接放进 JSON 字符串。

正确：

```json
{
  "css": ".widget-root{\n  width:100%;\n  height:100%;\n}"
}
```

如果浏览器提示：

```text
Bad control character in string literal in JSON
```

通常就是字符串里出现了没有转义的真实换行、制表符或控制字符。

### 4.5 不要把 JSON 和说明文字混在一起

AI 必须最后检查：

- 文件第一个有效字符是 `{`；
- 文件最后一个有效字符是 `}`；
- 中间没有 Markdown 代码围栏；
- 中间没有“好的，下面是……”等解释文字。

---

## 5. 图片应该怎么放

图片有两层概念：

1. 图片资源；
2. 图片在 HTML 里的位置。

两者必须同时写对。

### 5.1 图片资源放在 `images`

推荐结构：

```json
{
  "images": {
    "img1": "图片地址或 data URL",
    "avatar": "图片地址或 data URL"
  }
}
```

图片名称可以自己取，但名称必须和 `imageKeys`、HTML 占位符完全一致。

### 5.2 需要桌面更换的图片必须写入 `imageKeys`

例如：

```json
{
  "imageKeys": ["img1", "avatar"]
}
```

这表示：

- `img1` 是第一张可编辑图片；
- `avatar` 是第二张可编辑图片；
- 导入后，桌面编辑模式会根据这些字段显示图片选择项。

仅仅写 `images` 不够。

如果只有：

```json
{
  "images": {
    "img1": "..."
  }
}
```

图片可以作为默认图片显示，但不一定会出现桌面更换图片入口。

不要把图片字段只写进 `userFields`。本系统的图片编辑入口由 `imageKeys` 生成；`userFields` 主要用于文字和颜色。图片要能在桌面更换时，必须同时检查 `imageKeys`、HTML/CSS 占位符和图片默认值是否使用了同一个名称。

### 5.3 HTML 中必须使用同名占位符

```html
<div class="widget-root">
  <img class="widget-photo" src="{{img1}}" alt="">
  <img class="widget-avatar" src="{{avatar}}" alt="">
</div>
```

名称必须一一对应：

| 声明 | HTML 使用 |
|---|---|
| `imageKeys: ["img1"]` | `src="{{img1}}"` |
| `imageKeys: ["avatar"]` | `src="{{avatar}}"` |

以下写法会导致图片无法替换：

- `imageKeys` 写成 `photo`，HTML 却写 `{{img1}}`；
- `images` 里叫 `background`，HTML 却写 `{{bg}}`；
- 多写空格、大小写不一致或拼写错误；
- 直接把图片写死在 CSS 或 JavaScript 里，却没有占位符。

### 5.4 图片放入 CSS 背景时也可以使用占位符

```css
.widget-root {
  background-image: url("{{background}}");
  background-size: cover;
  background-position: center;
}
```

对应 JSON：

```json
{
  "images": {
    "background": "https://example.com/background.jpg"
  },
  "imageKeys": ["background"]
}
```

但是，给小白制作时更推荐使用 HTML 的 `<img>`，因为：

- 更容易检查图片是否存在；
- 更容易设置 `object-fit`；
- 更容易让 AI 理解图片位置；
- 更容易在预览中发现尺寸问题。

### 5.5 图片没有默认值时会怎样

如果只有：

```json
{
  "imageKeys": ["img1"]
}
```

但没有：

```json
{
  "images": {
    "img1": "..."
  }
}
```

导入不会因此报错。

桌面编辑前，小组件会显示透明占位图。用户点击编辑按钮并上传图片后，图片才会显示。

所以 AI 必须明确告诉用户：

> 如果希望导入后立即看到图片，请提供 `images` 默认图片；如果只想让用户以后自己上传，可以只写 `imageKeys`。

---

## 6. 图片地址怎么选择

### 方案 A：data URL，最稳定

格式：

```text
data:image/png;base64,xxxxxxxx...
```

优点：

- 不依赖外部网站；
- 离线也能显示；
- 分享 JSON 时图片跟着文件走。

缺点：

- 文件会变大；
- 不适合塞很多张超大原图；
- Base64 中不能插入真实换行。

适合：

- 重要主图；
- 用户希望长期保存的图片；
- 不想依赖网址的图片。

### 方案 B：图片 URL，文件更小

格式：

```text
https://example.com/image.png
```

优点：

- JSON 文件小；
- AI 处理起来比较简单。

缺点：

- 网站可能失效；
- 图片网站可能禁止跨域或防盗链；
- 手机离线时可能无法显示；
- 换网址后旧小组件可能失效。

适合：

- 临时测试；
- 不重要的装饰图；
- 用户明确接受联网依赖的情况。

### 方案 C：本地电脑路径，禁止使用

错误：

```text
C:\Users\你的名字\Desktop\photo.png
```

或者：

```text
file:///C:/Users/...
```

用户的电脑路径无法在手机和其他设备上使用。

正确做法：

- 把图片作为文件附件发给 AI；
- 让 AI 将图片嵌入 `images`；
- 或者先使用 `imageKeys`，导入后在桌面编辑模式上传。

### 图片建议

- 单张图片尽量小于 500 KB；
- 主图建议最长边不超过 1200 px；
- 桌面编辑器上传图片时会自动压缩；
- 不要把 10 张 10 MB 原图全部塞进一个组件；
- 使用 `object-fit: cover` 或 `contain`；
- 使用 `overflow:hidden` 防止图片冲出卡片；
- 为图片设置明确的 `alt` 属性。

---

## 7. “导入后在桌面更换图片”的正确实现方式

如果用户希望导入后直接更换图片，AI 必须生成下面三个部分。

### 第一部分：声明图片字段

```json
"imageKeys": ["img1"]
```

### 第二部分：在 HTML 中使用占位符

```html
<img class="photo" src="{{img1}}" alt="小组件图片">
```

### 第三部分：限制图片显示区域

```css
.photo {
  display: block;
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  object-fit: cover;
  object-position: center;
}
```

用户实际操作流程是：

```text
导入小组件
  ↓
回到桌面
  ↓
长按桌面进入编辑模式
  ↓
找到小组件右上角的铅笔按钮
  ↓
点击“选择图片”
  ↓
选择手机里的图片
  ↓
保存修改
```

不要让 AI 在小组件内部自己制作一个复杂的文件上传按钮来代替这个流程。

原因：

- 桌面编辑器已经提供了统一的上传入口；
- 桌面编辑器会压缩图片；
- 用户配置会保存到当前桌面实例；
- 小组件内部的文件选择器容易和桌面拖动冲突；
- 小组件内部的上传按钮不一定能在所有手机浏览器中正常工作。

小组件内部可以有“切换显示”“展开信息”“暂停动画”等按钮，但“更换用户图片”优先使用桌面编辑器提供的编辑入口。

---

## 8. 文字和颜色怎样做成可编辑

### 8.1 声明文字字段

```json
"userFields": [
  {
    "key": "title",
    "label": "标题文字",
    "type": "text"
  },
  {
    "key": "description",
    "label": "说明文字",
    "type": "text"
  }
]
```

### 8.2 在 JavaScript 中读取文字

```html
<span id="title">默认标题</span>
<span id="description">默认说明</span>

<script>
(function () {
  const root = document.currentScript?.parentElement;
  if (!root) return;

  const get = async (key, fallback) => {
    if (typeof widget === "undefined") return fallback;
    return await widget.getState(key, fallback);
  };

  Promise.all([
    get("title", "默认标题"),
    get("description", "默认说明")
  ]).then(([title, description]) => {
    const titleElement = root.querySelector("#title");
    const descriptionElement = root.querySelector("#description");
    if (titleElement) titleElement.textContent = title;
    if (descriptionElement) descriptionElement.textContent = description;
  });
})();
</script>
```

必须使用 `textContent`，不要使用 `innerHTML` 把用户输入直接插入页面。

### 8.3 声明颜色字段

```json
"userFields": [
  {
    "key": "textColor",
    "label": "文字颜色",
    "type": "color"
  },
  {
    "key": "cardColor",
    "label": "卡片颜色",
    "type": "color"
  }
]
```

本系统建议 `userFields.type` 只使用 `text` 或 `color` 来声明文字、颜色。图片不要用 `userFields` 代替 `imageKeys`，否则 AI 可能生成一个看似有“图片字段”、实际却没有桌面上传入口的文件。

### 8.4 在 CSS 中使用颜色占位符

```css
.widget-root {
  color: #40384f;
  color: {{textColor}};
  background: #ffffff;
  background: {{cardColor}};
}
```

第一行是默认值，第二行是用户选择值。

如果用户还没有选择颜色，第二行可能暂时无效，浏览器会继续使用前面的默认颜色。

名称必须一致：

```text
userFields.key = textColor
CSS 占位符 = {{textColor}}
```

不能写成：

```text
userFields.key = textColour
CSS 占位符 = {{textColor}}
```

### 8.5 字段声明不会自动改变画面

只写：

```json
{
  "key": "title",
  "label": "标题",
  "type": "text"
}
```

并不会自动找到页面中的某个文字。

AI 还必须：

- 用 `widget.getState("title", "默认值")` 读取它；
- 或者在 HTML/CSS 中使用对应的占位符；
- 或者在自己的 JavaScript 中把它写入指定元素。

---

## 9. 小组件应该支持哪些交互

交互应该服务于小组件本身，不要把小组件做成第二个 App。

适合的交互：

- 点击卡片展开或收起说明；
- 点击按钮切换“已完成/未完成”；
- 点击暂停或恢复动画；
- 点击切换不同装饰状态；
- 长按时显示组件内部提示；
- 点击图片显示更大的局部预览；
- 点击标签切换不同视觉层。

不建议的交互：

- 打开整个聊天页面；
- 读取所有聊天记录；
- 修改角色信息；
- 修改 Memory；
- 请求 API；
- 依赖外部服务器保存内容；
- 控制其他小组件；
- 改变桌面 App 排列；
- 在组件内部制作全屏弹窗；
- 用 `position:fixed` 覆盖手机。

### 交互代码的基本要求

```javascript
(function () {
  const root = document.currentScript?.parentElement;
  if (!root) return;

  const button = root.querySelector("[data-action='toggle']");
  const panel = root.querySelector("[data-panel]");

  button?.addEventListener("click", () => {
    panel?.classList.toggle("is-open");
  });
})();
```

必须：

- 从当前组件的 `root` 开始查询；
- 只修改当前组件内部；
- 给按钮设置清晰的 `aria-label` 或文字；
- 没有元素时不报错；
- 不依赖全局变量；
- 不重复给同一个按钮绑定事件。

### 动画和定时器

如果使用：

- `setInterval`；
- `setTimeout`；
- `MutationObserver`；
- `addEventListener`；

必须考虑组件被删除后的清理。

推荐结构：

```javascript
(function () {
  const root = document.currentScript?.parentElement;
  if (!root) return;

  const host = root.closest(".desktop-widget-container");
  const owner = host?.parentElement || document.body;

  const timer = setInterval(() => {
    if (!document.contains(host || root)) {
      clearInterval(timer);
    }
  }, 1000);

  const observer = new MutationObserver(() => {
    if (!document.contains(host || root)) {
      clearInterval(timer);
      observer.disconnect();
    }
  });

  observer.observe(owner, { childList: true, subtree: true });
})();
```

不要每 100 毫秒刷新整个组件，也不要每次刷新都创建一套新的 DOM。

### 交互状态和保存范围

小组件内部的按钮可以改变当前组件的显示状态，但不要把下面这件事想当然：

> `widget.setState()` 不是“自动保存到整个手机”的按钮。

它适合在当前组件运行期间更新状态。若某个内容需要让用户在桌面编辑器中修改并保存，应优先使用 `userFields`；如果只是“展开/收起”“暂停动画”这类临时交互，可以只保存在当前组件实例内。

AI 必须在交付说明里写清楚：

- 哪些内容可以在桌面编辑器里修改；
- 哪些内容只是点击后临时变化；
- 刷新页面或重新放置组件后，临时状态可能恢复默认；
- 不要承诺组件会自动保存聊天数据、角色数据或其他业务数据。

---

## 10. 尺寸和布局规则

PLAYGROUND 桌面是 4 列 × 5 行的手机网格。

小组件自身必须适应父容器，而不是假设自己永远占满手机屏幕。

推荐根节点：

```css
.widget-root {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  box-sizing: border-box;
  overflow: hidden;
  position: relative;
}
```

必须避免：

- `width: 100vw`；
- `height: 100vh`；
- `position: fixed`；
- 超大的固定宽度；
- 超大的固定高度；
- `z-index: 999999`；
- 依赖桌面外部元素的定位。

推荐使用：

- `width:100%`；
- `height:100%`；
- `max-width:100%`；
- `max-height:100%`；
- `min-width:0`；
- `min-height:0`；
- `overflow:hidden`；
- `object-fit:cover`；
- `object-fit:contain`；
- `clamp()`；
- `cqw`；
- `cqh`。

### 不同尺寸的设计建议

| 占用尺寸 | 适合内容 |
|---|---|
| 1×1 | 单张图片、短文字、时间、单个状态 |
| 2×1 | 图片加一行文字、横向小卡片 |
| 2×2 | 头像卡、照片卡、短信息卡 |
| 4×2 | 复杂票据、横向角色状态卡 |
| 4×3 或更大 | 只在内容确实很多时使用 |

小组件越大，越容易：

- 挡住 App；
- 让桌面空间不足；
- 在手机屏幕上显得拥挤；
- 让图片和文字比例失衡。

先做小尺寸版本，确认布局稳定后再扩大。

---

## 11. 高级模式示例：支持桌面换图、改文字、改颜色

下面示例是结构示范。AI 生成真实组件时，必须把 HTML、CSS、JavaScript 正确转义进 JSON。

```json
{
  "name": "照片留言卡",
  "type": "custom_widget_template",
  "version": 2,
  "size": "medium",
  "widthSpan": 2,
  "heightSpan": 2,
  "images": {},
  "imageKeys": [
    "background",
    "avatar"
  ],
  "userFields": [
    {
      "key": "title",
      "label": "标题文字",
      "type": "text"
    },
    {
      "key": "subtitle",
      "label": "副标题文字",
      "type": "text"
    },
    {
      "key": "textColor",
      "label": "文字颜色",
      "type": "color"
    },
    {
      "key": "cardColor",
      "label": "卡片颜色",
      "type": "color"
    }
  ],
  "html": "<div class=\"widget-root\"><div class=\"background\"><img class=\"background-image\" src=\"{{background}}\" alt=\"背景图片\"><div class=\"overlay\"></div></div><img class=\"avatar\" src=\"{{avatar}}\" alt=\"头像\"><div class=\"content\"><strong class=\"title\" id=\"title\">默认标题</strong><span class=\"subtitle\" id=\"subtitle\">默认副标题</span><button class=\"toggle\" type=\"button\" data-toggle aria-label=\"展开说明\">详情</button><span class=\"detail\" data-detail hidden>这里是组件自己的补充信息。</span></div><script>(function(){const root=document.currentScript?.parentElement;if(!root)return;const get=async(key,fallback)=>typeof widget!==\"undefined\"?await widget.getState(key,fallback):fallback;Promise.all([get(\"title\",\"默认标题\"),get(\"subtitle\",\"默认副标题\")]).then(([title,subtitle])=>{const titleNode=root.querySelector(\"#title\");const subtitleNode=root.querySelector(\"#subtitle\");if(titleNode)titleNode.textContent=title;if(subtitleNode)subtitleNode.textContent=subtitle;});const button=root.querySelector(\"[data-toggle]\");const detail=root.querySelector(\"[data-detail]\");button?.addEventListener(\"click\",()=>{if(!detail)return;detail.hidden=!detail.hidden;});})();</script></div>",
  "css": ".widget-root{width:100%;height:100%;min-width:0;min-height:0;box-sizing:border-box;position:relative;overflow:hidden;border-radius:18px;background:#ffffff;color:#453b58;color:{{textColor}};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;}.background{position:absolute;inset:0;overflow:hidden;background:{{cardColor}};}.background-image{display:block;width:100%;height:100%;object-fit:cover;object-position:center;}.overlay{position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 25%,rgba(0,0,0,.48) 100%);}.avatar{position:absolute;left:50%;bottom:28%;width:28%;aspect-ratio:1;border:3px solid #fff;border-radius:50%;object-fit:cover;transform:translateX(-50%);box-shadow:0 4px 12px rgba(0,0,0,.2);}.content{position:absolute;left:10%;right:10%;bottom:7%;display:flex;align-items:center;flex-direction:column;gap:4px;text-align:center;}.title{font-size:clamp(15px,8cqw,24px);font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}.subtitle{font-size:clamp(9px,4cqw,13px);opacity:.78;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}.toggle{border:0;border-radius:999px;padding:4px 10px;background:rgba(255,255,255,.72);color:inherit;font:inherit;font-size:10px;cursor:pointer;}.detail{font-size:10px;max-width:100%;overflow-wrap:anywhere;}"
}
```

注意：上面示例中的每个顶层字段只出现一次。示例没有携带默认图片，所以导入后应在桌面编辑模式里分别上传背景图和头像；如果没有真实图片，不要随意编造失效网址。

AI 最后输出时必须：

- 删除重复字段；
- 删除示例说明；
- 检查 JSON 是否能被解析；
- 检查所有图片占位符名称；
- 检查所有用户字段名称；
- 只输出一个完整 JSON。

---

## 12. 让 AI 生成组件时应该怎样描述需求

不要只说：

```text
帮我做一个好看的小组件。
```

这样 AI 不知道：

- 组件多大；
- 图片是否可替换；
- 哪些文字要可编辑；
- 颜色是否要可编辑；
- 是否需要按钮；
- 图片是默认图片还是用户上传图片。

### 普通模式：直接生成工坊代码

如果用户只是想先制作一个小组件、粘贴代码并预览，请告诉 AI：

```text
请根据你看到的《PLAYGROUND 小组件制作与导入指导》，为我制作一个可以直接粘贴到 PLAYGROUND「HTML/CSS/JS 代码」框的小组件。

我的需求：
- 小组件名称：
- 占用尺寸：例如 2x2
- 整体风格：
- 背景材质：
- 是否需要背景图片：
- 是否需要头像图片：
- 图片是否只需要在组件内部显示：
- 需要用户修改的文字：
- 需要用户修改的颜色：
- 需要哪些内部交互：
- 不需要哪些功能：

请遵守指导书：
1. 只输出一个 HTML/CSS/JS 混合代码块，供用户粘贴到工坊的代码框。
2. 不要输出 JSON，不要输出 `images`、`imageKeys`、`userFields` 或 `.widget.json` 文件内容。
3. HTML、CSS、JavaScript 必须放在同一个代码块中；CSS 放进 `<style>`，JavaScript 放进 `<script>`。
4. 小组件必须适配 4x5 桌面网格。
5. 不允许使用 body、html、:root、position:fixed、超大 z-index。
6. 所有交互只能影响当前小组件。
7. 如果没有真实图片，不要伪造本地路径或失效网址；使用渐变、纯色或明确的图片占位区域。
8. 代码块外用简短白话说明：粘贴位置、组件名称和建议尺寸。
```

### 高级模式：需要桌面铅笔按钮时

只有用户明确说“我要导入 `.widget.json`”“我要在桌面更换图片”或“我要用铅笔修改文字/颜色”时，才使用下面这段提示：

```text
请为我生成一个 PLAYGROUND 可导入的 .widget.json 文件。
请只输出合法 JSON，不要输出 Markdown 代码围栏和说明文字。

必须包含：html、css、imageKeys、userFields，并确保图片占位符、文字字段和颜色字段实际生效。
图片使用 images + imageKeys + {{图片字段名}}。
文字和颜色使用 userFields；文字通过 widget.getState 读取，颜色在 CSS 中使用同名占位符。
用户会把 JSON 保存为 .widget.json 文件，再从「外观」→「小组件管理」→「导入小组件」导入。
绝对不要让用户把 JSON 粘贴到「HTML/CSS/JS 代码」框。
```

如果用户想让 AI 使用自己提供的图片，要另外告诉 AI：

```text
我会把图片作为附件发给你。
请在普通模式下，把图片作为代码中的视觉资源使用，不要输出 JSON。
如果我明确选择高级 JSON 模式，再把它们作为默认图片放入 images。
同时把图片字段写入 imageKeys，让我导入后还能在桌面编辑模式中更换。
如果你无法安全地把图片嵌入 JSON，请保留 imageKeys 和 {{字段名}}，不要伪造图片地址。
```

---

## 13. 高级 JSON 模式：导入成功但没有看到小组件怎么办

普通代码模式不需要“导入文件”：保存工坊代码后，直接回到桌面添加即可。

这是正常流程，不一定是失败。

导入小组件只会把它放入“资源库”，不会自动放到桌面。

正确步骤：

1. 回到桌面；
2. 长按桌面；
3. 进入编辑模式；
4. 点击空白格；
5. 找到“组件工坊小部件”；
6. 选择刚刚导入的小组件；
7. 放置后点击完成。

如果桌面上已经有小组件，可能需要先切换到其他页面寻找空白位置。

---

## 14. 常见问题排查

### 问题零：桌面显示一大段 CSS 或 HTML 文字

这通常是把 JSON 粘贴到了「HTML/CSS/JS 代码」框，或者把 CSS 单独当成了 HTML 保存。

处理方法：

1. 删除桌面上显示异常的小组件；
2. 普通模式：只把 AI 输出的 HTML/CSS/JS 混合代码粘贴到工坊代码框；
3. 高级模式：把完整 JSON 保存为 `.widget.json` 文件，再从「小组件管理」导入；
4. 不要把 `"html"`、`"css"`、`"userFields"` 这些 JSON 字段连同外层 JSON 一起粘贴到代码框；
5. 重新保存并重新添加小组件。

### 问题一：提示 JSON 导入失败

检查：

- 是否把 Markdown 代码围栏一起保存了；
- JSON 内是否有注释；
- 是否多了最后一个逗号；
- 字符串中的双引号是否写成 `\"`；
- 字符串中的换行是否写成 `\\n`；
- 是否把真实文件路径写进了 JSON；
- 是否出现了不可见控制字符。

### 问题二：导入成功，但图片空白

检查：

- 是否写了 `imageKeys`；
- HTML 是否使用完全相同的 `{{字段名}}`；
- `images` 中是否存在对应名称；
- URL 是否能在手机浏览器直接打开；
- 如果没有默认图片，是否已经进入桌面编辑模式上传；
- 图片是否被外部网站防盗链。

### 问题三：没有出现铅笔编辑按钮

检查：

- 是否写了 `imageKeys`；
- 是否写了 `userFields`；
- 是否重新导入了最新版文件；
- 是否已经把小组件放到桌面；
- 是否真的进入了桌面编辑模式；
- 旧版本导入的小组件可能缺少模板元数据，建议重新导入。

### 问题四：文字字段可以填写，但画面不变

检查：

- `userFields.key` 和 `widget.getState()` 的 key 是否完全一致；
- 是否把文字写死在 HTML 中，没有读取状态；
- 是否使用了 `textContent` 更新正确的元素；
- 元素是否在当前组件的 `root` 内；
- 是否因为 CSS 的 `overflow:hidden` 把文字裁掉。

### 问题五：颜色字段可以选择，但画面不变

检查：

- `userFields.key` 是否和 CSS 中的 `{{颜色字段}}` 一致；
- CSS 是否真的使用了这个占位符；
- 是否有默认颜色作为前置回退；
- 颜色值是否写在合法的 CSS 属性中。

### 问题六：小组件太大、被裁切或挡住 App

检查：

- `widthSpan` 是否超过 4；
- `heightSpan` 是否超过 5；
- 根节点是否有 `width:100%;height:100%`；
- 图片是否有 `object-fit`；
- 长文字是否有省略号或换行；
- 是否误用了 `vw`、`vh`、`position:fixed`。

### 问题七：小组件越来越卡

检查：

- 是否重复创建 `setInterval`；
- 是否重复绑定点击事件；
- 是否每次更新都重建整个 HTML；
- 是否每 100 毫秒刷新一次；
- 是否使用了过大的图片；
- 是否加载了外部 CDN、远程字体或远程脚本；
- 组件删除后是否清理定时器和观察器。

---

## 15. 导入前最终自检清单

### 文件格式

- [ ] 已经先决定使用“普通代码模式”还是“高级 JSON 文件模式”；
- [ ] 普通代码模式只粘贴 HTML/CSS/JS 混合代码，不粘贴 JSON；
- [ ] 高级模式文件扩展名是 `.widget.json`；
- [ ] 高级模式文件只有一个 JSON 对象；
- [ ] 高级模式没有 Markdown 代码围栏、注释和尾逗号；
- [ ] 高级模式所有字符串中的双引号和换行已正确转义；
- [ ] 高级模式文件可以被 JSON 解析。

### 小组件基本信息

- [ ] 有 `name`；
- [ ] `type` 是 `custom_widget_template`；
- [ ] 有 `html`；
- [ ] 有 `css`；
- [ ] `widthSpan` 在 1 到 4 之间；
- [ ] `heightSpan` 在 1 到 5 之间；
- [ ] `size` 和实际尺寸一致。

### 图片

- [ ] 每个可换图片都写入 `imageKeys`；
- [ ] HTML 或 CSS 使用了同名占位符；
- [ ] 有默认图片时写入 `images`；
- [ ] 图片没有使用本地电脑路径；
- [ ] 图片没有依赖无法访问的网站；
- [ ] 图片设置了尺寸和裁剪方式；
- [ ] 没有把超大原图全部塞进文件。

### 文字和颜色

- [ ] 每个要修改的文字都写入 `userFields`；
- [ ] 每个要修改的颜色都写入 `userFields`；
- [ ] key 名称前后一致；
- [ ] 文字实际通过 `widget.getState` 读取；
- [ ] 颜色实际通过 CSS 占位符使用；
- [ ] 有默认文字和默认颜色。

### 交互和性能

- [ ] JavaScript 使用 IIFE；
- [ ] DOM 查询从当前 root 开始；
- [ ] 交互只影响当前组件；
- [ ] 没有污染 window；
- [ ] 没有修改业务数据；
- [ ] 没有使用 fixed 和超大 z-index；
- [ ] 定时器、监听器和观察器可以清理；
- [ ] 没有高频重建整个组件；
- [ ] 没有依赖远程脚本。

---

## 16. 最后给 AI 的强制要求

请把下面这段一起发给 AI：

```text
你必须把自己当成“为完全不懂代码的用户制作 PLAYGROUND 小组件的助手”。

默认使用普通代码模式：如果用户没有明确要求 `.widget.json`、桌面换图或铅笔编辑字段，就只输出一段可粘贴到「HTML/CSS/JS 代码」框的 HTML/CSS/JS 混合代码。

只有用户明确要求桌面换图、铅笔编辑文字/颜色或 `.widget.json` 文件时，才使用高级 JSON 模式。

绝对不要让用户把 JSON 粘贴到 HTML/CSS/JS 代码框，也不要让用户把普通 HTML/CSS/JS 代码当作 JSON 文件导入。

不要假设用户知道：
- JSON 是什么；
- 图片 URL 是什么；
- Base64 是什么；
- imageKeys 是什么；
- {{img1}} 为什么要和字段名一致；
- userFields 为什么不会自动改变画面；
- 导入后为什么不会自动出现在桌面；
- 为什么旧文件需要重新导入；
- 为什么图片太大可能导致保存失败。

因此你必须：
1. 先根据需求设计小组件结构。
2. 使用 4x5 手机桌面尺寸规则。
3. 只让组件操作自己的 root。
4. 普通代码模式只输出 HTML/CSS/JS 混合代码；HTML、CSS、JavaScript 必须放在同一个代码块中。
5. 普通代码模式不要输出 JSON、代码围栏外不要放大段解释，并告诉用户粘贴到哪个代码框。
6. 如果用户选择高级 JSON 模式，为每个可换图片同时生成 images、imageKeys 和 HTML 占位符。
7. 高级 JSON 模式为每个可改文字和颜色生成 userFields，并确保字段 key 在 JSON、HTML、CSS、JavaScript 中完全一致。
8. 高级 JSON 模式如果无法嵌入真实图片，不要伪造本地路径或失效网址；应保留 imageKeys 和图片占位符，并明确告诉用户导入后从桌面编辑模式上传。
9. 高级 JSON 模式输出前检查 JSON 语法、控制字符、逗号、引号、图片字段和尺寸。
10. 最终回复中用白话告诉用户：
    - 当前使用普通代码模式还是高级 JSON 模式；
    - 普通模式应该粘贴到哪里，或高级模式应该保存成什么文件；
    - 导入/保存后在哪里找；
    - 如何放到桌面；
    - 哪些字段可以修改；
    然后再输出对应格式的内容。
```
