mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
fix: preserve Telegram compatibility and stabilize Office
This commit is contained in:
parent
7f114e1c36
commit
24e690498b
23 changed files with 1679 additions and 314 deletions
|
|
@ -27,7 +27,9 @@ export const TOKEN_BOT_ENDPOINTS = Object.freeze({
|
|||
setWorkspace: 'bot.workspace.set',
|
||||
});
|
||||
|
||||
export function createTokenChannelApi(channel, connectionSummary) {
|
||||
export function createTokenChannelApi(channel, connectionSummary, {
|
||||
normalizeBotExtension = () => ({}),
|
||||
} = {}) {
|
||||
const unwrapRpcResult = (result) => {
|
||||
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
||||
throw new Error(`${channel} 服务返回了无法识别的响应`);
|
||||
|
|
@ -44,6 +46,7 @@ export function createTokenChannelApi(channel, connectionSummary) {
|
|||
if (!isRecord(value) || !id(value.botId)) return undefined;
|
||||
const connected = value.connected === true;
|
||||
const state = ACCOUNT_STATES.has(value.state) ? value.state : 'offline';
|
||||
const extension = normalizeBotExtension(value);
|
||||
return {
|
||||
botId: id(value.botId),
|
||||
connected,
|
||||
|
|
@ -65,6 +68,7 @@ export function createTokenChannelApi(channel, connectionSummary) {
|
|||
code: text(value.error.code, `${channel.toUpperCase()}_ACCOUNT_ERROR`, 80),
|
||||
message: text(value.error.message, `${channel}连接尚未就绪`),
|
||||
} : null,
|
||||
...(isRecord(extension) ? extension : {}),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -59,9 +59,11 @@ export function createTokenChannelSettings(definition) {
|
|||
credentialCloseLabel = '收起凭据',
|
||||
credentialNoun = 'Bot Token',
|
||||
emptyActionLabel = '填写 Bot Token',
|
||||
AccountSettings = null,
|
||||
accountSettingsEndpoint = null,
|
||||
} = definition;
|
||||
|
||||
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||
function AccountCard({ account, busy, testNotice, removing, onReconnect, onWorkspaceSave, onAccountSettingsSave, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
||||
const state = busy === 'reconnect' ? 'connecting' : account.state;
|
||||
const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
|
||||
const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
|
||||
|
|
@ -88,6 +90,11 @@ export function createTokenChannelSettings(definition) {
|
|||
disabled: Boolean(busy),
|
||||
onSave: onWorkspaceSave,
|
||||
}),
|
||||
AccountSettings ? h(AccountSettings, {
|
||||
account,
|
||||
busy: Boolean(busy),
|
||||
onSave: onAccountSettingsSave,
|
||||
}) : null,
|
||||
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
||||
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
||||
testNotice ? h('div', { className: 'ddt-summary dim-cardSummary', role: 'status' }, testNotice) : null,
|
||||
|
|
@ -260,6 +267,14 @@ export function createTokenChannelSettings(definition) {
|
|||
endpoints.setWorkspace,
|
||||
{ botId: account.botId, workspace },
|
||||
),
|
||||
onAccountSettingsSave: AccountSettings && accountSettingsEndpoint
|
||||
? (payload) => botAction(
|
||||
account,
|
||||
'settings',
|
||||
accountSettingsEndpoint,
|
||||
{ botId: account.botId, ...payload },
|
||||
)
|
||||
: undefined,
|
||||
onRequestRemove: () => setRemoveTarget(account.botId),
|
||||
onCancelRemove: () => setRemoveTarget(null),
|
||||
onConfirmRemove: async () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
|
||||
|
||||
export const TELEGRAM_RPC_CHANNEL = '/telegram';
|
||||
export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
|
||||
export const TELEGRAM_ENDPOINTS = Object.freeze({
|
||||
...TOKEN_BOT_ENDPOINTS,
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
|
||||
const api = createTokenChannelApi('Telegram', ' Bot API 长轮询');
|
||||
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 } };
|
||||
},
|
||||
});
|
||||
|
||||
export const unwrapRpcResult = api.unwrapRpcResult;
|
||||
export const normalizeSnapshot = api.normalizeSnapshot;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,105 @@
|
|||
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 [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('div', null,
|
||||
h('strong', null, '访问设置'),
|
||||
h('p', null, savedPrivateAllowlist
|
||||
? '群聊全部忽略,私聊仅允许白名单用户。'
|
||||
: '保持原有行为:私聊直接响应,群聊在被提及或回复时响应。')),
|
||||
h('span', { className: 'dtg-accessBadge', 'data-mode': policy.accessMode },
|
||||
savedPrivateAllowlist ? '已生效:安全模式' : '已生效:兼容模式')),
|
||||
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,
|
||||
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,
|
||||
|
|
@ -19,6 +113,8 @@ const channel = createTokenChannelSettings({
|
|||
emptyTitle: '接入 Telegram 机器人',
|
||||
emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
|
||||
platformLabel: 'Telegram',
|
||||
AccountSettings: TelegramAccessSettings,
|
||||
accountSettingsEndpoint: TELEGRAM_ENDPOINTS.setAccessPolicy,
|
||||
});
|
||||
|
||||
export const TelegramSettingsTab = channel.SettingsTab;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,21 @@ 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 { 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 { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.dtg-accessHeading strong { font-size: 13px; }
|
||||
.dtg-accessHeading p { margin: 3px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 1.5; }
|
||||
.dtg-accessBadge { flex: none; padding: 3px 8px; border-radius: 999px; color: #1687bd; background: #eaf7fd; font-size: 11px; font-weight: 700; }
|
||||
.dtg-accessBadge[data-mode="private-allowlist"] { color: #a15c00; background: #fff3d6; }
|
||||
.dtg-accessField { display: grid; gap: 5px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 600; }
|
||||
.dtg-accessField select, .dtg-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; }
|
||||
.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 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 { display: flex; justify-content: flex-end; }
|
||||
`;
|
||||
|
||||
export function installTelegramStyles() {
|
||||
|
|
|
|||
|
|
@ -281,6 +281,28 @@ const EN = Object.freeze({
|
|||
'接入 Telegram 机器人': 'Connect a Telegram bot',
|
||||
'先通过 @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',
|
||||
'群聊全部忽略,私聊仅允许白名单用户。': 'All group messages are ignored; only allowlisted users may send DMs.',
|
||||
'保持原有行为:私聊直接响应,群聊在被提及或回复时响应。': 'Keep the original behavior: respond to DMs and to group mentions or replies.',
|
||||
'安全模式': 'Safe mode',
|
||||
'兼容模式': 'Compatible mode',
|
||||
'已生效:安全模式': 'Active: Safe mode',
|
||||
'已生效:兼容模式': 'Active: Compatible mode',
|
||||
'模式': 'Mode',
|
||||
'兼容模式(默认)': 'Compatible mode (default)',
|
||||
'安全模式(私聊白名单)': 'Safe mode (private-chat allowlist)',
|
||||
'允许私聊的 Telegram User ID': 'Telegram User IDs allowed to send DMs',
|
||||
'每行一个数字 User ID': 'One numeric User ID per line',
|
||||
'白名单仅属于当前机器人。': 'This allowlist belongs only to the current bot.',
|
||||
'兼容模式下暂不使用白名单,切换模式时会保留。': 'Compatible mode does not enforce the allowlist; it is retained when modes change.',
|
||||
'白名单为空;保存后该机器人会拒绝所有入站消息。': 'The allowlist is empty; this bot will reject all inbound messages after saving.',
|
||||
'正在保存…': 'Saving…',
|
||||
'保存访问设置': 'Save access settings',
|
||||
'User ID 必须是 1–16 位正整数,每行一个。': 'Each User ID must be a 1–16 digit positive integer on its own line.',
|
||||
'Telegram 访问设置暂不可用。': 'Telegram access settings are currently unavailable.',
|
||||
'Telegram 访问设置保存失败。': 'Could not save Telegram access settings.',
|
||||
'接入 Discord 机器人': 'Connect a Discord bot',
|
||||
'先在 Developer Portal 创建 Bot 并邀请到服务器,再在这里完成接入。': 'Create a bot in the Developer Portal and invite it to your server, then connect it here.',
|
||||
'填写 Discord Developer Portal 的 Bot Token': 'Enter the Bot Token from the Discord Developer Portal',
|
||||
|
|
|
|||
|
|
@ -5,23 +5,7 @@ import { TelegramRuntime } from '../../../../src/channels/telegram/telegram-runt
|
|||
import { TelegramStateStore } from '../../../../src/channels/telegram/state-store.mjs';
|
||||
import { createTokenProductionController } from '../shared/production.mjs';
|
||||
|
||||
const TELEGRAM_USER_ID = /^[1-9]\d{0,15}$/;
|
||||
|
||||
export function normalizeTelegramAllowedUsers(value) {
|
||||
if (value === undefined) return Object.freeze([]);
|
||||
if (!Array.isArray(value)) {
|
||||
throw new TypeError('telegram.allowedUsers must be an array of numeric Telegram User IDs');
|
||||
}
|
||||
const normalized = value.map((entry) => {
|
||||
const userId = typeof entry === 'number' && Number.isSafeInteger(entry)
|
||||
? String(entry) : typeof entry === 'string' ? entry.trim() : '';
|
||||
if (!TELEGRAM_USER_ID.test(userId)) {
|
||||
throw new TypeError('telegram.allowedUsers contains an invalid Telegram User ID');
|
||||
}
|
||||
return userId;
|
||||
});
|
||||
return Object.freeze([...new Set(normalized)]);
|
||||
}
|
||||
export { normalizeTelegramAllowedUsers } from '../../../../src/channels/telegram/config-store.mjs';
|
||||
|
||||
export function createProductionController(ctx, config = {}, internals = {}) {
|
||||
return createTokenProductionController(ctx, config, internals, {
|
||||
|
|
@ -31,8 +15,5 @@ export function createProductionController(ctx, config = {}, internals = {}) {
|
|||
HarnessClient: TelegramHarnessClient,
|
||||
Controller: TelegramController,
|
||||
Runtime: TelegramRuntime,
|
||||
runtimeOptions: (runtimeConfig) => ({
|
||||
allowedPrivateUserIds: normalizeTelegramAllowedUsers(runtimeConfig.allowedUsers),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,71 @@
|
|||
import {
|
||||
TOKEN_BOT_ENDPOINTS,
|
||||
createTokenBotRpcHandler,
|
||||
installTokenBotRpc,
|
||||
} 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 = TOKEN_BOT_ENDPOINTS;
|
||||
export const TELEGRAM_ENDPOINTS = Object.freeze({
|
||||
...TOKEN_BOT_ENDPOINTS,
|
||||
setAccessPolicy: 'bot.access-policy.set',
|
||||
});
|
||||
export const TELEGRAM_RPC_ENDPOINTS = Object.freeze(Object.values(TELEGRAM_ENDPOINTS));
|
||||
|
||||
export function createTelegramRpcHandler(controller) {
|
||||
return createTokenBotRpcHandler(controller, { channel: 'Telegram' });
|
||||
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 操作失败,请稍后重试。' },
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function installTelegramRpc(ctx, controller, authority) {
|
||||
return installTokenBotRpc(ctx, controller, {
|
||||
channel: 'Telegram',
|
||||
rpcChannel: TELEGRAM_RPC_CHANNEL,
|
||||
authority,
|
||||
});
|
||||
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
|
||||
throw new TypeError('DSH Host Connection RPC is required');
|
||||
}
|
||||
return ctx.connection.rpc.handle(
|
||||
TELEGRAM_RPC_CHANNEL,
|
||||
createTelegramRpcHandler(controller),
|
||||
{ authority: resolveRpcAuthority(authority) },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue