@xmanrui/dsh-im 4.24.1 → 4.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/client.js +564 -279
- package/lib/index.js +298 -289
- package/package.json +4 -2
- 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/i18n.js +16 -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 +5 -3
- package/plugin-src/client/update-panel.js +25 -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-suggestions.mjs +13 -0
- package/plugin-src/host/index.mjs +3 -0
- package/scripts/verify-lan-management.mjs +1 -1
- package/scripts/verify-package.mjs +5 -1
- 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/matrix.mjs +75 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/session-channel-labels.mjs +1 -0
- package/src/channels/shared/text-harness-bridge.mjs +3 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { unlink } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { MatrixConfigStore, MatrixSidecarStore } from '../../../../src/channels/matrix/matrix-config-store.mjs';
|
|
5
|
+
import { MatrixController } from '../../../../src/channels/matrix/matrix-controller.mjs';
|
|
6
|
+
import { MatrixCryptoStore, matrixCryptoPathFor } from '../../../../src/channels/matrix/matrix-crypto-store.mjs';
|
|
7
|
+
import { MatrixHarnessClient } from '../../../../src/channels/matrix/matrix-harness-client.mjs';
|
|
8
|
+
import { MatrixRuntime } from '../../../../src/channels/matrix/matrix-runtime.mjs';
|
|
9
|
+
import { ConversationStateStore } from '../../../../src/channels/shared/conversation-state-store.mjs';
|
|
10
|
+
import {
|
|
11
|
+
BotWorkspaceStore,
|
|
12
|
+
createBotWorkspaceScope,
|
|
13
|
+
createWorkspaceAwareController,
|
|
14
|
+
observeBotWorkspaceRemovals,
|
|
15
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
16
|
+
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
17
|
+
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
18
|
+
import { commandsForChannel } from '../../../../src/channels/shared/command-catalog.mjs';
|
|
19
|
+
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
20
|
+
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
|
|
21
|
+
import { pluginPaths } from '../shared/production.mjs';
|
|
22
|
+
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
|
|
23
|
+
import { harnessConnection } from '../../harness-connection.mjs';
|
|
24
|
+
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
|
|
25
|
+
import {
|
|
26
|
+
getInboundTtlRuntime,
|
|
27
|
+
registerInboundTtlWorkspaces,
|
|
28
|
+
} from '../../inbound-ttl-runtime.mjs';
|
|
29
|
+
import {
|
|
30
|
+
accessPolicyProvider,
|
|
31
|
+
initialAccessPolicyFor,
|
|
32
|
+
} from '../shared/access-policy-production.mjs';
|
|
33
|
+
|
|
34
|
+
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
35
|
+
if (!ctx?.credentials) throw new TypeError('dsh-im matrix requires ctx.credentials');
|
|
36
|
+
const connection = harnessConnection(ctx, config);
|
|
37
|
+
|
|
38
|
+
const ResolvedConfigStore = internals.ConfigStore ?? MatrixConfigStore;
|
|
39
|
+
const ResolvedStateStore = internals.StateStore ?? ConversationStateStore;
|
|
40
|
+
const ResolvedSidecarStore = internals.SidecarStore ?? MatrixSidecarStore;
|
|
41
|
+
const ResolvedHarness = internals.HarnessClient ?? MatrixHarnessClient;
|
|
42
|
+
const ResolvedController = internals.Controller ?? MatrixController;
|
|
43
|
+
const ResolvedRuntime = internals.Runtime ?? MatrixRuntime;
|
|
44
|
+
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
|
|
45
|
+
const logger = typeof ctx.logger === 'function'
|
|
46
|
+
? ctx.logger('dsh-im:matrix') : (ctx.logger ?? console);
|
|
47
|
+
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
48
|
+
const paths = pluginPaths(config, 'matrix');
|
|
49
|
+
const configStore = await new ResolvedConfigStore(paths.config).load();
|
|
50
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
51
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
52
|
+
const workspaces = internals.workspaces
|
|
53
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
54
|
+
const configuredBots = configStore.list();
|
|
55
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
56
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
|
|
57
|
+
defaultAgentPreset: config.agentPreset,
|
|
58
|
+
initialAccessPolicy: initialAccessPolicyFor('matrix', bot),
|
|
59
|
+
})));
|
|
60
|
+
const observedConfigStore = typeof configStore.remove === 'function'
|
|
61
|
+
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
62
|
+
: configStore;
|
|
63
|
+
const stateStores = new Map();
|
|
64
|
+
const sidecarStores = new Map();
|
|
65
|
+
const cryptoStores = new Map();
|
|
66
|
+
const ResolvedCryptoStore = internals.CryptoStore ?? MatrixCryptoStore;
|
|
67
|
+
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
68
|
+
const sidecarPath = (botId) => resolve(paths.bots, botId, 'matrix.json');
|
|
69
|
+
const cryptoPath = (botId) => matrixCryptoPathFor(resolve(paths.bots, botId));
|
|
70
|
+
const stateFor = async (botId) => {
|
|
71
|
+
let state = stateStores.get(botId);
|
|
72
|
+
if (!state) {
|
|
73
|
+
state = await new ResolvedStateStore(statePath(botId)).load();
|
|
74
|
+
stateStores.set(botId, state);
|
|
75
|
+
}
|
|
76
|
+
return state;
|
|
77
|
+
};
|
|
78
|
+
const sidecarFor = async (botId) => {
|
|
79
|
+
let sidecar = sidecarStores.get(botId);
|
|
80
|
+
if (!sidecar) {
|
|
81
|
+
sidecar = await new ResolvedSidecarStore(sidecarPath(botId)).load();
|
|
82
|
+
sidecarStores.set(botId, sidecar);
|
|
83
|
+
}
|
|
84
|
+
return sidecar;
|
|
85
|
+
};
|
|
86
|
+
const cryptoFor = async (botId) => {
|
|
87
|
+
let crypto = cryptoStores.get(botId);
|
|
88
|
+
if (!crypto) {
|
|
89
|
+
crypto = await new ResolvedCryptoStore(cryptoPath(botId)).load();
|
|
90
|
+
cryptoStores.set(botId, crypto);
|
|
91
|
+
}
|
|
92
|
+
return crypto;
|
|
93
|
+
};
|
|
94
|
+
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
95
|
+
const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
|
|
96
|
+
const inboundTtlService = inboundTtl?.service ?? inboundTtl;
|
|
97
|
+
registerInboundTtlWorkspaces(ctx, inboundTtlService, {
|
|
98
|
+
workspaces,
|
|
99
|
+
configStore: observedConfigStore,
|
|
100
|
+
defaultWorkspace,
|
|
101
|
+
});
|
|
102
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
103
|
+
controlExecutor: internals.controlExecutor,
|
|
104
|
+
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
105
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
106
|
+
inboundTtlService,
|
|
107
|
+
});
|
|
108
|
+
const harness = new ResolvedHarness({
|
|
109
|
+
...connection,
|
|
110
|
+
workspace: defaultWorkspace,
|
|
111
|
+
autostart: false,
|
|
112
|
+
dshBin: config.dshBin ?? 'dsh',
|
|
113
|
+
...(commandExecutor ? { commandExecutor } : {}),
|
|
114
|
+
...(controlExecutor ? { controlExecutor } : {}),
|
|
115
|
+
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
116
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
117
|
+
});
|
|
118
|
+
const modelCatalog = () => listModelCatalog(harness);
|
|
119
|
+
const knownCommandNames = new Set(
|
|
120
|
+
commandsForChannel('matrix').flatMap((entry) => [entry.name, ...entry.aliases.map((alias) => alias.name)]),
|
|
121
|
+
);
|
|
122
|
+
const isKnownCommand = (name) => typeof name === 'string' && knownCommandNames.has(name.replace(/^\/+/, ''));
|
|
123
|
+
const coreController = new ResolvedController({
|
|
124
|
+
credentials: ctx.credentials,
|
|
125
|
+
configStore: observedConfigStore,
|
|
126
|
+
logger,
|
|
127
|
+
...(internals.inspectCredentials ? { inspectCredentials: internals.inspectCredentials } : {}),
|
|
128
|
+
createRuntime: async ({ botId, config: botConfig, accessToken, password, userId }) => {
|
|
129
|
+
const state = await stateFor(botId);
|
|
130
|
+
const sidecar = await sidecarFor(botId);
|
|
131
|
+
await workspaces.ensure(botId, {
|
|
132
|
+
defaultAgentPreset: config.agentPreset,
|
|
133
|
+
initialAccessPolicy: initialAccessPolicyFor('matrix', botConfig),
|
|
134
|
+
});
|
|
135
|
+
const workspaceScope = createBotWorkspaceScope(harness, {
|
|
136
|
+
botId, workspaces, state, agentPresetCatalog,
|
|
137
|
+
});
|
|
138
|
+
const e2eeMode = String(botConfig?.e2eeMode ?? 'optional').trim().toLowerCase();
|
|
139
|
+
const cryptoStore = e2eeMode === 'off' ? null : await cryptoFor(botId);
|
|
140
|
+
return new ResolvedRuntime({
|
|
141
|
+
config: botConfig,
|
|
142
|
+
...(accessToken ? { accessToken } : {}),
|
|
143
|
+
...(password ? { password } : {}),
|
|
144
|
+
...(userId ? { userId } : {}),
|
|
145
|
+
harness: workspaceScope.harness,
|
|
146
|
+
state: workspaceScope.state,
|
|
147
|
+
sidecar,
|
|
148
|
+
...(cryptoStore ? { cryptoStore } : {}),
|
|
149
|
+
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
|
|
150
|
+
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
|
151
|
+
channel: 'matrix', config: botConfig,
|
|
152
|
+
}),
|
|
153
|
+
...(typeof internals.isKnownCommand === 'function'
|
|
154
|
+
? { isKnownCommand: internals.isKnownCommand }
|
|
155
|
+
: { isKnownCommand }),
|
|
156
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
157
|
+
logger: {
|
|
158
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
159
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
160
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
161
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
deleteState: async ({ botId }) => {
|
|
166
|
+
const state = stateStores.get(botId);
|
|
167
|
+
stateStores.delete(botId);
|
|
168
|
+
const sidecar = sidecarStores.get(botId);
|
|
169
|
+
sidecarStores.delete(botId);
|
|
170
|
+
const crypto = cryptoStores.get(botId);
|
|
171
|
+
cryptoStores.delete(botId);
|
|
172
|
+
if (crypto && typeof crypto.remove === 'function') {
|
|
173
|
+
await crypto.remove();
|
|
174
|
+
} else {
|
|
175
|
+
try {
|
|
176
|
+
await unlink(cryptoPath(botId));
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (state && typeof state.remove === 'function') {
|
|
182
|
+
await state.remove();
|
|
183
|
+
} else {
|
|
184
|
+
try {
|
|
185
|
+
await unlink(statePath(botId));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (sidecar && typeof sidecar.remove === 'function') {
|
|
191
|
+
await sidecar.remove();
|
|
192
|
+
} else {
|
|
193
|
+
try {
|
|
194
|
+
await unlink(sidecarPath(botId));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const controller = createWorkspaceAwareController(coreController, {
|
|
202
|
+
workspaces,
|
|
203
|
+
stateFor,
|
|
204
|
+
agentPresetCatalog,
|
|
205
|
+
modelCatalog,
|
|
206
|
+
});
|
|
207
|
+
const supervisor = createSupervisor({
|
|
208
|
+
channel: 'matrix',
|
|
209
|
+
controller,
|
|
210
|
+
harness,
|
|
211
|
+
logger,
|
|
212
|
+
retryDelaysMs: config.retryDelaysMs,
|
|
213
|
+
healthyIntervalMs: config.healthyIntervalMs,
|
|
214
|
+
}).start();
|
|
215
|
+
return {
|
|
216
|
+
controller,
|
|
217
|
+
deliveryAdapter: createDeliveryAdapter({
|
|
218
|
+
channel: 'matrix', workspaces, coreController, stateFor,
|
|
219
|
+
}),
|
|
220
|
+
ready: supervisor.ready,
|
|
221
|
+
async close() {
|
|
222
|
+
await supervisor.close();
|
|
223
|
+
await controller.close();
|
|
224
|
+
harness.stopManagedProcess();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { SET_ALIAS_ENDPOINT, validAliasPayload } from '../shared/bot-alias-rpc.mjs';
|
|
2
|
+
import { registerManagementRpc } from '../../../management-rpc.mjs';
|
|
3
|
+
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
4
|
+
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
|
5
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
6
|
+
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
7
|
+
import {
|
|
8
|
+
publicWorkspaceError,
|
|
9
|
+
SET_WORKSPACE_ENDPOINT,
|
|
10
|
+
validWorkspacePayload,
|
|
11
|
+
} from '../shared/workspace-rpc.mjs';
|
|
12
|
+
import {
|
|
13
|
+
SET_AGENT_PRESET_ENDPOINT,
|
|
14
|
+
validAgentPresetPayload,
|
|
15
|
+
} from '../shared/agent-preset-rpc.mjs';
|
|
16
|
+
import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
|
|
17
|
+
import { isMatrixUserId, validateMatrixHomeserver } from '../../../../src/channels/matrix/matrix-api.mjs';
|
|
18
|
+
|
|
19
|
+
export const MATRIX_RPC_CHANNEL = '/matrix';
|
|
20
|
+
export const MATRIX_ENDPOINTS = Object.freeze({
|
|
21
|
+
status: 'connection.status',
|
|
22
|
+
bindCredentials: 'bot.bind-credentials',
|
|
23
|
+
reconnectBot: 'bot.reconnect',
|
|
24
|
+
deleteBot: 'bot.delete',
|
|
25
|
+
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
26
|
+
setModel: SET_MODEL_ENDPOINT,
|
|
27
|
+
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
28
|
+
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
29
|
+
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
30
|
+
setAlias: SET_ALIAS_ENDPOINT,
|
|
31
|
+
});
|
|
32
|
+
export const MATRIX_RPC_ENDPOINTS = Object.freeze(Object.values(MATRIX_ENDPOINTS));
|
|
33
|
+
|
|
34
|
+
const FORBIDDEN_PUBLIC_KEYS = new Set([
|
|
35
|
+
'token', 'accessToken', 'accessTokenRef', 'tokenRef',
|
|
36
|
+
'password', 'passwordRef', 'platformId', 'secret', 'secretRef',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function isRecord(value) {
|
|
40
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function exactKeys(value, allowed) {
|
|
44
|
+
return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validId(value) {
|
|
48
|
+
return typeof value === 'string' && /^matrix_[a-f0-9]{24}$/.test(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validHomeserver(value) {
|
|
52
|
+
return typeof value === 'string' && value.length <= 2_048 && validateMatrixHomeserver(value) !== null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validAccessToken(value) {
|
|
56
|
+
return typeof value === 'string' && value.trim().length >= 8 && value.length <= 4_096;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validUserId(value) {
|
|
60
|
+
return typeof value === 'string' && value.length <= 512 && isMatrixUserId(value.trim());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validPassword(value) {
|
|
64
|
+
return typeof value === 'string' && value.length >= 1 && value.length <= 1_024;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function payloadFailure(endpoint, payload) {
|
|
68
|
+
if (!isRecord(payload)) return 'Payload must be an object.';
|
|
69
|
+
if (endpoint === MATRIX_ENDPOINTS.status) {
|
|
70
|
+
return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
|
|
71
|
+
}
|
|
72
|
+
if (endpoint === MATRIX_ENDPOINTS.bindCredentials) {
|
|
73
|
+
const hasToken = payload.accessToken !== undefined;
|
|
74
|
+
const hasLogin = payload.userId !== undefined && payload.password !== undefined;
|
|
75
|
+
return exactKeys(payload, ['homeserver', 'accessToken', 'userId', 'password'])
|
|
76
|
+
&& validHomeserver(payload.homeserver)
|
|
77
|
+
&& (hasToken
|
|
78
|
+
? (payload.userId === undefined && payload.password === undefined && validAccessToken(payload.accessToken))
|
|
79
|
+
: (hasLogin && validUserId(payload.userId) && validPassword(payload.password)))
|
|
80
|
+
? null : 'bot.bind-credentials requires a homeserver plus an access token or a user id with password.';
|
|
81
|
+
}
|
|
82
|
+
if (endpoint === MATRIX_ENDPOINTS.reconnectBot) {
|
|
83
|
+
return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
|
|
84
|
+
&& (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
|
|
85
|
+
? null : 'bot.reconnect requires a botId.';
|
|
86
|
+
}
|
|
87
|
+
if (endpoint === MATRIX_ENDPOINTS.deleteBot) {
|
|
88
|
+
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
89
|
+
? null : 'bot.delete requires a botId and confirm=true.';
|
|
90
|
+
}
|
|
91
|
+
if (endpoint === MATRIX_ENDPOINTS.setWorkspace) {
|
|
92
|
+
return validWorkspacePayload(payload)
|
|
93
|
+
? null : '请输入工作区绝对路径。';
|
|
94
|
+
}
|
|
95
|
+
if (endpoint === MATRIX_ENDPOINTS.setModel) {
|
|
96
|
+
return validModelPayload(payload) ? null : '请选择有效模型。';
|
|
97
|
+
}
|
|
98
|
+
if (endpoint === MATRIX_ENDPOINTS.setAgentPreset) {
|
|
99
|
+
return validAgentPresetPayload(payload)
|
|
100
|
+
? null : '请选择 Agent Preset。';
|
|
101
|
+
}
|
|
102
|
+
if (endpoint === MATRIX_ENDPOINTS.setContextEnhancement) {
|
|
103
|
+
return validContextEnhancementPayload(payload)
|
|
104
|
+
? null : '请提交有效的上下文增强设置。';
|
|
105
|
+
}
|
|
106
|
+
if (endpoint === MATRIX_ENDPOINTS.setAccessPolicy) {
|
|
107
|
+
return validAccessPolicyPayload(payload)
|
|
108
|
+
? null : '请提交有效的访问设置。';
|
|
109
|
+
}
|
|
110
|
+
if (endpoint === MATRIX_ENDPOINTS.setAlias) {
|
|
111
|
+
return validAliasPayload(payload)
|
|
112
|
+
? null : '请输入有效的别名(最多 80 个字符)。';
|
|
113
|
+
}
|
|
114
|
+
return 'Unknown Matrix endpoint.';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sanitizePublic(value) {
|
|
118
|
+
if (Array.isArray(value)) return value.map(sanitizePublic);
|
|
119
|
+
if (!isRecord(value)) return value;
|
|
120
|
+
const safe = {};
|
|
121
|
+
for (const [key, child] of Object.entries(value)) {
|
|
122
|
+
if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child);
|
|
123
|
+
}
|
|
124
|
+
return safe;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function operationError(error) {
|
|
128
|
+
const workspaceError = publicWorkspaceError(error);
|
|
129
|
+
if (workspaceError) return workspaceError;
|
|
130
|
+
if (error?.code === 'invalid-config') {
|
|
131
|
+
return { code: 'invalid-config', message: error.message };
|
|
132
|
+
}
|
|
133
|
+
if (error?.code === 'auth-failed') {
|
|
134
|
+
return { code: 'auth-failed', message: error.message };
|
|
135
|
+
}
|
|
136
|
+
if (error?.code === 'login-failed' || error?.code === 'network' || error?.code === 'timeout') {
|
|
137
|
+
return { code: 'homeserver-unreachable', message: error.message };
|
|
138
|
+
}
|
|
139
|
+
return { code: 'matrix-operation-failed', message: 'Matrix 操作失败,请稍后重试。' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createMatrixRpcHandler(controller) {
|
|
143
|
+
for (const method of ['status', 'bindCredentials', 'reconnectBot', 'deleteBot']) {
|
|
144
|
+
if (typeof controller?.[method] !== 'function') {
|
|
145
|
+
throw new TypeError(`A complete Matrix controller is required (${method})`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return async (endpoint, payload, signal) => {
|
|
149
|
+
if (signal?.aborted) {
|
|
150
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
151
|
+
}
|
|
152
|
+
if (!MATRIX_RPC_ENDPOINTS.includes(endpoint)) {
|
|
153
|
+
return { ok: false, error: { code: 'bad-request', message: 'Unknown Matrix endpoint.' } };
|
|
154
|
+
}
|
|
155
|
+
const invalid = payloadFailure(endpoint, payload);
|
|
156
|
+
if (invalid) return { ok: false, error: { code: 'bad-request', message: invalid } };
|
|
157
|
+
try {
|
|
158
|
+
let value;
|
|
159
|
+
if (endpoint === MATRIX_ENDPOINTS.status) value = await controller.status();
|
|
160
|
+
else if (endpoint === MATRIX_ENDPOINTS.bindCredentials) value = await controller.bindCredentials(payload);
|
|
161
|
+
else if (endpoint === MATRIX_ENDPOINTS.reconnectBot) {
|
|
162
|
+
value = await controller.reconnectBot(payload.botId);
|
|
163
|
+
if (signal?.aborted) {
|
|
164
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
165
|
+
}
|
|
166
|
+
if (payload.sendTest === true) {
|
|
167
|
+
let testError = null;
|
|
168
|
+
try {
|
|
169
|
+
if (value?.bots?.find((bot) => bot?.botId === payload.botId)?.ready !== true) {
|
|
170
|
+
const unavailable = new Error('Bot is not connected');
|
|
171
|
+
unavailable.code = 'test-target-unavailable';
|
|
172
|
+
throw unavailable;
|
|
173
|
+
}
|
|
174
|
+
if (typeof controller.sendConnectionTest !== 'function') {
|
|
175
|
+
const unavailable = new Error('Connection test is unavailable');
|
|
176
|
+
unavailable.code = 'test-target-unavailable';
|
|
177
|
+
throw unavailable;
|
|
178
|
+
}
|
|
179
|
+
await controller.sendConnectionTest(payload.botId);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
testError = error;
|
|
182
|
+
}
|
|
183
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (endpoint === MATRIX_ENDPOINTS.setWorkspace) {
|
|
187
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
188
|
+
value = await controller.updateWorkspace(payload.botId, payload.workspace);
|
|
189
|
+
}
|
|
190
|
+
else if (endpoint === MATRIX_ENDPOINTS.setModel) {
|
|
191
|
+
if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
|
|
192
|
+
value = await controller.updateModel(payload.botId, payload.model);
|
|
193
|
+
}
|
|
194
|
+
else if (endpoint === MATRIX_ENDPOINTS.setContextEnhancement) {
|
|
195
|
+
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
196
|
+
value = await controller.updateContextEnhancement(payload.botId, payload.config);
|
|
197
|
+
}
|
|
198
|
+
else if (endpoint === MATRIX_ENDPOINTS.setAlias) {
|
|
199
|
+
if (typeof controller.updateAlias !== 'function') throw new Error('Alias update is unavailable');
|
|
200
|
+
value = await controller.updateAlias(payload.botId, payload.alias);
|
|
201
|
+
}
|
|
202
|
+
else if (endpoint === MATRIX_ENDPOINTS.setAccessPolicy) {
|
|
203
|
+
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
204
|
+
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
|
|
205
|
+
}
|
|
206
|
+
else if (endpoint === MATRIX_ENDPOINTS.setAgentPreset) {
|
|
207
|
+
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
|
|
208
|
+
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);
|
|
209
|
+
}
|
|
210
|
+
else value = await controller.deleteBot(payload.botId);
|
|
211
|
+
return signal?.aborted
|
|
212
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
213
|
+
: { ok: true, value: sanitizePublic(value) };
|
|
214
|
+
} catch (error) {
|
|
215
|
+
return signal?.aborted
|
|
216
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
217
|
+
: { ok: false, error: operationError(error) };
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function installMatrixRpc(ctx, controller, authority) {
|
|
223
|
+
return registerManagementRpc(ctx,
|
|
224
|
+
MATRIX_RPC_CHANNEL,
|
|
225
|
+
createMatrixRpcHandler(controller),
|
|
226
|
+
{ authority: resolveRpcAuthority(authority) },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -91,7 +91,7 @@ export function initialAccessPolicyFor(channel, config = {}) {
|
|
|
91
91
|
group: allowlistScope(),
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
if (['dingtalk', 'wecom', 'wecom-app', 'slack', 'discord', 'imessage'].includes(key)) {
|
|
94
|
+
if (['dingtalk', 'wecom', 'wecom-app', 'slack', 'discord', 'imessage', 'matrix'].includes(key)) {
|
|
95
95
|
return createAccessPolicy({ direct: openScope(), group: openScope() });
|
|
96
96
|
}
|
|
97
97
|
throw new TypeError(`Unsupported access-policy channel: ${channel}`);
|
|
@@ -4,7 +4,7 @@ const CHANNEL_NAMES = {
|
|
|
4
4
|
weixin: '微信', feishu: '飞书', dingtalk: '钉钉', wecom: '企业微信',
|
|
5
5
|
'wecom-app': '企业微信应用', qq: 'QQ', slack: 'Slack', telegram: 'Telegram',
|
|
6
6
|
discord: 'Discord', whatsapp: 'WhatsApp', imessage: 'iMessage',
|
|
7
|
-
email: '邮箱', office: 'AI Office',
|
|
7
|
+
email: '邮箱', matrix: 'Matrix', office: 'AI Office',
|
|
8
8
|
};
|
|
9
9
|
const INVALID_CONFIG_MESSAGES = new Set([
|
|
10
10
|
'dsh-weixin config contains invalid account data',
|
|
@@ -15,6 +15,7 @@ const INVALID_CONFIG_MESSAGES = new Set([
|
|
|
15
15
|
...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord', 'Email']
|
|
16
16
|
.map(channel => `dsh-im ${channel} config contains invalid bot data`),
|
|
17
17
|
'dsh-im WhatsApp config contains invalid account data',
|
|
18
|
+
'dsh-im Matrix config contains invalid bot data',
|
|
18
19
|
'dsh-im AI Office config is invalid',
|
|
19
20
|
'dsh-im workspace config is invalid',
|
|
20
21
|
]);
|
|
@@ -17,6 +17,7 @@ const CHANNELS = new Set([
|
|
|
17
17
|
'whatsapp',
|
|
18
18
|
'imessage',
|
|
19
19
|
'email',
|
|
20
|
+
'matrix',
|
|
20
21
|
]);
|
|
21
22
|
|
|
22
23
|
export function supportsDeliveryChannel(channel) {
|
|
@@ -137,6 +138,23 @@ function normalizeRoute(channel, kind, route) {
|
|
|
137
138
|
}
|
|
138
139
|
return { address: normalized.address.toLowerCase() };
|
|
139
140
|
}
|
|
141
|
+
case 'matrix': {
|
|
142
|
+
oneOf(kind, ['room', 'thread', 'dm']);
|
|
143
|
+
const normalized = routeWithStrings(
|
|
144
|
+
route,
|
|
145
|
+
kind === 'room' ? ['roomId'] : kind === 'thread' ? ['roomId', 'threadId'] : ['userId'],
|
|
146
|
+
);
|
|
147
|
+
if (kind === 'dm' && !/^@[^:\s]+:[^\s:]+(?::\d{1,5})?$/u.test(normalized.userId)) {
|
|
148
|
+
throw invalidTarget('route.userId must be a Matrix user id');
|
|
149
|
+
}
|
|
150
|
+
if (kind !== 'dm' && !/^[!][^:$\s]+:[^\s:$]+(?::\d{1,5})?$/.test(normalized.roomId)) {
|
|
151
|
+
throw invalidTarget('route.roomId must be a Matrix room id');
|
|
152
|
+
}
|
|
153
|
+
if (kind === 'thread' && !/^\$[^\s]+/.test(normalized.threadId)) {
|
|
154
|
+
throw invalidTarget('route.threadId must be a Matrix event id');
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
140
158
|
default:
|
|
141
159
|
throw new TypeError(`Unsupported delivery channel: ${channel}`);
|
|
142
160
|
}
|
|
@@ -9,6 +9,7 @@ const CHANNELS = new Set([
|
|
|
9
9
|
'telegram',
|
|
10
10
|
'discord',
|
|
11
11
|
'whatsapp',
|
|
12
|
+
'matrix',
|
|
12
13
|
]);
|
|
13
14
|
|
|
14
15
|
function isRecord(value) {
|
|
@@ -80,6 +81,15 @@ function whatsappSuggestion(key) {
|
|
|
80
81
|
return group ? { kind: 'group', route: { jid: group[1] } } : null;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
function matrixSuggestion(key) {
|
|
85
|
+
const dm = /^dm:(@[A-Za-z0-9._=\-\/+]+:[A-Za-z0-9.-]+(?::\d{1,5})?)$/.exec(key);
|
|
86
|
+
if (dm) return { kind: 'dm', route: { userId: dm[1] } };
|
|
87
|
+
const room = /^room:([!#][^:$\s]+:[A-Za-z0-9.-]+(?::\d{1,5})?)(?:\$(\$[^\s]+))?$/u.exec(key);
|
|
88
|
+
if (!room) return null;
|
|
89
|
+
if (room[2] === undefined) return { kind: 'room', route: { roomId: room[1] } };
|
|
90
|
+
return { kind: 'thread', route: { roomId: room[1], threadId: room[2] } };
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
/** Convert one persisted conversation key into a stable proactive-delivery route. */
|
|
84
94
|
export function deliverySuggestionFromConversationKey(channel, key) {
|
|
85
95
|
if (!CHANNELS.has(channel) || typeof key !== 'string') return null;
|
|
@@ -113,6 +123,8 @@ export function deliverySuggestionFromConversationKey(channel, key) {
|
|
|
113
123
|
return discordSuggestion(key);
|
|
114
124
|
case 'whatsapp':
|
|
115
125
|
return whatsappSuggestion(key);
|
|
126
|
+
case 'matrix':
|
|
127
|
+
return matrixSuggestion(key);
|
|
116
128
|
default:
|
|
117
129
|
return null;
|
|
118
130
|
}
|
|
@@ -133,6 +145,7 @@ export function privateDeliverySuggestionFromConversationKey(channel, key) {
|
|
|
133
145
|
telegram: 'direct',
|
|
134
146
|
discord: 'direct',
|
|
135
147
|
whatsapp: 'direct',
|
|
148
|
+
matrix: 'dm',
|
|
136
149
|
}[channel];
|
|
137
150
|
if (!privatePrefix || prefix !== privatePrefix) return null;
|
|
138
151
|
return deliverySuggestionFromConversationKey(channel, key);
|
|
@@ -11,6 +11,7 @@ import { apply as applyWeixin } from './channels/weixin/index.mjs';
|
|
|
11
11
|
import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
|
|
12
12
|
import { apply as applyIMessage } from './channels/imessage/index.mjs';
|
|
13
13
|
import { apply as applyEmail } from './channels/email/index.mjs';
|
|
14
|
+
import { apply as applyMatrix } from './channels/matrix/index.mjs';
|
|
14
15
|
import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
|
|
15
16
|
import { installHostLanguage } from './host-language.mjs';
|
|
16
17
|
import { installHostLanguageRpc } from './host-language-rpc.mjs';
|
|
@@ -62,6 +63,7 @@ export function createImHostPlugin(internals = {}) {
|
|
|
62
63
|
const startWhatsapp = internals.applyWhatsapp ?? applyWhatsapp;
|
|
63
64
|
const startIMessage = internals.applyIMessage ?? applyIMessage;
|
|
64
65
|
const startEmail = internals.applyEmail ?? applyEmail;
|
|
66
|
+
const startMatrix = internals.applyMatrix ?? applyMatrix;
|
|
65
67
|
const channels = [
|
|
66
68
|
['feishu', startFeishu],
|
|
67
69
|
['weixin', startWeixin],
|
|
@@ -75,6 +77,7 @@ export function createImHostPlugin(internals = {}) {
|
|
|
75
77
|
['whatsapp', startWhatsapp],
|
|
76
78
|
['imessage', startIMessage],
|
|
77
79
|
['email', startEmail],
|
|
80
|
+
['matrix', startMatrix],
|
|
78
81
|
['office', startOffice],
|
|
79
82
|
];
|
|
80
83
|
return Object.freeze({
|
|
@@ -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
|
}
|
|
@@ -45,6 +45,8 @@ const required = [
|
|
|
45
45
|
'plugin-src/host/channels/telegram/index.mjs',
|
|
46
46
|
'plugin-src/host/channels/discord/index.mjs',
|
|
47
47
|
'plugin-src/host/channels/whatsapp/index.mjs',
|
|
48
|
+
'plugin-src/host/channels/matrix/index.mjs',
|
|
49
|
+
'plugin-src/client/channels/matrix/index.js',
|
|
48
50
|
'src/channels/feishu/feishu-runtime.mjs',
|
|
49
51
|
'src/channels/weixin/weixin-runtime.mjs',
|
|
50
52
|
'src/channels/dingtalk/dingtalk-runtime.mjs',
|
|
@@ -56,6 +58,8 @@ const required = [
|
|
|
56
58
|
'src/channels/discord/discord-runtime.mjs',
|
|
57
59
|
'src/channels/whatsapp/whatsapp-runtime.mjs',
|
|
58
60
|
'src/channels/whatsapp/whatsapp-web-session.mjs',
|
|
61
|
+
'src/channels/matrix/matrix-runtime.mjs',
|
|
62
|
+
'src/channels/matrix/matrix-api.mjs',
|
|
59
63
|
'src/channels/shared/context-enhancement.mjs',
|
|
60
64
|
];
|
|
61
65
|
await Promise.all(required.map((path) => access(resolve(root, path))));
|
|
@@ -174,7 +178,7 @@ if (!client.includes('container-type: inline-size')
|
|
|
174
178
|
|| !client.includes('@container (max-width: 680px)')) {
|
|
175
179
|
throw new Error('client bundle does not contain the narrow-panel DingTalk QR layout');
|
|
176
180
|
}
|
|
177
|
-
for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp']) {
|
|
181
|
+
for (const marker of ['/feishu', '/weixin', '/dingtalk', '/wecom', '/qq', '/slack', '/telegram', '/discord', '/whatsapp', '/matrix']) {
|
|
178
182
|
if (!host.includes(marker)) {
|
|
179
183
|
throw new Error(`host bundle does not contain the internal ${marker} RPC provider`);
|
|
180
184
|
}
|