import * as React from 'react'; import { AccessPolicySettingsPage } from './access-policy-settings.js'; import { AccessGrantSettingsPage } from './access-grant-settings.js'; import { h, isEnglish, localizeText } from './i18n.js'; export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery'; const DELIVERY_DOCS_URL = Object.freeze({ zh: 'https://github.com/xmanrui/dsh-im/blob/main/PROACTIVE_DELIVERY.md', en: 'https://github.com/xmanrui/dsh-im/blob/main/PROACTIVE_DELIVERY.en.md', }); export const DELIVERY_ENDPOINTS = Object.freeze({ list: 'target.list', listSuggestions: 'target.suggestion.list', create: 'target.create', update: 'target.update', delete: 'target.delete', test: 'target.test', }); export const BOT_SETTINGS_TABS = Object.freeze([ Object.freeze({ id: 'delivery', label: '投递设置' }), Object.freeze({ id: 'access', label: '访问设置' }), ]); const CHANNEL_DEFINITIONS = Object.freeze({ weixin: { label: '微信', kinds: [{ value: 'user', label: '用户' }], fields: { user: [{ key: 'toUserId', label: '微信用户 ID', placeholder: '填写接收消息的用户 ID' }] }, }, feishu: { label: '飞书', kinds: [{ value: 'user', label: '私聊' }, { value: 'group', label: '群聊' }], fields: { user: [{ key: 'openId', label: 'Open ID', placeholder: 'ou_xxx' }], group: [{ key: 'chatId', label: '群 Chat ID', placeholder: 'oc_xxx' }], }, }, dingtalk: { label: '钉钉', kinds: [{ value: 'user', label: '用户' }, { value: 'group', label: '群聊' }], fields: { user: [{ key: 'userId', label: '用户 ID', placeholder: '填写钉钉用户 ID' }], group: [{ key: 'openConversationId', label: '群 Open Conversation ID', placeholder: 'cidxxx' }], }, }, wecom: { label: '企业微信', kinds: [{ value: 'user', label: '私聊' }, { value: 'group', label: '群聊' }], fields: { user: [{ key: 'chatId', label: '用户 ID', placeholder: '填写企业微信用户 ID' }], group: [{ key: 'chatId', label: '群 Chat ID', placeholder: '填写群 chatid' }], }, }, qq: { label: 'QQ', kinds: [{ value: 'user', label: '单聊' }, { value: 'group', label: '群聊' }], fields: { user: [{ key: 'userOpenId', label: '用户 Open ID', placeholder: '填写 user_openid' }], group: [{ key: 'groupOpenId', label: '群 Open ID', placeholder: '填写 group_openid' }], }, }, slack: { label: 'Slack', kinds: [{ value: 'conversation', label: '会话' }, { value: 'thread', label: '线程' }], fields: { conversation: [{ key: 'channelId', label: 'Channel ID', placeholder: 'C0123456789' }], thread: [ { key: 'channelId', label: 'Channel ID', placeholder: 'C0123456789' }, { key: 'threadTs', label: 'Thread 时间戳', placeholder: '1712345678.123456' }, ], }, }, telegram: { label: 'Telegram', kinds: [{ value: 'chat', label: '聊天' }, { value: 'topic', label: '话题' }], fields: { chat: [{ key: 'chatId', label: 'Chat ID', placeholder: '-1001234567890', inputMode: 'numeric' }], topic: [ { key: 'chatId', label: 'Chat ID', placeholder: '-1001234567890', inputMode: 'numeric' }, { key: 'messageThreadId', label: 'Topic ID', placeholder: '123', inputMode: 'numeric', integer: true }, ], }, }, discord: { label: 'Discord', kinds: [{ value: 'channel', label: '频道或私信' }], fields: { channel: [{ key: 'channelId', label: 'Channel ID', placeholder: '填写可发消息的 Channel ID', inputMode: 'numeric' }] }, }, whatsapp: { label: 'WhatsApp', kinds: [{ value: 'user', label: '用户' }, { value: 'group', label: '群聊' }], fields: { user: [{ key: 'jid', label: '用户 JID', placeholder: '8613800000000@s.whatsapp.net' }], group: [{ key: 'jid', label: '群 JID', placeholder: '1234567890@g.us' }], }, }, }); export const DELIVERY_CHANNEL_DEFINITIONS = CHANNEL_DEFINITIONS; function presentError(error, fallback) { return error?.message || fallback; } 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 targetsFrom(value) { if (Array.isArray(value)) return value; return Array.isArray(value?.targets) ? value.targets : []; } function suggestionsFrom(value) { if (Array.isArray(value)) return value; return Array.isArray(value?.suggestions) ? value.suggestions : []; } function kindLabel(definition, kind) { return definition.kinds.find((entry) => entry.value === kind)?.label ?? kind; } function fieldsFor(definition, kind) { return definition.fields[kind] ?? []; } function cleanDisplayName(value) { return typeof value === 'string' && value.trim() ? value.trim().slice(0, 80) : null; } function suggestionDisplayName(suggestion) { return cleanDisplayName( suggestion?.name ?? suggestion?.displayName ?? suggestion?.label, ); } function routeIdentity(definition, candidate) { const fields = fieldsFor(definition, candidate?.kind); if (fields.length === 0 || !candidate?.route || typeof candidate.route !== 'object') return null; const values = []; for (const field of fields) { const value = candidate.route[field.key]; if (value === undefined || value === null || String(value).trim() === '') return null; values.push(String(value).trim()); } return JSON.stringify([candidate.kind, ...values]); } function validSuggestions(definition, value) { const seen = new Set(); return suggestionsFrom(value).filter((suggestion) => { const identity = routeIdentity(definition, suggestion); if (!identity || seen.has(identity)) return false; seen.add(identity); return true; }); } function maskRouteValue(value) { const text = String(value ?? '').trim(); if (!text) return ''; const at = text.indexOf('@'); if (at > 0) { const local = text.slice(0, at); const domain = text.slice(at); const visible = local.length <= 4 ? `${local.slice(0, 1)}••${local.slice(-1)}` : `${local.slice(0, 3)}…${local.slice(-2)}`; return `${visible}${domain}`; } if (text.length <= 6) return `${text.slice(0, 2)}…${text.slice(-1)}`; if (text.length <= 12) return `${text.slice(0, 3)}…${text.slice(-3)}`; return `${text.slice(0, 5)}…${text.slice(-4)}`; } function suggestionFallbackName(definition, suggestion) { const displayName = suggestionDisplayName(suggestion); if (displayName) return displayName; const firstField = fieldsFor(definition, suggestion.kind)[0]; const route = firstField ? maskRouteValue(suggestion.route?.[firstField.key]) : ''; return `${localizeText(kindLabel(definition, suggestion.kind))}${route ? ` · ${route}` : ''}`; } function randomTargetId(targets) { const used = new Set(targets.map((target) => target?.targetId)); for (let attempt = 0; attempt < 32; attempt += 1) { const bytes = new Uint8Array(8); const crypto = globalThis.crypto; if (typeof crypto?.getRandomValues === 'function') { crypto.getRandomValues(bytes); } else { for (let index = 0; index < bytes.length; index += 1) { bytes[index] = Math.floor(Math.random() * 256); } } const suffix = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')).join(''); const targetId = `tgt_${suffix}`; if (!used.has(targetId)) return targetId; } throw new Error('无法生成未占用的 Target ID,请重试。'); } function DeliveryButton({ children, kind = 'secondary', className = '', ...props }) { return h('button', { ...props, type: props.type ?? 'button', className: `dim-deliveryButton ${className}`.trim(), 'data-kind': kind, }, children); } async function copyText(value) { const clipboard = globalThis.navigator?.clipboard; if (typeof clipboard?.writeText !== 'function') { throw new Error('当前浏览器不支持自动复制,请手动选择复制。'); } await clipboard.writeText(value); } function TargetForm({ definition, mode, initialValue, source, busy, connected, onCancel, onSave, onTest, }) { const editing = mode === 'edit'; const initialKind = initialValue?.kind && definition.fields[initialValue.kind] ? initialValue.kind : definition.kinds[0].value; const [targetId, setTargetId] = React.useState(initialValue?.targetId ?? ''); const [name, setName] = React.useState(initialValue?.name ?? ''); const [kind, setKind] = React.useState(initialKind); const [route, setRoute] = React.useState(initialValue?.route ?? {}); const [error, setError] = React.useState(null); const [testing, setTesting] = React.useState(false); const [testState, setTestState] = React.useState(null); const currentTarget = () => { const normalizedRoute = Object.fromEntries(fieldsFor(definition, kind).map((field) => { const raw = String(route[field.key] ?? '').trim(); return [field.key, field.integer ? Number(raw) : raw]; })); return { targetId: targetId.trim(), ...(name.trim() ? { name: name.trim() } : {}), kind, route: normalizedRoute, }; }; const submit = async (event) => { event.preventDefault(); setError(null); try { await onSave({ ...currentTarget(), }); } catch (caught) { setError(presentError(caught, '目标保存失败,请检查后重试。')); } }; const testTarget = async () => { if (typeof onTest !== 'function') return; setTesting(true); setTestState(null); try { const target = currentTarget(); await onTest({ kind: target.kind, route: target.route }); setTestState({ tone: 'success', message: '测试消息已发送,请到目标会话确认。' }); } catch (caught) { setTestState({ tone: 'error', message: presentError(caught, '测试发送失败,请稍后重试。') }); } finally { setTesting(false); } }; const testReady = fieldsFor(definition, kind).every((field) => ( String(route[field.key] ?? '').trim() )); return h('form', { className: 'dim-targetForm', onSubmit: submit }, h('div', { className: 'dim-targetFormHeading' }, h('h3', null, editing ? '编辑投递目标' : '新建投递目标'), editing ? null : h('p', null, source === 'suggestion' ? '已从会话自动填入目标信息;确认后再保存。' : '请手动填写从对应平台取得的原生标识。')), h('div', { className: 'dim-targetFormGrid' }, h('label', { className: 'dim-targetField' }, h('span', null, 'Target ID'), h('input', { name: 'targetId', value: targetId, readOnly: editing, disabled: testing, required: true, pattern: '[A-Za-z0-9._:@-]{1,128}', maxLength: 128, autoCapitalize: 'none', autoCorrect: 'off', spellCheck: false, placeholder: '例如 daily-report', onChange: (event) => setTargetId(event.target.value), })), h('label', { className: 'dim-targetField' }, h('span', null, '显示名称(可选)'), h('input', { name: 'name', value: name, disabled: testing, maxLength: 80, placeholder: definition.label === 'WhatsApp' ? '例如 张三 · 13800138000' : '例如 每日汇报群', onChange: (event) => setName(event.target.value), })), h('label', { className: 'dim-targetField' }, h('span', null, '目标类型'), h('select', { name: 'kind', value: kind, disabled: testing, onChange: (event) => { setKind(event.target.value); setRoute({}); setTestState(null); }, }, definition.kinds.map((entry) => h('option', { key: entry.value, value: entry.value, }, entry.label)))), fieldsFor(definition, kind).map((field) => h('label', { key: field.key, className: 'dim-targetField', }, h('span', null, field.label), h('input', { name: field.key, value: route[field.key] ?? '', disabled: testing, required: true, inputMode: field.inputMode, autoCapitalize: 'none', autoCorrect: 'off', spellCheck: false, placeholder: field.placeholder, onChange: (event) => { setRoute((current) => ({ ...current, [field.key]: event.target.value, })); setTestState(null); }, })))), error ? h('p', { className: 'dim-targetFormError', role: 'alert' }, error) : null, testState ? h('p', { className: 'dim-targetFeedback', 'data-tone': testState.tone, role: 'status', 'aria-live': 'polite', }, testState.message) : null, h('div', { className: 'dim-targetFormActions' }, h(DeliveryButton, { onClick: onCancel, disabled: busy || testing }, '取消'), h(DeliveryButton, { onClick: () => void testTarget(), disabled: busy || testing || !connected || !testReady || typeof onTest !== 'function', title: connected ? undefined : '机器人离线时不可发送测试消息', 'aria-label': '测试投递目标', }, testing ? '测试中…' : '测试'), h(DeliveryButton, { type: 'submit', kind: 'primary', disabled: busy || testing }, busy ? '正在保存…' : '保存目标'))); } function suggestionOptionLabel(definition, suggestion, added) { const name = suggestionDisplayName(suggestion) || suggestionFallbackName(definition, suggestion); const type = localizeText(kindLabel(definition, suggestion.kind)); const firstField = fieldsFor(definition, suggestion.kind)[0]; const route = firstField ? String(suggestion.route?.[firstField.key] ?? '').trim() : ''; const showRoute = route && !name.includes(route) && !name.includes(route.split('@')[0] || ''); return [name, type, showRoute ? maskRouteValue(route) : null, added ? localizeText('已添加') : null] .filter(Boolean) .join(' · '); } function TargetSuggestionPicker({ definition, phase, suggestions, error, targets, onRefresh, onSelect, onManual, onCancel, }) { const configured = new Set(targets.map((target) => routeIdentity(definition, target)).filter(Boolean)); const channelHint = definition.label === 'WhatsApp' ? '会列出已聊过的私聊/群聊,以及访问名单里记住的联系人与群(显示为昵称 · 电话 / 群名)。' : '选择后会自动填写目标信息和调用别名,确认后再保存。'; return h('section', { className: 'dim-targetSuggestions', 'aria-label': '从已聊过的会话选择' }, h('div', { className: 'dim-targetSuggestionHeading' }, h('div', null, h('h3', null, '从已聊过的会话选择'), h('p', null, channelHint)), h(DeliveryButton, { onClick: () => void onRefresh(), disabled: phase === 'loading', }, phase === 'loading' ? '正在刷新…' : '刷新')), phase === 'loading' ? h('div', { className: 'dim-targetSuggestionState', 'aria-busy': 'true' }, '正在读取已聊会话…') : phase === 'error' ? h('div', { className: 'dim-targetSuggestionState', role: 'alert' }, h('p', null, error), h(DeliveryButton, { onClick: () => void onRefresh() }, '重新读取')) : suggestions.length === 0 ? h('div', { className: 'dim-targetSuggestionState' }, h('strong', null, '还没有可选择的会话'), h('p', null, '先在对应平台与机器人聊一条消息(私聊或群聊),或先在「访问设置」里出现过联系人/群,再刷新。')) : h('ul', { className: 'dim-targetSuggestionList', role: 'listbox', 'aria-label': '可选投递会话' }, suggestions.map((suggestion, index) => { const identity = routeIdentity(definition, suggestion); const added = configured.has(identity); return h('li', { key: suggestion.id ?? suggestion.suggestionId ?? `${identity}:${index}` }, h('button', { type: 'button', className: 'dim-targetSuggestionItem', role: 'option', 'aria-disabled': added ? 'true' : undefined, disabled: added, onClick: () => { if (!added) onSelect(suggestion); }, }, // Platform display names must stay raw (bypass h() i18n phrase rewrite). React.createElement('strong', null, suggestionDisplayName(suggestion) || suggestionFallbackName(definition, suggestion)), h('span', null, [ localizeText(kindLabel(definition, suggestion.kind)), added ? localizeText('已添加') : null, ].filter(Boolean).join(' · ')), React.createElement('code', null, fieldsFor(definition, suggestion.kind) .map((field) => maskRouteValue(suggestion.route?.[field.key])) .filter(Boolean) .join(' · ')))); })), h('div', { className: 'dim-targetFormActions' }, h(DeliveryButton, { onClick: onCancel }, '取消'), h(DeliveryButton, { onClick: onManual }, '手动填写(高级)'))); } function TargetRow({ definition, target, botId, connected, rpcCall, onChanged, onEdit }) { const [action, setAction] = React.useState(null); const [testState, setTestState] = React.useState(null); const [copyState, setCopyState] = React.useState(null); const [confirmDelete, setConfirmDelete] = React.useState(false); const testTarget = async () => { setAction('test'); setTestState(null); try { await rpcCall(DELIVERY_ENDPOINTS.test, { botId, targetId: target.targetId }); setTestState({ tone: 'success', message: '测试消息已发送,请到目标会话确认。' }); } catch (error) { setTestState({ tone: 'error', message: presentError(error, '测试发送失败,请稍后重试。') }); } finally { setAction(null); } }; const copyPair = async () => { setCopyState(null); try { await copyText(JSON.stringify({ botId, targetId: target.targetId })); setCopyState('已复制调用参数'); } catch (error) { setCopyState(presentError(error, '复制失败。')); } }; const deleteTarget = async () => { setAction('delete'); try { await rpcCall(DELIVERY_ENDPOINTS.delete, { botId, targetId: target.targetId }); await onChanged(); } catch (error) { setTestState({ tone: 'error', message: presentError(error, '删除失败,请稍后重试。') }); setAction(null); setConfirmDelete(false); } }; return h('li', { className: 'dim-targetRow', 'data-target-id': target.targetId }, h('div', { className: 'dim-targetSummary' }, h('div', { className: 'dim-targetTitle' }, React.createElement('strong', null, target.name || target.targetId), h('span', null, kindLabel(definition, target.kind))), h('code', null, `targetId: ${target.targetId}`)), h('div', { className: 'dim-targetActions' }, h(DeliveryButton, { onClick: () => void copyPair() }, '复制调用参数'), h(DeliveryButton, { onClick: () => void testTarget(), disabled: !connected || action === 'test', title: connected ? undefined : '机器人离线时不可发送测试消息', 'aria-label': '测试投递目标', }, action === 'test' ? '测试中…' : '测试'), h(DeliveryButton, { onClick: onEdit, disabled: Boolean(action) }, '编辑'), h(DeliveryButton, { kind: 'danger', onClick: () => setConfirmDelete(true), disabled: Boolean(action), }, '删除')), copyState ? h('p', { className: 'dim-targetFeedback', role: 'status' }, copyState) : null, testState ? h('p', { className: 'dim-targetFeedback', 'data-tone': testState.tone, role: 'status', 'aria-live': 'polite', }, testState.message) : null, confirmDelete ? h('div', { className: 'dim-targetDeleteConfirm', role: 'alertdialog' }, h('p', null, '删除 ', h('code', null, target.targetId), '?使用这个 targetId 的外部调用将返回 unknown-target。'), h('div', { className: 'dim-targetFormActions' }, h(DeliveryButton, { onClick: () => setConfirmDelete(false), disabled: action === 'delete', }, '取消'), h(DeliveryButton, { kind: 'danger', onClick: () => void deleteTarget(), disabled: action === 'delete', }, action === 'delete' ? '正在删除…' : '确认删除'))) : null); } 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'); const [suggestions, setSuggestions] = React.useState([]); const [suggestionError, setSuggestionError] = React.useState(null); const [error, setError] = React.useState(null); 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 [accessGrant, setAccessGrant] = React.useState(account.accessGrant); const mounted = React.useRef(true); React.useEffect(() => { setAccessPolicy(account.accessPolicy); setAccessGrant(account.accessGrant); }, [account.botId, account.accessPolicy, account.accessGrant]); const invoke = React.useCallback(async (endpoint, payload = {}, signal) => { if (typeof rpcCall !== 'function') throw new Error('投递目标设置暂不可用。'); return unwrapRpcResult(await rpcCall(endpoint, payload, signal)); }, [rpcCall]); const loadTargets = React.useCallback(async ({ signal, silent = false } = {}) => { if (!silent) setPhase('loading'); setError(null); try { const value = await invoke(DELIVERY_ENDPOINTS.list, { botId: account.botId }, signal); if (signal?.aborted || !mounted.current) return; setTargets(targetsFrom(value)); setPhase('ready'); } catch (caught) { if (signal?.aborted || caught?.name === 'AbortError' || !mounted.current) return; setError(presentError(caught, '无法读取投递目标,请稍后重试。')); setPhase('error'); } }, [account.botId, invoke]); const loadSuggestions = React.useCallback(async () => { setSuggestionPhase('loading'); setSuggestionError(null); try { const value = await invoke(DELIVERY_ENDPOINTS.listSuggestions, { botId: account.botId }); if (!mounted.current) return; setSuggestions(validSuggestions(definition, value)); setSuggestionPhase('ready'); } catch (caught) { if (caught?.name === 'AbortError' || !mounted.current) return; setSuggestionError(presentError(caught, '无法读取已聊会话,请稍后重试。')); setSuggestionPhase('error'); } }, [account.botId, definition, invoke]); React.useEffect(() => { mounted.current = true; const controller = new AbortController(); void loadTargets({ signal: controller.signal }); return () => { mounted.current = false; controller.abort(); }; }, [loadTargets]); if (!definition) { return h('section', { className: 'dim-deliveryPage' }, h(DeliveryButton, { className: 'dim-deliveryBack', onClick: onBack }, '← 返回机器人列表'), h('p', { role: 'alert' }, '当前渠道暂不支持投递目标。')); } const saveTarget = async (target) => { setSaving(true); try { if (editor?.mode === 'edit') { await invoke(DELIVERY_ENDPOINTS.update, { botId: account.botId, targetId: editor.target.targetId, target: { ...(target.name ? { name: target.name } : {}), kind: target.kind, route: target.route, }, }); } else { await invoke(DELIVERY_ENDPOINTS.create, { botId: account.botId, target }); } await loadTargets({ silent: true }); if (mounted.current) setEditor(null); } finally { if (mounted.current) setSaving(false); } }; const openSuggestionPicker = () => { setEditor({ mode: 'suggestions' }); void loadSuggestions(); }; const selectSuggestion = (suggestion) => { const identity = routeIdentity(definition, suggestion); if (!identity) return; const route = Object.fromEntries(fieldsFor(definition, suggestion.kind).map((field) => [ field.key, suggestion.route[field.key], ])); setEditor({ mode: 'create', source: 'suggestion', draftKey: identity, initialValue: { targetId: randomTargetId(targets), name: suggestionFallbackName(definition, suggestion), kind: suggestion.kind, route, }, }); }; const copyBotId = async () => { setBotCopyState(null); try { await copyText(account.botId); setBotCopyState('已复制 Bot ID'); } catch (caught) { setBotCopyState(presentError(caught, '复制失败。')); } }; 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', 'aria-label': `${account.botName || definition.label}机器人设置`, }, h('header', { className: 'dim-deliveryHeader' }, h(DeliveryButton, { className: 'dim-deliveryBack', onClick: onBack }, '← 返回机器人列表')), h('div', { className: 'dim-botSettingsTabsBar' }, h('nav', { className: 'dim-botSettingsTabs', role: 'tablist', 'aria-label': '机器人设置页签', }, BOT_SETTINGS_TABS.map((tab) => h('button', { key: tab.id, id: `dim-bot-settings-${tab.id}-tab`, type: 'button', role: 'tab', className: 'dim-botSettingsTab', 'aria-selected': tab.id === activeTab.id, 'aria-controls': `dim-bot-settings-${tab.id}-panel`, tabIndex: tab.id === activeTab.id ? 0 : -1, onClick: () => setActiveTabId(tab.id), }, tab.label)))), h('div', { id: activePanelId, className: 'dim-botSettingsTabPanel', role: 'tabpanel', 'aria-labelledby': activeTabDomId, }, activeTab.id === 'access' ? (channel === 'whatsapp' ? h(AccessGrantSettingsPage, { channel, account: { ...account, accessGrant, groupSessionScope: account.groupSessionScope, }, rpcCall: accessRpcCall, onSaved: setAccessGrant, }) : 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' }, account.botName || '机器人设置'), h('a', { className: 'dim-deliveryDocsLink', href: isEnglish() ? DELIVERY_DOCS_URL.en : DELIVERY_DOCS_URL.zh, target: '_blank', rel: 'noopener noreferrer', 'aria-label': '打开主动投递使用文档', }, h('span', null, '使用文档'), h('span', { 'aria-hidden': 'true' }, '↗'))), h('div', { className: 'dim-deliveryBotId' }, h('span', null, 'Bot ID'), h('code', { title: account.botId }, account.botId), h(DeliveryButton, { onClick: () => void copyBotId() }, '复制')), botCopyState ? h('p', { className: 'dim-targetFeedback', role: 'status' }, botCopyState) : null), h('section', { className: 'dim-deliveryTargets', 'aria-labelledby': 'dim-delivery-targets-title' }, h('div', { className: 'dim-deliverySectionHeading' }, h('div', null, h('h3', { id: 'dim-delivery-targets-title' }, '投递目标'), account.connected ? null : h('p', null, '机器人当前离线;仍可配置目标,恢复连接后再测试。')), h(DeliveryButton, { kind: 'primary', onClick: openSuggestionPicker, disabled: Boolean(editor) || phase !== 'ready', title: phase === 'ready' ? undefined : '请先完成投递目标读取', }, '新建目标')), editor?.mode === 'suggestions' ? h(TargetSuggestionPicker, { definition, phase: suggestionPhase, suggestions, error: suggestionError, targets, onRefresh: loadSuggestions, onSelect: selectSuggestion, onManual: () => setEditor({ mode: 'create', source: 'manual', draftKey: 'manual', initialValue: { targetId: randomTargetId(targets) }, }), onCancel: () => setEditor(null), }) : editor ? h(TargetForm, { key: editor.mode === 'edit' ? editor.target.targetId : editor.draftKey, definition, mode: editor.mode, initialValue: editor.mode === 'edit' ? editor.target : editor.initialValue, source: editor.source, busy: saving, connected: account.connected, onCancel: () => setEditor(editor.source === 'suggestion' ? { mode: 'suggestions' } : null), onSave: saveTarget, onTest: (target) => invoke(DELIVERY_ENDPOINTS.test, { botId: account.botId, target, }), }) : null, phase === 'loading' ? h('div', { className: 'dim-deliveryState', 'aria-busy': 'true' }, '正在读取投递目标…') : phase === 'error' ? h('div', { className: 'dim-deliveryState', role: 'alert' }, h('p', null, error), h(DeliveryButton, { onClick: () => void loadTargets() }, '重新读取')) : targets.length === 0 ? h('div', { className: 'dim-deliveryState dim-deliveryEmpty' }, h('strong', null, '尚未配置投递目标'), h('p', null, '点击“新建目标”可从已聊过的会话选择,也可手动填写。')) : h('ul', { className: 'dim-targetList' }, targets.map((target) => h(TargetRow, { key: target.targetId, definition, target, botId: account.botId, connected: account.connected, rpcCall: invoke, onChanged: () => loadTargets({ silent: true }), onEdit: () => setEditor({ mode: 'edit', target, source: 'edit' }), }))))))); }