跳转至

17 - 图片引用标准与 AI 图库交互

状态:已发布(插件 4.28.0 + image-host 1.0.15,2026-10-20)。本文是 CIR v1 标准、实现说明与验收依据;实现状态见 §15。 方案提出时基线:插件 4.27.0(c8da50b)+ image-host 1.0.14;本次实现:插件 4.28.0 + image-host 1.0.15。 关联:16-图片生命周期与图床方案(图床存储/引用/清理)、04-工具系统、05-命令与配置、08-相关后端项目、09-注意事项与常见问题。 一句话目标:[img:<ref>] 成为全插件通用的图片引用标准;AI 能可靠地看图、批量看图、批量处理,并且清楚每张图来自哪个群、是否可信。


0. 背景与目标

0.1 调查结论(本次实现要修的现状)

问题 现状 本次实现
AI 直接看图 MCP image_search 只返回文本,工具结果里的 [img:<sha>] 不会变成图片;只能靠 image_to_text 让识图模型转述成文字 新增 CIR 翻译层 + image_view;image_to_text 接受标准引用
图床 id 可不可用 只有“发送”链路认 [img:<sha>];resolve_special_id / get_image / image_to_text 都不认 所有图片入口统一走 resolveRef
工具返回图片的来源 新产出/新入库的图片经常没有来源;画廊图来源显示为空 工具图片来源 = 工具调用所在的群/会话,永不写“未知”;来源写入元数据;自动标签只对工具产出图片打英文类型标签
工具返回图片的标签 上一版把来源/群/工具/类型打成标签,与元数据重复 元数据与标签分离:来源/群/工具/类型只写元数据;自动标签只对工具产出图片打英文类型标签(ai-generated/meme/mcp-image/tool-image),不调识图、不复制元数据
AI 批量看图 image_view 尚未实现;image_to_text 一次一张 image_view 支持 ids[](默认 ≤4);image_to_text 支持 ids[](默认 ≤3)
AI 批量处理 image_keep / image_tag 已支持 ids[],但上限不明确、没有 unkeep 明确上限(≤50/次);新增 image_unkeep;不提供 AI 删除
模型取图可达性 publicUrl 是内网 IP 时云模型取不到;失败后被永久标 urlBroken 按“模型可达性”自动选 URL/b64;404 才标坏,可恢复
提示词工程模式 工具结果 contentParts 被丢弃,图片看不到 PE 模式保留 image parts

0.2 本次实现的 5 个交付物

  1. CIR v1:[img:<ref>] 通用图片引用标准(含 h<sha256> 规范形式)。
  2. ImageRefAdapter 翻译层:工具输出 → 模型上下文、模型/工具入参 → Image、Image → 模型可消费内容。
  3. 工具图片来源(元数据)与标签分离:工具返回的图片必须有来源(工具调用所在群),来源/群/工具/类型写入元数据;标签只保留类型/内容语义;自动标签只对工具产出图片按产出者打英文类型标签(不调识图),不复制元数据。
  4. 看图与批量能力:image_view(批量看)、image_to_text(批量描述)、image_keep/tag/unkeep(批量处理)。
  5. 可达性与自愈:模型 URL/b64 选择、urlBroken 只对真删除生效、回退链。

1. 决策摘要

# 决策 说明
1 通用图片引用标准 CIR v1 [img:<ref>],ref 支持 h<sha256> / 裸 sha256 / 插件图片 ID / user_avatar: / group_avatar:,可带 :描述
2 翻译层只做“规范化 + 注册 + 解析” 工具结果里的 [img:<sha>] → [img:h<sha>] 并注册虚拟图;不做自动图片注入
3 工具结果规范化只对“图片引用生产者”生效 默认 image-host MCP(按服务器名),或 mcpServers 配置 "imageRefs": true;其它工具文本保持原样,不扩大提示注入面
4 虚拟图只进内存 LRU 不落 storage、不进 ImageIndex;修掉“识别后把 image_<sha> 写进存储”的问题
5 工具图片来源 = 工具调用上下文 平台/群/用户/会话/时间/工具名;没有群时用私聊/会话;任何情况下不写“未知”
6 元数据与标签分离 来源/群/工具/服务器/类型/信任一律写元数据(不写标签);标签只保留类型/内容语义;自动标签只对工具产出图片按产出者映射英文类型标签(不调识图)、幂等、带上限
7 看图入口 image_view(多模态直接看)/ image_to_text(文字描述);两者都接受 CIR
8 image_view 默认注册 作为「图片」分类内置工具常驻注册(两个小决策之一)
9 get_image 对图床引用选 A 明确报错“无协议端缓存,请用 URL 或 [img:] 发送”,不落本地文件(两个小决策之一)
10 批量上限 看图 ≤4 张/次;批量描述 ≤3 张/次;keep/tag/unkeep ≤50 个 id/次;search ≤50 条/页;不提供 AI 删除
11 模型可达性 公网 publicUrl 用 URL;内网/回环 publicUrl + 云模型改用 b64;本机模型可用 URL
12 失败不误标 模型下载失败先探 404;真删才 urlBroken;够不到 → b64 重试一次;可恢复
13 PE 模式保留 contentParts 提示词工程模式下工具返回的图片也能被模型看到
14 安全边界不变 用户输入仍 stripUserTags;context_edit_replace 仍转义;pub_send 仍净化;媒体队列仍转义显示
15 自动类型标签用英文 按产出者确定性映射:ai-generated / meme / mcp-image / tool-image;kebab-case、小写;不调用识图模型;手动内容标签语言不限
16 自动标签不调识图 自动类型标签由产出工具确定性映射,零额外模型调用、零额外请求;不根据图片内容猜测标签

2. 通用图片引用标准 CIR v1

2.1 语法

image-tag := "[" "img:" ref (":" description)? "]"
ref       := "h" 64HEX        // 图床托管图的规范形式(翻译层生成)
           | 64HEX            // 兼容输入:裸 sha256
           | plugin-id        // 插件已有图片 ID(旧格式,如 abc123 / AI绘图_xxx)
           | "user_avatar:" uid
           | "group_avatar:" gid
  • 全角括号 []、全角冒号 : 兼容(沿用现有解析习惯)。
  • 工具返回的 /i/<sha256> 绝对地址也作为 ref 接受(便于模型从搜索结果里复制)。
  • :描述 只用于展示/发送,不参与查找;描述可含冒号,解析取第一个冒号前的 ref。

2.2 规范形式与兼容

输入 规范输出 说明
[img:9f2c…(64hex)] [img:h9f2c…] 工具结果规范化时改写并注册虚拟图
[img:h9f2c…] 原样 已是规范形式
[img:9f2c…:群友发的猫] [img:h9f2c…:群友发的猫] 保留描述
[img:abc123] 原样 插件图片 ID 不变
[img:user_avatar:123] [img:user_avatar:123] 头像别名,不变
[img:https://…/i/9f2c…] [img:h9f2c…] 工具输出里的绝对地址
  • 不重写已持久化上下文:旧上下文里的 [img:<sha>] 不迁移,靠解析器兼容。
  • 只有“工具结果进入上下文”这一处会改写为 h<sha>;用户输入、助手回复、历史消息不改写。

2.3 谁必须遵守

  • 插件内置工具、MCP 工具(含 image-host 后端):输出图片引用必须用 [img:<ref>];接受图片参数必须接受 CIR 的任意形态。
  • 后端 image-host:
  • image_search 输出 [img:<sha>](现状不变,由插件规范化);
  • image_keep / image_rename / image_tag 接受 id / ids[] 里的 CIR / h<sha> / 裸 sha / 插件 ref(见 §9)。

2.4 安全边界

  • 用户原始文本仍走 stripUserTags,[img:] 被转义为字面量,不能借用户输入引用任意图片。
  • context_edit_replace 仍走 stripUserTags(防止模型把可发送标签写进持久化上下文)。
  • 工具结果里的 [img:] 是否可被解析,由“来源工具是否声明为图片引用生产者”决定(§3.5)。

3. 翻译层:ImageRefAdapter

3.1 位置与职责

新增 src/resource/image_ref.ts,是插件内的小模块(不是新服务、不加端口)。只做三件事:

  1. 工具输出 → 模型:把工具结果里的 [img:<sha>] 规范化为 [img:h<sha>],并注册虚拟图。
  2. 模型/工具 → Image:把任意 CIR 形态解析成 Image(插件图/图床图/头像)。
  3. Image → 模型内容:把引用变成模型可消费的 image_url(URL 或 data:image/...;base64)。
[MCP/内置工具] --[img:<sha256>]--> normalizeText --> 上下文 [img:h<sha256>]
[模型] --[img:h<sha>] / 裸 sha / 旧 id--> resolveRef --> Image
[模型] --image_view([img:h<sha>])--> resolveRef --> image_url(多模态)或文字描述(降级)

3.2 接口契约

export interface ParsedRef {
  kind: 'hosted' | 'plugin' | 'user_avatar' | 'group_avatar';
  id: string;             // hosted=sha256;plugin=imageId;avatar=uid/gid
  description?: string;   // 仅用于展示/发送
  canonical: string;      // [img:h<sha>] / [img:<pluginId>] / [img:avatar:...]
}

export function parseRef(raw: unknown): ParsedRef | null;
export function resolveRef(raw: unknown): Image | null;
export function normalizeText(text: string): { text: string; translated: number };
export async function toModelPart(image: Image, model?: { baseUrl?: string }):
  Promise<{ type: 'image_url'; image_url: { url: string } } | null>;

resolveRef 解析顺序:

  1. 去空白,剥 [img:] / [avatar:] / [group_avatar:] 包裹;
  2. 剥 :描述;
  3. h + 64hex / 64hex / {base}/i/<sha> → hosted;
  4. user_avatar: / group_avatar: → 头像;
  5. 其它 → Image.get(pluginId);
  6. 失败返回 null。

3.3 四个统一接入点

边界 位置 改动
工具结果入上下文 context.ts addToolCallbackMessage 若来源工具是图片引用生产者,文本走 normalizeText(见 §3.5)
MCP 工具结果 mcp.ts syncTools → tool.solve 按服务器 imageRefs 决定是否 normalizeText;保留 contentParts
请求组装 message.ts resolveImageById / textToMultimodalContent 改用 resolveRef + getModelSrc
发送与工具入参 message_segments.ts、context.ts findImage、tool_resolve_id.ts、special_id_params.ts、tool_image.ts、tool_raw.ts、tool_forum.ts、tool_meme.ts 全部改用 resolveRef,不再各自 Image.get(id.split(':')[0])

3.4 虚拟图注册表

  • Image.hostedRefs: Map<string, { img: Image; at: number }>,LRU 上限 256、TTL 1 小时。
  • 虚拟图:imageId = 'h' + sha256、hostedId = sha256、hostedUrl = '/i/' + sha256、virtualHosted = true、url = ''。
  • Image.save 对虚拟图直接 return;ImageIndex.touch 不调用;不写 image_h<sha> 存储键。
  • imageToText 成功后把 description 写回虚拟图缓存(仅内存),不落盘。
  • Image.fromHostedId 保留为兼容包装,内部走 resolveRef。

3.5 工具结果规范化的信任门控

  • MCP 服务器配置新增可选 "imageRefs": true(标准 mcpServers 扩展字段)。
  • 启动器/生成器给 image-host 自动写 "imageRefs": true;旧配置回退按服务器名 image-host 判定。
  • 只有这类服务器的工具结果会做 [img:<sha>] → [img:h<sha>] 转换与注册;其它工具文本原样保留。
  • 原因:工具结果属于外部数据,不能让任意网页/MCP 文本里的 [img:] 自动变成可发送引用(提示注入面)。

3.6 明确不做:不自动把工具结果里的图片全部注入

image_search 一次可能返回 20 条;自动把每个 [img:] 变成 pixel 会带来 token/延迟/隐私成本。翻译层只做“引用标签规范化 + 注册”,真正看图由显式 image_view / image_to_text 触发(可审计、可控)。


4. 工具图片:来源元数据与类型标签(新增)

4.1 定义与分类

“工具返回图片”分四类,处理方式不同:

类型 例子 是否入库 来源(元数据) 自动类型标签
A. 工具新产出 MCP image 内容块、text_to_image、meme_generator、工具返回 base64/data URL 是(入图床,异步) 工具调用所在的群/会话 + tool/server(必填) 是(按产出工具确定性映射;上传同事务;默认开)
B. 工具返回已有图 image_search、image_view、image_to_text、read_raw、pub_read 否(已在图床) 图片自身 ref 来源;另显示“本次调用群” 否(不重复打标)
C. 上下文图片 收到消息里的 [img:<pluginId>] 已在图床(或入库中) 原 ref 来源;缺失时标“插件抓取(历史数据)” 否
D. 长期图库图 WebUI 手动上传 / 收进图库 已在图床 显示“长期图库(人工上传)”,绝不写“未知” 否

4.2 来源规则:只写元数据,永不写“未知”

A 类(新产出) 的来源由工具调用上下文生成,写入 ref 元数据:

interface ToolImageSource {
  platform?: string;    // QQ / DISCORD / ...
  group_id?: string;    // 群号;私聊为空
  user_id?: string;     // 触发者
  session_id?: string;
  message_id?: string;  // 能拿到则填
  at: number;           // 时间
  tool: string;         // 工具名
  server?: string;      // MCP 服务器名
  via: 'tool';
}
  • 有群:group_id 必填,source 显示为「QQ 群<群号>」。
  • 私聊:group_id=''、user_id 必填,显示为「QQ 私聊<用户号>」。
  • 无 ctx 的命令/定时任务:填 session_id,显示为「会话 」。
  • 全部为空时兜底 via='tool',显示为「工具调用(会话未记录)」——仍是明确元数据,不出现“未知”。
  • 以上来源信息全部是元数据(ref.source),不生成任何标签。

B 类(已有图) 的来源显示:

  • 有 ref 来源:按原来源显示(平台/群/用户/时间)。
  • origin=manual 且无 ref 来源:显示「长期图库(人工上传)」。
  • origin=plugin 且无 ref 来源:显示「插件抓取(历史数据)」。
  • 结果行额外带「本次调用:群<群号>」或「本次调用:私聊」,来自工具调用上下文,仅展示、不写标签。

C/D 类:与 B 相同,来源字段永远有值。

4.3 元数据与标签的边界

4.3.1 什么写元数据,什么写标签

信息 存储位置 查询方式 是否写标签
图库/抓取(信任) blob.origin(manual/plugin) scope=gallery/plugin/all 否
平台/群/用户/会话/消息/时间 ref 的来源列 platform / group_id 过滤 否
产出工具 / MCP 服务器 ref.tool / ref.server tool / server 过滤 否
格式 / 大小 / 创建时间 / 最后访问 blob 列 排序/展示 否
产出类型(生成图 / 表情包 / MCP 图 / 工具图) tag 表(自动类型标签) tags[] 过滤 是(英文类型标签)
用户/AI 内容标签 tag 表 tags[] 过滤 是(语言不限)

规则:来源/群/工具/服务器/时间/文件名等“来源类”信息只写元数据,不写标签。 上一版的 来源: / 群: / 工具: / MCP: / 类型: 标签全部取消。

4.3.2 自动类型标签(确定性映射,不走识图)

自动标签只对工具产出的新图片生效,按“谁产出的”直接映射,不调用 image-understanding / 视觉模型,不根据图片内容猜测:

产出者 自动标签
text_to_image / 生图任务 ai-generated
meme_generator meme
MCP image 内容块(任意服务器) mcp-image
其它工具返回 base64/data URL、无法识别产出者 tool-image
收到消息里的图片(插件抓取,非工具产出) 不打标
image_search / image_view / image_to_text / read_raw 等返回的已有图 不打标

规则:

  • 英文小写 kebab-case(ai-generated / meme / mcp-image / tool-image);不用中文。
  • 每图最多 2 个自动类型标签;单标签长度 ≤ 32。
  • 幂等:INSERT OR IGNORE;重复入库不重复写。
  • 不写来源/群/用户/工具名/服务器名/时间/文件名/格式/信任等元数据。
  • 与手动内容标签共存;用户可在 WebUI 删除。
  • 零额外模型调用、零额外网络请求(映射在代码里完成)。
  • 配置见 §4.6。

4.3.3 写入时机

对象 时机 方式
A 类新产出 入库同事务 POST /api/upload 的可选 tags 只放类型标签;来源元数据通过 source / tool / server 字段传
B/C/D 类 — 不自动写标签;需要内容标签时由 AI 显式 image_tag 或用户在 WebUI 维护

4.4 工具图片入库流程(A 类)

工具产出图片字节/URL
  → 创建/补全 Image(sourceMeta = ToolImageSource;producer = text_to_image / meme_generator / mcp)
  → 按 producer 映射出 1–2 个英文类型标签(不调模型)
  → Image.adoptBase64ToHost() / captureToHost()
  → POST /api/upload { base64|url, ref, source, tags }
  → 后端同事务写 blob(origin) + ref(source, tool, server, via) + tag(类型标签)
  → 插件记录 hostedId/hostedUrl(不再保留 base64)
  • ref 表新增 tool / server / via 列;迁移 user_version 3(只追加)。
  • tags 只允许类型/内容标签;后端对 来源: / 群: / 工具: / MCP: / 类型: 前缀或不含语义的元数据式标签直接过滤。
  • 现有 sourceInfo() 只带 session/raw_url;需要补 group_id / user_id / platform / tool / server。
  • 生图/表情包:在工具提交时把 ToolImageSource 与 producer 类型存进 MediaTask,落地时按映射写标签,再入库。
  • MCP image block:在 normalizeMCPResult 时带上工具调用 ctx 与服务器名,入库时写 source + mcp-image 标签;不把服务器名/工具名写成标签。

4.5 工具结果文本格式

image_search 每行:

<id> | [图库·可信]   | 名称 | 标签:ai-generated,cat | 来源: 长期图库(人工上传) | 工具: - | 本次调用: 群<群号> | /i/<id> | [img:<id>]
<id> | [抓取·待验证] | 名称 | 标签:meme             | 来源: QQ 群<群号> 用户<用户号> | 工具: image_search | 本次调用: 群<群号> | /i/<id> | [img:<id>]
  • 来源/工具/本次调用是元数据,不是标签;标签只显示类型/内容(自动类型标签为英文)。
  • 来源字段不允许为空/-/“未知”;至少是三类明确元数据之一。
  • 本次调用:群<群号> 由插件在 MCP 结果后追加(或后端在结果里带,见 §9);仅用于让 AI 知道“这张图是在哪个群被工具拿到的”,不写成标签。

4.6 配置项

配置 分组 默认 说明
工具产出图片自动类型标签 图片 开 A 类新产出图片按产出工具自动打英文类型标签(ai-generated / meme / mcp-image / tool-image)
自动类型标签每图上限 图片 2 自动类型标签总数上限(1–4)

自动类型标签是确定性映射,不调用识图/视觉模型、不产生额外请求;失败不影响图片使用;日志 tag [image-tag]。


5. 看图与批量能力(新增)

5.1 image_view(默认注册)

{
  "name": "image_view",
  "description": "直接查看图片内容(把图片交给当前多模态模型)。id 支持标准图片引用 [img:<ref>]、图床托管 ID h<sha256>、裸 sha256、插件图片 ID。支持批量 ids(默认最多 4 张),当前聊天模型不支持视觉时自动降级为文字描述。注意:最终回复里写 [img:<ref>] 是发给用户,不是自己看图;要看图必须调用本工具。",
  "parameters": {
    "type": "object",
    "properties": {
      "id":   { "type": "string", "description": "单张图片引用" },
      "ids":  { "type": "array", "items": { "type": "string" }, "maxItems": 4, "description": "批量图片引用(与 id 二选一,最多 4 张)" },
      "detail": { "type": "string", "description": "可选:想看图中的什么细节" }
    },
    "additionalProperties": false
  }
}

行为:

  1. 逐个 resolveRef;失败项在文本里列出,不拖垮整批。
  2. 若 Model.getChatModel('chat')?.isMultimodal:对每张图 toModelPart;成功则返回 contentParts(≤4 个 image part)+ 文本摘要;超过单张/总量上限的图跳过并提示。
  3. 非多模态或取图失败:降级 imageToText(批量上限 3 张),返回文字描述。
  4. 文本摘要每行只给 [img:<canonical>];来源/标签以 image_search 结果为准(image_view 不额外查后端,避免每次看图多一次请求),不返回大段 base64。

上限(常量,可后续配置化):

项 默认 说明
IMAGE_VIEW_MAX_IMAGES 4 单次最多看几张
IMAGE_VIEW_INLINE_MAX_MB 4 单张内联 base64 上限;超过跳过
IMAGE_VIEW_TOTAL_MB 12 单次内联总量上限
IMAGE_VIEW_MAX_DESCRIBE 3 非多模态降级时最多描述几张

5.2 image_to_text(改造)

  • 入参:id? / ids?[](二选一,最多 3 张)、content?(关注点)。
  • 接受 CIR 任意形态(含 [img:<ref>] / :描述)。
  • 逐张调用识图模型,返回:
    [img:<canonical>]
    描述:…(来源/标签以 image_search 结果为准)
    
  • 单张行为与 4.27.0 完全一致;ids[] 是新增能力。
  • 失败项明确写“识别失败/未找到”,不静默丢图。

5.3 批量查看

  • 多模态模型:image_view(ids=[…≤4]) → 一次拿到 ≤4 张图。
  • 非多模态模型:image_view 自动降级 image_to_text(≤3 张)或 AI 自己多次 image_to_text。
  • 先搜后看:image_search 支持 limit ≤ 50 / offset 分页;AI 先筛出候选 id,再 image_view(ids)。
  • 提示词规则:超过 4 张要分批;不要把 20 张图一次性塞进一次 image_view。

5.4 批量处理

工具 参数 上限 说明
image_keep id? / ids?[] 50 收进长期图库(origin: plugin → manual),幂等
image_unkeep id? / ids?[] 50 新增:移出长期图库(manual → plugin),可逆;与 WebUI 的「移出图库」一致
image_tag id? / ids?[] + add?[] / remove?[] 50 ids / 10 tags 批量增删标签,幂等
image_rename id + name 1 单张(批量同名无意义,维持现设计)
image_search limit ≤ 50 50/页 分页
~~image_delete~~ — — 不做:删除只留给人(WebUI)

批量规则:

  • 单批 > 10 张时,提示词要求 AI 先向用户确认(避免误操作大量图片)。
  • 返回必须带 affected、failed、changed_ids;部分失败不整批报错。
  • 批量操作前建议先 image_search 核对来源/标签;image_keep 只对 plugin 生效,已是 manual 的幂等跳过。
  • 不提供 AI 批量删除/批量重命名/批量移出后清空等危险动作。

5.5 上限汇总

能力 默认上限 备注
image_view 单次张数 4 超出提示分批
image_view 单张内联 4 MB 超出跳过
image_view 单次内联总量 12 MB 超出跳过
image_to_text 单次张数 3 每张一次识图调用
image_keep/unkeep/tag ids 50 后端 MCP 侧硬限制
image_search limit 50 分页
自动类型标签/图 2 按产出者映射(ai-generated/meme/mcp-image/tool-image)
AI 删除 0 不提供

5.6 提示词工程模式(PE)

message.ts 的 PE 分支当前丢弃工具结果 contentParts,导致 image_view 与其它 MCP 图片在 PE 模式下看不到图。改为:

  • role=tool 且有 contentParts 且当前模型是多模态 → 返回 { role:'user', content: [textPart, ...imageParts] };
  • 无图片/非多模态 → 保持现有纯文本输出。
  • 非 PE 模式路径不变(已支持 contentParts)。

5.7 降级与失败

场景 行为
引用解析失败 文本列出失败项,其余继续
单张取图失败 跳过该张,文本说明原因;不整批失败
非多模态模型 自动降级文字描述
图片超过内联上限 跳过并提示用 image_to_text
图床 404 文本“图片已删除”,不重试
图床不可达 文本“图床不可达”,本轮降级,下轮可重试

6. 提示词定稿

6.1 MCP 后端工具描述

image_search

搜索私人图库。scope=gallery(默认)只搜长期图库:人工上传或确认收藏的图片,来源可信、不会被自动清理,可直接使用。scope=plugin 搜插件自动抓取的图片:来源混杂(群聊/私聊/来源可能未记录),未经确认,使用、发送或收藏前必须核对来源、名称与标签;涉及他人隐私或用途不明确时先询问用户。scope=all 两者都搜。 结果每行带 [图库·可信] / [抓取·待验证]、来源(平台/群/用户/时间)、工具/服务器、本次调用群、类型/内容标签和 [img:<id>]。来源/信任/工具/服务器是元数据,用 scope / platform / group_id / tool / server 过滤;tags 表示图片类型/内容(自动类型标签为英文)。[img:<id>] 是标准图片引用,可直接写进回复发送,也可传给看图工具。limit ≤ 50,可 offset 分页。

image_keep

把图片收进长期图库(origin → manual),此后不会被自动清理。只对插件自动抓取的图有意义。请先核对来源与用途,确认适宜后再收藏;单次最多 50 张。参数 id 或 ids[],也接受标准图片引用 [img:<ref>] / h<sha>。

image_unkeep

把图片移出长期图库(manual → plugin),之后重新进入自动淘汰候选。可逆操作;单次最多 50 张。参数 id 或 ids[]。

image_tag

给图片增删内容标签(手动语义标签,语言不限),支持批量 ids[](≤50)与 add[]/remove[](每组 ≤10 个)。自动类型标签(ai-generated/meme/mcp-image/tool-image)由系统维护,不要重复添加;不要写来源/群/工具/时间等元数据。幂等;返回真正变化的图片数与未找到项。

image_rename

给单张图片重命名(不支持批量同名)。

image_tags

列出长期图库的标签清单(按使用次数降序,支持关键字过滤与分页)。

6.2 插件系统提示词新增段(仅图床/MCP 启用时注入)

## 图片与图库
- 图片统一用 [img:<ref>] 引用(插件图片 ID、图床托管 ID h<sha256>、user_avatar:<ID>、group_avatar:<ID>)。
- 长期图库(origin=manual,image_search 默认 scope=gallery)的图片由人上传或确认收藏,来源可信、不会被自动清理,可直接用于回复/发送/以图生图。
- 插件自动抓取的图(origin=plugin)来源混杂(群聊/私聊/来源可能未记录),未经确认;使用、发送或收藏前必须核对来源、名称与标签;涉及他人隐私或用途不明确时先询问用户,不要擅自传播或收藏。
- 上下文里由用户/群消息带来的图片([img:插件ID])属于插件抓取,转发、外发或收藏前按同样规则处理。
- 工具返回图片的来源/群/工具/服务器是元数据,不是标签;检索时用 scope / group_id / tool / server 过滤来源,不要指望标签里出现群号或工具名。
- 工具产出的图片会自动打英文类型标签(ai-generated / meme / mcp-image / tool-image,按产出者映射,不调识图);标签用于类型/内容检索,不要把来源、群号、工具名、时间写进标签。
- 想把抓取的图升级为可信资产:确认后调用 image_keep(收进长期图库);移出用 image_unkeep。
- 在最终回复里写 [img:<ref>] 是发给用户;要自己查看图片内容,调用 image_view(多模态模型直接看,支持一次 ≤4 张)或 image_to_text(返回文字描述,支持一次 ≤3 张)。
- 批量处理:keep/unkeep/tag 单次 ≤50 张;超过 10 张先向用户确认;不要尝试删除图片(没有删除工具)。

6.3 内置工具描述

  • image_to_text:查看图片内容并返回文字描述;id 支持 CIR / 图床托管 ID / 插件图片 ID;支持 ids[](≤3)。
  • image_view:见 §5.1。
  • resolve_special_id(type=image):支持 CIR,返回 hosted_id、来源(元数据)、类型/内容标签。
  • get_resource_path:只服务本地资源;图床引用请用 image_view / [img:] 发送(见 §8.4)。

7. 全量 img 标签覆盖矩阵

复核范围:src/**/*.ts 中所有 [img: / avatar: / group_avatar: 文本命中,以及所有 Image.get(...) 直接调用。真正需要改动的函数/分支约 20 处;其余属于生产端、展示端或安全边界,保持不动。

7.1 生产端(写入上下文/描述)——不改

位置 行为 结论
image.ts handleImageMessageSegment 生成 [img:<pluginId>(:描述)] 不动
raw_marker.ts buildImageTruncateMarker 截断标记带 image_id 不动;read_raw 用解析器兼容
task_queue.ts、tool_meme.ts、mcp/result.ts 生图/表情包/MCP 图片结果生成 [img:<pluginId>] 不动
string.ts normalizeRenderTags 旧 <|img:...|> 归一化 不动
snapshot.ts pub_read 快照保留 [img:] 不动
string.ts stripUserTags 用户输入 [img:] 转义 不动(安全边界)
tool_context.ts sanitizeText 上下文编辑替换文本转义 不动(安全边界)
tool_media_tasks.ts 队列列表转义 [img:] 不动

7.2 解析/发送端——统一改走 resolveRef

位置 现状 改动
string.ts parseSpecialTokens 正则兼容 h<sha>:desc 不改(只验证)
string.ts transformContentToText findImage 改走 resolveRef
message_segments.ts textToSegments findImage + split(':') 改走 resolveRef
message.ts resolveImageById 只 Image.get 改走 resolveRef
message.ts textToMultimodalContent 已解析 Image 改走 resolveRef + getModelSrc
special_id_params.ts 只 Image.get 改走 resolveRef;get_image 按 §8.4
tool_resolve_id.ts 只 Image.get 改走 resolveRef,返回 hosted_id
tool_image.ts image_to_text findImage 不剥标签 改走 resolveRef;支持 ids[]
tool_image.ts resolveTtiImage 已剥标签 改走 resolveRef + 模型可用地址
tool_meme.ts findImage 改走 resolveRef
tool_meme.ts 上游参考图 img.url \|\| img.path 改走 getModelSrc,失败明确报错
tool_forum.ts extractImagesFromContent Image.get 改走 resolveRef
tool_forum.ts image_ids Image.get 改走 resolveRef
tool_raw.ts collectImageRawEntries / readImageById Image.get 改走 resolveRef;虚拟图无原文时提示先识别
api.ts imageToText Image.get/URL 改走 resolveRef
cmd/sub_cmd/image.ts findImage 不改(findImage 内部改)
tool_resource_path.ts 只查本地资源 不改;图床引用明确 NOT_FOUND(文档说明)
tool_pub.ts resolveSendMessage + 源会话 context 不改(解析器改)

7.3 解析器/发送/模型消费——新增或改造

位置 改动
新增 src/resource/image_ref.ts CIR 解析、规范化、虚拟图 LRU、resolveRef、toModelPart
image.ts virtualHosted;Image.save/Index 跳过;getModelSrc;imageToText 可达性/回退;markUrlBrokenByUrls hosted-aware
image_host.ts urlForModel(hostedId, modelBaseUrl);lastStatus;probeHosted(复用 b64 出口判断 404/不可达)
context.ts findImage 走 resolveRef
agent.ts 图片下载失败 hosted 探测 + b64 重试 + 不误标
mcp.ts 按 imageRefs 规范化结果文本;保留 contentParts
templates.ts 新增“图片与图库”段(条件注入)
配置层 mcpServers 增加 imageRefs;图片配置增加 §4.6 两项
image.ts getSendFile / ensureBytes 保持现有语义,仅新增 getModelSrc 调用方

7.4 持久化/清理——小幅改动

位置 改动
image.ts Image.save 虚拟图 no-op;不写 image_h<sha>
image_index.ts 虚拟图不 touch
image_gc.ts 不动(索引里只有插件图;虚拟图不进索引)
validKeysMap 不新增持久字段(模型内联标记只放内存)

8. 网络、失败与边界行为

8.1 模型可达性

新增 Image.getModelSrc(model?: { baseUrl?: string }):

base64                → data:image/...;base64
hosted:
  externalBase 为空   → fetchBase64 → data URL
  externalBase 公网   → remoteUrl
  externalBase 内网/回环:
      模型 baseUrl 也是内网/回环 → remoteUrl
      否则                        → fetchBase64 → data URL
非 hosted URL         → 现有逻辑(ensureBytes / 原始 URL)
头像                  → qlogo URL
  • externalBase = image-host.publicUrl(非回环时)或非回环的 url。
  • 模型端点分类:baseUrl 的 host 是字面量私网/回环 → 本机/内网模型;否则视为云模型。
  • 协议端发送仍用 getSendFile() / publicUrl,不受 getModelSrc 影响。
  • 可选配置 图床模型取图方式(自动 / 总是URL / 总是base64),默认自动;先不强制实现。

8.2 urlBroken 自愈

  • 模型报“无法下载图片”时:
  • 对 hosted URL:fetchBase64(id) 探测;
  • 200 → 图片存在,设置本轮/该图 inlineForModel,用 b64 重试一次,不标 urlBroken;
  • HTTP 404 → 真删除,标 urlBroken;
  • 网络错误/图床不可达 → 不标坏,下一轮再试。
  • hosted 图的 urlBroken 可恢复:探测 200 且 b64 成功后清除。
  • markUrlBrokenByUrls 只处理非 hosted URL;hosted URL 走新的 markHostedDownloadFailures。

8.3 回退链

模型 URL(可达时) → b64 → 原始稳定 URL(非 QQ 临时直链) → 文本标签
  • textToMultimodalContent / imageToText 不再在“图床取不到字节”时直接留文本。
  • imageToText 的 urlBroken 早退只对“确认已删”生效。

8.4 get_image 对图床引用(决策 A)

  • get_image 的 file 传 hosted 引用([img:h<sha>] / 裸 sha)时:
  • 返回 INVALID_FILE,提示:“图床图片没有协议端缓存文件;请直接用 URL 或 [img:<ref>] 发送;需要字节请用 image_view / image_to_text / /i/<id>/b64”。
  • 不下载、不落本地文件。
  • 插件 ID 的 get_image 行为保持现状。
  • resolve_special_id(type=image) 对 hosted 引用返回 hosted_id、内部 URL、来源(元数据)/类型/内容标签摘要,供 AI 判断。

9. 后端(image-host)改动

9.1 MCP 工具

改动 内容
image_search since 真正实现;多标签 AND;platform/group_id/tool/server 下推 SQL 并修正 total/offset;输出 origin(元数据)+ 来源(不写“未知”)+ 工具/服务器 + 本次调用群 + 类型/内容标签;limit ≤ 50
image_keep 支持 ref(插件 imageId)与 CIR;ids 上限 50;返回 changed_ids / skipped / failed;截断要提示
image_unkeep 新增:manual → plugin,可逆,ids ≤ 50
image_tag 只接受类型/内容标签;自动类型标签仅由系统写入;拒绝 来源:/群:/工具:/MCP:/类型: 等元数据式标签;上报 failed/not_found;affected 按图片数而非行数;add+remove 不重复计;字符串参数按单值处理;上限 50/10
image_rename id 格式/名称长度校验;回收站图是否允许改名明确(建议否)
image_tags 统计类型/内容标签;与 gallery 口径一致;默认只统计长期图库(或显式标注包含两类)
传输 tools/call 参数错误返回 isError;inputSchema 运行时校验;unknown tool 用 -32602;文档头改成 6 工具(含 unkeep)
文档 mcp.py docstring、backend.json 描述、docs/16 §5.11 同步

9.2 REST / 数据

接口/表 改动
POST /api/upload 增加可选 tags(只允许类型/内容标签);source 增加 tool / server / via;同事务写 blob(origin) + ref(source,tool,server,via) + tag(类型标签)
ref 表 新增 tool TEXT、server TEXT、via TEXT;迁移 PRAGMA user_version = 3(只追加列)
POST /api/batch 支持 AI 手动补内容标签(已有 action=tag);单次上限 50 张
image_search 输出 来源/工具/类型均为元数据且永远有值:有 ref 显示原来源;无 ref 按 origin 显示「长期图库(人工上传)」/「插件抓取(历史数据)」
WebUI 标签视图显示类型/内容标签;来源/工具/类型作为卡片元数据展示;不新增页面

9.3 自动类型标签写入

  • 只对 A 类新产出:upload 同事务写入 tags,按产出者映射:
  • text_to_image / 生图任务 → ai-generated
  • meme_generator → meme
  • MCP image 内容块 → mcp-image
  • 其它工具 base64/data URL → tool-image
  • B/C/D 类不自动写标签。
  • 后端过滤元数据式标签:以 来源: / 群: / 工具: / MCP: / 类型: 开头,或纯群号/工具名/时间/文件名的标签,一律拒绝。
  • 不调用识图/视觉模型;零额外模型调用、零额外请求。

10. 安全与“不影响正常行为”

10.1 硬约束

  1. 图床未配置时全链路零变化:无 hosted 引用、无 b64 差异、无额外网络请求;提示词段不注入。
  2. 不重写已持久化上下文:只对新的工具结果做规范化;旧 [img:<sha>] 靠解析器兼容。
  3. 不改变插件图片 ID 语义:生成、解析、发送、清理保持。
  4. 不改安全边界:用户输入仍转义;context_edit_replace 仍 stripUserTags;pub_send 仍净化;媒体队列仍转义显示。
  5. 工具结果规范化只对 imageRefs 服务器:其他工具文本原样,不扩大提示注入面。
  6. 虚拟图不落盘/不进索引:Image.save / ImageIndex 跳过,GC 行为不变。
  7. 模型 URL/b64 选择只影响“模型/上游”:协议端发送仍用 getSendFile。
  8. PE 修复只影响带 contentParts 的多模态工具结果:无图片时输出与现在一致。
  9. 自动类型标签写入失败不阻断主流程:只记日志,图片照常使用(映射在代码内完成,通常不会失败)。
  10. AI 仍不能删除图片:删除只在 WebUI。
  11. 新增 image_view 会让工具清单多一项(预期增量,属于本次实现的一部分);已有工具的名称、描述与行为不变。
  12. 标签不与元数据重复:来源/群/工具/服务器/类型/信任只写元数据;标签只做类型/内容语义(自动类型标签英文,手动标签语言不限)。
  13. 自动类型标签零模型调用:只按产出者确定性映射,不调用识图/视觉模型,不根据图片内容猜测。

10.2 回归清单

# 场景 期望
1 未配图床 既有单测全过;无额外 fetch
2 插件图片 ID(发送/识图/read_raw/resolve_special_id/get_image) 与 4.27.0 一致
3 用户输入 [img:...] 仍被转义,不可发送
4 context_edit_replace 写入 [img:sha] 仍被转义
5 pub_send 非 QQ 仍剥标签
6 media_tasks 列表 仍不显示可解析标签
7 旧 <|img:...|> 上下文 仍归一化并可发送
8 图床公网 publicUrl 仍用 URL,不额外取 b64
9 图床回环 + 无 publicUrl 仍用 b64
10 云模型 + 内网 publicUrl 改用 b64,不产生 urlBroken
11 图床删图 404 标坏一次,之后不再请求
12 图床宕机 不标坏,恢复后可用
13 PE 模式 + MCP 图片 能看到图;无图时输出一致
14 工具图片入库 有 source 元数据、无元数据式标签、自动类型标签为英文(ai-generated/meme/mcp-image/tool-image)、无“未知”
15 image_view 批量 4 张 返回 ≤4 个 image part;超限提示
16 image_tag/keep/unkeep 批量 部分失败不整批失败;affected 准确
17 标签内容 不出现群号/工具名/来源前缀;自动类型标签为英文 kebab-case;不调用识图模型

11. 逐文件改动清单

11.1 插件

文件 改动
src/resource/image_ref.ts 新增:CIR 解析、规范化、虚拟图 LRU、resolveRef、toModelPart
image.ts virtualHosted;save/index 跳过;getModelSrc;imageToText 可达性/回退;markUrlBrokenByUrls hosted-aware;fromHostedId 走 LRU
image_host.ts urlForModel;lastStatus;probeHosted;uploadByUrl/uploadBase64 支持 tags
context.ts findImage 走 resolveRef;工具结果规范化入口
message.ts 多模态解析走 resolveRef;getModelSrc;PE 模式保留 contentParts
string.ts transformContentToText 走 resolveRef(解析器不改)
message_segments.ts textToSegments 走 resolveRef
special_id_params.ts 走 resolveRef;get_image 决策 A
tool_resolve_id.ts 走 resolveRef;返回 hosted_id
tool_image.ts image_to_text 支持 CIR/ids[];新增 image_view;TTI 参考图走 getModelSrc
tool_meme.ts image_ids 走 resolveRef;上游参考图走 getModelSrc;入库 source 元数据 + 类型标签(meme)
tool_forum.ts 两处 Image.get → resolveRef;image_ids 支持 CIR
tool_raw.ts 两处 Image.get → resolveRef
api.ts imageToText 走 resolveRef
mcp.ts 按 imageRefs 规范化;返回图类型标签(mcp-image/tool-image)与调用上下文元数据
mcp/result.ts MCP image block 入库(source 元数据 + mcp-image 类型标签)
agent.ts 图片下载失败 hosted-aware;handleMessages 传 model
templates.ts “图片与图库”提示词段
media/task_queue.ts 生图落库带 source 元数据 + 类型标签(ai-generated)
配置层 imageRefs;§4.6 两项;get_image/批量相关常量
scripts/unit-test-image-host.ts、scripts/unit-test-entry.ts 新增 §12 用例

11.2 后端(aiplugin4-backends-shop/backends/image-host/)

文件 改动
mcp.py 6 工具(新增 unkeep)、CIR/ref 入参、信任/来源/调用群、since/多标签/SQL 过滤、tag 计数、keep 报告、isError、参数校验、文档头
store.py 搜索 total/SQL、tag 计数、回收站一致性、resolve_refs、ref tool/server/via、类型/内容标签入库
imagehost.py /api/upload 接收 tags(类型/内容)/source.tool/server/via;过滤元数据式标签;错误码
backend.json 描述改 6 工具
tests/ 补 keep/unkeep/rename/tag/search/MCP/CIR/自动类型标签用例

11.3 文档 / 生成器 / 启动器

  • 本文(17);docs/README.md 导航;docs-site 收录与 nav。
  • docs/16 增加“AI 引用/看图/工具图片”交叉引用。
  • docs/04 工具表增加 image_view;docs/05 配置表增加 §4.6 两项;docs/16 MCP 工具表增加 image_unkeep 并更新为 6 工具。
  • aiplugin4-config MCP 默认给 image-host 加 "imageRefs": true。
  • 启动器复制 MCP 配置时给 image-host 写 imageRefs。

12. 测试

12.1 插件单测

  1. parseRef 矩阵:[img:sha]、h<sha>、[img:sha:desc]、全角、/i/<sha>、插件 ID、头像、非法。
  2. normalizeText:sha → h;插件 ID 不动;非 imageRefs 工具不调用。
  3. 虚拟图:重复解析复用;不写 storage、不进 index;LRU 淘汰。
  4. 解析器覆盖:findImage、resolveImageById、textToSegments、resolve_special_id、get_image、image_to_text、image_ids、read_raw、agent API。
  5. image_view:多模态 contentPart(1/4 张)、非多模态描述、回环 b64、404、超限。
  6. image_to_text:单张旧行为不变;ids[] 批量与失败项。
  7. textToMultimodalContent 用户消息含 [img:h<sha>]。
  8. PE 模式:tool contentParts → user image part。
  9. 可达性:公网/内网/回环 × 模型公网/本机 四种组合。
  10. 降级:hosted 200 → 不标坏 + b64 重试;404 → 标坏;网络失败 → 不标坏;恢复。
  11. 工具图片:A 类入库带 source 元数据 + 英文类型标签(ai-generated/meme/mcp-image/tool-image);B 类不自动写标签;标签不含元数据;来源文本无“未知”。
  12. 标签校验:自动类型标签为英文 kebab-case;拒绝元数据式前缀与纯群号;自动路径不调用识图模型。
  13. 批量:keep/unkeep/tag 的 50 上限、部分失败、affected。
  14. 安全:用户输入/context_edit/pub_send/media 列表不变。
  15. 非图床路径 golden:与 4.27.0 输出一致。

12.2 后端单测

  • 自动类型标签(producer 映射、无元数据)入库幂等/上限;ref tool/server/via;image_search 来源永不空;since;多标签;平台/群/tool 分页;tag failed/affected;keep/unkeep;CIR/ref 入参;isError。
  • 回归:上传/去重/引用计数/cap/manual 保护/回收站/变更日志。

12.3 集成

  • 真图床:工具返回图片入库 → 来源=当前群(元数据)、标签为 ai-generated → image_search 可见 → image_view(ids=[…]) 一次看 4 张 → 回复 [img:h<sha>] 发送。
  • 长期图库图 image_search(scope=gallery) → 来源显示「长期图库(人工上传)」→ image_keep 幂等。
  • 删除图床图片 → 插件对账标坏;不重复请求。
  • 图床停掉 → 发送/识图降级不报错。
  • 同一张图两条 ref:keep 一条不影响另一条。

12.4 验收标准

  • 既有单测 + 新增测试全绿。
  • grep 复核:不存在未走 resolveRef 的图片解析入口。
  • 未配图床实例逐场景对比无差异。
  • 工具返回图片结果里不再出现“未知/空来源”。
  • image_view 批量与 image_keep/tag/unkeep 上限生效。

13. 分期

阶段 内容 依赖
A CIR + resolveRef + 虚拟图 + 全量解析点接入 无
B 工具图片入库/来源元数据/英文类型标签(producer 映射,不调识图) A
C image_view / image_to_text 批量 + PE contentParts 修复 A
D 可达性 + urlBroken 自愈 + 回退链 A
E 后端 MCP 6 工具/搜索/tag/keep/unkeep/CIR B
F 提示词、配置、文档、生成器/启动器同步 B/C/E
G 全量测试与回归 全部

14. 未决与后续

  • 图床模型取图方式 是否需要暴露为用户配置(自动/总是URL/总是base64)?默认自动;可后置。
  • image_tags 是否只统计长期图库,还是显式分成两组(gallery/plugin)?建议先统一为 gallery 口径并标注。
  • 自动类型标签的映射表是否覆盖全部产出工具(目前 4 类);新增产出工具时需同步扩展映射。
  • 是否给“收到消息图片”也打 chat-image 类型标签?当前不打,避免与插件抓取元数据重复。
  • 是否给 image_search 增加 id 精确查询(便于验证单张图来源/标签)?可作为 P2。
  • 本文与 16 的分工:16 管存储/引用/清理;17 管 AI 引用、看图、工具图来源元数据/英文类型标签、批量交互。

15. 实现状态(4.28.0 / image-host 1.0.15)

项 状态
CIR v1 解析/规范化 + Image.resolveRef + 虚拟图内存 LRU(不落 storage/索引) ✅
工具结果规范化(仅 imageRefs / image-host MCP);其它工具文本不动 ✅
image_view(默认注册,id/ids[]≤4,多模态 contentParts,非多模态/失败降级描述,内联 4MB/张、12MB/次) ✅
image_to_text(id/ids[]≤3,CIR,逐张失败可报告) ✅
get_image 对图床引用明确报错(决策 A) ✅
resolve_special_id(type=image) 返回 hosted_id / hosted_url ✅
工具产出来源元数据(source/tool/server/via;生成图/表情包/MCP image block) ✅
工具产出图片英文类型标签(ai-generated / meme / mcp-image / tool-image,确定性映射、不调识图) ✅
模型可达性(内网 publicUrl + 云模型走 b64)与 urlBroken 404/够不到区分、b64 重试 ✅
PE 模式保留工具结果 image contentParts ✅
image-host:ref tool/server/via、upload tags/source、MCP 6 工具(search/keep/unkeep/rename/tag/tags)、搜索 SQL 过滤/total/since/多标签、tag 计数与元数据标签拒绝 ✅
启动器「复制 MCP 配置」/ 生成器为 image-host 写 imageRefs: true ✅
文档/提示词/工具描述同步 ✅

已知差异/后续(不影响本次实现验收):

  • image_view 超过 4 张直接返回“请分批”,不静默截断。
  • 只有工具产出的图片自动打类型标签;image_search 等返回的已有图不自动改标签(避免读放大/污染图库)。
  • image_tags 默认 gallery 口径;REST /api/tags 默认全部来源以兼容 WebUI。
  • REST /api/batch 仍保留 500 ids 上限(50 上限在 MCP 工具侧),WebUI 不受影响。
  • get_resource_path 仍只服务本地资源;图床引用请用 image_view / image_to_text / [img:] 发送。
  • 后端 57 个 Python 单测 + 插件 483 个单测通过;另用临时 image-host 1.0.15 服务跑过 upload(tags/source) → MCP 6 工具 → search/keep/unkeep/tag/rename 契约 E2E(未做浏览器视觉 E2E)。