# 图片与持久媒体

把图片字节和规范引用一起保存，支持刷新恢复、撤销和 DOCX 导入导出。

正文 JSON 保存图片引用，图片字节由媒体服务管理。只下载 JSON，不能保证换浏览器后还看得到图片。默认内存媒体服务适合预览；正式产品需要自己的资源身份、存储和恢复流程。

## 1. 在创建实例前准备媒体服务

`media` 是创建时服务，所有已挂载后端与文件服务共享它。各接口的用途如下：

| 接口 | 职责 |
| --- | --- |
| `sink.put(asset)` | 同步登记完整字节，返回 `{ kind: 'asset', assetId }` |
| `resolver.resolve(reference)` | 根据引用取得字节，可同步或异步返回 |
| `registrar.register(asset)` | 返回 `accept()` / `rollback()` lease，管理可能失败的插入 |
| `createAssetId(kind)` | 分配在资源库内长期唯一的 ID |

不要把异步上传函数直接当作 `sink.put()`。可先由应用下载资源，建立同步工作集；或者上传完成后在一次受保护的提交里登记与插入。

## 2. 建立可保存的工作集

下面的例程复用 `@quirekit/media` 内存库，额外记录已接受的资源清单。`restored` 来自应用读取的资源字节；`allocateId` 使用服务器发放的全局唯一 ID，若在客户端生成 UUID，使用 `uuid` 的 `v4`。

### classic/media/session.ts

```ts
import { createQuireMemoryMediaRepository, type QuireMediaAssetInput, type QuireAssetMediaReference } from "@quirekit/media";
import type { ClassicEditorMediaServices } from "@quirekit/classic-editor";

/**
 * Builds the synchronous working set from bytes restored by the host before mounting.
 * @param restored - Assets fetched with the document's asset manifest.
 * @param allocateId - Host allocator of globally unique persistent IDs, such as server-issued IDs.
 * @returns Classic services and an accepted-asset snapshot for durable storage.
 */
export function createDocumentMedia(restored: readonly QuireMediaAssetInput[], allocateId: () => string) {
  const repository = createQuireMemoryMediaRepository();
  const references = new Map<string, QuireAssetMediaReference>();
  for (const asset of restored) { const reference = repository.put(asset); references.set(reference.assetId, reference); }
  const media: ClassicEditorMediaServices = {
    resolver: repository,
    createAssetId: allocateId,
    sink: {
      /** @param asset - Complete in-memory bytes. @returns Registered reference. */
      put(asset) { const reference = repository.put(asset); references.set(reference.assetId, reference); return reference; },
    },
    registrar: {
      /** @param asset - Provisional authoring bytes. @returns Lease accepting only committed assets into the save manifest. */
      register(asset) {
        const lease = repository.register(asset);
        return {
          reference: lease.reference,
          /** @returns Whether this operation accepted the asset. */
          accept() {
            const accepted = lease.accept();
            if (accepted) references.set(lease.reference.assetId, lease.reference);
            return accepted;
          },
          /** @returns Whether unaccepted new bytes were removed. */
          rollback: () => lease.rollback(),
        };
      },
    },
  };
  return {
    media,
    /** @returns Owned byte copies, including assets retained for undo; not JSON strings or object URLs. */
    snapshotAssets(): readonly QuireMediaAssetInput[] {
      return [...references.values()].map(reference => {
        const asset = repository.resolve(reference)!;
        return { assetId: reference.assetId, kind: asset.kind, bytes: asset.bytes,
          mimeType: asset.mimeType, fileName: asset.fileName, intrinsicSize: asset.intrinsicSize };
      });
    },
  };
}

```

创建时传 `media: session.media`。`snapshotAssets()` 返回独立字节副本，供应用上传；它不是可直接塞进普通 JSON 的二进制编码。应用可把字节存入对象存储或 IndexedDB，并另存包含 assetId、内容类型和存储位置的清单。

已经接受的资源仍可能被撤销历史引用。例程保留它们，不根据当前可见图片立即删除；资源回收由文档与历史的保存策略决定。相同 assetId 不能重新指向不同字节。

## 3. 保存与恢复的顺序

1. 捕获同一时点的 `instance.getJSON()` 与 `session.snapshotAssets()`。
2. 上传并确认本次正文所需资源；失败时保留未保存状态，不发布引用缺失资源的新版本。
3. 提交正文版本及资源清单。版本冲突与原子发布由应用存储协议处理。
4. 再次打开时，按清单取回字节，用原 assetId 创建工作集，然后创建 Classic 并传入正文和媒体服务。

普通 `JSON.stringify(Uint8Array)` 不会得到可直接恢复的媒体包；不要只保存浏览器 object URL。object URL 属于当前浏览器资源，不能作为文档的长期身份。

## 4. DOCX 与失败清理

官方 DOCX 服务解析时暂存新图片，只有导入仍有效、可以写入当前文档时才登记到 `instance.media`；正文提交失败会回滚本次新 lease。提供 registrar 能让失败清理有明确所有者；只有 sink 的自定义实现需要自行处理部分登记失败后的清理。

导出时 resolver 必须能返回真实字节，图片在屏幕上显示成功不能替代导出验证。外链引用的抓取策略由应用显式决定；跨域、失效链接和离线访问都要验证。转换警告与失败处理见[保存与文件](/classic/files)。

## 验证一次完整生命周期

插入图片后保存，销毁实例，使用新工作集和保存内容重新创建，再导出 DOCX；图片应可解析且 assetId 保持不变。另外验证上传失败、取消导入、重复导入同一资源，以及删除图片后撤销。检查资源字节和正文结构，不只检查页面截图。
