mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
feat: secure Telegram access and connect AI Office
This commit is contained in:
parent
e3ae772106
commit
b0606159e0
30 changed files with 2045 additions and 302 deletions
10
plugin-src/host/channels/office/index.mjs
Normal file
10
plugin-src/host/channels/office/index.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { createProductionController } from './production.mjs';
|
||||
import { installOfficeRpc } from './rpc.mjs';
|
||||
|
||||
export async function apply(ctx, config = {}) {
|
||||
if (config.controller) return installOfficeRpc(ctx, config.controller, config.rpcAuthority);
|
||||
const production = await createProductionController(ctx, config, config.internals ?? {});
|
||||
const dispose = installOfficeRpc(ctx, production.controller, config.rpcAuthority);
|
||||
ctx.effect(() => async () => production.close(), 'dsh-im: close AI Office connector');
|
||||
return dispose;
|
||||
}
|
||||
29
plugin-src/host/channels/office/production.mjs
Normal file
29
plugin-src/host/channels/office/production.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { homedir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
import { OfficeConfigStore } from '../../../../src/channels/office/config-store.mjs';
|
||||
import { OfficeController } from '../../../../src/channels/office/office-controller.mjs';
|
||||
import { OfficeRuntime } from '../../../../src/channels/office/office-runtime.mjs';
|
||||
|
||||
export function officePaths(config = {}) {
|
||||
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
|
||||
const root = resolve(config.dataDir ?? join(dshHome, 'integrations', 'dsh-office'));
|
||||
return { root, config: resolve(config.configPath ?? join(root, 'config.json')) };
|
||||
}
|
||||
|
||||
export async function createProductionController(ctx, config = {}, internals = {}) {
|
||||
const Store = internals.ConfigStore ?? OfficeConfigStore;
|
||||
const Controller = internals.Controller ?? OfficeController;
|
||||
const Runtime = internals.Runtime ?? OfficeRuntime;
|
||||
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 controller = new Controller({
|
||||
credentials: ctx.credentials,
|
||||
configStore,
|
||||
logger,
|
||||
createRuntime: (options) => new Runtime({ ...options, ...(internals.transport ? { transport: internals.transport } : {}) }),
|
||||
});
|
||||
await controller.initialize();
|
||||
return { controller, close: () => controller.close(), paths };
|
||||
}
|
||||
48
plugin-src/host/channels/office/rpc.mjs
Normal file
48
plugin-src/host/channels/office/rpc.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { resolveRpcAuthority } from '../../rpc-authority.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 exact(value, keys) { return record(value) && Object.keys(value).every((key) => keys.includes(key)); }
|
||||
|
||||
function validConfigure(payload) {
|
||||
return exact(payload, [
|
||||
'baseUrl', 'deviceId', 'deviceToken', 'maxConcurrency', 'heartbeatSeconds',
|
||||
'workspaces', 'instructionPresets',
|
||||
]) && typeof payload.baseUrl === 'string' && typeof payload.deviceId === 'string'
|
||||
&& (payload.deviceToken === undefined || typeof payload.deviceToken === 'string')
|
||||
&& record(payload.workspaces) && record(payload.instructionPresets);
|
||||
}
|
||||
|
||||
export function createOfficeRpcHandler(controller) {
|
||||
for (const method of ['status', 'configure', 'reconnect', 'test', 'remove']) {
|
||||
if (typeof controller?.[method] !== 'function') throw new TypeError(`AI Office controller requires ${method}()`);
|
||||
}
|
||||
return async (endpoint, payload, signal) => {
|
||||
if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
||||
try {
|
||||
let value;
|
||||
if (endpoint === OFFICE_RPC_ENDPOINTS.status && exact(payload, [])) value = await controller.status();
|
||||
else if (endpoint === OFFICE_RPC_ENDPOINTS.configure && validConfigure(payload)) value = await controller.configure(payload);
|
||||
else if (endpoint === OFFICE_RPC_ENDPOINTS.reconnect && exact(payload, [])) value = await controller.reconnect();
|
||||
else if (endpoint === OFFICE_RPC_ENDPOINTS.test && exact(payload, [])) value = await controller.test();
|
||||
else if (endpoint === OFFICE_RPC_ENDPOINTS.remove && exact(payload, ['confirm']) && payload.confirm === true) value = await controller.remove();
|
||||
else return { ok: false, error: { code: 'bad-request', message: 'Invalid AI Office connector request.' } };
|
||||
return { ok: true, value };
|
||||
} catch (error) {
|
||||
const code = error?.code === 'invalid-device-token' ? 'invalid-device-token'
|
||||
: error?.code === 'office-hook-unavailable' ? 'office-hook-unavailable' : 'office-operation-failed';
|
||||
const message = code === 'invalid-device-token' ? 'AI Office Device Token 无效。'
|
||||
: code === 'office-hook-unavailable' ? 'AI Office Hook 尚未上线或地址不正确。'
|
||||
: error instanceof TypeError ? error.message : 'AI Office 连接操作失败,请稍后重试。';
|
||||
return { ok: false, error: { code, message } };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function installOfficeRpc(ctx, controller, authority) {
|
||||
return ctx.connection.rpc.handle(
|
||||
OFFICE_RPC_CHANNEL,
|
||||
createOfficeRpcHandler(controller),
|
||||
{ authority: resolveRpcAuthority(authority) },
|
||||
);
|
||||
}
|
||||
|
|
@ -32,7 +32,9 @@ export function pluginPaths(config, channel) {
|
|||
}
|
||||
|
||||
export async function createTokenProductionController(ctx, config, internals, definitions) {
|
||||
const { channel, ConfigStore, StateStore, HarnessClient, Controller, Runtime } = definitions;
|
||||
const {
|
||||
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`);
|
||||
|
||||
|
|
@ -41,6 +43,11 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|||
const ResolvedHarness = internals.HarnessClient ?? HarnessClient;
|
||||
const ResolvedController = internals.Controller ?? Controller;
|
||||
const ResolvedRuntime = internals.Runtime ?? Runtime;
|
||||
const channelRuntimeOptions = typeof runtimeOptions === 'function' ? runtimeOptions(config) : {};
|
||||
if (!channelRuntimeOptions || typeof channelRuntimeOptions !== 'object'
|
||||
|| Array.isArray(channelRuntimeOptions)) {
|
||||
throw new TypeError(`dsh-im ${channel} runtimeOptions must return an object`);
|
||||
}
|
||||
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
|
||||
const logger = typeof ctx.logger === 'function'
|
||||
? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
|
||||
|
|
@ -91,6 +98,7 @@ export async function createTokenProductionController(ctx, config, internals, de
|
|||
await workspaces.ensure(botId);
|
||||
const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
|
||||
return new ResolvedRuntime({
|
||||
...channelRuntimeOptions,
|
||||
config: botConfig,
|
||||
token,
|
||||
harness: workspaceScope.harness,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,24 @@ import { TelegramRuntime } from '../../../../src/channels/telegram/telegram-runt
|
|||
import { TelegramStateStore } from '../../../../src/channels/telegram/state-store.mjs';
|
||||
import { createTokenProductionController } from '../shared/production.mjs';
|
||||
|
||||
const TELEGRAM_USER_ID = /^[1-9]\d{0,15}$/;
|
||||
|
||||
export function normalizeTelegramAllowedUsers(value) {
|
||||
if (value === undefined) return Object.freeze([]);
|
||||
if (!Array.isArray(value)) {
|
||||
throw new TypeError('telegram.allowedUsers must be an array of numeric Telegram User IDs');
|
||||
}
|
||||
const normalized = value.map((entry) => {
|
||||
const userId = typeof entry === 'number' && Number.isSafeInteger(entry)
|
||||
? String(entry) : typeof entry === 'string' ? entry.trim() : '';
|
||||
if (!TELEGRAM_USER_ID.test(userId)) {
|
||||
throw new TypeError('telegram.allowedUsers contains an invalid Telegram User ID');
|
||||
}
|
||||
return userId;
|
||||
});
|
||||
return Object.freeze([...new Set(normalized)]);
|
||||
}
|
||||
|
||||
export function createProductionController(ctx, config = {}, internals = {}) {
|
||||
return createTokenProductionController(ctx, config, internals, {
|
||||
channel: 'telegram',
|
||||
|
|
@ -13,5 +31,8 @@ export function createProductionController(ctx, config = {}, internals = {}) {
|
|||
HarnessClient: TelegramHarnessClient,
|
||||
Controller: TelegramController,
|
||||
Runtime: TelegramRuntime,
|
||||
runtimeOptions: (runtimeConfig) => ({
|
||||
allowedPrivateUserIds: normalizeTelegramAllowedUsers(runtimeConfig.allowedUsers),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { apply as applyDingtalk } from './channels/dingtalk/index.mjs';
|
||||
import { apply as applyDiscord } from './channels/discord/index.mjs';
|
||||
import { apply as applyOffice } from './channels/office/index.mjs';
|
||||
import { apply as applyFeishu } from './channels/feishu/index.mjs';
|
||||
import { apply as applyQq } from './channels/qq/index.mjs';
|
||||
import { apply as applySlack } from './channels/slack/index.mjs';
|
||||
|
|
@ -27,6 +28,7 @@ export function createImHostPlugin(internals = {}) {
|
|||
const startSlack = internals.applySlack ?? applySlack;
|
||||
const startTelegram = internals.applyTelegram ?? applyTelegram;
|
||||
const startDiscord = internals.applyDiscord ?? applyDiscord;
|
||||
const startOffice = internals.applyOffice ?? applyOffice;
|
||||
const startWhatsapp = internals.applyWhatsapp ?? applyWhatsapp;
|
||||
return Object.freeze({
|
||||
name,
|
||||
|
|
@ -41,6 +43,7 @@ export function createImHostPlugin(internals = {}) {
|
|||
await startTelegram(ctx, channelConfig(config, 'telegram'));
|
||||
await startDiscord(ctx, channelConfig(config, 'discord'));
|
||||
await startWhatsapp(ctx, channelConfig(config, 'whatsapp'));
|
||||
await startOffice(ctx, channelConfig(config, 'office'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue