dsh-qq-tools 0.7.0 → 0.8.0
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 +34 -11
- package/cordis.patch.yml +7 -1
- package/lib/client.js +328 -0
- package/lib/index.js +13 -7
- package/lib/shared.js +173 -31
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
给 DSH(DeepSeek Harness)里的大肥鱼会话用的 QQ 群/私聊工具,走 OneBot 11 HTTP + WebSocket。系统表情在 SnowLuma / NapCat 上都能发;收藏表情用 SnowLuma 的自定义表情扩展接口,和 QQ 客户端里的收藏夹是同一份数据。
|
|
4
4
|
|
|
5
|
-
8 个工具各自是一个独立的 cordis
|
|
5
|
+
8 个工具各自是一个独立的 cordis 组件,外加一个负责选择挂载会话的设置组件,在 DSH 插件页的「包含的组件」里可以逐个开关:
|
|
6
6
|
|
|
7
7
|
| 组件 | 工具 | 说明 |
|
|
8
8
|
| --- | --- | --- |
|
|
@@ -14,15 +14,18 @@
|
|
|
14
14
|
| 收藏表情 | `qq_list_faces` / `qq_favorite_face` | 看/管理 QQ 账号收藏夹(SnowLuma):收藏图片、取消收藏、改备注、移到最前 |
|
|
15
15
|
| 私聊 | `qq_list_friends` / `qq_get_private_history` / `qq_send_private_message` | 好友列表、私聊记录、发私聊消息;同时负责私聊消息唤醒 |
|
|
16
16
|
| 戳一戳 | `qq_send_poke` | 戳人;同时负责接收戳一戳事件 |
|
|
17
|
+
| 挂载会话 | — | 插件页里的「配置」页面:下拉选择 QQ 消息唤醒哪个 DSH 会话 |
|
|
17
18
|
|
|
18
|
-
关掉「发群消息」会同时停掉 @
|
|
19
|
+
关掉「发群消息」会同时停掉 @ 提醒轮询,关掉「私聊」会同时停掉私聊消息唤醒,关掉「戳一戳」会同时停掉戳一戳通知;三个只读工具互不影响。关掉「挂载会话」只会让配置页面不可用,已经写进配置的 `targetSession` 仍然生效。
|
|
19
20
|
|
|
20
21
|
## 目录结构
|
|
21
22
|
|
|
22
23
|
```
|
|
23
|
-
cordis.patch.yml
|
|
24
|
+
cordis.patch.yml 9 个组件行
|
|
24
25
|
lib/shared.js 共享核心:配置、OneBot、挂载、@ 轮询、私聊唤醒、戳一戳、表情、观察窗
|
|
25
|
-
lib/
|
|
26
|
+
lib/client.js 浏览器半侧:插件页里的挂载会话下拉框
|
|
27
|
+
lib/index.js 包根 carrier(承载浏览器半侧和挂载会话接口),也是旧的全量入口
|
|
28
|
+
locale/ 包根行在插件页显示的标题和说明
|
|
26
29
|
tools/qq-list-groups/ 组件入口 + locale
|
|
27
30
|
tools/qq-get-group-history/
|
|
28
31
|
tools/qq-get-group-members/
|
|
@@ -33,7 +36,19 @@ tools/qq-private-chat/
|
|
|
33
36
|
tools/qq-send-poke/
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
「挂载会话」用的是包根行(模块名 `dsh-qq-tools`,所以插件卡片名仍是 `dsh-qq-tools`):DSH 只从包根发现 `dsh.client`,子路径行不会扫描;`config.carrier: true` 让它只承载设置接口,不会重复挂一遍工具。
|
|
40
|
+
|
|
41
|
+
每个 `tools/<组件>/index.js` 只做一件事:把自己的工具挂到目标会话。`locale/{en,zh}.json` 提供插件页显示的标题和说明;左侧栏入口和插件行配置页在 `lib/client.js` 里注册。
|
|
42
|
+
|
|
43
|
+
## 选择挂载会话
|
|
44
|
+
|
|
45
|
+
左侧栏的「挂载会话」入口(也可以在「插件 → dsh-qq-tools → 挂载会话 → 配置」里打开)会列出当前 DSH 里的会话(标题 + 短 ID + 运行状态)。选中后保存,配置写回 `~/.dsh/qq-tools-config.json` 的 `targetSession`,各组件在 3 秒内重新挂到这个会话。
|
|
46
|
+
|
|
47
|
+
- 归档会话和子会话不会出现在列表里;如果当前目标后来被归档,列表会把它标成「当前目标(已归档)」。
|
|
48
|
+
- 工具只注册在选中的会话上,其他会话看不到 QQ 工具,也不会被 QQ 消息唤醒。
|
|
49
|
+
- 选「不挂载」或留空会卸下所有工具并停止唤醒。
|
|
50
|
+
- 保存时不校验会话是否存活:会话暂时关掉不会把配置清掉,等它重新打开后会自动挂上。
|
|
51
|
+
- 这个页面依赖「挂载会话」组件;关掉该组件后接口不可用,但已保存的 `targetSession` 仍然生效,也可以直接改配置文件。
|
|
37
52
|
|
|
38
53
|
## 配置
|
|
39
54
|
|
|
@@ -59,7 +74,7 @@ tools/qq-send-poke/
|
|
|
59
74
|
```
|
|
60
75
|
|
|
61
76
|
- `napCat` 是 OneBot 11 HTTP 地址(SnowLuma 默认 `http://127.0.0.1:3000`,NapCat 也一样);`wsUrl` 是 WebSocket 事件地址(默认 `ws://127.0.0.1:3001`)。
|
|
62
|
-
- `targetSession` 是目标会话 ID
|
|
77
|
+
- `targetSession` 是目标会话 ID;可以在插件页「挂载会话 → 配置」里下拉选择,也可以直接改这个文件;留空时工具不会挂到任何会话。
|
|
63
78
|
- `groups` 是允许看/发的群;空数组表示不限制。
|
|
64
79
|
- `mentionEnabled` / `pokeEnabled` / `privateEnabled` 分别是 @ 轮询、戳一戳事件、私聊唤醒的总闸,对应组件关掉时它们也不会运行。
|
|
65
80
|
- `privateUsers` 是允许唤醒和私聊工具的 QQ 白名单;空数组表示不限制。非空时,不在列表里的私聊消息不会唤醒,`qq_list_friends` 也不会列出他们。
|
|
@@ -84,7 +99,7 @@ tools/qq-send-poke/
|
|
|
84
99
|
|
|
85
100
|
私聊和群聊在程序里是分开的两条路径:群聊靠轮询 @,私聊靠 WebSocket 的 `message_type=private` 事件。私聊没有观察窗,每收到一条私聊消息都会直接唤醒目标会话。
|
|
86
101
|
|
|
87
|
-
- 唤醒提示里会带发送者昵称、QQ 号、`message_id`
|
|
102
|
+
- 唤醒提示里会带发送者昵称、QQ 号、`message_id` 和文字;如果消息里有图片,也会把图片一起给模型看(GIF 会拆成首/中/尾最多 3 帧,和群聊记录的处理一致)。
|
|
88
103
|
- 模型用 `qq_get_private_history` 看和对方的最近记录,用 `qq_send_private_message` 回文字,用 `qq_send_face` 回表情。
|
|
89
104
|
- `qq_list_friends` 列好友,可以按昵称、备注或 QQ 号搜;`privateUsers` 非空时只列白名单里的好友。
|
|
90
105
|
- `qq_get_private_history` 的消息带 `#message_id`,可以传给 `qq_send_private_message` / `qq_send_face` 的 `reply_to` 做引用回复。
|
|
@@ -102,12 +117,20 @@ dsh plugin --profile <profile> add github:cyhddyx/dsh-qq-tools
|
|
|
102
117
|
|
|
103
118
|
本插件是纯 JavaScript,没有构建步骤,所以不需要 `prepare` 脚本,也不需要给 pnpm 加 `allowBuilds` 授权。安装完成后重启 DSH,插件页里应该能看到 8 个组件,每个带开关。
|
|
104
119
|
|
|
105
|
-
### 从 npm
|
|
120
|
+
### 从 npm 安装(推荐)
|
|
121
|
+
|
|
122
|
+
已发布到 npm,装最新版:
|
|
106
123
|
|
|
107
124
|
```sh
|
|
108
125
|
dsh plugin --profile <profile> add dsh-qq-tools
|
|
109
126
|
```
|
|
110
127
|
|
|
128
|
+
需要固定版本时:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
dsh plugin --profile <profile> add dsh-qq-tools@0.8.0
|
|
132
|
+
```
|
|
133
|
+
|
|
111
134
|
### 本地开发 / 自己改代码
|
|
112
135
|
|
|
113
136
|
```sh
|
|
@@ -118,7 +141,7 @@ dsh plugin --profile <profile> add link:/path/to/dsh-qq-tools
|
|
|
118
141
|
|
|
119
142
|
```sh
|
|
120
143
|
pnpm pack
|
|
121
|
-
dsh plugin --profile <profile> add ./dsh-qq-tools-0.
|
|
144
|
+
dsh plugin --profile <profile> add ./dsh-qq-tools-0.8.0.tgz
|
|
122
145
|
```
|
|
123
146
|
|
|
124
147
|
`dsh plugin` 会在安装后自动把这个包追加进 profile 的 `dsh.profile.bundles`,不用手改 `package.json`。卸载用:
|
|
@@ -132,8 +155,8 @@ dsh plugin --profile <profile> remove dsh-qq-tools
|
|
|
132
155
|
- DSH 0.2.x(cordis 插件体系)
|
|
133
156
|
- Node.js 22+(内置 `fetch` / `WebSocket`)
|
|
134
157
|
- SnowLuma 提供 OneBot 11 HTTP 接口和 WebSocket 事件(收藏表情需要 SnowLuma;系统表情 NapCat 也能发)
|
|
135
|
-
- 可选:`sharp`(GIF
|
|
158
|
+
- 可选:`sharp`(GIF 帧拆分)。会自动使用插件自己装的、`SHARP_PATH` 指定的,或 DSH 宿主自带的 sharp;都找不到时 GIF 按静图处理
|
|
136
159
|
|
|
137
160
|
---
|
|
138
161
|
|
|
139
|
-
版本:0.
|
|
162
|
+
版本:0.8.0
|
package/cordis.patch.yml
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
# DSH 大肥鱼 QQ 群/私聊工具:
|
|
1
|
+
# DSH 大肥鱼 QQ 群/私聊工具:9 个组件,插件页里可以逐个开关
|
|
2
2
|
# · 列出 QQ 群 / 看群聊天记录 / 看群成员 —— 只读工具
|
|
3
3
|
# · 发群消息 —— 同时负责 @ 提醒与群后续轮询
|
|
4
4
|
# · 发表情 —— 在群里或私聊里发系统表情或 QQ 收藏夹里的自定义表情
|
|
5
5
|
# · 收藏表情 —— QQ 收藏夹(SnowLuma):qq_list_faces + qq_favorite_face
|
|
6
6
|
# · 私聊 —— 好友列表 / 私聊记录 / 发私聊消息,同时负责私聊消息唤醒
|
|
7
7
|
# · 戳一戳 —— 同时负责接收戳一戳事件
|
|
8
|
+
# · 挂载会话 —— 插件页里的「配置」页面,选择 QQ 消息唤醒哪个 DSH 会话
|
|
8
9
|
- insert:
|
|
9
10
|
- id: qq-tools-list-groups
|
|
10
11
|
name: 'dsh-qq-tools/tools/qq-list-groups'
|
|
@@ -22,3 +23,8 @@
|
|
|
22
23
|
name: 'dsh-qq-tools/tools/qq-private-chat'
|
|
23
24
|
- id: qq-tools-send-poke
|
|
24
25
|
name: 'dsh-qq-tools/tools/qq-send-poke'
|
|
26
|
+
# 包根行:DSH 只从包根发现 dsh.client,浏览器半侧和挂载会话接口都挂在这里。
|
|
27
|
+
- id: qq-tools-root
|
|
28
|
+
name: 'dsh-qq-tools'
|
|
29
|
+
config:
|
|
30
|
+
carrier: true
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// dsh-qq-tools 的浏览器半侧:左侧栏「挂载会话」入口 + 插件行配置页。
|
|
2
|
+
// 会话列表来自 slot 自带的 useSessions,归档集合来自 useWorkspaces;
|
|
3
|
+
// 读写走宿主半侧的 /api/dsh-qq-tools/target-session,最终仍然落在
|
|
4
|
+
// ~/.dsh/qq-tools-config.json 的 targetSession 字段上。
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'dsh-qq-tools',
|
|
7
|
+
factory: (require) => {
|
|
8
|
+
var module = { exports: {} }
|
|
9
|
+
var exports = module.exports
|
|
10
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
11
|
+
|
|
12
|
+
const react = require('react')
|
|
13
|
+
const h = react.createElement
|
|
14
|
+
|
|
15
|
+
const NS = 'qq-tools'
|
|
16
|
+
const API = '/api/dsh-qq-tools/target-session'
|
|
17
|
+
const SLOT_KEY = 'dsh-qq-tools#qq-tools-root'
|
|
18
|
+
const PANEL_ID = 'qq-tools-target-session'
|
|
19
|
+
const PANEL_ORDER = 50
|
|
20
|
+
const EMPTY_IDS = []
|
|
21
|
+
|
|
22
|
+
const zh = {
|
|
23
|
+
summary: 'QQ 消息挂载会话',
|
|
24
|
+
sidebarLabel: '挂载会话',
|
|
25
|
+
panelTitle: '挂载会话',
|
|
26
|
+
description: '只有这里选中的 DSH 会话会收到 QQ 群 @、群后续、私聊和戳一戳;QQ 工具也只挂在这个会话上。保存后约 3 秒生效。',
|
|
27
|
+
none: '不挂载(关闭 QQ 唤醒)',
|
|
28
|
+
untitled: '未命名会话',
|
|
29
|
+
running: '(运行中)',
|
|
30
|
+
missing: '当前目标(会话未运行)',
|
|
31
|
+
archived: '当前目标(已归档)',
|
|
32
|
+
refresh: '刷新',
|
|
33
|
+
save: '保存',
|
|
34
|
+
saving: '保存中…',
|
|
35
|
+
saved: '已保存,约 3 秒内生效',
|
|
36
|
+
loading: '读取中…',
|
|
37
|
+
failed: '操作失败:',
|
|
38
|
+
current: '当前挂载:',
|
|
39
|
+
empty: '当前没有挂载任何会话。',
|
|
40
|
+
unavailable: '接口不可用。请先在插件页启用「挂载会话」组件。',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const en = {
|
|
44
|
+
summary: 'QQ target session',
|
|
45
|
+
sidebarLabel: 'Target session',
|
|
46
|
+
panelTitle: 'Target session',
|
|
47
|
+
description: 'Only the selected DSH session receives QQ mentions, follow-ups, private messages and pokes; QQ tools are mounted there too. Changes apply within about 3 seconds.',
|
|
48
|
+
none: 'Do not mount (disable QQ wake-ups)',
|
|
49
|
+
untitled: 'Untitled session',
|
|
50
|
+
running: ' (running)',
|
|
51
|
+
missing: 'Current target (not running)',
|
|
52
|
+
archived: 'Current target (archived)',
|
|
53
|
+
refresh: 'Refresh',
|
|
54
|
+
save: 'Save',
|
|
55
|
+
saving: 'Saving…',
|
|
56
|
+
saved: 'Saved; applies within about 3 seconds',
|
|
57
|
+
loading: 'Loading…',
|
|
58
|
+
failed: 'Request failed: ',
|
|
59
|
+
current: 'Mounted to: ',
|
|
60
|
+
empty: 'No session is mounted.',
|
|
61
|
+
unavailable: 'Endpoint unavailable. Enable the "Target session" component on the plugin page first.',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const inject = ['slots', 'locale']
|
|
65
|
+
|
|
66
|
+
function shortId(id) {
|
|
67
|
+
const s = String(id || '')
|
|
68
|
+
return s.length > 12 ? s.slice(0, 8) + '…' : s
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 宿主在插件行未启用时返回 404/HTML;先把文本读出来再解析,避免 r.json() 直接抛。
|
|
72
|
+
function readJsonResponse(response) {
|
|
73
|
+
return response.text().then((text) => {
|
|
74
|
+
let body = {}
|
|
75
|
+
try { body = text ? JSON.parse(text) : {} } catch (error) {}
|
|
76
|
+
return { ok: response.ok, body }
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function TargetSessionCard(props) {
|
|
81
|
+
const t = props.t
|
|
82
|
+
const list = props.useSessions((state) => state)
|
|
83
|
+
const useWorkspaces = props.useWorkspaces || (() => EMPTY_IDS)
|
|
84
|
+
const archivedSessionIds = useWorkspaces((state) => (state && state.archivedSessionIds) || EMPTY_IDS) || EMPTY_IDS
|
|
85
|
+
const archived = react.useMemo(() => new Set(archivedSessionIds), [archivedSessionIds])
|
|
86
|
+
const [current, setCurrent] = react.useState('')
|
|
87
|
+
const [selected, setSelected] = react.useState('')
|
|
88
|
+
const [liveIds, setLiveIds] = react.useState([])
|
|
89
|
+
const [phase, setPhase] = react.useState('loading')
|
|
90
|
+
const [detail, setDetail] = react.useState('')
|
|
91
|
+
const [loaded, setLoaded] = react.useState(false)
|
|
92
|
+
|
|
93
|
+
const load = react.useCallback(() => {
|
|
94
|
+
setPhase('loading')
|
|
95
|
+
setDetail('')
|
|
96
|
+
fetch(API, { headers: { accept: 'application/json' } })
|
|
97
|
+
.then(readJsonResponse)
|
|
98
|
+
.then(({ ok, body }) => {
|
|
99
|
+
if (!ok || !body || body.ok !== true) {
|
|
100
|
+
throw new Error((body && body.error) || t('unavailable'))
|
|
101
|
+
}
|
|
102
|
+
const value = typeof body.targetSession === 'string' ? body.targetSession : ''
|
|
103
|
+
setCurrent(value)
|
|
104
|
+
setSelected(value)
|
|
105
|
+
setLiveIds(Array.isArray(body.liveSessionIds) ? body.liveSessionIds : [])
|
|
106
|
+
setLoaded(true)
|
|
107
|
+
setPhase('ready')
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
setDetail(String((error && error.message) || error))
|
|
111
|
+
setPhase('error')
|
|
112
|
+
})
|
|
113
|
+
}, [t])
|
|
114
|
+
|
|
115
|
+
react.useEffect(() => {
|
|
116
|
+
load()
|
|
117
|
+
}, [load])
|
|
118
|
+
|
|
119
|
+
const save = react.useCallback(() => {
|
|
120
|
+
setPhase('saving')
|
|
121
|
+
setDetail('')
|
|
122
|
+
fetch(API, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
125
|
+
body: JSON.stringify({ targetSession: selected }),
|
|
126
|
+
})
|
|
127
|
+
.then(readJsonResponse)
|
|
128
|
+
.then(({ ok, body }) => {
|
|
129
|
+
if (!ok || !body || body.ok !== true) {
|
|
130
|
+
throw new Error((body && body.error) || 'save failed')
|
|
131
|
+
}
|
|
132
|
+
const value = typeof body.targetSession === 'string' ? body.targetSession : ''
|
|
133
|
+
setCurrent(value)
|
|
134
|
+
setSelected(value)
|
|
135
|
+
setPhase('saved')
|
|
136
|
+
})
|
|
137
|
+
.catch((error) => {
|
|
138
|
+
setDetail(String((error && error.message) || error))
|
|
139
|
+
setPhase('error')
|
|
140
|
+
})
|
|
141
|
+
}, [selected])
|
|
142
|
+
|
|
143
|
+
const sessions = react.useMemo(() => {
|
|
144
|
+
const ids = Array.isArray(list && list.ids) ? list.ids : []
|
|
145
|
+
const byId = (list && list.byId) || {}
|
|
146
|
+
return ids.map((id) => byId[id]).filter(Boolean)
|
|
147
|
+
}, [list])
|
|
148
|
+
|
|
149
|
+
const options = [h('option', { key: '__none', value: '' }, t('none'))]
|
|
150
|
+
let selectedInList = false
|
|
151
|
+
for (const session of sessions) {
|
|
152
|
+
if (!session || !session.id) continue
|
|
153
|
+
if (session.origin === 'subagent') continue
|
|
154
|
+
if (archived.has(session.id)) continue
|
|
155
|
+
if (session.id === selected) selectedInList = true
|
|
156
|
+
const title = String(session.title || '').trim() || t('untitled')
|
|
157
|
+
const running = session.running === true || liveIds.indexOf(session.id) >= 0
|
|
158
|
+
options.push(h(
|
|
159
|
+
'option',
|
|
160
|
+
{ key: session.id, value: session.id },
|
|
161
|
+
title + ' · ' + shortId(session.id) + (running ? t('running') : ''),
|
|
162
|
+
))
|
|
163
|
+
}
|
|
164
|
+
if (selected && !selectedInList) {
|
|
165
|
+
options.push(h(
|
|
166
|
+
'option',
|
|
167
|
+
{ key: '__current', value: selected },
|
|
168
|
+
(archived.has(selected) ? t('archived') : t('missing')) + ' · ' + shortId(selected),
|
|
169
|
+
))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (props.view === 'summary') {
|
|
173
|
+
return h('span', null, current ? t('current') + shortId(current) : t('empty'))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let statusText = ''
|
|
177
|
+
let statusColor = 'var(--dsw-alias-label-tertiary)'
|
|
178
|
+
if (phase === 'loading') statusText = t('loading')
|
|
179
|
+
else if (phase === 'saving') statusText = t('saving')
|
|
180
|
+
else if (phase === 'saved') {
|
|
181
|
+
statusText = t('saved')
|
|
182
|
+
statusColor = 'var(--dsw-alias-state-success-primary, #2f9e44)'
|
|
183
|
+
} else if (phase === 'error') {
|
|
184
|
+
statusText = t('failed') + (detail || t('unavailable'))
|
|
185
|
+
statusColor = 'var(--dsw-alias-state-error-primary, #d64545)'
|
|
186
|
+
} else {
|
|
187
|
+
statusText = current ? t('current') + shortId(current) : t('empty')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const disabled = !loaded || phase === 'saving'
|
|
191
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } }, [
|
|
192
|
+
h('p', {
|
|
193
|
+
key: 'desc',
|
|
194
|
+
style: { margin: 0, fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-secondary)' },
|
|
195
|
+
}, t('description')),
|
|
196
|
+
h('div', { key: 'row', style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } }, [
|
|
197
|
+
h('select', {
|
|
198
|
+
key: 'select',
|
|
199
|
+
value: selected,
|
|
200
|
+
disabled,
|
|
201
|
+
onChange: (event) => {
|
|
202
|
+
setSelected(event.target.value)
|
|
203
|
+
if (phase === 'saved' || phase === 'error') setPhase('ready')
|
|
204
|
+
},
|
|
205
|
+
style: {
|
|
206
|
+
minWidth: 280,
|
|
207
|
+
maxWidth: '100%',
|
|
208
|
+
height: 36,
|
|
209
|
+
padding: '0 10px',
|
|
210
|
+
borderRadius: 8,
|
|
211
|
+
border: '0.5px solid var(--dsw-alias-border-l4)',
|
|
212
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
213
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
214
|
+
font: 'inherit',
|
|
215
|
+
fontSize: 13,
|
|
216
|
+
outline: 'none',
|
|
217
|
+
},
|
|
218
|
+
}, options),
|
|
219
|
+
h('button', {
|
|
220
|
+
key: 'refresh',
|
|
221
|
+
type: 'button',
|
|
222
|
+
disabled: phase === 'loading' || phase === 'saving',
|
|
223
|
+
onClick: load,
|
|
224
|
+
style: {
|
|
225
|
+
height: 36,
|
|
226
|
+
padding: '0 14px',
|
|
227
|
+
borderRadius: 8,
|
|
228
|
+
border: '0.5px solid var(--dsw-alias-border-l4)',
|
|
229
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
230
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
231
|
+
font: 'inherit',
|
|
232
|
+
fontSize: 13,
|
|
233
|
+
cursor: 'pointer',
|
|
234
|
+
},
|
|
235
|
+
}, t('refresh')),
|
|
236
|
+
h('button', {
|
|
237
|
+
key: 'save',
|
|
238
|
+
type: 'button',
|
|
239
|
+
disabled,
|
|
240
|
+
onClick: save,
|
|
241
|
+
style: {
|
|
242
|
+
height: 36,
|
|
243
|
+
padding: '0 16px',
|
|
244
|
+
borderRadius: 8,
|
|
245
|
+
border: 'none',
|
|
246
|
+
background: 'var(--dsw-alias-state-business-primary, #4d6bfe)',
|
|
247
|
+
color: '#fff',
|
|
248
|
+
font: 'inherit',
|
|
249
|
+
fontSize: 13,
|
|
250
|
+
cursor: disabled ? 'default' : 'pointer',
|
|
251
|
+
opacity: disabled ? 0.6 : 1,
|
|
252
|
+
},
|
|
253
|
+
}, phase === 'saving' ? t('saving') : t('save')),
|
|
254
|
+
]),
|
|
255
|
+
h('p', {
|
|
256
|
+
key: 'status',
|
|
257
|
+
style: { margin: 0, fontSize: 12, lineHeight: '18px', color: statusColor },
|
|
258
|
+
}, statusText),
|
|
259
|
+
])
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function TargetSessionIcon(props) {
|
|
263
|
+
const size = Number(props && props.size) || 16
|
|
264
|
+
return h('svg', {
|
|
265
|
+
viewBox: '0 0 16 16',
|
|
266
|
+
width: size,
|
|
267
|
+
height: size,
|
|
268
|
+
fill: 'none',
|
|
269
|
+
stroke: 'currentColor',
|
|
270
|
+
strokeWidth: 1.5,
|
|
271
|
+
strokeLinecap: 'round',
|
|
272
|
+
strokeLinejoin: 'round',
|
|
273
|
+
'aria-hidden': 'true',
|
|
274
|
+
'data-dsh-panel-entry': PANEL_ID,
|
|
275
|
+
}, [
|
|
276
|
+
h('path', {
|
|
277
|
+
key: 'bubble',
|
|
278
|
+
d: 'M2.5 2.75h9a1.5 1.5 0 0 1 1.5 1.5v3.5a1.5 1.5 0 0 1-1.5 1.5H6.75L4 12.25V9.25H2.5a1.5 1.5 0 0 1-1.5-1.5v-3.5a1.5 1.5 0 0 1 1.5-1.5Z',
|
|
279
|
+
}),
|
|
280
|
+
h('path', { key: 'line1', d: 'M4.25 5.5h5.5' }),
|
|
281
|
+
h('path', { key: 'line2', d: 'M4.25 7.75h3.25' }),
|
|
282
|
+
])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function TargetSessionPage(props) {
|
|
286
|
+
const t = props.t
|
|
287
|
+
return h('div', {
|
|
288
|
+
style: {
|
|
289
|
+
height: '100%',
|
|
290
|
+
boxSizing: 'border-box',
|
|
291
|
+
overflow: 'auto',
|
|
292
|
+
padding: '28px 36px 48px',
|
|
293
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
294
|
+
},
|
|
295
|
+
}, [
|
|
296
|
+
h('h1', {
|
|
297
|
+
key: 'title',
|
|
298
|
+
style: { margin: '0 0 18px', fontSize: 20, fontWeight: 600, lineHeight: '28px' },
|
|
299
|
+
}, t('panelTitle')),
|
|
300
|
+
h(TargetSessionCard, { key: 'card', ...props, view: 'page' }),
|
|
301
|
+
])
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function apply(ctx) {
|
|
305
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'qq-tools: dictionaries')
|
|
306
|
+
const t = ctx.locale.bind(NS)
|
|
307
|
+
ctx.effect(() => ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
308
|
+
name: 'sidebar.panellist',
|
|
309
|
+
id: PANEL_ID,
|
|
310
|
+
order: PANEL_ORDER,
|
|
311
|
+
label: () => t('sidebarLabel'),
|
|
312
|
+
}, TargetSessionIcon)), 'qq-tools: sidebar entry')
|
|
313
|
+
ctx.effect(() => ctx.slots.inject('main', () => ctx.slots.register({
|
|
314
|
+
name: 'main',
|
|
315
|
+
key: PANEL_ID,
|
|
316
|
+
}, (props) => h(TargetSessionPage, { ...props, t }))), 'qq-tools: main panel')
|
|
317
|
+
ctx.effect(() => ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
318
|
+
name: 'plugins.row.config',
|
|
319
|
+
key: SLOT_KEY,
|
|
320
|
+
locale: NS,
|
|
321
|
+
}, TargetSessionCard)), 'qq-tools: target session page')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
exports.apply = apply
|
|
325
|
+
exports.inject = inject
|
|
326
|
+
return module.exports
|
|
327
|
+
},
|
|
328
|
+
})
|
package/lib/index.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
// dsh-qq-tools ——
|
|
1
|
+
// dsh-qq-tools —— 包根入口。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
3
|
+
// 两用:
|
|
4
|
+
// · cordis.patch.yml 里的包根行用 config.carrier = true 启动它,只承载
|
|
5
|
+
// 「挂载会话」的 HTTP 接口;浏览器半侧(lib/client.js)也只能从包根行
|
|
6
|
+
// 被发现,子路径行不会被 dsh-client-modules 扫描到。
|
|
7
|
+
// · 旧配置直接引用 `dsh-qq-tools` 时没有 carrier 标记,仍然挂全部工具。
|
|
8
|
+
import { mountAll, mountTargetSessionRoute } from './shared.js'
|
|
7
9
|
|
|
8
10
|
export * from './shared.js'
|
|
9
11
|
|
|
10
12
|
export const name = 'qq-tools'
|
|
11
|
-
export const inject = ['agents', 'timer', 'tools', 'attachments']
|
|
13
|
+
export const inject = ['agents', 'timer', 'tools', 'attachments', 'webServer']
|
|
12
14
|
|
|
13
|
-
export function apply(ctx) {
|
|
15
|
+
export function apply(ctx, config) {
|
|
16
|
+
if (config && config.carrier === true) {
|
|
17
|
+
mountTargetSessionRoute(ctx)
|
|
18
|
+
return {}
|
|
19
|
+
}
|
|
14
20
|
return mountAll(ctx)
|
|
15
21
|
}
|
package/lib/shared.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
//
|
|
35
35
|
// 工具本身的开关在 DSH 插件页的组件列表里:关掉某个组件,它就不再注册对应工具。
|
|
36
36
|
|
|
37
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
|
|
37
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } 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,21 +47,36 @@ const DIAG_PATH = join(DSH_HOME, 'qq-tools-diag.log')
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
const requireFromHere = createRequire(import.meta.url)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
|
|
51
|
+
// sharp 是可选的,按这个顺序找:SHARP_PATH → 插件自己装的 → 宿主进程能解析到的。
|
|
52
|
+
// DSH 桌面版把 sharp 打在 resources/app.asar/dsh/node_modules 下,CLI 版跟着
|
|
53
|
+
// @deepseek-ai/dsh 的依赖走,所以从宿主的启动入口解析最稳,不用写死安装路径。
|
|
54
|
+
function loadSharp() {
|
|
55
|
+
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'))
|
|
61
|
+
}
|
|
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))
|
|
67
|
+
}
|
|
68
|
+
for (const probe of probes) {
|
|
69
|
+
try {
|
|
70
|
+
const s = probe()
|
|
71
|
+
if (s) return s
|
|
72
|
+
} catch (error) {}
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
const sharp = loadSharp()
|
|
78
|
+
let gifWarned = false
|
|
79
|
+
|
|
65
80
|
const DEFAULT_GROUPS = []
|
|
66
81
|
const DEFAULT_CFG = {
|
|
67
82
|
napCat: 'http://127.0.0.1:3000',
|
|
@@ -122,6 +137,28 @@ function loadCfg() {
|
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
|
|
140
|
+
// UI 保存挂载会话时只覆盖传进来的字段,其余配置原样保留;先写临时文件再换名,
|
|
141
|
+
// 避免写一半留下坏 JSON 让后面所有工具都退回默认配置。
|
|
142
|
+
export function saveCfg(patch) {
|
|
143
|
+
ensureCfg()
|
|
144
|
+
let current = {}
|
|
145
|
+
try {
|
|
146
|
+
const raw = JSON.parse(readFileSync(CFG_PATH, 'utf8'))
|
|
147
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) current = raw
|
|
148
|
+
} catch (error) {}
|
|
149
|
+
const next = { ...current, ...(patch && typeof patch === 'object' ? patch : {}) }
|
|
150
|
+
try {
|
|
151
|
+
mkdirSync(DSH_HOME, { recursive: true })
|
|
152
|
+
const tmp = CFG_PATH + '.tmp'
|
|
153
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), 'utf8')
|
|
154
|
+
renameSync(tmp, CFG_PATH)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
diag('保存配置失败:' + String((error && error.message) || error))
|
|
157
|
+
throw error
|
|
158
|
+
}
|
|
159
|
+
return next
|
|
160
|
+
}
|
|
161
|
+
|
|
125
162
|
function targetSessionId() {
|
|
126
163
|
const cfg = loadCfg()
|
|
127
164
|
return String(cfg.targetSession || '')
|
|
@@ -157,6 +194,103 @@ function findAgent(agents, id) {
|
|
|
157
194
|
return undefined
|
|
158
195
|
}
|
|
159
196
|
|
|
197
|
+
|
|
198
|
+
// ---------- 目标会话 UI 接口(由「挂载会话」组件启动) ----------
|
|
199
|
+
// 浏览器半侧只读写 targetSession:GET 返回当前值和活着的会话,POST 写回配置文件;
|
|
200
|
+
// 其他组件仍然每 3 秒 loadCfg(),所以保存后不需要重启。
|
|
201
|
+
const TARGET_SESSION_ROUTE = '/api/dsh-qq-tools/target-session'
|
|
202
|
+
|
|
203
|
+
function replyJson(res, status, body) {
|
|
204
|
+
const text = JSON.stringify(body)
|
|
205
|
+
try {
|
|
206
|
+
res.writeHead(status, {
|
|
207
|
+
'content-type': 'application/json; charset=utf-8',
|
|
208
|
+
'cache-control': 'no-store',
|
|
209
|
+
})
|
|
210
|
+
res.end(text)
|
|
211
|
+
} catch (error) {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function readRequestJson(req, max = 64 * 1024) {
|
|
215
|
+
const chunks = []
|
|
216
|
+
let size = 0
|
|
217
|
+
for await (const chunk of req) {
|
|
218
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
219
|
+
size += buf.length
|
|
220
|
+
if (size > max) throw new Error('body too large')
|
|
221
|
+
chunks.push(buf)
|
|
222
|
+
}
|
|
223
|
+
if (!chunks.length) return {}
|
|
224
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sameOriginRequest(req) {
|
|
228
|
+
const origin = String((req.headers && req.headers.origin) || '')
|
|
229
|
+
if (!origin) return true
|
|
230
|
+
try {
|
|
231
|
+
return new URL(origin).host === String((req.headers && req.headers.host) || '')
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function mountTargetSessionRoute(ctx) {
|
|
238
|
+
const webServer = ctx.get('webServer')
|
|
239
|
+
if (!webServer || typeof webServer.register !== 'function') {
|
|
240
|
+
diag('挂载会话:宿主没有 webServer,设置页接口不可用')
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
ctx.effect(() => {
|
|
244
|
+
const dispose = webServer.register({
|
|
245
|
+
kind: 'exact',
|
|
246
|
+
path: TARGET_SESSION_ROUTE,
|
|
247
|
+
handler: async (req, res) => {
|
|
248
|
+
if (!sameOriginRequest(req)) {
|
|
249
|
+
replyJson(res, 403, { ok: false, error: 'forbidden' })
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
const method = String(req.method || 'GET').toUpperCase()
|
|
253
|
+
if (method === 'GET') {
|
|
254
|
+
const liveSessionIds = []
|
|
255
|
+
try {
|
|
256
|
+
const agents = ctx.get('agents') || ctx.agents
|
|
257
|
+
for (const agent of (agents && agents.list()) || []) {
|
|
258
|
+
const sid = sessionIdOf(agent)
|
|
259
|
+
if (sid) liveSessionIds.push(sid)
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {}
|
|
262
|
+
replyJson(res, 200, { ok: true, targetSession: targetSessionId(), liveSessionIds })
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
if (method !== 'POST') {
|
|
266
|
+
replyJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
let body
|
|
270
|
+
try {
|
|
271
|
+
body = await readRequestJson(req)
|
|
272
|
+
} catch (error) {
|
|
273
|
+
replyJson(res, 400, { ok: false, error: 'invalid body' })
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
const raw = body && typeof body.targetSession === 'string' ? body.targetSession : ''
|
|
277
|
+
const targetSession = clean(raw).slice(0, 200)
|
|
278
|
+
try {
|
|
279
|
+
saveCfg({ targetSession })
|
|
280
|
+
} catch (error) {
|
|
281
|
+
replyJson(res, 500, { ok: false, error: 'save failed' })
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
diag('目标会话已更新:' + (targetSession || '(不挂载)'))
|
|
285
|
+
replyJson(res, 200, { ok: true, targetSession })
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
return () => {
|
|
289
|
+
try { dispose() } catch (error) {}
|
|
290
|
+
}
|
|
291
|
+
}, 'qq-tools: target session route')
|
|
292
|
+
}
|
|
293
|
+
|
|
160
294
|
function allowedGroup(groupId, cfg) {
|
|
161
295
|
if (!Array.isArray(cfg.groups) || cfg.groups.length === 0) return true
|
|
162
296
|
return cfg.groups.includes(Number(groupId))
|
|
@@ -220,6 +354,14 @@ function segText(segs) {
|
|
|
220
354
|
.join(''))
|
|
221
355
|
}
|
|
222
356
|
|
|
357
|
+
// mface(商城表情/表情包)基本都是动图;普通 image 段只能从名字和摘要里猜。
|
|
358
|
+
function animatedSeg(seg) {
|
|
359
|
+
if (!seg || !seg.data) return false
|
|
360
|
+
const d = seg.data
|
|
361
|
+
return seg.type === 'mface' ||
|
|
362
|
+
/gif|动画/i.test(String(d.summary || '') + ' ' + String(d.file || '') + ' ' + String(d.url || ''))
|
|
363
|
+
}
|
|
364
|
+
|
|
223
365
|
function fmtTime(sec) {
|
|
224
366
|
const t = Number(sec)
|
|
225
367
|
if (!Number.isFinite(t) || t <= 0) return ''
|
|
@@ -427,10 +569,14 @@ export function admitImagesFor(ctx) {
|
|
|
427
569
|
if (prepared.length >= 6) break
|
|
428
570
|
continue
|
|
429
571
|
}
|
|
572
|
+
if (!sharp && !gifWarned) {
|
|
573
|
+
gifWarned = true
|
|
574
|
+
diag('GIF 帧拆分不可用:没有加载到 sharp,动图会按静图处理')
|
|
575
|
+
}
|
|
430
576
|
}
|
|
431
577
|
prepared.push({
|
|
432
578
|
caption: String(x.caption || ''),
|
|
433
|
-
block: { type: 'image', data: bytes.toString('base64'), mediaType: mediaTypeOf(name), name },
|
|
579
|
+
block: { type: 'image', data: bytes.toString('base64'), mediaType: isGif ? 'image/gif' : mediaTypeOf(name), name },
|
|
434
580
|
})
|
|
435
581
|
if (prepared.length >= 6) break
|
|
436
582
|
}
|
|
@@ -491,11 +637,7 @@ export function makeHistoryTool(admitImages) {
|
|
|
491
637
|
const segs = Array.isArray(m.message) ? m.message : []
|
|
492
638
|
for (const seg of segs) {
|
|
493
639
|
if (!seg || (seg.type !== 'image' && seg.type !== 'mface') || !seg.data) continue
|
|
494
|
-
|
|
495
|
-
const animated =
|
|
496
|
-
seg.type === 'mface' ||
|
|
497
|
-
/gif|动画/i.test(String(d.summary || '') + ' ' + String(d.file || '') + ' ' + String(d.url || ''))
|
|
498
|
-
picked.push({ m, seg, animated })
|
|
640
|
+
picked.push({ m, seg, animated: animatedSeg(seg) })
|
|
499
641
|
}
|
|
500
642
|
}
|
|
501
643
|
const anim = picked.filter((x) => x.animated).slice(-2)
|
|
@@ -708,10 +850,7 @@ export function makePrivateHistoryTool(admitImages) {
|
|
|
708
850
|
const segs = Array.isArray(m.message) ? m.message : []
|
|
709
851
|
for (const seg of segs) {
|
|
710
852
|
if (!seg || (seg.type !== 'image' && seg.type !== 'mface') || !seg.data) continue
|
|
711
|
-
|
|
712
|
-
seg.type === 'mface' ||
|
|
713
|
-
/gif|动画/i.test(String(seg.data.summary || '') + ' ' + String(seg.data.file || '') + ' ' + String(seg.data.url || ''))
|
|
714
|
-
picked.push({ m, seg, animated })
|
|
853
|
+
picked.push({ m, seg, animated: animatedSeg(seg) })
|
|
715
854
|
}
|
|
716
855
|
}
|
|
717
856
|
const anim = picked.filter((x) => x.animated).slice(-2)
|
|
@@ -1594,19 +1733,22 @@ async function handlePrivateMessage(ctx, ev, admitImages) {
|
|
|
1594
1733
|
'不想回就安静待着;不要用 Markdown,不要提工具,短一点。'
|
|
1595
1734
|
const content = [{ type: 'text', text: prompt }]
|
|
1596
1735
|
if (typeof admitImages === 'function') {
|
|
1597
|
-
|
|
1736
|
+
// 和群/私聊记录一样:先保证最近两张动图能进来,剩下的名额给静图。
|
|
1737
|
+
const anim = []
|
|
1738
|
+
const stat = []
|
|
1598
1739
|
for (const seg of segs) {
|
|
1599
1740
|
if (!seg || (seg.type !== 'image' && seg.type !== 'mface') || !seg.data) continue
|
|
1600
|
-
|
|
1601
|
-
if (
|
|
1741
|
+
const item = { seg, animated: animatedSeg(seg) }
|
|
1742
|
+
if (item.animated) anim.push(item)
|
|
1743
|
+
else stat.push(item)
|
|
1602
1744
|
}
|
|
1745
|
+
const animPick = anim.slice(-2)
|
|
1746
|
+
const picked = [...animPick, ...stat.slice(-(4 - animPick.length))]
|
|
1603
1747
|
if (picked.length) {
|
|
1604
|
-
const images = await admitImages(picked.map((seg, i) => ({
|
|
1748
|
+
const images = await admitImages(picked.map(({ seg, animated }, i) => ({
|
|
1605
1749
|
data: seg.data,
|
|
1606
1750
|
cfg,
|
|
1607
|
-
animated
|
|
1608
|
-
seg.type === 'mface' ||
|
|
1609
|
-
/gif|动画/i.test(String(seg.data.summary || '') + ' ' + String(seg.data.file || '') + ' ' + String(seg.data.url || '')),
|
|
1751
|
+
animated,
|
|
1610
1752
|
name: (seg.data && seg.data.file) || `qq-private-${ev.message_id || 'x'}-${i}.jpg`,
|
|
1611
1753
|
caption: `私聊 ${name} 发的图片(消息 ${ev.message_id || '?'})`,
|
|
1612
1754
|
})))
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-qq-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "给大肥鱼会话用的 QQ 群/私聊工具:列群、看群记录、发群消息、发表情、收藏表情(SnowLuma
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "给大肥鱼会话用的 QQ 群/私聊工具:列群、看群记录、发群消息、发表情、收藏表情(SnowLuma 收藏夹)、好友/私聊记录、发私聊消息、私聊唤醒、群成员、戳一戳感知/戳回去、挂载会话选择。8 个工具加 1 个设置组件,可在插件页逐个开关。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"license": "MIT",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"lib",
|
|
28
28
|
"tools",
|
|
29
|
+
"locale",
|
|
29
30
|
"cordis.patch.yml",
|
|
30
31
|
"README.md",
|
|
31
32
|
"LICENSE"
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
},
|
|
36
37
|
"exports": {
|
|
37
38
|
".": "./lib/index.js",
|
|
39
|
+
"./client": "./lib/client.js",
|
|
38
40
|
"./tools/qq-list-groups": "./tools/qq-list-groups/index.js",
|
|
39
41
|
"./tools/qq-list-groups/locale/en.json": "./tools/qq-list-groups/locale/en.json",
|
|
40
42
|
"./tools/qq-list-groups/locale/zh.json": "./tools/qq-list-groups/locale/zh.json",
|
|
@@ -59,12 +61,22 @@
|
|
|
59
61
|
"./tools/qq-send-poke": "./tools/qq-send-poke/index.js",
|
|
60
62
|
"./tools/qq-send-poke/locale/en.json": "./tools/qq-send-poke/locale/en.json",
|
|
61
63
|
"./tools/qq-send-poke/locale/zh.json": "./tools/qq-send-poke/locale/zh.json",
|
|
64
|
+
"./locale/en.json": "./locale/en.json",
|
|
65
|
+
"./locale/zh.json": "./locale/zh.json",
|
|
62
66
|
"./package.json": "./package.json"
|
|
63
67
|
},
|
|
64
|
-
"description": "给大肥鱼会话用的 QQ 群/私聊工具:列群、看群记录、发群消息、发表情、收藏表情(SnowLuma 收藏夹)、好友/私聊记录、发私聊消息、私聊唤醒、群成员、戳一戳感知/戳回去。8 个工具各是一个独立组件,可在插件页逐个开关。",
|
|
65
68
|
"dsh": {
|
|
66
69
|
"bundle": {
|
|
67
70
|
"patch": "./cordis.patch.yml"
|
|
71
|
+
},
|
|
72
|
+
"client": {
|
|
73
|
+
"inject": [
|
|
74
|
+
"@deepseek-ai/dsh-client-locale",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
78
|
+
],
|
|
79
|
+
"platform": "web"
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
}
|