dsh-qqchat 0.2.3 → 0.2.4
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.en.md +1 -1
- package/README.md +1 -1
- package/client-src/plugin.cts +2 -1
- package/client-src/shared.cts +41 -0
- package/lib/client.js +45 -0
- package/lib/index.js +4 -1
- package/lib/session/agent-bridge.d.ts +4 -0
- package/lib/session/agent-bridge.js +35 -0
- package/package.json +1 -1
- package/src/index.ts +4 -1
- package/src/session/agent-bridge.ts +38 -1
package/README.en.md
CHANGED
|
@@ -220,7 +220,7 @@ QQ Sessions use DSH's native Agent Preset mechanism. The preset configured for t
|
|
|
220
220
|
## Notes
|
|
221
221
|
|
|
222
222
|
- An official QQ Bot is required; personal QQ account login is not supported.
|
|
223
|
-
- `0.2.
|
|
223
|
+
- `0.2.2` is still being refined; test with a dedicated Bot and group first.
|
|
224
224
|
- Rich media, full QQ emoji handling and some complex message formats may require compatibility mode.
|
|
225
225
|
- Uninstalling the plugin does not automatically delete saved chat or memory data.
|
|
226
226
|
|
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ QQ Session 使用 DSH 原生的 Agent Preset 机制。插件配置中的 preset
|
|
|
202
202
|
## 注意事项
|
|
203
203
|
|
|
204
204
|
- 需要 QQ 官方 Bot,不支持个人 QQ 账号登录。
|
|
205
|
-
- `0.2.
|
|
205
|
+
- `0.2.2` 仍在持续完善,建议先在测试 Bot 和测试群中使用。
|
|
206
206
|
- 富媒体、完整 QQ 表情和部分复杂消息格式可能需要使用兼容模式。
|
|
207
207
|
- 卸载插件不会自动删除已经保存的聊天和记忆数据。
|
|
208
208
|
|
package/client-src/plugin.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installStyles, setQQRpc } from './shared.cjs'
|
|
1
|
+
import { installQQSettingsIcon, installStyles, setQQRpc } from './shared.cjs'
|
|
2
2
|
import type { Ctx } from './shared.cjs'
|
|
3
3
|
import { QQSettings } from './settings.cjs'
|
|
4
4
|
import { QQSessionUtility, QQTranscriptNode, createQQMessageDefinition } from './workspace.cjs'
|
|
@@ -7,6 +7,7 @@ exports.inject = ['slots', 'connection', 'conversationEvents'] as const
|
|
|
7
7
|
exports.apply = function apply(ctx: Ctx) {
|
|
8
8
|
setQQRpc(ctx.connection.rpc)
|
|
9
9
|
ctx.effect(installStyles, 'dsh-qqchat: client styles')
|
|
10
|
+
ctx.effect(installQQSettingsIcon, 'dsh-qqchat: QQ settings icon')
|
|
10
11
|
ctx.conversationEvents.register(createQQMessageDefinition())
|
|
11
12
|
|
|
12
13
|
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, QQSettings))
|
package/client-src/shared.cts
CHANGED
|
@@ -55,6 +55,47 @@ export interface SidebarActionProps { wide: boolean; rpc: Rpc; sessions: Session
|
|
|
55
55
|
export const CHANNEL = '/qqchat'
|
|
56
56
|
const STYLE_ID = 'dsh-qqchat/client-ui-v2'
|
|
57
57
|
|
|
58
|
+
const QQ_ICON_PATH = 'M17.5359 12.5144L16.8402 10.7175C16.8408 10.6968 16.8494 10.3429 16.8494 10.1604C16.8494 7.08792 15.448 4.0003 12.0012 4C8.55459 4.0003 7.15292 7.08792 7.15292 10.1604C7.15292 10.3429 7.16151 10.6968 7.16209 10.7175L6.4667 12.5144C6.27608 13.0285 6.08776 13.564 5.94988 14.0232C5.29262 16.2126 5.50559 17.1186 5.66783 17.139C6.01581 17.1823 7.02221 15.4908 7.02221 15.4908C7.02221 16.4704 7.5095 17.7487 8.56405 18.6719C8.16963 18.7976 7.68635 18.9911 7.37564 19.2284C7.09645 19.442 7.13142 19.6594 7.18158 19.7473C7.40258 20.1329 10.9713 19.9935 12.0017 19.8733C13.0319 19.9935 16.6009 20.1329 16.8216 19.7473C16.872 19.6594 16.9067 19.442 16.6275 19.2284C16.3168 18.9911 15.8333 18.7976 15.4386 18.6716C16.4928 17.7487 16.9801 16.4704 16.9801 15.4908C16.9801 15.4908 17.9868 17.1823 18.3348 17.139C18.4967 17.1186 18.7131 16.2108 18.0524 14.0232C17.9125 13.56 17.7265 13.0285 17.5359 12.5144ZM18.5574 20.7407C18.1843 21.3926 17.7237 21.6334 17.1187 21.7981C16.8792 21.8633 16.621 21.9056 16.325 21.936C15.8844 21.9814 15.3392 22.001 14.712 22C13.786 21.9985 12.693 21.9491 12.0017 21.884C11.3103 21.9491 10.2173 21.9985 9.29129 22C8.66414 22.001 8.11889 21.9814 7.67832 21.936C7.38236 21.9056 7.12409 21.8633 6.88467 21.7981C6.27994 21.6335 5.81954 21.393 5.44496 20.7393C5.15165 20.2258 5.07747 19.6406 5.20612 19.0866C4.61376 18.9546 4.20483 18.6045 3.92733 18.1757C3.77911 17.9466 3.68408 17.7127 3.61845 17.4663C3.53001 17.1344 3.49486 16.7666 3.50184 16.3601C3.51532 15.5749 3.68902 14.5984 4.03435 13.4481C4.17427 12.9821 4.3614 12.4396 4.6015 11.7926L5.15467 10.3632C5.1536 10.287 5.15292 10.2154 5.15292 10.1604C5.15292 5.6047 7.58875 2.00038 12.0013 2C16.4138 2.00038 18.8494 5.60454 18.8494 10.1604C18.8494 10.2154 18.8487 10.2869 18.8477 10.3631L19.401 11.7923L19.4112 11.8191C19.636 12.4254 19.8242 12.9722 19.967 13.445C20.3145 14.5956 20.4889 15.5735 20.5018 16.361C20.5085 16.768 20.4728 17.1365 20.3837 17.4689C20.3178 17.7148 20.2228 17.9483 20.0746 18.1768C19.7976 18.6041 19.3905 18.9532 18.7974 19.0862C18.9266 19.6411 18.8523 20.2274 18.5574 20.7407Z'
|
|
59
|
+
|
|
60
|
+
/** Temporarily replaces DSH's fallback settings glyph for the QQ Chat row. */
|
|
61
|
+
export function installQQSettingsIcon(): () => void {
|
|
62
|
+
const originals = new Map<HTMLButtonElement, SVGElement>()
|
|
63
|
+
const apply = () => {
|
|
64
|
+
for (const button of Array.from(document.querySelectorAll('button'))) {
|
|
65
|
+
const label = button.querySelector('span[class$="_navLabel"]')
|
|
66
|
+
if (label?.textContent?.trim() !== 'QQ Chat') continue
|
|
67
|
+
if (button.querySelector('[data-dsh-qqchat-icon]')) continue
|
|
68
|
+
const icon = button.querySelector('svg[class$="_navIcon"]')
|
|
69
|
+
if (!icon) continue
|
|
70
|
+
originals.set(button, icon.cloneNode(true) as SVGElement)
|
|
71
|
+
icon.remove()
|
|
72
|
+
const replacement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
73
|
+
replacement.setAttribute('width', '16')
|
|
74
|
+
replacement.setAttribute('height', '16')
|
|
75
|
+
replacement.setAttribute('viewBox', '0 0 24 24')
|
|
76
|
+
replacement.setAttribute('fill', 'currentColor')
|
|
77
|
+
replacement.setAttribute('aria-hidden', 'true')
|
|
78
|
+
replacement.setAttribute('data-dsh-qqchat-icon', 'true')
|
|
79
|
+
replacement.classList.add('dsh-qqchat-nav-icon')
|
|
80
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
81
|
+
path.setAttribute('d', QQ_ICON_PATH)
|
|
82
|
+
replacement.append(path)
|
|
83
|
+
button.insertBefore(replacement, label)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const observer = new MutationObserver(apply)
|
|
87
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
88
|
+
apply()
|
|
89
|
+
return () => {
|
|
90
|
+
observer.disconnect()
|
|
91
|
+
for (const [button, original] of originals) {
|
|
92
|
+
const replacement = button.querySelector('[data-dsh-qqchat-icon]')
|
|
93
|
+
if (replacement) replacement.replaceWith(original)
|
|
94
|
+
}
|
|
95
|
+
originals.clear()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
58
99
|
const css = `
|
|
59
100
|
.qqs{display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary);font:inherit}.qqs *{box-sizing:border-box}
|
|
60
101
|
.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)}
|
package/lib/client.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.inject = ['slots', 'connection', 'conversationEvents'];
|
|
|
10
10
|
exports.apply = function apply(ctx) {
|
|
11
11
|
(0, shared_cjs_1.setQQRpc)(ctx.connection.rpc);
|
|
12
12
|
ctx.effect(shared_cjs_1.installStyles, 'dsh-qqchat: client styles');
|
|
13
|
+
ctx.effect(shared_cjs_1.installQQSettingsIcon, 'dsh-qqchat: QQ settings icon');
|
|
13
14
|
ctx.conversationEvents.register((0, workspace_cjs_1.createQQMessageDefinition)());
|
|
14
15
|
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
16
|
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, workspace_cjs_1.QQTranscriptNode));
|
|
@@ -249,6 +250,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
249
250
|
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;
|
|
250
251
|
exports.setQQRpc = setQQRpc;
|
|
251
252
|
exports.getQQRpc = getQQRpc;
|
|
253
|
+
exports.installQQSettingsIcon = installQQSettingsIcon;
|
|
252
254
|
exports.installStyles = installStyles;
|
|
253
255
|
exports.call = call;
|
|
254
256
|
exports.Modal = Modal;
|
|
@@ -265,6 +267,49 @@ function setQQRpc(rpc) { qqRpc = rpc; }
|
|
|
265
267
|
function getQQRpc() { return qqRpc; }
|
|
266
268
|
exports.CHANNEL = '/qqchat';
|
|
267
269
|
const STYLE_ID = 'dsh-qqchat/client-ui-v2';
|
|
270
|
+
const QQ_ICON_PATH = 'M17.5359 12.5144L16.8402 10.7175C16.8408 10.6968 16.8494 10.3429 16.8494 10.1604C16.8494 7.08792 15.448 4.0003 12.0012 4C8.55459 4.0003 7.15292 7.08792 7.15292 10.1604C7.15292 10.3429 7.16151 10.6968 7.16209 10.7175L6.4667 12.5144C6.27608 13.0285 6.08776 13.564 5.94988 14.0232C5.29262 16.2126 5.50559 17.1186 5.66783 17.139C6.01581 17.1823 7.02221 15.4908 7.02221 15.4908C7.02221 16.4704 7.5095 17.7487 8.56405 18.6719C8.16963 18.7976 7.68635 18.9911 7.37564 19.2284C7.09645 19.442 7.13142 19.6594 7.18158 19.7473C7.40258 20.1329 10.9713 19.9935 12.0017 19.8733C13.0319 19.9935 16.6009 20.1329 16.8216 19.7473C16.872 19.6594 16.9067 19.442 16.6275 19.2284C16.3168 18.9911 15.8333 18.7976 15.4386 18.6716C16.4928 17.7487 16.9801 16.4704 16.9801 15.4908C16.9801 15.4908 17.9868 17.1823 18.3348 17.139C18.4967 17.1186 18.7131 16.2108 18.0524 14.0232C17.9125 13.56 17.7265 13.0285 17.5359 12.5144ZM18.5574 20.7407C18.1843 21.3926 17.7237 21.6334 17.1187 21.7981C16.8792 21.8633 16.621 21.9056 16.325 21.936C15.8844 21.9814 15.3392 22.001 14.712 22C13.786 21.9985 12.693 21.9491 12.0017 21.884C11.3103 21.9491 10.2173 21.9985 9.29129 22C8.66414 22.001 8.11889 21.9814 7.67832 21.936C7.38236 21.9056 7.12409 21.8633 6.88467 21.7981C6.27994 21.6335 5.81954 21.393 5.44496 20.7393C5.15165 20.2258 5.07747 19.6406 5.20612 19.0866C4.61376 18.9546 4.20483 18.6045 3.92733 18.1757C3.77911 17.9466 3.68408 17.7127 3.61845 17.4663C3.53001 17.1344 3.49486 16.7666 3.50184 16.3601C3.51532 15.5749 3.68902 14.5984 4.03435 13.4481C4.17427 12.9821 4.3614 12.4396 4.6015 11.7926L5.15467 10.3632C5.1536 10.287 5.15292 10.2154 5.15292 10.1604C5.15292 5.6047 7.58875 2.00038 12.0013 2C16.4138 2.00038 18.8494 5.60454 18.8494 10.1604C18.8494 10.2154 18.8487 10.2869 18.8477 10.3631L19.401 11.7923L19.4112 11.8191C19.636 12.4254 19.8242 12.9722 19.967 13.445C20.3145 14.5956 20.4889 15.5735 20.5018 16.361C20.5085 16.768 20.4728 17.1365 20.3837 17.4689C20.3178 17.7148 20.2228 17.9483 20.0746 18.1768C19.7976 18.6041 19.3905 18.9532 18.7974 19.0862C18.9266 19.6411 18.8523 20.2274 18.5574 20.7407Z';
|
|
271
|
+
/** Temporarily replaces DSH's fallback settings glyph for the QQ Chat row. */
|
|
272
|
+
function installQQSettingsIcon() {
|
|
273
|
+
const originals = new Map();
|
|
274
|
+
const apply = () => {
|
|
275
|
+
for (const button of Array.from(document.querySelectorAll('button'))) {
|
|
276
|
+
const label = button.querySelector('span[class$="_navLabel"]');
|
|
277
|
+
if (label?.textContent?.trim() !== 'QQ Chat')
|
|
278
|
+
continue;
|
|
279
|
+
if (button.querySelector('[data-dsh-qqchat-icon]'))
|
|
280
|
+
continue;
|
|
281
|
+
const icon = button.querySelector('svg[class$="_navIcon"]');
|
|
282
|
+
if (!icon)
|
|
283
|
+
continue;
|
|
284
|
+
originals.set(button, icon.cloneNode(true));
|
|
285
|
+
icon.remove();
|
|
286
|
+
const replacement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
287
|
+
replacement.setAttribute('width', '16');
|
|
288
|
+
replacement.setAttribute('height', '16');
|
|
289
|
+
replacement.setAttribute('viewBox', '0 0 24 24');
|
|
290
|
+
replacement.setAttribute('fill', 'currentColor');
|
|
291
|
+
replacement.setAttribute('aria-hidden', 'true');
|
|
292
|
+
replacement.setAttribute('data-dsh-qqchat-icon', 'true');
|
|
293
|
+
replacement.classList.add('dsh-qqchat-nav-icon');
|
|
294
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
295
|
+
path.setAttribute('d', QQ_ICON_PATH);
|
|
296
|
+
replacement.append(path);
|
|
297
|
+
button.insertBefore(replacement, label);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const observer = new MutationObserver(apply);
|
|
301
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
302
|
+
apply();
|
|
303
|
+
return () => {
|
|
304
|
+
observer.disconnect();
|
|
305
|
+
for (const [button, original] of originals) {
|
|
306
|
+
const replacement = button.querySelector('[data-dsh-qqchat-icon]');
|
|
307
|
+
if (replacement)
|
|
308
|
+
replacement.replaceWith(original);
|
|
309
|
+
}
|
|
310
|
+
originals.clear();
|
|
311
|
+
};
|
|
312
|
+
}
|
|
268
313
|
const css = `
|
|
269
314
|
.qqs{display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary);font:inherit}.qqs *{box-sizing:border-box}
|
|
270
315
|
.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)}
|
package/lib/index.js
CHANGED
|
@@ -31,7 +31,10 @@ export function apply(ctx, inputConfig = {}) {
|
|
|
31
31
|
const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media);
|
|
32
32
|
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime), { authority: 'loopback' });
|
|
33
33
|
ctx.effect(() => {
|
|
34
|
-
void
|
|
34
|
+
void (async () => {
|
|
35
|
+
await bridge.ensureWorkspaceMembership();
|
|
36
|
+
await runtime.start();
|
|
37
|
+
})().catch(error => logger.error?.(error));
|
|
35
38
|
return () => runtime.stop();
|
|
36
39
|
}, 'dsh-qqchat runtime');
|
|
37
40
|
}
|
|
@@ -22,6 +22,7 @@ export declare class DshQQBridge {
|
|
|
22
22
|
private readonly activeAttachments;
|
|
23
23
|
private readonly activeMediaReadable;
|
|
24
24
|
private readonly memorySnapshots;
|
|
25
|
+
private qqWorkspace?;
|
|
25
26
|
private readonly disposeEvent;
|
|
26
27
|
private readonly disposeToolGate;
|
|
27
28
|
private readonly disposeCommands;
|
|
@@ -30,12 +31,15 @@ export declare class DshQQBridge {
|
|
|
30
31
|
constructor(ctx: Context, db: QQChatDatabase, memory: MemoryEngine, config: QQChatConfig, logger?: LoggerLike);
|
|
31
32
|
dispose(): Promise<void>;
|
|
32
33
|
ensureChatSession(chatType: ChatType, row: GroupRow | MemberRow): Promise<string>;
|
|
34
|
+
ensureWorkspaceMembership(): Promise<void>;
|
|
33
35
|
recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession?: boolean): Promise<string | undefined>;
|
|
34
36
|
reply(message: QQNormalizedMessage, group: GroupRow | undefined, member: MemberRow, attachments?: StoredAttachmentSummary[], onTextDelta?: (delta: string) => void, isOwner?: boolean): Promise<string>;
|
|
35
37
|
private onSessionEvent;
|
|
36
38
|
private injectMemorySnapshot;
|
|
37
39
|
private restoreMemorySnapshot;
|
|
38
40
|
private ensureAgent;
|
|
41
|
+
/** Keep all QQ sessions together in one DSH Workspace using the host API. */
|
|
42
|
+
private attachToQQWorkspace;
|
|
39
43
|
private isArchived;
|
|
40
44
|
private rememberRoute;
|
|
41
45
|
private appendDisplayIfMissing;
|
|
@@ -33,6 +33,7 @@ export class DshQQBridge {
|
|
|
33
33
|
activeAttachments = new Map();
|
|
34
34
|
activeMediaReadable = new Map();
|
|
35
35
|
memorySnapshots = new Map();
|
|
36
|
+
qqWorkspace;
|
|
36
37
|
disposeEvent;
|
|
37
38
|
disposeToolGate;
|
|
38
39
|
disposeCommands;
|
|
@@ -97,6 +98,16 @@ export class DshQQBridge {
|
|
|
97
98
|
const { sessionId } = await this.ensureAgent(chatType, row);
|
|
98
99
|
return sessionId;
|
|
99
100
|
}
|
|
101
|
+
async ensureWorkspaceMembership() {
|
|
102
|
+
const sessionIds = [
|
|
103
|
+
...this.db.listGroups().map(row => row.dsh_session_id),
|
|
104
|
+
...this.db.listDirectChats().map(row => row.dsh_session_id),
|
|
105
|
+
];
|
|
106
|
+
for (const sessionId of sessionIds) {
|
|
107
|
+
if (sessionId)
|
|
108
|
+
await this.attachToQQWorkspace(sessionId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
100
111
|
async recordTranscript(event, row, createSession = false) {
|
|
101
112
|
const existing = this.db.getChatSession(event.chatType, Number(row.id));
|
|
102
113
|
if (!existing && !createSession)
|
|
@@ -274,6 +285,7 @@ export class DshQQBridge {
|
|
|
274
285
|
this.ensureSelection(live);
|
|
275
286
|
this.ensureTitle(live.session, chatType, row);
|
|
276
287
|
this.rememberRoute(chatType, row, live, sessionId);
|
|
288
|
+
await this.attachToQQWorkspace(sessionId);
|
|
277
289
|
return { agent: live, sessionId };
|
|
278
290
|
}
|
|
279
291
|
const resumed = await this.tryResume(sessionId);
|
|
@@ -281,6 +293,7 @@ export class DshQQBridge {
|
|
|
281
293
|
this.ensureSelection(resumed.agent);
|
|
282
294
|
this.ensureTitle(resumed.agent.session, chatType, row);
|
|
283
295
|
this.rememberRoute(chatType, row, resumed.agent, sessionId);
|
|
296
|
+
await this.attachToQQWorkspace(sessionId);
|
|
284
297
|
return { agent: resumed.agent, sessionId };
|
|
285
298
|
}
|
|
286
299
|
}
|
|
@@ -299,8 +312,30 @@ export class DshQQBridge {
|
|
|
299
312
|
this.db.setChatSession(chatType, Number(row.id), sessionId);
|
|
300
313
|
this.ensureTitle(handle.agent.session, chatType, row);
|
|
301
314
|
this.rememberRoute(chatType, row, handle.agent, sessionId);
|
|
315
|
+
await this.attachToQQWorkspace(sessionId);
|
|
302
316
|
return { agent: handle.agent, sessionId };
|
|
303
317
|
}
|
|
318
|
+
/** Keep all QQ sessions together in one DSH Workspace using the host API. */
|
|
319
|
+
async attachToQQWorkspace(sessionId) {
|
|
320
|
+
const registry = this.ctx.workspaceRegistry;
|
|
321
|
+
if (!registry)
|
|
322
|
+
return;
|
|
323
|
+
if (!this.qqWorkspace) {
|
|
324
|
+
this.qqWorkspace = registry.create(process.cwd(), 'QQ Chat').catch(error => {
|
|
325
|
+
this.logger.warn?.(`[dsh-qqchat] QQ Workspace 创建失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`);
|
|
326
|
+
return undefined;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const workspace = await this.qqWorkspace;
|
|
330
|
+
if (!workspace)
|
|
331
|
+
return;
|
|
332
|
+
try {
|
|
333
|
+
await workspace.attachSession(SessionId(sessionId));
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 加入 Workspace 失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
304
339
|
isArchived(sessionId) {
|
|
305
340
|
const registry = this.ctx.workspaceRegistry;
|
|
306
341
|
return registry?.archivedSessionIds.includes(sessionId) ?? false;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -40,7 +40,10 @@ export function apply(ctx: Context, inputConfig: QQChatConfigInput = {}): void {
|
|
|
40
40
|
|
|
41
41
|
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime), { authority: 'loopback' })
|
|
42
42
|
ctx.effect(() => {
|
|
43
|
-
void
|
|
43
|
+
void (async () => {
|
|
44
|
+
await bridge.ensureWorkspaceMembership()
|
|
45
|
+
await runtime.start()
|
|
46
|
+
})().catch(error => logger.error?.(error))
|
|
44
47
|
return () => runtime.stop()
|
|
45
48
|
}, 'dsh-qqchat runtime')
|
|
46
49
|
}
|
|
@@ -39,7 +39,11 @@ interface AgentPreset { id: string }
|
|
|
39
39
|
interface AgentPresetService { resolve(id?: string): Promise<AgentPreset>; list(): Promise<AgentPreset[]>; mount(ctx: Context, id: string): Promise<unknown>; recompose(ctx: Context, id: string): Promise<AgentPreset> }
|
|
40
40
|
interface SessionPersistenceService { inspect(id: SessionId): Promise<{ meta: Session['header']; events: readonly SessionEvent[] }> }
|
|
41
41
|
interface SessionTitleService { get(session: Session): unknown; rename(session: Session, title: string): unknown }
|
|
42
|
-
interface
|
|
42
|
+
interface WorkspaceEntity { attachSession(sessionId: SessionId): Promise<void> }
|
|
43
|
+
interface WorkspaceRegistryService {
|
|
44
|
+
archivedSessionIds: readonly string[]
|
|
45
|
+
create(path: string, title?: string): Promise<WorkspaceEntity>
|
|
46
|
+
}
|
|
43
47
|
interface Composition { presetId?: string; setup?: AgentSetup }
|
|
44
48
|
interface ActiveActor { chatType: ChatType; senderId: string }
|
|
45
49
|
|
|
@@ -62,6 +66,7 @@ export class DshQQBridge {
|
|
|
62
66
|
private readonly activeAttachments = new Map<string, Set<string>>()
|
|
63
67
|
private readonly activeMediaReadable = new Map<string, boolean>()
|
|
64
68
|
private readonly memorySnapshots = new Map<string, MemorySnapshotState>()
|
|
69
|
+
private qqWorkspace?: Promise<WorkspaceEntity | undefined>
|
|
65
70
|
private readonly disposeEvent: () => void
|
|
66
71
|
private readonly disposeToolGate: () => void
|
|
67
72
|
private readonly disposeCommands: () => void
|
|
@@ -122,6 +127,16 @@ export class DshQQBridge {
|
|
|
122
127
|
return sessionId
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
async ensureWorkspaceMembership(): Promise<void> {
|
|
131
|
+
const sessionIds = [
|
|
132
|
+
...this.db.listGroups().map(row => row.dsh_session_id),
|
|
133
|
+
...this.db.listDirectChats().map(row => row.dsh_session_id),
|
|
134
|
+
]
|
|
135
|
+
for (const sessionId of sessionIds) {
|
|
136
|
+
if (sessionId) await this.attachToQQWorkspace(sessionId)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
125
140
|
async recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession = false): Promise<string | undefined> {
|
|
126
141
|
const existing = this.db.getChatSession(event.chatType, Number(row.id))
|
|
127
142
|
if (!existing && !createSession) return undefined
|
|
@@ -291,6 +306,7 @@ export class DshQQBridge {
|
|
|
291
306
|
this.ensureSelection(live)
|
|
292
307
|
this.ensureTitle(live.session, chatType, row)
|
|
293
308
|
this.rememberRoute(chatType, row, live, sessionId)
|
|
309
|
+
await this.attachToQQWorkspace(sessionId)
|
|
294
310
|
return { agent: live, sessionId }
|
|
295
311
|
}
|
|
296
312
|
const resumed = await this.tryResume(sessionId)
|
|
@@ -298,6 +314,7 @@ export class DshQQBridge {
|
|
|
298
314
|
this.ensureSelection(resumed.agent)
|
|
299
315
|
this.ensureTitle(resumed.agent.session, chatType, row)
|
|
300
316
|
this.rememberRoute(chatType, row, resumed.agent, sessionId)
|
|
317
|
+
await this.attachToQQWorkspace(sessionId)
|
|
301
318
|
return { agent: resumed.agent, sessionId }
|
|
302
319
|
}
|
|
303
320
|
}
|
|
@@ -317,9 +334,29 @@ export class DshQQBridge {
|
|
|
317
334
|
this.db.setChatSession(chatType, Number(row.id), sessionId)
|
|
318
335
|
this.ensureTitle(handle.agent.session, chatType, row)
|
|
319
336
|
this.rememberRoute(chatType, row, handle.agent, sessionId)
|
|
337
|
+
await this.attachToQQWorkspace(sessionId)
|
|
320
338
|
return { agent: handle.agent, sessionId }
|
|
321
339
|
}
|
|
322
340
|
|
|
341
|
+
/** Keep all QQ sessions together in one DSH Workspace using the host API. */
|
|
342
|
+
private async attachToQQWorkspace(sessionId: string): Promise<void> {
|
|
343
|
+
const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
|
|
344
|
+
if (!registry) return
|
|
345
|
+
if (!this.qqWorkspace) {
|
|
346
|
+
this.qqWorkspace = registry.create(process.cwd(), 'QQ Chat').catch(error => {
|
|
347
|
+
this.logger.warn?.(`[dsh-qqchat] QQ Workspace 创建失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`)
|
|
348
|
+
return undefined
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
const workspace = await this.qqWorkspace
|
|
352
|
+
if (!workspace) return
|
|
353
|
+
try {
|
|
354
|
+
await workspace.attachSession(SessionId(sessionId))
|
|
355
|
+
} catch (error) {
|
|
356
|
+
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 加入 Workspace 失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
323
360
|
private isArchived(sessionId: string): boolean {
|
|
324
361
|
const registry = (this.ctx as unknown as { workspaceRegistry?: WorkspaceRegistryService }).workspaceRegistry
|
|
325
362
|
return registry?.archivedSessionIds.includes(sessionId) ?? false
|