@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
|
@@ -221,6 +221,7 @@ const EN = Object.freeze({
|
|
|
221
221
|
'开启(自动开启独立飞书话题)': 'On (auto-open a dedicated Feishu topic)',
|
|
222
222
|
'开启后,群聊中向机器人提问会自动开启独立飞书话题,回复落在话题内;每个话题是 dsh 会话列表里一条独立会话,上下文互不串。私聊不受影响。': 'When enabled, questions to the bot in a group auto-open a dedicated Feishu topic and replies stay inside it. Each topic is an independent conversation in dsh with its own context. Private chats are unaffected.',
|
|
223
223
|
'分步直推': 'Step push',
|
|
224
|
+
'查看分步直推说明': 'View step push help',
|
|
224
225
|
'开启后逐步推送工具调用与过程说明': 'Push tool calls and process notes step by step',
|
|
225
226
|
'关闭(保持流式卡模式)': 'Off (keep the streaming card)',
|
|
226
227
|
'开启(逐步推送工具调用与过程说明)': 'On (push tool calls and process notes step by step)',
|
|
@@ -359,6 +360,14 @@ const EN = Object.freeze({
|
|
|
359
360
|
'Agent Preset 无效。': 'The Agent Preset is invalid.',
|
|
360
361
|
'Agent Preset 不存在或不可用。': 'The Agent Preset does not exist or is unavailable.',
|
|
361
362
|
'模型': 'Model',
|
|
363
|
+
'模型与思考强度': 'Model and reasoning effort',
|
|
364
|
+
'思考强度': 'Reasoning effort',
|
|
365
|
+
'跟随模型默认': 'Model default',
|
|
366
|
+
'先选择模型,再设置思考强度。': 'Select a model to configure its reasoning effort.',
|
|
367
|
+
'该模型未提供可调节的思考强度。': 'This model does not provide adjustable reasoning effort.',
|
|
368
|
+
'已保存的思考强度已不可用,请选择其他强度或恢复默认。': 'The saved reasoning effort is unavailable. Choose another effort or restore the default.',
|
|
369
|
+
'使用模型或服务提供方的默认思考强度。': 'Use the default reasoning effort from the model or provider.',
|
|
370
|
+
'当前模型不支持所选思考强度,请重新选择。': 'This model does not support the selected reasoning effort. Choose another effort.',
|
|
362
371
|
'查看模型设置说明': 'View model setting help',
|
|
363
372
|
'跟随默认模型': 'Follow the default model',
|
|
364
373
|
'模型修改失败,请重试。': 'Could not update the model. Try again.',
|
|
@@ -459,7 +468,7 @@ const EN = Object.freeze({
|
|
|
459
468
|
'权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
|
|
460
469
|
'正在为现有飞书应用申请群消息权限二维码,请稍候。': 'Requesting a group-message permission QR code for the existing Feishu app…',
|
|
461
470
|
'群消息权限没有开通完成': 'Group-message permission was not granted',
|
|
462
|
-
'扫码会更新现有飞书应用,增量补充当前缺少的卡片按钮回调、读取用户消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”)、上传机器人图片或文件所需的 im:resource,以及原生命令面板所需的 application:app_slash_command:read / write;不会创建新应用。确认页只显示当前缺少项,完成后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with the missing card-button callback, im:message:readonly for reading images or files in user messages (shown by Feishu as “Read direct and group messages”), im:resource for uploading images or files sent by the bot, and application:app_slash_command:read / write for the native command panel. It does not create a new app. The confirmation page shows only missing items; this bot reconnects briefly afterward, while other bots are unaffected.',
|
|
471
|
+
'扫码会更新现有飞书应用,增量补充当前缺少的卡片按钮回调、读取用户消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”)、上传机器人图片或文件所需的 im:resource、接收群内其他机器人 @ 当前机器人所需的 im:message.group_at_msg.include_bot:readonly,以及原生命令面板所需的 application:app_slash_command:read / write;不会创建新应用。确认页只显示当前缺少项,完成后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with the missing card-button callback, im:message:readonly for reading images or files in user messages (shown by Feishu as “Read direct and group messages”), im:resource for uploading images or files sent by the bot, im:message.group_at_msg.include_bot:readonly for receiving group messages from other bots that mention this bot, and application:app_slash_command:read / write for the native command panel. It does not create a new app. The confirmation page shows only missing items; this bot reconnects briefly afterward, while other bots are unaffected.',
|
|
463
472
|
'核对现有应用名称,并确认只新增当前缺少的上述配置': 'Review the existing app name and confirm that only the missing items described above are added',
|
|
464
473
|
'保持本页打开,等待权限与回调补全完成': 'Keep this page open until permissions and the callback are complete',
|
|
465
474
|
'取消补全': 'Cancel setup',
|
|
@@ -891,6 +900,60 @@ const EN = Object.freeze({
|
|
|
891
900
|
'确认清理当前已过期的附件?': 'Clean up the attachments that are currently expired?',
|
|
892
901
|
'确认清理': 'Confirm cleanup',
|
|
893
902
|
'正在清理…': 'Cleaning up…',
|
|
903
|
+
'正在读取企业微信应用状态…': 'Loading WeCom app status…',
|
|
904
|
+
'添加企业微信应用': 'Add WeCom app',
|
|
905
|
+
'收起表单': 'Collapse form',
|
|
906
|
+
'添加企业微信自建应用': 'Add a WeCom self-built app',
|
|
907
|
+
'正在填写': 'Filling in details',
|
|
908
|
+
'尚未绑定企业微信应用': 'No WeCom app connected yet',
|
|
909
|
+
'在企业微信中创建自建应用,即可在微信里使用': 'Create a self-built app in WeCom to use it inside WeChat',
|
|
910
|
+
'在应用管理后台创建自建应用,开启「接收消息」并填入下面的参数。成员的微信关注该企业的微信插件后,就能在微信中与本应用双向对话。': 'Create the self-built app in the WeCom admin console, enable receive-messages, and fill in the parameters below. Once members follow the enterprise WeChat plugin, they can chat with this app directly inside WeChat.',
|
|
911
|
+
'填写应用参数': 'Fill in app parameters',
|
|
912
|
+
'企业 ID(CorpID)': 'Corp ID',
|
|
913
|
+
'例如 ww1234567890abcdef': 'for example ww1234567890abcdef',
|
|
914
|
+
'应用 AgentId': 'AgentId',
|
|
915
|
+
'例如 1000002': 'for example 1000002',
|
|
916
|
+
'应用 Secret': 'App Secret',
|
|
917
|
+
'应用详情页的 Secret': 'The Secret from the app details page',
|
|
918
|
+
'回调 Token': 'Callback Token',
|
|
919
|
+
'接收消息 → API 接收中的 Token': 'The Token from Receive messages → API receive',
|
|
920
|
+
'43 位字母或数字': '43 alphanumeric characters',
|
|
921
|
+
'代理地址(可选)': 'API proxy (optional)',
|
|
922
|
+
'留空直连 qyapi.weixin.qq.com': 'leave empty to reach qyapi.weixin.qq.com directly',
|
|
923
|
+
'公网回调基址(可选)': 'Public callback base URL (optional)',
|
|
924
|
+
'例如 https://im.example.com': 'for example https://im.example.com',
|
|
925
|
+
'流式回复:开': 'Streaming replies: on',
|
|
926
|
+
'流式回复:关': 'Streaming replies: off',
|
|
927
|
+
'企业微信客户端实时出字;微信端不支持时自动改为整段发送': 'Streams replies in the WeCom client; falls back to segmented text messages when the WeChat side does not support it',
|
|
928
|
+
'绑定企业微信自建应用': 'Bind a WeCom self-built app',
|
|
929
|
+
'回调 URL(填入企业微信后台「接收消息 → 设置API接收」)': 'Callback URL (paste into the WeCom console under Receive messages → API receive)',
|
|
930
|
+
'未配置公网回调基址:请在本页「设置」中填写回调基址,或手动拼接 回调基址 + 路径': 'No public callback base URL configured: fill it in under Settings on this page, or manually combine the base URL and the path',
|
|
931
|
+
'重置密钥': 'Reset secret',
|
|
932
|
+
'重置中…': 'Resetting…',
|
|
933
|
+
'重置密钥后回调 URL 会变化,需要同步更新企业微信后台。若公网反代未就绪,可临时使用 http://服务器IP:端口 形式的回调地址。': 'Resetting the secret changes the callback URL; update the WeCom console accordingly. If the public reverse proxy is not ready yet, a http://server-ip:port callback address works temporarily.',
|
|
934
|
+
'保存设置': 'Save settings',
|
|
935
|
+
'这会删除本机保存的应用凭据、回调配置及会话映射。企业微信后台的应用不会被删除。': 'This removes the locally stored app credentials, callback configuration, and session mappings. The app in the WeCom console is not deleted.',
|
|
936
|
+
'保留应用': 'Keep app',
|
|
937
|
+
'已绑定的企业微信应用': 'Connected WeCom apps',
|
|
938
|
+
'HTTP 回调通道': 'HTTP callback channel',
|
|
939
|
+
'企业微信应用': 'WeCom app',
|
|
940
|
+
'企业 ID 已保存': 'Corp ID saved securely',
|
|
941
|
+
'企业微信应用回调通道就绪': 'WeCom app callback channel is ready',
|
|
942
|
+
'企业微信应用连接尚未就绪': 'WeCom app connection is not ready',
|
|
943
|
+
'企业微信应用仍未就绪,插件会继续自动重试。': 'The WeCom app is still not ready; the plugin keeps retrying automatically.',
|
|
944
|
+
'连接检查完成。': 'Connection check finished.',
|
|
945
|
+
'连接检查完成,测试消息已发送。': 'Connection check finished; the test message was sent.',
|
|
946
|
+
'连接检查完成。应用尚未收到可用于测试的私聊消息。': 'Connection check finished. The app has not received a private chat usable for testing yet.',
|
|
947
|
+
'企业微信应用已绑定。请把回调 URL 填入企业微信后台并保存。': 'The WeCom app is bound. Paste the callback URL into the WeCom console and save.',
|
|
948
|
+
'回调密钥已重置,请把新的回调 URL 更新到企业微信后台。': 'The callback secret was reset; update the new callback URL in the WeCom console.',
|
|
949
|
+
'回调密钥重置失败,请稍后重试。': 'The callback secret reset failed; try again later.',
|
|
950
|
+
'无法读取企业微信应用状态': 'Could not load WeCom app status',
|
|
951
|
+
'企业微信应用设置': 'WeCom app settings',
|
|
952
|
+
'企业微信应用设置页缺少 RPC 连接': 'The WeCom app settings page is missing the RPC connection',
|
|
953
|
+
'企业微信应用操作失败': 'The WeCom app operation failed',
|
|
954
|
+
'企业微信应用操作失败,请稍后重试': 'The WeCom app operation failed; try again later',
|
|
955
|
+
'企业微信应用服务返回了无法识别的响应': 'The WeCom app service returned an unrecognized response',
|
|
956
|
+
'企业微信应用服务没有返回有效的机器人列表': 'The WeCom app service did not return a valid bot list',
|
|
894
957
|
});
|
|
895
958
|
|
|
896
959
|
export const en = EN;
|
|
@@ -36,6 +36,9 @@ import { installTelegramStyles } from './channels/telegram/styles.js';
|
|
|
36
36
|
import { WECOM_RPC_CHANNEL } from './channels/wecom/api.js';
|
|
37
37
|
import { WecomSettingsTab } from './channels/wecom/index.js';
|
|
38
38
|
import { installWecomStyles } from './channels/wecom/styles.js';
|
|
39
|
+
import { WECOM_APP_RPC_CHANNEL } from './channels/wecom-app/api.js';
|
|
40
|
+
import { WecomAppSettingsTab } from './channels/wecom-app/index.js';
|
|
41
|
+
import { installWecomAppStyles } from './channels/wecom-app/styles.js';
|
|
39
42
|
import { WeixinSettingsTab } from './channels/weixin/index.js';
|
|
40
43
|
import { WEIXIN_RPC_CHANNEL } from './channels/weixin/api.js';
|
|
41
44
|
import { installWeixinStyles } from './channels/weixin/styles.js';
|
|
@@ -59,6 +62,7 @@ import {
|
|
|
59
62
|
replacePageLocation,
|
|
60
63
|
} from './loopback-recovery.js';
|
|
61
64
|
import { installImStyles } from './styles.js';
|
|
65
|
+
import { installSessionChannelLogos } from './session-channel-logos.js';
|
|
62
66
|
import { UpdatePanel, UPDATE_RPC_CHANNEL } from './update-panel.js';
|
|
63
67
|
import { WorkspaceDirectoryPickerContext } from './workspace-editor.js';
|
|
64
68
|
|
|
@@ -86,6 +90,7 @@ const CHANNELS = Object.freeze([
|
|
|
86
90
|
{ id: 'telegram', label: 'Telegram' },
|
|
87
91
|
{ id: 'discord', label: 'Discord' },
|
|
88
92
|
{ id: 'whatsapp', label: 'WhatsApp' },
|
|
93
|
+
{ id: 'wecomApp', label: '企业微信应用', note: '(实验功能)' },
|
|
89
94
|
{ id: 'office', label: 'AI Office', note: '(实验功能)' },
|
|
90
95
|
]);
|
|
91
96
|
|
|
@@ -112,6 +117,10 @@ function WecomLogo() {
|
|
|
112
117
|
return h('span', { className: 'dim-logo dim-logoWecom', 'aria-hidden': 'true' }, h(WecomLogoGlyph));
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
function WecomAppLogo() {
|
|
121
|
+
return h('span', { className: 'dim-logo dim-logoWecomApp', 'aria-hidden': 'true' }, h(WecomLogoGlyph));
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
function TelegramLogo() {
|
|
116
125
|
return h('span', { className: 'dim-logo dim-logoTelegram', 'aria-hidden': 'true' },
|
|
117
126
|
h(TelegramLogoGlyph));
|
|
@@ -142,6 +151,7 @@ function ChannelLogo({ channel }) {
|
|
|
142
151
|
if (channel === 'feishu') return h(FeishuLogo);
|
|
143
152
|
if (channel === 'dingtalk') return h(DingtalkLogo);
|
|
144
153
|
if (channel === 'wecom') return h(WecomLogo);
|
|
154
|
+
if (channel === 'wecomApp') return h(WecomAppLogo);
|
|
145
155
|
if (channel === 'qq') return h(QqLogo);
|
|
146
156
|
if (channel === 'slack') return h(SlackLogo);
|
|
147
157
|
if (channel === 'telegram') return h(TelegramLogo);
|
|
@@ -174,6 +184,7 @@ export function IMSettingsTab({
|
|
|
174
184
|
slackRpcCall,
|
|
175
185
|
telegramRpcCall,
|
|
176
186
|
wecomRpcCall,
|
|
187
|
+
wecomAppRpcCall,
|
|
177
188
|
weixinRpcCall,
|
|
178
189
|
whatsappRpcCall,
|
|
179
190
|
officeRpcCall,
|
|
@@ -212,6 +223,7 @@ export function IMSettingsTab({
|
|
|
212
223
|
slackRpcCall,
|
|
213
224
|
telegramRpcCall,
|
|
214
225
|
wecomRpcCall,
|
|
226
|
+
wecomAppRpcCall,
|
|
215
227
|
weixinRpcCall,
|
|
216
228
|
whatsappRpcCall,
|
|
217
229
|
officeRpcCall,
|
|
@@ -235,6 +247,7 @@ export function IMSettingsTab({
|
|
|
235
247
|
telegramRpcCall,
|
|
236
248
|
updateRpcCall,
|
|
237
249
|
wecomRpcCall,
|
|
250
|
+
wecomAppRpcCall,
|
|
238
251
|
weixinRpcCall,
|
|
239
252
|
whatsappRpcCall,
|
|
240
253
|
]);
|
|
@@ -343,6 +356,8 @@ export function IMSettingsTab({
|
|
|
343
356
|
? h(DingtalkSettingsTab, { rpcCall: rpcCalls.dingtalkRpcCall })
|
|
344
357
|
: active.id === 'wecom'
|
|
345
358
|
? h(WecomSettingsTab, { rpcCall: rpcCalls.wecomRpcCall })
|
|
359
|
+
: active.id === 'wecomApp'
|
|
360
|
+
? h(WecomAppSettingsTab, { rpcCall: rpcCalls.wecomAppRpcCall })
|
|
346
361
|
: active.id === 'qq'
|
|
347
362
|
? h(QqSettingsTab, { rpcCall: rpcCalls.qqRpcCall })
|
|
348
363
|
: active.id === 'slack'
|
|
@@ -366,11 +381,14 @@ export function apply(ctx) {
|
|
|
366
381
|
const t = ctx.locale.bind(IM_LOCALE_NAMESPACE);
|
|
367
382
|
setImTranslator(t);
|
|
368
383
|
|
|
384
|
+
ctx.effect(() => installSessionChannelLogos(), 'im-settings: Session channel logos');
|
|
385
|
+
|
|
369
386
|
ctx.effect(() => {
|
|
370
387
|
const disposers = [
|
|
371
388
|
installFeishuStyles(),
|
|
372
389
|
installWeixinStyles(),
|
|
373
390
|
installWecomStyles(),
|
|
391
|
+
installWecomAppStyles(),
|
|
374
392
|
installQqStyles(),
|
|
375
393
|
installSlackStyles(),
|
|
376
394
|
installTelegramStyles(),
|
|
@@ -394,6 +412,8 @@ export function apply(ctx) {
|
|
|
394
412
|
ctx.connection.rpc.call(QQ_RPC_CHANNEL, endpoint, payload, signal);
|
|
395
413
|
const wecomRpcCall = (endpoint, payload, signal) =>
|
|
396
414
|
ctx.connection.rpc.call(WECOM_RPC_CHANNEL, endpoint, payload, signal);
|
|
415
|
+
const wecomAppRpcCall = (endpoint, payload, signal) =>
|
|
416
|
+
ctx.connection.rpc.call(WECOM_APP_RPC_CHANNEL, endpoint, payload, signal);
|
|
397
417
|
const telegramRpcCall = (endpoint, payload, signal) =>
|
|
398
418
|
ctx.connection.rpc.call(TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
|
|
399
419
|
const discordRpcCall = (endpoint, payload, signal) =>
|
|
@@ -430,6 +450,7 @@ export function apply(ctx) {
|
|
|
430
450
|
slackRpcCall,
|
|
431
451
|
telegramRpcCall,
|
|
432
452
|
wecomRpcCall,
|
|
453
|
+
wecomAppRpcCall,
|
|
433
454
|
weixinRpcCall,
|
|
434
455
|
whatsappRpcCall,
|
|
435
456
|
officeRpcCall,
|
|
@@ -2,103 +2,169 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
EMPTY_MODEL_CATALOG,
|
|
5
|
+
modelCatalogEntry,
|
|
5
6
|
modelSelectionId,
|
|
6
7
|
normalizeModelCatalog,
|
|
7
8
|
normalizeModelSelection,
|
|
9
|
+
sameModelSelection,
|
|
8
10
|
} from '../../src/channels/shared/model-setting.mjs';
|
|
9
|
-
import { h } from './i18n.js';
|
|
11
|
+
import { h, localizeText } from './i18n.js';
|
|
10
12
|
|
|
11
13
|
export const SET_MODEL_ENDPOINT = 'bot.model.set';
|
|
12
14
|
export { EMPTY_MODEL_CATALOG, normalizeModelCatalog, normalizeModelSelection };
|
|
13
15
|
|
|
14
16
|
export const ModelCatalogContext = React.createContext(EMPTY_MODEL_CATALOG);
|
|
15
17
|
|
|
16
|
-
function
|
|
17
|
-
return
|
|
18
|
+
function chevron(open = false) {
|
|
19
|
+
return h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', 'aria-hidden': true,
|
|
20
|
+
className: 'dim-modelChevron', style: open ? { transform: 'rotate(90deg)' } : undefined },
|
|
21
|
+
h('path', { d: 'm6 4 4 4-4 4', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5 }));
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export function ModelEditor({ model = null, disabled = false, onSave }) {
|
|
21
|
-
const catalog = normalizeModelCatalog(
|
|
22
|
-
|
|
23
|
-
);
|
|
24
|
-
const
|
|
25
|
-
const
|
|
25
|
+
const catalog = normalizeModelCatalog(React.useContext(ModelCatalogContext));
|
|
26
|
+
const id = React.useId();
|
|
27
|
+
const rootRef = React.useRef(null);
|
|
28
|
+
const menuRef = React.useRef(null);
|
|
29
|
+
const triggerRef = React.useRef(null);
|
|
30
|
+
const restoreFocusRef = React.useRef(false);
|
|
31
|
+
const savingRef = React.useRef(false);
|
|
32
|
+
const [pane, setPane] = React.useState(null);
|
|
26
33
|
const [saving, setSaving] = React.useState(false);
|
|
27
34
|
const [error, setError] = React.useState(null);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const current = normalizeModelSelection(model);
|
|
36
|
+
const entry = modelCatalogEntry(catalog, current);
|
|
37
|
+
const currentAvailable = !current || Boolean(entry);
|
|
38
|
+
const reasoning = entry?.reasoning;
|
|
39
|
+
const effort = current?.reasoningEffort;
|
|
40
|
+
const effectiveEffort = effort ?? reasoning?.defaultEffort;
|
|
41
|
+
const effortEntry = reasoning?.efforts.find((level) => level.id === effectiveEffort);
|
|
42
|
+
const effortUnavailable = effort !== undefined
|
|
43
|
+
&& !reasoning?.efforts.some((level) => level.id === effort);
|
|
44
|
+
const defaultName = reasoning?.efforts.find((level) => level.id === reasoning.defaultEffort)?.name;
|
|
45
|
+
const defaultLabel = localizeText('跟随模型默认') + (defaultName ? ` · ${defaultName}` : '');
|
|
46
|
+
const effortLabel = !current ? localizeText('跟随默认模型')
|
|
47
|
+
: effort === undefined ? defaultLabel : effortEntry?.name ?? effort;
|
|
48
|
+
const effortHint = !current ? '先选择模型,再设置思考强度。'
|
|
49
|
+
: !currentAvailable ? null
|
|
50
|
+
: effortUnavailable ? '已保存的思考强度已不可用,请选择其他强度或恢复默认。'
|
|
51
|
+
: !reasoning ? '该模型未提供可调节的思考强度。' : null;
|
|
52
|
+
const effortDisabled = !current || !currentAvailable || (!reasoning && effort === undefined);
|
|
33
53
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
const close = (restoreFocus = false) => {
|
|
55
|
+
restoreFocusRef.current = restoreFocus;
|
|
56
|
+
setPane(null);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
// A successful RPC also unlocks the card in its parent. Wait for that
|
|
61
|
+
// commit before focusing the trigger, which cannot be focused disabled.
|
|
62
|
+
if (!pane && !saving && !disabled && restoreFocusRef.current) {
|
|
63
|
+
restoreFocusRef.current = false;
|
|
64
|
+
triggerRef.current?.focus();
|
|
39
65
|
}
|
|
40
|
-
}
|
|
41
|
-
|
|
66
|
+
}, [pane, saving, disabled]);
|
|
67
|
+
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!pane) return undefined;
|
|
70
|
+
const options = menuRef.current?.querySelectorAll('[role="menuitemradio"]');
|
|
71
|
+
const selected = menuRef.current?.querySelector('[aria-checked="true"]');
|
|
72
|
+
(selected ?? options?.[0])?.focus();
|
|
73
|
+
const outside = (event) => {
|
|
74
|
+
if (!rootRef.current?.contains(event.target)) setPane(null);
|
|
75
|
+
};
|
|
76
|
+
globalThis.document?.addEventListener('mousedown', outside);
|
|
77
|
+
return () => globalThis.document?.removeEventListener('mousedown', outside);
|
|
78
|
+
}, [pane]);
|
|
42
79
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
if (nextValue && !next) return;
|
|
80
|
+
const save = async (next) => {
|
|
81
|
+
if (savingRef.current || disabled) return;
|
|
82
|
+
if (sameModelSelection(current, next)) { close(true); return; }
|
|
83
|
+
savingRef.current = true;
|
|
48
84
|
setSaving(true);
|
|
49
85
|
setError(null);
|
|
50
86
|
try {
|
|
51
87
|
await onSave?.(next);
|
|
88
|
+
close(true);
|
|
52
89
|
} catch (cause) {
|
|
53
90
|
setError(cause?.message ?? '模型修改失败,请重试。');
|
|
54
91
|
} finally {
|
|
92
|
+
savingRef.current = false;
|
|
55
93
|
setSaving(false);
|
|
56
94
|
}
|
|
57
95
|
};
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
const option = (key, label, description, selected, next) => h('button', {
|
|
98
|
+
key, type: 'button', role: 'menuitemradio', 'aria-checked': selected,
|
|
99
|
+
className: 'dim-modelOption', disabled: disabled || saving,
|
|
100
|
+
onClick: () => { void save(next); },
|
|
101
|
+
},
|
|
102
|
+
h('span', { className: 'dim-modelOptionCopy' },
|
|
103
|
+
h('span', { className: 'dim-modelOptionName' }, label),
|
|
104
|
+
description ? h('span', { className: 'dim-modelDescription' }, description) : null),
|
|
105
|
+
h('span', { className: 'dim-modelCheck', 'aria-hidden': true }, selected ? '✓' : ''));
|
|
106
|
+
|
|
107
|
+
const row = (key, label, value, blocked = false) => h('button', {
|
|
108
|
+
type: 'button', className: 'dim-modelRow', disabled: disabled || saving || blocked,
|
|
109
|
+
'aria-label': label, 'aria-haspopup': 'menu', 'aria-expanded': pane === key,
|
|
110
|
+
'aria-controls': pane === key ? `${id}-menu` : undefined,
|
|
111
|
+
'aria-describedby': key === 'effort' && effortHint ? `${id}-hint` : undefined,
|
|
112
|
+
onClick: (event) => {
|
|
113
|
+
triggerRef.current = event.currentTarget;
|
|
114
|
+
setPane(pane === key ? null : key);
|
|
115
|
+
},
|
|
116
|
+
}, h('span', { className: 'dim-modelRowLabel' }, label),
|
|
117
|
+
h('span', { className: 'dim-modelValue', title: value }, value), chevron(pane === key));
|
|
118
|
+
|
|
119
|
+
return h('div', {
|
|
120
|
+
ref: rootRef, className: 'dim-preset dim-modelSetting',
|
|
121
|
+
onBlur: (event) => {
|
|
122
|
+
if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) close();
|
|
123
|
+
},
|
|
124
|
+
onKeyDown: (event) => {
|
|
125
|
+
if (!pane) return;
|
|
126
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(true); }
|
|
127
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
128
|
+
const items = [...(menuRef.current?.querySelectorAll('[role="menuitemradio"]:not(:disabled)') ?? [])];
|
|
129
|
+
if (!items.length) return;
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
const at = items.indexOf(globalThis.document?.activeElement);
|
|
132
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1
|
|
133
|
+
: (at + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
134
|
+
items[next]?.focus();
|
|
82
135
|
},
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
136
|
+
},
|
|
137
|
+
h('div', { className: 'dim-presetHeader' },
|
|
138
|
+
h('span', { className: 'dim-presetTitle' }, '模型与思考强度',
|
|
139
|
+
h('span', { className: 'dim-presetHelp' },
|
|
140
|
+
h('button', { type: 'button', className: 'dim-presetHelpButton',
|
|
141
|
+
'aria-label': '查看模型设置说明', 'aria-describedby': `${id}-help` },
|
|
142
|
+
h('span', { 'aria-hidden': true }, '?')),
|
|
143
|
+
h('span', { id: `${id}-help`, className: 'dim-presetTooltip', role: 'tooltip' },
|
|
144
|
+
'只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。'))),
|
|
145
|
+
saving ? h('span', { className: 'dim-presetStatus', role: 'status' }, '保存中…') : null),
|
|
146
|
+
row('model', '模型', entry?.name ?? (current ? modelSelectionId(current) : localizeText('跟随默认模型'))),
|
|
147
|
+
row('effort', '思考强度', effortLabel, effortDisabled),
|
|
148
|
+
pane ? h('div', { ref: menuRef, id: `${id}-menu`, role: 'menu',
|
|
149
|
+
'aria-label': pane === 'model' ? '模型' : '思考强度', 'aria-busy': saving,
|
|
150
|
+
className: 'dim-modelMenu' },
|
|
151
|
+
pane === 'model' ? [
|
|
152
|
+
option('default', '跟随默认模型', null, !current, null),
|
|
153
|
+
...catalog.groups.map((group) => h('section', { key: group.id, role: 'group', 'aria-label': group.name },
|
|
154
|
+
h('div', { className: 'dim-modelGroupTitle' }, group.name),
|
|
155
|
+
...group.models.map((choice) => {
|
|
156
|
+
const selected = current?.provider === group.id && current?.model === choice.id;
|
|
157
|
+
return option(choice.id, choice.name, choice.description ?? `${group.id}/${choice.id}`,
|
|
158
|
+
selected, selected ? current : { provider: group.id, model: choice.id });
|
|
159
|
+
}))),
|
|
160
|
+
] : [
|
|
161
|
+
option('provider-default', defaultLabel, '使用模型或服务提供方的默认思考强度。', effort === undefined,
|
|
162
|
+
current ? { provider: current.provider, model: current.model } : null),
|
|
163
|
+
...(reasoning?.efforts ?? []).map((level) => option(`effort:${level.id}`, level.name, level.description,
|
|
164
|
+
effort === level.id, { ...current, reasoningEffort: level.id })),
|
|
165
|
+
]) : null,
|
|
166
|
+
effortHint ? h('p', { id: `${id}-hint`, className: 'dim-modelHint',
|
|
167
|
+
role: effortUnavailable ? 'status' : undefined }, effortHint) : null,
|
|
168
|
+
error || !currentAvailable ? h('p', { className: 'dim-presetError', role: error ? 'alert' : 'status' },
|
|
169
|
+
error ?? '当前模型已不可用,请选择其他模型或跟随默认模型。') : null);
|
|
104
170
|
}
|