@xmanrui/dsh-im 4.15.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 +2 -1
- package/README.md +9 -5
- package/lib/client.js +1729 -614
- 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 +15 -3
- 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 +63 -0
- package/plugin-src/client/index.js +18 -0
- package/plugin-src/client/model-setting.js +135 -69
- 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 +7 -2
- package/scripts/verify-model-setting.mjs +54 -0
- package/src/channels/feishu/bridge.mjs +295 -9
- package/src/channels/feishu/feishu-cards.mjs +38 -0
- package/src/channels/feishu/feishu-channel.mjs +88 -20
- 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 +12 -0
- 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/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 { installQqRpc } from './rpc.mjs';
|
|
2
|
+
import { createQqRpcHandler, installQqRpc, QQ_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-qq-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 installQqRpc(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 QQ bot connections');
|
|
24
|
-
return disposeRpc;
|
|
12
|
+
return installProductionChannel(ctx, config, {
|
|
13
|
+
channel: 'qq',
|
|
14
|
+
rpcChannel: QQ_RPC_CHANNEL,
|
|
15
|
+
createProduction: () => createProductionController(ctx, config, config.internals),
|
|
16
|
+
createHandler: controller => createQqRpcHandler(controller, config.rpcOptions),
|
|
17
|
+
});
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export function createQqHostPlugin(config) {
|
|
@@ -91,7 +91,7 @@ export function initialAccessPolicyFor(channel, config = {}) {
|
|
|
91
91
|
group: allowlistScope(),
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
if (['dingtalk', 'wecom', 'slack', 'discord'].includes(key)) {
|
|
94
|
+
if (['dingtalk', 'wecom', 'wecom-app', 'slack', 'discord'].includes(key)) {
|
|
95
95
|
return createAccessPolicy({ direct: openScope(), group: openScope() });
|
|
96
96
|
}
|
|
97
97
|
throw new TypeError(`Unsupported access-policy channel: ${channel}`);
|
|
@@ -13,7 +13,6 @@ export function validModelPayload(payload) {
|
|
|
13
13
|
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
|
|
14
14
|
if (payload.model === null) return true;
|
|
15
15
|
return isRecord(payload.model)
|
|
16
|
-
&& Object.keys(payload.model).
|
|
17
|
-
&& Object.keys(payload.model).every((key) => ['provider', 'model'].includes(key))
|
|
16
|
+
&& Object.keys(payload.model).every((key) => ['provider', 'model', 'reasoningEffort'].includes(key))
|
|
18
17
|
&& normalizeModelSelection(payload.model) !== null;
|
|
19
18
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { t } from '../../../../src/channels/shared/i18n.mjs';
|
|
2
|
+
|
|
3
|
+
const CHANNEL_NAMES = {
|
|
4
|
+
weixin: '微信', feishu: '飞书', dingtalk: '钉钉', wecom: '企业微信',
|
|
5
|
+
'wecom-app': '企业微信应用', qq: 'QQ', slack: 'Slack', telegram: 'Telegram',
|
|
6
|
+
discord: 'Discord', whatsapp: 'WhatsApp', office: 'AI Office',
|
|
7
|
+
};
|
|
8
|
+
const INVALID_CONFIG_MESSAGES = new Set([
|
|
9
|
+
'dsh-weixin config contains invalid account data',
|
|
10
|
+
'dsh-feishu config contains an invalid bot entry',
|
|
11
|
+
'dsh-feishu config contains duplicate bot identities',
|
|
12
|
+
'dsh-feishu config is incomplete or invalid',
|
|
13
|
+
'dsh-dingtalk config contains invalid bot data',
|
|
14
|
+
...['Enterprise WeChat', 'Enterprise WeChat app', 'QQ', 'Slack', 'Telegram', 'Discord']
|
|
15
|
+
.map(channel => `dsh-im ${channel} config contains invalid bot data`),
|
|
16
|
+
'dsh-im WhatsApp config contains invalid account data',
|
|
17
|
+
'dsh-im AI Office config is invalid',
|
|
18
|
+
'dsh-im workspace config is invalid',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function publicChannelInitializing(channel) {
|
|
22
|
+
return {
|
|
23
|
+
code: `${channel}-initializing`,
|
|
24
|
+
message: t('{channel}正在初始化,请稍后重新读取。', { channel: t(CHANNEL_NAMES[channel]) }),
|
|
25
|
+
details: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Only public guidance crosses RPC; parser excerpts, paths and secrets stay in logs. */
|
|
30
|
+
export function publicChannelStartupError(channel, error) {
|
|
31
|
+
const params = { channel: t(CHANNEL_NAMES[channel]), id: channel };
|
|
32
|
+
if (error instanceof SyntaxError || INVALID_CONFIG_MESSAGES.has(error?.message)) {
|
|
33
|
+
return {
|
|
34
|
+
code: `${channel}-startup-config-invalid`,
|
|
35
|
+
message: channel === 'office'
|
|
36
|
+
? t('{channel}配置格式错误。请检查其数据目录中的 config.json,修复后重启 DSH。详细原因请查看启动日志。', params)
|
|
37
|
+
: t('{channel}配置格式错误。请检查其数据目录中的 config.json 和 workspaces.json,修复后重启 DSH。详细原因请查看启动日志。', params),
|
|
38
|
+
details: {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') {
|
|
42
|
+
return {
|
|
43
|
+
code: `${channel}-startup-permission-denied`,
|
|
44
|
+
message: t('无法读取或写入{channel}配置。请检查其数据目录的访问权限,修复后重启 DSH。详细原因请查看启动日志。', params),
|
|
45
|
+
details: {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
code: `${channel}-startup-failed`,
|
|
50
|
+
message: t('{channel}初始化失败。请查看 DSH 启动日志中 failed to activate {id} 后的错误,修复后重启 DSH。', params),
|
|
51
|
+
details: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
2
|
+
import { publicChannelInitializing, publicChannelStartupError } from './startup-error.mjs';
|
|
3
|
+
|
|
4
|
+
/** Mount the native management RPC before any fallible production initialization. */
|
|
5
|
+
export async function installProductionChannel(ctx, config, {
|
|
6
|
+
channel, rpcChannel, createProduction, createHandler,
|
|
7
|
+
}) {
|
|
8
|
+
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
|
9
|
+
throw new TypeError('DSH Host Connection RPC is required');
|
|
10
|
+
}
|
|
11
|
+
let startupError = publicChannelInitializing(channel);
|
|
12
|
+
let handler = async () => ({ ok: false, error: startupError });
|
|
13
|
+
const disposeRpc = ctx.connection.rpc.handle(rpcChannel, (endpoint, payload, signal) => {
|
|
14
|
+
if (signal?.aborted) {
|
|
15
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.', details: {} } };
|
|
16
|
+
}
|
|
17
|
+
return handler(endpoint, payload, signal);
|
|
18
|
+
}, { authority: resolveRpcAuthority(config.rpcAuthority) });
|
|
19
|
+
const logger = typeof ctx.logger === 'function' ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
|
|
20
|
+
let production;
|
|
21
|
+
let unregisterDelivery;
|
|
22
|
+
let closing;
|
|
23
|
+
const closeProduction = () => (closing ??= (async () => {
|
|
24
|
+
try {
|
|
25
|
+
await unregisterDelivery?.();
|
|
26
|
+
} finally {
|
|
27
|
+
await production?.close();
|
|
28
|
+
}
|
|
29
|
+
})());
|
|
30
|
+
try {
|
|
31
|
+
production = await createProduction();
|
|
32
|
+
unregisterDelivery = config.deliveryService && production.deliveryAdapter
|
|
33
|
+
? config.deliveryService.registerAdapter(production.deliveryAdapter) : undefined;
|
|
34
|
+
const readyHandler = createHandler(production.controller);
|
|
35
|
+
ctx.effect(() => closeProduction, `dsh-im: close ${channel} connections`);
|
|
36
|
+
handler = readyHandler;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
startupError = publicChannelStartupError(channel, error);
|
|
39
|
+
logger.error?.(`[dsh-im] failed to activate ${channel}; management RPC remains available`, error);
|
|
40
|
+
try {
|
|
41
|
+
await closeProduction();
|
|
42
|
+
} catch (cleanupError) {
|
|
43
|
+
logger.error?.(`[dsh-im] failed to close partially initialized ${channel} resources`, cleanupError);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return disposeRpc;
|
|
47
|
+
}
|
|
@@ -24,6 +24,7 @@ export function publicWorkspaceError(error) {
|
|
|
24
24
|
'agent-preset-unavailable',
|
|
25
25
|
'model-selection-invalid',
|
|
26
26
|
'model-selection-unavailable',
|
|
27
|
+
'model-reasoning-unavailable',
|
|
27
28
|
'context-enhancement-invalid',
|
|
28
29
|
].includes(error?.code)) return null;
|
|
29
30
|
return { code: error.code, message: error.message };
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { createProductionController } from './production.mjs';
|
|
2
|
-
import { installSlackRpc } from './rpc.mjs';
|
|
2
|
+
import { createSlackRpcHandler, installSlackRpc, SLACK_RPC_CHANNEL } from './rpc.mjs';
|
|
3
|
+
import { installProductionChannel } from '../shared/startup.mjs';
|
|
3
4
|
|
|
4
5
|
export const name = 'dsh-im-slack-host';
|
|
5
6
|
export const inject = ['connection', 'credentials', 'typertGateway'];
|
|
6
7
|
|
|
7
8
|
export async function apply(ctx, config = {}) {
|
|
8
9
|
if (config?.controller) return installSlackRpc(ctx, config.controller, config.rpcAuthority);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
await production.close();
|
|
16
|
-
}, 'dsh-im: close Slack bot connections');
|
|
17
|
-
return disposeRpc;
|
|
10
|
+
return installProductionChannel(ctx, config, {
|
|
11
|
+
channel: 'slack',
|
|
12
|
+
rpcChannel: SLACK_RPC_CHANNEL,
|
|
13
|
+
createProduction: () => createProductionController(ctx, config, config.internals ?? {}),
|
|
14
|
+
createHandler: controller => createSlackRpcHandler(controller),
|
|
15
|
+
});
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export function createSlackHostPlugin(config) {
|
|
@@ -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
|
+
}
|