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.
- package/README.md +5 -5
- package/lib/shared.js +69 -26
- 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`
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
if (process.
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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)
|
|
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
|
|
473
|
-
//
|
|
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
|
-
//
|
|
729
|
-
|
|
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