mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
Give the settings panel the full right-pane width instead of a left channel list. Co-authored-by: Cursor <cursoragent@cursor.com>
401 lines
15 KiB
JavaScript
401 lines
15 KiB
JavaScript
import * as React from 'react';
|
|
import manifest from '../../package.json' with { type: 'json' };
|
|
|
|
import {
|
|
DingtalkLogoGlyph,
|
|
DiscordLogoGlyph,
|
|
FeishuLogoGlyph,
|
|
OfficeLogoGlyph,
|
|
QqLogoGlyph,
|
|
SlackLogoGlyph,
|
|
TelegramLogoGlyph,
|
|
WecomLogoGlyph,
|
|
WeixinLogoGlyph,
|
|
WhatsappLogoGlyph,
|
|
} from './channel-logos.js';
|
|
import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
|
|
import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
|
|
import { DISCORD_RPC_CHANNEL } from './channels/discord/api.js';
|
|
import { DiscordSettingsTab } from './channels/discord/index.js';
|
|
import { installDiscordStyles } from './channels/discord/styles.js';
|
|
import { FeishuSettingsTab } from './channels/feishu/index.js';
|
|
import { FEISHU_RPC_CHANNEL } from './channels/feishu/api.js';
|
|
import { installFeishuStyles } from './channels/feishu/styles.js';
|
|
import { QQ_RPC_CHANNEL } from './channels/qq/api.js';
|
|
import { QqSettingsTab } from './channels/qq/index.js';
|
|
import { installQqStyles } from './channels/qq/styles.js';
|
|
import { OFFICE_RPC_CHANNEL } from './channels/office/api.js';
|
|
import { OfficeSettingsTab } from './channels/office/index.js';
|
|
import { installOfficeStyles } from './channels/office/styles.js';
|
|
import { SLACK_RPC_CHANNEL } from './channels/slack/api.js';
|
|
import { SlackSettingsTab } from './channels/slack/index.js';
|
|
import { installSlackStyles } from './channels/slack/styles.js';
|
|
import { TELEGRAM_RPC_CHANNEL } from './channels/telegram/api.js';
|
|
import { TelegramSettingsTab } from './channels/telegram/index.js';
|
|
import { installTelegramStyles } from './channels/telegram/styles.js';
|
|
import { WECOM_RPC_CHANNEL } from './channels/wecom/api.js';
|
|
import { WecomSettingsTab } from './channels/wecom/index.js';
|
|
import { installWecomStyles } from './channels/wecom/styles.js';
|
|
import { WeixinSettingsTab } from './channels/weixin/index.js';
|
|
import { WEIXIN_RPC_CHANNEL } from './channels/weixin/api.js';
|
|
import { installWeixinStyles } from './channels/weixin/styles.js';
|
|
import { WHATSAPP_RPC_CHANNEL } from './channels/whatsapp/api.js';
|
|
import { WhatsappSettingsTab } from './channels/whatsapp/index.js';
|
|
import { installWhatsappStyles } from './channels/whatsapp/styles.js';
|
|
import { en, h, IM_LOCALE_NAMESPACE, setImTranslator, zh } from './i18n.js';
|
|
import { BotSettingsContext } from './channel-card-meta.js';
|
|
import {
|
|
DELIVERY_RPC_CHANNEL,
|
|
DeliveryTargetSettingsPage,
|
|
} from './delivery-settings.js';
|
|
import {
|
|
createLoopbackAwareRpcCalls,
|
|
replacePageLocation,
|
|
} from './loopback-recovery.js';
|
|
import { installImStyles } from './styles.js';
|
|
import { UpdatePanel, UPDATE_RPC_CHANNEL } from './update-panel.js';
|
|
import { WorkspaceDirectoryPickerContext } from './workspace-editor.js';
|
|
|
|
export const name = 'im-settings';
|
|
export const inject = ['slots', 'connection', 'locale', 'workspaces'];
|
|
export const IM_PLUGIN_VERSION = manifest.version;
|
|
|
|
function callWorkspaceDirectoryApi(ctx, method, ...args) {
|
|
// Current DSH owns directory operations on uiWorkspace; legacy Hosts keep them on workspaces.
|
|
const uiWorkspace = typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : undefined;
|
|
const service = typeof uiWorkspace?.[method] === 'function' ? uiWorkspace : ctx.workspaces;
|
|
if (typeof service?.[method] !== 'function') {
|
|
throw new Error('无法读取目录,请重试。');
|
|
}
|
|
return service[method](...args);
|
|
}
|
|
|
|
const CHANNELS = Object.freeze([
|
|
{ id: 'weixin', label: '微信' },
|
|
{ id: 'feishu', label: '飞书' },
|
|
{ id: 'dingtalk', label: '钉钉' },
|
|
{ id: 'wecom', label: '企业微信' },
|
|
{ id: 'qq', label: 'QQ' },
|
|
{ id: 'slack', label: 'Slack' },
|
|
{ id: 'telegram', label: 'Telegram' },
|
|
{ id: 'discord', label: 'Discord' },
|
|
{ id: 'whatsapp', label: 'WhatsApp' },
|
|
{ id: 'office', label: 'AI Office', note: '(实验功能)' },
|
|
]);
|
|
|
|
function WeixinLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoWeixin', 'aria-hidden': 'true' },
|
|
h(WeixinLogoGlyph));
|
|
}
|
|
|
|
function FeishuLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoFeishu', 'aria-hidden': 'true' },
|
|
h(FeishuLogoGlyph));
|
|
}
|
|
|
|
function DingtalkLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoDingtalk', 'aria-hidden': 'true' },
|
|
h(DingtalkLogoGlyph));
|
|
}
|
|
|
|
function QqLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoQq', 'aria-hidden': 'true' }, h(QqLogoGlyph));
|
|
}
|
|
|
|
function WecomLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoWecom', 'aria-hidden': 'true' }, h(WecomLogoGlyph));
|
|
}
|
|
|
|
function TelegramLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoTelegram', 'aria-hidden': 'true' },
|
|
h(TelegramLogoGlyph));
|
|
}
|
|
|
|
function SlackLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoSlack', 'aria-hidden': 'true' },
|
|
h(SlackLogoGlyph));
|
|
}
|
|
|
|
function DiscordLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoDiscord', 'aria-hidden': 'true' },
|
|
h(DiscordLogoGlyph));
|
|
}
|
|
|
|
function WhatsappLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoWhatsapp', 'aria-hidden': 'true' },
|
|
h(WhatsappLogoGlyph));
|
|
}
|
|
|
|
function OfficeLogo() {
|
|
return h('span', { className: 'dim-logo dim-logoOffice', 'aria-hidden': 'true' },
|
|
h(OfficeLogoGlyph));
|
|
}
|
|
|
|
function ChannelLogo({ channel }) {
|
|
if (channel === 'weixin') return h(WeixinLogo);
|
|
if (channel === 'feishu') return h(FeishuLogo);
|
|
if (channel === 'dingtalk') return h(DingtalkLogo);
|
|
if (channel === 'wecom') return h(WecomLogo);
|
|
if (channel === 'qq') return h(QqLogo);
|
|
if (channel === 'slack') return h(SlackLogo);
|
|
if (channel === 'telegram') return h(TelegramLogo);
|
|
if (channel === 'discord') return h(DiscordLogo);
|
|
if (channel === 'whatsapp') return h(WhatsappLogo);
|
|
return h(OfficeLogo);
|
|
}
|
|
|
|
export function LoopbackRecoveryNotice({ recovery, onNavigate = replacePageLocation }) {
|
|
return h('div', {
|
|
className: 'dim-loopbackRecovery',
|
|
role: 'alert',
|
|
},
|
|
h('div', { className: 'dim-loopbackRecoveryCopy' },
|
|
h('strong', null, '请改用 localhost 重新打开'),
|
|
h('p', null, '页面会在当前端口重新打开,机器人配置不会改变。'),
|
|
h('code', null, recovery.origin)),
|
|
h('button', {
|
|
type: 'button',
|
|
className: 'dim-loopbackRecoveryAction',
|
|
onClick: () => onNavigate(recovery.url),
|
|
}, '使用 localhost 重新打开'));
|
|
}
|
|
|
|
export function IMSettingsTab({
|
|
dingtalkRpcCall,
|
|
discordRpcCall,
|
|
feishuRpcCall,
|
|
qqRpcCall,
|
|
slackRpcCall,
|
|
telegramRpcCall,
|
|
wecomRpcCall,
|
|
weixinRpcCall,
|
|
whatsappRpcCall,
|
|
officeRpcCall,
|
|
updateRpcCall,
|
|
deliveryRpcCall,
|
|
workspaceDirectoryPicker,
|
|
browserLocation = globalThis.location,
|
|
navigateToRecoveryUrl = replacePageLocation,
|
|
}) {
|
|
const [selected, setSelected] = React.useState('weixin');
|
|
const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
|
|
const [runningVersion, setRunningVersion] = React.useState(IM_PLUGIN_VERSION);
|
|
const [deliverySettings, setDeliverySettings] = React.useState(null);
|
|
const githubTooltipId = React.useId();
|
|
const channelSelectId = React.useId();
|
|
const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
|
|
const reportLoopbackRecovery = React.useCallback((recovery) => {
|
|
setLoopbackRecovery((current) => current?.url === recovery.url ? current : recovery);
|
|
}, []);
|
|
const reportUpdateStatus = React.useCallback((snapshot) => {
|
|
setRunningVersion(snapshot.runningVersion);
|
|
}, []);
|
|
const rpcCalls = React.useMemo(() => createLoopbackAwareRpcCalls({
|
|
dingtalkRpcCall,
|
|
discordRpcCall,
|
|
feishuRpcCall,
|
|
qqRpcCall,
|
|
slackRpcCall,
|
|
telegramRpcCall,
|
|
wecomRpcCall,
|
|
weixinRpcCall,
|
|
whatsappRpcCall,
|
|
officeRpcCall,
|
|
updateRpcCall,
|
|
deliveryRpcCall,
|
|
}, {
|
|
location: browserLocation,
|
|
onRecovery: reportLoopbackRecovery,
|
|
}), [
|
|
browserLocation,
|
|
dingtalkRpcCall,
|
|
discordRpcCall,
|
|
deliveryRpcCall,
|
|
feishuRpcCall,
|
|
officeRpcCall,
|
|
qqRpcCall,
|
|
reportLoopbackRecovery,
|
|
slackRpcCall,
|
|
telegramRpcCall,
|
|
updateRpcCall,
|
|
wecomRpcCall,
|
|
weixinRpcCall,
|
|
whatsappRpcCall,
|
|
]);
|
|
const botSettingsContext = React.useMemo(() => Object.freeze({
|
|
openBotSettings: setDeliverySettings,
|
|
}), []);
|
|
return h(WorkspaceDirectoryPickerContext.Provider, { value: workspaceDirectoryPicker },
|
|
h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
|
|
h('header', { className: 'dim-title' },
|
|
h('div', { className: 'dim-brand' },
|
|
h('div', { className: 'dim-brandHeading' },
|
|
h('strong', { className: 'dim-brandName' }, 'DSH-IM'),
|
|
h('span', { className: 'dim-brandVersion' }, `v${runningVersion}`)),
|
|
h('p', null, '让 DeepSeek Harness 触手可及')),
|
|
h('div', { className: 'dim-titleActions' },
|
|
h(UpdatePanel, {
|
|
rpcCall: rpcCalls.updateRpcCall,
|
|
clientVersion: IM_PLUGIN_VERSION,
|
|
onStatus: reportUpdateStatus,
|
|
}),
|
|
h('span', { className: 'dim-githubAction' },
|
|
h('a', {
|
|
className: 'dim-githubLink',
|
|
href: 'https://github.com/xmanrui/dsh-im',
|
|
target: '_blank',
|
|
rel: 'noopener noreferrer',
|
|
'aria-label': 'dsh-im GitHub',
|
|
'aria-describedby': githubTooltipId,
|
|
},
|
|
h('span', null, 'GitHub'),
|
|
h('span', { className: 'dim-githubArrow', 'aria-hidden': 'true' }, '↗')),
|
|
h('span', {
|
|
id: githubTooltipId,
|
|
className: 'dim-githubTooltip',
|
|
role: 'tooltip',
|
|
}, '帮助与反馈 · 前往 GitHub'))),
|
|
),
|
|
h('div', { className: 'dim-layout' },
|
|
h('div', { className: 'dim-channelPicker' },
|
|
h('label', {
|
|
className: 'dim-channelPickerLabel',
|
|
htmlFor: channelSelectId,
|
|
}, 'IM 渠道'),
|
|
h('div', { className: 'dim-channelPickerControl' },
|
|
h(ChannelLogo, { channel: active.id }),
|
|
h('select', {
|
|
id: channelSelectId,
|
|
className: 'dim-channelPickerSelect',
|
|
value: active.id,
|
|
'aria-label': 'IM 渠道',
|
|
onChange: (event) => {
|
|
setSelected(event.target.value);
|
|
setDeliverySettings(null);
|
|
},
|
|
}, CHANNELS.map((channel) => h('option', {
|
|
key: channel.id,
|
|
value: channel.id,
|
|
}, channel.label, channel.note || null))))),
|
|
h('main', {
|
|
className: 'dim-panel',
|
|
id: `dim-panel-${active.id}`,
|
|
'aria-label': active.label,
|
|
},
|
|
loopbackRecovery
|
|
? h(LoopbackRecoveryNotice, {
|
|
recovery: loopbackRecovery,
|
|
onNavigate: navigateToRecoveryUrl,
|
|
})
|
|
: null,
|
|
h(BotSettingsContext.Provider, { value: botSettingsContext },
|
|
deliverySettings?.channel === active.id
|
|
? h(DeliveryTargetSettingsPage, {
|
|
channel: active.id,
|
|
account: deliverySettings,
|
|
rpcCall: rpcCalls.deliveryRpcCall,
|
|
accessRpcCall: rpcCalls[`${active.id}RpcCall`],
|
|
onBack: () => setDeliverySettings(null),
|
|
})
|
|
: active.id === 'weixin'
|
|
? h(WeixinSettingsTab, { rpcCall: rpcCalls.weixinRpcCall })
|
|
: active.id === 'feishu'
|
|
? h(FeishuSettingsTab, { rpcCall: rpcCalls.feishuRpcCall })
|
|
: active.id === 'dingtalk'
|
|
? h(DingtalkSettingsTab, { rpcCall: rpcCalls.dingtalkRpcCall })
|
|
: active.id === 'wecom'
|
|
? h(WecomSettingsTab, { rpcCall: rpcCalls.wecomRpcCall })
|
|
: active.id === 'qq'
|
|
? h(QqSettingsTab, { rpcCall: rpcCalls.qqRpcCall })
|
|
: active.id === 'slack'
|
|
? h(SlackSettingsTab, { rpcCall: rpcCalls.slackRpcCall })
|
|
: active.id === 'telegram'
|
|
? h(TelegramSettingsTab, { rpcCall: rpcCalls.telegramRpcCall })
|
|
: active.id === 'discord'
|
|
? h(DiscordSettingsTab, { rpcCall: rpcCalls.discordRpcCall })
|
|
: active.id === 'whatsapp'
|
|
? h(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall })
|
|
: h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
|
|
),
|
|
));
|
|
}
|
|
|
|
export function apply(ctx) {
|
|
ctx.effect(
|
|
() => ctx.locale.register(IM_LOCALE_NAMESPACE, { zh, en }),
|
|
'im-settings: bilingual dictionaries',
|
|
);
|
|
const t = ctx.locale.bind(IM_LOCALE_NAMESPACE);
|
|
setImTranslator(t);
|
|
|
|
ctx.effect(() => {
|
|
const disposers = [
|
|
installFeishuStyles(),
|
|
installWeixinStyles(),
|
|
installWecomStyles(),
|
|
installQqStyles(),
|
|
installSlackStyles(),
|
|
installTelegramStyles(),
|
|
installDiscordStyles(),
|
|
installWhatsappStyles(),
|
|
installOfficeStyles(),
|
|
installImStyles(),
|
|
];
|
|
return () => {
|
|
for (const dispose of disposers.reverse()) dispose();
|
|
};
|
|
}, 'im-settings: install combined channel styles');
|
|
|
|
const feishuRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal);
|
|
const weixinRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
|
|
const dingtalkRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
|
|
const qqRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(QQ_RPC_CHANNEL, endpoint, payload, signal);
|
|
const wecomRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(WECOM_RPC_CHANNEL, endpoint, payload, signal);
|
|
const telegramRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
|
|
const discordRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(DISCORD_RPC_CHANNEL, endpoint, payload, signal);
|
|
const whatsappRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
|
|
const slackRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(SLACK_RPC_CHANNEL, endpoint, payload, signal);
|
|
const officeRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(OFFICE_RPC_CHANNEL, endpoint, payload, signal);
|
|
const updateRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(UPDATE_RPC_CHANNEL, endpoint, payload, signal);
|
|
const deliveryRpcCall = (endpoint, payload, signal) =>
|
|
ctx.connection.rpc.call(DELIVERY_RPC_CHANNEL, endpoint, payload, signal);
|
|
const workspaceDirectoryPicker = Object.freeze({
|
|
listDirectory: (path, signal) =>
|
|
callWorkspaceDirectoryApi(ctx, 'listDirectory', path, signal),
|
|
pickDirectory: () => callWorkspaceDirectoryApi(ctx, 'pickDirectory'),
|
|
});
|
|
|
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
name: 'settings.section',
|
|
id: 'xmanrui-dsh-im',
|
|
order: 21,
|
|
label: () => t('IM机器人'),
|
|
locale: IM_LOCALE_NAMESPACE,
|
|
inject: () => ({
|
|
dingtalkRpcCall,
|
|
discordRpcCall,
|
|
feishuRpcCall,
|
|
qqRpcCall,
|
|
slackRpcCall,
|
|
telegramRpcCall,
|
|
wecomRpcCall,
|
|
weixinRpcCall,
|
|
whatsappRpcCall,
|
|
officeRpcCall,
|
|
updateRpcCall,
|
|
deliveryRpcCall,
|
|
workspaceDirectoryPicker,
|
|
}),
|
|
}, IMSettingsTab));
|
|
}
|