@xmanrui/dsh-im 4.14.0 → 4.16.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 +10 -2
- package/README.md +17 -6
- package/lib/client.js +1980 -616
- package/lib/index.js +303 -293
- package/package.json +6 -2
- package/plugin-src/client/access-policy-settings.js +6 -0
- package/plugin-src/client/channels/feishu/index.js +16 -4
- package/plugin-src/client/channels/wecom-app/api.js +124 -0
- package/plugin-src/client/channels/wecom-app/index.js +612 -0
- package/plugin-src/client/channels/wecom-app/styles.js +25 -0
- package/plugin-src/client/delivery-settings.js +7 -0
- package/plugin-src/client/i18n.js +64 -1
- package/plugin-src/client/index.js +21 -0
- package/plugin-src/client/model-setting.js +135 -69
- package/plugin-src/client/session-channel-logos.js +241 -0
- package/plugin-src/client/styles.js +22 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
- package/plugin-src/host/channels/discord/index.mjs +8 -10
- package/plugin-src/host/channels/feishu/index.mjs +8 -15
- package/plugin-src/host/channels/office/index.mjs +9 -5
- package/plugin-src/host/channels/qq/index.mjs +8 -15
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
- package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
- package/plugin-src/host/channels/shared/startup.mjs +47 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +8 -10
- package/plugin-src/host/channels/telegram/index.mjs +8 -10
- package/plugin-src/host/channels/wecom/index.mjs +8 -15
- package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
- package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
- package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
- package/plugin-src/host/channels/weixin/index.mjs +8 -15
- package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/delivery-suggestions.mjs +4 -0
- package/plugin-src/host/index.mjs +21 -2
- package/plugin-src/host/session-title-prefix.mjs +122 -0
- package/scripts/verify-model-setting.mjs +54 -0
- package/scripts/verify-session-channel-logos.mjs +59 -0
- package/scripts/verify-session-title-prefix.mjs +149 -0
- package/src/channels/feishu/bridge.mjs +305 -12
- package/src/channels/feishu/feishu-cards.mjs +38 -0
- package/src/channels/feishu/feishu-channel.mjs +88 -20
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/feishu/repair-manager.mjs +3 -2
- package/src/channels/shared/bot-workspace-store.mjs +11 -3
- package/src/channels/shared/command-catalog.mjs +102 -0
- package/src/channels/shared/i18n-en/feishu.mjs +14 -2
- package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
- package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/model-setting.mjs +43 -8
- package/src/channels/shared/session-channel-labels.mjs +26 -0
- package/src/channels/shared/text-harness-bridge.mjs +2 -26
- package/src/channels/telegram/telegram-api.mjs +18 -6
- package/src/channels/telegram/telegram-runtime.mjs +34 -32
- package/src/channels/wecom/send-error.mjs +31 -0
- package/src/channels/wecom/wecom-bridge.mjs +59 -17
- package/src/channels/wecom-app/callback-server.mjs +471 -0
- package/src/channels/wecom-app/config-store.mjs +240 -0
- package/src/channels/wecom-app/harness-client.mjs +11 -0
- package/src/channels/wecom-app/state-store.mjs +107 -0
- package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installTelegramRpc } from './rpc.mjs';
|
|
2
|
+
import { createTelegramRpcHandler, installTelegramRpc, TELEGRAM_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-telegram-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -8,15 +9,12 @@ export async function apply(ctx, config = {}) {
|
|
|
8
9
|
if (config?.controller) {
|
|
9
10
|
return installTelegramRpc(ctx, config.controller, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
await production.close();
|
|
18
|
-
}, 'dsh-im: close Telegram bot connections');
|
|
19
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'telegram',
|
|
14
|
+
rpcChannel: TELEGRAM_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
16
|
+
createHandler: controller => createTelegramRpcHandler(controller),
|
|
17
|
+
});
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export function createTelegramHostPlugin(config) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installWecomRpc } from './rpc.mjs';
|
|
2
|
+
import { createWecomRpcHandler, installWecomRpc, WECOM_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-wecom-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -8,20 +9,12 @@ export async function apply(ctx, config = {}) {
|
|
|
8
9
|
if (config?.controller) {
|
|
9
10
|
return installWecomRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
config.rpcOptions,
|
|
18
|
-
config.rpcAuthority,
|
|
19
|
-
);
|
|
20
|
-
ctx.effect(() => async () => {
|
|
21
|
-
await unregisterDelivery?.();
|
|
22
|
-
await production.close();
|
|
23
|
-
}, 'dsh-im: close Enterprise WeChat bot connections');
|
|
24
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'wecom',
|
|
14
|
+
rpcChannel: WECOM_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
16
|
+
createHandler: controller => createWecomRpcHandler(controller, config.rpcOptions),
|
|
17
|
+
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export function createWecomHostPlugin(config) {
|
|
@@ -228,12 +228,17 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
export function installWecomRpc(ctx, controller, options, authority) {
|
|
231
|
+
return installWecomRpcHandler(ctx, createWecomRpcHandler(controller, options), authority);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Mount the existing Connection transport before the production controller is ready. */
|
|
235
|
+
export function installWecomRpcHandler(ctx, handler, authority) {
|
|
231
236
|
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
232
237
|
throw new TypeError('DSH Host Connection RPC is required');
|
|
233
238
|
}
|
|
234
239
|
return ctx.connection.rpc.handle(
|
|
235
240
|
WECOM_RPC_CHANNEL,
|
|
236
|
-
|
|
241
|
+
handler,
|
|
237
242
|
{ authority: resolveRpcAuthority(authority) },
|
|
238
243
|
);
|
|
239
244
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createProductionController } from './production.mjs';
|
|
2
|
+
import { createWecomAppRpcHandler, installWecomAppRpc, WECOM_APP_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
4
|
+
|
|
5
|
+
export const name = 'dsh-im-wecom-app-host';
|
|
6
|
+
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
7
|
+
|
|
8
|
+
export async function apply(ctx, config = {}) {
|
|
9
|
+
if (config?.controller) {
|
|
10
|
+
return installWecomAppRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
11
|
+
}
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'wecom-app',
|
|
14
|
+
rpcChannel: WECOM_APP_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
16
|
+
createHandler: controller => createWecomAppRpcHandler(controller, config.rpcOptions),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createWecomAppHostPlugin(config) {
|
|
21
|
+
return Object.freeze({ name, inject, apply: (ctx) => apply(ctx, config) });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { createProductionController } from './production.mjs';
|
|
25
|
+
export {
|
|
26
|
+
WECOM_APP_ENDPOINTS,
|
|
27
|
+
WECOM_APP_RPC_CHANNEL,
|
|
28
|
+
WECOM_APP_RPC_ENDPOINTS,
|
|
29
|
+
createWecomAppRpcHandler,
|
|
30
|
+
installWecomAppRpc,
|
|
31
|
+
} from './rpc.mjs';
|
|
32
|
+
export { WecomAppController } from '../../../../src/channels/wecom-app/wecom-app-controller.mjs';
|
|
33
|
+
export { WecomAppRuntime } from '../../../../src/channels/wecom-app/wecom-app-runtime.mjs';
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { unlink } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { WecomAppConfigStore } from '../../../../src/channels/wecom-app/config-store.mjs';
|
|
6
|
+
import { WecomAppHarnessClient } from '../../../../src/channels/wecom-app/harness-client.mjs';
|
|
7
|
+
import { WecomAppStateStore } from '../../../../src/channels/wecom-app/state-store.mjs';
|
|
8
|
+
import { WecomAppController } from '../../../../src/channels/wecom-app/wecom-app-controller.mjs';
|
|
9
|
+
import { WecomAppRuntime } from '../../../../src/channels/wecom-app/wecom-app-runtime.mjs';
|
|
10
|
+
import { WecomAppCallbackServer } from '../../../../src/channels/wecom-app/callback-server.mjs';
|
|
11
|
+
import {
|
|
12
|
+
BotWorkspaceStore,
|
|
13
|
+
createBotWorkspaceScope,
|
|
14
|
+
createWorkspaceAwareController,
|
|
15
|
+
observeBotWorkspaceRemovals,
|
|
16
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
|
+
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
|
|
18
|
+
import { listModelCatalog } from '../../../../src/channels/shared/model-setting.mjs';
|
|
19
|
+
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
|
|
20
|
+
import { createConnectionSupervisor } from '../wecom/connection-supervisor.mjs';
|
|
21
|
+
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
|
|
22
|
+
import { harnessConnection } from '../../harness-connection.mjs';
|
|
23
|
+
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
|
|
24
|
+
import {
|
|
25
|
+
getInboundTtlRuntime,
|
|
26
|
+
registerInboundTtlWorkspaces,
|
|
27
|
+
} from '../../inbound-ttl-runtime.mjs';
|
|
28
|
+
import {
|
|
29
|
+
accessPolicyProvider,
|
|
30
|
+
initialAccessPolicyFor,
|
|
31
|
+
} from '../shared/access-policy-production.mjs';
|
|
32
|
+
|
|
33
|
+
function pluginPaths(config) {
|
|
34
|
+
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
|
|
35
|
+
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-wecom-app'));
|
|
36
|
+
return {
|
|
37
|
+
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
38
|
+
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
39
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeOrigin(value) {
|
|
44
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
45
|
+
return raw ? raw.replace(/\/+$/u, '') : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
49
|
+
if (!ctx?.credentials) throw new TypeError('dsh-im Enterprise WeChat app requires ctx.credentials');
|
|
50
|
+
const connection = harnessConnection(ctx, config);
|
|
51
|
+
|
|
52
|
+
const ConfigStore = internals.ConfigStore ?? WecomAppConfigStore;
|
|
53
|
+
const StateStore = internals.StateStore ?? WecomAppStateStore;
|
|
54
|
+
const Harness = internals.HarnessClient ?? WecomAppHarnessClient;
|
|
55
|
+
const Controller = internals.Controller ?? WecomAppController;
|
|
56
|
+
const Runtime = internals.Runtime ?? WecomAppRuntime;
|
|
57
|
+
const CallbackServer = internals.CallbackServer ?? WecomAppCallbackServer;
|
|
58
|
+
const createSupervisor = internals.createConnectionSupervisor ?? createConnectionSupervisor;
|
|
59
|
+
const baseLogger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:wecom-app') : (ctx.logger ?? console);
|
|
60
|
+
// The cordis logger never surfaces in the web profile journal; tee every
|
|
61
|
+
// level to the process console so callback verification attempts stay
|
|
62
|
+
// observable (journald captures the dsh web process stdout).
|
|
63
|
+
const logger = {
|
|
64
|
+
log: (...args) => { baseLogger.log?.(...args); console.log('[dsh-im:wecom-app]', ...args); },
|
|
65
|
+
info: (...args) => { baseLogger.info?.(...args); console.info('[dsh-im:wecom-app]', ...args); },
|
|
66
|
+
warn: (...args) => { baseLogger.warn?.(...args); console.warn('[dsh-im:wecom-app]', ...args); },
|
|
67
|
+
error: (...args) => { baseLogger.error?.(...args); console.error('[dsh-im:wecom-app]', ...args); },
|
|
68
|
+
};
|
|
69
|
+
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
|
|
70
|
+
const paths = pluginPaths(config);
|
|
71
|
+
const configStore = await new ConfigStore(paths.config).load();
|
|
72
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
73
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
74
|
+
const workspaces = internals.workspaces
|
|
75
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
76
|
+
const configuredBots = configStore.list();
|
|
77
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
78
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
|
|
79
|
+
defaultAgentPreset: config.agentPreset,
|
|
80
|
+
initialAccessPolicy: initialAccessPolicyFor('wecom-app', bot),
|
|
81
|
+
})));
|
|
82
|
+
const observedConfigStore = typeof configStore.remove === 'function'
|
|
83
|
+
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
84
|
+
: configStore;
|
|
85
|
+
const callbackServer = internals.callbackServer ?? new CallbackServer({
|
|
86
|
+
host: config.callbackListenHost ?? '127.0.0.1',
|
|
87
|
+
port: config.callbackListenPort ?? 30987,
|
|
88
|
+
logger,
|
|
89
|
+
});
|
|
90
|
+
// The callback listener starts on demand with the first bound app and
|
|
91
|
+
// stops once the last app is removed, so hosts that never use this channel
|
|
92
|
+
// do not keep port 30987 occupied.
|
|
93
|
+
const ensureCallbackListener = async () => {
|
|
94
|
+
if (callbackServer.listening !== true) await callbackServer.start();
|
|
95
|
+
return callbackServer;
|
|
96
|
+
};
|
|
97
|
+
const baseRegisterRoute = callbackServer.registerRoute.bind(callbackServer);
|
|
98
|
+
callbackServer.registerRoute = (route) => ensureCallbackListener().then(() => baseRegisterRoute(route));
|
|
99
|
+
const baseUnregisterRoute = callbackServer.unregisterRoute.bind(callbackServer);
|
|
100
|
+
callbackServer.unregisterRoute = (botId) => {
|
|
101
|
+
baseUnregisterRoute(botId);
|
|
102
|
+
if (callbackServer.routeCount() === 0 && callbackServer.listening) return callbackServer.stop();
|
|
103
|
+
};
|
|
104
|
+
const publicOrigin = normalizeOrigin(config.callbackBaseUrl);
|
|
105
|
+
const buildCallbackUrl = (bot) => {
|
|
106
|
+
const origin = normalizeOrigin(bot?.callbackBaseUrl) ?? publicOrigin;
|
|
107
|
+
return origin ? `${origin}${callbackServer.routePath(bot)}` : null;
|
|
108
|
+
};
|
|
109
|
+
const stateStores = new Map();
|
|
110
|
+
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
111
|
+
const stateFor = async (botId) => {
|
|
112
|
+
let state = stateStores.get(botId);
|
|
113
|
+
if (!state) {
|
|
114
|
+
state = await new StateStore(statePath(botId)).load();
|
|
115
|
+
stateStores.set(botId, state);
|
|
116
|
+
}
|
|
117
|
+
return state;
|
|
118
|
+
};
|
|
119
|
+
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
120
|
+
const inboundTtl = internals.inboundTtl ?? getInboundTtlRuntime(ctx, config);
|
|
121
|
+
const inboundTtlService = inboundTtl?.service ?? inboundTtl;
|
|
122
|
+
registerInboundTtlWorkspaces(ctx, inboundTtlService, {
|
|
123
|
+
workspaces,
|
|
124
|
+
configStore: observedConfigStore,
|
|
125
|
+
defaultWorkspace,
|
|
126
|
+
});
|
|
127
|
+
const { controlExecutor, sessionMaintenanceExecutor, fileIngressExecutor } = createHarnessSessionExecutors(ctx, {
|
|
128
|
+
controlExecutor: internals.controlExecutor,
|
|
129
|
+
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
130
|
+
fileIngressExecutor: internals.fileIngressExecutor,
|
|
131
|
+
inboundTtlService,
|
|
132
|
+
});
|
|
133
|
+
const harness = new Harness({
|
|
134
|
+
...connection,
|
|
135
|
+
workspace: defaultWorkspace,
|
|
136
|
+
autostart: false,
|
|
137
|
+
dshBin: config.dshBin ?? 'dsh',
|
|
138
|
+
...(commandExecutor ? { commandExecutor } : {}),
|
|
139
|
+
...(controlExecutor ? { controlExecutor } : {}),
|
|
140
|
+
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
141
|
+
...(fileIngressExecutor ? { fileIngressExecutor } : {}),
|
|
142
|
+
});
|
|
143
|
+
const modelCatalog = () => listModelCatalog(harness);
|
|
144
|
+
const coreController = new Controller({
|
|
145
|
+
credentials: ctx.credentials,
|
|
146
|
+
configStore: observedConfigStore,
|
|
147
|
+
buildCallbackUrl,
|
|
148
|
+
logger,
|
|
149
|
+
createRuntime: async ({ botId, config: botConfig, secrets }) => {
|
|
150
|
+
const state = await stateFor(botId);
|
|
151
|
+
await workspaces.ensure(botId, {
|
|
152
|
+
defaultAgentPreset: config.agentPreset,
|
|
153
|
+
initialAccessPolicy: initialAccessPolicyFor('wecom-app', botConfig),
|
|
154
|
+
});
|
|
155
|
+
const workspaceScope = createBotWorkspaceScope(harness, {
|
|
156
|
+
botId, workspaces, state, agentPresetCatalog,
|
|
157
|
+
});
|
|
158
|
+
return new Runtime({
|
|
159
|
+
config: botConfig,
|
|
160
|
+
secrets,
|
|
161
|
+
harness: workspaceScope.harness,
|
|
162
|
+
state: workspaceScope.state,
|
|
163
|
+
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
|
|
164
|
+
accessPolicy: accessPolicyProvider(workspaces, botId, {
|
|
165
|
+
channel: 'wecom-app', config: botConfig,
|
|
166
|
+
}),
|
|
167
|
+
callbackServer,
|
|
168
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
169
|
+
logger: {
|
|
170
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
171
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
172
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
173
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
deleteState: async ({ botId }) => {
|
|
178
|
+
const state = stateStores.get(botId);
|
|
179
|
+
stateStores.delete(botId);
|
|
180
|
+
if (state && typeof state.remove === 'function') {
|
|
181
|
+
await state.remove();
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
await unlink(statePath(botId));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
const controller = createWorkspaceAwareController(coreController, {
|
|
192
|
+
workspaces,
|
|
193
|
+
stateFor,
|
|
194
|
+
agentPresetCatalog,
|
|
195
|
+
modelCatalog,
|
|
196
|
+
});
|
|
197
|
+
const supervisor = createSupervisor({
|
|
198
|
+
controller,
|
|
199
|
+
harness,
|
|
200
|
+
logger,
|
|
201
|
+
retryDelaysMs: config.retryDelaysMs,
|
|
202
|
+
healthyIntervalMs: config.healthyIntervalMs,
|
|
203
|
+
}).start();
|
|
204
|
+
return {
|
|
205
|
+
controller,
|
|
206
|
+
deliveryAdapter: createDeliveryAdapter({
|
|
207
|
+
channel: 'wecom-app', workspaces, coreController, stateFor,
|
|
208
|
+
}),
|
|
209
|
+
ready: supervisor.ready,
|
|
210
|
+
async close() {
|
|
211
|
+
await supervisor.close();
|
|
212
|
+
await controller.close();
|
|
213
|
+
await callbackServer.stop();
|
|
214
|
+
harness.stopManagedProcess();
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
|
2
|
+
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
|
3
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
|
+
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
5
|
+
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
|
6
|
+
import { SET_MODEL_ENDPOINT, validModelPayload } from '../shared/model-setting-rpc.mjs';
|
|
7
|
+
import {
|
|
8
|
+
connectionTestTargetUnavailable,
|
|
9
|
+
publicConnectionTestResult,
|
|
10
|
+
} from '../../../../src/channels/shared/connection-test.mjs';
|
|
11
|
+
|
|
12
|
+
export const WECOM_APP_RPC_CHANNEL = '/wecom-app';
|
|
13
|
+
export const WECOM_APP_ENDPOINTS = Object.freeze({
|
|
14
|
+
status: 'connection.status',
|
|
15
|
+
bindApp: 'bot.bind',
|
|
16
|
+
updateSettings: 'bot.settings.update',
|
|
17
|
+
resetCallbackSecret: 'bot.callback-secret.reset',
|
|
18
|
+
reconnectBot: 'bot.reconnect',
|
|
19
|
+
deleteBot: 'bot.delete',
|
|
20
|
+
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
21
|
+
setModel: SET_MODEL_ENDPOINT,
|
|
22
|
+
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
23
|
+
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
|
|
24
|
+
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
|
|
25
|
+
});
|
|
26
|
+
export const WECOM_APP_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_APP_ENDPOINTS));
|
|
27
|
+
|
|
28
|
+
const FORBIDDEN_PUBLIC_KEYS = new Set([
|
|
29
|
+
'secret', 'secretRef', 'token', 'encodingAESKey', 'callbackTokenRef', 'callbackKeyRef',
|
|
30
|
+
'callbackSecret', 'corpSecret', 'bot_info',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function exactKeys(value, allowed) {
|
|
38
|
+
return isRecord(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validId(value) {
|
|
42
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validCredential(value, maxLength) {
|
|
46
|
+
return typeof value === 'string' && value.trim().length > 0 && value.length <= maxLength;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function payloadFailure(endpoint, payload) {
|
|
50
|
+
if (!isRecord(payload)) return 'Payload must be an object.';
|
|
51
|
+
if (endpoint === WECOM_APP_ENDPOINTS.status) return exactKeys(payload, []) ? null : 'connection.status does not accept fields.';
|
|
52
|
+
if (endpoint === WECOM_APP_ENDPOINTS.bindApp) {
|
|
53
|
+
const allowed = ['corpId', 'agentId', 'secret', 'token', 'encodingAESKey', 'apiBaseUrl', 'callbackBaseUrl', 'streamEnabled'];
|
|
54
|
+
if (!exactKeys(payload, allowed)) return 'bot.bind received unsupported fields.';
|
|
55
|
+
if (!validCredential(payload.corpId, 128) || !validCredential(payload.agentId, 32)
|
|
56
|
+
|| !validCredential(payload.secret, 256) || !validCredential(payload.token, 128)
|
|
57
|
+
|| !validCredential(payload.encodingAESKey, 128)) {
|
|
58
|
+
return 'bot.bind requires corpId, agentId, secret, token, and encodingAESKey.';
|
|
59
|
+
}
|
|
60
|
+
if (payload.apiBaseUrl !== undefined && payload.apiBaseUrl !== null && typeof payload.apiBaseUrl !== 'string') {
|
|
61
|
+
return 'bot.bind received an invalid apiBaseUrl.';
|
|
62
|
+
}
|
|
63
|
+
if (payload.callbackBaseUrl !== undefined && payload.callbackBaseUrl !== null && typeof payload.callbackBaseUrl !== 'string') {
|
|
64
|
+
return 'bot.bind received an invalid callbackBaseUrl.';
|
|
65
|
+
}
|
|
66
|
+
if (payload.streamEnabled !== undefined && payload.streamEnabled !== true && payload.streamEnabled !== false) {
|
|
67
|
+
return 'bot.bind received an invalid streamEnabled flag.';
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (endpoint === WECOM_APP_ENDPOINTS.updateSettings) {
|
|
72
|
+
if (!exactKeys(payload, ['botId', 'apiBaseUrl', 'callbackBaseUrl', 'streamEnabled']) || !validId(payload.botId)) {
|
|
73
|
+
return 'bot.settings.update requires a botId and settings fields.';
|
|
74
|
+
}
|
|
75
|
+
if (payload.apiBaseUrl !== undefined && payload.apiBaseUrl !== null && typeof payload.apiBaseUrl !== 'string') {
|
|
76
|
+
return 'bot.settings.update received an invalid apiBaseUrl.';
|
|
77
|
+
}
|
|
78
|
+
if (payload.callbackBaseUrl !== undefined && payload.callbackBaseUrl !== null && typeof payload.callbackBaseUrl !== 'string') {
|
|
79
|
+
return 'bot.settings.update received an invalid callbackBaseUrl.';
|
|
80
|
+
}
|
|
81
|
+
if (payload.streamEnabled !== undefined && payload.streamEnabled !== true && payload.streamEnabled !== false) {
|
|
82
|
+
return 'bot.settings.update received an invalid streamEnabled flag.';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (endpoint === WECOM_APP_ENDPOINTS.resetCallbackSecret) {
|
|
87
|
+
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
88
|
+
? null : 'bot.callback-secret.reset requires a botId.';
|
|
89
|
+
}
|
|
90
|
+
if (endpoint === WECOM_APP_ENDPOINTS.reconnectBot) {
|
|
91
|
+
return exactKeys(payload, ['botId', 'sendTest'])
|
|
92
|
+
&& validId(payload.botId)
|
|
93
|
+
&& (payload.sendTest === undefined || payload.sendTest === true)
|
|
94
|
+
? null : 'bot.reconnect requires a botId and optional sendTest=true.';
|
|
95
|
+
}
|
|
96
|
+
if (endpoint === WECOM_APP_ENDPOINTS.deleteBot) {
|
|
97
|
+
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
98
|
+
? null : 'bot.delete requires a botId and confirm=true.';
|
|
99
|
+
}
|
|
100
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setWorkspace) {
|
|
101
|
+
return validWorkspacePayload(payload) ? null : '请输入工作区绝对路径。';
|
|
102
|
+
}
|
|
103
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setModel) {
|
|
104
|
+
return validModelPayload(payload) ? null : '请选择有效模型。';
|
|
105
|
+
}
|
|
106
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setAgentPreset) {
|
|
107
|
+
return validAgentPresetPayload(payload) ? null : '请选择 Agent Preset。';
|
|
108
|
+
}
|
|
109
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setContextEnhancement) {
|
|
110
|
+
return validContextEnhancementPayload(payload) ? null : '请提交有效的上下文增强设置。';
|
|
111
|
+
}
|
|
112
|
+
if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
|
|
113
|
+
return validAccessPolicyPayload(payload) ? null : '请提交有效的访问设置。';
|
|
114
|
+
}
|
|
115
|
+
return 'Unknown Enterprise WeChat app endpoint.';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sanitizePublic(value) {
|
|
119
|
+
if (Array.isArray(value)) return value.map(sanitizePublic);
|
|
120
|
+
if (!isRecord(value)) return value;
|
|
121
|
+
const safe = {};
|
|
122
|
+
for (const [key, child] of Object.entries(value)) {
|
|
123
|
+
if (!FORBIDDEN_PUBLIC_KEYS.has(key)) safe[key] = sanitizePublic(child);
|
|
124
|
+
}
|
|
125
|
+
return safe;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function publicStatus(status) {
|
|
129
|
+
return sanitizePublic(structuredClone(status));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createWecomAppRpcHandler(controller) {
|
|
133
|
+
for (const method of ['status', 'bindApp', 'updateAppSettings', 'resetCallbackSecret', 'reconnectBot', 'deleteBot']) {
|
|
134
|
+
if (typeof controller?.[method] !== 'function') {
|
|
135
|
+
throw new TypeError(`A complete Enterprise WeChat app controller is required (${method})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return async (endpoint, payload, signal) => {
|
|
139
|
+
if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
140
|
+
if (!WECOM_APP_RPC_ENDPOINTS.includes(endpoint)) {
|
|
141
|
+
return { ok: false, error: { code: 'bad-request', message: 'Unknown Enterprise WeChat app endpoint.', details: {} } };
|
|
142
|
+
}
|
|
143
|
+
const invalid = payloadFailure(endpoint, payload);
|
|
144
|
+
if (invalid) return { ok: false, error: { code: 'bad-request', message: invalid } };
|
|
145
|
+
try {
|
|
146
|
+
let value;
|
|
147
|
+
if (endpoint === WECOM_APP_ENDPOINTS.status) value = await publicStatus(await controller.status());
|
|
148
|
+
else if (endpoint === WECOM_APP_ENDPOINTS.bindApp) {
|
|
149
|
+
value = await publicStatus(await controller.bindApp(payload));
|
|
150
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.updateSettings) {
|
|
151
|
+
value = await publicStatus(await controller.updateAppSettings(payload.botId, {
|
|
152
|
+
apiBaseUrl: payload.apiBaseUrl,
|
|
153
|
+
callbackBaseUrl: payload.callbackBaseUrl,
|
|
154
|
+
streamEnabled: payload.streamEnabled,
|
|
155
|
+
}));
|
|
156
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.resetCallbackSecret) {
|
|
157
|
+
value = await publicStatus(await controller.resetCallbackSecret(payload.botId));
|
|
158
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.reconnectBot) {
|
|
159
|
+
const snapshot = await controller.reconnectBot(payload.botId);
|
|
160
|
+
if (signal?.aborted) {
|
|
161
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
162
|
+
}
|
|
163
|
+
let testMessage;
|
|
164
|
+
if (payload.sendTest === true) {
|
|
165
|
+
const connected = snapshot?.bots?.some(
|
|
166
|
+
(bot) => bot?.botId === payload.botId && bot?.connected === true,
|
|
167
|
+
);
|
|
168
|
+
if (!connected || typeof controller.sendConnectionTest !== 'function') {
|
|
169
|
+
testMessage = publicConnectionTestResult(connectionTestTargetUnavailable('企业微信应用'));
|
|
170
|
+
} else {
|
|
171
|
+
try {
|
|
172
|
+
await controller.sendConnectionTest(payload.botId);
|
|
173
|
+
testMessage = publicConnectionTestResult();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
testMessage = publicConnectionTestResult(error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) });
|
|
180
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setWorkspace) {
|
|
181
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
182
|
+
value = await publicStatus(await controller.updateWorkspace(payload.botId, payload.workspace));
|
|
183
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setModel) {
|
|
184
|
+
if (typeof controller.updateModel !== 'function') throw new Error('Model update is unavailable');
|
|
185
|
+
value = await publicStatus(await controller.updateModel(payload.botId, payload.model));
|
|
186
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setContextEnhancement) {
|
|
187
|
+
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
|
|
188
|
+
value = await controller.updateContextEnhancement(
|
|
189
|
+
payload.botId, payload.config, (status) => publicStatus(status),
|
|
190
|
+
);
|
|
191
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setAccessPolicy) {
|
|
192
|
+
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
|
|
193
|
+
value = await controller.updateAccessPolicy(
|
|
194
|
+
payload.botId, payload.policy, (status) => publicStatus(status),
|
|
195
|
+
);
|
|
196
|
+
} else if (endpoint === WECOM_APP_ENDPOINTS.setAgentPreset) {
|
|
197
|
+
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent Preset update is unavailable');
|
|
198
|
+
value = await publicStatus(await controller.updateAgentPreset(payload.botId, payload.agentPreset));
|
|
199
|
+
} else {
|
|
200
|
+
value = await publicStatus(await controller.deleteBot(payload.botId));
|
|
201
|
+
}
|
|
202
|
+
return signal?.aborted
|
|
203
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } }
|
|
204
|
+
: { ok: true, value };
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const workspaceError = publicWorkspaceError(error);
|
|
207
|
+
return signal?.aborted
|
|
208
|
+
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } }
|
|
209
|
+
: { ok: false, error: workspaceError
|
|
210
|
+
? { ...workspaceError, details: {} }
|
|
211
|
+
: { code: 'wecom-app-operation-failed', message: '企业微信应用操作失败,请稍后重试。', details: {} } };
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function installWecomAppRpc(ctx, controller, options, authority) {
|
|
217
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
218
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
219
|
+
}
|
|
220
|
+
return ctx.connection.rpc.handle(
|
|
221
|
+
WECOM_APP_RPC_CHANNEL,
|
|
222
|
+
createWecomAppRpcHandler(controller, options),
|
|
223
|
+
{ authority: resolveRpcAuthority(authority) },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installWeixinRpc } from './rpc.mjs';
|
|
2
|
+
import { createWeixinRpcHandler, installWeixinRpc, WEIXIN_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-weixin-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
@@ -9,20 +10,12 @@ export async function apply(ctx, config = {}) {
|
|
|
9
10
|
return installWeixinRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
config.rpcOptions,
|
|
19
|
-
config.rpcAuthority,
|
|
20
|
-
);
|
|
21
|
-
ctx.effect(() => async () => {
|
|
22
|
-
await unregisterDelivery?.();
|
|
23
|
-
await production.close();
|
|
24
|
-
}, 'dsh-weixin: close account connections');
|
|
25
|
-
return disposeRpc;
|
|
13
|
+
return installProductionChannel(ctx, config, {
|
|
14
|
+
channel: 'weixin',
|
|
15
|
+
rpcChannel: WEIXIN_RPC_CHANNEL,
|
|
16
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
17
|
+
createHandler: controller => createWeixinRpcHandler(controller, config.rpcOptions),
|
|
18
|
+
});
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
export function createWeixinHostPlugin(config) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installWhatsappRpc } from './rpc.mjs';
|
|
2
|
+
import { createWhatsappRpcHandler, installWhatsappRpc, WHATSAPP_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-whatsapp-host';
|
|
5
6
|
export const inject = ['connection', 'typertGateway'];
|
|
@@ -8,20 +9,12 @@ export async function apply(ctx, config = {}) {
|
|
|
8
9
|
if (config?.controller) {
|
|
9
10
|
return installWhatsappRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
config.rpcOptions,
|
|
18
|
-
config.rpcAuthority,
|
|
19
|
-
);
|
|
20
|
-
ctx.effect(() => async () => {
|
|
21
|
-
await unregisterDelivery?.();
|
|
22
|
-
await production.close();
|
|
23
|
-
}, 'dsh-im: close WhatsApp Web connections');
|
|
24
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'whatsapp',
|
|
14
|
+
rpcChannel: WHATSAPP_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
16
|
+
createHandler: controller => createWhatsappRpcHandler(controller, config.rpcOptions),
|
|
17
|
+
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export function createWhatsappHostPlugin(config) {
|
|
@@ -9,6 +9,7 @@ const CHANNELS = new Set([
|
|
|
9
9
|
'feishu',
|
|
10
10
|
'dingtalk',
|
|
11
11
|
'wecom',
|
|
12
|
+
'wecom-app',
|
|
12
13
|
'qq',
|
|
13
14
|
'slack',
|
|
14
15
|
'telegram',
|
|
@@ -82,6 +83,9 @@ function normalizeRoute(channel, kind, route) {
|
|
|
82
83
|
case 'wecom':
|
|
83
84
|
oneOf(kind, ['user', 'group']);
|
|
84
85
|
return routeWithStrings(route, ['chatId']);
|
|
86
|
+
case 'wecom-app':
|
|
87
|
+
oneOf(kind, ['user']);
|
|
88
|
+
return routeWithStrings(route, ['chatId']);
|
|
85
89
|
case 'qq':
|
|
86
90
|
oneOf(kind, ['user', 'group']);
|
|
87
91
|
return routeWithStrings(route, kind === 'user' ? ['userOpenId'] : ['groupOpenId']);
|