# AI-随心工坊 HTML 工具开发说明（供外部 AI 参考）

本文档描述在本站「工具」iframe 中运行的 **单页 HTML** 的约定与协议，**仅包含**运行环境、Host 桥协议、TOOL_META、实现要点、样式与示例，供生成此类 HTML 时参考。

---

## 一、运行环境

- 工具内容为 **单页 HTML**（可内嵌 CSS、JavaScript）。
- 该 HTML 在站内以 **iframe** 形式加载运行；**父页**为工坊页面。
- iframe 与父页 **仅通过 postMessage 通信**，不能访问父页的 window、DOM、cookie 等。
- 父页可能改变布局（例如进入/退出全屏），**不会重新加载 iframe**，因此文档内状态（表单、输入内容等）在布局切换后仍然保留；无需为此单独持久化到 host:setStorage。
- 不要假设运行在 Node、命令行或其他环境；仅考虑浏览器 iframe 单页。

---

## 二、Host 桥协议（postMessage）

所有与父页的能力交互均通过 `window.parent.postMessage` 发送请求，并监听 `message` 事件接收结果。消息体为对象，包含：

- **type**：字符串，表示请求或响应的类型。
- **id**：字符串，**每次请求必须唯一**（建议使用 `'req-' + Date.now()` 或带随机后缀），用于在回调中匹配「哪次请求的回复」。
- **payload**：对象，请求参数或响应结果。

### 1. 调 AI（host:chat）— 会消耗用户 API Key

- **请求**：向父页发送  
  `{ type: 'host:chat', id: '<唯一 id>', payload: { messages: [ ... ], options?: { enableSearch?: true | false | 'auto' } } }`  
  - **messages**（必填）：对话消息数组。  
  - **options.enableSearch**（可选）：`true` 表示本次强制先联网检索再回答；`false` 或不传表示不检索；`"auto"` 表示由平台根据最后一条用户消息**自动判断**是否需要检索（闲聊不查、事实/数据/实时才查）。**固定需检索的入口**（如简历分析按钮）传 `true`；**聊天类希望按需检索**时传 `"auto"`；翻译、角色扮演等明确不需检索时传 `false` 或不传。
- **响应**：父页回复  
  `{ type: 'host:chatResult', id: '<同上 id>', payload: { content: '<模型回复>' } }` 或  
  `{ type: 'host:chatResult', id: '<同上 id>', payload: { error: '<错误信息>' } }`
- 父页会使用**用户在本站设置页「我的 AI」中填写的 API Key** 调用大模型；若用户未填写，会返回 error（如「未设置 AI API Key，请在设置页填写「我的 AI」」）。**每次发送 host:chat 都会触发一次 AI 调用。**
- **展示 content 时的提示样式**：若 `content` 以 `【此次反馈基于实时数据检索】` 或 `【未检索成功，有可能说胡话】` 开头，表示本次使用了检索或检索未拿到数据；建议将该提示行与正文区分展示（如单独一行、不同颜色），换行后显示正文。
- **可选：检索/生成状态**：流式 chat 时，父页可能在等待期间向 iframe 推送 `{ type: 'host:chatStatus', payload: { status: 'searching' | 'generating' } }`。工具可监听 `message` 事件，当 `event.data.type === 'host:chatStatus'` 时根据 `payload.status` 更新等待提示（如「检索中」「思考中」）。**注意**：配置了 job 队列时 chat 走轮询，不推送 host:chatStatus，工具可不处理。

### 2. 请求外链（host:fetchUrl）

- **请求**：`{ type: 'host:fetchUrl', id: '<唯一 id>', payload: { url: '<目标 URL>' } }`
- **响应**：`{ type: 'host:fetchUrlResult', id: '<同上 id>', payload: { text: '<响应正文>' } }` 或  
  `{ type: 'host:fetchUrlResult', id: '<同上 id>', payload: { error: '<错误信息>' } }`
- 父页通过本站代理请求该 URL（受本站代理白名单限制）。

### 3. 选择文件（host:pickFile）

- **请求**：`{ type: 'host:pickFile', id: '<唯一 id>', payload: { accept?: '<MIME 类型>', multiple?: true|false } }`
- **响应**：`{ type: 'host:pickFileResult', id: '<同上 id>', payload: { files: [ { name, type, content: '<base64>' }, ... ] } }` 或  
  `{ type: 'host:pickFileResult', id: '<同上 id>', payload: { error: '<错误信息>' } }`

### 4. 下载/导出文件（host:exportFile）

- **请求**：`{ type: 'host:exportFile', id: '<唯一 id>', payload: { filename: '<文件名>', mimeType: '<MIME>', content: '<base64 或 Blob>' } }`
- 父页会触发浏览器下载；无结构化响应，可不监听对应 Result。

### 5. 用户数据存储（host:getStorage / host:setStorage）— 需站点配置云端存储

- **读存档**：向父页发送  
  `{ type: 'host:getStorage', id: '<唯一 id>', payload: { key: '<键名>' } }`  
  - **key**（必填）：字符串，工具内自定义的存储键（如 `memos`、`gameState`）。  
- **响应**：父页回复  
  `{ type: 'host:getStorageResult', id: '<同上 id>', payload: { value: '<字符串或 null>' } }` 或  
  `{ type: 'host:getStorageResult', id: '<同上 id>', payload: { error: '<错误信息>', value: null } }`  
  - 数据按「当前用户 + 当前工具」隔离，仅本人可读写；当前用户可为**登录用户**（跨设备同步）或**匿名用户**（仅本机有效），由站点与父页决定，工具无需区分。未配置存储或键不存在时 `value` 为 `null`。

- **写存档**：向父页发送  
  `{ type: 'host:setStorage', id: '<唯一 id>', payload: { key: '<键名>', value: '<字符串>' } }`  
  - **key**（必填）：字符串。**value**：字符串，可传空字符串。  
- **响应**：父页回复  
  `{ type: 'host:setStorageResult', id: '<同上 id>', payload: { ok: true } }` 或  
  `{ type: 'host:setStorageResult', id: '<同上 id>', payload: { error: '<错误信息>' } }`

- 适用于备忘录、游戏进度等需要「下次打开仍存在」的数据。若站点未配置 CloudBase 或 Vercel KV，会返回 error（如「用户数据存储未配置」），工具可降级为仅内存或提示用户。
- 本站**内置「备忘录」工具**即为此类用法的示例：使用 `host:getStorage`（key: `memos`）加载列表、`host:setStorage` 保存，单页 HTML 位于站内 `tools/iframe/memo.html`，可作参考。

---

## 三、工具卡片信息（TOOL_META，可选）

若希望在站内展示工具名称和描述，可在 HTML 文档最前面（`<!DOCTYPE html>` 之后）增加一行注释：

- 格式：`<!-- TOOL_META: {"name":"工具名","description":"简短描述"} -->`
- 示例：`<!-- TOOL_META: {"name":"五子棋人机对战","description":"您执黑先行，AI 执白，先连五子者胜"} -->`

生成时建议一并输出该注释，便于在站内展示工具名称与描述。

---

## 四、实现要点

1. **唯一 id**：每次向父页发起的请求使用不同的 `id`，在 `message` 回调中根据 `event.data.id` 与本地保存的 id 匹配，再处理对应的 `payload`（content 或 error）。
2. **错误处理**：收到任意 `host:xxxResult` 时，先检查 `payload.error`；若有则提示用户（如「未设置 AI API Key，请在设置页填写「我的 AI」」、检索或网络错误等），不要仅按成功处理。
3. **会消耗 API Key 的标记**：若工具内会发送 `host:chat`，建议在 HTML 的 `<head>` 中增加注释：  
   `<!-- requiresApiKey: true -->`  
   便于本站识别并提示用户配置「我的 AI」。
4. **单文件自包含**：工具应为一份可独立运行的 HTML，CSS/JS 尽量内联或内嵌；若引用外链（如 CDN），需考虑用户将工具另存后仍可访问（或说明仅在线可用）。
5. **host:chatResult 的 payload**：当前包含 `content`（成功时）或 `error`（失败时）；响应中后续可能增加 `searchUsed`、`warning` 等可选字段，工具可按需解析并展示（如未使用联网检索时的提示），未实现时忽略即可。
6. **host:getStorageResult / host:setStorageResult**：先检查 `payload.error`；无 error 时 getStorageResult 使用 `payload.value`（可能为 `null`），setStorageResult 表示写入成功。存储键建议使用有意义的命名（如 `memos`、`settings`），value 可存 JSON 字符串以便存对象。

---

## 五、样式约定（可选）

若希望与页面风格一致，可使用 CSS 变量（由父页提供）：

- `var(--fg)` 文字颜色  
- `var(--bg)` 背景色  
- `var(--card)` 卡片/输入框背景  
- `var(--border)` 边框色  
- `var(--accent)` 强调/链接色  
- `var(--muted)` 次要文字色  

---

## 六、示例（host:chat）

```javascript
var id = 'chat-' + Date.now();
window.addEventListener('message', function onResult(event) {
  if (event.data && event.data.type === 'host:chatResult' && event.data.id === id) {
    window.removeEventListener('message', onResult);
    var payload = event.data.payload || {};
    if (payload.error) {
      console.error(payload.error); // 或提示用户
    } else {
      console.log(payload.content); // 模型回复
    }
  }
});
window.parent.postMessage({
  type: 'host:chat',
  id: id,
  payload: {
    messages: [
      { role: 'system', content: '你是翻译助手。' },
      { role: 'user', content: 'Hello' }
    ]
    // 固定需检索传 options: { enableSearch: true }；聊天类希望平台自动判断是否检索传 enableSearch: 'auto'；翻译/角色扮演等不传
  }
}, '*');
```
