fix: use in-process Harness API for local plugin connections

This commit is contained in:
xmanrui 2026-08-28 01:24:17 +08:00
parent f4ded46278
commit f0b6b38b45
40 changed files with 1413 additions and 603 deletions

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installDingtalkRpc } from './rpc.mjs';
export const name = 'dsh-dingtalk-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -17,17 +17,9 @@ import {
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createConnectionSupervisor } from './connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-dingtalk requires an initialized DSH webServer port');
}
return new URL(`http://127.0.0.1:${port}`);
}
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-dingtalk'));
@ -41,7 +33,7 @@ function pluginPaths(config) {
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-dingtalk requires ctx.credentials');
if (!ctx?.webServer) throw new TypeError('dsh-dingtalk requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const ConfigStore = internals.ConfigStore ?? DingtalkConfigStore;
const DeviceAuth = internals.DeviceAuth ?? DingtalkDeviceAuth;
@ -92,7 +84,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installDiscordRpc } from './rpc.mjs';
export const name = 'dsh-im-discord-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -3,7 +3,7 @@ import { createProductionController } from './production.mjs';
import { installFeishuRpc } from './rpc.mjs';
export const name = 'dsh-feishu-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
function controllerFrom(ctx, config) {
if (config?.controller) return config.controller;

View file

@ -5,6 +5,7 @@ import * as Lark from '@larksuiteoapi/node-sdk';
import HttpsProxyAgent from 'https-proxy-agent';
import { createConnectionSupervisor } from './connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
import { verifyFeishuApp } from '../../../../src/channels/feishu/feishu-app.mjs';
import { FeishuRuntime } from '../../../../src/channels/feishu/feishu-runtime.mjs';
@ -39,17 +40,6 @@ export function createFeishuWebSocketAgent(
return proxyUrl ? createAgent(proxyUrl) : undefined;
}
function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-feishu requires an initialized DSH webServer port');
}
// Even when DSH listens on all interfaces, its own plugin talks through the
// loopback authority accepted by Connection's request-trust fence.
return new URL(`http://127.0.0.1:${port}`);
}
function pluginPaths(config) {
const dshHome = resolve(config.dshHome
?? process.env.DSH_HOME
@ -70,7 +60,7 @@ function pluginPaths(config) {
*/
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-feishu requires ctx.credentials');
if (!ctx?.webServer) throw new TypeError('dsh-feishu requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const lark = internals.lark ?? Lark;
const Controller = internals.Controller ?? MultiBotDshFeishuController;
@ -139,7 +129,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
// This plugin is already hosted by a running DSH process. Starting a
// second DSH would create a competing server and lifecycle.

View file

@ -5,7 +5,7 @@ import { OfficeConfigStore } from '../../../../src/channels/office/config-store.
import { OfficeController } from '../../../../src/channels/office/office-controller.mjs';
import { OfficeRuntime } from '../../../../src/channels/office/office-runtime.mjs';
import { HarnessClient } from '../../../../src/channels/shared/harness-client.mjs';
import { harnessOrigin } from '../shared/production.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
export function officePaths(config = {}) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@ -14,6 +14,7 @@ export function officePaths(config = {}) {
}
export async function createProductionController(ctx, config = {}, internals = {}) {
const connection = harnessConnection(ctx, config);
const Store = internals.ConfigStore ?? OfficeConfigStore;
const Controller = internals.Controller ?? OfficeController;
const Runtime = internals.Runtime ?? OfficeRuntime;
@ -21,9 +22,8 @@ export async function createProductionController(ctx, config = {}, internals = {
const paths = officePaths(config);
const configStore = await new Store(paths.config).load();
const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:office') : (ctx.logger ?? console);
const harnessBaseUrl = harnessOrigin(ctx.webServer, config.harnessBaseUrl);
const createHarness = internals.createHarness ?? (({ workspace }) => new ResolvedHarness({
baseUrl: harnessBaseUrl,
...connection,
workspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installQqRpc } from './rpc.mjs';
export const name = 'dsh-im-qq-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -17,17 +17,9 @@ import {
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createConnectionSupervisor } from './connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-im QQ requires an initialized DSH webServer port');
}
return new URL(`http://127.0.0.1:${port}`);
}
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-qq'));
@ -40,7 +32,7 @@ function pluginPaths(config) {
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-im QQ requires ctx.credentials');
if (!ctx?.webServer) throw new TypeError('dsh-im QQ requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const ConfigStore = internals.ConfigStore ?? QqConfigStore;
const StateStore = internals.StateStore ?? QqStateStore;
@ -83,7 +75,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -4,6 +4,7 @@ import { join, resolve } from 'node:path';
import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
import {
BotWorkspaceStore,
@ -13,15 +14,6 @@ import {
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
export function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-im token channel requires an initialized DSH webServer port');
}
return new URL(`http://127.0.0.1:${port}`);
}
export function pluginPaths(config, channel) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', `dsh-${channel}`));
@ -37,7 +29,7 @@ export async function createTokenProductionController(ctx, config, internals, de
channel, ConfigStore, StateStore, HarnessClient, Controller, Runtime, runtimeOptions,
} = definitions;
if (!ctx?.credentials) throw new TypeError(`dsh-im ${channel} requires ctx.credentials`);
if (!ctx?.webServer) throw new TypeError(`dsh-im ${channel} requires ctx.webServer`);
const connection = harnessConnection(ctx, config);
const ResolvedConfigStore = internals.ConfigStore ?? ConfigStore;
const ResolvedStateStore = internals.StateStore ?? StateStore;
@ -84,7 +76,7 @@ export async function createTokenProductionController(ctx, config, internals, de
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new ResolvedHarness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installSlackRpc } from './rpc.mjs';
export const name = 'dsh-im-slack-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) return installSlackRpc(ctx, config.controller, config.rpcAuthority);

View file

@ -14,13 +14,14 @@ import {
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
import { harnessOrigin, pluginPaths } from '../shared/production.mjs';
import { pluginPaths } from '../shared/production.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
if (!ctx?.webServer) throw new TypeError('dsh-im slack requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const ResolvedConfigStore = internals.ConfigStore ?? SlackConfigStore;
const ResolvedStateStore = internals.StateStore ?? SlackStateStore;
@ -62,7 +63,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new ResolvedHarness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installTelegramRpc } from './rpc.mjs';
export const name = 'dsh-im-telegram-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installWecomRpc } from './rpc.mjs';
export const name = 'dsh-im-wecom-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -17,17 +17,9 @@ import {
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createConnectionSupervisor } from './connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-im Enterprise WeChat requires an initialized DSH webServer port');
}
return new URL(`http://127.0.0.1:${port}`);
}
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-wecom'));
@ -40,7 +32,7 @@ function pluginPaths(config) {
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-im Enterprise WeChat requires ctx.credentials');
if (!ctx?.webServer) throw new TypeError('dsh-im Enterprise WeChat requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const ConfigStore = internals.ConfigStore ?? WecomConfigStore;
const StateStore = internals.StateStore ?? WecomStateStore;
@ -86,7 +78,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installWeixinRpc } from './rpc.mjs';
export const name = 'dsh-weixin-host';
export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
export const inject = ['connection', 'credentials', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -20,17 +20,9 @@ import {
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createConnectionSupervisor } from './connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-weixin requires an initialized DSH webServer port');
}
return new URL(`http://127.0.0.1:${port}`);
}
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-weixin'));
@ -44,7 +36,7 @@ function pluginPaths(config) {
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-weixin requires ctx.credentials');
if (!ctx?.webServer) throw new TypeError('dsh-weixin requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const ConfigStore = internals.ConfigStore ?? WeixinConfigStore;
const StateStore = internals.StateStore ?? WeixinStateStore;
@ -89,7 +81,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
import { installWhatsappRpc } from './rpc.mjs';
export const name = 'dsh-im-whatsapp-host';
export const inject = ['connection', 'webServer', 'typertGateway'];
export const inject = ['connection', 'apiProxy', 'typertGateway'];
export async function apply(ctx, config = {}) {
if (config?.controller) {

View file

@ -17,19 +17,11 @@ import {
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
function harnessOrigin(webServer, configured) {
if (configured !== undefined) return new URL(configured);
const port = webServer?.port;
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error('dsh-im WhatsApp requires an initialized DSH webServer port');
}
return new URL(`http://127.0.0.1:${port}`);
}
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-whatsapp'));
@ -47,7 +39,7 @@ function pluginPaths(config) {
}
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.webServer) throw new TypeError('dsh-im WhatsApp requires ctx.webServer');
const connection = harnessConnection(ctx, config);
const logger = typeof ctx.logger === 'function'
? ctx.logger('dsh-im:whatsapp') : (ctx.logger ?? console);
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
@ -89,7 +81,7 @@ export async function createProductionController(ctx, config = {}, internals = {
fileIngressExecutor: internals.fileIngressExecutor,
});
const harness = new Harness({
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
...connection,
workspace: defaultWorkspace,
autostart: false,
dshBin: config.dshBin ?? 'dsh',

View file

@ -0,0 +1,15 @@
/** Connect to this Host directly unless an external Harness URL was configured. */
export function harnessConnection(ctx, config = {}) {
if (config.harnessBaseUrl !== undefined) {
return { baseUrl: new URL(config.harnessBaseUrl) };
}
if (!ctx?.apiProxy) {
throw new TypeError('dsh-im requires the Host apiProxy service; check that DSH has finished loading its Host services');
}
return {
apiProxy: ctx.apiProxy,
// Cordis child contexts share one root; different Hosts must not share
// ownership of pending questions and approvals.
interactionScope: ctx.root ?? ctx,
};
}

View file

@ -15,7 +15,7 @@ export const name = 'dsh-im-host';
export const inject = [
'connection',
'credentials',
'webServer',
'apiProxy',
'typertGateway',
];