mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 05:20:46 +08:00
Plugins inject conversation.session.header.actions; resolve sessionId back to the bound chat and render nickname/phone or group title. Co-authored-by: Cursor <cursoragent@cursor.com>
417 lines
16 KiB
JavaScript
417 lines
16 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 {
|
||
ChannelPeerLabel,
|
||
resolveWhatsappChannelPeer,
|
||
} from './channel-peer-label.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'),
|
||
});
|
||
|
||
const resolveChannelPeer = (sessionId, signal) =>
|
||
resolveWhatsappChannelPeer(whatsappRpcCall, sessionId, signal);
|
||
|
||
ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
|
||
name: 'conversation.session.header.actions',
|
||
id: 'im-channel-peer',
|
||
// Immediately after Agent Preset (−10); schedule/jobs follow at 10+.
|
||
order: 0,
|
||
locale: IM_LOCALE_NAMESPACE,
|
||
inject: () => ({ resolveChannelPeer }),
|
||
}, ChannelPeerLabel));
|
||
|
||
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));
|
||
}
|