cortico-world-forward-reader 0.1.7 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  给 [Coopanion](https://github.com/Pal-AI-Lab/Coopanion) 的 Coo 一个工具:把 QQ 的「合并转发 / 聊天记录」一层层拆到底。
4
4
 
5
- > **前置:先装「QQ 群聊」插件(qqbot)。** 这个 World 自己不连 QQ,也不存 QQ 的连接信息——它借 QQ 群聊插件配好的那套东西干活:
5
+ > **前置:先装「QQ群聊插件」。** 这个 World 自己不连 QQ,也不存 QQ 的连接信息——它借 QQ群聊插件配好的那套东西干活:
6
6
  > SnowLuma 的 OneBot HTTP 接口用来取消息,插件里配的视觉端点用来识图。
7
7
  > 没装那个插件,这里两个工具都只会回「连不上」。
8
8
 
@@ -28,17 +28,27 @@ Coopanion 自带的消息展开只摊开一层,转发里套着的转发看不
28
28
 
29
29
  ## 前置
30
30
 
31
- - 装了 **QQ 群聊插件(qqbot)**:SnowLuma 连接、OneBot 地址、视觉端点都在它那儿配。
31
+ - 装了 **QQ群聊插件(qqbot)**:SnowLuma 连接、OneBot 地址、视觉端点都在它那儿配。
32
32
  - 装了 **SnowLuma**(OneBot v11 实现),HTTP 接口开着,默认 `127.0.0.1:3000`。
33
33
  - OneBot 配置文件里有 `accessToken`。默认读 `D:/snowluma/config/onebot_<QQ号>.json`,也可以在配置页手填路径或直接填地址和 token。
34
34
 
35
- ## 识图(可选,默认关)
35
+ ## 图片(默认当附件交出去)
36
36
 
37
- 拆到图片时,可以让视觉端点在图后面补一句「这张图是什么」。
37
+ 拆到图片时,图会被下载下来、作为**附件**跟回执一起交给模型。
38
38
 
39
- - 端点默认**跟着 QQ 群聊插件走**:先在 QQ 群聊插件里把视觉端点、模型、key 配好,这里就自动认得到,不用再填一遍。
40
- - 那边没配、或者想单独用别的模型,才在下面的「视觉端点 / 模型 / 密钥」里填。
41
- - 一张图一次调用,会花钱,所以默认关;工具调用时也可以当场传 `captions: true`。
39
+ - 能看图的模型(主模型支持图片)就直接看到图,不用多花一次调用。
40
+ - 看不见图的模型只会收到一行文字说明(谁、什么时候、链接),不会因为带图出错。
41
+ - 一次最多贴 `attachMax` 张(默认 6),单张超过 `attachMaxMb`(默认 8 MB)就跳过,正文里仍然有链接。
42
+ - 不想贴附件,把「把图片当附件一起交出去」关掉,就退回只给链接。
43
+
44
+ ## 识图(只在要文字描述时才跑)
45
+
46
+ 附件之外还想要一句**文字描述**时,在调用时传 `captions: true`——**没有开关会自动识图**,因为图已经当附件交给模型了,再识一遍是白花钱。
47
+
48
+ - 端点默认**跟着 QQ群聊插件走**:那边配好了视觉端点、模型、key,这里就自动认得到,不用再填一遍。
49
+ - 配置页里**没有**端点/模型/密钥这几格;那边没配,识图就只会写一句「没找到视觉端点」。
50
+ - 它只对**看不见图的模型**有用(纯文字模型只会读到一行「谁、什么时候、一张图、链接」),能看图的模型用不上。
51
+ - 一张图一次调用,会花钱,所以不传就不跑。
42
52
  - 识图失败不会弄坏整份拆解,只会在那张图后面写一句失败原因。
43
53
 
44
54
 
@@ -50,6 +60,8 @@ Coopanion 自带的消息展开只摊开一层,转发里套着的转发看不
50
60
  - 嵌套层数上限(默认 8)
51
61
  - 回执字数上限(默认 12000)
52
62
  - 请求超时(默认 15000 ms)
63
+ - 把图片当附件一起交出去(默认开)、一次最多处理几张(默认 6,附件和识图共用)、单张上限 MB(默认 8)
64
+ - 识图:每句描述字数(不传 `captions: true` 就不会跑;端点自动跟着 QQ群聊插件走,不单独配)
53
65
 
54
66
  ## 限制
55
67
 
package/describe.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "tags": [
6
6
  "read"
7
7
  ],
8
- "description": "拆开 QQ 的「合并转发 / 聊天记录」卡片,包括转发里套着的转发。\n什么时候用:收到 `qq.forward` 事件、或者 QQ 消息里写着「[合并转发]」但只摊开了一层、里面还有「又一层」时;想看清某段聊天记录到底是谁说了什么,也用它。\n怎么给编号:QQ 消息事件行首的 `#编号`(如 `#265277450`)就是 message_id,原样填进 ref;带不带 `#`、前后有空格都不要紧。编号可能是负数(如 `#-1175570559`),负号是编号的一部分,要一起填,别去掉。\n它只读,只走本机的 SnowLuma OneBot HTTP 接口(默认 127.0.0.1:3000,由 QQ 群聊插件配好),不发消息、不改任何东西;接口没开或 token 不对会直接说清楚哪一步不对。输出过长会截断。\n图片:默认只给链接;传 `captions: true` 会顺便把图交给视觉端点,在结尾单列一节说每张图是什么(一张图一次调用,按需开)。",
8
+ "description": "拆开 QQ 的「合并转发 / 聊天记录」卡片,包括转发里套着的转发。\n什么时候用:收到 `qq.forward` 事件、或者 QQ 消息里写着「[合并转发]」但只摊开了一层、里面还有「又一层」时;想看清某段聊天记录到底是谁说了什么,也用它。\n怎么给编号:QQ 消息事件行首的 `#编号`(如 `#265277450`)就是 message_id,原样填进 ref;带不带 `#`、前后有空格都不要紧。编号可能是负数(如 `#-1175570559`),负号是编号的一部分,要一起填,别去掉。\n它只读,只走本机的 SnowLuma OneBot HTTP 接口(默认 127.0.0.1:3000,由 QQ群聊插件配好),不发消息、不改任何东西;接口没开或 token 不对会直接说清楚哪一步不对。输出过长会截断。\n图片:默认把图下载成附件,跟回执一起交给你——能看图的模型直接就看得到;看不见图的模型只会收到一行文字说明,不会出错。不会自动识图;还想要一句「这张图是什么」的文字描述,再传 `captions: true`(会花钱)。",
9
9
  "parameters": {
10
10
  "type": "object",
11
11
  "properties": {
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "captions": {
29
29
  "type": "boolean",
30
- "description": "顺便给图片配描述(默认按配置,通常关)。一张图一次视觉调用,会花一点钱;图多的时候只识前几张。"
30
+ "description": "要一句「这张图是什么」的文字描述才传 true——不传就不识图,一次视觉调用一张图,会花钱。"
31
31
  }
32
32
  },
33
33
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-forward-reader",
3
- "version": "0.1.7",
3
+ "version": "0.1.11",
4
4
  "description": "转发拆解 World:把 QQ 的「合并转发」一层层拆到底,嵌套在里面的转发也展开,输出带缩进的人话。只读,不发消息。",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -41,11 +41,11 @@
41
41
  "locales": {
42
42
  "zh": {
43
43
  "displayName": "转发拆解",
44
- "description": "【需要先装 QQ 群聊插件】把 QQ 的「合并转发」一层层拆到底——嵌套在里面的转发也会展开,按人、按时间列成带缩进的文字。只读:借 QQ 群聊插件(qqbot)配好的本机 SnowLuma OneBot HTTP 接口取消息,不发消息、不改任何东西。识图也借那边的视觉端点,用之前先在 QQ 群聊插件里配好端点/模型/key。"
44
+ "description": "【需要先装 QQ群聊插件】把 QQ 的「合并转发」一层层拆到底——嵌套在里面的转发也会展开,按人、按时间列成带缩进的文字。只读:借 QQ群聊插件(qqbot)配好的本机 SnowLuma OneBot HTTP 接口取消息,不发消息、不改任何东西。拆到的图片默认作为附件一起交给模型(能看图就直接看到,看不见图只会读到一行文字说明)。"
45
45
  },
46
46
  "en": {
47
47
  "displayName": "Forward Reader",
48
- "description": "Requires the QQ chat (qqbot) plugin. Expands QQ merged-forward messages recursively, including forwards nested inside forwards, into an indented transcript. Read-only: it reuses the SnowLuma OneBot HTTP API that plugin already set up, and never sends or changes anything. Image captions reuse that plugin's vision endpoint too — configure it there first."
48
+ "description": "Requires the QQ chat (qqbot) plugin. Expands QQ merged-forward messages recursively, including forwards nested inside forwards, into an indented transcript. Read-only: it reuses the SnowLuma OneBot HTTP API that plugin already set up, and never sends or changes anything. Images are attached to the tool result by default, so a model that accepts images sees them; others just get a one-line text note."
49
49
  }
50
50
  },
51
51
  "describe": "describe.json",
package/src/config.js CHANGED
@@ -18,22 +18,26 @@ export const DEFAULTS = {
18
18
  /** 单次请求超时(毫秒)。 */
19
19
  timeoutMs: 15000,
20
20
 
21
- // ── 识图(可选,默认关)────────────────────────────────────────────
22
- /** 拆到图片时,顺便让视觉端点说一句图里是什么。一百张图就一百次调用,默认关。 */
23
- caption: false,
24
- /** 一次拆解最多识几张图,多出来的只留链接。 */
25
- captionMax: 6,
26
- /** 每句描述最多多少字。 */
27
- captionChars: 60,
21
+ // ── 图片附件(默认开)─────────────────────────────────────────────
22
+ /**
23
+ * 拆到的图片下载下来,作为附件跟回执一起交给模型。
24
+ * 能看图的模型直接看到;看不见的模型只会收到 fallbackText,不会出错。
25
+ */
26
+ attachImages: true,
27
+ /** 一次最多贴几张附件,多出来的只在正文留链接;识图也用这一个上限。 */
28
+ attachMax: 6,
29
+ /** 单张图最大多少 MB,超了就不贴(正文里还有链接)。 */
30
+ attachMaxMb: 8,
31
+
32
+ // ── 识图(只在工具调用时明确要求才跑)─────────────────────────────────
28
33
  /**
29
- * 想复用别的配置文件里的视觉端点时才填;留空=自己去本机 Coopanion 的配置里找。
30
- * 一般不用碰:这台机器上已经配好的视觉端点会被自动认出来。
34
+ * 不设开关:图默认当附件交给模型,再自动识一遍是白花钱。
35
+ * 只有 qq_forward_read 传了 captions: true 才会调视觉端点。
31
36
  */
32
- visionConfig: '',
33
- /** 下面三项留空就沿用找到的那份配置;填了就以这里为准。 */
34
- visionEndpoint: '',
35
- visionModel: '',
36
- visionKey: '',
37
+ /** 每句描述最多多少字。 */
38
+ captionChars: 60,
39
+ // 视觉端点不在这配:自己去本机 Coopanion 的配置里找(一般就是 QQ群聊插件那份),
40
+ // 配置页里一个格子都不露——留空格子只会让人以为非得填。
37
41
  };
38
42
 
39
43
  export const CONFIG_GROUP = {
@@ -43,13 +47,14 @@ export const CONFIG_GROUP = {
43
47
  type: 'object',
44
48
  title: '转发拆解',
45
49
  description:
46
- '【前置:先装 QQ 群聊插件】把 QQ 的「合并转发」一层层拆到底:借那个插件配好的 SnowLuma OneBot HTTP 接口递归调 get_forward_msg,' +
47
- '嵌套在里面的转发也会展开;识图也借它的视觉端点(识图前先在 QQ 群聊插件里配好端点/模型/key)。只读,不发消息、不改 QQ 配置。',
50
+ '把 QQ 的「合并转发」一层层拆到底,嵌套在里面的转发也展开,按人、按时间列成带缩进的文字。' +
51
+ '前置:先装 QQ群聊插件(本 World 借它的 OneBot 接口取消息,自己不连 QQ)。' +
52
+ '拆到的图片默认作为附件交给模型,看不见图的模型只会读到一行文字说明。只读,不发消息、不改 QQ 配置。',
48
53
  properties: {
49
54
  'worlds.forward-reader.snowlumaConfig': {
50
55
  type: 'string',
51
56
  title: 'SnowLuma 配置路径',
52
- description: 'OneBot 配置文件(含 accessToken)。默认 D:/snowluma/config/onebot_2565315695.json。',
57
+ description: 'OneBot 配置文件,里面存着 accessToken;换了 QQ 号才需要改。',
53
58
  'x-hot': true,
54
59
  },
55
60
  'worlds.forward-reader.baseUrl': {
@@ -88,22 +93,28 @@ export const CONFIG_GROUP = {
88
93
  maximum: 120000,
89
94
  'x-hot': true,
90
95
  },
91
- 'worlds.forward-reader.caption': {
96
+ 'worlds.forward-reader.attachImages': {
92
97
  type: 'boolean',
93
- title: '拆到图片时顺手识图',
98
+ title: '把图片当附件一起交出去',
94
99
  description:
95
- '打开后,拆解里出现的图片会被下载、交给视觉端点,在图后补一句「这张图是什么」。' +
96
- '一张图一次调用,会花一点钱,默认关;工具调用时也可以单独指定。',
100
+ '拆到的图片下载下来当附件交给模型:能看图就直接看到,看不见图只会读到一行文字说明。关掉就只留链接。',
97
101
  'x-hot': true,
98
102
  },
99
- 'worlds.forward-reader.captionMax': {
103
+ 'worlds.forward-reader.attachMax': {
100
104
  type: 'integer',
101
- title: '一次最多识几张图',
102
- description: '默认 6;多出来的图只留链接,不送去识图。',
105
+ title: '一次最多处理几张图',
106
+ description: '默认 6:附件和识图共用这一个上限,多出来的只在正文里留链接。',
103
107
  minimum: 0,
104
108
  maximum: 30,
105
109
  'x-hot': true,
106
110
  },
111
+ 'worlds.forward-reader.attachMaxMb': {
112
+ type: 'integer',
113
+ title: '单张图大小上限(MB)',
114
+ description: '超过这个大小的图不贴,只在正文里留链接。默认 8。',
115
+ minimum: 1,
116
+ maximum: 32,
117
+ },
107
118
  'worlds.forward-reader.captionChars': {
108
119
  type: 'integer',
109
120
  title: '每句描述字数',
@@ -111,29 +122,6 @@ export const CONFIG_GROUP = {
111
122
  minimum: 20,
112
123
  maximum: 200,
113
124
  },
114
- 'worlds.forward-reader.visionConfig': {
115
- type: 'string',
116
- title: '视觉端点配置路径(一般不用填)',
117
- description:
118
- '留空就行:留空时会自己在本机 Coopanion 的配置里找已经配好的视觉端点。' +
119
- '只有想改用另一份配置文件时才填它的路径。',
120
- },
121
- 'worlds.forward-reader.visionEndpoint': {
122
- type: 'string',
123
- title: '视觉端点(可选)',
124
- description:
125
- '留空就行:会用 QQ 群聊插件里配好的视觉端点(那边没配的话,在这里填端点/模型/key)。',
126
- },
127
- 'worlds.forward-reader.visionModel': {
128
- type: 'string',
129
- title: '视觉模型(可选)',
130
- description: '留空就跟端点自动找到的那份配置走。',
131
- },
132
- 'worlds.forward-reader.visionKey': {
133
- type: 'string',
134
- title: '视觉端点密钥(可选)',
135
- description: '留空就用找到的那份配置里的密钥;别把 key 写进仓库。',
136
- },
137
125
  },
138
126
  },
139
127
  };
package/src/index.js CHANGED
@@ -36,6 +36,33 @@ function pad(depth) {
36
36
  return ' '.repeat(depth);
37
37
  }
38
38
 
39
+ const EXT_BY_MIME = {
40
+ 'image/jpeg': 'jpg',
41
+ 'image/png': 'png',
42
+ 'image/gif': 'gif',
43
+ 'image/webp': 'webp',
44
+ 'image/bmp': 'bmp',
45
+ 'image/avif': 'avif',
46
+ };
47
+
48
+ const MIME_BY_EXT = {
49
+ jpg: 'image/jpeg',
50
+ jpeg: 'image/jpeg',
51
+ png: 'image/png',
52
+ gif: 'image/gif',
53
+ webp: 'image/webp',
54
+ bmp: 'image/bmp',
55
+ avif: 'image/avif',
56
+ };
57
+
58
+ /** 优先信 Content-Type,其次看链接后缀;都不是图就当 jpeg 试。 */
59
+ function mimeOf(contentType, url) {
60
+ const ct = String(contentType || '').split(';')[0].trim().toLowerCase();
61
+ if (/^image\//.test(ct)) return ct;
62
+ const ext = (String(url || '').split('?')[0].match(/\.([a-z0-9]+)$/i) || [])[1];
63
+ return MIME_BY_EXT[String(ext || '').toLowerCase()] || 'image/jpeg';
64
+ }
65
+
39
66
  function senderName(sender) {
40
67
  if (!sender) return '?';
41
68
  return sender.card || sender.nickname || (sender.user_id ? String(sender.user_id) : '?');
@@ -166,7 +193,8 @@ export class ForwardReaderWorld {
166
193
  const whole = foldMessage(segments, { links: true });
167
194
  const who = senderName(data?.sender);
168
195
  const when = clockOf(data?.time);
169
- const wantCaption = Boolean(args.captions ?? cfg.caption);
196
+ // 识图只在调用时明确要求才跑:图已经当附件交给模型了,再自动识一遍是白花钱。
197
+ const wantCaption = args.captions === true;
170
198
 
171
199
  if (!whole.nested.length) {
172
200
  // 不是合并转发:直接把这条消息本身给人看。
@@ -176,7 +204,7 @@ export class ForwardReaderWorld {
176
204
  out.push('');
177
205
  out.push('这条不是合并转发卡片,所以没有可拆的层。');
178
206
  if (wantCaption) await this.addCaptions(out, whole.images.map((i) => ({ ...i, who, when })));
179
- return cap(out.join('\n'), limit);
207
+ return this.finish(out, limit, whole.images.map((i) => ({ ...i, who, when })));
180
208
  }
181
209
 
182
210
  out.push(`# 转发拆解 · 消息 ${rawRef}${who !== '?' ? ` · ${who}` : ''}${when ? ` · ${when}` : ''}`);
@@ -195,18 +223,66 @@ export class ForwardReaderWorld {
195
223
  if (wantCaption) await this.addCaptions(out, state.images);
196
224
 
197
225
  out.unshift(`共 ${state.levels} 层 / ${state.items} 条${state.truncated ? '(已达条数上限,后面没再往下翻)' : ''}`);
198
- return cap(out.join('\n'), limit);
226
+ return this.finish(out, limit, state.images);
227
+ }
228
+
229
+ /**
230
+ * 收尾:正文截断,再把图片当附件一起交出去。
231
+ * 能看图的模型直接看到图;看不见的模型只收到 fallbackText,不会报错。
232
+ */
233
+ async finish(out, limit, images) {
234
+ const blobs = await this.collectBlobs(images || []);
235
+ if (blobs.length) {
236
+ out.push('');
237
+ out.push(
238
+ `—— 有 ${blobs.length} 张图片已经作为附件一起给你了(看不见图的话,正文里的链接还在;想让它转述一句就传 captions: true)——`
239
+ );
240
+ }
241
+ const text = cap(out.join('\n'), limit);
242
+ return blobs.length ? { text, blobs } : text;
243
+ }
244
+
245
+ /** 把图片下载成附件;太大、下载失败、不是图的,直接跳过(正文里本来就有链接)。 */
246
+ async collectBlobs(images) {
247
+ if (!this.cfg.attachImages) return [];
248
+ const max = clampInt(this.cfg.attachMax, 6, 0, 30);
249
+ if (max <= 0) return [];
250
+ const maxBytes = clampInt(this.cfg.attachMaxMb, 8, 1, 32) * 1024 * 1024;
251
+ const timeoutMs = clampInt(this.cfg.timeoutMs, 15000, 1000, 120000);
252
+ const picked = images.filter((i) => i && i.url).slice(0, max);
253
+ const blobs = [];
254
+ for (let i = 0; i < picked.length; i += 1) {
255
+ const img = picked[i];
256
+ try {
257
+ const res = await fetch(img.url, { signal: AbortSignal.timeout(timeoutMs) });
258
+ if (!res.ok) continue;
259
+ const bytes = new Uint8Array(await res.arrayBuffer());
260
+ if (!bytes.length || bytes.length > maxBytes) continue;
261
+ const mime = mimeOf(res.headers.get('content-type'), img.url);
262
+ if (!/^image\//.test(mime)) continue;
263
+ const tag = `${img.who && img.who !== '?' ? img.who + ' ' : ''}${img.when || ''}`.trim();
264
+ blobs.push({
265
+ bytes,
266
+ mime,
267
+ name: `forward-${i + 1}.${EXT_BY_MIME[mime] || 'png'}`,
268
+ fallbackText: `${tag ? '[' + tag + '] ' : ''}图片 ${i + 1}:${img.url}`,
269
+ });
270
+ } catch {
271
+ // 拿不到就当没有这张:不炸整份拆解。
272
+ }
273
+ }
274
+ return blobs;
199
275
  }
200
276
 
201
277
  /** 把拆出来的图片送去识图,在结尾单列一节。失败也是一句话,不炸整份拆解。 */
202
278
  async addCaptions(out, images) {
203
279
  const list = (images || []).filter((i) => i && i.url);
204
280
  if (!list.length) return;
205
- const max = clampInt(this.cfg.captionMax, 6, 0, 30);
281
+ const max = clampInt(this.cfg.attachMax, 6, 0, 30);
206
282
  const chars = clampInt(this.cfg.captionChars, 60, 20, 200);
207
283
  const picked = max > 0 ? list.slice(0, max) : [];
208
284
  if (!picked.length) return;
209
- const vision = await resolveVision(this.cfg);
285
+ const vision = await resolveVision();
210
286
  out.push('');
211
287
  out.push(`—— 图片 ${list.length} 张${list.length > picked.length ? `(只识前 ${picked.length} 张)` : ''} ——`);
212
288
  for (const img of picked) {
package/src/tools.js CHANGED
@@ -17,9 +17,10 @@ export const TOOL_DECLS = [
17
17
  '想看清某段聊天记录到底是谁说了什么,也用它。\n' +
18
18
  '怎么给编号:QQ 消息事件行首的 `#编号`(如 `#265277450`)就是 message_id,原样填进 ref;带不带 `#`、前后有空格都不要紧。' +
19
19
  '编号可能是负数(如 `#-1175570559`),负号是编号的一部分,要一起填,别去掉。\n' +
20
- '它只读,只走本机的 SnowLuma OneBot HTTP 接口(默认 127.0.0.1:3000,由 QQ 群聊插件配好),不发消息、不改任何东西;' +
20
+ '它只读,只走本机的 SnowLuma OneBot HTTP 接口(默认 127.0.0.1:3000,由 QQ群聊插件配好),不发消息、不改任何东西;' +
21
21
  '接口没开或 token 不对会直接说清楚哪一步不对。输出过长会截断。\n' +
22
- '图片:默认只给链接;传 `captions: true` 会顺便把图交给视觉端点,在结尾单列一节说每张图是什么(一张图一次调用,按需开)。',
22
+ '图片:默认把图下载成附件,跟回执一起交给你——能看图的模型直接就看得到;看不见图的模型只会收到一行文字说明,不会出错。' +
23
+ '不会自动识图;还想要一句「这张图是什么」的文字描述,再传 `captions: true`(会花钱)。',
23
24
  parameters: {
24
25
  type: 'object',
25
26
  properties: {
@@ -41,7 +42,7 @@ export const TOOL_DECLS = [
41
42
  },
42
43
  captions: {
43
44
  type: 'boolean',
44
- description: '顺便给图片配描述(默认按配置,通常关)。一张图一次视觉调用,会花一点钱;图多的时候只识前几张。',
45
+ description: '要一句「这张图是什么」的文字描述才传 true——不传就不识图,一次视觉调用一张图,会花钱。',
45
46
  },
46
47
  },
47
48
  required: ['ref'],
package/src/vision.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * 识图:借本机已经配好的视觉端点,给一张图配一句描述。
3
3
  *
4
- * 端点从哪来:优先用这个 World 自己的 visionEndpoint / visionModel / visionKey;
5
- * 没填就自动去本机 Coopanion 的配置里找(一般是 QQ 那份 qqbot.vision),找到就用,
6
- * 找不到就老实说没找到。写死不合适的盘符和模型名没有意义——换台机器就跑不通。
4
+ * 端点从哪来:本机 Coopanion 的配置里找(一般是 QQ 那份 qqbot.vision),找到就用,
5
+ * 找不到就老实说没找到。配置页里不给格子填——留空的格子只会让人以为非得填。
6
+ * 写死不合适的盘符和模型名没有意义——换台机器就跑不通。
7
7
  * 端点按 OpenAI 兼容格式 POST /chat/completions,content 里混 text + image_url(data URL 也行)。
8
8
  *
9
9
  * 踩过的坑:
@@ -58,21 +58,9 @@ async function readHostVision(configPath) {
58
58
  return null;
59
59
  }
60
60
 
61
- /** 配置里的显式值优先,其次读宿主配置。 */
62
- export async function resolveVision(cfg) {
63
- const own = {
64
- endpoint: String(cfg.visionEndpoint || '').trim(),
65
- model: String(cfg.visionModel || '').trim(),
66
- key: String(cfg.visionKey || '').trim(),
67
- };
68
- if (own.endpoint && own.model) return own;
69
- const host = await readHostVision(cfg.visionConfig);
70
- if (!host) return null;
71
- return {
72
- endpoint: own.endpoint || host.endpoint,
73
- model: own.model || host.model,
74
- key: own.key || host.key,
75
- };
61
+ /** 只认本机 Coopanion 配置里已经配好的视觉端点;找不到就返回 null。 */
62
+ export async function resolveVision() {
63
+ return readHostVision(process.env.COOPANION_CONFIG || '');
76
64
  }
77
65
 
78
66
  function guessMime(buf) {
@@ -104,7 +92,7 @@ async function fetchImage(url, timeoutMs) {
104
92
  export async function captionImage(vision, url, opts = {}) {
105
93
  const timeoutMs = opts.timeoutMs ?? 30000;
106
94
  const chars = opts.chars ?? 60;
107
- if (!vision) return '(没找到视觉端点:先在 QQ 群聊插件里配好,或在这里填端点/模型/key)';
95
+ if (!vision) return '(没找到视觉端点:需要先在 QQ群聊插件里把视觉端点配好)';
108
96
  let buf;
109
97
  try {
110
98
  buf = await fetchImage(url, timeoutMs);