dsh-qq-tools 0.8.1 → 0.8.2

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.
Files changed (3) hide show
  1. package/README.md +5 -5
  2. package/lib/shared.js +69 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -99,7 +99,7 @@ tools/qq-send-poke/
99
99
 
100
100
  私聊和群聊在程序里是分开的两条路径:群聊靠轮询 @,私聊靠 WebSocket 的 `message_type=private` 事件。私聊没有观察窗,每收到一条私聊消息都会直接唤醒目标会话。
101
101
 
102
- - 唤醒提示里会带发送者昵称、QQ 号、`message_id` 和文字;如果消息里有图片,也会把图片一起给模型看(GIF 会拆成首/中/尾最多 3 帧,和群聊记录的处理一致)。
102
+ - 唤醒提示里会带发送者昵称、QQ 号、`message_id` 和文字;如果消息里有图片,也会把图片一起给模型看(有 sharp 时 GIF 会拆成首/中/尾最多 3 帧;没有 sharp 时交给 DSH 显示首帧,和群聊记录的处理一致)。
103
103
  - 模型用 `qq_get_private_history` 看和对方的最近记录,用 `qq_send_private_message` 回文字,用 `qq_send_face` 回表情。
104
104
  - `qq_list_friends` 列好友,可以按昵称、备注或 QQ 号搜;`privateUsers` 非空时只列白名单里的好友。
105
105
  - `qq_get_private_history` 的消息带 `#message_id`,可以传给 `qq_send_private_message` / `qq_send_face` 的 `reply_to` 做引用回复。
@@ -128,7 +128,7 @@ dsh plugin --profile <profile> add dsh-qq-tools
128
128
  需要固定版本时:
129
129
 
130
130
  ```sh
131
- dsh plugin --profile <profile> add dsh-qq-tools@0.8.1
131
+ dsh plugin --profile <profile> add dsh-qq-tools@0.8.2
132
132
  ```
133
133
 
134
134
  ### 本地开发 / 自己改代码
@@ -141,7 +141,7 @@ dsh plugin --profile <profile> add link:/path/to/dsh-qq-tools
141
141
 
142
142
  ```sh
143
143
  pnpm pack
144
- dsh plugin --profile <profile> add ./dsh-qq-tools-0.8.1.tgz
144
+ dsh plugin --profile <profile> add ./dsh-qq-tools-0.8.2.tgz
145
145
  ```
146
146
 
147
147
  `dsh plugin` 会在安装后自动把这个包追加进 profile 的 `dsh.profile.bundles`,不用手改 `package.json`。卸载用:
@@ -155,8 +155,8 @@ dsh plugin --profile <profile> remove dsh-qq-tools
155
155
  - DSH 0.2.x(cordis 插件体系)
156
156
  - Node.js 22+(内置 `fetch` / `WebSocket`)
157
157
  - SnowLuma 提供 OneBot 11 HTTP 接口和 WebSocket 事件(收藏表情需要 SnowLuma;系统表情 NapCat 也能发)
158
- - 可选:`sharp`(GIF 帧拆分、超过 4MiB 的图片缩放、其他格式转码)。会自动使用插件自己装的、`SHARP_PATH` 指定的,或 DSH 宿主自带的 sharp;都找不到时 GIF 按静图处理,大图直接交给 DSH 归一化(DSH 默认单张源图上限 20MiB)。
158
+ - 可选:`sharp`(GIF 拆帧、尺寸被拒时的兜底重编码)。会从 `SHARP_PATH`、插件自己的 `node_modules`、DSH 宿主 runtimeDir(桌面版通过 `process.argv[2]` 传入)等位置自动查找;找不到时 GIF 交给 DSH 显示首帧,大图仍由 DSH 自动归一化(默认单张源图上限 20MiB),图片不会因此丢失。
159
159
 
160
160
  ---
161
161
 
162
- 版本:0.8.1
162
+ 版本:0.8.2
package/lib/shared.js CHANGED
@@ -34,7 +34,7 @@
34
34
  //
35
35
  // 工具本身的开关在 DSH 插件页的组件列表里:关掉某个组件,它就不再注册对应工具。
36
36
 
37
- import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } from 'node:fs'
37
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
38
38
  import { join } from 'node:path'
39
39
  import { homedir } from 'node:os'
40
40
  import { randomUUID } from 'node:crypto'
@@ -47,34 +47,82 @@ const DIAG_PATH = join(DSH_HOME, 'qq-tools-diag.log')
47
47
 
48
48
 
49
49
  const requireFromHere = createRequire(import.meta.url)
50
+ let sharpSource = ''
50
51
 
51
- // sharp 是可选的,按这个顺序找:SHARP_PATH → 插件自己装的 → 宿主进程能解析到的。
52
- // DSH 桌面版把 sharp 打在 resources/app.asar/dsh/node_modules 下,CLI 版跟着
53
- // @deepseek-ai/dsh 的依赖走,所以从宿主的启动入口解析最稳,不用写死安装路径。
52
+ function sharpFromDir(dir) {
53
+ try {
54
+ const s = createRequire(join(String(dir), 'noop.js'))('sharp')
55
+ return s || null
56
+ } catch (error) {
57
+ return null
58
+ }
59
+ }
60
+
61
+ function sharpFromCache() {
62
+ try {
63
+ const cache = requireFromHere.cache || {}
64
+ for (const key of Object.keys(cache)) {
65
+ if (!/[\\/]node_modules[\\/]sharp[\\/]/.test(key)) continue
66
+ const mod = cache[key]
67
+ if (mod && mod.exports) return mod.exports
68
+ }
69
+ } catch (error) {}
70
+ return null
71
+ }
72
+
73
+ // sharp 是可选的,按顺序找:SHARP_PATH → 插件自己装的 → 宿主 runtime → 常见宿主目录。
74
+ // 关键的一条是 process.argv[2]:DSH 桌面版的 dsh-desktop-host 用 argv[2] 接 runtimeDir,
75
+ // 插件和宿主在同一个进程里,所以 runtimeDir/node_modules/sharp 就是宿主实际加载的那份。
76
+ // 不写死安装路径;找不到时核心图片逻辑照常工作,只是不拆 GIF 帧,
77
+ // 尺寸被宿主拒绝时也没法兜底重编码。
54
78
  function loadSharp() {
55
79
  const probes = []
56
- if (process.env.SHARP_PATH) probes.push(() => requireFromHere(process.env.SHARP_PATH))
57
- probes.push(() => requireFromHere('sharp'))
58
- if (process.argv[1]) probes.push(() => createRequire(process.argv[1])('sharp'))
59
- if (process.resourcesPath) {
60
- probes.push(() => createRequire(join(process.resourcesPath, 'app.asar', 'dsh', 'package.json'))('sharp'))
80
+ const add = (label, fn) => probes.push({ label, fn })
81
+
82
+ if (process.env.SHARP_PATH) add('SHARP_PATH', () => requireFromHere(process.env.SHARP_PATH))
83
+ add('插件 node_modules', () => requireFromHere('sharp'))
84
+ if (process.argv[1]) add('宿主入口', () => createRequire(process.argv[1])('sharp'))
85
+ if (process.argv[2]) add('宿主 runtimeDir', () => sharpFromDir(process.argv[2]))
86
+
87
+ // 不同启动方式下 runtimeDir 的位置可能变;把 argv 里看起来像目录的路径都试一遍。
88
+ for (let i = 1; i < process.argv.length; i++) {
89
+ const raw = String(process.argv[i] || '')
90
+ if (!raw) continue
91
+ try {
92
+ if (existsSync(raw) && statSync(raw).isDirectory()) add('argv 目录 ' + raw, () => sharpFromDir(raw))
93
+ } catch (error) {}
61
94
  }
62
- for (const p of [
63
- '/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/sharp',
64
- '/usr/lib/node_modules/@deepseek-ai/dsh/node_modules/sharp',
65
- ]) {
66
- probes.push(() => requireFromHere(p))
95
+
96
+ if (process.cwd()) add('当前目录', () => sharpFromDir(process.cwd()))
97
+ if (process.resourcesPath) {
98
+ for (const root of [
99
+ join(process.resourcesPath, 'app.asar', 'dsh'),
100
+ join(process.resourcesPath, 'app.asar.unpacked', 'dsh'),
101
+ join(process.resourcesPath, 'app.asar'),
102
+ join(process.resourcesPath, 'app.asar.unpacked'),
103
+ join(process.resourcesPath, 'runtime'),
104
+ ]) {
105
+ add('resources ' + root, () => sharpFromDir(root))
106
+ }
67
107
  }
108
+ add('已加载模块缓存', sharpFromCache)
109
+
68
110
  for (const probe of probes) {
69
111
  try {
70
- const s = probe()
71
- if (s) return s
112
+ const s = probe.fn()
113
+ if (s) {
114
+ sharpSource = probe.label
115
+ return s
116
+ }
72
117
  } catch (error) {}
73
118
  }
74
119
  return null
75
120
  }
76
121
 
77
122
  const sharp = loadSharp()
123
+ diag('sharp:' + (sharp
124
+ ? '已加载(' + sharpSource + ')'
125
+ : '没找到;GIF 只取首帧,大图交给 DSH 归一化,图片不会因此丢失'))
78
126
  let gifWarned = false
79
127
 
80
128
  const DEFAULT_GROUPS = []
@@ -469,8 +517,9 @@ export function makeListTool() {
469
517
 
470
518
  // ---------- 图片类型与尺寸 ----------
471
519
  // DSH 附件层按真实字节复核 mediaType(IMAGE_TYPE_MISMATCH),文件名和 QQ 的
472
- // animated 提示都可能撒谎,所以只信字节。DSH 默认单张源图上限 20MiB,
473
- // 归一化目标 4MiB;源图超过 4MiB 时本地先缩一下,避免白白走一遍宿主重编码。
520
+ // animated 提示都可能撒谎,所以只信字节。大图不依赖 sharp:DSH 默认单张源图
521
+ // 上限 20MiB、归一化目标 4MiB,直接交给它处理;sharp 只用于 GIF 拆帧和
522
+ // admit 被尺寸拒绝后的兜底重编码。
474
523
  const ADMIT_IMAGE_LIMIT = 6
475
524
  const IMAGE_SOFT_LIMIT = 4 * 1024 * 1024
476
525
  const IMAGE_MEDIA_TYPES = {
@@ -725,14 +774,8 @@ export function admitImagesFor(ctx) {
725
774
  diag('GIF 帧拆分不可用:没有加载到 sharp,动图会按静图处理')
726
775
  }
727
776
  }
728
- // 以前这里直接丢弃 >5MiB 的图;DSH 实际能收 20MiB 并自动归一化,所以改成先缩放。
729
- if (bytes.length > IMAGE_SOFT_LIMIT) {
730
- const shrunk = await shrinkImage(bytes)
731
- if (shrunk) {
732
- bytes = shrunk.bytes
733
- mediaType = shrunk.mediaType
734
- }
735
- }
777
+ // 大图不在这里预缩放:DSH 自己会把源图归一化到 4MiB,20MiB 以内都能收。
778
+ // 只有 admit 因尺寸被拒时才在 retryImageBlock 里用 sharp 兜底重编码。
736
779
  prepared.push({
737
780
  caption: String(x.caption || ''),
738
781
  block: { type: 'image', data: bytes.toString('base64'), mediaType, name },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-qq-tools",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "给大肥鱼会话用的 QQ 群/私聊工具:列群、看群记录、发群消息、发表情、收藏表情(SnowLuma 收藏夹)、好友/私聊记录、发私聊消息、私聊唤醒、群成员、戳一戳感知/戳回去、挂载会话选择。8 个工具加 1 个设置组件,可在插件页逐个开关。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",