mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
feat: add unified IM access policies
This commit is contained in:
parent
075d2713c1
commit
840e5aa857
89 changed files with 5573 additions and 1676 deletions
351
plugin-src/client/access-policy-settings.js
Normal file
351
plugin-src/client/access-policy-settings.js
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
DEFAULT_ACCESS_POLICY,
|
||||
normalizeAccessPolicy,
|
||||
validateAccessPolicy,
|
||||
} from '../../src/channels/shared/access-policy.mjs';
|
||||
import { h, localizeText } from './i18n.js';
|
||||
|
||||
export const ACCESS_POLICY_ENDPOINT = 'bot.access-policy.set';
|
||||
|
||||
export const ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
|
||||
weixin: Object.freeze({
|
||||
directUserLabel: '微信用户 ID',
|
||||
directPlaceholder: '填写微信用户 ID',
|
||||
groupSupported: false,
|
||||
}),
|
||||
feishu: Object.freeze({
|
||||
directUserLabel: '飞书 Open ID',
|
||||
directPlaceholder: 'ou_xxx',
|
||||
groupUserLabel: '群成员 Open ID',
|
||||
groupPlaceholder: 'ou_xxx',
|
||||
}),
|
||||
dingtalk: Object.freeze({
|
||||
directUserLabel: '钉钉用户 ID',
|
||||
directPlaceholder: '填写 senderStaffId 或 senderId',
|
||||
groupUserLabel: '群成员用户 ID',
|
||||
groupPlaceholder: '填写 senderStaffId 或 senderId',
|
||||
}),
|
||||
wecom: Object.freeze({
|
||||
directUserLabel: '企业微信用户 ID',
|
||||
directPlaceholder: '填写 userid',
|
||||
groupUserLabel: '群成员用户 ID',
|
||||
groupPlaceholder: '填写 userid',
|
||||
}),
|
||||
qq: Object.freeze({
|
||||
directUserLabel: 'QQ User Open ID',
|
||||
directPlaceholder: '填写 user_openid',
|
||||
groupUserLabel: '群成员 Open ID',
|
||||
groupPlaceholder: '填写 member_openid',
|
||||
}),
|
||||
slack: Object.freeze({
|
||||
directUserLabel: 'Slack User ID',
|
||||
directPlaceholder: 'U0123456789',
|
||||
groupUserLabel: '群成员 User ID',
|
||||
groupPlaceholder: 'U0123456789',
|
||||
}),
|
||||
telegram: Object.freeze({
|
||||
directUserLabel: 'Telegram User ID',
|
||||
directPlaceholder: '填写数字 User ID',
|
||||
groupUserLabel: '群成员 User ID',
|
||||
groupPlaceholder: '填写数字 User ID',
|
||||
}),
|
||||
discord: Object.freeze({
|
||||
directUserLabel: 'Discord User ID',
|
||||
directPlaceholder: '填写数字 User ID',
|
||||
groupUserLabel: '群成员 User ID',
|
||||
groupPlaceholder: '填写数字 User ID',
|
||||
}),
|
||||
whatsapp: Object.freeze({
|
||||
directUserLabel: 'WhatsApp 电话号码或 JID',
|
||||
directPlaceholder: '8613800000000 或完整 JID',
|
||||
groupUserLabel: '群成员电话号码或 JID',
|
||||
groupPlaceholder: '8613800000000 或完整 JID',
|
||||
}),
|
||||
});
|
||||
|
||||
function clonePolicy(policy) {
|
||||
const cloneScope = (scope) => ({
|
||||
mode: scope.mode,
|
||||
open: {
|
||||
defaultCanExecuteCommands: scope.open.defaultCanExecuteCommands,
|
||||
commandPermissionOverrides: scope.open.commandPermissionOverrides.map((user) => ({
|
||||
...user,
|
||||
})),
|
||||
},
|
||||
allowlist: {
|
||||
users: scope.allowlist.users.map((user) => ({ ...user })),
|
||||
},
|
||||
});
|
||||
return {
|
||||
direct: cloneScope(policy.direct),
|
||||
group: cloneScope(policy.group),
|
||||
};
|
||||
}
|
||||
|
||||
function unwrapRpcResult(result) {
|
||||
if (result?.ok === true) return result.value;
|
||||
if (result?.ok === false) {
|
||||
const error = new Error(result.error?.message || '访问设置保存失败,请稍后重试。');
|
||||
error.code = result.error?.code;
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function policyFromSnapshot(value, botId) {
|
||||
const source = value?.snapshot ?? value;
|
||||
const bot = Array.isArray(source?.bots)
|
||||
? source.bots.find((entry) => entry?.botId === botId)
|
||||
: null;
|
||||
return normalizeAccessPolicy(bot?.accessPolicy ?? source?.accessPolicy ?? source?.policy);
|
||||
}
|
||||
|
||||
function commandValue(value) {
|
||||
return value === 'allow';
|
||||
}
|
||||
|
||||
function ScenePolicyEditor({
|
||||
scene,
|
||||
title,
|
||||
policy,
|
||||
userLabel,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
unsupported = false,
|
||||
onChange,
|
||||
}) {
|
||||
const ownerHelpId = React.useId();
|
||||
const emptyAllowlistHelpId = React.useId();
|
||||
const allowlist = policy.mode === 'allowlist';
|
||||
const collectionKey = allowlist ? 'users' : 'commandPermissionOverrides';
|
||||
const branchKey = allowlist ? 'allowlist' : 'open';
|
||||
const users = policy[branchKey][collectionKey];
|
||||
const emptyAllowlist = allowlist && users.length === 0;
|
||||
const updateUsers = (nextUsers) => onChange({
|
||||
...policy,
|
||||
[branchKey]: {
|
||||
...policy[branchKey],
|
||||
[collectionKey]: nextUsers,
|
||||
},
|
||||
});
|
||||
const updateUser = (index, patch) => updateUsers(users.map((user, userIndex) => (
|
||||
userIndex === index ? { ...user, ...patch } : user
|
||||
)));
|
||||
|
||||
return h('fieldset', {
|
||||
className: 'dim-accessScene',
|
||||
disabled,
|
||||
'data-scene': scene,
|
||||
'aria-label': localizeText(title),
|
||||
},
|
||||
h('legend', null,
|
||||
h('span', { className: 'dim-accessLegendContent' },
|
||||
h('span', null, title),
|
||||
h('span', { className: 'dim-channelHelp dim-accessLegendHelp' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-channelHelpButton',
|
||||
'aria-label': [localizeText(title), localizeText('查看访问权限说明')].join(' '),
|
||||
'aria-describedby': ownerHelpId,
|
||||
}, h('span', { 'aria-hidden': true }, '?')),
|
||||
h('span', {
|
||||
id: ownerHelpId,
|
||||
className: 'dim-channelTooltip dim-accessHelpTooltip',
|
||||
role: 'tooltip',
|
||||
}, '原所有者或扫码接入者始终可以访问并执行命令;以下设置仅约束其他用户。')))),
|
||||
unsupported
|
||||
? h('div', { className: 'dim-accessUnsupported', role: 'note' },
|
||||
h('strong', null, '当前渠道不支持群聊'),
|
||||
h('p', null, '此区域无需配置,保存私聊设置时会保留现有群聊策略。'))
|
||||
: h(React.Fragment, null,
|
||||
h('div', { className: 'dim-accessControls', 'data-mode': policy.mode },
|
||||
h('label', { className: 'dim-accessField' },
|
||||
h('span', null, '访问模式'),
|
||||
h('select', {
|
||||
value: policy.mode,
|
||||
'aria-label': [localizeText(title), localizeText('访问模式')].join(' '),
|
||||
onChange: (event) => onChange({ ...policy, mode: event.target.value }),
|
||||
},
|
||||
h('option', { value: 'open' }, '允许所有用户'),
|
||||
h('option', { value: 'allowlist' }, '仅白名单用户'))),
|
||||
allowlist ? null : h('label', { className: 'dim-accessField' },
|
||||
h('span', null, '默认命令权限'),
|
||||
h('select', {
|
||||
value: policy.open.defaultCanExecuteCommands ? 'allow' : 'deny',
|
||||
'aria-label': [localizeText(title), localizeText('默认命令权限')].join(' '),
|
||||
onChange: (event) => onChange({
|
||||
...policy,
|
||||
open: {
|
||||
...policy.open,
|
||||
defaultCanExecuteCommands: commandValue(event.target.value),
|
||||
},
|
||||
}),
|
||||
},
|
||||
h('option', { value: 'allow' }, '可以执行命令'),
|
||||
h('option', { value: 'deny' }, '不可以执行命令')))),
|
||||
h('div', { className: 'dim-accessUsers' },
|
||||
h('div', { className: 'dim-accessUsersHeading' },
|
||||
h('div', { className: 'dim-accessUsersTitle' },
|
||||
h('strong', null, allowlist ? '白名单用户' : '命令权限例外'),
|
||||
emptyAllowlist
|
||||
? h('span', { className: 'dim-channelHelp dim-accessUsersHelp' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-channelHelpButton',
|
||||
'aria-label': [localizeText(title), localizeText('查看白名单说明')].join(' '),
|
||||
'aria-describedby': emptyAllowlistHelpId,
|
||||
}, h('span', { 'aria-hidden': true }, '?')),
|
||||
h('span', {
|
||||
id: emptyAllowlistHelpId,
|
||||
className: 'dim-channelTooltip dim-accessEmptyAllowlistTooltip',
|
||||
role: 'tooltip',
|
||||
}, '当前没有白名单用户,保存后普通用户将无法使用机器人。'))
|
||||
: null),
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-deliveryButton dim-accessAddUser',
|
||||
'aria-label': [localizeText(title), localizeText('新增用户')].join(' '),
|
||||
title: localizeText('新增用户'),
|
||||
onClick: () => updateUsers([...users, {
|
||||
id: '',
|
||||
canExecuteCommands: allowlist
|
||||
? false
|
||||
: !policy.open.defaultCanExecuteCommands,
|
||||
}]),
|
||||
}, h('span', { 'aria-hidden': true }, '+'))),
|
||||
users.length === 0
|
||||
? h('div', { className: 'dim-accessUsersEmpty' }, '尚未添加用户')
|
||||
: h('ul', { className: 'dim-accessUserList' }, users.map((user, index) =>
|
||||
h('li', { key: `${scene}-${policy.mode}-${index}`, className: 'dim-accessUserRow' },
|
||||
h('label', { className: 'dim-accessField dim-accessUserId' },
|
||||
h('span', null, userLabel),
|
||||
h('input', {
|
||||
value: user.id,
|
||||
maxLength: 256,
|
||||
required: true,
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off',
|
||||
spellCheck: false,
|
||||
placeholder,
|
||||
'aria-label': [localizeText(title), localizeText(userLabel), index + 1].join(' '),
|
||||
onChange: (event) => updateUser(index, { id: event.target.value }),
|
||||
})),
|
||||
h('label', { className: 'dim-accessField dim-accessUserCommand' },
|
||||
h('span', null, '命令权限'),
|
||||
h('select', {
|
||||
value: user.canExecuteCommands ? 'allow' : 'deny',
|
||||
'aria-label': [
|
||||
localizeText(title), localizeText('用户'), index + 1,
|
||||
localizeText('命令权限'),
|
||||
].join(' '),
|
||||
onChange: (event) => updateUser(index, {
|
||||
canExecuteCommands: commandValue(event.target.value),
|
||||
}),
|
||||
},
|
||||
h('option', { value: 'allow' }, '可以执行命令'),
|
||||
h('option', { value: 'deny' }, '不可以执行命令'))),
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-deliveryButton dim-accessDeleteUser',
|
||||
'data-kind': 'danger',
|
||||
'aria-label': [
|
||||
localizeText(title), localizeText('删除'),
|
||||
localizeText('用户'), index + 1,
|
||||
].join(' '),
|
||||
onClick: () => updateUsers(users.filter((_, userIndex) => userIndex !== index)),
|
||||
}, '删除')))))));
|
||||
}
|
||||
|
||||
export function AccessPolicySettingsPage({ channel, account, rpcCall, onSaved }) {
|
||||
const definition = ACCESS_CHANNEL_DEFINITIONS[channel];
|
||||
const initialPolicy = normalizeAccessPolicy(account?.accessPolicy);
|
||||
const initialKey = JSON.stringify(initialPolicy);
|
||||
const [draft, setDraft] = React.useState(() => clonePolicy(
|
||||
initialPolicy ?? DEFAULT_ACCESS_POLICY,
|
||||
));
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [feedback, setFeedback] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const next = normalizeAccessPolicy(account?.accessPolicy);
|
||||
setDraft(clonePolicy(next ?? DEFAULT_ACCESS_POLICY));
|
||||
}, [account?.botId, initialKey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFeedback(null);
|
||||
}, [account?.botId]);
|
||||
|
||||
if (!definition) {
|
||||
return h('div', { className: 'dim-accessState', role: 'alert' },
|
||||
'当前渠道暂不支持访问设置。');
|
||||
}
|
||||
|
||||
const save = async (event) => {
|
||||
event.preventDefault();
|
||||
setFeedback(null);
|
||||
setSaving(true);
|
||||
try {
|
||||
const policy = validateAccessPolicy(draft);
|
||||
if (typeof rpcCall !== 'function') throw new Error('访问设置暂不可用。');
|
||||
const value = unwrapRpcResult(await rpcCall(ACCESS_POLICY_ENDPOINT, {
|
||||
botId: account.botId,
|
||||
policy,
|
||||
}));
|
||||
const saved = policyFromSnapshot(value, account.botId);
|
||||
if (!saved) throw new Error('服务没有返回已保存的访问策略,请刷新后重试。');
|
||||
setDraft(clonePolicy(saved));
|
||||
onSaved?.(saved);
|
||||
setFeedback({ tone: 'success', message: '访问设置已保存。' });
|
||||
} catch (error) {
|
||||
setFeedback({
|
||||
tone: 'error',
|
||||
message: error?.message || '访问设置保存失败,请稍后重试。',
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return h('form', {
|
||||
className: 'dim-accessPage',
|
||||
onSubmit: (event) => void save(event),
|
||||
},
|
||||
initialPolicy
|
||||
? null
|
||||
: h('div', { className: 'dim-accessState', role: 'alert' },
|
||||
'访问策略尚未就绪,请返回机器人列表刷新后重试。'),
|
||||
h(ScenePolicyEditor, {
|
||||
scene: 'direct',
|
||||
title: '私聊',
|
||||
policy: draft.direct,
|
||||
userLabel: definition.directUserLabel,
|
||||
placeholder: definition.directPlaceholder,
|
||||
disabled: saving,
|
||||
onChange: (direct) => { setDraft((current) => ({ ...current, direct })); setFeedback(null); },
|
||||
}),
|
||||
h(ScenePolicyEditor, {
|
||||
scene: 'group',
|
||||
title: '群聊',
|
||||
policy: draft.group,
|
||||
userLabel: definition.groupUserLabel ?? definition.directUserLabel,
|
||||
placeholder: definition.groupPlaceholder ?? definition.directPlaceholder,
|
||||
disabled: saving || definition.groupSupported === false,
|
||||
unsupported: definition.groupSupported === false,
|
||||
onChange: (group) => { setDraft((current) => ({ ...current, group })); setFeedback(null); },
|
||||
}),
|
||||
feedback ? h('p', {
|
||||
className: 'dim-accessFeedback',
|
||||
'data-tone': feedback.tone,
|
||||
role: feedback.tone === 'error' ? 'alert' : 'status',
|
||||
'aria-live': 'polite',
|
||||
}, feedback.message) : null,
|
||||
h('div', { className: 'dim-accessActions' },
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
className: 'dim-deliveryButton',
|
||||
'data-kind': 'primary',
|
||||
disabled: saving || !initialPolicy,
|
||||
}, saving ? '正在保存…' : '保存访问设置')));
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ function SettingsGlyph() {
|
|||
h('path', { d: 'M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06a1.7 1.7 0 0 0-1.88-.34 1.7 1.7 0 0 0-1.03 1.55V21h-4v-.08A1.7 1.7 0 0 0 8.97 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.52-1.03H3v-4h.08A1.7 1.7 0 0 0 4.6 8.97a1.7 1.7 0 0 0-.34-1.88l-.06-.06L7.03 4.2l.06.06a1.7 1.7 0 0 0 1.88.34A1.7 1.7 0 0 0 10 3.08V3h4v.08a1.7 1.7 0 0 0 1.03 1.52 1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06a1.7 1.7 0 0 0-.34 1.88A1.7 1.7 0 0 0 20.92 10H21v4h-.08A1.7 1.7 0 0 0 19.4 15Z' }));
|
||||
}
|
||||
|
||||
export function BotSettingsButton({ channel, botId, botName, connected }) {
|
||||
export function BotSettingsButton({ channel, botId, botName, connected, accessPolicy }) {
|
||||
const { openBotSettings } = React.useContext(BotSettingsContext);
|
||||
const tooltipId = React.useId();
|
||||
return h('span', { className: 'dim-botSettingsAction' },
|
||||
|
|
@ -37,6 +37,7 @@ export function BotSettingsButton({ channel, botId, botName, connected }) {
|
|||
botId,
|
||||
botName,
|
||||
connected: Boolean(connected),
|
||||
accessPolicy,
|
||||
}),
|
||||
}, h(SettingsGlyph)),
|
||||
h('span', {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
|
||||
export const DINGTALK_RPC_CHANNEL = '/dingtalk';
|
||||
|
|
@ -15,6 +16,7 @@ export const DINGTALK_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: 'bot.context-enhancement.set',
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
|
||||
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
||||
|
|
@ -165,6 +167,9 @@ function normalizeBot(value) {
|
|||
workspace: optionalString(value.workspace, 4_096) ?? '',
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: optionalString(bot.name, 100) ?? '钉钉机器人',
|
||||
clientIdMasked: optionalString(bot.clientIdMasked, 140) ?? '已安全保存',
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ export function AccountCard({
|
|||
botId: account.botId,
|
||||
botName: account.bot.name,
|
||||
connected: account.connected,
|
||||
accessPolicy: account.accessPolicy,
|
||||
}))),
|
||||
h(WorkspaceEditor, {
|
||||
workspace: account.workspace,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId } from "../../agent-preset.js";
|
||||
import { normalizeLastMessageError } from "../../last-message-error.js";
|
||||
import { normalizeAccessPolicy } from "../../../../src/channels/shared/access-policy.mjs";
|
||||
import { normalizeContextEnhancementConfig } from "../../../../src/channels/shared/context-enhancement.mjs";
|
||||
|
||||
export const FEISHU_RPC_CHANNEL = "/feishu";
|
||||
|
|
@ -26,6 +27,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: "bot.workspace.set",
|
||||
setAgentPreset: "bot.preset.set",
|
||||
setContextEnhancement: "bot.context-enhancement.set",
|
||||
setAccessPolicy: "bot.access-policy.set",
|
||||
setGroupResponseMode: "bot.group-response-mode.set",
|
||||
// Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
|
||||
testConnection: "connection.test",
|
||||
|
|
@ -206,6 +208,9 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
|||
workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...(Object.hasOwn(value, "accessPolicy")
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
||||
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
||||
bot: normalizeBot(value.bot),
|
||||
|
|
|
|||
|
|
@ -609,6 +609,7 @@ export function BotCard({
|
|||
botId: connection.botId,
|
||||
botName: bot.name,
|
||||
connected,
|
||||
accessPolicy: connection.accessPolicy,
|
||||
})),
|
||||
),
|
||||
h(WorkspaceEditor, {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
|
||||
export const QQ_RPC_CHANNEL = '/qq';
|
||||
|
|
@ -15,6 +16,7 @@ export const QQ_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: 'bot.context-enhancement.set',
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
|
||||
const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
|
||||
|
|
@ -90,6 +92,9 @@ function normalizeBot(value) {
|
|||
workspace: text(value.workspace, '', 4_096),
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: text(value.bot?.name, 'QQ机器人', 100),
|
||||
appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ export function AccountCard({
|
|||
botId: account.botId,
|
||||
botName: account.bot.name,
|
||||
connected: account.connected,
|
||||
accessPolicy: account.accessPolicy,
|
||||
}))),
|
||||
h(WorkspaceEditor, {
|
||||
workspace: account.workspace,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
|
||||
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
||||
|
|
@ -31,6 +32,7 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: 'bot.context-enhancement.set',
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
|
||||
export function createTokenChannelApi(channel, connectionSummary, {
|
||||
|
|
@ -60,6 +62,9 @@ export function createTokenChannelApi(channel, connectionSummary, {
|
|||
workspace: text(value.workspace, '', 4_096),
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: text(value.bot?.name, `${channel}机器人`, 100),
|
||||
username: text(value.bot?.username, '', 100),
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export function createTokenChannelSettings(definition) {
|
|||
botId: account.botId,
|
||||
botName: account.bot.name,
|
||||
connected: account.connected,
|
||||
accessPolicy: account.accessPolicy,
|
||||
}))),
|
||||
h(WorkspaceEditor, {
|
||||
workspace: account.workspace,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,9 @@
|
|||
import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
|
||||
|
||||
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;
|
||||
|
||||
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询', {
|
||||
normalizeBotExtension: (value) => {
|
||||
const source = value?.accessPolicy;
|
||||
const accessMode = source?.accessMode === 'private-allowlist'
|
||||
? 'private-allowlist' : 'compatible';
|
||||
const allowedUsers = Array.isArray(source?.allowedUsers)
|
||||
? [...new Set(source.allowedUsers.filter((entry) => (
|
||||
typeof entry === 'string' && /^[1-9]\d{0,15}$/.test(entry)
|
||||
)))]
|
||||
: [];
|
||||
return { accessPolicy: { accessMode, allowedUsers } };
|
||||
},
|
||||
});
|
||||
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询');
|
||||
|
||||
export const unwrapRpcResult = api.unwrapRpcResult;
|
||||
export const normalizeSnapshot = api.normalizeSnapshot;
|
||||
|
|
|
|||
|
|
@ -1,121 +1,11 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { TelegramLogoGlyph } from '../../channel-logos.js';
|
||||
import { createTokenChannelSettings } from '../shared/token-channel.js';
|
||||
import { h } from '../../i18n.js';
|
||||
import {
|
||||
TELEGRAM_ENDPOINTS,
|
||||
telegramClientApi,
|
||||
} from './api.js';
|
||||
import { installTelegramStyles } from './styles.js';
|
||||
|
||||
function policyFor(account) {
|
||||
return {
|
||||
accessMode: account?.accessPolicy?.accessMode === 'private-allowlist'
|
||||
? 'private-allowlist' : 'compatible',
|
||||
allowedUsers: Array.isArray(account?.accessPolicy?.allowedUsers)
|
||||
? account.accessPolicy.allowedUsers : [],
|
||||
};
|
||||
}
|
||||
|
||||
function allowedUsersFromText(value) {
|
||||
const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
||||
if (entries.some((entry) => !/^[1-9]\d{0,15}$/.test(entry))) {
|
||||
throw new TypeError('User ID 必须是 1–16 位正整数,每行一个。');
|
||||
}
|
||||
return [...new Set(entries)];
|
||||
}
|
||||
|
||||
export function TelegramAccessSettings({ account, busy = false, onSave }) {
|
||||
const policy = policyFor(account);
|
||||
const sourceUsers = policy.allowedUsers.join('\n');
|
||||
const accessHelpId = React.useId();
|
||||
const [accessMode, setAccessMode] = React.useState(policy.accessMode);
|
||||
const [allowedUsers, setAllowedUsers] = React.useState(sourceUsers);
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAccessMode(policy.accessMode);
|
||||
setAllowedUsers(sourceUsers);
|
||||
setError(null);
|
||||
}, [policy.accessMode, sourceUsers]);
|
||||
|
||||
const save = async (event) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const normalized = allowedUsersFromText(allowedUsers);
|
||||
if (typeof onSave !== 'function') throw new Error('Telegram 访问设置暂不可用。');
|
||||
await onSave({ accessMode, allowedUsers: normalized });
|
||||
} catch (caught) {
|
||||
setError(caught?.message ?? 'Telegram 访问设置保存失败。');
|
||||
}
|
||||
};
|
||||
|
||||
const privateAllowlist = accessMode === 'private-allowlist';
|
||||
const savedPrivateAllowlist = policy.accessMode === 'private-allowlist';
|
||||
const emptyAllowlist = privateAllowlist && allowedUsers.trim() === '';
|
||||
return h('form', { className: 'dtg-access', onSubmit: save },
|
||||
h('div', { className: 'dtg-accessHeading' },
|
||||
h('strong', null, '访问设置'),
|
||||
h('span', { className: 'dtg-accessStatus' },
|
||||
h('span', { className: 'dtg-accessBadge', 'data-mode': policy.accessMode },
|
||||
savedPrivateAllowlist ? '已生效:安全模式' : '已生效:兼容模式'),
|
||||
h('span', { className: 'dtg-accessHelp' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dtg-accessHelpButton',
|
||||
'aria-label': '查看 Telegram 访问模式说明',
|
||||
'aria-describedby': accessHelpId,
|
||||
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
||||
h('span', {
|
||||
id: accessHelpId,
|
||||
className: 'dtg-accessTooltip',
|
||||
role: 'tooltip',
|
||||
},
|
||||
h('span', { className: 'dtg-accessTooltipItem' },
|
||||
h('strong', null, '兼容模式'),
|
||||
h('span', null, '保持原有行为:私聊直接响应,群聊在被提及或回复时响应。')),
|
||||
h('span', { className: 'dtg-accessTooltipItem' },
|
||||
h('strong', null, '安全模式'),
|
||||
h('span', null, '群聊全部忽略,私聊仅允许白名单用户。')))))),
|
||||
h('label', { className: 'dtg-accessField' },
|
||||
h('span', null, '模式'),
|
||||
h('select', {
|
||||
value: accessMode,
|
||||
disabled: busy,
|
||||
'aria-label': 'Telegram 访问模式',
|
||||
onChange: (event) => { setAccessMode(event.target.value); setError(null); },
|
||||
},
|
||||
h('option', { value: 'compatible' }, '兼容模式(默认)'),
|
||||
h('option', { value: 'private-allowlist' }, '安全模式(私聊白名单)'))),
|
||||
h('label', { className: 'dtg-accessField' },
|
||||
h('span', null, '允许私聊的 Telegram User ID'),
|
||||
h('textarea', {
|
||||
value: allowedUsers,
|
||||
disabled: busy || !privateAllowlist,
|
||||
rows: 3,
|
||||
placeholder: '每行一个数字 User ID',
|
||||
'aria-label': '允许私聊的 Telegram User ID',
|
||||
onChange: (event) => { setAllowedUsers(event.target.value); setError(null); },
|
||||
}),
|
||||
h('small', null, privateAllowlist
|
||||
? '白名单仅属于当前机器人。'
|
||||
: '兼容模式下暂不使用白名单,切换模式时会保留。')),
|
||||
emptyAllowlist
|
||||
? h('p', { className: 'dtg-accessWarning', role: 'status' },
|
||||
'白名单为空;保存后该机器人会拒绝所有入站消息。')
|
||||
: null,
|
||||
error ? h('p', { className: 'dtg-accessError', role: 'alert' }, error) : null,
|
||||
h('div', { className: 'dtg-accessActions' },
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
className: 'ddt-button',
|
||||
'data-kind': 'secondary',
|
||||
disabled: busy,
|
||||
}, busy ? '正在保存…' : '保存访问设置')));
|
||||
}
|
||||
|
||||
const channel = createTokenChannelSettings({
|
||||
channel: 'Telegram',
|
||||
endpoints: TELEGRAM_ENDPOINTS,
|
||||
|
|
@ -129,8 +19,6 @@ const channel = createTokenChannelSettings({
|
|||
emptyTitle: '接入 Telegram 机器人',
|
||||
emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
|
||||
platformLabel: 'Telegram',
|
||||
AccountSettings: TelegramAccessSettings,
|
||||
accountSettingsEndpoint: TELEGRAM_ENDPOINTS.setAccessPolicy,
|
||||
});
|
||||
|
||||
export const TelegramSettingsTab = channel.SettingsTab;
|
||||
|
|
|
|||
|
|
@ -4,34 +4,6 @@ const CSS = String.raw`
|
|||
.dtg-page { --ddt-accent: #229ed9; --ddt-accent-deep: #1687bd; --ddt-accent-wash: #eaf7fd; }
|
||||
.dtg-avatar { color: #fff; background: #229ed9; }
|
||||
.dtg-avatar svg { display: block; }
|
||||
.dtg-access { min-width: 0; width: 100%; max-width: 100%; display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
|
||||
.dtg-accessHeading { position: relative; min-width: 0; max-width: 100%; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 12px; }
|
||||
.dtg-accessHeading > strong { min-width: 0; font-size: 13px; overflow-wrap: anywhere; }
|
||||
.dtg-accessStatus { min-width: 0; max-width: 100%; flex: 0 1 auto; display: inline-flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 6px; }
|
||||
.dtg-accessBadge { min-width: 0; max-width: 100%; flex: 0 1 auto; padding: 3px 8px; border-radius: 999px; color: #1687bd; background: #eaf7fd; font-size: 11px; font-weight: 700; overflow-wrap: anywhere; text-align: center; }
|
||||
.dtg-accessBadge[data-mode="private-allowlist"] { color: #a15c00; background: #fff3d6; }
|
||||
.dtg-accessHelp { position: static; display: inline-flex; flex: none; }
|
||||
.dtg-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #229ed9 28%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #1687bd; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
|
||||
.dtg-accessHelpButton:hover { border-color: #229ed9; color: #1178a8; background: #eaf7fd; }
|
||||
.dtg-accessHelpButton:focus-visible { outline: none; border-color: #229ed9; box-shadow: 0 0 0 3px color-mix(in srgb, #229ed9 18%, transparent); }
|
||||
.dtg-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: min(300px, 100%); max-width: 100%; display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
||||
.dtg-accessTooltipItem { display: grid; gap: 2px; }
|
||||
.dtg-accessTooltipItem + .dtg-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
|
||||
.dtg-accessTooltipItem strong { color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; line-height: 17px; font-weight: 700; }
|
||||
.dtg-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; font-weight: 400; }
|
||||
.dtg-accessHelp:hover .dtg-accessTooltip, .dtg-accessHelp:focus-within .dtg-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
||||
.dtg-accessField { min-width: 0; max-width: 100%; display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
|
||||
.dtg-accessField select, .dtg-accessField textarea { min-width: 0; width: 100%; max-width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
|
||||
.dtg-accessField select { height: 34px; padding: 0 9px; }
|
||||
.dtg-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.dtg-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
|
||||
.dtg-accessField > span, .dtg-accessField small { overflow-wrap: anywhere; }
|
||||
.dtg-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
|
||||
.dtg-accessWarning, .dtg-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
|
||||
.dtg-accessWarning { color: #a15c00; }
|
||||
.dtg-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
|
||||
.dtg-accessActions { min-width: 0; max-width: 100%; display: flex; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.dtg-accessActions .ddt-button { max-width: 100%; white-space: normal; }
|
||||
`;
|
||||
|
||||
export function installTelegramStyles() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
|
||||
export const WECOM_RPC_CHANNEL = '/wecom';
|
||||
|
|
@ -15,6 +16,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: 'bot.context-enhancement.set',
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
|
||||
const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
|
||||
|
|
@ -99,6 +101,9 @@ function normalizeBot(value) {
|
|||
workspace: text(value.workspace, '', 4_096),
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: text(value.bot?.name, '企业微信机器人', 100),
|
||||
appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ export function AccountCard({
|
|||
botId: account.botId,
|
||||
botName: account.bot.name,
|
||||
connected: account.connected,
|
||||
accessPolicy: account.accessPolicy,
|
||||
}))),
|
||||
h(WorkspaceEditor, {
|
||||
workspace: account.workspace,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
|
||||
export const WEIXIN_RPC_CHANNEL = '/weixin';
|
||||
|
|
@ -14,6 +15,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: 'bot.workspace.set',
|
||||
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
||||
setContextEnhancement: 'bot.context-enhancement.set',
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
|
||||
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
||||
|
|
@ -124,6 +126,9 @@ function normalizeBot(value) {
|
|||
workspace: string(value.workspace).slice(0, 4_096),
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: string(value.bot.name, '微信机器人'),
|
||||
accountIdMasked: string(value.bot.accountIdMasked, '已安全保存'),
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ export function AccountCard({
|
|||
botId: account.botId,
|
||||
botName: account.bot.name,
|
||||
connected: account.connected,
|
||||
accessPolicy: account.accessPolicy,
|
||||
}))),
|
||||
h(WorkspaceEditor, {
|
||||
workspace: account.workspace,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
||||
import { normalizeLastMessageError } from '../../last-message-error.js';
|
||||
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
||||
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
||||
|
||||
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
||||
|
|
@ -91,16 +92,9 @@ function normalizeBot(value) {
|
|||
workspace: text(value.workspace, '', 4_096),
|
||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
||||
accessPolicy: {
|
||||
accessMode: ['self-only', 'private-allowlist', 'open'].includes(
|
||||
value.accessPolicy?.accessMode,
|
||||
) ? value.accessPolicy.accessMode : 'self-only',
|
||||
allowedNumbers: Array.isArray(value.accessPolicy?.allowedNumbers)
|
||||
? [...new Set(value.accessPolicy.allowedNumbers.filter((entry) => (
|
||||
typeof entry === 'string' && /^[1-9]\d{4,14}$/.test(entry)
|
||||
)))]
|
||||
: [],
|
||||
},
|
||||
...(Object.hasOwn(value, 'accessPolicy')
|
||||
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
||||
: {}),
|
||||
bot: {
|
||||
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
||||
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
||||
|
|
|
|||
|
|
@ -31,119 +31,6 @@ import { installWhatsappStyles } from './styles.js';
|
|||
|
||||
const ACTIVE_STATES = new Set(['pending', 'connecting']);
|
||||
|
||||
function accessPolicyFor(account) {
|
||||
const accessMode = ['self-only', 'private-allowlist', 'open'].includes(
|
||||
account?.accessPolicy?.accessMode,
|
||||
) ? account.accessPolicy.accessMode : 'self-only';
|
||||
return {
|
||||
accessMode,
|
||||
allowedNumbers: Array.isArray(account?.accessPolicy?.allowedNumbers)
|
||||
? account.accessPolicy.allowedNumbers : [],
|
||||
};
|
||||
}
|
||||
|
||||
function allowedNumbersFromText(value) {
|
||||
const entries = value.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
||||
const normalized = entries.map((entry) => entry.replace(/^\+/, ''));
|
||||
if (normalized.some((entry) => !/^[1-9]\d{4,14}$/.test(entry))) {
|
||||
throw new TypeError('电话号码必须包含国家或地区代码,每行一个。');
|
||||
}
|
||||
return [...new Set(normalized)];
|
||||
}
|
||||
|
||||
export function WhatsappAccessSettings({ account, busy = false, onSave }) {
|
||||
const policy = accessPolicyFor(account);
|
||||
const sourceNumbers = policy.allowedNumbers.join('\n');
|
||||
const helpId = React.useId();
|
||||
const [accessMode, setAccessMode] = React.useState(policy.accessMode);
|
||||
const [allowedNumbers, setAllowedNumbers] = React.useState(sourceNumbers);
|
||||
const [error, setError] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAccessMode(policy.accessMode);
|
||||
setAllowedNumbers(sourceNumbers);
|
||||
setError(null);
|
||||
}, [policy.accessMode, sourceNumbers]);
|
||||
|
||||
const save = async (event) => {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
try {
|
||||
const normalized = allowedNumbersFromText(allowedNumbers);
|
||||
if (typeof onSave !== 'function') throw new Error('WhatsApp 访问设置暂不可用。');
|
||||
await onSave({ accessMode, allowedNumbers: normalized });
|
||||
} catch (caught) {
|
||||
setError(caught?.message ?? 'WhatsApp 访问设置保存失败。');
|
||||
}
|
||||
};
|
||||
|
||||
const allowlistEnabled = accessMode === 'private-allowlist';
|
||||
const labels = {
|
||||
'self-only': '仅自己模式',
|
||||
'private-allowlist': '指定联系人模式',
|
||||
open: '开放响应模式',
|
||||
};
|
||||
return h('form', { className: 'dwa-access', onSubmit: save },
|
||||
h('div', { className: 'dwa-accessHeading' },
|
||||
h('strong', null, '访问设置'),
|
||||
h('span', { className: 'dwa-accessStatus' },
|
||||
h('span', { className: 'dwa-accessBadge', 'data-mode': policy.accessMode },
|
||||
['已生效:', labels[policy.accessMode]]),
|
||||
h('span', { className: 'dwa-accessHelp' },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dwa-accessHelpButton',
|
||||
'aria-label': '查看 WhatsApp 访问模式说明',
|
||||
'aria-describedby': helpId,
|
||||
}, h('span', { 'aria-hidden': 'true' }, '?')),
|
||||
h('span', { id: helpId, className: 'dwa-accessTooltip', role: 'tooltip' },
|
||||
h('span', { className: 'dwa-accessTooltipItem' },
|
||||
h('strong', null, '仅自己模式'),
|
||||
h('span', null, '只响应已绑定 WhatsApp 账号的自聊消息。')),
|
||||
h('span', { className: 'dwa-accessTooltipItem' },
|
||||
h('strong', null, '指定联系人模式'),
|
||||
h('span', null, '响应自聊和白名单联系人的私聊,忽略群聊。')),
|
||||
h('span', { className: 'dwa-accessTooltipItem' },
|
||||
h('strong', null, '开放响应模式'),
|
||||
h('span', null, '响应所有私聊、已绑定账号自己发出的群聊消息,以及其他群成员的提及或回复。')))))),
|
||||
h('label', { className: 'dwa-accessField' },
|
||||
h('span', null, '模式'),
|
||||
h('select', {
|
||||
value: accessMode,
|
||||
disabled: busy,
|
||||
'aria-label': 'WhatsApp 访问模式',
|
||||
onChange: (event) => { setAccessMode(event.target.value); setError(null); },
|
||||
},
|
||||
h('option', { value: 'self-only' }, '仅自己模式(默认)'),
|
||||
h('option', { value: 'private-allowlist' }, '指定联系人模式'),
|
||||
h('option', { value: 'open' }, '开放响应模式'))),
|
||||
allowlistEnabled
|
||||
? h('label', { className: 'dwa-accessField' },
|
||||
h('span', null, '允许私聊的 WhatsApp 电话号码'),
|
||||
h('textarea', {
|
||||
value: allowedNumbers,
|
||||
disabled: busy,
|
||||
rows: 3,
|
||||
placeholder: '每行一个含国家或地区代码的号码',
|
||||
'aria-label': '允许私聊的 WhatsApp 电话号码',
|
||||
onChange: (event) => { setAllowedNumbers(event.target.value); setError(null); },
|
||||
}),
|
||||
h('small', null, '可以包含开头的 +,保存时会自动移除。'))
|
||||
: null,
|
||||
allowlistEnabled && allowedNumbers.trim() === ''
|
||||
? h('p', { className: 'dwa-accessWarning', role: 'status' },
|
||||
'白名单为空;保存后将只接受自聊消息。')
|
||||
: null,
|
||||
error ? h('p', { className: 'dwa-accessError', role: 'alert' }, error) : null,
|
||||
h('div', { className: 'dwa-accessActions' },
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
className: 'ddt-button',
|
||||
'data-kind': 'secondary',
|
||||
disabled: busy,
|
||||
}, busy ? '正在保存…' : '保存访问设置')));
|
||||
}
|
||||
|
||||
const Button = React.forwardRef(function Button(
|
||||
{ children, kind = 'secondary', className = '', ...props },
|
||||
ref,
|
||||
|
|
@ -300,7 +187,6 @@ export function WhatsappAccountCard({
|
|||
onWorkspaceSave,
|
||||
onAgentPresetSave,
|
||||
onContextEnhancementSave,
|
||||
onAccessPolicySave,
|
||||
onRequestRemove,
|
||||
onConfirmRemove,
|
||||
onCancelRemove,
|
||||
|
|
@ -333,6 +219,7 @@ export function WhatsappAccountCard({
|
|||
botId: account.botId,
|
||||
botName: account.bot.name,
|
||||
connected: account.connected,
|
||||
accessPolicy: account.accessPolicy,
|
||||
}))),
|
||||
h(WorkspaceEditor, {
|
||||
workspace: account.workspace,
|
||||
|
|
@ -349,11 +236,6 @@ export function WhatsappAccountCard({
|
|||
disabled: Boolean(busy),
|
||||
onSave: onContextEnhancementSave,
|
||||
}),
|
||||
h(WhatsappAccessSettings, {
|
||||
account,
|
||||
busy: Boolean(busy),
|
||||
onSave: onAccessPolicySave,
|
||||
}),
|
||||
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
||||
h('div', { className: 'dim-cardFooterLayout' },
|
||||
h('div', { className: 'ddt-actions dim-cardActions' },
|
||||
|
|
@ -617,12 +499,6 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|||
WHATSAPP_ENDPOINTS.setContextEnhancement,
|
||||
{ botId: account.botId, config },
|
||||
),
|
||||
onAccessPolicySave: (accessPolicy) => botAction(
|
||||
account,
|
||||
'access',
|
||||
WHATSAPP_ENDPOINTS.setAccessPolicy,
|
||||
{ botId: account.botId, ...accessPolicy },
|
||||
),
|
||||
onRequestRemove: () => setRemoveTarget(account.botId),
|
||||
onCancelRemove: () => setRemoveTarget(null),
|
||||
onConfirmRemove: async () => {
|
||||
|
|
|
|||
|
|
@ -4,31 +4,6 @@ const CSS = String.raw`
|
|||
.dwa-page { --ddt-accent: #25d366; --ddt-accent-deep: #128c7e; --ddt-accent-wash: #eafbf0; }
|
||||
.dwa-avatar { color: #fff; background: #25d366; }
|
||||
.dwa-avatar svg { display: block; }
|
||||
.dwa-access { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 10px; background: var(--dsw-alias-bg-layer-2, #f7f8fa); }
|
||||
.dwa-accessHeading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.dwa-accessHeading > strong { font-size: 13px; }
|
||||
.dwa-accessStatus { min-width: 0; display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
|
||||
.dwa-accessBadge { flex: none; padding: 3px 8px; border-radius: 999px; color: #08785f; background: #eafbf0; font-size: 11px; font-weight: 700; }
|
||||
.dwa-accessBadge[data-mode="private-allowlist"] { color: #0f6f8f; background: #eaf7fd; }
|
||||
.dwa-accessBadge[data-mode="open"] { color: #a15c00; background: #fff3d6; }
|
||||
.dwa-accessHelp { position: relative; display: inline-flex; flex: none; }
|
||||
.dwa-accessHelpButton { width: 20px; height: 20px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #25d366 34%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #128c7e; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; line-height: 1; font-weight: 750; cursor: help; }
|
||||
.dwa-accessTooltip { position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; width: 270px; max-width: min(290px, calc(100vw - 48px)); display: grid; gap: 8px; padding: 10px 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
||||
.dwa-accessTooltipItem { display: grid; gap: 2px; }
|
||||
.dwa-accessTooltipItem + .dwa-accessTooltipItem { padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l2, #eef0f3); }
|
||||
.dwa-accessTooltipItem strong { font-size: 12px; line-height: 17px; }
|
||||
.dwa-accessTooltipItem > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
|
||||
.dwa-accessHelp:hover .dwa-accessTooltip, .dwa-accessHelp:focus-within .dwa-accessTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
||||
.dwa-accessField { display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
|
||||
.dwa-accessField select, .dwa-accessField textarea { width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #c9cdd4); border-radius: 7px; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-weight: 400; }
|
||||
.dwa-accessField select { height: 34px; padding: 0 9px; }
|
||||
.dwa-accessField textarea { min-height: 68px; padding: 8px 9px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.dwa-accessField textarea:disabled { color: var(--dsw-alias-label-tertiary, #8f959e); background: var(--dsw-alias-bg-module-platform, #f2f3f5); cursor: not-allowed; resize: none; opacity: 1; }
|
||||
.dwa-accessField small { color: var(--dsw-alias-label-secondary, #646a73); font-weight: 400; }
|
||||
.dwa-accessWarning, .dwa-accessError { margin: 0; font-size: 12px; line-height: 1.5; }
|
||||
.dwa-accessWarning { color: #a15c00; }
|
||||
.dwa-accessError { color: var(--dsw-alias-state-error-primary, #d83931); }
|
||||
.dwa-accessActions { display: flex; justify-content: flex-end; }
|
||||
`;
|
||||
|
||||
export function installWhatsappStyles() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { AccessPolicySettingsPage } from './access-policy-settings.js';
|
||||
import { h, isEnglish, localizeText } from './i18n.js';
|
||||
|
||||
export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
|
||||
|
|
@ -20,6 +21,7 @@ export const DELIVERY_ENDPOINTS = Object.freeze({
|
|||
|
||||
export const BOT_SETTINGS_TABS = Object.freeze([
|
||||
Object.freeze({ id: 'delivery', label: '投递设置' }),
|
||||
Object.freeze({ id: 'access', label: '访问设置' }),
|
||||
]);
|
||||
|
||||
const CHANNEL_DEFINITIONS = Object.freeze({
|
||||
|
|
@ -536,8 +538,15 @@ function TargetRow({ definition, target, botId, connected, rpcCall, onChanged, o
|
|||
: null);
|
||||
}
|
||||
|
||||
export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }) {
|
||||
export function DeliveryTargetSettingsPage({
|
||||
channel,
|
||||
account,
|
||||
rpcCall,
|
||||
accessRpcCall,
|
||||
onBack,
|
||||
}) {
|
||||
const definition = CHANNEL_DEFINITIONS[channel];
|
||||
const [activeTabId, setActiveTabId] = React.useState(BOT_SETTINGS_TABS[0].id);
|
||||
const [phase, setPhase] = React.useState('loading');
|
||||
const [targets, setTargets] = React.useState([]);
|
||||
const [suggestionPhase, setSuggestionPhase] = React.useState('idle');
|
||||
|
|
@ -547,8 +556,13 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
|
|||
const [editor, setEditor] = React.useState(null);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
const [botCopyState, setBotCopyState] = React.useState(null);
|
||||
const [accessPolicy, setAccessPolicy] = React.useState(account.accessPolicy);
|
||||
const mounted = React.useRef(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAccessPolicy(account.accessPolicy);
|
||||
}, [account.botId, account.accessPolicy]);
|
||||
|
||||
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
||||
if (typeof rpcCall !== 'function') throw new Error('投递目标设置暂不可用。');
|
||||
return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
||||
|
|
@ -658,9 +672,10 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
|
|||
}
|
||||
};
|
||||
|
||||
const deliveryTab = BOT_SETTINGS_TABS[0];
|
||||
const deliveryTabId = `dim-bot-settings-${deliveryTab.id}-tab`;
|
||||
const deliveryPanelId = `dim-bot-settings-${deliveryTab.id}-panel`;
|
||||
const activeTab = BOT_SETTINGS_TABS.find((tab) => tab.id === activeTabId)
|
||||
?? BOT_SETTINGS_TABS[0];
|
||||
const activeTabDomId = `dim-bot-settings-${activeTab.id}-tab`;
|
||||
const activePanelId = `dim-bot-settings-${activeTab.id}-panel`;
|
||||
|
||||
return h('section', {
|
||||
className: 'dim-deliveryPage',
|
||||
|
|
@ -679,16 +694,25 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
|
|||
type: 'button',
|
||||
role: 'tab',
|
||||
className: 'dim-botSettingsTab',
|
||||
'aria-selected': tab.id === deliveryTab.id,
|
||||
'aria-selected': tab.id === activeTab.id,
|
||||
'aria-controls': `dim-bot-settings-${tab.id}-panel`,
|
||||
tabIndex: tab.id === deliveryTab.id ? 0 : -1,
|
||||
tabIndex: tab.id === activeTab.id ? 0 : -1,
|
||||
onClick: () => setActiveTabId(tab.id),
|
||||
}, tab.label)))),
|
||||
h('div', {
|
||||
id: deliveryPanelId,
|
||||
id: activePanelId,
|
||||
className: 'dim-botSettingsTabPanel',
|
||||
role: 'tabpanel',
|
||||
'aria-labelledby': deliveryTabId,
|
||||
'aria-labelledby': activeTabDomId,
|
||||
},
|
||||
activeTab.id === 'access'
|
||||
? h(AccessPolicySettingsPage, {
|
||||
channel,
|
||||
account: { ...account, accessPolicy },
|
||||
rpcCall: accessRpcCall,
|
||||
onSaved: setAccessPolicy,
|
||||
})
|
||||
: h(React.Fragment, null,
|
||||
h('section', { className: 'dim-deliveryIdentity', 'aria-labelledby': 'dim-delivery-bot-title' },
|
||||
h('div', { className: 'dim-deliveryIdentityHeading' },
|
||||
h('h2', { id: 'dim-delivery-bot-title', className: 'dim-deliveryBotName' },
|
||||
|
|
@ -773,5 +797,5 @@ export function DeliveryTargetSettingsPage({ channel, account, rpcCall, onBack }
|
|||
rpcCall: invoke,
|
||||
onChanged: () => loadTargets({ silent: true }),
|
||||
onEdit: () => setEditor({ mode: 'edit', target, source: 'edit' }),
|
||||
}))))));
|
||||
})))))));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,59 @@ const EN = Object.freeze({
|
|||
'机器人设置': 'Bot settings',
|
||||
'机器人设置页签': 'Bot settings tabs',
|
||||
'投递设置': 'Delivery settings',
|
||||
'访问设置': 'Access settings',
|
||||
'查看访问权限说明': 'View access permission details',
|
||||
'允许所有用户': 'Allow all users',
|
||||
'仅白名单用户': 'Allowlisted users only',
|
||||
'默认命令权限': 'Default command permission',
|
||||
'命令权限': 'Command permission',
|
||||
'可以执行命令': 'Can run commands',
|
||||
'不可以执行命令': 'Cannot run commands',
|
||||
'白名单用户': 'Allowlisted users',
|
||||
'查看白名单说明': 'View allowlist details',
|
||||
'命令权限例外': 'Command permission exceptions',
|
||||
'当前没有白名单用户,保存后普通用户将无法使用机器人。': 'There are currently no allowlisted users. After saving, regular users will not be able to use the bot.',
|
||||
'新增用户': 'Add user',
|
||||
'尚未添加用户': 'No users added',
|
||||
'当前渠道不支持群聊': 'Group chat is not supported by this channel',
|
||||
'原所有者或扫码接入者始终可以访问并执行命令;以下设置仅约束其他用户。': 'The original owner or QR-code operator can always access the bot and run commands; the settings below apply only to other users.',
|
||||
'此区域无需配置,保存私聊设置时会保留现有群聊策略。': 'No setup is needed here. Saving direct-message settings keeps the existing group policy.',
|
||||
'访问设置已保存。': 'Access settings saved.',
|
||||
'访问设置暂不可用。': 'Access settings are currently unavailable.',
|
||||
'访问设置保存失败,请稍后重试。': 'Could not save access settings. Try again later.',
|
||||
'服务没有返回已保存的访问策略,请刷新后重试。': 'The service did not return the saved access policy. Refresh and try again.',
|
||||
'访问策略尚未就绪,请返回机器人列表刷新后重试。': 'The access policy is not ready. Return to the bot list, refresh, and try again.',
|
||||
'当前渠道暂不支持访问设置。': 'Access settings are not supported by this channel yet.',
|
||||
'用户标识无效。': 'The user ID is invalid.',
|
||||
'用户标识必须是字符串。': 'The user ID must be a string.',
|
||||
'用户标识不能为空、包含控制字符或超过 256 个字符。': 'The user ID cannot be empty, contain control characters, or exceed 256 characters.',
|
||||
'用户条目必须包含用户标识和命令权限。': 'Each user entry must include a user ID and command permission.',
|
||||
'命令权限必须是布尔值。': 'Command permission must be a boolean.',
|
||||
'访问模式只能是 open 或 allowlist。': 'Access mode must be open or allowlist.',
|
||||
'开放模式设置必须完整。': 'Open-mode settings must be complete.',
|
||||
'开放模式默认命令权限必须是布尔值。': 'The open-mode default command permission must be a boolean.',
|
||||
'开放模式命令权限覆盖用户必须是数组。': 'Open-mode command permission overrides must be an array.',
|
||||
'开放模式命令权限覆盖用户不能包含重复的用户标识。': 'Open-mode command permission overrides cannot contain duplicate user IDs.',
|
||||
'白名单模式设置必须完整。': 'Allowlist-mode settings must be complete.',
|
||||
'白名单模式用户必须是数组。': 'Allowlist-mode users must be an array.',
|
||||
'白名单模式用户不能包含重复的用户标识。': 'Allowlist-mode users cannot contain duplicate user IDs.',
|
||||
'访问场景设置必须同时包含模式、开放模式设置和白名单模式设置。': 'Each access context must include its mode, open-mode settings, and allowlist-mode settings.',
|
||||
'请同时提交完整的私聊和群聊访问设置。': 'Submit complete direct-message and group access settings together.',
|
||||
'填写微信用户 ID': 'Enter a WeChat user ID',
|
||||
'飞书 Open ID': 'Feishu Open ID',
|
||||
'群成员 Open ID': 'Group member Open ID',
|
||||
'钉钉用户 ID': 'DingTalk user ID',
|
||||
'填写 senderStaffId 或 senderId': 'Enter senderStaffId or senderId',
|
||||
'群成员用户 ID': 'Group member user ID',
|
||||
'企业微信用户 ID': 'WeCom user ID',
|
||||
'填写 userid': 'Enter userid',
|
||||
'填写 member_openid': 'Enter member_openid',
|
||||
'QQ User Open ID': 'QQ User Open ID',
|
||||
'群成员 User ID': 'Group member User ID',
|
||||
'填写数字 User ID': 'Enter a numeric user ID',
|
||||
'WhatsApp 电话号码或 JID': 'WhatsApp phone number or JID',
|
||||
'群成员电话号码或 JID': 'Group member phone number or JID',
|
||||
'8613800000000 或完整 JID': '8613800000000 or a full JID',
|
||||
'IM 渠道': 'IM channels',
|
||||
'让 DeepSeek Harness 触手可及': 'Connecting DeepSeek Harness',
|
||||
'当前版本': 'Current version',
|
||||
|
|
@ -524,7 +577,6 @@ const EN = Object.freeze({
|
|||
'先通过 @BotFather 获取 Bot Token,再在这里完成接入。': 'Get a Bot Token from @BotFather, then connect it here.',
|
||||
'填写 @BotFather 生成的 Bot Token': 'Enter the Bot Token from @BotFather',
|
||||
'访问模式': 'Access mode',
|
||||
'访问设置': 'Access settings',
|
||||
'Telegram 访问模式': 'Telegram access mode',
|
||||
'查看 Telegram 访问模式说明': 'View Telegram access mode details',
|
||||
'群聊全部忽略,私聊仅允许白名单用户。': 'All group messages are ignored; only allowlisted users may send DMs.',
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ export function IMSettingsTab({
|
|||
channel: active.id,
|
||||
account: deliverySettings,
|
||||
rpcCall: rpcCalls.deliveryRpcCall,
|
||||
accessRpcCall: rpcCalls[`${active.id}RpcCall`],
|
||||
onBack: () => setDeliverySettings(null),
|
||||
})
|
||||
: active.id === 'weixin'
|
||||
|
|
|
|||
|
|
@ -435,6 +435,38 @@ const CSS = String.raw`
|
|||
.dim-targetField input[readonly] { color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
||||
.dim-targetFormError { margin: 10px 0 0; font-size: 12px; line-height: 18px; }
|
||||
.dim-targetFormActions { display: flex; justify-content: flex-end; gap: 7px; margin-top: 12px; }
|
||||
.dim-accessPage { min-width: 0; display: grid; gap: 14px; }
|
||||
.dim-accessScene { position: relative; min-width: 0; margin: 0; padding: 16px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 12px; background: var(--dsw-alias-bg-layer-3, #fff); }
|
||||
.dim-accessScene > legend { padding: 0 6px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; line-height: 22px; font-weight: 650; }
|
||||
.dim-accessLegendContent { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.dim-panel .dim-accessLegendHelp { position: static; }
|
||||
.dim-accessLegendHelp .dim-channelTooltip { top: 20px; right: auto; left: 16px; width: min(320px, calc(100% - 32px)); max-width: none; }
|
||||
.dim-accessControls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.dim-accessControls[data-mode="allowlist"] { grid-template-columns: minmax(0, 1fr); }
|
||||
.dim-accessField { min-width: 0; display: grid; align-content: start; gap: 5px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 16px; }
|
||||
.dim-accessField input, .dim-accessField select { width: 100%; min-width: 0; height: 36px; padding: 0 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; }
|
||||
.dim-accessField input:focus, .dim-accessField select:focus { outline: 2px solid color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 28%, transparent); border-color: var(--dsw-alias-state-business-primary, #3370ff); }
|
||||
.dim-accessUsers { min-width: 0; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
|
||||
.dim-accessUsersHeading { position: relative; min-width: 0; display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.dim-accessUsersHeading > div { min-width: 0; }
|
||||
.dim-accessUsersTitle { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.dim-accessUsersHeading strong { color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; line-height: 20px; font-weight: 620; }
|
||||
.dim-accessUsersHeading p { margin: 2px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 17px; }
|
||||
.dim-panel .dim-accessUsersHelp { position: static; }
|
||||
.dim-accessUsersHelp .dim-channelTooltip { top: calc(100% + 7px); right: auto; left: 0; width: min(320px, 100%); max-width: none; }
|
||||
.dim-accessAddUser { width: 32px; height: 32px; min-height: 32px; flex: 0 0 32px; padding: 0; font-size: 20px; line-height: 1; }
|
||||
.dim-accessUsersEmpty { margin-top: 10px; padding: 15px 12px; border: 1px dashed var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; line-height: 18px; text-align: center; }
|
||||
.dim-accessUserList { display: grid; gap: 9px; margin: 10px 0 0; padding: 0; list-style: none; }
|
||||
.dim-accessUserRow { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) minmax(145px, 180px) max-content; align-items: end; gap: 10px; padding: 11px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
||||
.dim-accessDeleteUser { margin-bottom: 1px; }
|
||||
.dim-accessUnsupported { padding: 18px 14px; border: 1px dashed var(--dsw-alias-border-l2, #dfe1e5); border-radius: 9px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f7f8fa); text-align: center; }
|
||||
.dim-accessUnsupported strong { color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; line-height: 20px; }
|
||||
.dim-accessUnsupported p { margin: 4px 0 0; font-size: 12px; line-height: 18px; }
|
||||
.dim-accessState { padding: 11px 13px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 24%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 9px; color: var(--dsw-alias-state-warn-primary, #d97706); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 7%, var(--dsw-alias-bg-layer-1, #fff)); font-size: 12px; line-height: 18px; }
|
||||
.dim-accessFeedback { margin: 0; padding: 10px 12px; border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f7f8fa); font-size: 12px; line-height: 18px; }
|
||||
.dim-accessFeedback[data-tone="success"] { color: var(--dsw-alias-state-success-primary, #20a162); }
|
||||
.dim-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
||||
.dim-accessActions { display: flex; justify-content: flex-end; }
|
||||
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
|
||||
.dim-panel .ddt-headingCopy { display: none; }
|
||||
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
||||
|
|
@ -458,6 +490,9 @@ const CSS = String.raw`
|
|||
.dim-targetActions { justify-content: flex-start; }
|
||||
.dim-targetFormGrid { grid-template-columns: minmax(0, 1fr); }
|
||||
.dim-targetSuggestionHeading { align-items: stretch; flex-direction: column; }
|
||||
.dim-accessControls { grid-template-columns: minmax(0, 1fr); }
|
||||
.dim-accessUserRow { grid-template-columns: minmax(0, 1fr); }
|
||||
.dim-accessDeleteUser { justify-self: start; }
|
||||
}
|
||||
@media (max-width: 840px) {
|
||||
.dim-title { align-items: flex-start; }
|
||||
|
|
@ -486,6 +521,7 @@ const CSS = String.raw`
|
|||
.dim-deliveryBotId { grid-template-columns: minmax(0, 1fr) max-content; }
|
||||
.dim-deliveryBotId > span { grid-column: 1 / -1; }
|
||||
.dim-targetActions .dim-deliveryButton { flex: 1 1 auto; }
|
||||
.dim-accessActions .dim-deliveryButton { width: 100%; }
|
||||
.dim-directoryPickerBackdrop { padding: 10px; }
|
||||
.dim-directoryPicker { height: calc(100vh - 20px); min-height: 0; border-radius: 14px; }
|
||||
.dim-directoryPickerHeader { padding: 18px 17px 14px; }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 } : {}),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
137
plugin-src/host/channels/shared/access-policy-production.mjs
Normal file
137
plugin-src/host/channels/shared/access-policy-production.mjs
Normal 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
17
plugin-src/host/channels/shared/access-policy-rpc.mjs
Normal file
17
plugin-src/host/channels/shared/access-policy-rpc.mjs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue