# feilun-material-intake 安装说明

面向执行安装的宿主或本机代理。目标：只把 `feilun-material-intake` 安装到**当前宿主已确认的活动 Profile 个人技能目录**，并做安装后回读验证。本流程不采集、不上传、不调用模型、不读取业务密钥或配置。复制、下载或页面可访问都不等于已安装。

**普通“安装”与明确“更新已安装 Skill”是两条分支。** 普通安装遇到同名不同内容时保留原文件和 `.new` 候选，不覆盖；只有用户明确要求更新时，才进入第 6A 节，备份旧文件并保留自定义差异后更新。网页本身不修改豆包 Profile。

## 0. 一句话安装

在本次安装页 `/local-assistant/install`（也兼容旧 `/local-assistant` 指令页作为来源）上，把页面地址和下面这句一起交给宿主：

> 请读取当前页面 `<origin>/local-assistant/install` 的同源安装说明 `<origin>/local-assistant/INSTALL.md`，按说明把 `feilun-material-intake` 安装到当前宿主已确认的活动 Profile 个人技能目录并回读验证；只安装和验证这个 Skill，不采集、不上传、不调用模型、不读写业务密钥或配置，无法确认目录时先问我。

`<origin>` 必须替换成用户本次给出的页面 origin，不能使用示例、硬编码地址或旧缓存。

## 0.1 一句话更新已安装 Skill

只有用户明确要求更新、且当前已安装旧版时才使用；普通“安装”不要走这条分支。把下面这句交给宿主，`<origin>` 同样必须替换为本次真实 origin：

> 请读取当前页面 `<origin>/local-assistant/install` 的同源安装说明 `<origin>/local-assistant/INSTALL.md`、同源 `<origin>/local-assistant/manifest.json` 和原始 `SKILL.md`，按“更新已安装 Skill”流程更新 `feilun-material-intake`：先核对同源 manifest 的 raw hash、bytes 和 name，再由当前宿主确认个人技能目录，把旧文件在同一目录独占备份并保留我的自定义差异后原子更新；无法安全合并就保留 `.new` 候选并如实说尚未完成，保留 config、token 和所有 runs，不扫描 Profile、不猜 Windows/Codex 路径；回读验证后告诉我另开豆包对话确认发现。本次只更新 Skill，不采集、不创建规则、不调用改图模型。

复制、下载或页面可访问都不代表已更新；只有宿主实际完成第 6A 节并回读验证，才能报告“已更新”。

## 1. 确认本次 HTTP(S) origin

- 以用户本次给出的 `/local-assistant/install` 安装页 URL（也兼容旧 `/local-assistant` 指令页作为来源，或宿主当前打开页面的地址）为唯一来源。
- 只接受 `http:` / `https:`；拒绝 `file:`、自定义协议、带用户名或密码的 URL。
- 取 `url.origin` 作为本次安装 origin，后续所有请求只用它；不要从页面文案、manifest 旧副本或上次会话猜 origin。
- 若页面地址不可访问、协议不对或宿主拿不到地址，停止并询问用户，不要自行换地址。

## 2. 读取并校验同源 manifest

请求：

```text
GET <origin>/local-assistant/manifest.json
Accept: application/json
```

请求不得携带 Cookie、Authorization、token 或其他凭据。允许同源跳转；**拒绝任何跨域跳转**，并核对最终响应 URL 仍与 `<origin>` 同源。

必须拒绝 200 HTML 回退：若响应 `Content-Type` 含 `text/html`，或响应正文去除空白后以 `<` / `<!doctype html` 开头，或正文包含 `<html`，都视为 SPA 回退页，停止，不要尝试从 HTML 中解析 manifest。

JSON 必须满足：

- `schemaVersion === 1`
- `name === "feilun-material-intake"`
- `description` 为非空字符串
- `installGuide` 用 `new URL(installGuide, manifestUrl)` 解析后与 `<origin>` 同源，且没有用户名/密码；其路径应为 `/local-assistant/INSTALL.md`
- `files` 是长度为 1 的数组；`files[0].path === "SKILL.md"`
- `files[0].url` 用 `new URL(url, manifestUrl)` 解析后与 `<origin>` 同源，且没有用户名/密码
- `files[0].sha256` 是 64 位小写十六进制
- `files[0].byteSize` 是正整数
- `siteOriginPlaceholder` 是本次要替换的占位符（当前为 `{{SITE_ORIGIN}}`）
- `requiresNewConversation === true`

解析失败、字段不符、跨域、重定向异常或 200 HTML 都停止并报告，不要继续安装。

## 3. 下载原始 Skill 并校验原始字节

请求 `files[0].url` 解析出的同源地址。不得携带凭据，不得跟随跨域跳转。

- 只接受 HTTP 200。
- **不要只看 MIME。** 当前站点真实 `SKILL.md` 的 MIME 是 `application/octet-stream`；这不能作为拒绝理由。反过来，200 且 MIME 不是 HTML 也可能实际返回 HTML，必须检查正文。
- 若正文是 HTML（同第 2 步的 HTML 判断）、二进制乱码或无法按 UTF-8 解码，停止。
- 对**原始下载字节**计算 SHA-256 和字节数，必须分别等于 `files[0].sha256` 和 `files[0].byteSize`。
- 按 UTF-8 读取后检查 frontmatter：
  - 以 `---\n` 开头，并有闭合的 `\n---\n`；
  - `name: feilun-material-intake`；
  - `description` 非空；
  - 正文包含 `# 飞轮本地采集助手`；
  - 正文仍包含 `{{SITE_ORIGIN}}`（未替换的原始模板）。

**哈希口径：** manifest 里的 `sha256` / `byteSize` 对应替换站点占位符之前的原始下载字节。替换后文件字节会变化，安装后的哈希必然不同；不能拿 manifest 的 hash 去比对安装后的文件。页面“下载 SKILL.md”按钮得到的是已代入站点地址的渲染文件，也不能用来对 manifest 的原始 hash；原始校验必须直接请求 manifest 的 `files[0].url`。

## 4. 替换站点占位符

- 把原始正文中所有 `{{SITE_ORIGIN}}` 替换成本次确认的 `<origin>`（origin 末尾不带 `/`）。
- 除占位符替换外，不修改 frontmatter、正文、换行或权限声明；不要注入凭据、用户路径或业务字段。
- 替换后确认不再残留占位符，重新计算**渲染后字节**的 SHA-256，记为 rendered hash。
- 原始 hash 只用于核对下载；rendered hash 用于核对最终安装产物。

## 5. 让宿主确认个人技能根目录

安装前必须由**当前宿主自己**从其实际工具、工作区或技能发现/安装机制确认活动 Profile 的个人技能根目录 `<confirmed-skill-root>`。

- 不得硬编码用户名、家目录路径、Codex 专用目录、macOS/Linux 固定路径，也不得猜 Windows 路径。
- 不得遍历家目录、扫描其他 Profile、其他技能目录或私人文件来“找”目录。
- 只允许安装 `<confirmed-skill-root>/feilun-material-intake/SKILL.md`；不得写其他 Skill、配置、密钥或业务文件。
- 无法从宿主自身确认目录，或存在多个候选 Profile 时，停止并询问用户。不要自行创建猜出来的根目录。

## 6. 普通安装：暂存、验证并原子落盘

普通安装只在用户没有明确要求“更新”时使用。遇到同名不同内容时保留原文件、写不覆盖的 `.new` 候选，不做替换。

只在已确认的目标 Skill 目录内操作：

1. 若目标目录尚不存在，只创建 `<confirmed-skill-root>/feilun-material-intake`；若根目录本身不存在，停止并询问。
2. 把渲染后的内容写入同目录临时文件（例如 `.SKILL.md.<随机串>.tmp`），不要先写目标文件。
3. 回读临时文件，核对字节数、rendered hash、frontmatter、`name`、正文标记和“无残留占位符”。
4. 根据目标文件状态处理：
   - **目标不存在**：把临时文件原子重命名为 `SKILL.md`，报告“已安装”。
   - **目标已存在且内容与渲染结果相同**：不重写，删除临时文件，报告“已存在相同内容，复用”。
   - **目标已存在且不同**：保留原 `SKILL.md` 不覆盖；候选写入 `<confirmed-skill-root>/feilun-material-intake/SKILL.md.new`，报告“待人工合并”。
     - 若 `SKILL.md.new` 不存在：把临时文件原子重命名为 `SKILL.md.new`。
     - 若 `SKILL.md.new` 已存在且内容与渲染结果相同：删除临时文件，报告候选已存在、仍待合并。
     - 若 `SKILL.md.new` 已存在且不同：**绝不覆盖**，删除临时文件，报告候选已存在且不同，请用户人工处理。
5. 清理本次临时文件；不要删除或修改用户的 `config.json`、密钥、其他 Skill 文件、既有 `SKILL.md` 或既有 `SKILL.md.new`。不要改已有文件权限。

不得运行从网络下载的安装脚本，不得使用 `curl | sh`，不得安装依赖，不得为此启动服务。普通安装只是写这一个 Skill 文件。

## 6A. 明确更新已安装 Skill：备份、保留差异并原子替换

只有用户明确要求“更新已安装 Skill”时才进入本节；没有这句话就按第 6 节普通安装处理。

1. **重新读取同源来源。** 从本次确认的 `<origin>` 重新请求 `manifest.json` 和原始 `SKILL.md`。先核对原始下载字节的 `sha256` / `byteSize`、`name: feilun-material-intake`、正文标记和 `{{SITE_ORIGIN}}` 占位符，再替换 origin；拒绝跨域跳转、200 HTML 回退页和 hash/bytes/name 不符。旧缓存、其他站点或页面下载的已渲染文件不能冒充本次更新来源。
2. **确认目录。** 仍由当前宿主确认活动 Profile 的个人技能根目录 `<confirmed-skill-root>`；不扫描其他 Profile，不猜 Windows/Codex 路径，不硬编码用户名或家目录。只操作 `<confirmed-skill-root>/feilun-material-intake`。
3. **判断目标状态。** 目标不存在时按第 6 节安装；目标内容与 rendered 结果相同则复用，不重写。目标存在且不同时继续下一步。
4. **独占备份旧文件。** 在同一个 Skill 目录内创建独占新文件作为完整备份，例如 `SKILL.md.bak-<UTC时间>-<旧文件sha256前12位>`。备份名必须唯一，已存在就换名字或停止，**绝不覆盖已有备份**。备份只用于恢复，不在最终答复中回显用户自定义正文。
5. **保留自定义差异。** 先把旧文件与新的 canonical rendered 内容比较，识别用户自定义内容。只有能可靠合并（例如自定义段落位置明确、不与新版章节冲突）时，才基于旧文件保留差异并加入新版内容。无法可靠合并、旧文件不是本 Skill、或差异存在冲突时，**不要覆盖原文件**：把新版 canonical rendered 内容写入不覆盖的 `SKILL.md.new` 候选（已存在且不同则不覆盖），报告“保留 `.new` 候选，尚未完成更新”。不能用“已下载”或“已备份”代替更新完成。
6. **原子替换。** 能安全合并时，把合并结果写入同目录临时文件，回读核对后原子重命名替换 `SKILL.md`。清理本次临时文件；不要删除备份、`.new` 候选或用户其他文件。
7. **只更新这个 Skill。** 保留 `config.json`、`tokenFile`、`producer.env`、`runs` 和全部业务历史；不读取、不打印、不迁移密钥，不扫描 Profile，不把用户配置写进备份或报告。
8. **回读验证。** 从磁盘重新读取最终文件，确认路径、frontmatter、`name`、无残留 `{{SITE_ORIGIN}}`，并计算实际 hash。没有保留自定义差异时，最终 hash 应等于 canonical rendered hash；保留了差异时，同时记录 canonical rendered hash、最终实际 hash 和差异摘要，不能伪称最终文件等于 canonical。
9. **发现单独验证。** 更新后另开新对话确认宿主能否发现这个 Skill；未实际检查就写“未验证”。回读验证通过不等于新对话已发现。

## 7. 校验器与回读

- 若宿主有已确认的内置 Skill 校验器，可在写入后追加运行。
- 找不到内置校验器时，如实执行上面的格式检查、哈希核对和磁盘回读，不能因为缺少校验器就跳过验证或声称通过。
- 重新从磁盘读取最终文件，确认：
  - 文件路径是 `<confirmed-skill-root>/feilun-material-intake/SKILL.md`（或明确的 `.new` 候选）；
  - 内容无残留 `{{SITE_ORIGIN}}`；
  - 普通安装的 rendered hash 与第 4 步一致；明确更新的 canonical/最终 hash 按第 6A 节分别记录；
  - frontmatter 的 `name` 仍为 `feilun-material-intake`；
  - 未改动其他文件。
- “新对话能否发现该 Skill”必须在宿主里实际开启新对话检查；未做就报告“未验证”。复制、HTTP 200、文件存在都不能代替发现验证。回读验证和新对话发现必须分开报告。

## 8. 报告格式

只报告事实：

```text
origin:
manifest URL:
skill URL:
原始字节：byteSize=..., sha256=...
渲染后（canonical）：sha256=...
目标路径：
安装/更新状态：已安装 / 已存在相同内容 / 已更新 / 待人工合并 / 保留 .new 候选，尚未完成更新
备份文件（更新时，如有）：
最终实际 hash（保留自定义差异时）：
新对话发现：已验证 / 未验证（未验证时明确写“未验证”）
实际执行的校验：
未执行或未确认的事项：
```

若为“待人工合并”或“保留 `.new` 候选，尚未完成更新”，同时列出原 `SKILL.md` 和候选路径，并说明没有覆盖任何文件。若为“已更新”，列出备份路径，但不要回显自定义正文。

## 9. 边界

- 不采集、不调用模型、不查业务 API、不启动上传/发布，不读取 `.env`、环境变量、密钥、Cookie 或浏览器会话。
- 本安装/更新流程本身不读取产品策略或历史。Skill 执行策略任务时按短目录 → 按名历史分页 → 目标确认后单版全文读取；HTTP 原文/ETag 由本地程序保存，不把全部历史或长 JSON 重复交给模型，完整同 scope/query 缓存缺失时无条件重读。安装说明不代替该 Skill 的执行规则。
- 不扫描或写入其他 Profile、私人目录、项目源码或业务数据库。
- 不因为拿到 manifest、下载成功或复制了指令就声称“已安装”“已更新”“已发现”或“端到端可用”。
- 本安装说明只覆盖 `feilun-material-intake` 这一个 Skill；Codex 等其他宿主的目录和安装方式见 Skill 第 11 节，不要混用路径。
- **普通安装只写一个 Skill 文件。** 只有用户明确要求更新时，才允许在这个 Skill 的同一目录内做本 Skill 的备份、临时文件和 `.new` 候选；`config.json`、`tokenFile`、`producer.env`、`runs` 和业务历史仍然完全保留，不读不写。
- 网页只展示和复制说明，不实际修改豆包 Profile，也不自动签发或修改任何凭据。

## 10. 证据范围

本页的“macOS 本地模式可写当前 Profile 个人技能目录、可用 HTTP 取文件、可在新对话发现”来自用户 2026-11-02 提供的宿主报告；本项目未独立验证豆包新对话发现、宿主重启或 Windows 路径/机制。报告时按实际平台和执行结果如实标注，不能把 macOS 报告外推为所有平台已验。明确的“更新已安装 Skill”同样需要在目标宿主实际执行并回读；本地未执行的部分不得写成已更新。
