mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +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
|
|
@ -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_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.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 { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||
import {
|
||||
|
|
@ -336,12 +337,10 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|||
}
|
||||
|
||||
export function installDingtalkRpc(ctx, controller, options, authority) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
DINGTALK_RPC_CHANNEL,
|
||||
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 { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
||||
import { validAgentPresetPayload } from '../shared/agent-preset-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. */
|
||||
export function installFeishuRpc(ctx, controller, options, authority) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
FEISHU_RPC_CHANNEL,
|
||||
createFeishuRpcHandler(controller, options),
|
||||
{ authority: resolveRpcAuthority(authority) },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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';
|
||||
|
||||
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) {
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
OFFICE_RPC_CHANNEL,
|
||||
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_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.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 { 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) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
QQ_RPC_CHANNEL,
|
||||
createQqRpcHandler(controller, options),
|
||||
{ authority: resolveRpcAuthority(authority) },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-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 { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||
import {
|
||||
|
|
@ -197,12 +198,10 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
|||
}
|
||||
|
||||
export function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
rpcChannel,
|
||||
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_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
||||
import {
|
||||
publicWorkspaceError,
|
||||
|
|
@ -201,12 +202,10 @@ export function createSlackRpcHandler(controller) {
|
|||
}
|
||||
|
||||
export function installSlackRpc(ctx, controller, authority) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
SLACK_RPC_CHANNEL,
|
||||
createSlackRpcHandler(controller),
|
||||
{ authority: resolveRpcAuthority(authority) },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
createTokenBotRpcHandler,
|
||||
} from '../shared/rpc.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||
|
||||
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
||||
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
|
||||
|
|
@ -13,12 +14,10 @@ export function createTelegramRpcHandler(controller) {
|
|||
}
|
||||
|
||||
export function installTelegramRpc(ctx, controller, authority) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
TELEGRAM_RPC_CHANNEL,
|
||||
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_GROUP_SESSION_SCOPE_ENDPOINT, validGroupSessionScopePayload } from '../shared/group-session-scope-rpc.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 { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
|
||||
import {
|
||||
|
|
@ -228,12 +229,10 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
|||
}
|
||||
|
||||
export function installWecomRpc(ctx, controller, options, authority) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
WECOM_RPC_CHANNEL,
|
||||
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_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 { installConnectionRpcChannel } from '../../connection-rpc-mount.mjs';
|
||||
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
||||
import {
|
||||
publicWorkspaceError,
|
||||
|
|
@ -260,12 +261,10 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
|
|||
}
|
||||
|
||||
export function installWeixinRpc(ctx, controller, options, authority) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
WEIXIN_RPC_CHANNEL,
|
||||
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_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.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 { 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) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
WHATSAPP_RPC_CHANNEL,
|
||||
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 { installConnectionRpcChannel } from './connection-rpc-mount.mjs';
|
||||
|
||||
export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
|
||||
export const DELIVERY_TEST_MESSAGE = 'DSH-IM 主动投递测试成功。';
|
||||
|
|
@ -147,12 +148,10 @@ export function createDeliveryRpcHandler(service) {
|
|||
}
|
||||
|
||||
export function installDeliveryRpc(ctx, service, { authority } = {}) {
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
resolveRpcAuthority(authority);
|
||||
return installConnectionRpcChannel(
|
||||
ctx,
|
||||
DELIVERY_RPC_CHANNEL,
|
||||
createDeliveryRpcHandler(service),
|
||||
{ authority: resolveRpcAuthority(authority) },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export const inject = [
|
|||
'connection',
|
||||
'credentials',
|
||||
'typertGateway',
|
||||
// Plugin-side RPC mounts use ctx.webServer.register (see connection-rpc-mount.mjs).
|
||||
'webServer',
|
||||
];
|
||||
|
||||
function channelConfig(config, name, deliveryService) {
|
||||
|
|
@ -188,21 +190,11 @@ export function createImHostPlugin(internals = {}) {
|
|||
},
|
||||
}));
|
||||
}
|
||||
const activate = async (readyCtx) => {
|
||||
await activateChannels(readyCtx, config, deliveryService);
|
||||
};
|
||||
if (typeof ctx?.inject === 'function') {
|
||||
const modern = typeof ctx?.typertGateway?.stream === 'function';
|
||||
await ctx.inject(
|
||||
modern ? ['sessionController', 'workspaceController'] : ['apiProxy'],
|
||||
activate,
|
||||
);
|
||||
ctx.inject(['webServer'], (httpCtx) => {
|
||||
startDeliveryHttp(httpCtx, deliveryService);
|
||||
});
|
||||
return;
|
||||
}
|
||||
await activate(ctx);
|
||||
|
||||
// Activate on this fiber (already injects webServer). Do not defer to a
|
||||
// sessionController child fiber — that fiber cannot read webServer, so
|
||||
// every channel rpc.handle throws and the UI sees HTTP 405.
|
||||
await activateChannels(ctx, config, deliveryService);
|
||||
if (ctx?.webServer?.register && typeof ctx?.effect === 'function') {
|
||||
startDeliveryHttp(ctx, deliveryService);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { createUpdateRuntime } from './update-runtime.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_ENDPOINTS = Object.freeze(['update.status', 'update.check', 'update.install']);
|
||||
|
|
@ -42,9 +44,12 @@ export function createUpdateRpcHandler(service) {
|
|||
export function installUpdateRpc(ctx, options = {}) {
|
||||
const runtime = options.runtime ?? createUpdateRuntime({ ctx, moduleUrl: import.meta.url });
|
||||
const service = options.service ?? createUpdateService({ runtime });
|
||||
const dispose = ctx.connection.rpc.handle(UPDATE_RPC_CHANNEL, createUpdateRpcHandler(service), {
|
||||
authority: 'loopback',
|
||||
});
|
||||
resolveRpcAuthority('loopback');
|
||||
const dispose = installConnectionRpcChannel(
|
||||
ctx,
|
||||
UPDATE_RPC_CHANNEL,
|
||||
createUpdateRpcHandler(service),
|
||||
);
|
||||
ctx.effect(() => () => service.close(), 'dsh-im: close update installer');
|
||||
return dispose;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue