# 接入 Speclip 桌面端

给 [speclip-v4](https://github.com/agegr/pi-web) 侧实现者看的对接契约。pi-media 这一侧已经按本文实现完毕，Speclip 侧的接线尚未开始。

本文写于 2026-08-23，对照的是 speclip-v4 当时的 `main`。

## 一、凭证文件契约（唯一硬契约）

pi-media 的 `media-cloud` Extension 从这个文件读取云服务 API key。**它只读不写**——写入由 Speclip 的设置页负责。

### 路径

```
<agentDir>/media-credentials.json
```

`agentDir` 由 pi-media 调 `getAgentDir()`（`@earendil-works/pi-coding-agent`）解析。Speclip 在 fork API server 时已经通过 `PI_CODING_AGENT_DIR` 把它指向了 `~/.speclip/agent`（见 `src/main/api-server.ts` 的 `#spawn`），所以两侧天然对齐，不需要额外传参。

### 格式

```json
{
  "schemaVersion": 1,
  "providers": {
    "pexels": { "apiKey": "..." }
  }
}
```

- `schemaVersion` 必须是数字 `1`，其他值一律拒绝。
- `providers` 下每个条目是一个对象，`apiKey` 必须是非空字符串（读取时会 trim）。
- 未来新增服务商时往 `providers` 里加键即可，**不要**改 `schemaVersion`——加键是向后兼容的。

### 权限要求（会被强制）

文件里是明文 key，所以 pi-media 在读取前会检查 POSIX mode：

```
(mode & 0o077) !== 0  →  直接报错，提示 chmod 600
```

同时用 `lstat` 拒绝符号链接。**Speclip 写入时必须落 `0600`，父目录 `0700`。**

`web/lib/provider-credential-store.ts` 里的 `updateStoredCredentials` 已经是这个写法（`0600` + `proper-lockfile` + 写后 `chmodSync`），照搬即可。注意不要用 `copyFileSync` 做备份——它会继承 umask，`models-config-store.ts` 的注释里记了这个教训。

### 错误行为

key 缺失或不可用时，工具在 `execute` 内抛出可读的错误，错误信息里带文件路径和期望格式。**注册期不读文件**，所以没配 key 不会妨碍 Extension 加载，也不会让 `discoverAndLoadExtensions` 报错。

## 二、把 pi-media 挂进去

三处，缺一不可。

### 1. 依赖

`web/package.json` 加 `@speclip/pi-media`。

### 2. 捆绑名单

```ts
// web/lib/bundled-plugins.ts
const BUNDLED_PACKAGE_NAMES = ["@viccydev/pi-graph", "@speclip/pi-media"] as const;
```

pi-media 的 `pi:` 清单结构与 pi-graph 完全一致（`extensions` / `skills` / `prompts`），现有的 `getBundledResourceLoaderOptions()` 不用改。

### 3. 打包资产（漏了会在开发环境完全看不出来）

```js
// scripts/build-server.mjs
const PI_RUNTIME_ASSETS = [
  // ...
  {
    path: 'node_modules/@speclip/pi-media',
    required: 'extensions/media-local/index.ts',
  },
]
```

`@vercel/nft` 静态追踪看不见 Pi 包——它们是运行时从 `package.json` 发现的。**不加这条，`npm run dev` 一切正常，打出来的安装包里 pi-media 静默消失。**

## 三、待决问题

### 平台冲突（必须先解决）

pi-media 声明：

```json
"os": ["darwin", "linux"]
```

因为不可变探测和渲染链路依赖继承的 POSIX 文件描述符（把源文件 `open` 后立刻 `unlink`，只把 fd 交给 ffmpeg），Windows 上没有等价机制。而 speclip-v4 的 `electron-builder.yml` 是出 Windows NSIS 包的。

直接加成 `dependencies`，Windows 上 `npm install` 会因 `EBADPLATFORM` 失败。三条路：

1. **`optionalDependencies`** — 安装失败不阻断，但要在 `bundled-plugins.ts` 里容忍解析失败，并在设置页把这些工具标成当前平台不可用。改动最小。
2. **按平台构建** — Windows 构建流程里排除，其余平台捆绑。CI 复杂度上升。
3. **放宽 pi-media 的 `os` 限制** — 需要先给 Windows 写一条不依赖 fd 传递的渲染路径，这是真实工作量，不是改个字段。

**建议走 1**，把 2/3 留到确实有 Windows 用户诉求时再说。

### 素材库 UI（可选，非阻塞）

侧边栏已有 `materials`（「素材库」）导航项，图标、双语文案、路由都齐了，点进去是 `NavPlaceholderPane` 空占位（`web/components/SidebarNavPanes.tsx`）。

`pexels_search` 返回的候选里带了 `previewUrl`、尺寸、时长、作者和可选清晰度列表，够直接渲染成网格。要做的话这是天然落点，但和本次接入无依赖关系。

### 署名展示

Pexels 授权条款要求署名。下载回来的素材，来源记录形如：

```json
{
  "kind": "pexels",
  "url": "https://videos.pexels.com/video-files/25961000/1920.mp4",
  "assetKind": "video",
  "assetId": 25961000,
  "pexelsUrl": "https://www.pexels.com/video/a-boat-25961000/",
  "author": "Nisasu",
  "authorUrl": "https://www.pexels.com/@nisasu-1151927884"
}
```

这个 `source` 会沿 `MediaRef → EditSnapshot → RenderArtifact` 一路带到成片，Speclip 侧可以从渲染凭证里直接读出来展示。条款还要求**发起 API 请求的应用给 Pexels 一个显眼链接**，这一条 pi-media 无法代劳，需要 Speclip 在 UI 上落实。

## 四、已经不需要担心的事

- **`ctx.ui.confirm`** — `asset_import` 依赖它做交互确认。Speclip 的 `createExtensionUiContext()`（`web/lib/rpc-manager.ts`）已经把 `confirm` marshal 成 SSE 事件交给 `ExtensionDialog.tsx`，能直接用。
- **Extension 工具不被过滤** — `rpc-manager.ts` 里 `withExtensionTools()` 会把包提供的工具重新加回工具表，`media_probe` / `render` 这些在 Speclip 会话里是激活的。
- **asar** — `electron-builder.yml` 通过 `afterPack` 把整个 server 树留在 asar 外面，对需要 spawn ffmpeg 的包正好合适。
- **ffmpeg / ffprobe** — pi-media 的普通探测、联系表与渲染要求它们在 `PATH` 里；`media_scene_detect` 进一步要求 FFmpeg 9.0+、`--enable-libonnxruntime` 和 `dnn_processing`。Speclip 目前不打包这个特制构建，集成时需要随应用分发兼容版并设置 `PI_MEDIA_FFMPEG_BINARY`，或让工具把安装说明明确返回给用户。
