@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.
Files changed (49) hide show
  1. package/PROACTIVE_DELIVERY.en.md +12 -4
  2. package/PROACTIVE_DELIVERY.md +12 -4
  3. package/README.en.md +11 -3
  4. package/README.md +11 -3
  5. package/THIRD_PARTY_NOTICES.md +2 -0
  6. package/lib/client.js +654 -311
  7. package/lib/index.js +298 -289
  8. package/package.json +7 -8
  9. package/plugin-src/client/channel-logos.js +11 -0
  10. package/plugin-src/client/channels/matrix/api.js +11 -0
  11. package/plugin-src/client/channels/matrix/index.js +151 -0
  12. package/plugin-src/client/channels/matrix/styles.js +36 -0
  13. package/plugin-src/client/global-settings.js +26 -14
  14. package/plugin-src/client/i18n.js +20 -0
  15. package/plugin-src/client/index.js +20 -0
  16. package/plugin-src/client/session-channel-logos.js +2 -1
  17. package/plugin-src/client/styles.js +19 -3
  18. package/plugin-src/client/update-panel.js +31 -11
  19. package/plugin-src/host/channels/matrix/index.mjs +31 -0
  20. package/plugin-src/host/channels/matrix/production.mjs +227 -0
  21. package/plugin-src/host/channels/matrix/rpc.mjs +228 -0
  22. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  23. package/plugin-src/host/channels/shared/startup-error.mjs +2 -1
  24. package/plugin-src/host/delivery-adapter.mjs +18 -0
  25. package/plugin-src/host/delivery-rpc.mjs +7 -2
  26. package/plugin-src/host/delivery-service.mjs +8 -2
  27. package/plugin-src/host/delivery-suggestions.mjs +13 -0
  28. package/plugin-src/host/index.mjs +3 -0
  29. package/scripts/verify-injected-context.mjs +120 -0
  30. package/scripts/verify-lan-management.mjs +1 -1
  31. package/scripts/verify-package.mjs +6 -1
  32. package/src/channels/feishu/feishu-runtime.mjs +13 -3
  33. package/src/channels/matrix/matrix-api.mjs +696 -0
  34. package/src/channels/matrix/matrix-bridge.mjs +20 -0
  35. package/src/channels/matrix/matrix-config-store.mjs +356 -0
  36. package/src/channels/matrix/matrix-controller.mjs +404 -0
  37. package/src/channels/matrix/matrix-crypto-store.mjs +279 -0
  38. package/src/channels/matrix/matrix-crypto.mjs +1014 -0
  39. package/src/channels/matrix/matrix-harness-client.mjs +11 -0
  40. package/src/channels/matrix/matrix-normalize.mjs +357 -0
  41. package/src/channels/matrix/matrix-rich-text.mjs +313 -0
  42. package/src/channels/matrix/matrix-runtime.mjs +900 -0
  43. package/src/channels/shared/command-catalog.mjs +1 -1
  44. package/src/channels/shared/i18n-en/image-input.mjs +1 -0
  45. package/src/channels/shared/i18n-en/matrix.mjs +75 -0
  46. package/src/channels/shared/i18n-en.mjs +2 -0
  47. package/src/channels/shared/injected-context.mjs +3 -3
  48. package/src/channels/shared/session-channel-labels.mjs +1 -0
  49. 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({ text }),
664
+ msg_type: format === 'markdown' ? 'interactive' : 'text',
665
+ content: JSON.stringify(content),
656
666
  },
657
667
  });
658
668
  if (response?.code && response.code !== 0) {