@xmanrui/dsh-im 4.14.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +10 -2
- package/README.md +17 -6
- package/lib/client.js +1980 -616
- package/lib/index.js +303 -293
- package/package.json +6 -2
- package/plugin-src/client/access-policy-settings.js +6 -0
- package/plugin-src/client/channels/feishu/index.js +16 -4
- package/plugin-src/client/channels/wecom-app/api.js +124 -0
- package/plugin-src/client/channels/wecom-app/index.js +612 -0
- package/plugin-src/client/channels/wecom-app/styles.js +25 -0
- package/plugin-src/client/delivery-settings.js +7 -0
- package/plugin-src/client/i18n.js +64 -1
- package/plugin-src/client/index.js +21 -0
- package/plugin-src/client/model-setting.js +135 -69
- package/plugin-src/client/session-channel-logos.js +241 -0
- package/plugin-src/client/styles.js +22 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +8 -15
- package/plugin-src/host/channels/discord/index.mjs +8 -10
- package/plugin-src/host/channels/feishu/index.mjs +8 -15
- package/plugin-src/host/channels/office/index.mjs +9 -5
- package/plugin-src/host/channels/qq/index.mjs +8 -15
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/model-setting-rpc.mjs +1 -2
- package/plugin-src/host/channels/shared/startup-error.mjs +53 -0
- package/plugin-src/host/channels/shared/startup.mjs +47 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +8 -10
- package/plugin-src/host/channels/telegram/index.mjs +8 -10
- package/plugin-src/host/channels/wecom/index.mjs +8 -15
- package/plugin-src/host/channels/wecom/rpc.mjs +6 -1
- package/plugin-src/host/channels/wecom-app/index.mjs +33 -0
- package/plugin-src/host/channels/wecom-app/production.mjs +217 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +225 -0
- package/plugin-src/host/channels/weixin/index.mjs +8 -15
- package/plugin-src/host/channels/whatsapp/index.mjs +8 -15
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/delivery-suggestions.mjs +4 -0
- package/plugin-src/host/index.mjs +21 -2
- package/plugin-src/host/session-title-prefix.mjs +122 -0
- package/scripts/verify-model-setting.mjs +54 -0
- package/scripts/verify-session-channel-logos.mjs +59 -0
- package/scripts/verify-session-title-prefix.mjs +149 -0
- package/src/channels/feishu/bridge.mjs +305 -12
- package/src/channels/feishu/feishu-cards.mjs +38 -0
- package/src/channels/feishu/feishu-channel.mjs +88 -20
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/feishu/repair-manager.mjs +3 -2
- package/src/channels/shared/bot-workspace-store.mjs +11 -3
- package/src/channels/shared/command-catalog.mjs +102 -0
- package/src/channels/shared/i18n-en/feishu.mjs +14 -2
- package/src/channels/shared/i18n-en/shared-a.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +9 -0
- package/src/channels/shared/i18n-en/wecom-app.mjs +33 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/model-setting.mjs +43 -8
- package/src/channels/shared/session-channel-labels.mjs +26 -0
- package/src/channels/shared/text-harness-bridge.mjs +2 -26
- package/src/channels/telegram/telegram-api.mjs +18 -6
- package/src/channels/telegram/telegram-runtime.mjs +34 -32
- package/src/channels/wecom/send-error.mjs +31 -0
- package/src/channels/wecom/wecom-bridge.mjs +59 -17
- package/src/channels/wecom-app/callback-server.mjs +471 -0
- package/src/channels/wecom-app/config-store.mjs +240 -0
- package/src/channels/wecom-app/harness-client.mjs +11 -0
- package/src/channels/wecom-app/state-store.mjs +107 -0
- package/src/channels/wecom-app/wecom-app-api.mjs +432 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +1059 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +440 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +221 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { WecomLogoGlyph } from '../../channel-logos.js';
|
|
4
|
+
import { CollapsibleAccountSection } from '../shared/collapsible-account.js';
|
|
5
|
+
import { h } from '../../i18n.js';
|
|
6
|
+
import { WorkspaceEditor } from '../../workspace-editor.js';
|
|
7
|
+
import { ContextEnhancementEditor } from '../../context-enhancement.js';
|
|
8
|
+
import {
|
|
9
|
+
AgentPresetCatalogContext,
|
|
10
|
+
AgentPresetEditor,
|
|
11
|
+
EMPTY_AGENT_PRESET_CATALOG,
|
|
12
|
+
} from '../../agent-preset.js';
|
|
13
|
+
import {
|
|
14
|
+
EMPTY_MODEL_CATALOG,
|
|
15
|
+
ModelCatalogContext,
|
|
16
|
+
ModelEditor,
|
|
17
|
+
} from '../../model-setting.js';
|
|
18
|
+
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
19
|
+
import {
|
|
20
|
+
BotSettingsButton,
|
|
21
|
+
BotStatusMeta,
|
|
22
|
+
ChannelListHeading,
|
|
23
|
+
LastMessageErrorSummary,
|
|
24
|
+
} from '../../channel-card-meta.js';
|
|
25
|
+
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
26
|
+
import {
|
|
27
|
+
WECOM_APP_ENDPOINTS,
|
|
28
|
+
normalizeSnapshot,
|
|
29
|
+
presentError,
|
|
30
|
+
unwrapRpcResult,
|
|
31
|
+
} from './api.js';
|
|
32
|
+
import { installWecomAppStyles } from './styles.js';
|
|
33
|
+
|
|
34
|
+
const Button = React.forwardRef(function Button({ children, kind = 'secondary', className = '', ...props }, ref) {
|
|
35
|
+
return h('button', {
|
|
36
|
+
...props,
|
|
37
|
+
ref,
|
|
38
|
+
type: 'button',
|
|
39
|
+
className: `ddt-button ${className}`.trim(),
|
|
40
|
+
'data-kind': kind,
|
|
41
|
+
}, children);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function checkedTime(value) {
|
|
45
|
+
if (!value) return '尚未检查';
|
|
46
|
+
try {
|
|
47
|
+
return new Intl.DateTimeFormat('zh-CN', {
|
|
48
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
49
|
+
}).format(new Date(value));
|
|
50
|
+
} catch {
|
|
51
|
+
return '刚刚';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function Heading({ totals, adding, busy, onAdd, addButtonRef }) {
|
|
56
|
+
return h('div', { className: 'ddt-heading' },
|
|
57
|
+
h('div', { className: 'ddt-tools' },
|
|
58
|
+
h('div', { className: 'dim-bindActions' },
|
|
59
|
+
h(Button, {
|
|
60
|
+
kind: 'primary',
|
|
61
|
+
className: 'dim-scanButton',
|
|
62
|
+
onClick: onAdd,
|
|
63
|
+
disabled: adding || busy,
|
|
64
|
+
ref: addButtonRef,
|
|
65
|
+
'aria-label': '添加企业微信自建应用',
|
|
66
|
+
}, adding ? '收起表单' : '添加企业微信应用')),
|
|
67
|
+
totals.configured > 0
|
|
68
|
+
? h('div', { className: 'ddt-badge dim-onlineBadge' },
|
|
69
|
+
h('span', null, `${totals.connected} / ${totals.configured} 在线`))
|
|
70
|
+
: null));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function LoadingView() {
|
|
74
|
+
return h('div', { className: 'ddt-card ddt-loading dim-surfaceCard dim-loadingView', 'aria-busy': 'true' },
|
|
75
|
+
h('div', { className: 'ddt-spinner dim-spinner' }),
|
|
76
|
+
h('span', null, '正在读取企业微信应用状态…'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function EmptyView({ busy, onStart }) {
|
|
80
|
+
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
|
81
|
+
h('div', { className: 'ddt-cardBody ddt-empty dim-surfaceBody dim-emptyView' },
|
|
82
|
+
h('div', { className: 'dim-emptyCopy' },
|
|
83
|
+
h('div', { className: 'ddt-stateLabel dim-stateLabel' },
|
|
84
|
+
h('span', { className: 'ddt-dot dim-stateDot' }), h('span', null, '尚未绑定企业微信应用')),
|
|
85
|
+
h('h3', null, '在企业微信中创建自建应用,即可在微信里使用'),
|
|
86
|
+
h('p', null, '在应用管理后台创建自建应用,开启「接收消息」并填入下面的参数。成员的微信关注该企业的微信插件后,就能在微信中与本应用双向对话。'),
|
|
87
|
+
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
88
|
+
h(Button, { kind: 'primary', onClick: onStart, disabled: busy }, '填写应用参数'))),
|
|
89
|
+
h('div', { className: 'ddt-brandMark dim-emptyBrand dwecomapp-brand', 'aria-hidden': 'true' },
|
|
90
|
+
h(WecomLogoGlyph, { size: 64 }))));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function Field({ label, value, onChange, placeholder, busy, required = false, type = 'text' }) {
|
|
94
|
+
return h('label', { className: 'dim-credentialField' },
|
|
95
|
+
h('span', null, label),
|
|
96
|
+
h('input', {
|
|
97
|
+
value,
|
|
98
|
+
onChange: (event) => onChange(event.target.value),
|
|
99
|
+
placeholder,
|
|
100
|
+
type,
|
|
101
|
+
autoCapitalize: 'none',
|
|
102
|
+
autoCorrect: 'off',
|
|
103
|
+
spellCheck: false,
|
|
104
|
+
autoComplete: 'off',
|
|
105
|
+
disabled: busy,
|
|
106
|
+
required,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function BindForm({ busy, error, onSubmit, onCancel }) {
|
|
111
|
+
const [corpId, setCorpId] = React.useState('');
|
|
112
|
+
const [agentId, setAgentId] = React.useState('');
|
|
113
|
+
const [secret, setSecret] = React.useState('');
|
|
114
|
+
const [token, setToken] = React.useState('');
|
|
115
|
+
const [aesKey, setAesKey] = React.useState('');
|
|
116
|
+
const [apiBaseUrl, setApiBaseUrl] = React.useState('');
|
|
117
|
+
const [callbackBaseUrl, setCallbackBaseUrl] = React.useState('');
|
|
118
|
+
const [streamEnabled, setStreamEnabled] = React.useState(true);
|
|
119
|
+
const headingId = React.useId();
|
|
120
|
+
const formRef = React.useRef(null);
|
|
121
|
+
|
|
122
|
+
const submit = (event) => {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
if (busy) return;
|
|
125
|
+
void onSubmit?.({
|
|
126
|
+
corpId: corpId.trim(),
|
|
127
|
+
agentId: agentId.trim(),
|
|
128
|
+
secret: secret.trim(),
|
|
129
|
+
token: token.trim(),
|
|
130
|
+
encodingAESKey: aesKey.trim(),
|
|
131
|
+
apiBaseUrl: apiBaseUrl.trim() || undefined,
|
|
132
|
+
callbackBaseUrl: callbackBaseUrl.trim() || undefined,
|
|
133
|
+
streamEnabled,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return h('section', {
|
|
138
|
+
className: 'ddt-card dim-surfaceCard dim-credentialPanel',
|
|
139
|
+
'aria-labelledby': headingId,
|
|
140
|
+
},
|
|
141
|
+
h('h3', { id: headingId, className: 'dim-credentialTitle' }, '绑定企业微信自建应用'),
|
|
142
|
+
h('form', { ref: formRef, className: 'dim-credentialForm', onSubmit: submit },
|
|
143
|
+
h('div', { className: 'dim-appFieldGrid' },
|
|
144
|
+
h(Field, { label: '企业 ID(CorpID)', value: corpId, onChange: setCorpId, placeholder: '例如 ww1234567890abcdef', busy, required: true }),
|
|
145
|
+
h(Field, { label: '应用 AgentId', value: agentId, onChange: setAgentId, placeholder: '例如 1000002', busy, required: true }),
|
|
146
|
+
h(Field, { label: '应用 Secret', value: secret, onChange: setSecret, placeholder: '应用详情页的 Secret', busy, required: true, type: 'password' }),
|
|
147
|
+
h(Field, { label: '回调 Token', value: token, onChange: setToken, placeholder: '接收消息 → API 接收中的 Token', busy, required: true, type: 'password' }),
|
|
148
|
+
h(Field, { label: 'EncodingAESKey', value: aesKey, onChange: setAesKey, placeholder: '43 位字母或数字', busy, required: true, type: 'password' }),
|
|
149
|
+
h(Field, { label: '代理地址(可选)', value: apiBaseUrl, onChange: setApiBaseUrl, placeholder: '留空直连 qyapi.weixin.qq.com', busy })),
|
|
150
|
+
h('div', { className: 'dim-appFieldGrid' },
|
|
151
|
+
h(Field, { label: '公网回调基址(可选)', value: callbackBaseUrl, onChange: setCallbackBaseUrl, placeholder: '例如 https://im.example.com', busy }),
|
|
152
|
+
h('div', { className: 'dim-appSwitchRow' },
|
|
153
|
+
h('button', {
|
|
154
|
+
type: 'button',
|
|
155
|
+
className: 'ddt-button dim-streamToggle',
|
|
156
|
+
'aria-pressed': streamEnabled ? 'true' : 'false',
|
|
157
|
+
onClick: () => setStreamEnabled((value) => !value),
|
|
158
|
+
disabled: busy,
|
|
159
|
+
}, streamEnabled ? '流式回复:开' : '流式回复:关'),
|
|
160
|
+
h('span', { className: 'dim-switchHint' }, '企业微信客户端实时出字;微信端不支持时自动改为整段发送'))),
|
|
161
|
+
error ? h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' }, h('p', null, error.message)) : null,
|
|
162
|
+
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
163
|
+
h(Button, { kind: 'primary', onClick: () => formRef.current?.requestSubmit(), disabled: busy }, busy ? '正在绑定…' : '保存并连接'),
|
|
164
|
+
h(Button, { kind: 'quiet', onClick: onCancel, disabled: busy }, '取消'))));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function CallbackUrlBox({ url, busy, resetBusy, onCopy, onReset }) {
|
|
168
|
+
const [copied, setCopied] = React.useState(false);
|
|
169
|
+
const copy = async () => {
|
|
170
|
+
try {
|
|
171
|
+
await navigator.clipboard.writeText(url);
|
|
172
|
+
setCopied(true);
|
|
173
|
+
window.setTimeout(() => setCopied(false), 2_000);
|
|
174
|
+
} catch {
|
|
175
|
+
setCopied(false);
|
|
176
|
+
}
|
|
177
|
+
onCopy?.();
|
|
178
|
+
};
|
|
179
|
+
return h('div', { className: 'dim-callbackBox' },
|
|
180
|
+
h('strong', null, '回调 URL(填入企业微信后台「接收消息 → 设置API接收」)'),
|
|
181
|
+
h('div', { className: 'dim-callbackRow' },
|
|
182
|
+
h('input', {
|
|
183
|
+
value: url || '未配置公网回调基址:请在本页「设置」中填写回调基址,或手动拼接 回调基址 + 路径',
|
|
184
|
+
readOnly: true,
|
|
185
|
+
onFocus: (event) => event.target.select?.(),
|
|
186
|
+
}),
|
|
187
|
+
url ? h(Button, { onClick: copy, disabled: busy, small: true }, copied ? '已复制' : '复制') : null,
|
|
188
|
+
h(Button, { kind: 'danger', onClick: onReset, disabled: busy || resetBusy }, resetBusy ? '重置中…' : '重置密钥')),
|
|
189
|
+
h('span', { className: 'dim-callbackHint' },
|
|
190
|
+
'重置密钥后回调 URL 会变化,需要同步更新企业微信后台。若公网反代未就绪,可临时使用 http://服务器IP:端口 形式的回调地址。'));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function AppSettingsEditor({ bot, busy, onSave }) {
|
|
194
|
+
const [apiBaseUrl, setApiBaseUrl] = React.useState(bot.apiBaseUrl ?? '');
|
|
195
|
+
const [callbackBaseUrl, setCallbackBaseUrl] = React.useState(bot.callbackBaseUrl ?? '');
|
|
196
|
+
const [streamEnabled, setStreamEnabled] = React.useState(bot.streamEnabled !== false);
|
|
197
|
+
const [dirty, setDirty] = React.useState(false);
|
|
198
|
+
const formRef = React.useRef(null);
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
setApiBaseUrl(bot.apiBaseUrl ?? '');
|
|
201
|
+
setCallbackBaseUrl(bot.callbackBaseUrl ?? '');
|
|
202
|
+
setStreamEnabled(bot.streamEnabled !== false);
|
|
203
|
+
setDirty(false);
|
|
204
|
+
}, [bot.apiBaseUrl, bot.callbackBaseUrl, bot.streamEnabled]);
|
|
205
|
+
const touch = () => setDirty(true);
|
|
206
|
+
const save = (event) => {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
if (!dirty || busy) return;
|
|
209
|
+
void onSave({
|
|
210
|
+
apiBaseUrl: apiBaseUrl.trim() || null,
|
|
211
|
+
callbackBaseUrl: callbackBaseUrl.trim() || null,
|
|
212
|
+
streamEnabled,
|
|
213
|
+
});
|
|
214
|
+
setDirty(false);
|
|
215
|
+
};
|
|
216
|
+
return h('form', { ref: formRef, className: 'dim-appFieldGrid', onSubmit: save },
|
|
217
|
+
h(Field, { label: '代理地址(可选)', value: apiBaseUrl, onChange: (value) => { setApiBaseUrl(value); touch(); }, placeholder: '留空直连 qyapi.weixin.qq.com', busy }),
|
|
218
|
+
h(Field, { label: '公网回调基址(可选)', value: callbackBaseUrl, onChange: (value) => { setCallbackBaseUrl(value); touch(); }, placeholder: '例如 https://im.example.com', busy }),
|
|
219
|
+
h('div', { className: 'dim-appSwitchRow' },
|
|
220
|
+
h('button', {
|
|
221
|
+
type: 'button',
|
|
222
|
+
className: 'ddt-button dim-streamToggle',
|
|
223
|
+
'aria-pressed': streamEnabled ? 'true' : 'false',
|
|
224
|
+
onClick: () => { setStreamEnabled((value) => !value); touch(); },
|
|
225
|
+
disabled: busy,
|
|
226
|
+
}, streamEnabled ? '流式回复:开' : '流式回复:关')),
|
|
227
|
+
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
228
|
+
h(Button, { onClick: () => formRef.current?.requestSubmit(), disabled: busy || !dirty }, busy ? '保存中…' : '保存设置')));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
|
|
232
|
+
return h('div', { className: 'ddt-confirm dim-confirm', role: 'alertdialog' },
|
|
233
|
+
h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
|
|
234
|
+
h('p', null, '这会删除本机保存的应用凭据、回调配置及会话映射。企业微信后台的应用不会被删除。'),
|
|
235
|
+
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
236
|
+
h(Button, { onClick: onCancel, disabled: busy }, '保留应用'),
|
|
237
|
+
h(Button, { kind: 'danger', onClick: onConfirm, disabled: busy }, busy ? '正在移除…' : '确认移除接入')));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function AccountCard({
|
|
241
|
+
account,
|
|
242
|
+
busy,
|
|
243
|
+
feedback,
|
|
244
|
+
removing,
|
|
245
|
+
onReconnect,
|
|
246
|
+
onSettingsSave,
|
|
247
|
+
onSecretReset,
|
|
248
|
+
onWorkspaceSave,
|
|
249
|
+
onModelSave,
|
|
250
|
+
onAgentPresetSave,
|
|
251
|
+
onContextEnhancementSave,
|
|
252
|
+
onRequestRemove,
|
|
253
|
+
onConfirmRemove,
|
|
254
|
+
onCancelRemove,
|
|
255
|
+
}) {
|
|
256
|
+
const tone = account.connected ? 'success' : account.state === 'error' ? 'error' : 'warning';
|
|
257
|
+
const stateLabel = account.connected ? '运行正常' : account.state === 'connecting' ? '正在连接' : '连接未就绪';
|
|
258
|
+
const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
|
|
259
|
+
const elementId = `wecomapp-settings-${account.botId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
260
|
+
return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
|
|
261
|
+
h('div', { className: 'ddt-cardBody dim-botCardBody' },
|
|
262
|
+
h(CollapsibleAccountSection, {
|
|
263
|
+
id: elementId,
|
|
264
|
+
header: h('div', { className: 'ddt-accountTop dim-botCardTop' },
|
|
265
|
+
h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
|
|
266
|
+
h('div', { className: 'ddt-avatar dim-botAvatar dwecomapp-avatar', 'aria-hidden': 'true' }, h(WecomLogoGlyph, { size: 29 })),
|
|
267
|
+
h('div', { className: 'dim-botName' },
|
|
268
|
+
h('h3', null, account.bot.name),
|
|
269
|
+
h('p', null, `${account.bot.corpIdMasked} · AgentId ${account.bot.agentId}`))),
|
|
270
|
+
h('div', {
|
|
271
|
+
className: 'dim-botCardTools',
|
|
272
|
+
onClick: (event) => { event.stopPropagation(); },
|
|
273
|
+
onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') event.stopPropagation(); },
|
|
274
|
+
},
|
|
275
|
+
h(BotStatusMeta, {
|
|
276
|
+
className: 'ddt-health',
|
|
277
|
+
dotClassName: 'ddt-dot',
|
|
278
|
+
tone,
|
|
279
|
+
stateLabel,
|
|
280
|
+
lastCheckedAt: account.health.lastCheckedAt,
|
|
281
|
+
formatCheckedTime: checkedTime,
|
|
282
|
+
}),
|
|
283
|
+
h(BotSettingsButton, {
|
|
284
|
+
channel: 'wecomApp',
|
|
285
|
+
botId: account.botId,
|
|
286
|
+
botName: account.bot.name,
|
|
287
|
+
connected: account.connected,
|
|
288
|
+
accessPolicy: account.accessPolicy,
|
|
289
|
+
})))
|
|
290
|
+
},
|
|
291
|
+
h(CallbackUrlBox, {
|
|
292
|
+
url: account.bot.callbackUrl,
|
|
293
|
+
busy: Boolean(busy),
|
|
294
|
+
resetBusy: busy === 'reset',
|
|
295
|
+
onReset: onSecretReset,
|
|
296
|
+
}),
|
|
297
|
+
h(AppSettingsEditor, {
|
|
298
|
+
bot: account.bot,
|
|
299
|
+
busy: Boolean(busy),
|
|
300
|
+
onSave: onSettingsSave,
|
|
301
|
+
}),
|
|
302
|
+
h(WorkspaceEditor, {
|
|
303
|
+
workspace: account.workspace,
|
|
304
|
+
disabled: Boolean(busy),
|
|
305
|
+
onSave: onWorkspaceSave,
|
|
306
|
+
}),
|
|
307
|
+
h(ModelEditor, {
|
|
308
|
+
model: account.model,
|
|
309
|
+
disabled: Boolean(busy),
|
|
310
|
+
onSave: onModelSave,
|
|
311
|
+
}),
|
|
312
|
+
h(AgentPresetEditor, {
|
|
313
|
+
agentPreset: account.agentPreset,
|
|
314
|
+
disabled: Boolean(busy),
|
|
315
|
+
onSave: onAgentPresetSave,
|
|
316
|
+
}),
|
|
317
|
+
h(ContextEnhancementEditor, {
|
|
318
|
+
config: account.contextEnhancement,
|
|
319
|
+
disabled: Boolean(busy),
|
|
320
|
+
onSave: onContextEnhancementSave,
|
|
321
|
+
}),
|
|
322
|
+
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
323
|
+
h('div', { className: 'dim-cardFooterLayout' },
|
|
324
|
+
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
325
|
+
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
326
|
+
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
|
|
327
|
+
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
328
|
+
account.lastMessageError ? h(LastMessageErrorSummary, {
|
|
329
|
+
className: 'ddt-summary',
|
|
330
|
+
error: account.lastMessageError,
|
|
331
|
+
}) : null,
|
|
332
|
+
feedback ? h('div', {
|
|
333
|
+
className: 'ddt-summary dim-cardFeedback',
|
|
334
|
+
role: 'status',
|
|
335
|
+
'aria-live': 'polite',
|
|
336
|
+
}, feedback) : null)),
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
removing ? h(RemoveConfirmation, {
|
|
340
|
+
account, busy: busy === 'delete', onConfirm: onConfirmRemove, onCancel: onCancelRemove,
|
|
341
|
+
}) : null);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function WecomAppSettingsTab({ rpcCall }) {
|
|
345
|
+
const [model, setModel] = React.useState({
|
|
346
|
+
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
|
|
347
|
+
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
|
348
|
+
modelCatalog: EMPTY_MODEL_CATALOG,
|
|
349
|
+
});
|
|
350
|
+
const [bindOpen, setBindOpen] = React.useState(false);
|
|
351
|
+
const [bindError, setBindError] = React.useState(null);
|
|
352
|
+
const [busy, setBusy] = React.useState(false);
|
|
353
|
+
const [busyByBot, setBusyByBot] = React.useState({});
|
|
354
|
+
const [feedbackByBot, setFeedbackByBot] = React.useState({});
|
|
355
|
+
const [removeTarget, setRemoveTarget] = React.useState(null);
|
|
356
|
+
const [notice, setNotice] = React.useState('');
|
|
357
|
+
const mounted = React.useRef(true);
|
|
358
|
+
const workspaceFence = useWorkspaceSnapshotFence();
|
|
359
|
+
const addButtonRef = React.useRef(null);
|
|
360
|
+
const noticeFrameRef = React.useRef(null);
|
|
361
|
+
|
|
362
|
+
const announce = React.useCallback((message) => {
|
|
363
|
+
if (!mounted.current) return;
|
|
364
|
+
if (noticeFrameRef.current !== null) {
|
|
365
|
+
window.cancelAnimationFrame(noticeFrameRef.current);
|
|
366
|
+
noticeFrameRef.current = null;
|
|
367
|
+
}
|
|
368
|
+
setNotice('');
|
|
369
|
+
if (message) {
|
|
370
|
+
noticeFrameRef.current = window.requestAnimationFrame(() => {
|
|
371
|
+
noticeFrameRef.current = null;
|
|
372
|
+
if (mounted.current) setNotice(message);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}, []);
|
|
376
|
+
|
|
377
|
+
React.useEffect(() => {
|
|
378
|
+
const disposeDingtalk = installDingtalkStyles();
|
|
379
|
+
const disposeWecomApp = installWecomAppStyles();
|
|
380
|
+
mounted.current = true;
|
|
381
|
+
return () => {
|
|
382
|
+
mounted.current = false;
|
|
383
|
+
if (noticeFrameRef.current !== null) {
|
|
384
|
+
window.cancelAnimationFrame(noticeFrameRef.current);
|
|
385
|
+
noticeFrameRef.current = null;
|
|
386
|
+
}
|
|
387
|
+
disposeWecomApp();
|
|
388
|
+
disposeDingtalk();
|
|
389
|
+
};
|
|
390
|
+
}, []);
|
|
391
|
+
|
|
392
|
+
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
|
393
|
+
if (typeof rpcCall !== 'function') throw new TypeError('企业微信应用设置页缺少 RPC 连接');
|
|
394
|
+
return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
|
395
|
+
}, [rpcCall]);
|
|
396
|
+
|
|
397
|
+
const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
|
398
|
+
const workspaceVersion = workspaceFence.beginStatus();
|
|
399
|
+
if (workspaceVersion === null) return undefined;
|
|
400
|
+
if (!silent && mounted.current) setModel((current) => ({ ...current, phase: 'loading', error: null }));
|
|
401
|
+
try {
|
|
402
|
+
const snapshot = normalizeSnapshot(await invoke(WECOM_APP_ENDPOINTS.status, {}, signal));
|
|
403
|
+
if (!mounted.current || signal?.aborted
|
|
404
|
+
|| !workspaceFence.canCommitStatus(workspaceVersion)) return undefined;
|
|
405
|
+
setModel({
|
|
406
|
+
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
|
407
|
+
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
|
408
|
+
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
|
409
|
+
});
|
|
410
|
+
if (restore && snapshot.bots.length > 0) setBindOpen(false);
|
|
411
|
+
return snapshot;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
|
|
414
|
+
&& workspaceFence.canCommitStatus(workspaceVersion)) {
|
|
415
|
+
setModel((current) => ({ ...current, phase: silent ? current.phase : 'error', error: presentError(error) }));
|
|
416
|
+
}
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
}, [invoke, workspaceFence]);
|
|
420
|
+
|
|
421
|
+
React.useEffect(() => {
|
|
422
|
+
const controller = new AbortController();
|
|
423
|
+
void loadStatus({ signal: controller.signal, restore: true });
|
|
424
|
+
return () => controller.abort();
|
|
425
|
+
}, [loadStatus]);
|
|
426
|
+
|
|
427
|
+
React.useEffect(() => {
|
|
428
|
+
if (model.phase !== 'ready') return undefined;
|
|
429
|
+
const controller = new AbortController();
|
|
430
|
+
const timer = window.setInterval(() => void loadStatus({ signal: controller.signal, silent: true }), 15_000);
|
|
431
|
+
return () => { controller.abort(); window.clearInterval(timer); };
|
|
432
|
+
}, [loadStatus, model.phase]);
|
|
433
|
+
|
|
434
|
+
const bindApp = React.useCallback(async (payload) => {
|
|
435
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
436
|
+
setBusy(true);
|
|
437
|
+
setBindError(null);
|
|
438
|
+
try {
|
|
439
|
+
const snapshot = normalizeSnapshot(await invoke(WECOM_APP_ENDPOINTS.bindApp, payload));
|
|
440
|
+
if (!mounted.current) return;
|
|
441
|
+
if (workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
442
|
+
setModel({
|
|
443
|
+
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
|
444
|
+
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
|
445
|
+
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
setBindOpen(false);
|
|
449
|
+
announce('企业微信应用已绑定。请把回调 URL 填入企业微信后台并保存。');
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (mounted.current) setBindError(presentError(error));
|
|
452
|
+
} finally {
|
|
453
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
454
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
455
|
+
if (mounted.current) setBusy(false);
|
|
456
|
+
}
|
|
457
|
+
}, [announce, invoke, loadStatus, workspaceFence]);
|
|
458
|
+
|
|
459
|
+
const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
|
|
460
|
+
const snapshotVersion = workspaceFence.beginMutation();
|
|
461
|
+
setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
|
|
462
|
+
try {
|
|
463
|
+
const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
|
|
464
|
+
if (mounted.current && workspaceFence.canCommitMutation(snapshotVersion)) {
|
|
465
|
+
setModel({
|
|
466
|
+
phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
|
|
467
|
+
agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
|
468
|
+
modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return snapshot;
|
|
472
|
+
} finally {
|
|
473
|
+
const shouldRefresh = workspaceFence.endMutation();
|
|
474
|
+
if (shouldRefresh && mounted.current) void loadStatus({ silent: true });
|
|
475
|
+
if (mounted.current) setBusyByBot((current) => {
|
|
476
|
+
const next = { ...current }; delete next[account.botId]; return next;
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}, [invoke, loadStatus, workspaceFence]);
|
|
480
|
+
|
|
481
|
+
const reconnect = React.useCallback(async (account) => {
|
|
482
|
+
setFeedbackByBot((current) => {
|
|
483
|
+
const next = { ...current };
|
|
484
|
+
delete next[account.botId];
|
|
485
|
+
return next;
|
|
486
|
+
});
|
|
487
|
+
try {
|
|
488
|
+
const snapshot = await botAction(
|
|
489
|
+
account,
|
|
490
|
+
'reconnect',
|
|
491
|
+
WECOM_APP_ENDPOINTS.reconnectBot,
|
|
492
|
+
{ botId: account.botId, sendTest: true },
|
|
493
|
+
);
|
|
494
|
+
if (!snapshot) return;
|
|
495
|
+
const refreshed = snapshot.bots.find((bot) => bot.botId === account.botId);
|
|
496
|
+
let feedback;
|
|
497
|
+
if (!refreshed?.connected) {
|
|
498
|
+
feedback = '企业微信应用仍未就绪,插件会继续自动重试。';
|
|
499
|
+
} else if (snapshot.testMessage?.sent) {
|
|
500
|
+
feedback = '连接检查完成,测试消息已发送。';
|
|
501
|
+
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
|
|
502
|
+
feedback = '连接检查完成。应用尚未收到可用于测试的私聊消息。';
|
|
503
|
+
} else if (snapshot.testMessage) {
|
|
504
|
+
feedback = '连接检查完成,但测试消息发送失败。';
|
|
505
|
+
} else {
|
|
506
|
+
feedback = '连接检查完成。';
|
|
507
|
+
}
|
|
508
|
+
if (mounted.current) {
|
|
509
|
+
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
|
|
510
|
+
}
|
|
511
|
+
announce(feedback);
|
|
512
|
+
} catch {
|
|
513
|
+
const feedback = '连接检查失败,请稍后重试。';
|
|
514
|
+
if (mounted.current) {
|
|
515
|
+
setFeedbackByBot((current) => ({ ...current, [account.botId]: feedback }));
|
|
516
|
+
}
|
|
517
|
+
announce(feedback);
|
|
518
|
+
}
|
|
519
|
+
}, [announce, botAction]);
|
|
520
|
+
|
|
521
|
+
const botList = model.bots.length > 0
|
|
522
|
+
? h('section', { className: 'dim-listSection' },
|
|
523
|
+
h(ChannelListHeading, {
|
|
524
|
+
className: 'ddt-listHeading',
|
|
525
|
+
title: '已绑定的企业微信应用',
|
|
526
|
+
connectionLabel: 'HTTP 回调通道',
|
|
527
|
+
}),
|
|
528
|
+
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
|
|
529
|
+
h('li', { key: account.botId }, h(AccountCard, {
|
|
530
|
+
account,
|
|
531
|
+
busy: busyByBot[account.botId],
|
|
532
|
+
feedback: feedbackByBot[account.botId],
|
|
533
|
+
removing: removeTarget === account.botId,
|
|
534
|
+
onReconnect: () => void reconnect(account),
|
|
535
|
+
onSettingsSave: (settings) => botAction(
|
|
536
|
+
account,
|
|
537
|
+
'settings',
|
|
538
|
+
WECOM_APP_ENDPOINTS.updateSettings,
|
|
539
|
+
{ botId: account.botId, ...settings },
|
|
540
|
+
),
|
|
541
|
+
onSecretReset: async () => {
|
|
542
|
+
try {
|
|
543
|
+
await botAction(account, 'reset', WECOM_APP_ENDPOINTS.resetCallbackSecret, { botId: account.botId });
|
|
544
|
+
announce('回调密钥已重置,请把新的回调 URL 更新到企业微信后台。');
|
|
545
|
+
} catch {
|
|
546
|
+
announce('回调密钥重置失败,请稍后重试。');
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
onWorkspaceSave: (workspace) => botAction(
|
|
550
|
+
account,
|
|
551
|
+
'workspace',
|
|
552
|
+
WECOM_APP_ENDPOINTS.setWorkspace,
|
|
553
|
+
{ botId: account.botId, workspace },
|
|
554
|
+
),
|
|
555
|
+
onModelSave: (selectedModel) => botAction(
|
|
556
|
+
account,
|
|
557
|
+
'model',
|
|
558
|
+
WECOM_APP_ENDPOINTS.setModel,
|
|
559
|
+
{ botId: account.botId, model: selectedModel },
|
|
560
|
+
),
|
|
561
|
+
onAgentPresetSave: (agentPreset) => botAction(
|
|
562
|
+
account,
|
|
563
|
+
'preset',
|
|
564
|
+
WECOM_APP_ENDPOINTS.setAgentPreset,
|
|
565
|
+
{ botId: account.botId, agentPreset },
|
|
566
|
+
),
|
|
567
|
+
onContextEnhancementSave: (config) => botAction(
|
|
568
|
+
account,
|
|
569
|
+
'context-enhancement',
|
|
570
|
+
WECOM_APP_ENDPOINTS.setContextEnhancement,
|
|
571
|
+
{ botId: account.botId, config },
|
|
572
|
+
),
|
|
573
|
+
onRequestRemove: () => setRemoveTarget(account.botId),
|
|
574
|
+
onCancelRemove: () => setRemoveTarget(null),
|
|
575
|
+
onConfirmRemove: async () => {
|
|
576
|
+
await botAction(account, 'delete', WECOM_APP_ENDPOINTS.deleteBot, { botId: account.botId, confirm: true });
|
|
577
|
+
if (mounted.current) setRemoveTarget(null);
|
|
578
|
+
},
|
|
579
|
+
})))))
|
|
580
|
+
: null;
|
|
581
|
+
|
|
582
|
+
const bindView = bindOpen
|
|
583
|
+
? h(BindForm, {
|
|
584
|
+
busy,
|
|
585
|
+
error: bindError,
|
|
586
|
+
onSubmit: bindApp,
|
|
587
|
+
onCancel: () => { setBindOpen(false); setBindError(null); },
|
|
588
|
+
})
|
|
589
|
+
: null;
|
|
590
|
+
|
|
591
|
+
return h(ModelCatalogContext.Provider, {
|
|
592
|
+
value: model.modelCatalog ?? EMPTY_MODEL_CATALOG,
|
|
593
|
+
}, h(AgentPresetCatalogContext.Provider, {
|
|
594
|
+
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
|
595
|
+
}, h('section', { className: 'ddt-page dwecomapp-page dim-channelPage', 'aria-label': '企业微信应用设置' },
|
|
596
|
+
h(Heading, {
|
|
597
|
+
totals: model.totals,
|
|
598
|
+
adding: bindOpen,
|
|
599
|
+
busy,
|
|
600
|
+
onAdd: () => { setBindOpen((value) => !value); setBindError(null); },
|
|
601
|
+
addButtonRef,
|
|
602
|
+
}),
|
|
603
|
+
h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
|
604
|
+
model.phase === 'loading' ? h(LoadingView)
|
|
605
|
+
: model.phase === 'error'
|
|
606
|
+
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信应用状态'), h('p', null, model.error?.message), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
|
607
|
+
: h(React.Fragment, null,
|
|
608
|
+
bindView,
|
|
609
|
+
model.bots.length === 0 && !bindOpen
|
|
610
|
+
? h(EmptyView, { busy, onStart: () => setBindOpen(true) }) : null,
|
|
611
|
+
botList))));
|
|
612
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const WECOM_APP_STYLE_ID = 'xmanrui-dsh-im-wecom-app-settings';
|
|
2
|
+
|
|
3
|
+
const CSS = String.raw`
|
|
4
|
+
.dwecomapp-page { --ddt-accent: #07c160; --ddt-accent-deep: #059a4c; --ddt-accent-wash: #eefaf3; }
|
|
5
|
+
.dwecomapp-avatar, .dwecomapp-brand { color: #07c160; background: #fff; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); }
|
|
6
|
+
.dwecomapp-avatar svg, .dwecomapp-brand svg { display: block; }
|
|
7
|
+
.dim-appFieldGrid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
|
8
|
+
.dim-callbackBox { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border: 1px dashed var(--dsw-alias-border-l2, #e5e6eb); border-radius: 8px; }
|
|
9
|
+
.dim-callbackBox strong { font-size: 12px; color: var(--dsw-alias-text-secondary, #646a73); }
|
|
10
|
+
.dim-callbackRow { display: flex; gap: 8px; align-items: center; }
|
|
11
|
+
.dim-callbackRow input { flex: 1 1 auto; min-width: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
12
|
+
.dim-appSwitchRow { display: flex; align-items: center; gap: 8px; font-size: 13px; }
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export function installWecomAppStyles() {
|
|
16
|
+
if (typeof document === 'undefined') return () => {};
|
|
17
|
+
const existing = document.querySelector(`style[data-plugin-css="${WECOM_APP_STYLE_ID}"]`);
|
|
18
|
+
if (existing) return () => {};
|
|
19
|
+
const style = document.createElement('style');
|
|
20
|
+
style.dataset.plugin = '@xmanrui/dsh-im';
|
|
21
|
+
style.dataset.pluginCss = WECOM_APP_STYLE_ID;
|
|
22
|
+
style.textContent = CSS;
|
|
23
|
+
document.head.appendChild(style);
|
|
24
|
+
return () => style.remove();
|
|
25
|
+
}
|
|
@@ -65,6 +65,13 @@ const CHANNEL_DEFINITIONS = Object.freeze({
|
|
|
65
65
|
group: [{ key: 'chatId', label: '群 Chat ID', placeholder: '填写群 chatid' }],
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
|
+
wecomApp: {
|
|
69
|
+
label: '企业微信应用',
|
|
70
|
+
kinds: [{ value: 'user', label: '私聊' }],
|
|
71
|
+
fields: {
|
|
72
|
+
user: [{ key: 'chatId', label: '用户 ID', placeholder: '填写企业微信用户 ID' }],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
68
75
|
qq: {
|
|
69
76
|
label: 'QQ',
|
|
70
77
|
kinds: [{ value: 'user', label: '单聊' }, { value: 'group', label: '群聊' }],
|