@xmanrui/dsh-im 4.24.1 → 4.26.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/PROACTIVE_DELIVERY.en.md +12 -4
- package/PROACTIVE_DELIVERY.md +12 -4
- package/README.en.md +11 -3
- package/README.md +11 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/client.js +654 -311
- package/lib/index.js +298 -289
- package/package.json +7 -8
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/matrix/api.js +11 -0
- package/plugin-src/client/channels/matrix/index.js +151 -0
- package/plugin-src/client/channels/matrix/styles.js +36 -0
- package/plugin-src/client/global-settings.js +26 -14
- package/plugin-src/client/i18n.js +20 -0
- package/plugin-src/client/index.js +20 -0
- package/plugin-src/client/session-channel-logos.js +2 -1
- package/plugin-src/client/styles.js +19 -3
- package/plugin-src/client/update-panel.js +31 -11
- package/plugin-src/host/channels/matrix/index.mjs +31 -0
- package/plugin-src/host/channels/matrix/production.mjs +227 -0
- package/plugin-src/host/channels/matrix/rpc.mjs +228 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/startup-error.mjs +2 -1
- package/plugin-src/host/delivery-adapter.mjs +18 -0
- package/plugin-src/host/delivery-rpc.mjs +7 -2
- package/plugin-src/host/delivery-service.mjs +8 -2
- package/plugin-src/host/delivery-suggestions.mjs +13 -0
- package/plugin-src/host/index.mjs +3 -0
- package/scripts/verify-injected-context.mjs +120 -0
- package/scripts/verify-lan-management.mjs +1 -1
- package/scripts/verify-package.mjs +6 -1
- package/src/channels/feishu/feishu-runtime.mjs +13 -3
- package/src/channels/matrix/matrix-api.mjs +696 -0
- package/src/channels/matrix/matrix-bridge.mjs +20 -0
- package/src/channels/matrix/matrix-config-store.mjs +356 -0
- package/src/channels/matrix/matrix-controller.mjs +404 -0
- package/src/channels/matrix/matrix-crypto-store.mjs +279 -0
- package/src/channels/matrix/matrix-crypto.mjs +1014 -0
- package/src/channels/matrix/matrix-harness-client.mjs +11 -0
- package/src/channels/matrix/matrix-normalize.mjs +357 -0
- package/src/channels/matrix/matrix-rich-text.mjs +313 -0
- package/src/channels/matrix/matrix-runtime.mjs +900 -0
- package/src/channels/shared/command-catalog.mjs +1 -1
- package/src/channels/shared/i18n-en/image-input.mjs +1 -0
- package/src/channels/shared/i18n-en/matrix.mjs +75 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/injected-context.mjs +3 -3
- package/src/channels/shared/session-channel-labels.mjs +1 -0
- package/src/channels/shared/text-harness-bridge.mjs +3 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Verify the built Host hook against a built DSH's real JSONL persistence. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { createImHostPlugin } from '../lib/index.js';
|
|
10
|
+
import {
|
|
11
|
+
captureContextEnhancement, enhanceContextContent, INJECTED_CONTEXT_TAGS,
|
|
12
|
+
} from '../src/channels/shared/context-enhancement.mjs';
|
|
13
|
+
|
|
14
|
+
if (!process.argv[2]) throw new Error('Pass the path of a built DSH checkout.');
|
|
15
|
+
const harnessRoot = resolve(process.argv[2]);
|
|
16
|
+
const fromHarness = createRequire(resolve(harnessRoot, 'packages/session/session-persistence-jsonl/package.json'));
|
|
17
|
+
const load = (name) => import(pathToFileURL(fromHarness.resolve(name)).href);
|
|
18
|
+
const { Context } = await load('@deepseek-ai/cordis');
|
|
19
|
+
const { SESSION_FORMAT_VERSION } = await load('@deepseek-ai/dsh-session');
|
|
20
|
+
const { default: JsonlSessionPersistence } = await load('@deepseek-ai/dsh-session-persistence-jsonl');
|
|
21
|
+
assert.equal(SESSION_FORMAT_VERSION, 4, 'This regression verifies Session format V4.');
|
|
22
|
+
|
|
23
|
+
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-context-'));
|
|
24
|
+
const ctx = new Context();
|
|
25
|
+
const source = { channel: 'feishu', senderName: 'Context test' };
|
|
26
|
+
const guidance = 'Reply briefly.';
|
|
27
|
+
const reply = `${INJECTED_CONTEXT_TAGS.replyOpen}${JSON.stringify({
|
|
28
|
+
authorName: 'Quoted author', content: 'Quoted text',
|
|
29
|
+
})}${INJECTED_CONTEXT_TAGS.replyClose}`;
|
|
30
|
+
|
|
31
|
+
function enhanced(content, fields = ['channel', 'senderName'], instructions = '') {
|
|
32
|
+
const snapshot = captureContextEnhancement({
|
|
33
|
+
botId: 'context-test',
|
|
34
|
+
getSettings: () => ({
|
|
35
|
+
group: { enabled: true, fields, guidance: instructions },
|
|
36
|
+
direct: { enabled: false, fields: [], guidance: '' },
|
|
37
|
+
}),
|
|
38
|
+
}, 'group');
|
|
39
|
+
return enhanceContextContent(content, snapshot, () => source);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function turnEvents(messages, turn = 1, offset = 0) {
|
|
43
|
+
return [
|
|
44
|
+
{ type: 'turn/start', data: { turn } },
|
|
45
|
+
{ type: 'step/start', data: { turn, step: 1 } },
|
|
46
|
+
...messages.map(data => ({ type: 'user/message', data, surfaceOp: 'append' })),
|
|
47
|
+
{ type: 'step/end', data: { turn, step: 1 } },
|
|
48
|
+
{ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } },
|
|
49
|
+
].map((event, index) => ({ ...event, seq: offset + index, time: offset + index + 1 }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const cases = [
|
|
53
|
+
['plain', 'Question', ['user']],
|
|
54
|
+
['source', enhanced('Question'), ['user', 'notice']],
|
|
55
|
+
['guidance', enhanced('Question', [], guidance), ['user', 'instructions']],
|
|
56
|
+
['quote', `${reply}\n\nQuestion`, ['notice', 'user']],
|
|
57
|
+
['combined', enhanced(`${reply}\n\nQuestion`, ['channel'], guidance),
|
|
58
|
+
['notice', 'user', 'notice', 'instructions']],
|
|
59
|
+
['structured', enhanced([{ type: 'text', text: reply }, { type: 'text', text: 'Question' }]),
|
|
60
|
+
['notice', 'user', 'notice']],
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await ctx.plugin(JsonlSessionPersistence, { root: directory, compression: 'none' });
|
|
65
|
+
for (const [name, value] of Object.entries({
|
|
66
|
+
connection: {}, credentials: {}, typertGateway: { stream() {} },
|
|
67
|
+
sessionController: {}, workspaceController: {},
|
|
68
|
+
})) ctx.provide(name, value);
|
|
69
|
+
// Keep the real bundled context hook; unrelated channel connections stay idle.
|
|
70
|
+
const internals = Object.fromEntries([
|
|
71
|
+
'Feishu', 'Weixin', 'Dingtalk', 'Wecom', 'WecomApp', 'Qq', 'Slack',
|
|
72
|
+
'Telegram', 'Discord', 'Whatsapp', 'IMessage', 'Email', 'Matrix', 'Office',
|
|
73
|
+
].map(name => [`apply${name}`, async () => {}]));
|
|
74
|
+
const host = ctx.plugin(createImHostPlugin({
|
|
75
|
+
...internals,
|
|
76
|
+
installHostLanguage: () => {},
|
|
77
|
+
installSessionSyncCoordinator: () => {},
|
|
78
|
+
}));
|
|
79
|
+
await host.await();
|
|
80
|
+
|
|
81
|
+
for (const [id, content, expectedForms] of cases) {
|
|
82
|
+
const message = {
|
|
83
|
+
id: `${id}-user`, role: 'user',
|
|
84
|
+
content: typeof content === 'string' ? [{ type: 'text', text: content }] : content,
|
|
85
|
+
source: { kind: 'user', rpcId: `feishu-${id}` },
|
|
86
|
+
};
|
|
87
|
+
const decision = { kind: 'enter', messages: [message] };
|
|
88
|
+
const result = await ctx.waterfall('agent/pre-step', { agent: { session: { id } } },
|
|
89
|
+
async () => decision);
|
|
90
|
+
assert.deepEqual(result.messages.map(entry => entry.source.kind === 'user' ? 'user' : entry.source.form), expectedForms, id);
|
|
91
|
+
const human = result.messages.find(entry => entry.source.kind === 'user');
|
|
92
|
+
assert.equal(human.id, message.id);
|
|
93
|
+
assert.deepEqual(human.source, message.source);
|
|
94
|
+
assert.deepEqual(human.content, [{ type: 'text', text: 'Question' }]);
|
|
95
|
+
for (const context of result.messages.filter(entry => entry !== human)) {
|
|
96
|
+
assert.equal(context.source.kind, 'plugin:dsh-im', id);
|
|
97
|
+
assert.equal(Object.hasOwn(context.source, 'plugin'), false, id);
|
|
98
|
+
if (context.source.form === 'notice') assert.ok(context.source.summary.length > 0, id);
|
|
99
|
+
}
|
|
100
|
+
assert.equal(await ctx.waterfall('agent/pre-step', { agent: { session: { id } } }, async () => result), result);
|
|
101
|
+
|
|
102
|
+
const header = { version: SESSION_FORMAT_VERSION, id, createdAt: 1, isSeeded: false, delegationDepth: 0 };
|
|
103
|
+
const events = turnEvents(result.messages);
|
|
104
|
+
const writer = await ctx.sessionPersistence.create(header);
|
|
105
|
+
try { await writer.append(events); } finally { await writer.close(); }
|
|
106
|
+
// Reopen for a second turn, then verify both turns through a fresh read handle.
|
|
107
|
+
const resumed = await ctx.sessionPersistence.open(id, 'write');
|
|
108
|
+
const second = turnEvents([{ ...human, id: `${id}-second` }], 2, events.length);
|
|
109
|
+
try { await resumed.append(second); } finally { await resumed.close(); }
|
|
110
|
+
const reader = await ctx.sessionPersistence.open(id, 'read');
|
|
111
|
+
try {
|
|
112
|
+
assert.deepEqual((await reader.read()).events, [...events, ...second], id);
|
|
113
|
+
} finally { await reader.close(); }
|
|
114
|
+
console.log(`PASS ${id}: bundled hook, persistence, reopen and next turn`);
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
try { await ctx.fiber.dispose(); }
|
|
118
|
+
finally { await rm(directory, { recursive: true, force: true }); }
|
|
119
|
+
}
|
|
120
|
+
console.log(`Injected context verified against ${harnessRoot} (format V4).`);
|
|
@@ -161,7 +161,7 @@ try {
|
|
|
161
161
|
headers: { cookie: lan.cookie },
|
|
162
162
|
}), 200);
|
|
163
163
|
const channels = ['feishu', 'weixin', 'dingtalk', 'wecom', 'wecom-app', 'qq',
|
|
164
|
-
'slack', 'telegram', 'discord', 'whatsapp', 'imessage', 'office'];
|
|
164
|
+
'slack', 'telegram', 'discord', 'whatsapp', 'imessage', 'matrix', 'office'];
|
|
165
165
|
for (const channel of channels) {
|
|
166
166
|
expectStatus(`LAN default: ${channel}`, await readyStatus(lan, channel), 200, true);
|
|
167
167
|
}
|
|
@@ -23,6 +23,7 @@ const required = [
|
|
|
23
23
|
'lib/client.js',
|
|
24
24
|
'bin/dsh-im.mjs',
|
|
25
25
|
'cordis.patch.yml',
|
|
26
|
+
'scripts/verify-injected-context.mjs',
|
|
26
27
|
'README.md',
|
|
27
28
|
'README.en.md',
|
|
28
29
|
'PROACTIVE_DELIVERY.md',
|
|
@@ -45,6 +46,8 @@ const required = [
|
|
|
45
46
|
'plugin-src/host/channels/telegram/index.mjs',
|
|
46
47
|
'plugin-src/host/channels/discord/index.mjs',
|
|
47
48
|
'plugin-src/host/channels/whatsapp/index.mjs',
|
|
49
|
+
'plugin-src/host/channels/matrix/index.mjs',
|
|
50
|
+
'plugin-src/client/channels/matrix/index.js',
|
|
48
51
|
'src/channels/feishu/feishu-runtime.mjs',
|
|
49
52
|
'src/channels/weixin/weixin-runtime.mjs',
|
|
50
53
|
'src/channels/dingtalk/dingtalk-runtime.mjs',
|
|
@@ -56,6 +59,8 @@ const required = [
|
|
|
56
59
|
'src/channels/discord/discord-runtime.mjs',
|
|
57
60
|
'src/channels/whatsapp/whatsapp-runtime.mjs',
|
|
58
61
|
'src/channels/whatsapp/whatsapp-web-session.mjs',
|
|
62
|
+
'src/channels/matrix/matrix-runtime.mjs',
|
|
63
|
+
'src/channels/matrix/matrix-api.mjs',
|
|
59
64
|
'src/channels/shared/context-enhancement.mjs',
|
|
60
65
|
];
|
|
61
66
|
await Promise.all(required.map((path) => access(resolve(root, path))));
|
|
@@ -174,7 +179,7 @@ if (!client.includes('container-type: inline-size')
|
|
|
174
179
|
|| !client.includes('@container (max-width: 680px)')) {
|
|
175
180
|
throw new Error('client bundle does not contain the narrow-panel DingTalk QR layout');
|
|
176
181
|
}
|
|
177
|
-
for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp']) {
|
|
182
|
+
for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp', '/matrix']) {
|
|
178
183
|
if (!host.includes(marker)) {
|
|
179
184
|
throw new Error(`host bundle does not contain the internal ${marker} RPC provider`);
|
|
180
185
|
}
|
|
@@ -628,7 +628,7 @@ export class FeishuRuntime {
|
|
|
628
628
|
});
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
async sendProactiveText(target, text, { signal } = {}) {
|
|
631
|
+
async sendProactiveText(target, text, { signal, format = 'plain' } = {}) {
|
|
632
632
|
if (!this.#status.ready || !this.#client) {
|
|
633
633
|
const error = new Error('飞书机器人尚未连接');
|
|
634
634
|
error.code = 'bot-not-connected';
|
|
@@ -646,13 +646,23 @@ export class FeishuRuntime {
|
|
|
646
646
|
error.code = 'invalid-target';
|
|
647
647
|
throw error;
|
|
648
648
|
}
|
|
649
|
+
if (format !== 'plain' && format !== 'markdown') {
|
|
650
|
+
const error = new TypeError('Message format must be plain or markdown');
|
|
651
|
+
error.code = 'bad-request';
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
649
654
|
signal?.throwIfAborted();
|
|
655
|
+
// Use the same native Markdown element as chat, without opening a stream
|
|
656
|
+
// or retrying as plain text after a possibly accepted delivery.
|
|
657
|
+
const content = format === 'markdown'
|
|
658
|
+
? { schema: '2.0', body: { elements: [{ tag: 'markdown', content: text }] } }
|
|
659
|
+
: { text };
|
|
650
660
|
const response = await this.#client.im.v1.message.create({
|
|
651
661
|
params: { receive_id_type: receiveIdType },
|
|
652
662
|
data: {
|
|
653
663
|
receive_id: receiveId,
|
|
654
|
-
msg_type: 'text',
|
|
655
|
-
content: JSON.stringify(
|
|
664
|
+
msg_type: format === 'markdown' ? 'interactive' : 'text',
|
|
665
|
+
content: JSON.stringify(content),
|
|
656
666
|
},
|
|
657
667
|
});
|
|
658
668
|
if (response?.code && response.code !== 0) {
|