@xmanrui/dsh-im 1.0.2 → 1.1.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 +18 -0
- package/README.md +18 -0
- package/assets/logo-dsh-im-chinese-readme-3x2.png +0 -0
- package/assets/logo_cn.png +0 -0
- package/lib/client.js +603 -560
- package/lib/index.js +163 -163
- package/package.json +1 -1
- package/plugin-src/client/agent-preset.js +15 -6
- package/plugin-src/client/channel-card-meta.js +48 -0
- package/plugin-src/client/channels/dingtalk/index.js +25 -19
- package/plugin-src/client/channels/dingtalk/styles.js +0 -6
- package/plugin-src/client/channels/feishu/index.js +41 -35
- package/plugin-src/client/channels/feishu/styles.js +0 -5
- package/plugin-src/client/channels/qq/index.js +24 -16
- package/plugin-src/client/channels/shared/token-channel.js +32 -24
- package/plugin-src/client/channels/wecom/index.js +24 -16
- package/plugin-src/client/channels/weixin/index.js +29 -23
- package/plugin-src/client/channels/weixin/styles.js +0 -5
- package/plugin-src/client/channels/whatsapp/index.js +27 -23
- package/plugin-src/client/i18n.js +2 -0
- package/plugin-src/client/styles.js +23 -8
- package/plugin-src/host/index.mjs +14 -1
- package/src/channels/dingtalk/dingtalk-api.mjs +215 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +155 -4
- package/src/channels/discord/discord-api.mjs +134 -6
- package/src/channels/discord/discord-runtime.mjs +15 -4
- package/src/channels/feishu/bridge.mjs +223 -15
- package/src/channels/feishu/feishu-channel.mjs +227 -1
- package/src/channels/feishu/plugin-controller.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +217 -10
- package/src/channels/shared/editable-message-stream.mjs +18 -1
- package/src/channels/shared/harness-client.mjs +99 -7
- package/src/channels/shared/semantic/artifact.mjs +748 -0
- package/src/channels/shared/semantic/delivery.mjs +153 -0
- package/src/channels/shared/text-harness-bridge.mjs +149 -3
- package/src/channels/shared/workspace-session.mjs +15 -1
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +167 -4
- package/src/channels/slack/slack-runtime.mjs +21 -5
- package/src/channels/telegram/telegram-api.mjs +111 -5
- package/src/channels/telegram/telegram-runtime.mjs +18 -4
- package/src/channels/wecom/wecom-bridge.mjs +260 -12
- package/src/channels/weixin/weixin-api.mjs +268 -2
- package/src/channels/weixin/weixin-bridge.mjs +134 -3
- package/src/channels/weixin/weixin-controller.mjs +5 -1
- package/src/channels/weixin/weixin-runtime.mjs +5 -1
- package/src/channels/whatsapp/whatsapp-runtime.mjs +108 -5
|
@@ -77,10 +77,6 @@ const CSS = String.raw`
|
|
|
77
77
|
.dxw-accountIdentity h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; }
|
|
78
78
|
.dxw-accountIdentity p { color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; margin-top: 4px; }
|
|
79
79
|
.dxw-health { display: inline-flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; white-space: nowrap; }
|
|
80
|
-
.dxw-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 20px 0; }
|
|
81
|
-
.dxw-metric { padding: 12px 14px; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
82
|
-
.dxw-metric dt { color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; }
|
|
83
|
-
.dxw-metric dd { overflow: hidden; margin: 5px 0 0; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
80
|
.dxw-accountFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
|
|
85
81
|
.dxw-accountFooter .dxw-actions { flex: none; flex-wrap: nowrap; gap: 8px; margin-top: 0; }
|
|
86
82
|
.dxw-accountFooter .dxw-button { flex: none; white-space: nowrap; }
|
|
@@ -99,7 +95,6 @@ const CSS = String.raw`
|
|
|
99
95
|
.dxw-logo { display: none; }
|
|
100
96
|
.dxw-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
|
101
97
|
.dxw-qrCopy { width: 100%; }
|
|
102
|
-
.dxw-metrics { grid-template-columns: minmax(0, 1fr); }
|
|
103
98
|
.dxw-cardBody { padding: 20px; }
|
|
104
99
|
}
|
|
105
100
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
EMPTY_AGENT_PRESET_CATALOG,
|
|
11
11
|
} from '../../agent-preset.js';
|
|
12
12
|
import { useWorkspaceSnapshotFence } from '../../workspace-snapshot-fence.js';
|
|
13
|
+
import { BotStatusMeta, ChannelListHeading } from '../../channel-card-meta.js';
|
|
13
14
|
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
14
15
|
import {
|
|
15
16
|
WHATSAPP_ENDPOINTS,
|
|
@@ -197,15 +198,14 @@ export function WhatsappAccountCard({
|
|
|
197
198
|
}, h(WhatsappLogoGlyph, { size: 29 })),
|
|
198
199
|
h('div', { className: 'dim-botName' },
|
|
199
200
|
h('h3', null, account.bot.name), h('p', null, account.bot.idMasked))),
|
|
200
|
-
h(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
|
|
201
|
+
h(BotStatusMeta, {
|
|
202
|
+
className: 'ddt-health',
|
|
203
|
+
dotClassName: 'ddt-dot',
|
|
204
|
+
tone,
|
|
205
|
+
stateLabel,
|
|
206
|
+
lastCheckedAt: account.health.lastCheckedAt,
|
|
207
|
+
formatCheckedTime: checkedTime,
|
|
208
|
+
})),
|
|
209
209
|
h(WorkspaceEditor, {
|
|
210
210
|
workspace: account.workspace,
|
|
211
211
|
disabled: Boolean(busy),
|
|
@@ -217,18 +217,19 @@ export function WhatsappAccountCard({
|
|
|
217
217
|
onSave: onAgentPresetSave,
|
|
218
218
|
}),
|
|
219
219
|
h('div', { className: 'ddt-accountFooter dim-cardFooter' },
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
h(
|
|
230
|
-
className: 'dim-
|
|
231
|
-
|
|
220
|
+
h('div', { className: 'dim-cardFooterLayout' },
|
|
221
|
+
h('div', { className: 'ddt-actions dim-cardActions' },
|
|
222
|
+
h(Button, {
|
|
223
|
+
className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy),
|
|
224
|
+
}, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
|
|
225
|
+
h(Button, {
|
|
226
|
+
className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
|
|
227
|
+
}, '移除接入')),
|
|
228
|
+
summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
|
|
229
|
+
testNotice ? h('div', {
|
|
230
|
+
className: 'ddt-summary dim-cardFeedback',
|
|
231
|
+
role: 'status',
|
|
232
|
+
}, testNotice) : null))),
|
|
232
233
|
removing ? h(RemoveConfirmation, {
|
|
233
234
|
account,
|
|
234
235
|
busy: busy === 'delete',
|
|
@@ -439,8 +440,11 @@ export function WhatsappSettingsTab({ rpcCall }) {
|
|
|
439
440
|
|
|
440
441
|
const botList = model.bots.length > 0
|
|
441
442
|
? h('section', { className: 'dim-listSection' },
|
|
442
|
-
h(
|
|
443
|
-
|
|
443
|
+
h(ChannelListHeading, {
|
|
444
|
+
className: 'ddt-listHeading',
|
|
445
|
+
title: '已接入的 WhatsApp 机器人',
|
|
446
|
+
connectionLabel: 'WhatsApp Web',
|
|
447
|
+
}),
|
|
444
448
|
h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
|
|
445
449
|
h('li', { key: account.botId }, h(WhatsappAccountCard, {
|
|
446
450
|
account,
|
|
@@ -107,6 +107,7 @@ const EN = Object.freeze({
|
|
|
107
107
|
'离线': 'Offline',
|
|
108
108
|
'已断开': 'Disconnected',
|
|
109
109
|
'消息通道': 'Message channel',
|
|
110
|
+
'查看消息通道说明': 'View message channel details',
|
|
110
111
|
'最近检查': 'Last checked',
|
|
111
112
|
'当前工作区': 'Current workspace',
|
|
112
113
|
'选择目录': 'Choose folder',
|
|
@@ -136,6 +137,7 @@ const EN = Object.freeze({
|
|
|
136
137
|
'工作区路径必须指向一个目录。': 'The workspace path must point to a directory.',
|
|
137
138
|
'找不到要修改的机器人。': 'The bot could not be found.',
|
|
138
139
|
'Agent Preset': 'Agent Preset',
|
|
140
|
+
'查看 Agent Preset 说明': 'View Agent Preset help',
|
|
139
141
|
'跟随 Host 默认': 'Follow the Host default',
|
|
140
142
|
'(已不可用)': ' (unavailable)',
|
|
141
143
|
'只影响新建会话;若当前聊天已有会话,先发送 /new,再发送普通消息生效。': 'This affects only new sessions. If the current chat already has a session, send /new, then send a regular message to apply it.',
|
|
@@ -87,6 +87,14 @@ const CSS = String.raw`
|
|
|
87
87
|
.dim-panel .dim-listSection { display: flex; flex-direction: column; gap: 0; }
|
|
88
88
|
.dim-panel .dim-listHeading { min-height: 0; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 6px; padding: 0; }
|
|
89
89
|
.dim-panel .dim-listHeading h3 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: normal; font-weight: 650; }
|
|
90
|
+
.dim-panel .dim-listTitle { min-width: 0; display: inline-flex; align-items: center; gap: 6px; }
|
|
91
|
+
.dim-panel .dim-channelHelp { position: relative; display: inline-flex; flex: none; }
|
|
92
|
+
.dim-panel .dim-channelHelpButton { width: 17px; height: 17px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #1677ff 28%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #1677ff; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 11px; line-height: 1; font-weight: 700; cursor: help; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
|
|
93
|
+
.dim-panel .dim-channelHelpButton:hover { border-color: #1677ff; color: #0f5fce; background: color-mix(in srgb, #1677ff 8%, var(--dsw-alias-bg-layer-1, #fff)); }
|
|
94
|
+
.dim-panel .dim-channelHelpButton:focus-visible { outline: none; border-color: #1677ff; box-shadow: 0 0 0 3px color-mix(in srgb, #1677ff 16%, transparent); }
|
|
95
|
+
.dim-panel .dim-channelTooltip { position: absolute; top: calc(100% + 7px); left: 0; z-index: 30; width: max-content; max-width: min(280px, calc(100vw - 48px)); display: flex; align-items: baseline; gap: 5px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); font-size: 11px; line-height: 16px; font-weight: 400; white-space: normal; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
|
96
|
+
.dim-panel .dim-channelTooltip strong { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 600; white-space: nowrap; }
|
|
97
|
+
.dim-panel .dim-channelHelp:hover .dim-channelTooltip, .dim-panel .dim-channelHelp:focus-within .dim-channelTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
90
98
|
.dim-panel .dim-botList { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
|
91
99
|
.dim-panel .dim-loadingView { padding: 38px; color: var(--dsw-alias-label-secondary, #646a73); text-align: center; }
|
|
92
100
|
.dim-panel .dim-loadingView h3 { margin: 0 0 7px; color: var(--dsw-alias-label-primary, #1f2329); font-size: 17px; line-height: normal; font-weight: 650; }
|
|
@@ -143,13 +151,18 @@ const CSS = String.raw`
|
|
|
143
151
|
.dim-panel .dim-workspaceEdit:disabled { cursor: not-allowed; opacity: .55; }
|
|
144
152
|
.dim-panel .dim-workspacePath { min-width: 0; max-width: 100%; grid-column: 1 / -1; grid-row: 2; display: block; overflow-x: auto; overflow-y: hidden; color: var(--dsw-alias-label-primary, #1f2329); font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; }
|
|
145
153
|
.dim-panel .dim-preset { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; column-gap: 10px; row-gap: 4px; margin-top: 6px; padding: 6px 10px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
|
|
146
|
-
.dim-panel .dim-presetHeader { display:
|
|
147
|
-
.dim-panel .dim-
|
|
154
|
+
.dim-panel .dim-presetHeader { position: relative; min-width: 0; grid-column: 1 / -1; grid-row: 1; display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
|
|
155
|
+
.dim-panel .dim-presetTitle { min-width: 0; display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
|
156
|
+
.dim-panel .dim-presetHelp { display: inline-flex; align-items: center; flex: none; }
|
|
157
|
+
.dim-panel .dim-presetHelpButton { width: 17px; height: 17px; display: grid; place-items: center; padding: 0; border: 1px solid color-mix(in srgb, #1677ff 28%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 50%; color: #1677ff; background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 11px; line-height: 1; font-weight: 700; cursor: help; transition: border-color .15s ease, color .15s ease, background .15s ease, box-shadow .15s ease; }
|
|
158
|
+
.dim-panel .dim-presetHelpButton:hover { border-color: #1677ff; color: #0f5fce; background: color-mix(in srgb, #1677ff 8%, var(--dsw-alias-bg-layer-1, #fff)); }
|
|
159
|
+
.dim-panel .dim-presetHelpButton:focus-visible { outline: none; border-color: #1677ff; box-shadow: 0 0 0 3px color-mix(in srgb, #1677ff 16%, transparent); }
|
|
160
|
+
.dim-panel .dim-presetTooltip { position: absolute; top: calc(100% + 7px); left: 0; z-index: 30; width: min(320px, 100%); padding: 9px 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 10px 28px rgb(31 35 41 / 16%); font-size: 11px; line-height: 16px; font-weight: 400; overflow-wrap: anywhere; white-space: normal; opacity: 0; visibility: hidden; transform: translateY(-3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
|
|
161
|
+
.dim-panel .dim-presetHelp:hover .dim-presetTooltip, .dim-panel .dim-presetHelp:focus-within .dim-presetTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
148
162
|
.dim-panel .dim-presetStatus { grid-column: 2; grid-row: 1; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; white-space: nowrap; }
|
|
149
163
|
.dim-panel .dim-presetSelect { min-width: 0; max-width: 100%; grid-column: 1 / -1; grid-row: 2; height: 30px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 12px; }
|
|
150
164
|
.dim-panel .dim-presetSelect:disabled { cursor: not-allowed; opacity: .55; }
|
|
151
|
-
.dim-panel .dim-
|
|
152
|
-
.dim-panel .dim-presetError { grid-column: 1 / -1; grid-row: 4; margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.4; }
|
|
165
|
+
.dim-panel .dim-presetError { grid-column: 1 / -1; grid-row: 3; margin: 0; color: var(--dsw-alias-state-error-primary, #d54941); font-size: 12px; line-height: 1.4; }
|
|
153
166
|
.dim-directoryPickerBackdrop { --dim-blue: var(--dsw-alias-state-business-primary, #3370ff); --dim-blue-soft: color-mix(in srgb, var(--dim-blue) 9%, transparent); position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 24px; background: rgb(15 17 21 / 42%); backdrop-filter: blur(3px); }
|
|
154
167
|
.dim-directoryPickerBackdrop, .dim-directoryPickerBackdrop *, .dim-directoryPickerBackdrop *::before, .dim-directoryPickerBackdrop *::after { box-sizing: border-box; }
|
|
155
168
|
.dim-directoryPicker { width: min(720px, 100%); height: min(620px, calc(100vh - 48px)); min-height: 420px; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 18px; outline: none; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 24px 72px rgb(15 17 21 / 24%); }
|
|
@@ -192,6 +205,9 @@ const CSS = String.raw`
|
|
|
192
205
|
.dim-directoryPickerActions button:hover:not(:disabled) { filter: brightness(.97); }
|
|
193
206
|
.dim-directoryPickerActions button:disabled { cursor: not-allowed; opacity: .52; }
|
|
194
207
|
.dim-panel .dim-cardSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font: inherit; font-size: 12px; font-weight: 400; line-height: normal; }
|
|
208
|
+
.dim-panel .dim-cardFooterLayout { min-width: 0; width: 100%; display: flex; flex-direction: column; align-items: stretch; gap: 9px; }
|
|
209
|
+
.dim-panel .dim-cardFooterLayout > .dim-cardActions { align-self: flex-end; }
|
|
210
|
+
.dim-panel .dim-cardFeedback { width: 100%; padding: 8px 10px; border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-module-platform, #f7f8fa); font: inherit; font-size: 12px; font-weight: 400; line-height: 18px; overflow-wrap: anywhere; white-space: normal; }
|
|
195
211
|
.dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
|
|
196
212
|
.dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 32px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; font-weight: 560; line-height: normal; white-space: nowrap; }
|
|
197
213
|
.dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
@@ -206,15 +222,13 @@ const CSS = String.raw`
|
|
|
206
222
|
.dim-panel .dim-botName { min-width: 0; }
|
|
207
223
|
.dim-panel .dim-botName h3 { overflow: hidden; margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 15px; font-weight: 650; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
|
208
224
|
.dim-panel .dim-botName p { overflow: hidden; margin: 4px 0 0; color: var(--dsw-alias-label-secondary, #646a73); font: 12px ui-monospace, SFMono-Regular, monospace; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
|
225
|
+
.dim-panel .dim-botHealthGroup { flex: none; display: grid; justify-items: end; gap: 5px; }
|
|
209
226
|
.dim-panel .dim-botCard .dim-botHealth { flex: none; min-height: 0; display: inline-flex; align-items: center; gap: 7px; padding: 0; border: 0; border-radius: 0; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
|
227
|
+
.dim-panel .dim-lastChecked { display: inline-flex; align-items: baseline; gap: 4px; color: var(--dsw-alias-label-tertiary, #8f959e); font: inherit; font-size: 11px; font-weight: 400; line-height: normal; white-space: nowrap; }
|
|
210
228
|
.dim-panel .dim-botCard .dim-healthDot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: #aeb3bb; box-shadow: none; }
|
|
211
229
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="success"] { background: var(--dsw-alias-state-success-primary, #20a162); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary, #20a162) 14%, transparent); }
|
|
212
230
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
|
|
213
231
|
.dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
|
|
214
|
-
.dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 6px 0; }
|
|
215
|
-
.dim-panel .dim-botMetric { min-width: 0; padding: 6px 8px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
|
|
216
|
-
.dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
|
|
217
|
-
.dim-panel .dim-botMetric dd { overflow: hidden; margin: 3px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 12px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
|
|
218
232
|
.dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
|
|
219
233
|
.dim-panel .ddt-headingCopy { display: none; }
|
|
220
234
|
.dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
|
|
@@ -245,6 +259,7 @@ const CSS = String.raw`
|
|
|
245
259
|
}
|
|
246
260
|
@media (max-width: 720px) {
|
|
247
261
|
.dim-panel .dim-botCardTop { flex-direction: column; align-items: stretch; }
|
|
262
|
+
.dim-panel .dim-botHealthGroup { justify-items: start; }
|
|
248
263
|
}
|
|
249
264
|
@media (max-width: 560px) {
|
|
250
265
|
.dim-title { flex-direction: column; gap: 10px; }
|
|
@@ -8,9 +8,15 @@ import { apply as applyTelegram } from './channels/telegram/index.mjs';
|
|
|
8
8
|
import { apply as applyWecom } from './channels/wecom/index.mjs';
|
|
9
9
|
import { apply as applyWeixin } from './channels/weixin/index.mjs';
|
|
10
10
|
import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
|
|
11
|
+
import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
|
|
11
12
|
|
|
12
13
|
export const name = 'dsh-im-host';
|
|
13
|
-
export const inject = [
|
|
14
|
+
export const inject = [
|
|
15
|
+
'connection',
|
|
16
|
+
'credentials',
|
|
17
|
+
'webServer',
|
|
18
|
+
'typertGateway',
|
|
19
|
+
];
|
|
14
20
|
|
|
15
21
|
function channelConfig(config, name) {
|
|
16
22
|
const channel = config[name] ?? {};
|
|
@@ -34,6 +40,13 @@ export function createImHostPlugin(internals = {}) {
|
|
|
34
40
|
name,
|
|
35
41
|
inject,
|
|
36
42
|
async apply(ctx, config = {}) {
|
|
43
|
+
if (typeof ctx?.inject === 'function') {
|
|
44
|
+
ctx.inject(['tools', 'systemPrompt'], (artifactCtx) => {
|
|
45
|
+
installOutboundArtifactTool(artifactCtx);
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
installOutboundArtifactTool(ctx);
|
|
49
|
+
}
|
|
37
50
|
await startFeishu(ctx, channelConfig(config, 'feishu'));
|
|
38
51
|
await startWeixin(ctx, channelConfig(config, 'weixin'));
|
|
39
52
|
await startDingtalk(ctx, channelConfig(config, 'dingtalk'));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { extname } from 'node:path';
|
|
2
3
|
|
|
3
4
|
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
|
4
5
|
|
|
@@ -25,8 +26,67 @@ function nonEmptyString(value) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function safeProviderCode(value) {
|
|
28
|
-
const code =
|
|
29
|
-
return code &&
|
|
29
|
+
const code = value === undefined || value === null ? null : String(value).trim();
|
|
30
|
+
return code && /^-?[A-Za-z0-9_.:-]{1,160}$/.test(code) ? code : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function preserveArtifactMetadata(target, source) {
|
|
34
|
+
if (Number.isInteger(source?.status)) target.status = source.status;
|
|
35
|
+
if (source?.providerCode !== undefined) target.providerCode = source.providerCode;
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dingtalkArtifactError(cause, { fallback = 'artifact-provider-rejected' } = {}) {
|
|
40
|
+
if (cause?.code?.startsWith?.('artifact-')) return cause;
|
|
41
|
+
const status = Number(cause?.status);
|
|
42
|
+
const providerCode = safeProviderCode(cause?.providerCode);
|
|
43
|
+
const providerText = providerCode ?? '';
|
|
44
|
+
let code = fallback;
|
|
45
|
+
let message = 'DingTalk could not prepare the file for delivery.';
|
|
46
|
+
if (status === 401 || status === 403 || providerCode === '401' || providerCode === '403'
|
|
47
|
+
|| /(?:permission|forbidden|unauthor|access.?denied|\.auth(?:\.|$))/i.test(providerText)) {
|
|
48
|
+
code = 'artifact-permission-required';
|
|
49
|
+
message = 'DingTalk denied permission to send the file.';
|
|
50
|
+
} else if (status === 413 || providerCode === '413'
|
|
51
|
+
|| /(?:too.?large|size.?limit)/i.test(providerText)) {
|
|
52
|
+
code = 'artifact-too-large';
|
|
53
|
+
message = 'The file exceeds DingTalk\'s size limit.';
|
|
54
|
+
} else if (status === 429 || providerCode === '429'
|
|
55
|
+
|| /(?:rate.?limit|too.?many|throttl)/i.test(providerText)) {
|
|
56
|
+
code = 'artifact-rate-limited';
|
|
57
|
+
message = 'DingTalk rate-limited file delivery.';
|
|
58
|
+
} else if (fallback === 'artifact-provider-rejected') {
|
|
59
|
+
message = 'DingTalk rejected the file message.';
|
|
60
|
+
}
|
|
61
|
+
const error = new Error(message, { cause });
|
|
62
|
+
error.code = code;
|
|
63
|
+
return preserveArtifactMetadata(error, cause);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function uncertainDingtalkDelivery(cause) {
|
|
67
|
+
const error = new Error('DingTalk file delivery result is uncertain', { cause });
|
|
68
|
+
error.code = 'artifact-delivery-uncertain';
|
|
69
|
+
return preserveArtifactMetadata(error, cause);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rejectedProviderResponse(value) {
|
|
73
|
+
if (!value || typeof value !== 'object') return null;
|
|
74
|
+
for (const field of ['errcode', 'code']) {
|
|
75
|
+
if (value[field] !== undefined && value[field] !== 0 && value[field] !== '0') {
|
|
76
|
+
return safeProviderCode(value[field]) ?? 'rejected';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classifyDingtalkFinalDeliveryError(error, signal) {
|
|
83
|
+
if (signal?.aborted) throw abortError(signal);
|
|
84
|
+
const status = Number(error?.status);
|
|
85
|
+
if (error?.code === 'network-error' || error?.code === 'timeout'
|
|
86
|
+
|| error?.code === 'invalid-response' || (status >= 500 && status < 600)) {
|
|
87
|
+
return uncertainDingtalkDelivery(error);
|
|
88
|
+
}
|
|
89
|
+
return dingtalkArtifactError(error);
|
|
30
90
|
}
|
|
31
91
|
|
|
32
92
|
function secureDingtalkDownloadUrl(value) {
|
|
@@ -174,6 +234,74 @@ async function requestJson(fetchImpl, url, {
|
|
|
174
234
|
}
|
|
175
235
|
}
|
|
176
236
|
|
|
237
|
+
async function requestMultipart(fetchImpl, url, { body, signal, timeoutMs = 60_000 } = {}) {
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
let timedOut = false;
|
|
240
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
241
|
+
if (signal?.aborted) throw abortError(signal);
|
|
242
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
243
|
+
const timer = setTimeout(() => {
|
|
244
|
+
timedOut = true;
|
|
245
|
+
controller.abort();
|
|
246
|
+
}, timeoutMs);
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetchImpl(url, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
body,
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
redirect: 'error',
|
|
253
|
+
});
|
|
254
|
+
let value;
|
|
255
|
+
let parseError;
|
|
256
|
+
try {
|
|
257
|
+
value = await response.json();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
parseError = error;
|
|
260
|
+
}
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new DingtalkApiError(
|
|
263
|
+
'http-error',
|
|
264
|
+
`钉钉服务请求失败(HTTP ${response.status})。`,
|
|
265
|
+
{ status: response.status, providerCode: safeProviderCode(value?.code ?? value?.errcode) },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (parseError) {
|
|
269
|
+
throw new DingtalkApiError(
|
|
270
|
+
'invalid-response',
|
|
271
|
+
'钉钉服务返回了无法解析的响应。',
|
|
272
|
+
{ cause: parseError },
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (signal?.aborted) throw abortError(signal);
|
|
278
|
+
if (timedOut) throw new DingtalkApiError('timeout', '钉钉服务请求超时。', { cause: error });
|
|
279
|
+
if (error instanceof DingtalkApiError) throw error;
|
|
280
|
+
throw new DingtalkApiError('network-error', '暂时无法完成钉钉文件上传请求。', { cause: error });
|
|
281
|
+
} finally {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
signal?.removeEventListener('abort', onAbort);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeFileTarget(target) {
|
|
288
|
+
const robotCode = nonEmptyString(target?.robotCode);
|
|
289
|
+
if (!robotCode) throw new TypeError('DingTalk robotCode is required');
|
|
290
|
+
if (target?.type === 'group') {
|
|
291
|
+
const openConversationId = nonEmptyString(target.openConversationId);
|
|
292
|
+
if (openConversationId) return { type: 'group', robotCode, openConversationId };
|
|
293
|
+
}
|
|
294
|
+
if (target?.type === 'user') {
|
|
295
|
+
const userId = nonEmptyString(target.userId);
|
|
296
|
+
if (userId) return { type: 'user', robotCode, userId };
|
|
297
|
+
}
|
|
298
|
+
throw new TypeError('DingTalk file target is invalid');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function dingtalkFileType(fileName) {
|
|
302
|
+
return extname(fileName).slice(1).toLowerCase();
|
|
303
|
+
}
|
|
304
|
+
|
|
177
305
|
function normalizeCardTarget(target) {
|
|
178
306
|
if (target?.type === 'user') {
|
|
179
307
|
const userId = nonEmptyString(target.userId);
|
|
@@ -648,6 +776,91 @@ export function createDingtalkApi({
|
|
|
648
776
|
return true;
|
|
649
777
|
},
|
|
650
778
|
|
|
779
|
+
async sendFile({ clientId, clientSecret, target, file, signal }) {
|
|
780
|
+
if (!file || typeof file !== 'object'
|
|
781
|
+
|| typeof file.fileName !== 'string' || !file.fileName
|
|
782
|
+
|| !Buffer.isBuffer(file.bytes)) {
|
|
783
|
+
throw new TypeError('A DingTalk file is required');
|
|
784
|
+
}
|
|
785
|
+
const normalizedTarget = normalizeFileTarget(target);
|
|
786
|
+
const fileType = dingtalkFileType(file.fileName);
|
|
787
|
+
let token;
|
|
788
|
+
try {
|
|
789
|
+
token = await accessToken({ clientId, clientSecret, signal });
|
|
790
|
+
} catch (error) {
|
|
791
|
+
if (signal?.aborted) throw abortError(signal);
|
|
792
|
+
const status = Number(error?.status);
|
|
793
|
+
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
794
|
+
? 'artifact-provider-rejected'
|
|
795
|
+
: 'artifact-provider-failed';
|
|
796
|
+
throw dingtalkArtifactError(error, { fallback });
|
|
797
|
+
}
|
|
798
|
+
const uploadUrl = new URL('media/upload', DINGTALK_REGISTRATION_BASE_URL);
|
|
799
|
+
uploadUrl.searchParams.set('access_token', token);
|
|
800
|
+
uploadUrl.searchParams.set('type', 'file');
|
|
801
|
+
const form = new FormData();
|
|
802
|
+
form.append(
|
|
803
|
+
'media',
|
|
804
|
+
new Blob([file.bytes], { type: file.mediaType ?? 'application/octet-stream' }),
|
|
805
|
+
file.fileName,
|
|
806
|
+
);
|
|
807
|
+
let uploaded;
|
|
808
|
+
try {
|
|
809
|
+
uploaded = await requestMultipart(fetchImpl, uploadUrl, { body: form, signal });
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (signal?.aborted) throw abortError(signal);
|
|
812
|
+
const status = Number(error?.status);
|
|
813
|
+
const fallback = error?.code === 'http-error' && status >= 400 && status < 500
|
|
814
|
+
? 'artifact-provider-rejected'
|
|
815
|
+
: 'artifact-provider-failed';
|
|
816
|
+
throw dingtalkArtifactError(error, { fallback });
|
|
817
|
+
}
|
|
818
|
+
const uploadRejection = rejectedProviderResponse(uploaded);
|
|
819
|
+
if (uploadRejection || !nonEmptyString(uploaded?.media_id)) {
|
|
820
|
+
throw dingtalkArtifactError(new DingtalkApiError(
|
|
821
|
+
'upload-rejected',
|
|
822
|
+
'钉钉服务拒绝了文件上传。',
|
|
823
|
+
{ providerCode: uploadRejection ?? 'missing-media-id' },
|
|
824
|
+
));
|
|
825
|
+
}
|
|
826
|
+
signal?.throwIfAborted();
|
|
827
|
+
const messageBody = {
|
|
828
|
+
robotCode: normalizedTarget.robotCode,
|
|
829
|
+
msgKey: 'sampleFile',
|
|
830
|
+
msgParam: JSON.stringify({
|
|
831
|
+
mediaId: uploaded.media_id,
|
|
832
|
+
fileName: file.fileName,
|
|
833
|
+
fileType,
|
|
834
|
+
}),
|
|
835
|
+
...(normalizedTarget.type === 'group'
|
|
836
|
+
? { openConversationId: normalizedTarget.openConversationId }
|
|
837
|
+
: { userIds: [normalizedTarget.userId] }),
|
|
838
|
+
};
|
|
839
|
+
const pathname = normalizedTarget.type === 'group'
|
|
840
|
+
? 'v1.0/robot/groupMessages/send'
|
|
841
|
+
: 'v1.0/robot/oToMessages/batchSend';
|
|
842
|
+
let response;
|
|
843
|
+
try {
|
|
844
|
+
response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
|
|
845
|
+
body: messageBody,
|
|
846
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
847
|
+
signal,
|
|
848
|
+
action: '文件消息发送',
|
|
849
|
+
});
|
|
850
|
+
} catch (error) {
|
|
851
|
+
throw classifyDingtalkFinalDeliveryError(error, signal);
|
|
852
|
+
}
|
|
853
|
+
const sendRejection = rejectedProviderResponse(response);
|
|
854
|
+
if (sendRejection) {
|
|
855
|
+
throw dingtalkArtifactError(new DingtalkApiError(
|
|
856
|
+
'send-rejected',
|
|
857
|
+
'钉钉服务拒绝了文件消息。',
|
|
858
|
+
{ providerCode: sendRejection },
|
|
859
|
+
));
|
|
860
|
+
}
|
|
861
|
+
return response;
|
|
862
|
+
},
|
|
863
|
+
|
|
651
864
|
clearAccessToken(clientId) {
|
|
652
865
|
const appKey = nonEmptyString(clientId);
|
|
653
866
|
if (appKey) tokenCache.delete(appKey);
|