feat: add unified IM access policies

This commit is contained in:
xmanrui 2026-09-01 10:45:12 +08:00
parent 075d2713c1
commit 840e5aa857
89 changed files with 5573 additions and 1676 deletions

View file

@ -20,6 +20,10 @@ 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 {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@ -60,6 +64,7 @@ export async function createProductionController(ctx, config = {}, internals = {
}
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('dingtalk', bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -101,7 +106,10 @@ export async function createProductionController(ctx, config = {}, internals = {
logger,
createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('dingtalk', botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -111,6 +119,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, {
channel: 'dingtalk', config: botConfig,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
maxMessageChars: config.maxMessageChars ?? 4_000,
connectTimeoutMs: config.connectTimeoutMs ?? 15_000,

View file

@ -1,5 +1,6 @@
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 { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@ -20,6 +21,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
approveSender: 'bot.sender.approve',
revokeSender: 'bot.sender.revoke',
});
@ -100,6 +102,10 @@ function payloadFailure(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === DINGTALK_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
return exactKeys(payload, ['botId', 'requestId', 'confirm'])
&& validId(payload.botId)
@ -262,6 +268,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
value = await controller.updateContextEnhancement(
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === DINGTALK_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === DINGTALK_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await publicStatus(

View file

@ -24,6 +24,10 @@ import {
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
import { listAgentPresetCatalog } from '../../../../src/channels/shared/agent-preset.mjs';
import { createDeliveryAdapter } from '../../delivery-adapter.mjs';
import {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
// The WebSocket agent built here is only used for the Feishu long connection,
// whose endpoint is open.feishu.cn (Feishu) or open.larksuite.com (Lark).
@ -120,6 +124,7 @@ export async function createProductionController(ctx, config = {}, internals = {
}
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.id, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('feishu', bot),
})));
const observedConfigStore = typeof configStore.removeBot === 'function'
? observeBotWorkspaceRemovals(configStore, {
@ -181,7 +186,10 @@ export async function createProductionController(ctx, config = {}, internals = {
createRuntime: async ({ botId, config: botConfig, appSecret, repair }) => {
const state = await stateFor(botConfig);
const id = botId ?? botConfig.id ?? botConfig.appId;
await workspaces.ensure(id, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(id, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('feishu', botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId: id, workspaces, state, agentPresetCatalog,
});
@ -198,6 +206,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId: id, getSettings: () => workspaces.contextEnhancementFor(id) },
accessPolicy: accessPolicyProvider(workspaces, id, {
channel: 'feishu', config: botConfig,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
slashCommands: config.slashCommands !== false,
...(wsAgent ? { wsAgent } : {}),

View file

@ -5,21 +5,27 @@ import {
} from '../../../../src/channels/shared/agent-preset.mjs';
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
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 { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
import { validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
import { validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
import {
isFeishuGroupResponseMode,
normalizeFeishuGroupResponseMode,
} from '../../../../src/channels/feishu/group-response-mode.mjs';
import {
FEISHU_ENDPOINTS,
FEISHU_ENDPOINTS as FEISHU_CLIENT_ENDPOINTS,
FEISHU_RPC_CHANNEL,
} from '../../../client/channels/feishu/api.js';
export { FEISHU_ENDPOINTS, FEISHU_RPC_CHANNEL };
export const FEISHU_ENDPOINTS = Object.freeze({
...FEISHU_CLIENT_ENDPOINTS,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
});
export { FEISHU_RPC_CHANNEL };
export const FEISHU_MULTI_ENDPOINTS = Object.freeze({
reconnectBot: 'bot.reconnect',
disconnectBot: 'bot.disconnect',
@ -279,6 +285,7 @@ function publicBotEntry(entry) {
configured: source.configured === true,
agentPreset: normalizeAgentPresetId(source.agentPreset),
contextEnhancement: normalizeContextEnhancementConfig(source.contextEnhancement),
accessPolicy: normalizeAccessPolicy(source.accessPolicy),
groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
bot: publicBot(source.bot),
@ -422,6 +429,10 @@ function validPayload(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === FEISHU_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
if (endpoint === FEISHU_ENDPOINTS.setGroupResponseMode) {
return hasOnlyKeys(payload, new Set(['botId', 'groupResponseMode']))
&& safeOpaqueId(payload.botId)
@ -679,6 +690,12 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
payload.botId, payload.config,
(status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
);
} else if (endpoint === FEISHU_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(
payload.botId, payload.policy,
(status) => toPublicFeishuStatus(status, { encodeQr: cachedEncodeQr }),
);
} else if (endpoint === FEISHU_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await toPublicFeishuStatus(

View file

@ -20,6 +20,10 @@ 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 {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@ -54,6 +58,7 @@ export async function createProductionController(ctx, config = {}, internals = {
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('qq', bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -92,7 +97,10 @@ export async function createProductionController(ctx, config = {}, internals = {
logger,
createRuntime: async ({ botId, config: botConfig, appSecret }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('qq', botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -102,6 +110,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, {
channel: 'qq', config: botConfig,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
logger: {

View file

@ -4,6 +4,7 @@ import {
publicConnectionTestResult,
} from '../../../../src/channels/shared/connection-test.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 { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@ -20,6 +21,7 @@ export const QQ_ENDPOINTS = Object.freeze({
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
});
export const QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_ENDPOINTS));
@ -82,6 +84,10 @@ function payloadFailure(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === QQ_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
return 'Unknown QQ endpoint.';
}
@ -182,6 +188,11 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
value = await controller.updateContextEnhancement(
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === QQ_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === QQ_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await publicStatus(

View file

@ -0,0 +1,137 @@
import {
createAccessPolicy,
createAccessPolicyScope,
} from '../../../../src/channels/shared/access-policy.mjs';
function policyUsers(users) {
return users.map((id) => ({ id, canExecuteCommands: true }));
}
function openScope(allowlistUsers = []) {
return createAccessPolicyScope({
mode: 'open',
open: {
defaultCanExecuteCommands: true,
commandPermissionOverrides: [],
},
allowlist: { users: policyUsers(allowlistUsers) },
});
}
function allowlistScope(users = []) {
return createAccessPolicyScope({
mode: 'allowlist',
open: {
defaultCanExecuteCommands: false,
commandPermissionOverrides: [],
},
allowlist: { users: policyUsers(users) },
});
}
function cleanIds(values) {
return [...new Set((Array.isArray(values) ? values : [values])
.filter((value) => typeof value === 'string' || typeof value === 'number'
|| typeof value === 'bigint')
.map((value) => String(value).trim())
.filter(Boolean))];
}
function whatsappNumberJids(values) {
return cleanIds(values).map((value) => `${value.replace(/^\+/, '')}@s.whatsapp.net`);
}
/**
* Build the one-time, backwards-compatible seed for a bot whose workspace
* document does not yet contain an access policy.
*/
export function initialAccessPolicyFor(channel, config = {}) {
const key = String(channel ?? '').trim().toLowerCase();
if (key === 'weixin') {
return createAccessPolicy({
direct: allowlistScope(),
group: allowlistScope(),
});
}
if (key === 'feishu') {
const owners = cleanIds(config.ownerOpenIds ?? config.ownerOpenId);
const scope = owners.includes('*') ? openScope() : allowlistScope();
return createAccessPolicy({ direct: scope, group: scope });
}
if (key === 'qq') {
const owners = cleanIds(config.ownerUserOpenid);
return createAccessPolicy({
direct: owners.includes('*') ? openScope() : allowlistScope(),
group: openScope(),
});
}
if (key === 'telegram') {
const users = cleanIds(config.allowedUsers);
if ((config.accessMode ?? 'compatible') === 'private-allowlist') {
return createAccessPolicy({
direct: allowlistScope(users),
group: allowlistScope(),
});
}
return createAccessPolicy({
direct: openScope(users),
group: openScope(),
});
}
if (key === 'whatsapp') {
const mode = config.accessMode ?? 'self-only';
const allowed = whatsappNumberJids(config.allowedNumbers);
if (mode === 'open') {
return createAccessPolicy({ direct: openScope(allowed), group: openScope() });
}
return createAccessPolicy({
direct: allowlistScope(mode === 'private-allowlist'
? allowed
: []),
group: allowlistScope(),
});
}
if (['dingtalk', 'wecom', 'slack', 'discord'].includes(key)) {
return createAccessPolicy({ direct: openScope(), group: openScope() });
}
throw new TypeError(`Unsupported access-policy channel: ${channel}`);
}
export function privilegedSenderIdsFor(channel, config = {}) {
const key = String(channel ?? '').trim().toLowerCase();
if (key === 'weixin') return cleanIds(config.ownerUserId);
if (key === 'feishu') {
return cleanIds(config.ownerOpenIds ?? config.ownerOpenId).filter((id) => id !== '*');
}
if (key === 'dingtalk') {
const approved = Array.isArray(config.approvedSenders) ? config.approvedSenders : [];
return cleanIds(approved.map((entry) => entry?.staffId));
}
if (key === 'qq') return cleanIds(config.ownerUserOpenid).filter((id) => id !== '*');
if (key === 'whatsapp') return cleanIds(config.accountJid);
return [];
}
export function accessPolicyProvider(workspaces, botId, { channel, config, equals } = {}) {
if (!workspaces || typeof workspaces.accessPolicyFor !== 'function') {
throw new TypeError('A workspace store with access policies is required');
}
const privilegedSenderIds = new Set(privilegedSenderIdsFor(channel, config));
const sameSender = typeof equals === 'function' ? equals : (left, right) => left === right;
return Object.freeze({
botId,
getSettings: () => workspaces.accessPolicyFor(botId),
isPrivileged(senderIds, conversationType) {
if (!['direct', 'group'].includes(conversationType)) return false;
const candidates = Array.isArray(senderIds) ? senderIds : [senderIds];
try {
return candidates.some((senderId) => typeof senderId === 'string'
&& [...privilegedSenderIds].some((privilegedId) => (
sameSender(senderId.trim(), privilegedId) === true
)));
} catch {
return false;
}
},
});
}

View file

@ -0,0 +1,17 @@
import { validateAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
export const SET_ACCESS_POLICY_ENDPOINT = 'bot.access-policy.set';
export function validAccessPolicyPayload(payload) {
try {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)
|| Reflect.ownKeys(payload).length !== 2
|| !Object.hasOwn(payload, 'botId') || !Object.hasOwn(payload, 'policy')
|| typeof payload.botId !== 'string'
|| !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) return false;
validateAccessPolicy(payload.policy);
return true;
} catch {
return false;
}
}

View file

@ -17,6 +17,10 @@ import {
createDeliveryAdapter,
supportsDeliveryChannel,
} from '../../delivery-adapter.mjs';
import {
accessPolicyProvider,
initialAccessPolicyFor,
} from './access-policy-production.mjs';
export function pluginPaths(config, channel) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@ -46,6 +50,11 @@ export async function createTokenProductionController(ctx, config, internals, de
throw new TypeError(`dsh-im ${channel} runtimeOptions must return an object`);
}
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
const seedAccessPolicy = typeof definitions.initialAccessPolicyForBot === 'function'
? definitions.initialAccessPolicyForBot
// Telegram is the only token channel with a legacy access model. Other
// current token channels preserve their fully-open baseline.
: (bot) => initialAccessPolicyFor(channel === 'telegram' ? 'telegram' : 'discord', bot);
const logger = typeof ctx.logger === 'function'
? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
const agentPresetCatalog = () => listAgentPresetCatalog(ctx);
@ -59,6 +68,7 @@ export async function createTokenProductionController(ctx, config, internals, de
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: seedAccessPolicy(bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -96,7 +106,10 @@ export async function createTokenProductionController(ctx, config, internals, de
...(internals.inspectToken ? { inspectToken: internals.inspectToken } : {}),
createRuntime: async ({ botId, config: botConfig, token }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: seedAccessPolicy(botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -107,6 +120,7 @@ export async function createTokenProductionController(ctx, config, internals, de
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, { channel, config: botConfig }),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
logger: {

View file

@ -1,4 +1,5 @@
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
import {
@ -19,6 +20,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
});
const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@ -77,6 +79,10 @@ function payloadFailure(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
return 'Unknown bot endpoint.';
}
@ -161,6 +167,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setContextEnhancement) {
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
value = await controller.updateContextEnhancement(payload.botId, payload.config);
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);

View file

@ -19,6 +19,10 @@ 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';
import {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
export async function createProductionController(ctx, config = {}, internals = {}) {
if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
@ -43,6 +47,7 @@ export async function createProductionController(ctx, config = {}, internals = {
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('slack', bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -80,7 +85,10 @@ export async function createProductionController(ctx, config = {}, internals = {
...(internals.inspectCredentials ? { inspectCredentials: internals.inspectCredentials } : {}),
createRuntime: async ({ botId, config: botConfig, botToken, appToken }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('slack', botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -91,6 +99,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, {
channel: 'slack', config: botConfig,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
logger: {

View file

@ -1,4 +1,5 @@
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 { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
import {
@ -20,6 +21,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
});
export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
@ -81,6 +83,10 @@ function payloadFailure(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === SLACK_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
return 'Unknown Slack endpoint.';
}
@ -164,6 +170,10 @@ export function createSlackRpcHandler(controller) {
if (typeof controller.updateContextEnhancement !== 'function') throw new Error('Context enhancement update is unavailable');
value = await controller.updateContextEnhancement(payload.botId, payload.config);
}
else if (endpoint === SLACK_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(payload.botId, payload.policy);
}
else if (endpoint === SLACK_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await controller.updateAgentPreset(payload.botId, payload.agentPreset);

View file

@ -3,60 +3,13 @@ import {
createTokenBotRpcHandler,
} from '../shared/rpc.mjs';
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { normalizeTelegramAccessPolicy } from '../../../../src/channels/telegram/config-store.mjs';
export const TELEGRAM_RPC_CHANNEL = '/telegram';
export const TELEGRAM_ENDPOINTS = Object.freeze({
...TOKEN_BOT_ENDPOINTS,
setAccessPolicy: 'bot.access-policy.set',
});
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
export const TELEGRAM_RPC_ENDPOINTS = Object.freeze(Object.values(TELEGRAM_ENDPOINTS));
export function createTelegramRpcHandler(controller) {
if (typeof controller?.setAccessPolicy !== 'function') {
throw new TypeError('A complete Telegram controller is required (setAccessPolicy)');
}
const sharedHandler = createTokenBotRpcHandler(controller, { channel: 'Telegram' });
return async (endpoint, payload, signal) => {
if (endpoint !== TELEGRAM_ENDPOINTS.setAccessPolicy) {
return sharedHandler(endpoint, payload, signal);
}
if (signal?.aborted) {
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
}
const keys = payload && typeof payload === 'object' && !Array.isArray(payload)
? Object.keys(payload) : [];
if (keys.length !== 3 || !keys.every((key) => (
['botId', 'accessMode', 'allowedUsers'].includes(key)
)) || typeof payload.botId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)) {
return {
ok: false,
error: { code: 'bad-request', message: 'bot.access-policy.set requires a valid policy.' },
};
}
let accessPolicy;
try {
accessPolicy = normalizeTelegramAccessPolicy(payload);
} catch {
return {
ok: false,
error: { code: 'bad-request', message: '请输入有效的 Telegram 访问模式和数字 User ID。' },
};
}
try {
const value = await controller.setAccessPolicy(payload.botId, accessPolicy);
return signal?.aborted
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
: { ok: true, value };
} catch {
return signal?.aborted
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
: {
ok: false,
error: { code: 'telegram-operation-failed', message: 'Telegram 操作失败,请稍后重试。' },
};
}
};
return createTokenBotRpcHandler(controller, { channel: 'Telegram' });
}
export function installTelegramRpc(ctx, controller, authority) {

View file

@ -20,6 +20,10 @@ 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 {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@ -54,6 +58,7 @@ export async function createProductionController(ctx, config = {}, internals = {
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('wecom', bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -95,7 +100,10 @@ export async function createProductionController(ctx, config = {}, internals = {
logger,
createRuntime: async ({ botId, config: botConfig, secret }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('wecom', botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -105,6 +113,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, {
channel: 'wecom', config: botConfig,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,

View file

@ -1,5 +1,6 @@
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 { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
import { SET_AGENT_PRESET_ENDPOINT, validAgentPresetPayload } from '../shared/agent-preset-rpc.mjs';
@ -20,6 +21,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
});
export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
@ -82,6 +84,10 @@ function payloadFailure(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === WECOM_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
return 'Unknown Enterprise WeChat endpoint.';
}
@ -183,6 +189,11 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
value = await controller.updateContextEnhancement(
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === WECOM_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === WECOM_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await publicStatus(

View file

@ -23,6 +23,10 @@ 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 {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
function pluginPaths(config) {
const dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
@ -60,6 +64,7 @@ export async function createProductionController(ctx, config = {}, internals = {
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('weixin', bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -98,7 +103,10 @@ export async function createProductionController(ctx, config = {}, internals = {
logger,
createRuntime: async ({ botId, config: accountConfig, token }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('weixin', accountConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -109,6 +117,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, {
channel: 'weixin', config: accountConfig,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
maxMessageChars: config.maxMessageChars ?? DEFAULT_WEIXIN_MAX_MESSAGE_CHARS,
logger: {

View file

@ -1,5 +1,6 @@
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 { resolveRpcAuthority } from '../../rpc-authority.mjs';
import {
publicWorkspaceError,
@ -27,6 +28,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
});
export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
@ -89,6 +91,10 @@ function payloadFailure(endpoint, payload) {
return validContextEnhancementPayload(payload)
? null : '请提交有效的上下文增强设置。';
}
if (endpoint === WEIXIN_ENDPOINTS.setAccessPolicy) {
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
return 'Unknown Weixin endpoint.';
}
@ -218,6 +224,11 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
value = await controller.updateContextEnhancement(
payload.botId, payload.config, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === WEIXIN_ENDPOINTS.setAccessPolicy) {
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(
payload.botId, payload.policy, (status) => publicStatus(status, cachedEncode),
);
} else if (endpoint === WEIXIN_ENDPOINTS.setAgentPreset) {
if (typeof controller.updateAgentPreset !== 'function') throw new Error('Agent preset update is unavailable');
value = await publicStatus(

View file

@ -6,7 +6,10 @@ import { WhatsappConfigStore } from '../../../../src/channels/whatsapp/config-st
import { WhatsappHarnessClient } from '../../../../src/channels/whatsapp/harness-client.mjs';
import { WhatsappStateStore } from '../../../../src/channels/whatsapp/state-store.mjs';
import { WhatsappController } from '../../../../src/channels/whatsapp/whatsapp-controller.mjs';
import { WhatsappRuntime } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
import {
WhatsappRuntime,
whatsappAccessPolicyIdsEqual,
} from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
import { createWhatsappWebSession } from '../../../../src/channels/whatsapp/whatsapp-web-session.mjs';
import {
BotWorkspaceStore,
@ -20,6 +23,10 @@ import { createTokenConnectionSupervisor } from '../shared/connection-supervisor
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
import { harnessConnection } from '../../harness-connection.mjs';
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
import {
accessPolicyProvider,
initialAccessPolicyFor,
} from '../shared/access-policy-production.mjs';
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
@ -61,6 +68,7 @@ export async function createProductionController(ctx, config = {}, internals = {
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('whatsapp', bot),
})));
const observedConfigStore = typeof configStore.remove === 'function'
? observeBotWorkspaceRemovals(configStore, { workspaces })
@ -98,7 +106,10 @@ export async function createProductionController(ctx, config = {}, internals = {
logger,
createRuntime: async ({ botId, config: botConfig, authDir }) => {
const state = await stateFor(botId);
await workspaces.ensure(botId, { defaultAgentPreset: config.agentPreset });
await workspaces.ensure(botId, {
defaultAgentPreset: config.agentPreset,
initialAccessPolicy: initialAccessPolicyFor('whatsapp', botConfig),
});
const workspaceScope = createBotWorkspaceScope(harness, {
botId, workspaces, state, agentPresetCatalog,
});
@ -108,6 +119,9 @@ export async function createProductionController(ctx, config = {}, internals = {
harness: workspaceScope.harness,
state: workspaceScope.state,
contextEnhancement: { botId, getSettings: () => workspaces.contextEnhancementFor(botId) },
accessPolicy: accessPolicyProvider(workspaces, botId, {
channel: 'whatsapp', config: botConfig, equals: whatsappAccessPolicyIdsEqual,
}),
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
createSession,

View file

@ -1,7 +1,7 @@
import QRCode from 'qrcode';
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
import { normalizeWhatsappAccessPolicy } from '../../../../src/channels/whatsapp/config-store.mjs';
import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
@ -15,7 +15,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
cancelProvisioning: 'provision.cancel',
reconnectBot: 'bot.reconnect',
deleteBot: 'bot.delete',
setAccessPolicy: 'bot.access-policy.set',
setAccessPolicy: SET_ACCESS_POLICY_ENDPOINT,
setWorkspace: SET_WORKSPACE_ENDPOINT,
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
setContextEnhancement: SET_CONTEXT_ENHANCEMENT_ENDPOINT,
@ -55,15 +55,8 @@ function payloadFailure(endpoint, payload) {
&& payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
}
if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
if (!exactKeys(payload, ['botId', 'accessMode', 'allowedNumbers'])
|| Object.keys(payload).length !== 3
|| !validId(payload.botId)) return '请输入有效的 WhatsApp 访问模式和电话号码。';
try {
normalizeWhatsappAccessPolicy(payload);
return null;
} catch {
return '请输入有效的 WhatsApp 访问模式和电话号码。';
}
return validAccessPolicyPayload(payload)
? null : '请提交有效的访问设置。';
}
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
return validWorkspacePayload(payload)
@ -111,7 +104,7 @@ async function publicStatus(value, encodeQr) {
}
export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot', 'setAccessPolicy']) {
for (const method of ['status', 'startProvisioning', 'registrationStatus', 'cancelProvisioning', 'reconnectBot', 'deleteBot']) {
if (typeof controller?.[method] !== 'function') {
throw new TypeError(`A complete WhatsApp controller is required (${method})`);
}
@ -191,9 +184,11 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
cachedEncode,
);
} else if (endpoint === WHATSAPP_ENDPOINTS.setAccessPolicy) {
value = await publicStatus(
await controller.setAccessPolicy(payload.botId, normalizeWhatsappAccessPolicy(payload)),
cachedEncode,
if (typeof controller.updateAccessPolicy !== 'function') throw new Error('Access policy update is unavailable');
value = await controller.updateAccessPolicy(
payload.botId,
payload.policy,
(status) => publicStatus(status, cachedEncode),
);
} else {
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);