cortico-world-forward-reader 0.1.5 → 0.1.6

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,6 +2,10 @@
2
2
 
3
3
  给 [Coopanion](https://github.com/Pal-AI-Lab/Coopanion) 的 Coo 一个工具:把 QQ 的「合并转发 / 聊天记录」一层层拆到底。
4
4
 
5
+ > **前置:先装「QQ 群聊」插件(qqbot)。** 这个 World 自己不连 QQ,也不存 QQ 的连接信息——它借 QQ 群聊插件配好的那套东西干活:
6
+ > SnowLuma 的 OneBot HTTP 接口用来取消息,插件里配的视觉端点用来识图。
7
+ > 没装那个插件,这里两个工具都只会回「连不上」。
8
+
5
9
  Coopanion 自带的消息展开只摊开一层,转发里套着的转发看不见。这个 World 借本机 SnowLuma 的 OneBot HTTP 接口递归调 `get_forward_msg`,把整棵树拉出来,按「谁 · 什么时候 · 说了什么」列成带缩进的文字。
6
10
 
7
11
  **只读**:只调 `get_login_info` / `get_msg` / `get_forward_msg`,不发消息、不改配置、不写任何东西。
@@ -24,9 +28,19 @@ Coopanion 自带的消息展开只摊开一层,转发里套着的转发看不
24
28
 
25
29
  ## 前置
26
30
 
31
+ - 装了 **QQ 群聊插件(qqbot)**:SnowLuma 连接、OneBot 地址、视觉端点都在它那儿配。
27
32
  - 装了 **SnowLuma**(OneBot v11 实现),HTTP 接口开着,默认 `127.0.0.1:3000`。
28
33
  - OneBot 配置文件里有 `accessToken`。默认读 `D:/snowluma/config/onebot_<QQ号>.json`,也可以在配置页手填路径或直接填地址和 token。
29
34
 
35
+ ## 识图(可选,默认关)
36
+
37
+ 拆到图片时,可以让视觉端点在图后面补一句「这张图是什么」。
38
+
39
+ - 端点默认**跟着 QQ 群聊插件已经配好的那份走**,不用在这里再填一遍 key;想单独用别的模型,才填配置里的端点/模型/密钥。
40
+ - 一张图一次调用,会花钱,所以默认关;工具调用时也可以当场传 `captions: true`。
41
+ - 识图失败不会弄坏整份拆解,只会在那张图后面写一句失败原因。
42
+
43
+
30
44
  ## 配置项
31
45
 
32
46
  - SnowLuma 配置路径(默认 `D:/snowluma/config/onebot_<QQ号>.json`)
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),不发消息、不改任何东西;接口没开或 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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortico-world-forward-reader",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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 的「合并转发」一层层拆到底——嵌套在里面的转发也会展开,按人、按时间列成带缩进的文字。只读:借本机 SnowLuma 的 OneBot HTTP 接口取消息,不发消息、不改任何东西。"
44
+ "description": "【需要先装 QQ 群聊插件】把 QQ 的「合并转发」一层层拆到底——嵌套在里面的转发也会展开,按人、按时间列成带缩进的文字。只读:借 QQ 群聊插件(qqbot)配好的本机 SnowLuma OneBot HTTP 接口取消息,识图也借那里的视觉端点,不发消息、不改任何东西。"
45
45
  },
46
46
  "en": {
47
47
  "displayName": "Forward Reader",
48
- "description": "Expands QQ merged-forward messages recursively, including forwards nested inside forwards, into an indented transcript. Read-only: it queries the local SnowLuma OneBot HTTP API and never sends or changes anything."
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 and the vision endpoint that the QQ chat plugin already set up, and never sends or changes anything."
49
49
  }
50
50
  },
51
51
  "describe": "describe.json",
package/src/config.js CHANGED
@@ -25,9 +25,12 @@ export const DEFAULTS = {
25
25
  captionMax: 6,
26
26
  /** 每句描述最多多少字。 */
27
27
  captionChars: 60,
28
- /** 从哪儿读视觉端点(Coopanion 主配置里的 qqbot.vision)。 */
29
- visionConfig: 'D:/Coopanion/data/home/companion/config.json',
30
- /** 下面三项一般留空:留空就用 visionConfig 里配的值。 */
28
+ /**
29
+ * 想复用别的配置文件里的视觉端点时才填;留空=自己去本机 Coopanion 的配置里找。
30
+ * 一般不用碰:这台机器上已经配好的视觉端点会被自动认出来。
31
+ */
32
+ visionConfig: '',
33
+ /** 下面三项留空就沿用找到的那份配置;填了就以这里为准。 */
31
34
  visionEndpoint: '',
32
35
  visionModel: '',
33
36
  visionKey: '',
@@ -40,8 +43,8 @@ export const CONFIG_GROUP = {
40
43
  type: 'object',
41
44
  title: '转发拆解',
42
45
  description:
43
- '把 QQ 的「合并转发」一层层拆到底:借 SnowLuma 的 OneBot HTTP 接口递归调 get_forward_msg,' +
44
- '嵌套在里面的转发也会展开。只读,不发消息、不改 QQ 配置。',
46
+ '【前置:先装 QQ 群聊插件】把 QQ 的「合并转发」一层层拆到底:借那个插件配好的 SnowLuma OneBot HTTP 接口递归调 get_forward_msg,' +
47
+ '嵌套在里面的转发也会展开;识图也借它的视觉端点。只读,不发消息、不改 QQ 配置。',
45
48
  properties: {
46
49
  'worlds.forward-reader.snowlumaConfig': {
47
50
  type: 'string',
@@ -110,24 +113,25 @@ export const CONFIG_GROUP = {
110
113
  },
111
114
  'worlds.forward-reader.visionConfig': {
112
115
  type: 'string',
113
- title: '视觉端点配置路径',
116
+ title: '视觉端点配置路径(一般不用填)',
114
117
  description:
115
- '从哪个配置文件读视觉端点(默认读 Coopanion 主配置里的 qqbot.vision:endpoint / model / apiKeySecret)。',
118
+ '留空就行:留空时会自己在本机 Coopanion 的配置里找已经配好的视觉端点。' +
119
+ '只有想改用另一份配置文件时才填它的路径。',
116
120
  },
117
121
  'worlds.forward-reader.visionEndpoint': {
118
122
  type: 'string',
119
123
  title: '视觉端点(可选)',
120
- description: '留空就用上面配置文件里的值;填了就盖过它。',
124
+ description: '填了就忽略上面自动找到的那份,改用这里填的;留空即自动。',
121
125
  },
122
126
  'worlds.forward-reader.visionModel': {
123
127
  type: 'string',
124
128
  title: '视觉模型(可选)',
125
- description: '留空就用配置文件里的值。',
129
+ description: '留空就跟端点自动找到的那份配置走。',
126
130
  },
127
131
  'worlds.forward-reader.visionKey': {
128
132
  type: 'string',
129
133
  title: '视觉端点密钥(可选)',
130
- description: '留空就用配置文件里的值;别把 key 写进仓库。',
134
+ description: '留空就用找到的那份配置里的密钥;别把 key 写进仓库。',
131
135
  },
132
136
  },
133
137
  },
package/src/tools.js CHANGED
@@ -17,7 +17,7 @@ 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),不发消息、不改任何东西;' +
20
+ '它只读,只走本机的 SnowLuma OneBot HTTP 接口(默认 127.0.0.1:3000,由 QQ 群聊插件配好),不发消息、不改任何东西;' +
21
21
  '接口没开或 token 不对会直接说清楚哪一步不对。输出过长会截断。\n' +
22
22
  '图片:默认只给链接;传 `captions: true` 会顺便把图交给视觉端点,在结尾单列一节说每张图是什么(一张图一次调用,按需开)。',
23
23
  parameters: {
package/src/vision.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * 识图:拿 Coopanion 里已经配好的视觉端点(qqbot.vision)给一张图配一句描述。
2
+ * 识图:借本机已经配好的视觉端点,给一张图配一句描述。
3
3
  *
4
- * 为什么不直接抄一个模型名和 key:主人在设置里换端点/换模型时,这里跟着变,不用改代码。
4
+ * 端点从哪来:优先用这个 World 自己的 visionEndpoint / visionModel / visionKey;
5
+ * 没填就自动去本机 Coopanion 的配置里找(一般是 QQ 那份 qqbot.vision),找到就用,
6
+ * 找不到就老实说没找到。写死不合适的盘符和模型名没有意义——换台机器就跑不通。
5
7
  * 端点按 OpenAI 兼容格式 POST /chat/completions,content 里混 text + image_url(data URL 也行)。
6
8
  *
7
9
  * 踩过的坑:
@@ -9,16 +11,32 @@
9
11
  * 2. 图片要先下载成 buffer 再转 base64,直接把外链丢过去有的端点拉不到(QQ 的图还带 rkey)。
10
12
  */
11
13
  import { readFile } from 'node:fs/promises';
14
+ import { dirname, join } from 'node:path';
12
15
 
13
16
  const MAX_BYTES = 6 * 1024 * 1024;
14
17
 
15
- /** 从 Coopanion 的配置文件里读 qqbot.vision;读不到就返回 null。 */
16
- async function readHostVision(configPath) {
17
- if (!configPath) return null;
18
+ /**
19
+ * 本机上可能放着 Coopanion 配置的地方,按顺序试。
20
+ * 不写死盘符:跑在 Coopanion 里时应用目录就是 execPath 的目录,外面手动跑也能从 cwd 猜。
21
+ */
22
+ function configCandidates() {
23
+ const dirs = [dirname(process.execPath), join(dirname(process.execPath), '..'), process.cwd()];
24
+ const out = [];
25
+ if (process.env.COOPANION_CONFIG) out.push(process.env.COOPANION_CONFIG);
26
+ for (const dir of dirs) {
27
+ out.push(join(dir, 'data', 'home', 'companion', 'config.json'));
28
+ out.push(join(dir, 'home', 'companion', 'config.json'));
29
+ out.push(join(dir, 'config.json'));
30
+ }
31
+ return [...new Set(out)];
32
+ }
33
+
34
+ /** 从一份 Coopanion 配置里取视觉端点;没配就返回 null。 */
35
+ async function readVisionFrom(configPath) {
18
36
  try {
19
37
  const raw = await readFile(configPath, 'utf8');
20
38
  const json = JSON.parse(raw);
21
- const v = json?.worlds?.qqbot?.vision ?? json?.qqbot?.vision;
39
+ const v = json?.worlds?.qqbot?.vision ?? json?.qqbot?.vision ?? json?.vision;
22
40
  if (!v || !v.endpoint || !v.model) return null;
23
41
  return {
24
42
  endpoint: String(v.endpoint),
@@ -30,6 +48,16 @@ async function readHostVision(configPath) {
30
48
  }
31
49
  }
32
50
 
51
+ /** 配置里指定了路径就只读那一份,没指定就自己在本机找。 */
52
+ async function readHostVision(configPath) {
53
+ if (configPath) return readVisionFrom(configPath);
54
+ for (const path of configCandidates()) {
55
+ const found = await readVisionFrom(path);
56
+ if (found) return found;
57
+ }
58
+ return null;
59
+ }
60
+
33
61
  /** 配置里的显式值优先,其次读宿主配置。 */
34
62
  export async function resolveVision(cfg) {
35
63
  const own = {
@@ -76,7 +104,7 @@ async function fetchImage(url, timeoutMs) {
76
104
  export async function captionImage(vision, url, opts = {}) {
77
105
  const timeoutMs = opts.timeoutMs ?? 30000;
78
106
  const chars = opts.chars ?? 60;
79
- if (!vision) return '(没找到能用的视觉端点)';
107
+ if (!vision) return '(没找到能用的视觉端点:可以在设置里填端点/模型/key)';
80
108
  let buf;
81
109
  try {
82
110
  buf = await fetchImage(url, timeoutMs);