dsh-qqchat 0.2.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +276 -0
  3. package/README.md +211 -0
  4. package/client-src/plugin.cts +15 -0
  5. package/client-src/settings.cts +216 -0
  6. package/client-src/shared.cts +104 -0
  7. package/client-src/workspace.cts +164 -0
  8. package/cordis.patch.yml +7 -0
  9. package/docs/ARCHITECTURE.en.md +346 -0
  10. package/docs/ARCHITECTURE.md +411 -0
  11. package/docs/DEVELOPMENT.md +69 -0
  12. package/docs/MEDIA_AND_QUOTES.md +644 -0
  13. package/docs/MEMORY.md +99 -0
  14. package/docs/MEMORY_CONTEXT.md +254 -0
  15. package/docs/QUOTES.md +355 -0
  16. package/docs/README.md +22 -0
  17. package/docs/SECURITY.md +62 -0
  18. package/docs/STREAMING.md +347 -0
  19. package/docs/assets//346/211/253/347/240/201/351/223/276/346/216/245.jpg +0 -0
  20. package/docs/assets//347/276/244/350/201/212/347/225/214/351/235/242.jpg +0 -0
  21. package/docs/assets//350/256/260/345/277/206/347/263/273/347/273/237.jpg +0 -0
  22. package/docs/assets//350/256/276/347/275/256/347/225/214/351/235/242.jpg +0 -0
  23. package/docs/devlog.md +24 -0
  24. package/lib/client.js +433 -0
  25. package/lib/commands/dispatch.d.ts +16 -0
  26. package/lib/commands/dispatch.js +48 -0
  27. package/lib/config.d.ts +3 -0
  28. package/lib/config.js +63 -0
  29. package/lib/gateway/api.d.ts +30 -0
  30. package/lib/gateway/api.js +202 -0
  31. package/lib/gateway/auth.d.ts +35 -0
  32. package/lib/gateway/auth.js +80 -0
  33. package/lib/gateway/crypto.d.ts +2 -0
  34. package/lib/gateway/crypto.js +17 -0
  35. package/lib/gateway/gateway.d.ts +32 -0
  36. package/lib/gateway/gateway.js +219 -0
  37. package/lib/gateway/normalize.d.ts +6 -0
  38. package/lib/gateway/normalize.js +248 -0
  39. package/lib/index.d.ts +14 -0
  40. package/lib/index.js +35 -0
  41. package/lib/media/image-tool.d.ts +4 -0
  42. package/lib/media/image-tool.js +62 -0
  43. package/lib/media/media-tools.d.ts +3 -0
  44. package/lib/media/media-tools.js +68 -0
  45. package/lib/media/store.d.ts +14 -0
  46. package/lib/media/store.js +176 -0
  47. package/lib/session/agent-bridge.d.ts +56 -0
  48. package/lib/session/agent-bridge.js +613 -0
  49. package/lib/session/memory-snapshot.d.ts +23 -0
  50. package/lib/session/memory-snapshot.js +27 -0
  51. package/lib/session/runtime.d.ts +41 -0
  52. package/lib/session/runtime.js +395 -0
  53. package/lib/shared/augmentations.d.ts +40 -0
  54. package/lib/shared/augmentations.js +1 -0
  55. package/lib/shared/logging.d.ts +14 -0
  56. package/lib/shared/logging.js +41 -0
  57. package/lib/storage/db.d.ts +82 -0
  58. package/lib/storage/db.js +669 -0
  59. package/lib/storage/memory.d.ts +50 -0
  60. package/lib/storage/memory.js +566 -0
  61. package/lib/transport/rpc.d.ts +18 -0
  62. package/lib/transport/rpc.js +318 -0
  63. package/lib/types.d.ts +416 -0
  64. package/lib/types.js +1 -0
  65. package/package.json +115 -0
  66. package/scripts/wrap-client.mjs +20 -0
  67. package/src/commands/dispatch.ts +75 -0
  68. package/src/config.ts +70 -0
  69. package/src/gateway/api.ts +260 -0
  70. package/src/gateway/auth.ts +99 -0
  71. package/src/gateway/crypto.ts +16 -0
  72. package/src/gateway/gateway.ts +207 -0
  73. package/src/gateway/normalize.ts +236 -0
  74. package/src/index.ts +46 -0
  75. package/src/media/image-tool.ts +62 -0
  76. package/src/media/media-tools.ts +66 -0
  77. package/src/media/store.ts +157 -0
  78. package/src/session/agent-bridge.ts +624 -0
  79. package/src/session/memory-snapshot.ts +41 -0
  80. package/src/session/runtime.ts +368 -0
  81. package/src/shared/augmentations.ts +45 -0
  82. package/src/shared/logging.ts +36 -0
  83. package/src/storage/db.ts +760 -0
  84. package/src/storage/memory.ts +581 -0
  85. package/src/transport/rpc.ts +316 -0
  86. package/src/types.ts +456 -0
  87. package/tsconfig.build.json +12 -0
  88. package/tsconfig.client.json +14 -0
  89. package/tsconfig.json +23 -0
package/lib/client.js ADDED
@@ -0,0 +1,433 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-qqchat", factory: (require) => {
2
+ var __qqModules = {
3
+ "./plugin.cjs": function(module, exports, require) {
4
+ "use strict";
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const shared_cjs_1 = require("./shared.cjs");
7
+ const settings_cjs_1 = require("./settings.cjs");
8
+ const workspace_cjs_1 = require("./workspace.cjs");
9
+ exports.inject = ['slots', 'connection', 'conversationEvents'];
10
+ exports.apply = function apply(ctx) {
11
+ (0, shared_cjs_1.setQQRpc)(ctx.connection.rpc);
12
+ ctx.effect(shared_cjs_1.installStyles, 'dsh-qqchat: client styles');
13
+ ctx.conversationEvents.register((0, workspace_cjs_1.createQQMessageDefinition)());
14
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, settings_cjs_1.QQSettings));
15
+ ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, workspace_cjs_1.QQTranscriptNode));
16
+ ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'qqchat-memory', order: 30, inject: () => ({ rpc: ctx.connection.rpc }) }, workspace_cjs_1.QQSessionUtility));
17
+ };
18
+
19
+ },
20
+ "./settings.cjs": function(module, exports, require) {
21
+ "use strict";
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.QQSettings = QQSettings;
24
+ const shared_cjs_1 = require("./shared.cjs");
25
+ let qqSettingsCache;
26
+ function QQSettings({ rpc }) {
27
+ const [account, setAccount] = (0, shared_cjs_1.useState)(() => qqSettingsCache?.account || null);
28
+ const [settings, setSettings] = (0, shared_cjs_1.useState)(() => qqSettingsCache?.settings || null);
29
+ const [members, setMembers] = (0, shared_cjs_1.useState)(() => qqSettingsCache?.members || []);
30
+ const [loading, setLoading] = (0, shared_cjs_1.useState)(() => qqSettingsCache === undefined);
31
+ const [auth, setAuth] = (0, shared_cjs_1.useState)(null);
32
+ const [logs, setLogs] = (0, shared_cjs_1.useState)(null);
33
+ const [busy, setBusy] = (0, shared_cjs_1.useState)(false);
34
+ const [error, setError] = (0, shared_cjs_1.useState)('');
35
+ const [groupReplyMenuOpen, setGroupReplyMenuOpen] = (0, shared_cjs_1.useState)(false);
36
+ const [directReplyMenuOpen, setDirectReplyMenuOpen] = (0, shared_cjs_1.useState)(false);
37
+ const [ownerMenuOpen, setOwnerMenuOpen] = (0, shared_cjs_1.useState)(false);
38
+ const refresh = (0, shared_cjs_1.useCallback)(async () => {
39
+ try {
40
+ const [status, prefs] = await Promise.all([
41
+ (0, shared_cjs_1.call)(rpc, 'status'),
42
+ (0, shared_cjs_1.call)(rpc, 'settings/get'),
43
+ ]);
44
+ const next = {
45
+ account: status.accounts.find(item => item.enabled) || null,
46
+ settings: prefs.settings,
47
+ members: prefs.members || [],
48
+ };
49
+ qqSettingsCache = next;
50
+ setAccount(next.account);
51
+ setSettings(next.settings);
52
+ setMembers(next.members);
53
+ setError('');
54
+ }
55
+ catch (err) {
56
+ setError(err instanceof Error ? err.message : String(err));
57
+ }
58
+ finally {
59
+ setLoading(false);
60
+ }
61
+ }, [rpc]);
62
+ (0, shared_cjs_1.useEffect)(() => { void refresh(); }, [refresh]);
63
+ (0, shared_cjs_1.useEffect)(() => {
64
+ if (!auth)
65
+ return;
66
+ let stopped = false;
67
+ let timer;
68
+ const poll = async () => {
69
+ if (stopped)
70
+ return;
71
+ try {
72
+ const state = await (0, shared_cjs_1.call)(rpc, 'auth/poll', { taskId: auth.taskId });
73
+ if (state.status === 'success') {
74
+ setAuth(null);
75
+ await refresh();
76
+ return;
77
+ }
78
+ if (state.status === 'fail' || state.status === 'expired') {
79
+ setError(state.reason || '二维码已失效');
80
+ setAuth(null);
81
+ return;
82
+ }
83
+ }
84
+ catch (err) {
85
+ if (!stopped) {
86
+ setError(err instanceof Error ? err.message : String(err));
87
+ setAuth(null);
88
+ }
89
+ return;
90
+ }
91
+ if (!stopped)
92
+ timer = setTimeout(() => void poll(), 3000);
93
+ };
94
+ timer = setTimeout(() => void poll(), 3000);
95
+ return () => { stopped = true; if (timer)
96
+ clearTimeout(timer); };
97
+ }, [auth, rpc, refresh]);
98
+ const patch = (0, shared_cjs_1.useCallback)(async (value) => {
99
+ if (!settings)
100
+ return;
101
+ const optimistic = { ...settings, ...value };
102
+ setSettings(optimistic);
103
+ try {
104
+ const result = await (0, shared_cjs_1.call)(rpc, 'settings/update', { patch: value });
105
+ setSettings(result.settings);
106
+ if (qqSettingsCache)
107
+ qqSettingsCache = { ...qqSettingsCache, settings: result.settings };
108
+ setError('');
109
+ }
110
+ catch (err) {
111
+ setSettings(settings);
112
+ setError(err instanceof Error ? err.message : String(err));
113
+ }
114
+ }, [rpc, settings]);
115
+ const startAuth = async () => {
116
+ setBusy(true);
117
+ try {
118
+ setAuth(await (0, shared_cjs_1.call)(rpc, 'auth/start'));
119
+ setError('');
120
+ }
121
+ catch (err) {
122
+ setError(err instanceof Error ? err.message : String(err));
123
+ }
124
+ finally {
125
+ setBusy(false);
126
+ }
127
+ };
128
+ const disconnect = async () => {
129
+ if (!account)
130
+ return;
131
+ setBusy(true);
132
+ try {
133
+ await (0, shared_cjs_1.call)(rpc, 'account/disconnect', { accountId: account.id });
134
+ setAccount(null);
135
+ if (qqSettingsCache)
136
+ qqSettingsCache = { ...qqSettingsCache, account: null };
137
+ setAuth(null);
138
+ setError('已取消连接,可以重新扫码绑定 Bot。');
139
+ }
140
+ catch (err) {
141
+ setError(err instanceof Error ? err.message : String(err));
142
+ }
143
+ finally {
144
+ setBusy(false);
145
+ }
146
+ };
147
+ const openLogs = async () => {
148
+ try {
149
+ setLogs((await (0, shared_cjs_1.call)(rpc, 'logs/list', { limit: 300 })).logs);
150
+ }
151
+ catch (err) {
152
+ setError(err instanceof Error ? err.message : String(err));
153
+ }
154
+ };
155
+ const ownerPicker = (0, shared_cjs_1.h)(shared_cjs_1.Menu, {
156
+ open: ownerMenuOpen,
157
+ onClose: () => setOwnerMenuOpen(false),
158
+ selectedId: settings?.ownerUserId,
159
+ onSelect: (id) => { setOwnerMenuOpen(false); void patch({ ownerUserId: id }); },
160
+ anchor: (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => setOwnerMenuOpen(!ownerMenuOpen) }, '选择用户'),
161
+ align: 'end',
162
+ items: [{ id: '', label: '不设置 Owner' }, ...members.map(member => ({ id: member.platformUserId, label: member.displayName || 'QQ 群友' }))],
163
+ });
164
+ const formatLabel = (value) => value === 'markdown' ? 'Markdown' : value === 'compat' ? '纯文本兼容' : '智能兼容';
165
+ const formatItems = [{ id: 'smart', label: '智能兼容' }, { id: 'markdown', label: 'Markdown' }, { id: 'compat', label: '纯文本兼容' }];
166
+ const groupReplyPicker = (0, shared_cjs_1.h)(shared_cjs_1.Menu, {
167
+ open: groupReplyMenuOpen,
168
+ onClose: () => setGroupReplyMenuOpen(false),
169
+ selectedId: settings?.groupReplyFormat,
170
+ onSelect: (id) => { setGroupReplyMenuOpen(false); void patch({ groupReplyFormat: id }); },
171
+ anchor: (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => setGroupReplyMenuOpen(!groupReplyMenuOpen) }, formatLabel(settings?.groupReplyFormat)),
172
+ items: formatItems,
173
+ align: 'end',
174
+ });
175
+ const directReplyPicker = (0, shared_cjs_1.h)(shared_cjs_1.Menu, {
176
+ open: directReplyMenuOpen,
177
+ onClose: () => setDirectReplyMenuOpen(false),
178
+ selectedId: settings?.directReplyFormat,
179
+ onSelect: (id) => { setDirectReplyMenuOpen(false); void patch({ directReplyFormat: id }); },
180
+ anchor: (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => setDirectReplyMenuOpen(!directReplyMenuOpen) }, formatLabel(settings?.directReplyFormat)),
181
+ items: formatItems,
182
+ align: 'end',
183
+ });
184
+ const head = (0, shared_cjs_1.h)('div', { className: 'qqsHead' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqsTitle' }, 'QQ Chat'), (0, shared_cjs_1.h)('div', { className: 'qqMuted' }, 'QQ Bot 接收、兼容与权限设置')), (0, shared_cjs_1.h)('div', { className: 'qqStatus' }, loading ? '加载中…' : account ? (account.gatewayStatus === 'online' ? '已连接' : '已授权') : '未连接', account && (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', disabled: busy, onClick: () => void disconnect() }, '取消连接')));
185
+ if (loading)
186
+ return (0, shared_cjs_1.h)('div', { className: 'qqs' }, head, (0, shared_cjs_1.h)('div', { className: 'qqMuted' }, '正在读取 QQ Chat 设置…'));
187
+ if (!account)
188
+ return (0, shared_cjs_1.h)('div', { className: 'qqs' }, head, error && (0, shared_cjs_1.h)('div', { className: 'qqError' }, error), (0, shared_cjs_1.h)('div', { className: 'qqConnect' }, (0, shared_cjs_1.h)('div', { className: 'qqsTitle' }, auth ? '使用 QQ 扫码授权' : '连接 QQ Bot'), (0, shared_cjs_1.h)('div', { className: 'qqMuted' }, '扫码和凭据解密都在 DSH Host 侧完成。'), auth && (0, shared_cjs_1.h)('div', { className: 'qqQr' }, (0, shared_cjs_1.h)('img', { src: auth.qrDataUrl, alt: 'QQ 授权二维码' })), (0, shared_cjs_1.h)(shared_cjs_1.Button, { variant: 'primary', disabled: busy, onClick: () => void startAuth() }, auth ? '重新生成二维码' : '扫码连接')));
189
+ if (!settings)
190
+ return (0, shared_cjs_1.h)('div', { className: 'qqs' }, head, (0, shared_cjs_1.h)('div', { className: 'qqMuted' }, '正在读取设置…'));
191
+ const receiveButtons = (0, shared_cjs_1.h)('div', { className: 'qqChoiceButtons' }, ...['auto', 'mention', 'silent'].map(mode => (0, shared_cjs_1.h)(shared_cjs_1.Button, {
192
+ key: mode,
193
+ size: 'sm',
194
+ variant: settings.groupReceiveMode === mode ? 'primary' : 'outline',
195
+ onClick: () => void patch({ groupReceiveMode: mode }),
196
+ }, mode === 'auto' ? '自动回应' : mode === 'mention' ? '@回复' : '静默记录')));
197
+ const receiveGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '消息接收'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊接收方式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '自动回应:每条群消息都会触发 Agent;@回复:所有消息都记录,但只有 @Bot 才唤醒 Agent;静默记录:只记录群聊与记忆,不主动回应。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, receiveButtons)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 群聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, groupReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '私聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 私聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, directReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('label', { className: 'qqCheckRow' }, (0, shared_cjs_1.h)('input', {
198
+ type: 'checkbox',
199
+ checked: settings.directStreamingEnabled && settings.directReplyFormat !== 'compat',
200
+ disabled: settings.directReplyFormat === 'compat',
201
+ onChange: (event) => void patch({ directStreamingEnabled: event.target.checked }),
202
+ }), (0, shared_cjs_1.h)('span', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '私聊流式回复'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, settings.directReplyFormat === 'compat'
203
+ ? '纯文本兼容模式不支持流式传输,已自动关闭。'
204
+ : settings.directStreamingEnabled
205
+ ? '使用 QQ 官方流式接口逐步更新私聊回复;部分 QQ 客户端可能看不到流式效果。'
206
+ : '使用普通一次性消息发送,兼容性更好。')))));
207
+ const memoryGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '记忆系统'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '启用记忆系统'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '记忆会注入每轮 Agent 上下文,并在空闲时整理近期消息;这可能降低上下文缓存命中率并增加输入 token。关闭后停止记忆注入和后台整理,但不会删除已有记忆。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: settings.memoryEnabled ? 'primary' : 'outline', onClick: () => void patch({ memoryEnabled: !settings.memoryEnabled }) }, settings.memoryEnabled ? '已启用' : '已关闭'))));
208
+ const ownerSetting = !settings.groupMembersCanUseTools
209
+ ? (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, 'Owner stable ID'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '使用 stable sender ID,不根据昵称判断。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, (0, shared_cjs_1.h)(shared_cjs_1.Input, {
210
+ value: settings.ownerUserId,
211
+ placeholder: '粘贴 stable ID',
212
+ onChange: (event) => setSettings({ ...settings, ownerUserId: event.target.value }),
213
+ onBlur: (event) => void patch({ ownerUserId: event.currentTarget.value }),
214
+ }), ownerPicker))
215
+ : null;
216
+ const toolsGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '工具权限'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('label', { className: 'qqCheckRow' }, (0, shared_cjs_1.h)('input', { type: 'checkbox', checked: settings.groupMembersCanUseTools, onChange: (event) => void patch({ groupMembersCanUseTools: event.target.checked }) }), (0, shared_cjs_1.h)('span', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群成员可用工具'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, settings.groupMembersCanUseTools ? '群友触发的 Agent 回合可以使用当前 preset 的全部工具。' : '关闭后只有 Owner 触发的群聊回合可以执行工具;其他群友仍可正常聊天,但工具调用会被拒绝。')))), ownerSetting);
217
+ const mediaGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '媒体与文件权限'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('label', { className: 'qqCheckRow' }, (0, shared_cjs_1.h)('input', { type: 'checkbox', checked: settings.groupMembersCanReceiveMedia, onChange: (event) => void patch({ groupMembersCanReceiveMedia: event.target.checked }) }), (0, shared_cjs_1.h)('span', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '接收群成员媒体/文件'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, settings.groupMembersCanReceiveMedia ? '下载并在会话中显示群成员发送的图片、文件、音频和视频。' : '群成员发送的媒体不会下载、存储或注入会话;Owner 和私聊不受影响。')))), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('label', { className: 'qqCheckRow' }, (0, shared_cjs_1.h)('input', { type: 'checkbox', checked: settings.groupMembersCanReadMedia, onChange: (event) => void patch({ groupMembersCanReadMedia: event.target.checked }) }), (0, shared_cjs_1.h)('span', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '允许群成员读取媒体/文件'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, settings.groupMembersCanReadMedia ? '群成员触发的回合可以调用 QQChat 媒体工具读取当前消息附件。' : '群成员仍可看到已接收的媒体,但不能调用媒体工具读取内容;Owner 和私聊不受影响。')))));
218
+ const diagnosticsGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '诊断'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '插件日志'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, account.gatewayLastError ? `最近网关错误:${account.gatewayLastError}` : '查看 QQ Gateway、授权、Agent bridge 与消息发送的最近日志。')), (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => void openLogs() }, '查看日志')));
219
+ return (0, shared_cjs_1.h)('div', { className: 'qqs' }, head, error && (0, shared_cjs_1.h)('div', { className: 'qqError' }, error), receiveGroup, memoryGroup, mediaGroup, toolsGroup, diagnosticsGroup, logs && (0, shared_cjs_1.h)(shared_cjs_1.Modal, { title: 'QQ Chat 日志', onClose: () => setLogs(null) }, logs.length ? logs.map(log => (0, shared_cjs_1.h)('div', { key: log.id, className: `qqLog ${log.level}` }, (0, shared_cjs_1.h)('span', { className: 'qqLogMeta' }, `${(0, shared_cjs_1.dateTime)(log.time)} [${log.level}]`), log.message)) : (0, shared_cjs_1.h)('div', { className: 'qqMuted' }, '暂无日志。')));
220
+ }
221
+
222
+ },
223
+ "./shared.cjs": function(module, exports, require) {
224
+ "use strict";
225
+ Object.defineProperty(exports, "__esModule", { value: true });
226
+ exports.dateTime = exports.time = exports.initials = exports.short = exports.CHANNEL = exports.h = exports.useSyncExternalStore = exports.useState = exports.useMemo = exports.useEffect = exports.useCallback = exports.Menu = exports.Input = exports.Button = exports.React = void 0;
227
+ exports.setQQRpc = setQQRpc;
228
+ exports.getQQRpc = getQQRpc;
229
+ exports.installStyles = installStyles;
230
+ exports.call = call;
231
+ exports.Modal = Modal;
232
+ exports.React = require('react');
233
+ const dshPrimitives = require('@deepseek-ai/dsh-client-ui-primitives');
234
+ const { Button: DshButton, Input: DshInput, Menu: DshMenu, Modal: DshModal } = dshPrimitives;
235
+ exports.Button = DshButton;
236
+ exports.Input = DshInput;
237
+ exports.Menu = DshMenu;
238
+ exports.useCallback = exports.React.useCallback, exports.useEffect = exports.React.useEffect, exports.useMemo = exports.React.useMemo, exports.useState = exports.React.useState, exports.useSyncExternalStore = exports.React.useSyncExternalStore;
239
+ exports.h = exports.React.createElement;
240
+ let qqRpc;
241
+ function setQQRpc(rpc) { qqRpc = rpc; }
242
+ function getQQRpc() { return qqRpc; }
243
+ exports.CHANNEL = '/qqchat';
244
+ const STYLE_ID = 'dsh-qqchat/client-ui-v2';
245
+ const css = `
246
+ .qqs{display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary);font:inherit}.qqs *{box-sizing:border-box}
247
+ .qqsHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.qqsBrand{display:flex;align-items:center;gap:10px}.qqsLogo{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-weight:700}.qqsTitle{font-size:15px;font-weight:620}.qqMuted{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.55}.qqStatus{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}.qqDot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-warn-primary)}.qqDot.online{background:var(--dsw-alias-state-success-primary)}
248
+ .qqCard{border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}.qqCardHead{padding:12px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:13px;font-weight:600}.qqSetting{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:13px 14px;border-bottom:1px solid var(--dsw-alias-border-l1)}.qqSetting:last-child{border-bottom:0}.qqSettingTitle{font-size:13px}.qqSettingHelp{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:4px;line-height:1.5;max-width:470px}.qqControl{min-width:190px;display:flex;justify-content:flex-end;align-items:center;gap:8px}.qqSelect,.qqInput{font:inherit;font-size:12px;color:inherit;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 9px;outline:none}.qqInput{width:250px}.qqSelect{min-width:180px}.qqSwitch{position:relative;width:38px;height:22px;border:0;border-radius:999px;background:var(--dsw-alias-bg-overlay);cursor:pointer}.qqSwitch.on{background:var(--dsw-alias-brand-primary)}.qqSwitch:after{content:'';position:absolute;width:16px;height:16px;border-radius:50%;top:3px;left:3px;background:var(--dsw-alias-bg-base);transition:transform .15s}.qqSwitch.on:after{transform:translateX(16px)}
249
+ .qqSegment{display:flex;padding:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-base)}.qqSegment button{font:inherit;font-size:11px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);padding:6px 9px;border-radius:6px;cursor:pointer;white-space:nowrap}.qqSegment button.active{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
250
+ .qqBtn{font:inherit;font-size:12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:inherit;border-radius:8px;padding:7px 11px;cursor:pointer}.qqBtn:hover{background:var(--dsw-alias-bg-layer-2)}.qqBtn.primary{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-base);border-color:transparent}.qqBtn:disabled{opacity:.45;cursor:default}.qqError{font-size:12px;color:var(--dsw-alias-state-error-primary)}
251
+ .qqConnect{padding:24px;display:flex;flex-direction:column;align-items:center;gap:14px;text-align:center}.qqQr{padding:14px;background:#fff;border-radius:14px}.qqQr img{display:block;width:210px;height:210px}
252
+ .qqModalMask{position:fixed;inset:0;z-index:10000;background:#0007;display:grid;place-items:center;padding:24px}.qqModal{width:min(720px,calc(100vw - 40px));max-height:min(760px,calc(100vh - 40px));display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:14px;box-shadow:0 16px 60px #0007;overflow:hidden}.qqModalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 15px;border-bottom:1px solid var(--dsw-alias-border-l1)}.qqModalTitle{font-size:14px;font-weight:620}.qqModalBody{overflow:auto;padding:14px}.qqClose{border:0;background:transparent;color:inherit;font-size:18px;cursor:pointer}
253
+ .qqLog{font:11px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;padding:7px 0;border-bottom:1px solid var(--dsw-alias-border-l1);white-space:pre-wrap;word-break:break-word}.qqLogMeta{opacity:.65;margin-right:8px}.qqLog.warn{color:var(--dsw-alias-state-warn-primary)}.qqLog.error{color:var(--dsw-alias-state-error-primary)}
254
+ .qqFootButton{border:0;background:transparent;color:var(--dsw-alias-label-secondary);height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;gap:8px;padding:0 10px;cursor:pointer;font:inherit;font-size:12px}.qqFootButton:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.qqFootIcon{width:20px;height:20px;border-radius:6px;display:grid;place-items:center;font-size:10px;font-weight:700;border:1px solid var(--dsw-alias-border-l2)}
255
+ .qqChatToolbar{display:flex;gap:8px;margin-bottom:12px}.qqSearch{flex:1;font:inherit;background:var(--dsw-alias-bg-base);color:inherit;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:8px 10px}.qqChatSection{margin:14px 0 6px;font-size:11px;color:var(--dsw-alias-label-secondary)}.qqChatRow{width:100%;display:flex;align-items:center;gap:10px;padding:10px;border:0;background:transparent;color:inherit;border-radius:9px;text-align:left;cursor:pointer}.qqChatRow:hover{background:var(--dsw-alias-bg-layer-2)}.qqAvatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-size:11px;font-weight:650;flex:0 0 auto}.qqGrow{min-width:0;flex:1}.qqEllipsis{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.qqBadge{font-size:10px;padding:2px 5px;border-radius:5px;background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-secondary)}
256
+ .qqTranscript{display:flex;gap:9px;align-items:flex-start;margin:14px 0}.qqTranscript.out{flex-direction:row-reverse}.qqTranscriptBody{max-width:min(78%,680px)}.qqTranscript.out .qqTranscriptBody{text-align:right}.qqTranscriptMeta{font-size:11px;color:var(--dsw-alias-label-secondary);margin:0 3px 5px}.qqBubble{display:inline-block;text-align:left;white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.55;padding:9px 11px;border-radius:5px 13px 13px 13px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1)}.qqTranscript.out .qqBubble{border-radius:13px 5px 13px 13px;background:var(--dsw-static-deepseek-50,var(--dsw-alias-bg-layer-2))}.qqQuote{max-width:100%;margin:0 0 7px;padding:7px 10px 8px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-brand-primary);border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 72%,transparent);color:var(--dsw-alias-label-secondary);text-align:left;white-space:normal;word-break:break-word;overflow:hidden}.qqQuoteLabel{margin-bottom:3px;font-size:10px;line-height:14px;font-weight:650;color:var(--dsw-alias-brand-primary);letter-spacing:.02em}.qqQuoteText{font-size:12px;line-height:1.5;white-space:pre-wrap;display:-webkit-box;-webkit-line-clamp:8;-webkit-box-orient:vertical;overflow:hidden}.qqTranscript.out .qqQuote{border-left:1px solid var(--dsw-alias-border-l1);border-right:3px solid var(--dsw-alias-brand-primary)}
257
+ .qqHeaderButton{font:inherit;font-size:11px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border-radius:7px;padding:5px 8px;cursor:pointer}
258
+ .qqMemoryGrid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.qqMemoryCard{border:1px solid var(--dsw-alias-border-l1);border-radius:9px;overflow:hidden}.qqMemoryCard h4{font-size:11px;margin:0;padding:8px 9px;border-bottom:1px solid var(--dsw-alias-border-l1)}.qqMemoryDoc{padding:9px;font-size:11px;line-height:1.55;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;max-height:180px;overflow:auto}.qqMemberList{margin-top:12px}.qqMemberRow{display:flex;align-items:center;gap:8px;padding:8px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px}.qqMemberId{font:10px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-secondary)}
259
+ @media(max-width:720px){.qqSetting{flex-direction:column}.qqControl{width:100%;justify-content:flex-start}.qqMemoryGrid{grid-template-columns:1fr}.qqInput{width:100%}}
260
+
261
+ /* QQChat contributes layout only; controls, dialogs and selection surfaces are
262
+ DSH primitives. Message colors use the host token system directly. */
263
+ .qqs{display:block;width:100%;min-width:0}.qqsHead{width:100%}.qqSettingsGroup{display:block;width:100%;min-width:0;margin-top:22px}.qqSettingsGroup h3{margin:0 0 8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}.qqSettingsGroup .qqSetting{display:flex;width:100%;min-width:0;flex:0 0 auto;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.qqSettingsGroup .qqSetting:last-child{border-bottom:0}.qqSetting>div:first-child{flex:1 1 auto;min-width:0}.qqControl{flex:0 0 auto}.qqChoiceButtons{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.qqSettingsGroup select{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 8px}.qqCheckRow{display:flex;align-items:flex-start;gap:10px;cursor:pointer}.qqCheckRow input{flex:none;width:16px;height:16px;margin:2px 0 0;accent-color:var(--dsw-alias-button-primary-fill);cursor:pointer}.qqConnect{display:flex;flex-direction:column;align-items:center;gap:14px;padding:24px 0;text-align:center}.qqTranscript{margin:16px 0}.qqTranscriptBody{max-width:min(78%,680px)}.qqBubble{border:0;border-radius:18px;padding:9px 13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.qqTranscript.out .qqBubble{border-radius:18px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.qqTranscriptMeta{margin:0 4px 4px;font-size:12px;color:var(--dsw-alias-label-secondary)}.qqQuote{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}
264
+ .qqWideModal{width:min(760px,calc(100vw - 48px));max-height:calc(100vh - 48px);min-height:0;overflow:hidden}.qqModalContent{min-height:0;overflow:auto}.qqModalContent .qqMemoryGrid{min-width:0}.qqModalContent .qqLog{overflow-wrap:anywhere;word-break:break-word}
265
+ `.concat('.qqQuoteSender{margin-top:4px;font-size:10px;color:var(--dsw-alias-label-secondary)}.qqMediaList{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}.qqMediaCard{display:flex;align-items:center;gap:6px;max-width:100%;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);font-size:11px}.qqMediaKind{color:var(--dsw-alias-brand-primary);font-weight:650}.qqMediaPreview{width:42px;height:42px;object-fit:cover;border-radius:6px}.qqMediaName{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.qqMediaSize,.qqMediaQuoted{color:var(--dsw-alias-label-secondary)}');
266
+ const qqThemeOverrides = '.qqTranscript.out .qqBubble{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}';
267
+ function installStyles() {
268
+ if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`))
269
+ return () => { };
270
+ const node = document.createElement('style');
271
+ node.dataset.pluginCss = STYLE_ID;
272
+ node.textContent = css + qqThemeOverrides;
273
+ document.head.appendChild(node);
274
+ return () => node.remove();
275
+ }
276
+ async function call(rpc, endpoint, payload = {}) {
277
+ const result = await rpc.call(exports.CHANNEL, endpoint, payload);
278
+ if (!result.ok)
279
+ throw new Error(result.error?.message || 'QQ Chat 请求失败');
280
+ return result.value;
281
+ }
282
+ const short = (value) => value.length > 10 ? `…${value.slice(-10)}` : value;
283
+ exports.short = short;
284
+ const initials = (value) => String(value || '?').trim().slice(0, 2).toUpperCase();
285
+ exports.initials = initials;
286
+ const time = (value) => value ? new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
287
+ exports.time = time;
288
+ const dateTime = (value) => value ? new Date(value).toLocaleString() : '';
289
+ exports.dateTime = dateTime;
290
+ function Modal({ title, onClose, children }) {
291
+ return (0, exports.h)(DshModal, { open: true, title, onClose, closeLabel: '关闭', className: 'qqWideModal', contentClassName: 'qqModalContent' }, children);
292
+ }
293
+
294
+ },
295
+ "./workspace.cjs": function(module, exports, require) {
296
+ "use strict";
297
+ Object.defineProperty(exports, "__esModule", { value: true });
298
+ exports.QQTranscriptNode = QQTranscriptNode;
299
+ exports.MemoryCard = MemoryCard;
300
+ exports.QQSessionUtility = QQSessionUtility;
301
+ exports.createQQMessageDefinition = createQQMessageDefinition;
302
+ const shared_cjs_1 = require("./shared.cjs");
303
+ function formatBytes(value) {
304
+ if (!value || value < 1024)
305
+ return `${value || 0} B`;
306
+ if (value < 1024 * 1024)
307
+ return `${(value / 1024).toFixed(1)} KB`;
308
+ return `${(value / 1024 / 1024).toFixed(1)} MB`;
309
+ }
310
+ function AttachmentCards({ attachments, sessionId }) {
311
+ if (!attachments?.length)
312
+ return null;
313
+ const rpc = (0, shared_cjs_1.getQQRpc)();
314
+ const [previews, setPreviews] = (0, shared_cjs_1.useState)({});
315
+ (0, shared_cjs_1.useEffect)(() => {
316
+ if (!rpc || !sessionId)
317
+ return;
318
+ let cancelled = false;
319
+ void Promise.all(attachments.filter(item => item.kind === 'image').map(async (item) => {
320
+ try {
321
+ const result = await (0, shared_cjs_1.call)(rpc, 'attachment/read', { sessionId, attachmentId: item.id });
322
+ return result.dataUrl ? [item.id, result.dataUrl] : null;
323
+ }
324
+ catch {
325
+ return null;
326
+ }
327
+ })).then(items => {
328
+ if (cancelled)
329
+ return;
330
+ setPreviews(current => ({ ...current, ...Object.fromEntries(items.filter((item) => Boolean(item))) }));
331
+ });
332
+ return () => { cancelled = true; };
333
+ }, [attachments, rpc, sessionId]);
334
+ return (0, shared_cjs_1.h)('div', { className: 'qqMediaList' }, ...attachments.map(attachment => (0, shared_cjs_1.h)('div', { className: 'qqMediaCard', key: `${attachment.id}:${attachment.quoted ? 'quote' : 'own'}` }, (0, shared_cjs_1.h)('span', { className: 'qqMediaKind' }, attachment.kind === 'image' ? '图片' : attachment.kind === 'video' ? '视频' : attachment.kind === 'voice' || attachment.kind === 'audio' ? '语音' : '文件'), previews[attachment.id] ? (0, shared_cjs_1.h)('img', { className: 'qqMediaPreview', src: previews[attachment.id], alt: attachment.filename }) : null, (0, shared_cjs_1.h)('span', { className: 'qqMediaName' }, attachment.filename), (0, shared_cjs_1.h)('span', { className: 'qqMediaSize' }, formatBytes(attachment.sizeBytes)), attachment.quoted ? (0, shared_cjs_1.h)('span', { className: 'qqMediaQuoted' }, '引用') : null)));
335
+ }
336
+ function QQTranscriptNode({ node }) {
337
+ const data = node.data;
338
+ const outbound = data.direction === 'outbound' || data.isOwner === true;
339
+ const hasQuote = Boolean(data.quote) || Boolean(data.quotedText);
340
+ const quoteText = data.quote?.text || data.quotedText || '';
341
+ return (0, shared_cjs_1.h)('div', { className: `qqTranscript${outbound ? ' out' : ''}` }, (0, shared_cjs_1.h)('div', { className: 'qqTranscriptBody' }, !data.isOwner && (0, shared_cjs_1.h)('div', { className: 'qqTranscriptMeta' }, data.senderName || (outbound ? 'Owner' : 'QQ 用户'), ' · ', (0, shared_cjs_1.time)(data.createdAt)), hasQuote && (0, shared_cjs_1.h)('div', { className: 'qqQuote', title: quoteText }, (0, shared_cjs_1.h)('div', { className: 'qqQuoteLabel' }, '引用消息'), quoteText ? (0, shared_cjs_1.h)('div', { className: 'qqQuoteText' }, quoteText) : null, data.quote?.senderName ? (0, shared_cjs_1.h)('div', { className: 'qqQuoteSender' }, data.quote.senderName) : null, (0, shared_cjs_1.h)(AttachmentCards, { attachments: data.quote?.attachments, sessionId: data.sessionId })), (0, shared_cjs_1.h)('div', { className: 'qqBubble' }, data.content), (0, shared_cjs_1.h)(AttachmentCards, { attachments: data.attachments, sessionId: data.sessionId })));
342
+ }
343
+ function MemoryCard({ title, value }) {
344
+ return (0, shared_cjs_1.h)('section', { className: 'qqMemoryCard' }, (0, shared_cjs_1.h)('h4', null, title), (0, shared_cjs_1.h)('div', { className: 'qqMemoryDoc' }, value || '还没有形成这部分记忆。'));
345
+ }
346
+ function MemberMemory({ member, onBack }) {
347
+ return (0, shared_cjs_1.h)(shared_cjs_1.React.Fragment, null, (0, shared_cjs_1.h)('div', { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 } }, (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: onBack }, '返回群记忆'), (0, shared_cjs_1.h)('div', { className: 'qqGrow' }, (0, shared_cjs_1.h)('div', { className: 'qqsTitle' }, member.displayName || '群友'), (0, shared_cjs_1.h)('div', { className: 'qqMemberId' }, member.platformUserId))), (0, shared_cjs_1.h)('div', { className: 'qqMemoryGrid' }, (0, shared_cjs_1.h)(MemoryCard, { title: '成员画像 · profile', value: member.memory.profile }), (0, shared_cjs_1.h)(MemoryCard, { title: '行为模式 · pattern', value: member.memory.pattern }), (0, shared_cjs_1.h)(MemoryCard, { title: '成员摘要 · summary', value: member.memory.summary }), (0, shared_cjs_1.h)(MemoryCard, { title: '成员长期记忆 · memory', value: member.memory.memory })));
348
+ }
349
+ function QQSessionUtility({ sessionId, rpc }) {
350
+ const [info, setInfo] = (0, shared_cjs_1.useState)(null);
351
+ const [open, setOpen] = (0, shared_cjs_1.useState)(false);
352
+ const [selectedMember, setSelectedMember] = (0, shared_cjs_1.useState)(null);
353
+ const isQQSession = String(sessionId).startsWith('qqchat-');
354
+ if (!isQQSession)
355
+ return null;
356
+ const show = async () => {
357
+ try {
358
+ setInfo(await (0, shared_cjs_1.call)(rpc, 'chat/info', { sessionId }));
359
+ setSelectedMember(null);
360
+ setOpen(true);
361
+ }
362
+ catch {
363
+ // Host may still be reconciling a just-created Session.
364
+ }
365
+ };
366
+ const memoryBody = info
367
+ ? selectedMember
368
+ ? (0, shared_cjs_1.h)(MemberMemory, { member: selectedMember, onBack: () => setSelectedMember(null) })
369
+ : (0, shared_cjs_1.h)(shared_cjs_1.React.Fragment, null, (0, shared_cjs_1.h)('div', { className: 'qqMemoryGrid' }, (0, shared_cjs_1.h)(MemoryCard, { title: '画像 · profile', value: info.memory.profile }), (0, shared_cjs_1.h)(MemoryCard, { title: '摘要 · summary', value: info.memory.summary }), info.chatType === 'group'
370
+ ? (0, shared_cjs_1.h)(MemoryCard, { title: '长期记忆 · memory', value: info.memory.memory })
371
+ : (0, shared_cjs_1.h)(MemoryCard, { title: '行为模式 · pattern', value: info.memory.pattern }), info.chatType === 'group'
372
+ ? (0, shared_cjs_1.h)(MemoryCard, { title: '近期沉淀 · daily', value: info.memory.daily })
373
+ : null), info.chatType === 'group'
374
+ ? (0, shared_cjs_1.h)('div', { className: 'qqMemberList' }, (0, shared_cjs_1.h)('div', { className: 'qqChatSection' }, `群友 · ${info.members.length} · 点击查看成员记忆`), ...info.members.map(member => (0, shared_cjs_1.h)('button', {
375
+ type: 'button',
376
+ key: member.id,
377
+ className: 'qqMemberRow',
378
+ onClick: () => setSelectedMember(member),
379
+ style: { width: '100%', border: 0, background: 'transparent', color: 'inherit', textAlign: 'left', cursor: 'pointer' },
380
+ }, (0, shared_cjs_1.h)('div', { className: 'qqGrow' }, (0, shared_cjs_1.h)('div', null, member.displayName || '群友'), member.aliases?.length ? (0, shared_cjs_1.h)('div', { className: 'qqMemberId' }, `历史昵称:${member.aliases.join('、')}`) : null, member.nicknames?.length ? (0, shared_cjs_1.h)('div', { className: 'qqMemberId' }, `群内称呼:${member.nicknames.join('、')}`) : null), (0, shared_cjs_1.h)('span', { className: 'qqBadge' }, '查看记忆'))))
381
+ : null)
382
+ : null;
383
+ return (0, shared_cjs_1.h)(shared_cjs_1.React.Fragment, null, (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => void show() }, 'QQ 记忆'), open && info
384
+ ? (0, shared_cjs_1.h)(shared_cjs_1.Modal, {
385
+ title: selectedMember
386
+ ? `${selectedMember.displayName || '群友'} · 成员记忆`
387
+ : info.chatType === 'group' ? `${info.title} · 群记忆` : `${info.title} · 用户记忆`,
388
+ onClose: () => { setOpen(false); setSelectedMember(null); },
389
+ }, memoryBody)
390
+ : null);
391
+ }
392
+ function createQQMessageDefinition() {
393
+ return {
394
+ kind: 'qqchat-message',
395
+ target: 'chat',
396
+ match: (event) => event.type === 'qqchat/message' && event.data
397
+ ? { id: event.data.messageId, role: 'start' }
398
+ : null,
399
+ start: (_context, match) => match.event.data,
400
+ update: (context) => context.state,
401
+ buildViewNode: (context) => {
402
+ if (!context.state)
403
+ return null;
404
+ const first = context.start ?? context.matches?.[0];
405
+ return {
406
+ key: context.key,
407
+ kind: 'qqchat-message',
408
+ id: context.id,
409
+ target: 'chat',
410
+ anchorSeq: first?.event.seq ?? 0,
411
+ location: first?.location ?? { kind: 'unresolved' },
412
+ visibility: 'visible',
413
+ data: context.state,
414
+ };
415
+ },
416
+ };
417
+ }
418
+
419
+ }
420
+ };
421
+ var __qqCache = Object.create(null);
422
+ function __qqRequire(id) {
423
+ var factory = __qqModules[id];
424
+ if (!factory) return require(id);
425
+ var cached = __qqCache[id];
426
+ if (cached) return cached.exports;
427
+ var module = { exports: {} };
428
+ __qqCache[id] = module;
429
+ factory(module, module.exports, __qqRequire);
430
+ return module.exports;
431
+ }
432
+ return __qqRequire("./plugin.cjs");
433
+ } })
@@ -0,0 +1,16 @@
1
+ import type { Agent } from '@deepseek-ai/dsh-agent';
2
+ import type { CommandRuntime } from '@deepseek-ai/dsh-commands';
3
+ export interface QQCommandReply {
4
+ handled: boolean;
5
+ text?: string;
6
+ }
7
+ export interface QQCommandScope {
8
+ chatType: 'group' | 'c2c';
9
+ isOwner: boolean;
10
+ }
11
+ /** Normalize a QQ command that may be prefixed by the bot mention. */
12
+ export declare function qqCommandText(text: string, mentioned: boolean): string | undefined;
13
+ /** Return the command name from a complete slash-command line. */
14
+ export declare function slashCommandName(input: string): string | undefined;
15
+ /** Dispatch one QQ slash command through DSH's native command registry. */
16
+ export declare function dispatchQQCommand(commands: CommandRuntime, agent: Agent, input: string, scope?: QQCommandScope): Promise<QQCommandReply>;
@@ -0,0 +1,48 @@
1
+ const READ_ONLY_GROUP_COMMANDS = new Set([
2
+ 'help', 'qqhelp', 'qqcommands', 'qqping', 'qqversion', 'qqstatus',
3
+ ]);
4
+ /** Normalize a QQ command that may be prefixed by the bot mention. */
5
+ export function qqCommandText(text, mentioned) {
6
+ const input = text.trim();
7
+ if (/^\/[a-z][a-z0-9_-]*(?:\s|$)/u.test(input))
8
+ return input;
9
+ if (!mentioned)
10
+ return undefined;
11
+ const prefixed = /^(?:<@!?[^>]+>|@[^\s]+)\s*(\/[a-z][a-z0-9_-]*(?:\s|$).*)$/su.exec(input);
12
+ return prefixed?.[1]?.trim();
13
+ }
14
+ /** Return the command name from a complete slash-command line. */
15
+ export function slashCommandName(input) {
16
+ const match = /^\/([a-z][a-z0-9_-]*)(?=\s|$)/u.exec(input);
17
+ return match?.[1];
18
+ }
19
+ function renderCommandList(commands) {
20
+ if (commands.length === 0)
21
+ return '当前 DSH 没有可用命令。';
22
+ return ['可用 DSH 命令:', ...commands.map(command => {
23
+ const hint = command.input?.hint ? ` ${command.input.hint}` : '';
24
+ return `/${command.name}${hint} — ${command.description}`;
25
+ })].join('\n');
26
+ }
27
+ /** Dispatch one QQ slash command through DSH's native command registry. */
28
+ export async function dispatchQQCommand(commands, agent, input, scope = { chatType: 'c2c', isOwner: true }) {
29
+ if (!input.startsWith('/'))
30
+ return { handled: false };
31
+ const name = slashCommandName(input);
32
+ if (name === undefined)
33
+ return { handled: true, text: '命令格式无效。发送 /help 查看可用命令。' };
34
+ // Native commands bypass the tools/pre-execute seam. Keep group control
35
+ // commands owner-only so they cannot change or stop a shared group Session.
36
+ if (scope.chatType === 'group' && !scope.isOwner && !READ_ONLY_GROUP_COMMANDS.has(name)) {
37
+ return { handled: true, text: '普通群成员只能使用查询类命令。' };
38
+ }
39
+ // QQ has no browser download channel for the Web-only command.
40
+ if (name === 'export' && commands.find(agent, name) !== undefined) {
41
+ return { handled: true, text: '/export 仅支持 DSH Web 下载会话日志,QQ 中暂不支持。' };
42
+ }
43
+ const execution = await commands.execute(agent, input, [], new AbortController().signal);
44
+ if (execution === undefined) {
45
+ return { handled: true, text: `未知或格式无效的命令:/${name}\n\n${renderCommandList(commands.list(agent))}` };
46
+ }
47
+ return { handled: true, text: execution.result.text?.trim() || `/${name} 已执行。` };
48
+ }
@@ -0,0 +1,3 @@
1
+ import type { QQChatConfig, QQChatConfigInput, QQChatRuntimeSettings } from './types.js';
2
+ export declare function resolveConfig(input?: QQChatConfigInput): QQChatConfig;
3
+ export declare function defaultRuntimeSettings(config: QQChatConfig): QQChatRuntimeSettings;
package/lib/config.js ADDED
@@ -0,0 +1,63 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ const DEFAULT_CONFIG = Object.freeze({
4
+ source: 'dsh-qqchat',
5
+ sandbox: false,
6
+ groupChatEnabled: true,
7
+ groupRequiresAt: true,
8
+ groupReadEnabled: true,
9
+ replyFormat: 'smart',
10
+ recentGroupMessages: 40,
11
+ reflectionIdleMs: 120_000,
12
+ reflectionBatchSize: 20,
13
+ reflectionMaxMessages: 80,
14
+ memoryMaxTokens: 1400,
15
+ memoryCompressionMaxTokens: 15000,
16
+ });
17
+ export function resolveConfig(input = {}) {
18
+ const home = process.env.DSH_HOME || join(homedir(), '.dsh');
19
+ const replyFormat = ['smart', 'markdown', 'compat'].includes(input.replyFormat ?? '')
20
+ ? input.replyFormat
21
+ : DEFAULT_CONFIG.replyFormat;
22
+ return Object.freeze({
23
+ dataDir: resolve(input.dataDir || join(home, 'plugins', 'dsh-qqchat')),
24
+ source: input.source || DEFAULT_CONFIG.source,
25
+ sandbox: input.sandbox === true,
26
+ agentPreset: input.agentPreset,
27
+ provider: input.provider,
28
+ model: input.model,
29
+ maxTokens: positiveIntOrUndefined(input.maxTokens),
30
+ groupChatEnabled: input.groupChatEnabled !== false,
31
+ groupRequiresAt: input.groupRequiresAt !== false,
32
+ groupReadEnabled: input.groupReadEnabled !== false,
33
+ replyFormat,
34
+ recentGroupMessages: positiveInt(input.recentGroupMessages, DEFAULT_CONFIG.recentGroupMessages),
35
+ reflectionIdleMs: positiveInt(input.reflectionIdleMs, DEFAULT_CONFIG.reflectionIdleMs),
36
+ reflectionBatchSize: positiveInt(input.reflectionBatchSize, DEFAULT_CONFIG.reflectionBatchSize),
37
+ reflectionMaxMessages: positiveInt(input.reflectionMaxMessages, DEFAULT_CONFIG.reflectionMaxMessages),
38
+ memoryMaxTokens: positiveInt(input.memoryMaxTokens, DEFAULT_CONFIG.memoryMaxTokens),
39
+ memoryCompressionMaxTokens: positiveInt(input.memoryCompressionMaxTokens, DEFAULT_CONFIG.memoryCompressionMaxTokens),
40
+ });
41
+ }
42
+ export function defaultRuntimeSettings(config) {
43
+ const groupReceiveMode = !config.groupChatEnabled
44
+ ? 'silent'
45
+ : config.groupRequiresAt ? 'mention' : 'auto';
46
+ return {
47
+ memoryEnabled: true,
48
+ groupReceiveMode,
49
+ groupReplyFormat: 'compat',
50
+ directReplyFormat: config.replyFormat,
51
+ directStreamingEnabled: config.replyFormat !== 'compat',
52
+ groupMembersCanUseTools: false,
53
+ groupMembersCanReceiveMedia: true,
54
+ groupMembersCanReadMedia: false,
55
+ ownerUserId: '',
56
+ };
57
+ }
58
+ function positiveInt(value, fallback) {
59
+ return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : fallback;
60
+ }
61
+ function positiveIntOrUndefined(value) {
62
+ return Number.isSafeInteger(value) && Number(value) > 0 ? Number(value) : undefined;
63
+ }
@@ -0,0 +1,30 @@
1
+ import type { QQChatDatabase } from '../storage/db.js';
2
+ import type { AccountRow, QQChatConfig, QQSendOptions, ReplyFormat } from '../types.js';
3
+ export interface QQPrivateTextStream {
4
+ push(delta: string): void;
5
+ finish(text: string): Promise<void>;
6
+ hasSent(): boolean;
7
+ }
8
+ export declare class QQApiClient {
9
+ private readonly db;
10
+ readonly config: QQChatConfig;
11
+ private readonly tokens;
12
+ private readonly sequences;
13
+ private streamSequence;
14
+ constructor(db: QQChatDatabase, config: QQChatConfig);
15
+ clearToken(accountId: number): void;
16
+ token(account: AccountRow): Promise<string>;
17
+ gatewayUrl(account: AccountRow): Promise<string>;
18
+ private nextSeq;
19
+ /**
20
+ * QQ stream_messages requires a fresh msg_seq for each stream session.
21
+ * It is intentionally not scoped by the inbound message id: using 1 for
22
+ * every reply causes QQ to reject later streams as duplicated messages.
23
+ */
24
+ private nextStreamSeq;
25
+ private request;
26
+ sendText(account: AccountRow, targetId: string, text: string, { group, messageId, format }?: QQSendOptions): Promise<Record<string, unknown>>;
27
+ sendReplyWithActiveFallback(account: AccountRow, targetId: string, text: string, options: QQSendOptions): Promise<Record<string, unknown>>;
28
+ createPrivateTextStream(account: AccountRow, targetId: string, { messageId, format }?: QQSendOptions): QQPrivateTextStream;
29
+ }
30
+ export declare function isReplyFormat(value: string): value is ReplyFormat;