@xmanrui/dsh-im 4.22.0 → 4.24.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 +5 -4
- package/README.md +5 -4
- package/lib/client.js +2584 -1153
- package/lib/index.js +290 -298
- package/package.json +14 -2
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +23 -7
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +8 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +69 -1
- package/plugin-src/client/index.js +40 -4
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +8 -2
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +14 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +13 -5
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +699 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +604 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +39 -11
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +287 -57
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +150 -25
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +215 -14
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
|
|
1
2
|
import { BotName } from '../../bot-alias.js';
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
|
|
@@ -150,7 +151,7 @@ function ProvisionView({ provision, busy, onRetry, onClose }) {
|
|
|
150
151
|
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
|
151
152
|
h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
|
|
152
153
|
h('h3', null, '企业微信机器人没有绑定完成'),
|
|
153
|
-
h(
|
|
154
|
+
h(ConnectionError, { error: error }),
|
|
154
155
|
h('span', { className: 'ddt-errorCode' }, error.code),
|
|
155
156
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
156
157
|
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
|
@@ -241,6 +242,7 @@ export function AccountCard({
|
|
|
241
242
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
242
243
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
|
|
243
244
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
245
|
+
account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
|
|
244
246
|
account.lastMessageError ? h(LastMessageErrorSummary, {
|
|
245
247
|
className: 'ddt-summary',
|
|
246
248
|
error: account.lastMessageError,
|
|
@@ -258,6 +260,7 @@ export function AccountCard({
|
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
export function WecomSettingsTab({ rpcCall }) {
|
|
263
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
261
264
|
const [model, setModel] = React.useState({
|
|
262
265
|
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
|
|
263
266
|
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
|
@@ -309,7 +312,16 @@ export function WecomSettingsTab({ rpcCall }) {
|
|
|
309
312
|
|
|
310
313
|
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
|
311
314
|
if (typeof rpcCall !== 'function') throw new TypeError('企业微信设置页缺少 RPC 连接');
|
|
312
|
-
|
|
315
|
+
const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
|
|
316
|
+
if (operation && mounted.current) setOperationError(null);
|
|
317
|
+
try {
|
|
318
|
+
const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
|
319
|
+
if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
|
|
320
|
+
return value;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
313
325
|
}, [rpcCall]);
|
|
314
326
|
|
|
315
327
|
const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
|
@@ -589,6 +601,7 @@ export function WecomSettingsTab({ rpcCall }) {
|
|
|
589
601
|
}, h(AgentPresetCatalogContext.Provider, {
|
|
590
602
|
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
|
591
603
|
}, h('section', { className: 'ddt-page dwecom-page dim-channelPage', 'aria-label': '企业微信设置' },
|
|
604
|
+
operationError ? h(ConnectionError, { error: operationError }) : null,
|
|
592
605
|
h(Heading, {
|
|
593
606
|
totals: model.totals,
|
|
594
607
|
adding: Boolean(provision),
|
|
@@ -601,7 +614,7 @@ export function WecomSettingsTab({ rpcCall }) {
|
|
|
601
614
|
h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
|
602
615
|
model.phase === 'loading' ? h(LoadingView)
|
|
603
616
|
: model.phase === 'error'
|
|
604
|
-
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信机器人状态'), h(
|
|
617
|
+
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信机器人状态'), h(ConnectionError, { error: model.error }), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
|
605
618
|
: h(React.Fragment, null,
|
|
606
619
|
credentialView,
|
|
607
620
|
provisionView,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeConnectionError as normalizeTestError } from '../../connection-error.js';
|
|
2
|
+
import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
|
|
1
3
|
import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
2
4
|
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
|
3
5
|
import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
|
|
@@ -55,7 +57,7 @@ function normalizeTestMessage(value) {
|
|
|
55
57
|
const code = value.code === 'test-target-unavailable'
|
|
56
58
|
? 'test-target-unavailable'
|
|
57
59
|
: 'test-message-failed';
|
|
58
|
-
return { sent: false, code };
|
|
60
|
+
return { sent: false, code, ...(value.error ? { error: normalizeTestError(value.error) } : {}) };
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export function unwrapRpcResult(result) {
|
|
@@ -63,6 +65,7 @@ export function unwrapRpcResult(result) {
|
|
|
63
65
|
if (!result.ok) {
|
|
64
66
|
const error = new Error(text(result.error?.message, '企业微信应用操作失败'));
|
|
65
67
|
error.code = text(result.error?.code, 'WECOM_APP_RPC_ERROR', 80);
|
|
68
|
+
Object.assign(error, diagnosticFields(result.error));
|
|
66
69
|
throw error;
|
|
67
70
|
}
|
|
68
71
|
return result.value;
|
|
@@ -99,6 +102,7 @@ function normalizeBot(value) {
|
|
|
99
102
|
},
|
|
100
103
|
lastMessageError: normalizeLastMessageError(value.lastMessageError),
|
|
101
104
|
error: isRecord(value.error) ? {
|
|
105
|
+
...diagnosticFields(value.error),
|
|
102
106
|
code: text(value.error.code, 'WECOM_APP_ACCOUNT_ERROR', 80),
|
|
103
107
|
message: text(value.error.message, '企业微信应用连接尚未就绪'),
|
|
104
108
|
} : null,
|
|
@@ -121,6 +125,7 @@ export function normalizeSnapshot(value) {
|
|
|
121
125
|
|
|
122
126
|
export function presentError(error) {
|
|
123
127
|
return {
|
|
128
|
+
...diagnosticFields(error),
|
|
124
129
|
code: text(error?.code, 'WECOM_APP_ERROR', 80),
|
|
125
130
|
message: text(error?.message, '企业微信应用操作失败,请稍后重试'),
|
|
126
131
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
|
|
1
2
|
import { BotName } from '../../bot-alias.js';
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
|
|
@@ -159,7 +160,7 @@ function BindForm({ busy, error, onSubmit, onCancel }) {
|
|
|
159
160
|
disabled: busy,
|
|
160
161
|
}, streamEnabled ? '流式回复:开' : '流式回复:关'),
|
|
161
162
|
h('span', { className: 'dim-switchHint' }, '企业微信客户端实时出字;微信端不支持时自动改为整段发送'))),
|
|
162
|
-
error ? h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' }, h(
|
|
163
|
+
error ? h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' }, h(ConnectionError, { error: error })) : null,
|
|
163
164
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
164
165
|
h(Button, { kind: 'primary', onClick: () => formRef.current?.requestSubmit(), disabled: busy }, busy ? '正在绑定…' : '保存并连接'),
|
|
165
166
|
h(Button, { kind: 'quiet', onClick: onCancel, disabled: busy }, '取消'))));
|
|
@@ -327,6 +328,7 @@ export function AccountCard({
|
|
|
327
328
|
h(Button, { className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy) }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
328
329
|
h(Button, { className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy) }, '移除接入')),
|
|
329
330
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
331
|
+
account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
|
|
330
332
|
account.lastMessageError ? h(LastMessageErrorSummary, {
|
|
331
333
|
className: 'ddt-summary',
|
|
332
334
|
error: account.lastMessageError,
|
|
@@ -344,6 +346,7 @@ export function AccountCard({
|
|
|
344
346
|
}
|
|
345
347
|
|
|
346
348
|
export function WecomAppSettingsTab({ rpcCall }) {
|
|
349
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
347
350
|
const [model, setModel] = React.useState({
|
|
348
351
|
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
|
|
349
352
|
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
|
@@ -393,7 +396,16 @@ export function WecomAppSettingsTab({ rpcCall }) {
|
|
|
393
396
|
|
|
394
397
|
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
|
395
398
|
if (typeof rpcCall !== 'function') throw new TypeError('企业微信应用设置页缺少 RPC 连接');
|
|
396
|
-
|
|
399
|
+
const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
|
|
400
|
+
if (operation && mounted.current) setOperationError(null);
|
|
401
|
+
try {
|
|
402
|
+
const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
|
403
|
+
if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
|
|
404
|
+
return value;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
397
409
|
}, [rpcCall]);
|
|
398
410
|
|
|
399
411
|
const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
|
@@ -601,6 +613,7 @@ export function WecomAppSettingsTab({ rpcCall }) {
|
|
|
601
613
|
}, h(AgentPresetCatalogContext.Provider, {
|
|
602
614
|
value: model.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
|
|
603
615
|
}, h('section', { className: 'ddt-page dwecomapp-page dim-channelPage', 'aria-label': '企业微信应用设置' },
|
|
616
|
+
operationError ? h(ConnectionError, { error: operationError }) : null,
|
|
604
617
|
h(Heading, {
|
|
605
618
|
totals: model.totals,
|
|
606
619
|
adding: bindOpen,
|
|
@@ -611,7 +624,7 @@ export function WecomAppSettingsTab({ rpcCall }) {
|
|
|
611
624
|
h('div', { className: 'ddt-visuallyHidden', role: 'status', 'aria-live': 'polite' }, notice),
|
|
612
625
|
model.phase === 'loading' ? h(LoadingView)
|
|
613
626
|
: model.phase === 'error'
|
|
614
|
-
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信应用状态'), h(
|
|
627
|
+
? h('div', { className: 'ddt-card dim-surfaceCard' }, h('div', { className: 'ddt-inlineError dim-inlineError' }, h('h3', null, '无法读取企业微信应用状态'), h(ConnectionError, { error: model.error }), h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
|
615
628
|
: h(React.Fragment, null,
|
|
616
629
|
bindView,
|
|
617
630
|
model.bots.length === 0 && !bindOpen
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeConnectionError as normalizeTestError } from '../../connection-error.js';
|
|
1
2
|
import { normalizeWeixinDiagnosticDetails } from '../../../../src/channels/weixin/diagnostic-details.mjs';
|
|
2
3
|
import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
3
4
|
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
|
@@ -55,7 +56,7 @@ function normalizeTestMessage(value) {
|
|
|
55
56
|
const code = value.code === 'test-target-unavailable'
|
|
56
57
|
? 'test-target-unavailable'
|
|
57
58
|
: 'test-message-failed';
|
|
58
|
-
return { sent: false, code };
|
|
59
|
+
return { sent: false, code, ...(value.error ? { error: normalizeTestError(value.error) } : {}) };
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export function normalizeConnectionError(value, fallbackCode = 'WEIXIN_ERROR', fallbackMessage = '微信操作失败,请稍后重试') {
|
|
@@ -1,71 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { h, localizeText } from '../../i18n.js';
|
|
3
|
-
import { normalizeConnectionError } from './api.js';
|
|
4
|
-
import { CONFIG_ISSUE_LABELS } from '../../../../src/channels/weixin/diagnostic-details.mjs';
|
|
5
|
-
|
|
6
|
-
const STAGE_LABELS = {
|
|
7
|
-
'startup.load': '加载微信配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
|
|
8
|
-
'qr.verify': '提交配对码', 'qr.cancel': '取消绑定', 'credential.read': '读取登录凭据', 'credential.save': '保存登录凭据',
|
|
9
|
-
'credential.remove': '移除登录凭据', 'account.save': '保存账号配置', 'account.remove': '移除账号配置',
|
|
10
|
-
'state.load': '读取账号状态', 'state.write': '保存账号状态', 'state.cleanup': '清理账号状态',
|
|
11
|
-
'workspace.write': '保存工作区设置', 'workspace.cleanup': '清理工作区设置', 'runtime.prepare': '准备消息连接',
|
|
12
|
-
activation: '激活微信账号', 'harness.check': '检查 DSH 宿主', 'connection.start': '启动微信连接',
|
|
13
|
-
'connection.poll': '同步微信消息', 'connection.stop': '停止微信连接', 'status.read': '读取连接状态',
|
|
14
|
-
rollback: '恢复原账号状态', 'management.request': '访问 DSH 管理接口',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function provisioningErrorTitle(error) {
|
|
18
|
-
const stage = error?.details?.stage;
|
|
19
|
-
if (stage === 'qr.begin' || stage === 'qr.encode') return '无法生成微信二维码';
|
|
20
|
-
if (stage === 'qr.poll') return '查询微信扫码状态失败';
|
|
21
|
-
if (stage === 'management.request') return '无法完成微信管理请求';
|
|
22
|
-
return '微信没有绑定完成';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function formatWeixinDiagnostic(value) {
|
|
26
|
-
const error = normalizeConnectionError(value);
|
|
27
|
-
const details = error.details ?? {};
|
|
28
|
-
return [
|
|
29
|
-
localizeText(error.message), details.hint ? localizeText(details.hint) : null,
|
|
30
|
-
localizeText('错误码') + ': ' + error.code,
|
|
31
|
-
...['operation', 'stage', 'reason', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
32
|
-
.filter(field => details[field] !== undefined)
|
|
33
|
-
.map(field => `${field}: ${details[field]}`),
|
|
34
|
-
].filter(Boolean).join('\n');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function WeixinConnectionError({ error: value, warning = false }) {
|
|
38
|
-
const error = normalizeConnectionError(value);
|
|
39
|
-
const details = error.details ?? {};
|
|
40
|
-
const [copyState, setCopyState] = React.useState(null);
|
|
41
|
-
React.useEffect(() => setCopyState(null), [details.referenceId, error.code, error.message]);
|
|
42
|
-
const copy = async () => {
|
|
43
|
-
try {
|
|
44
|
-
if (typeof globalThis.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable');
|
|
45
|
-
await globalThis.navigator.clipboard.writeText(formatWeixinDiagnostic(error));
|
|
46
|
-
setCopyState('copied');
|
|
47
|
-
} catch { setCopyState('manual'); }
|
|
48
|
-
};
|
|
49
|
-
const fields = [
|
|
50
|
-
['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
|
|
51
|
-
['底层原因', details.reason], ['HTTP 状态', details.httpStatus], ['微信返回码', details.providerCode],
|
|
52
|
-
['配置文件', details.file], ['配置字段', details.field],
|
|
53
|
-
['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
|
|
54
|
-
['参考号', details.referenceId], ['发生时间', details.occurredAt], ['插件版本', details.pluginVersion],
|
|
55
|
-
].filter(([, text]) => text !== undefined && text !== '');
|
|
56
|
-
return h('div', { className: 'dxw-summary dim-cardSummary', 'data-weixin-diagnostic': true, role: warning ? 'status' : undefined },
|
|
57
|
-
h('p', null, error.message),
|
|
58
|
-
details.hint ? h('p', null, details.hint) : null,
|
|
59
|
-
h('details', { style: { marginTop: 8 } },
|
|
60
|
-
h('summary', { style: { cursor: 'pointer' } }, '诊断详情'),
|
|
61
|
-
h('dl', { style: { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)', gap: '4px 12px', margin: '10px 0' } },
|
|
62
|
-
...fields.flatMap(([label, text]) => [h('dt', { key: `${label}-label` }, label),
|
|
63
|
-
React.createElement('dd', { key: label, style: { margin: 0, overflowWrap: 'anywhere' } }, String(text))])),
|
|
64
|
-
!details.referenceId ? h('p', null, '未取得 Host 诊断参考号。') : null,
|
|
65
|
-
h('div', { className: 'dim-viewActions' },
|
|
66
|
-
h('button', { type: 'button', className: 'dxw-button', onClick: copy }, copyState === 'copied' ? '诊断信息已复制' : '复制诊断信息')),
|
|
67
|
-
copyState === 'manual' ? h('div', null,
|
|
68
|
-
h('p', null, '无法访问剪贴板,请选择并复制以下诊断信息。'),
|
|
69
|
-
React.createElement('textarea', { readOnly: true, value: formatWeixinDiagnostic(error), rows: 7,
|
|
70
|
-
'aria-label': localizeText('诊断信息'), style: { width: '100%', boxSizing: 'border-box', marginTop: 8 } })) : null));
|
|
71
|
-
}
|
|
1
|
+
export { ConnectionError as WeixinConnectionError, formatConnectionDiagnostic as formatWeixinDiagnostic, provisioningErrorTitle } from '../../connection-error.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { diagnosticFields } from '../../../../src/channels/shared/diagnostic-details.mjs';
|
|
1
2
|
import { normalizeBotAlias } from '../../../../src/channels/shared/bot-alias.mjs';
|
|
2
3
|
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
|
3
4
|
import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } from '../../model-setting.js';
|
|
@@ -52,6 +53,7 @@ export function unwrapRpcResult(result) {
|
|
|
52
53
|
if (!result.ok) {
|
|
53
54
|
const error = new Error(text(result.error?.message, 'WhatsApp 操作失败'));
|
|
54
55
|
error.code = text(result.error?.code, 'WHATSAPP_RPC_ERROR', 80);
|
|
56
|
+
Object.assign(error, diagnosticFields(result.error));
|
|
55
57
|
throw error;
|
|
56
58
|
}
|
|
57
59
|
return result.value;
|
|
@@ -79,6 +81,7 @@ export function normalizeProvisioning(value, now = Date.now()) {
|
|
|
79
81
|
if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
|
|
80
82
|
if (id(source.botId)) result.botId = id(source.botId);
|
|
81
83
|
if (isRecord(source.error)) result.error = {
|
|
84
|
+
...diagnosticFields(source.error),
|
|
82
85
|
code: text(source.error.code, 'WHATSAPP_PROVISION_FAILED', 80),
|
|
83
86
|
message: text(source.error.message, 'WhatsApp 没有接入完成'),
|
|
84
87
|
};
|
|
@@ -112,6 +115,7 @@ function normalizeBot(value) {
|
|
|
112
115
|
},
|
|
113
116
|
lastMessageError: normalizeLastMessageError(value.lastMessageError),
|
|
114
117
|
error: isRecord(value.error) ? {
|
|
118
|
+
...diagnosticFields(value.error),
|
|
115
119
|
code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
|
|
116
120
|
message: text(value.error.message, 'WhatsApp 连接尚未就绪'),
|
|
117
121
|
} : null,
|
|
@@ -136,6 +140,7 @@ export function normalizeSnapshot(value) {
|
|
|
136
140
|
|
|
137
141
|
export function presentError(error) {
|
|
138
142
|
return {
|
|
143
|
+
...diagnosticFields(error),
|
|
139
144
|
code: text(error?.code, 'WHATSAPP_ERROR', 80),
|
|
140
145
|
message: text(error?.message, 'WhatsApp 操作失败,请稍后重试'),
|
|
141
146
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConnectionError, normalizeConnectionError } from '../../connection-error.js';
|
|
1
2
|
import { BotName } from '../../bot-alias.js';
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
|
|
@@ -168,7 +169,7 @@ export function ProvisionView({ provision, busy, onRetry, onClose }) {
|
|
|
168
169
|
return h('div', { className: 'ddt-card dim-surfaceCard' },
|
|
169
170
|
h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
|
|
170
171
|
h('h3', null, 'WhatsApp 没有接入完成'),
|
|
171
|
-
h(
|
|
172
|
+
h(ConnectionError, { error: error }),
|
|
172
173
|
h('span', { className: 'ddt-errorCode' }, error.code),
|
|
173
174
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
174
175
|
h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
|
|
@@ -268,6 +269,7 @@ export function WhatsappAccountCard({
|
|
|
268
269
|
className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
|
|
269
270
|
}, '移除接入')),
|
|
270
271
|
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
272
|
+
account.error ? h(ConnectionError, { error: account.error, showMessage: false }) : null,
|
|
271
273
|
account.lastMessageError ? h(LastMessageErrorSummary, {
|
|
272
274
|
className: 'ddt-summary',
|
|
273
275
|
error: account.lastMessageError,
|
|
@@ -287,6 +289,7 @@ export function WhatsappAccountCard({
|
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
export function WhatsappSettingsTab({ rpcCall }) {
|
|
292
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
290
293
|
const [model, setModel] = React.useState({
|
|
291
294
|
phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
|
|
292
295
|
agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
|
|
@@ -315,7 +318,16 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
315
318
|
|
|
316
319
|
const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
|
|
317
320
|
if (typeof rpcCall !== 'function') throw new TypeError('WhatsApp 设置页缺少 RPC 连接');
|
|
318
|
-
|
|
321
|
+
const operation = !['connection.status', 'provision.poll', 'provision.begin', 'provision.cancel'].includes(endpoint);
|
|
322
|
+
if (operation && mounted.current) setOperationError(null);
|
|
323
|
+
try {
|
|
324
|
+
const value = unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
|
325
|
+
if (operation && mounted.current) setOperationError(value?.testMessage?.error ?? value?.warnings?.[0] ?? null);
|
|
326
|
+
return value;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (operation && mounted.current && !signal?.aborted && error?.name !== 'AbortError') setOperationError(normalizeConnectionError(error));
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
319
331
|
}, [rpcCall]);
|
|
320
332
|
|
|
321
333
|
const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
|
|
@@ -558,6 +570,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
558
570
|
className: 'ddt-page dwa-page dim-channelPage',
|
|
559
571
|
'aria-label': 'WhatsApp 设置',
|
|
560
572
|
},
|
|
573
|
+
operationError ? h(ConnectionError, { error: operationError }) : null,
|
|
561
574
|
h(Heading, {
|
|
562
575
|
totals: model.totals,
|
|
563
576
|
busy,
|
|
@@ -570,7 +583,7 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
570
583
|
? h('div', { className: 'ddt-card dim-surfaceCard' },
|
|
571
584
|
h('div', { className: 'ddt-inlineError dim-inlineError' },
|
|
572
585
|
h('h3', null, '无法读取 WhatsApp 机器人状态'),
|
|
573
|
-
h(
|
|
586
|
+
h(ConnectionError, { error: model.error }),
|
|
574
587
|
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
|
575
588
|
: h(React.Fragment, null,
|
|
576
589
|
provision?.status === 'pending'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { h, localizeText } from './i18n.js';
|
|
3
|
+
import { normalizeDiagnosticDetails } from '../../src/channels/shared/diagnostic-details.mjs';
|
|
4
|
+
import { CONFIG_ISSUE_LABELS } from '../../src/channels/shared/diagnostic-details.mjs';
|
|
5
|
+
|
|
6
|
+
const STAGE_LABELS = {
|
|
7
|
+
'credential.verify': '验证凭据', 'permission.check': '检查权限', 'sdk.load': '加载渠道组件',
|
|
8
|
+
operation: '执行渠道操作', 'message.receive': '接收消息', 'message.send': '发送消息', 'connection.test': '发送测试消息',
|
|
9
|
+
'startup.load': '加载渠道配置', 'qr.begin': '申请二维码', 'qr.encode': '生成二维码图片', 'qr.poll': '查询扫码状态',
|
|
10
|
+
'qr.verify': '提交配对码', 'qr.cancel': '取消绑定', 'credential.read': '读取登录凭据', 'credential.save': '保存登录凭据',
|
|
11
|
+
'credential.remove': '移除登录凭据', 'account.save': '保存账号配置', 'account.remove': '移除账号配置',
|
|
12
|
+
'state.load': '读取账号状态', 'state.write': '保存账号状态', 'state.cleanup': '清理账号状态',
|
|
13
|
+
'workspace.write': '保存工作区设置', 'workspace.cleanup': '清理工作区设置', 'runtime.prepare': '准备消息连接',
|
|
14
|
+
activation: '激活账号', 'harness.check': '检查 DSH 宿主', 'connection.start': '启动连接',
|
|
15
|
+
'connection.poll': '监测连接与消息', 'connection.stop': '停止连接', 'status.read': '读取连接状态',
|
|
16
|
+
rollback: '恢复原账号状态', 'management.request': '访问 DSH 管理接口',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function provisioningErrorTitle(error) {
|
|
20
|
+
const stage = error?.details?.stage;
|
|
21
|
+
if (stage === 'qr.begin' || stage === 'qr.encode') return '无法生成微信二维码';
|
|
22
|
+
if (stage === 'qr.poll') return '查询微信扫码状态失败';
|
|
23
|
+
if (stage === 'management.request') return '无法完成微信管理请求';
|
|
24
|
+
return '微信没有绑定完成';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatConnectionDiagnostic(value) {
|
|
28
|
+
const error = normalizeConnectionError(value);
|
|
29
|
+
const details = error.details ?? {};
|
|
30
|
+
return [
|
|
31
|
+
localizeText(error.message), details.hint ? localizeText(details.hint) : null,
|
|
32
|
+
localizeText('错误码') + ': ' + error.code,
|
|
33
|
+
...['channel', 'operation', 'stage', 'reason', 'reasons', 'host', 'durationMs', 'timeoutMs', 'nodeVersion', 'dshVersion', 'platform', 'dependencies', 'proxyConfigured', 'truncated', 'httpStatus', 'providerCode', 'resource', 'file', 'field', 'issue', 'referenceId', 'occurredAt', 'rollback', 'pluginVersion']
|
|
34
|
+
.filter(field => details[field] !== undefined)
|
|
35
|
+
.map(field => `${field}: ${typeof details[field] === 'object' ? JSON.stringify(details[field]) : details[field]}`),
|
|
36
|
+
].filter(Boolean).join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ConnectionError({ error: value, warning = false, showMessage = true }) {
|
|
40
|
+
const error = normalizeConnectionError(value);
|
|
41
|
+
const details = error.details ?? {};
|
|
42
|
+
const isWarning = warning || error.code.endsWith('cleanup-failed');
|
|
43
|
+
const [copyState, setCopyState] = React.useState(null);
|
|
44
|
+
React.useEffect(() => setCopyState(null), [details.referenceId, error.code, error.message]);
|
|
45
|
+
const copy = async () => {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof globalThis.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable');
|
|
48
|
+
await globalThis.navigator.clipboard.writeText(formatConnectionDiagnostic(error));
|
|
49
|
+
setCopyState('copied');
|
|
50
|
+
} catch { setCopyState('manual'); }
|
|
51
|
+
};
|
|
52
|
+
const fields = [
|
|
53
|
+
['错误码', error.code], ['失败阶段', localizeText(STAGE_LABELS[details.stage] ?? details.stage ?? '')],
|
|
54
|
+
['底层原因', details.reason === 'unknown' ? localizeText('暂未识别') : details.reason], ['多个原因', details.reasons?.join(', ')], ['服务域名', details.host], ['请求耗时(毫秒)', details.durationMs], ['超时阈值(毫秒)', details.timeoutMs], ['Node 版本', details.nodeVersion], ['DSH 版本', details.dshVersion], ['依赖版本', details.dependencies ? Object.entries(details.dependencies).map(([name, version]) => `${name}: ${version}`).join(', ') : undefined], ['代理已配置', details.proxyConfigured], ['原因链已截断', details.truncated], ['运行平台', details.platform], ['HTTP 状态', details.httpStatus], ['服务返回码', details.providerCode],
|
|
55
|
+
['配置文件', details.file], ['配置字段', details.field],
|
|
56
|
+
['校验原因', details.issue ? localizeText(CONFIG_ISSUE_LABELS[details.issue]) : undefined],
|
|
57
|
+
['参考号', details.referenceId], ['发生时间', details.occurredAt], ['插件版本', details.pluginVersion],
|
|
58
|
+
].filter(([, text]) => text !== undefined && text !== '');
|
|
59
|
+
return h('div', { className: 'dim-connectionDiagnostic', 'data-connection-diagnostic': true, 'data-weixin-diagnostic': details.channel === 'weixin' || /^WX-CONN/.test(details.referenceId ?? '') || undefined, role: isWarning ? 'status' : undefined, 'data-warning': isWarning || undefined },
|
|
60
|
+
showMessage ? h('p', null, error.message) : null,
|
|
61
|
+
details.hint ? h('p', null, details.hint) : null,
|
|
62
|
+
h('details', { style: { marginTop: 8 } },
|
|
63
|
+
h('summary', { style: { cursor: 'pointer' } }, '诊断详情'),
|
|
64
|
+
h('dl', { style: { display: 'grid', gridTemplateColumns: 'auto minmax(0, 1fr)', gap: '4px 12px', margin: '10px 0' } },
|
|
65
|
+
...fields.flatMap(([label, text]) => [h('dt', { key: `${label}-label` }, label),
|
|
66
|
+
React.createElement('dd', { key: label, style: { margin: 0, overflowWrap: 'anywhere' } }, String(text))])),
|
|
67
|
+
!details.referenceId ? h('p', null, '未取得 Host 诊断参考号。') : null,
|
|
68
|
+
h('div', { className: 'dim-viewActions' },
|
|
69
|
+
h('button', { type: 'button', className: 'dxw-button', onClick: copy }, copyState === 'copied' ? '诊断信息已复制' : '复制诊断信息')),
|
|
70
|
+
copyState === 'manual' ? h('div', null,
|
|
71
|
+
h('p', null, '无法访问剪贴板,请选择并复制以下诊断信息。'),
|
|
72
|
+
React.createElement('textarea', { readOnly: true, value: formatConnectionDiagnostic(error), rows: 7,
|
|
73
|
+
'aria-label': localizeText('诊断信息'), style: { width: '100%', boxSizing: 'border-box', marginTop: 8 } })) : null));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeConnectionError(value, fallbackCode = 'operation-failed', fallbackMessage = '操作失败,请查看原因和诊断详情。') {
|
|
77
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
78
|
+
const text = (v, fallback, n) => typeof v === 'string' && v.trim() ? v.trim().slice(0, n) : fallback;
|
|
79
|
+
const details = normalizeDiagnosticDetails({ ...source.details,
|
|
80
|
+
referenceId: source.details?.referenceId ?? source.referenceId,
|
|
81
|
+
hint: source.details?.hint ?? source.hint,
|
|
82
|
+
});
|
|
83
|
+
return { code: text(source.code, fallbackCode, 100), message: text(source.message, fallbackMessage, 500),
|
|
84
|
+
...(Object.keys(details).length ? { details } : {}),
|
|
85
|
+
...(source.hint ? { hint: details.hint } : {}), ...(source.referenceId ? { referenceId: details.referenceId } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
normalizeInboundTtlHours,
|
|
7
7
|
} from '../../src/channels/shared/inbound-ttl.mjs';
|
|
8
8
|
import { h } from './i18n.js';
|
|
9
|
+
import { DEFAULT_IMAGE_INPUT_SETTINGS, normalizeImageInputSettings } from '../../src/channels/shared/image-input-policy.mjs';
|
|
9
10
|
|
|
10
11
|
export const GLOBAL_SETTINGS_RPC_CHANNEL = '/dsh-im-settings';
|
|
11
12
|
|
|
@@ -18,6 +19,8 @@ export const GLOBAL_SETTINGS_ENDPOINTS = Object.freeze({
|
|
|
18
19
|
getTtl: 'settings.inbound-ttl.get',
|
|
19
20
|
setTtl: 'settings.inbound-ttl.set',
|
|
20
21
|
sweep: 'settings.inbound-ttl.sweep',
|
|
22
|
+
getImages: 'settings.image-input.get',
|
|
23
|
+
setImages: 'settings.image-input.set',
|
|
21
24
|
});
|
|
22
25
|
|
|
23
26
|
function presentError(error, fallback) {
|
|
@@ -56,6 +59,82 @@ function GlobalButton({ children, kind = 'secondary', className = '', ...props }
|
|
|
56
59
|
}, children);
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
export function ImageInputSettings({ rpcCall }) {
|
|
63
|
+
const [values, setValues] = React.useState(DEFAULT_IMAGE_INPUT_SETTINGS);
|
|
64
|
+
const [phase, setPhase] = React.useState('loading');
|
|
65
|
+
const [error, setError] = React.useState(null);
|
|
66
|
+
const [saved, setSaved] = React.useState(false);
|
|
67
|
+
const mounted = React.useRef(false);
|
|
68
|
+
const saving = React.useRef(false);
|
|
69
|
+
const id = React.useId();
|
|
70
|
+
const load = React.useCallback(async (signal) => {
|
|
71
|
+
setPhase('loading');
|
|
72
|
+
setError(null);
|
|
73
|
+
try {
|
|
74
|
+
const result = unwrapRpcResult(await rpcCall(GLOBAL_SETTINGS_ENDPOINTS.getImages, {}, signal));
|
|
75
|
+
if (signal?.aborted || !mounted.current) return;
|
|
76
|
+
const settings = normalizeImageInputSettings(result);
|
|
77
|
+
if (!settings) throw new Error('通用设置返回了无法识别的响应。');
|
|
78
|
+
setValues(settings);
|
|
79
|
+
setPhase('ready');
|
|
80
|
+
} catch (caught) {
|
|
81
|
+
if (signal?.aborted || !mounted.current) return;
|
|
82
|
+
setError(presentError(caught, '无法读取或保存图片设置,请稍后重试。'));
|
|
83
|
+
setPhase('error');
|
|
84
|
+
}
|
|
85
|
+
}, [rpcCall]);
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
mounted.current = true;
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
void load(controller.signal);
|
|
90
|
+
return () => { mounted.current = false; controller.abort(); };
|
|
91
|
+
}, [load]);
|
|
92
|
+
const save = async (event) => {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
if (saving.current || phase !== 'ready') return;
|
|
95
|
+
const settings = normalizeImageInputSettings(values);
|
|
96
|
+
setSaved(false);
|
|
97
|
+
if (!settings) {
|
|
98
|
+
setError('图片限制无效:请输入正整数,原图接收上限和总量上限不能小于单图上限。');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
saving.current = true;
|
|
102
|
+
setPhase('saving');
|
|
103
|
+
setError(null);
|
|
104
|
+
try {
|
|
105
|
+
const result = unwrapRpcResult(await rpcCall(GLOBAL_SETTINGS_ENDPOINTS.setImages, settings));
|
|
106
|
+
if (!normalizeImageInputSettings(result)) throw new Error('通用设置返回了无法识别的响应。');
|
|
107
|
+
if (mounted.current) { setValues(result); setSaved(true); }
|
|
108
|
+
} catch (caught) {
|
|
109
|
+
if (mounted.current) setError(presentError(caught, '无法读取或保存图片设置,请稍后重试。'));
|
|
110
|
+
} finally {
|
|
111
|
+
saving.current = false;
|
|
112
|
+
if (mounted.current) setPhase('ready');
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const fields = [
|
|
116
|
+
['maxDownloadMb', '原图接收上限 (MB)'],
|
|
117
|
+
['maxImageMb', '模型单图上限 (MB)'],
|
|
118
|
+
['maxTotalMb', '模型图片总量上限 (MB)'],
|
|
119
|
+
['maxImages', '每条消息最多图片数'],
|
|
120
|
+
];
|
|
121
|
+
return h('section', { className: 'dim-globalSection', 'aria-label': '图片输入', 'aria-busy': phase === 'loading' || phase === 'saving' },
|
|
122
|
+
h('h3', null, '图片输入'),
|
|
123
|
+
h('p', null, '适用于支持图片的聊天渠道。原图按附件保留时长保存,发送给模型的副本会自动缩放或压缩;无法直接发送时交给模型按文件处理。'),
|
|
124
|
+
h('form', { onSubmit: save },
|
|
125
|
+
...fields.map(([key, label]) => h('div', { className: 'dim-globalTtlRow', key },
|
|
126
|
+
h('label', { htmlFor: `${id}-${key}` }, label),
|
|
127
|
+
h('input', { id: `${id}-${key}`, className: 'dim-globalTtlInput', type: 'number', min: 1, step: 1,
|
|
128
|
+
value: values[key], disabled: phase !== 'ready',
|
|
129
|
+
onChange: (event) => { setValues((current) => ({ ...current, [key]: event.target.value })); setSaved(false); setError(null); },
|
|
130
|
+
}))),
|
|
131
|
+
h(GlobalButton, { type: 'submit', kind: 'primary', disabled: phase !== 'ready' }, phase === 'saving' ? '保存中…' : '保存图片设置'),
|
|
132
|
+
phase === 'error' ? h(GlobalButton, { onClick: () => void load() }, '重试') : null),
|
|
133
|
+
error ? h('p', { role: 'alert' }, error) : null,
|
|
134
|
+
saved ? h('p', { role: 'status' }, '已保存') : null,
|
|
135
|
+
phase === 'loading' ? h('p', { role: 'status' }, '正在读取图片设置…') : null);
|
|
136
|
+
}
|
|
137
|
+
|
|
59
138
|
export function GlobalSettingsPanel({ rpcCall }) {
|
|
60
139
|
const [phase, setPhase] = React.useState('loading');
|
|
61
140
|
const [loadError, setLoadError] = React.useState(null);
|
|
@@ -337,5 +416,6 @@ export function GlobalSettingsPanel({ rpcCall }) {
|
|
|
337
416
|
role: inlineStatus.role,
|
|
338
417
|
'aria-live': inlineStatus.role === 'status' ? 'polite' : undefined,
|
|
339
418
|
}, inlineStatus.message)
|
|
340
|
-
: null))
|
|
419
|
+
: null)),
|
|
420
|
+
h(ImageInputSettings, { rpcCall })));
|
|
341
421
|
}
|