mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
Mount IM RPC channels via plugin webServer to fix HTTP 405.
connection.rpc.handle looks up webServer on a Cordis fiber that does not inject it, so channel routes never registered after host restart. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
dd2365407e
commit
6107692418
18 changed files with 666 additions and 330 deletions
|
|
@ -571,7 +571,7 @@ var React25 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "dsh-im-ops",
|
name: "dsh-im-ops",
|
||||||
version: "4.9.1-ops.30",
|
version: "4.9.1-ops.31",
|
||||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
@ -637,6 +637,7 @@ var package_default = {
|
||||||
"plugin-src",
|
"plugin-src",
|
||||||
"scripts",
|
"scripts",
|
||||||
"src",
|
"src",
|
||||||
|
".npmrc",
|
||||||
"PROACTIVE_DELIVERY.md",
|
"PROACTIVE_DELIVERY.md",
|
||||||
"PROACTIVE_DELIVERY.en.md",
|
"PROACTIVE_DELIVERY.en.md",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
|
|
@ -676,7 +677,6 @@ var package_default = {
|
||||||
undici: "7.29.0"
|
undici: "7.29.0"
|
||||||
},
|
},
|
||||||
devDependencies: {
|
devDependencies: {
|
||||||
"@deepseek-ai/cordis": "4.0.1",
|
|
||||||
"@larksuiteoapi/node-sdk": "1.73.0",
|
"@larksuiteoapi/node-sdk": "1.73.0",
|
||||||
"@whiskeysockets/baileys": "7.0.0-rc14",
|
"@whiskeysockets/baileys": "7.0.0-rc14",
|
||||||
esbuild: "0.25.9",
|
esbuild: "0.25.9",
|
||||||
|
|
|
||||||
462
lib/index.js
462
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -3,6 +3,7 @@ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -336,12 +337,10 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installDingtalkRpc(ctx, controller, options, authority) {
|
export function installDingtalkRpc(ctx, controller, options, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
DINGTALK_RPC_CHANNEL,
|
DINGTALK_RPC_CHANNEL,
|
||||||
createDingtalkRpcHandler(controller, options),
|
createDingtalkRpcHandler(controller, options),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { publicConnectionTestResult } from '../../../../src/channels/shared/conn
|
||||||
import { publicMessageFailure } from '../../../../src/channels/shared/message-failure.mjs';
|
import { publicMessageFailure } from '../../../../src/channels/shared/message-failure.mjs';
|
||||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
|
|
@ -738,12 +739,10 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
||||||
|
|
||||||
/** Register the `/feishu` logical channel with its configured browser authority. */
|
/** Register the `/feishu` logical channel with its configured browser authority. */
|
||||||
export function installFeishuRpc(ctx, controller, options, authority) {
|
export function installFeishuRpc(ctx, controller, options, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
FEISHU_RPC_CHANNEL,
|
FEISHU_RPC_CHANNEL,
|
||||||
createFeishuRpcHandler(controller, options),
|
createFeishuRpcHandler(controller, options),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS } from '../../../../src/channels/office/protocol.mjs';
|
import { OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS } from '../../../../src/channels/office/protocol.mjs';
|
||||||
|
|
||||||
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
function record(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
|
||||||
|
|
@ -40,9 +41,10 @@ export function createOfficeRpcHandler(controller) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installOfficeRpc(ctx, controller, authority) {
|
export function installOfficeRpc(ctx, controller, authority) {
|
||||||
return ctx.connection.rpc.handle(
|
resolveRpcAuthority(authority);
|
||||||
|
return installConnectionRpcChannel(
|
||||||
|
ctx,
|
||||||
OFFICE_RPC_CHANNEL,
|
OFFICE_RPC_CHANNEL,
|
||||||
createOfficeRpcHandler(controller),
|
createOfficeRpcHandler(controller),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
|
|
||||||
|
|
@ -227,12 +228,10 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installQqRpc(ctx, controller, options, authority) {
|
export function installQqRpc(ctx, controller, options, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
QQ_RPC_CHANNEL,
|
QQ_RPC_CHANNEL,
|
||||||
createQqRpcHandler(controller, options),
|
createQqRpcHandler(controller, options),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from './group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from './group-session-scope-rpc.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -197,12 +198,10 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) {
|
export function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
rpcChannel,
|
rpcChannel,
|
||||||
createTokenBotRpcHandler(controller, { channel }),
|
createTokenBotRpcHandler(controller, { channel }),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||||
import {
|
import {
|
||||||
publicWorkspaceError,
|
publicWorkspaceError,
|
||||||
|
|
@ -201,12 +202,10 @@ export function createSlackRpcHandler(controller) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installSlackRpc(ctx, controller, authority) {
|
export function installSlackRpc(ctx, controller, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
SLACK_RPC_CHANNEL,
|
SLACK_RPC_CHANNEL,
|
||||||
createSlackRpcHandler(controller),
|
createSlackRpcHandler(controller),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
createTokenBotRpcHandler,
|
createTokenBotRpcHandler,
|
||||||
} from '../shared/rpc.mjs';
|
} from '../shared/rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
|
|
||||||
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
||||||
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
|
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
|
||||||
|
|
@ -13,12 +14,10 @@ export function createTelegramRpcHandler(controller) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installTelegramRpc(ctx, controller, authority) {
|
export function installTelegramRpc(ctx, controller, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
TELEGRAM_RPC_CHANNEL,
|
TELEGRAM_RPC_CHANNEL,
|
||||||
createTelegramRpcHandler(controller),
|
createTelegramRpcHandler(controller),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -228,12 +229,10 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installWecomRpc(ctx, controller, options, authority) {
|
export function installWecomRpc(ctx, controller, options, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
WECOM_RPC_CHANNEL,
|
WECOM_RPC_CHANNEL,
|
||||||
createWecomRpcHandler(controller, options),
|
createWecomRpcHandler(controller, options),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QRCode from 'qrcode';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
import {
|
import {
|
||||||
publicWorkspaceError,
|
publicWorkspaceError,
|
||||||
|
|
@ -260,12 +261,10 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installWeixinRpc(ctx, controller, options, authority) {
|
export function installWeixinRpc(ctx, controller, options, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
WEIXIN_RPC_CHANNEL,
|
WEIXIN_RPC_CHANNEL,
|
||||||
createWeixinRpcHandler(controller, options),
|
createWeixinRpcHandler(controller, options),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
import { SET_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
|
||||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||||
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||||
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||||
|
|
||||||
|
|
@ -282,12 +283,10 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installWhatsappRpc(ctx, controller, options, authority) {
|
export function installWhatsappRpc(ctx, controller, options, authority) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
WHATSAPP_RPC_CHANNEL,
|
WHATSAPP_RPC_CHANNEL,
|
||||||
createWhatsappRpcHandler(controller, options),
|
createWhatsappRpcHandler(controller, options),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
200
plugin-src/host/connection-rpc-mount.mjs
Normal file
200
plugin-src/host/connection-rpc-mount.mjs
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
/**
|
||||||
|
* Mount a Connection-compatible unary RPC channel via webServer.
|
||||||
|
*
|
||||||
|
* DSH `connection.rpc.handle` registers with `owner.webServer`, but Cordis
|
||||||
|
* rebinds `connection.ctx` onto the Connection fiber (which does not inject
|
||||||
|
* webServer). That throws "cannot get property webServer without inject" and
|
||||||
|
* the browser sees empty HTTP 405 from frontend-static.
|
||||||
|
*
|
||||||
|
* This helper registers the same POST JSON envelope on the plugin fiber that
|
||||||
|
* already injects `webServer`, and still uses `connection.requestRejection`
|
||||||
|
* for Host/Origin + browser auth.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/;
|
||||||
|
const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
|
||||||
|
const MAX_BODY_BYTES = 16 * 1024 * 1024;
|
||||||
|
const INVALID_REQUEST_RPC_ID = 'invalid-request';
|
||||||
|
|
||||||
|
function assertChannel(channel) {
|
||||||
|
if (!CHANNEL_PATTERN.test(channel) || channel === '/api') {
|
||||||
|
throw new TypeError(`dsh-im: invalid or reserved RPC channel ${JSON.stringify(channel)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endpointFromPath(channel, pathname) {
|
||||||
|
if (!pathname.startsWith(`${channel}/`)) return undefined;
|
||||||
|
const endpoint = pathname.slice(channel.length + 1);
|
||||||
|
const segments = endpoint.split('/');
|
||||||
|
if (segments.some((segment) => (
|
||||||
|
segment === ''
|
||||||
|
|| segment === '.'
|
||||||
|
|| segment === '..'
|
||||||
|
|| !ENDPOINT_SEGMENT_PATTERN.test(segment)
|
||||||
|
))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isJsonContentType(value) {
|
||||||
|
return typeof value === 'string'
|
||||||
|
&& value.split(';', 1)[0].trim().toLowerCase() === 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeRaw(res, status, body, headers = {}) {
|
||||||
|
if (res.destroyed || res.writableEnded) return;
|
||||||
|
res.writeHead(status, headers);
|
||||||
|
res.end(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJson(res, status, value) {
|
||||||
|
const body = JSON.stringify(value);
|
||||||
|
writeRaw(res, status, body, {
|
||||||
|
'cache-control': 'no-store',
|
||||||
|
'content-type': 'application/json; charset=utf-8',
|
||||||
|
'content-length': String(Buffer.byteLength(body)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonBody(req) {
|
||||||
|
const declared = Number(req.headers['content-length']);
|
||||||
|
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
|
||||||
|
const error = new Error('payload-too-large');
|
||||||
|
error.status = 413;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const chunks = [];
|
||||||
|
let length = 0;
|
||||||
|
for await (const chunk of req) {
|
||||||
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||||
|
length += buffer.length;
|
||||||
|
if (length > MAX_BODY_BYTES) {
|
||||||
|
const error = new Error('payload-too-large');
|
||||||
|
error.status = 413;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
chunks.push(buffer);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(Buffer.concat(chunks, length).toString('utf8'));
|
||||||
|
} catch {
|
||||||
|
const error = new Error('body is not JSON');
|
||||||
|
error.status = 400;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClientRequest(value) {
|
||||||
|
return value !== null
|
||||||
|
&& typeof value === 'object'
|
||||||
|
&& !Array.isArray(value)
|
||||||
|
&& value.type === 'client-request'
|
||||||
|
&& typeof value.rpcId === 'string'
|
||||||
|
&& typeof value.method === 'string'
|
||||||
|
&& Object.prototype.hasOwnProperty.call(value, 'payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverResponse(rpcId, result) {
|
||||||
|
return { type: 'server-response', rpcId, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} ctx Cordis context with webServer + connection
|
||||||
|
* @param {string} channel e.g. `/weixin`
|
||||||
|
* @param {(endpoint: string, payload: unknown, signal?: AbortSignal) => Promise<{ok: boolean, value?: unknown, error?: object}>} handler
|
||||||
|
* @returns {() => Promise<void>} disposer
|
||||||
|
*/
|
||||||
|
export function installConnectionRpcChannel(ctx, channel, handler) {
|
||||||
|
assertChannel(channel);
|
||||||
|
if (typeof handler !== 'function') {
|
||||||
|
throw new TypeError('dsh-im RPC handler must be a function');
|
||||||
|
}
|
||||||
|
if (!ctx?.webServer || typeof ctx.webServer.register !== 'function') {
|
||||||
|
throw new TypeError('DSH Host webServer is required to mount IM RPC channels');
|
||||||
|
}
|
||||||
|
if (typeof ctx?.effect !== 'function') {
|
||||||
|
throw new TypeError('DSH Host ctx.effect is required to mount IM RPC channels');
|
||||||
|
}
|
||||||
|
const connection = ctx.connection;
|
||||||
|
if (!connection || typeof connection.requestRejection !== 'function') {
|
||||||
|
throw new TypeError('DSH Host connection.requestRejection is required to mount IM RPC channels');
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = {
|
||||||
|
kind: 'prefix',
|
||||||
|
path: channel,
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const rejection = connection.requestRejection(req);
|
||||||
|
if (rejection !== undefined) {
|
||||||
|
writeRaw(res, rejection, rejection === 401 ? 'unauthorized' : 'forbidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname;
|
||||||
|
const endpoint = endpointFromPath(channel, pathname);
|
||||||
|
if (req.method !== 'POST' || endpoint === undefined) {
|
||||||
|
writeRaw(res, 404, 'not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isJsonContentType(req.headers['content-type'])) {
|
||||||
|
writeRaw(res, 415, 'content type must be application/json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const abort = new AbortController();
|
||||||
|
const onClose = () => {
|
||||||
|
if (!res.writableEnded) abort.abort();
|
||||||
|
};
|
||||||
|
res.once('close', onClose);
|
||||||
|
try {
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = await readJsonBody(req);
|
||||||
|
} catch (error) {
|
||||||
|
writeRaw(res, error.status === 413 ? 413 : 400, error.message || 'bad request');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isClientRequest(body)) {
|
||||||
|
writeJson(res, 200, serverResponse(
|
||||||
|
typeof body?.rpcId === 'string' ? body.rpcId : INVALID_REQUEST_RPC_ID,
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
code: 'gateway/bad-request',
|
||||||
|
message: 'invalid client-request message',
|
||||||
|
details: { issues: [] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body.method !== endpoint) {
|
||||||
|
writeJson(res, 200, serverResponse(body.rpcId, {
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
code: 'gateway/bad-request',
|
||||||
|
message: `method ${JSON.stringify(body.method)} does not match endpoint ${JSON.stringify(endpoint)}`,
|
||||||
|
details: { issues: [] },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await handler(endpoint, body.payload, abort.signal);
|
||||||
|
writeJson(res, 200, serverResponse(body.rpcId, result));
|
||||||
|
} catch (error) {
|
||||||
|
writeRaw(res, 500, `handler failure: ${String(error)}`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
res.off('close', onClose);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return ctx.effect(
|
||||||
|
() => ctx.webServer.register(route),
|
||||||
|
`dsh-im: ${channel} rpc channel`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { resolveRpcAuthority } from './rpc-authority.mjs';
|
import { resolveRpcAuthority } from './rpc-authority.mjs';
|
||||||
|
import { installConnectionRpcChannel } from './connection-rpc-mount.mjs';
|
||||||
|
|
||||||
export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
|
export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
|
||||||
export const DELIVERY_TEST_MESSAGE = 'DSH-IM 主动投递测试成功。';
|
export const DELIVERY_TEST_MESSAGE = 'DSH-IM 主动投递测试成功。';
|
||||||
|
|
@ -147,12 +148,10 @@ export function createDeliveryRpcHandler(service) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function installDeliveryRpc(ctx, service, { authority } = {}) {
|
export function installDeliveryRpc(ctx, service, { authority } = {}) {
|
||||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
resolveRpcAuthority(authority);
|
||||||
throw new TypeError('DSH Host Connection RPC is required');
|
return installConnectionRpcChannel(
|
||||||
}
|
ctx,
|
||||||
return ctx.connection.rpc.handle(
|
|
||||||
DELIVERY_RPC_CHANNEL,
|
DELIVERY_RPC_CHANNEL,
|
||||||
createDeliveryRpcHandler(service),
|
createDeliveryRpcHandler(service),
|
||||||
{ authority: resolveRpcAuthority(authority) },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ export const inject = [
|
||||||
'connection',
|
'connection',
|
||||||
'credentials',
|
'credentials',
|
||||||
'typertGateway',
|
'typertGateway',
|
||||||
|
// Plugin-side RPC mounts use ctx.webServer.register (see connection-rpc-mount.mjs).
|
||||||
|
'webServer',
|
||||||
];
|
];
|
||||||
|
|
||||||
function channelConfig(config, name, deliveryService) {
|
function channelConfig(config, name, deliveryService) {
|
||||||
|
|
@ -188,21 +190,11 @@ export function createImHostPlugin(internals = {}) {
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
const activate = async (readyCtx) => {
|
|
||||||
await activateChannels(readyCtx, config, deliveryService);
|
// Activate on this fiber (already injects webServer). Do not defer to a
|
||||||
};
|
// sessionController child fiber — that fiber cannot read webServer, so
|
||||||
if (typeof ctx?.inject === 'function') {
|
// every channel rpc.handle throws and the UI sees HTTP 405.
|
||||||
const modern = typeof ctx?.typertGateway?.stream === 'function';
|
await activateChannels(ctx, config, deliveryService);
|
||||||
await ctx.inject(
|
|
||||||
modern ? ['sessionController', 'workspaceController'] : ['apiProxy'],
|
|
||||||
activate,
|
|
||||||
);
|
|
||||||
ctx.inject(['webServer'], (httpCtx) => {
|
|
||||||
startDeliveryHttp(httpCtx, deliveryService);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await activate(ctx);
|
|
||||||
if (ctx?.webServer?.register && typeof ctx?.effect === 'function') {
|
if (ctx?.webServer?.register && typeof ctx?.effect === 'function') {
|
||||||
startDeliveryHttp(ctx, deliveryService);
|
startDeliveryHttp(ctx, deliveryService);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { createUpdateRuntime } from './update-runtime.mjs';
|
import { createUpdateRuntime } from './update-runtime.mjs';
|
||||||
import { createUpdateService } from './update-service.mjs';
|
import { createUpdateService } from './update-service.mjs';
|
||||||
|
import { installConnectionRpcChannel } from './connection-rpc-mount.mjs';
|
||||||
|
import { resolveRpcAuthority } from './rpc-authority.mjs';
|
||||||
|
|
||||||
export const UPDATE_RPC_CHANNEL = '/dsh-im';
|
export const UPDATE_RPC_CHANNEL = '/dsh-im';
|
||||||
export const UPDATE_ENDPOINTS = Object.freeze(['update.status', 'update.check', 'update.install']);
|
export const UPDATE_ENDPOINTS = Object.freeze(['update.status', 'update.check', 'update.install']);
|
||||||
|
|
@ -42,9 +44,12 @@ export function createUpdateRpcHandler(service) {
|
||||||
export function installUpdateRpc(ctx, options = {}) {
|
export function installUpdateRpc(ctx, options = {}) {
|
||||||
const runtime = options.runtime ?? createUpdateRuntime({ ctx, moduleUrl: import.meta.url });
|
const runtime = options.runtime ?? createUpdateRuntime({ ctx, moduleUrl: import.meta.url });
|
||||||
const service = options.service ?? createUpdateService({ runtime });
|
const service = options.service ?? createUpdateService({ runtime });
|
||||||
const dispose = ctx.connection.rpc.handle(UPDATE_RPC_CHANNEL, createUpdateRpcHandler(service), {
|
resolveRpcAuthority('loopback');
|
||||||
authority: 'loopback',
|
const dispose = installConnectionRpcChannel(
|
||||||
});
|
ctx,
|
||||||
|
UPDATE_RPC_CHANNEL,
|
||||||
|
createUpdateRpcHandler(service),
|
||||||
|
);
|
||||||
ctx.effect(() => () => service.close(), 'dsh-im: close update installer');
|
ctx.effect(() => () => service.close(), 'dsh-im: close update installer');
|
||||||
return dispose;
|
return dispose;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
133
test/connection-rpc-mount.test.mjs
Normal file
133
test/connection-rpc-mount.test.mjs
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { EventEmitter } from 'node:events';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { installConnectionRpcChannel } from '../plugin-src/host/connection-rpc-mount.mjs';
|
||||||
|
|
||||||
|
function mockRequest({ method = 'POST', url, headers = {}, body }) {
|
||||||
|
const req = new EventEmitter();
|
||||||
|
req.method = method;
|
||||||
|
req.url = url;
|
||||||
|
req.headers = { 'content-type': 'application/json', ...headers };
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (body !== undefined) req.emit('data', Buffer.from(body));
|
||||||
|
req.emit('end');
|
||||||
|
});
|
||||||
|
req[Symbol.asyncIterator] = async function* () {
|
||||||
|
if (body !== undefined) yield Buffer.from(body);
|
||||||
|
};
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockResponse() {
|
||||||
|
const res = new EventEmitter();
|
||||||
|
res.statusCode = undefined;
|
||||||
|
res.headers = undefined;
|
||||||
|
res.body = undefined;
|
||||||
|
res.writableEnded = false;
|
||||||
|
res.destroyed = false;
|
||||||
|
res.writeHead = (status, headers) => {
|
||||||
|
res.statusCode = status;
|
||||||
|
res.headers = headers;
|
||||||
|
};
|
||||||
|
res.end = (body) => {
|
||||||
|
res.body = body;
|
||||||
|
res.writableEnded = true;
|
||||||
|
res.emit('finish');
|
||||||
|
};
|
||||||
|
res.off = res.removeListener.bind(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCtx({ rejection } = {}) {
|
||||||
|
const routes = [];
|
||||||
|
const effects = [];
|
||||||
|
return {
|
||||||
|
routes,
|
||||||
|
effects,
|
||||||
|
connection: {
|
||||||
|
requestRejection() {
|
||||||
|
return rejection;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
register(route) {
|
||||||
|
routes.push(route);
|
||||||
|
return () => {
|
||||||
|
const index = routes.indexOf(route);
|
||||||
|
if (index >= 0) routes.splice(index, 1);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effect(factory, label) {
|
||||||
|
const dispose = factory();
|
||||||
|
effects.push({ label, dispose });
|
||||||
|
return async () => {
|
||||||
|
await dispose?.();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('installConnectionRpcChannel mounts a POST JSON RPC channel on webServer', async () => {
|
||||||
|
const ctx = createCtx();
|
||||||
|
const calls = [];
|
||||||
|
installConnectionRpcChannel(ctx, '/weixin', async (endpoint, payload) => {
|
||||||
|
calls.push([endpoint, payload]);
|
||||||
|
return { ok: true, value: { endpoint, payload } };
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(ctx.routes.length, 1);
|
||||||
|
assert.equal(ctx.routes[0].kind, 'prefix');
|
||||||
|
assert.equal(ctx.routes[0].path, '/weixin');
|
||||||
|
assert.equal(ctx.effects[0].label, 'dsh-im: /weixin rpc channel');
|
||||||
|
|
||||||
|
const req = mockRequest({
|
||||||
|
url: '/weixin/connection.status',
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'client-request',
|
||||||
|
rpcId: 'rpc-1',
|
||||||
|
method: 'connection.status',
|
||||||
|
payload: { botId: 'b1' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const res = mockResponse();
|
||||||
|
await ctx.routes[0].handler(req, res);
|
||||||
|
|
||||||
|
assert.equal(res.statusCode, 200);
|
||||||
|
assert.deepEqual(JSON.parse(res.body), {
|
||||||
|
type: 'server-response',
|
||||||
|
rpcId: 'rpc-1',
|
||||||
|
result: { ok: true, value: { endpoint: 'connection.status', payload: { botId: 'b1' } } },
|
||||||
|
});
|
||||||
|
assert.deepEqual(calls, [['connection.status', { botId: 'b1' }]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('installConnectionRpcChannel rejects unauthenticated requests via connection.requestRejection', async () => {
|
||||||
|
const ctx = createCtx({ rejection: 401 });
|
||||||
|
installConnectionRpcChannel(ctx, '/weixin', async () => ({ ok: true, value: {} }));
|
||||||
|
|
||||||
|
const req = mockRequest({
|
||||||
|
url: '/weixin/connection.status',
|
||||||
|
body: '{}',
|
||||||
|
});
|
||||||
|
const res = mockResponse();
|
||||||
|
await ctx.routes[0].handler(req, res);
|
||||||
|
|
||||||
|
assert.equal(res.statusCode, 401);
|
||||||
|
assert.equal(res.body, 'unauthorized');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('installConnectionRpcChannel requires webServer and connection.requestRejection', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => installConnectionRpcChannel({ effect() {} }, '/weixin', async () => ({})),
|
||||||
|
/webServer is required/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => installConnectionRpcChannel({
|
||||||
|
effect() {},
|
||||||
|
webServer: { register() {} },
|
||||||
|
}, '/weixin', async () => ({})),
|
||||||
|
/requestRejection is required/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -43,6 +43,7 @@ test('Host composes nine IM channels and the AI Office connector inside one plug
|
||||||
'connection',
|
'connection',
|
||||||
'credentials',
|
'credentials',
|
||||||
'typertGateway',
|
'typertGateway',
|
||||||
|
'webServer',
|
||||||
]);
|
]);
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
['feishu', ctx, { ...config.feishu, rpcAuthority: 'trusted-host', deliveryService }],
|
['feishu', ctx, { ...config.feishu, rpcAuthority: 'trusted-host', deliveryService }],
|
||||||
|
|
@ -83,7 +84,10 @@ test('Host provides #65 and installs #84 with the same delivery service', async
|
||||||
installDeliveryHttp: (...args) => http.push(args),
|
installDeliveryHttp: (...args) => http.push(args),
|
||||||
});
|
});
|
||||||
const ctx = {
|
const ctx = {
|
||||||
connection: { rpc: {} },
|
connection: {
|
||||||
|
rpc: {},
|
||||||
|
requestRejection() {},
|
||||||
|
},
|
||||||
webServer: { register() {} },
|
webServer: { register() {} },
|
||||||
effect() {},
|
effect() {},
|
||||||
provide: (...args) => provided.push(args),
|
provide: (...args) => provided.push(args),
|
||||||
|
|
@ -109,9 +113,11 @@ test('#65 activates a real Cordis consumer without crossing the Connection RPC',
|
||||||
handle: () => async () => {},
|
handle: () => async () => {},
|
||||||
call: (...args) => rpcCalls.push(args),
|
call: (...args) => rpcCalls.push(args),
|
||||||
},
|
},
|
||||||
|
requestRejection() {},
|
||||||
});
|
});
|
||||||
ctx.provide('credentials', {});
|
ctx.provide('credentials', {});
|
||||||
ctx.provide('typertGateway', { stream() {} });
|
ctx.provide('typertGateway', { stream() {} });
|
||||||
|
ctx.provide('webServer', { register() { return () => {}; } });
|
||||||
ctx.provide('sessionController', {});
|
ctx.provide('sessionController', {});
|
||||||
ctx.provide('workspaceController', {});
|
ctx.provide('workspaceController', {});
|
||||||
|
|
||||||
|
|
@ -131,6 +137,7 @@ test('#65 activates a real Cordis consumer without crossing the Connection RPC',
|
||||||
createDeliveryService: () => deliveryService,
|
createDeliveryService: () => deliveryService,
|
||||||
installUpdateRpc: () => async () => {},
|
installUpdateRpc: () => async () => {},
|
||||||
installDeliveryRpc: () => async () => {},
|
installDeliveryRpc: () => async () => {},
|
||||||
|
installDeliveryHttp: () => async () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const host = ctx.plugin(createImHostPlugin(internals));
|
const host = ctx.plugin(createImHostPlugin(internals));
|
||||||
|
|
@ -153,34 +160,41 @@ test('#65 activates a real Cordis consumer without crossing the Connection RPC',
|
||||||
assert.equal(rpcCalls.length, 0);
|
assert.equal(rpcCalls.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Host waits for apiProxy on legacy Harness and Controllers on modern Harness', async () => {
|
test('Host activates channels on its own fiber (injects webServer) so RPC routes can mount', async () => {
|
||||||
for (const [modern, expected] of [
|
const calls = [];
|
||||||
[false, ['apiProxy']],
|
const deliveryHttp = [];
|
||||||
[true, ['sessionController', 'workspaceController']],
|
const plugin = createImHostPlugin({
|
||||||
]) {
|
...Object.fromEntries(CHANNELS.map(([channel, applyName]) => [
|
||||||
const injections = [];
|
|
||||||
const calls = [];
|
|
||||||
const plugin = createImHostPlugin(Object.fromEntries(CHANNELS.map(([channel, applyName]) => [
|
|
||||||
applyName,
|
applyName,
|
||||||
async () => calls.push(channel),
|
async () => calls.push(channel),
|
||||||
])));
|
])),
|
||||||
const ctx = {
|
installDeliveryHttp: (ctx) => deliveryHttp.push(ctx),
|
||||||
credentials: {},
|
installUpdateRpc: () => async () => {},
|
||||||
typertGateway: modern ? { stream() {} } : { invoke() {} },
|
installDeliveryRpc: () => async () => {},
|
||||||
inject(dependencies, callback) {
|
});
|
||||||
injections.push(dependencies);
|
const ctx = {
|
||||||
if (dependencies.includes('tools') || dependencies.includes('webServer')) return {};
|
credentials: {},
|
||||||
return {
|
typertGateway: { stream() {}, invoke() {} },
|
||||||
then(resolve, reject) {
|
connection: {
|
||||||
Promise.resolve(callback(ctx)).then(resolve, reject);
|
rpc: { handle: () => async () => {} },
|
||||||
},
|
requestRejection() {},
|
||||||
};
|
},
|
||||||
},
|
webServer: { register() {} },
|
||||||
};
|
effect() {},
|
||||||
await plugin.apply(ctx, {});
|
inject(dependencies, callback) {
|
||||||
assert.deepEqual(injections[0], expected);
|
if (dependencies.includes('tools') || dependencies.includes('systemPrompt')) return {};
|
||||||
assert.deepEqual(calls, CHANNELS.map(([channel]) => channel));
|
return callback(ctx);
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
await plugin.apply(ctx, {});
|
||||||
|
assert.deepEqual(plugin.inject, [
|
||||||
|
'connection',
|
||||||
|
'credentials',
|
||||||
|
'typertGateway',
|
||||||
|
'webServer',
|
||||||
|
]);
|
||||||
|
assert.equal(deliveryHttp.length, 1);
|
||||||
|
assert.deepEqual(calls, CHANNELS.map(([channel]) => channel));
|
||||||
});
|
});
|
||||||
|
|
||||||
const CHANNELS = [
|
const CHANNELS = [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue