@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.
Files changed (47) hide show
  1. package/README.en.md +18 -0
  2. package/README.md +18 -0
  3. package/assets/logo-dsh-im-chinese-readme-3x2.png +0 -0
  4. package/assets/logo_cn.png +0 -0
  5. package/lib/client.js +603 -560
  6. package/lib/index.js +163 -163
  7. package/package.json +1 -1
  8. package/plugin-src/client/agent-preset.js +15 -6
  9. package/plugin-src/client/channel-card-meta.js +48 -0
  10. package/plugin-src/client/channels/dingtalk/index.js +25 -19
  11. package/plugin-src/client/channels/dingtalk/styles.js +0 -6
  12. package/plugin-src/client/channels/feishu/index.js +41 -35
  13. package/plugin-src/client/channels/feishu/styles.js +0 -5
  14. package/plugin-src/client/channels/qq/index.js +24 -16
  15. package/plugin-src/client/channels/shared/token-channel.js +32 -24
  16. package/plugin-src/client/channels/wecom/index.js +24 -16
  17. package/plugin-src/client/channels/weixin/index.js +29 -23
  18. package/plugin-src/client/channels/weixin/styles.js +0 -5
  19. package/plugin-src/client/channels/whatsapp/index.js +27 -23
  20. package/plugin-src/client/i18n.js +2 -0
  21. package/plugin-src/client/styles.js +23 -8
  22. package/plugin-src/host/index.mjs +14 -1
  23. package/src/channels/dingtalk/dingtalk-api.mjs +215 -2
  24. package/src/channels/dingtalk/dingtalk-bridge.mjs +155 -4
  25. package/src/channels/discord/discord-api.mjs +134 -6
  26. package/src/channels/discord/discord-runtime.mjs +15 -4
  27. package/src/channels/feishu/bridge.mjs +223 -15
  28. package/src/channels/feishu/feishu-channel.mjs +227 -1
  29. package/src/channels/feishu/plugin-controller.mjs +1 -0
  30. package/src/channels/qq/qq-bridge.mjs +217 -10
  31. package/src/channels/shared/editable-message-stream.mjs +18 -1
  32. package/src/channels/shared/harness-client.mjs +99 -7
  33. package/src/channels/shared/semantic/artifact.mjs +748 -0
  34. package/src/channels/shared/semantic/delivery.mjs +153 -0
  35. package/src/channels/shared/text-harness-bridge.mjs +149 -3
  36. package/src/channels/shared/workspace-session.mjs +15 -1
  37. package/src/channels/slack/manifest.mjs +1 -0
  38. package/src/channels/slack/slack-api.mjs +167 -4
  39. package/src/channels/slack/slack-runtime.mjs +21 -5
  40. package/src/channels/telegram/telegram-api.mjs +111 -5
  41. package/src/channels/telegram/telegram-runtime.mjs +18 -4
  42. package/src/channels/wecom/wecom-bridge.mjs +260 -12
  43. package/src/channels/weixin/weixin-api.mjs +268 -2
  44. package/src/channels/weixin/weixin-bridge.mjs +134 -3
  45. package/src/channels/weixin/weixin-controller.mjs +5 -1
  46. package/src/channels/weixin/weixin-runtime.mjs +5 -1
  47. 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('div', { className: 'ddt-health dim-botHealth' },
201
- h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }),
202
- h('span', null, stateLabel))),
203
- h('dl', { className: 'ddt-metrics dim-botMetrics' },
204
- h('div', { className: 'ddt-metric dim-botMetric' },
205
- h('dt', null, '消息通道'),
206
- h('dd', null, account.connected ? 'WhatsApp Web' : '离线')),
207
- h('div', { className: 'ddt-metric dim-botMetric' },
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
- summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
221
- testNotice ? h('div', {
222
- className: 'ddt-summary dim-cardSummary',
223
- role: 'status',
224
- }, testNotice) : null,
225
- h('div', { className: 'ddt-actions dim-cardActions' },
226
- h(Button, {
227
- className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy),
228
- }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
229
- h(Button, {
230
- className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
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('div', { className: 'ddt-listHeading dim-listHeading' },
443
- h('h3', null, '已接入的 WhatsApp 机器人')),
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: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
147
- .dim-panel .dim-presetHeader > span { grid-column: 1; grid-row: 1; white-space: nowrap; }
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-presetHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.4; }
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 = ['connection', 'credentials', 'webServer', 'typertGateway'];
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 = nonEmptyString(value);
29
- return code && /^[A-Za-z0-9_.:-]{1,160}$/.test(code) ? code : undefined;
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);