# 图片与外部资源

为私有资源配置解析、释放和插入流程，避免把临时 URL 当持久数据。

## 源码字段与浏览器资源分开

Markdown 保存可持久资源地址，浏览器可能需要短期签名 URL、Blob 或内部资源接口。`Image.configure({ resolve })` 负责把源码目标转换为 `{ url, release? }`，不是把凭证写进图片语法。

下面是完整的同源资源接入。它只接受 `/assets/<id>`，需要应用自行实现 `GET /api/assets/:id`，本网站不会向这个演示地址发送请求。

### recipes/image.ts

```ts
import { Image } from "@quirekit/msfe-image";
import { StarterKit } from "@quirekit/msfe-starter-kit";
/** A same-origin asset resolver; authentication remains in the host fetch policy. */
export const PrivateImages = Image.configure({
  /** @param source - Canonical asset reference. @param signal - This image's lifetime. @returns One browser resource lease. */
  async resolve(source, signal) {
    const match = /^\/assets\/([a-zA-Z0-9_-]+)$/.exec(source);
    if (!match) throw new Error("unsupported-asset-reference");
    const response = await fetch(`/api/assets/${encodeURIComponent(match[1]!)}`, { signal, credentials: "same-origin" });
    if (!response.ok) throw new Error(`asset-http-${response.status}`);
    const blob = await response.blob();
    if (!/^image\/(png|jpeg|webp|gif)$/.test(blob.type) || blob.size > 5_000_000) throw new Error("unsupported-image");
    const url = URL.createObjectURL(blob);
    return { url, release() { URL.revokeObjectURL(url); } };
  },
});
/** Replace the preset's image owner; the host must implement GET /api/assets/:id. */
export const privateImageKit = StarterKit.configure({ image: PrivateImages });

```

`privateImageKit` 替换了 StarterKit 的 image 项，避免同名重复安装。源码中的 `/assets/` 相对路径经过 host resolver 变成 Blob URL；资源释放时撤销该 URL。示例限制图片类型与 5 MB 大小，可按真实存储协议调整。

## 粘贴、拖入与上传

图片功能自带粘贴与拖入：剪贴板或拖放中只有图片文件时接管，存好后在落点插入 `![替代文字](<地址>)`。上传期间落点由源码锚点保持，只读时不处理。图片存到哪里由 `upload` 决定：

~~~ts
import { Image, type ImageUploader } from '@quirekit/msfe-image'

const uploader: ImageUploader = {
  async upload(file, { source, name, signal }) {
    const body = new FormData()
    body.append('file', file, name ?? 'image')
    const response = await fetch('/api/assets', { method: 'POST', body, signal })
    if (!response.ok) throw new Error('upload-failed')
    return (await response.json()).url // 写入 Markdown 的持久地址
  },
}

Image.configure({ upload: uploader })
~~~

- 提供 `upload`：上传完成后写入返回的地址；某张图片失败时不写入它，提示由宿主在 `upload` 里给出。`source` 区分粘贴、拖入、对话框、Word 导入与 AI 等入口，可用于配额或命名。
- 省略 `upload`：PNG、JPEG、GIF、WebP 内联为 `data:` 地址，文档自包含但会变大；SVG 不内联，需要宿主上传。
- `upload: false`：不处理粘贴与拖入，交给应用自己的扩展。

`resolve` 是可选的反向查询，导出 Word、PDF 时把存储地址取回为 data URI。

## 插入命令

~~~ts
const inserted = editor.commands.insertImage({
  url: '/assets/diagram_42',
  alt: '系统结构图',
  title: '架构评审',
})
~~~

命令验证字段并编码为 Markdown。自己实现上传入口（如对话框）时，先捕获目标、上传文件、取得持久地址，再执行命令；长时间等待使用 [映射锚点](/guide/selection)，取消或换文档后不应插入到新的光标位置。

内置 Image 在调用 resolver 之前就校验源码地址；不接受 `asset:` 等自定义协议。使用安全相对路径或 HTTPS 地址作为持久标识。若必须支持新协议，需要自己的字段校验和 codec，不能只换 resolver。

## 更换图片组件

`Image.extend({ addNodeView() { return VueNodeViewRenderer(ImageView) } })` 可以继承字段 codec 和资源协议。组件通过 `nodeView.status` 处理加载、成功、错误，使用 `nodeView.resource` 中的结果，并通过 `updateAttributes` 或受控动作修改源码。

可增加标题表单、错误提示和设计系统操作按钮；不要从 `.cm-content` 移出图片范围或直接改 `src` 后声称已保存。组件显示资源改变与 Markdown 字段改变是两件不同的事。

## 链接与外部打开策略

链接功能的 `Link.configure({ openLink })` 接管显式用户动作的打开策略；只在应用需要时打开安全链接。投影构建或图片加载不能自动触发外部导航。引用式链接/图片、相对地址和特殊协议需要按实际 codec 支持范围处理，不能把显示成功等同于任意格式都可精确写回。

## 验证

覆盖成功、HTTP 失败、非图片响应、体积超限、迟到响应、删除节点、只读及撤销；粘贴与拖入分别验证配置上传与内联两种情况。确认离开页面后没有遗留 object URL，源码始终保存持久 ID。进一步参见 [资源生命周期](/extensions/resources) 和 [UI 与主题](/customization/theme)。
