@xmanrui/dsh-im 0.8.0 → 0.10.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 (48) hide show
  1. package/README.en.md +8 -4
  2. package/README.md +8 -4
  3. package/lib/client.js +16 -14
  4. package/lib/index.js +123 -121
  5. package/package.json +1 -1
  6. package/plugin-src/client/styles.js +15 -14
  7. package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
  8. package/plugin-src/host/channels/dingtalk/production.mjs +3 -0
  9. package/plugin-src/host/channels/discord/index.mjs +1 -1
  10. package/plugin-src/host/channels/feishu/index.mjs +1 -1
  11. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  12. package/plugin-src/host/channels/qq/index.mjs +1 -1
  13. package/plugin-src/host/channels/qq/production.mjs +3 -0
  14. package/plugin-src/host/channels/shared/production.mjs +3 -0
  15. package/plugin-src/host/channels/slack/index.mjs +1 -1
  16. package/plugin-src/host/channels/slack/production.mjs +3 -0
  17. package/plugin-src/host/channels/telegram/index.mjs +1 -1
  18. package/plugin-src/host/channels/wecom/index.mjs +1 -1
  19. package/plugin-src/host/channels/wecom/production.mjs +3 -0
  20. package/plugin-src/host/channels/weixin/index.mjs +1 -1
  21. package/plugin-src/host/channels/weixin/production.mjs +3 -0
  22. package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
  23. package/plugin-src/host/channels/whatsapp/production.mjs +3 -0
  24. package/plugin-src/host/harness-command-executor.mjs +21 -0
  25. package/plugin-src/host/index.mjs +1 -1
  26. package/src/channels/dingtalk/dingtalk-api.mjs +82 -1
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +162 -13
  28. package/src/channels/discord/discord-api.mjs +1 -1
  29. package/src/channels/discord/discord-runtime.mjs +44 -1
  30. package/src/channels/feishu/bridge.mjs +45 -11
  31. package/src/channels/feishu/message-utils.mjs +142 -8
  32. package/src/channels/feishu/plugin-controller.mjs +1 -0
  33. package/src/channels/qq/qq-bridge.mjs +107 -13
  34. package/src/channels/shared/bot-workspace-store.mjs +13 -0
  35. package/src/channels/shared/compact-command.mjs +95 -0
  36. package/src/channels/shared/harness-client.mjs +32 -2
  37. package/src/channels/shared/image-prompt.mjs +268 -0
  38. package/src/channels/shared/text-harness-bridge.mjs +49 -9
  39. package/src/channels/shared/workspace-session.mjs +2 -1
  40. package/src/channels/slack/manifest.mjs +1 -0
  41. package/src/channels/slack/slack-api.mjs +95 -0
  42. package/src/channels/slack/slack-runtime.mjs +24 -3
  43. package/src/channels/telegram/telegram-api.mjs +37 -0
  44. package/src/channels/telegram/telegram-runtime.mjs +71 -9
  45. package/src/channels/wecom/wecom-bridge.mjs +163 -16
  46. package/src/channels/weixin/weixin-api.mjs +98 -2
  47. package/src/channels/weixin/weixin-bridge.mjs +55 -12
  48. package/src/channels/whatsapp/whatsapp-runtime.mjs +144 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "通过扫码、App Manifest或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord和WhatsApp)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -132,12 +132,13 @@ const CSS = String.raw`
132
132
  .dim-panel .dim-confirm { padding: 18px 24px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
133
133
  .dim-panel .dim-confirm strong, .dim-panel .dim-confirm h4 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 1.4; font-weight: 650; }
134
134
  .dim-panel .dim-confirm p { margin: 7px 0 0; color: var(--dsw-alias-label-secondary, #646a73); line-height: 1.6; }
135
- .dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
136
- .dim-panel .dim-workspace { min-width: 0; display: grid; gap: 7px; margin-top: 14px; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l1, #eef0f3); border-radius: 9px; background: var(--dsw-alias-bg-module-platform, #f7f8fa); }
137
- .dim-panel .dim-workspaceHeader { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
138
- .dim-panel .dim-workspaceEdit { padding: 0; border: 0; color: #1677ff; background: transparent; font: inherit; font-weight: 560; cursor: pointer; }
135
+ .dim-panel .dim-cardFooter { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 6px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
136
+ .dim-panel .dim-workspace { 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); }
137
+ .dim-panel .dim-workspaceHeader { display: contents; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: normal; }
138
+ .dim-panel .dim-workspaceHeader > span { grid-column: 1; grid-row: 1; white-space: nowrap; }
139
+ .dim-panel .dim-workspaceEdit { grid-column: 2; grid-row: 1; padding: 0; border: 0; color: #1677ff; background: transparent; font: inherit; font-weight: 560; white-space: nowrap; cursor: pointer; }
139
140
  .dim-panel .dim-workspaceEdit:disabled { cursor: not-allowed; opacity: .55; }
140
- .dim-panel .dim-workspacePath { min-width: 0; display: block; overflow: hidden; color: var(--dsw-alias-label-primary, #1f2329); font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; text-overflow: ellipsis; white-space: nowrap; }
141
+ .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; }
141
142
  .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); }
142
143
  .dim-directoryPickerBackdrop, .dim-directoryPickerBackdrop *, .dim-directoryPickerBackdrop *::before, .dim-directoryPickerBackdrop *::after { box-sizing: border-box; }
143
144
  .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%); }
@@ -181,16 +182,16 @@ const CSS = String.raw`
181
182
  .dim-directoryPickerActions button:disabled { cursor: not-allowed; opacity: .52; }
182
183
  .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; }
183
184
  .dim-panel .dim-cardActions { flex: none; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin: 0 0 0 auto; }
184
- .dim-panel .dim-cardActions .dim-cardAction { flex: none; min-height: 34px; padding: 0 13px; 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; }
185
+ .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; }
185
186
  .dim-panel .dim-cardActions .dim-cardAction:hover:not(:disabled) { border-color: #aeb3bb; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
186
187
  .dim-panel .dim-cardActions .dim-cardAction[data-kind="danger"] { color: var(--dsw-alias-state-error-primary, #d54941); }
187
188
  .dim-panel .dim-botCard { position: relative; overflow: hidden; border: 1px solid var(--dsw-alias-border-l2, #e5e6eb); border-radius: 14px; background: var(--dsw-alias-bg-layer-1, #fff); box-shadow: 0 1px 2px rgb(31 35 41 / 3%); }
188
189
  .dim-panel .dim-botCard::before { display: none; }
189
- .dim-panel .dim-botCardBody { position: relative; padding: 14px; }
190
- .dim-panel .dim-botCardTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
191
- .dim-panel .dim-botIdentity { min-width: 0; display: flex; align-items: center; gap: 12px; }
192
- .dim-panel .dim-botAvatar { flex: none; width: 42px; height: 42px; display: grid; place-items: center; overflow: hidden; border-radius: 12px; box-shadow: none; }
193
- .dim-panel .dim-botAvatar svg { width: 29px; height: 29px; }
190
+ .dim-panel .dim-botCardBody { position: relative; padding: 12px; }
191
+ .dim-panel .dim-botCardTop { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
192
+ .dim-panel .dim-botIdentity { min-width: 0; display: flex; align-items: center; gap: 10px; }
193
+ .dim-panel .dim-botAvatar { flex: none; width: 38px; height: 38px; display: grid; place-items: center; overflow: hidden; border-radius: 11px; box-shadow: none; }
194
+ .dim-panel .dim-botAvatar svg { width: 27px; height: 27px; }
194
195
  .dim-panel .dim-botName { min-width: 0; }
195
196
  .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; }
196
197
  .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; }
@@ -199,10 +200,10 @@ const CSS = String.raw`
199
200
  .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); }
200
201
  .dim-panel .dim-botCard .dim-healthDot[data-tone="warning"] { background: var(--dsw-alias-state-warn-primary, #d97706); }
201
202
  .dim-panel .dim-botCard .dim-healthDot[data-tone="error"] { background: var(--dsw-alias-state-error-primary, #d54941); }
202
- .dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 8px 0; }
203
- .dim-panel .dim-botMetric { min-width: 0; padding: 8px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
203
+ .dim-panel .dim-botMetrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 6px 0; }
204
+ .dim-panel .dim-botMetric { min-width: 0; padding: 6px 8px; border: 0; border-radius: 9px; background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
204
205
  .dim-panel .dim-botMetric dt { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; font-weight: 400; line-height: normal; }
205
- .dim-panel .dim-botMetric dd { overflow: hidden; margin: 5px 0 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 13px; font-weight: 400; line-height: normal; text-overflow: ellipsis; white-space: nowrap; }
206
+ .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; }
206
207
  .dim-panel .dim-botCard .dim-cardFooter { margin-top: 0; }
207
208
  .dim-panel .ddt-headingCopy { display: none; }
208
209
  .dim-panel .ddt-qrFrame, .dim-panel .ddt-countdown { width: min(270px, 100%); }
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installDingtalkRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-dingtalk-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
18
19
 
19
20
  function harnessOrigin(webServer, configured) {
20
21
  if (configured !== undefined) return new URL(configured);
@@ -79,12 +80,14 @@ export async function createProductionController(ctx, config = {}, internals = {
79
80
  }
80
81
  return state;
81
82
  };
83
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
82
84
  const harness = new Harness({
83
85
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
84
86
  workspace: defaultWorkspace,
85
87
  agentPreset: config.agentPreset ?? 'standard',
86
88
  autostart: false,
87
89
  dshBin: config.dshBin ?? 'dsh',
90
+ ...(commandExecutor ? { commandExecutor } : {}),
88
91
  });
89
92
  const coreController = new Controller({
90
93
  deviceAuth,
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installDiscordRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-im-discord-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -3,7 +3,7 @@ import { createProductionController } from './production.mjs';
3
3
  import { installFeishuRpc } from './rpc.mjs';
4
4
 
5
5
  export const name = 'dsh-feishu-host';
6
- export const inject = ['connection', 'credentials', 'webServer'];
6
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
7
7
 
8
8
  function controllerFrom(ctx, config) {
9
9
  if (config?.controller) return config.controller;
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
3
3
  import { unlink } from 'node:fs/promises';
4
4
  import * as Lark from '@larksuiteoapi/node-sdk';
5
5
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
6
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
6
7
  import { verifyFeishuApp } from '../../../../src/channels/feishu/feishu-app.mjs';
7
8
  import { FeishuRuntime } from '../../../../src/channels/feishu/feishu-runtime.mjs';
8
9
  import { HarnessClient } from '../../../../src/channels/feishu/harness-client.mjs';
@@ -105,6 +106,7 @@ export async function createProductionController(ctx, config = {}, internals = {
105
106
  if (!botConfig) throw new Error('Unknown Feishu bot');
106
107
  return stateFor(botConfig);
107
108
  };
109
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
108
110
  const harness = new Harness({
109
111
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
110
112
  workspace: defaultWorkspace,
@@ -113,6 +115,7 @@ export async function createProductionController(ctx, config = {}, internals = {
113
115
  // second DSH would create a competing server and lifecycle.
114
116
  autostart: false,
115
117
  dshBin: config.dshBin ?? 'dsh',
118
+ ...(commandExecutor ? { commandExecutor } : {}),
116
119
  });
117
120
 
118
121
  const coreController = new Controller({
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installQqRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-im-qq-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
18
19
 
19
20
  function harnessOrigin(webServer, configured) {
20
21
  if (configured !== undefined) return new URL(configured);
@@ -70,12 +71,14 @@ export async function createProductionController(ctx, config = {}, internals = {
70
71
  }
71
72
  return state;
72
73
  };
74
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
73
75
  const harness = new Harness({
74
76
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
75
77
  workspace: defaultWorkspace,
76
78
  agentPreset: config.agentPreset ?? 'standard',
77
79
  autostart: false,
78
80
  dshBin: config.dshBin ?? 'dsh',
81
+ ...(commandExecutor ? { commandExecutor } : {}),
79
82
  });
80
83
  const coreController = new Controller({
81
84
  qrAuth,
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
 
5
5
  import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
6
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
6
7
  import {
7
8
  BotWorkspaceStore,
8
9
  createBotWorkspaceScope,
@@ -64,12 +65,14 @@ export async function createTokenProductionController(ctx, config, internals, de
64
65
  }
65
66
  return state;
66
67
  };
68
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
67
69
  const harness = new ResolvedHarness({
68
70
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
69
71
  workspace: defaultWorkspace,
70
72
  agentPreset: config.agentPreset ?? 'standard',
71
73
  autostart: false,
72
74
  dshBin: config.dshBin ?? 'dsh',
75
+ ...(commandExecutor ? { commandExecutor } : {}),
73
76
  });
74
77
  const coreController = new ResolvedController({
75
78
  credentials: ctx.credentials,
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installSlackRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-im-slack-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) return installSlackRpc(ctx, config.controller, config.rpcAuthority);
@@ -14,6 +14,7 @@ import {
14
14
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
15
15
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
16
16
  import { harnessOrigin, pluginPaths } from '../shared/production.mjs';
17
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
17
18
 
18
19
  export async function createProductionController(ctx, config = {}, internals = {}) {
19
20
  if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
@@ -49,12 +50,14 @@ export async function createProductionController(ctx, config = {}, internals = {
49
50
  }
50
51
  return state;
51
52
  };
53
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
52
54
  const harness = new ResolvedHarness({
53
55
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
54
56
  workspace: defaultWorkspace,
55
57
  agentPreset: config.agentPreset ?? 'standard',
56
58
  autostart: false,
57
59
  dshBin: config.dshBin ?? 'dsh',
60
+ ...(commandExecutor ? { commandExecutor } : {}),
58
61
  });
59
62
  const coreController = new ResolvedController({
60
63
  credentials: ctx.credentials,
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installTelegramRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-im-telegram-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installWecomRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-im-wecom-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
18
19
 
19
20
  function harnessOrigin(webServer, configured) {
20
21
  if (configured !== undefined) return new URL(configured);
@@ -73,12 +74,14 @@ export async function createProductionController(ctx, config = {}, internals = {
73
74
  }
74
75
  return state;
75
76
  };
77
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
76
78
  const harness = new Harness({
77
79
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
78
80
  workspace: defaultWorkspace,
79
81
  agentPreset: config.agentPreset ?? 'standard',
80
82
  autostart: false,
81
83
  dshBin: config.dshBin ?? 'dsh',
84
+ ...(commandExecutor ? { commandExecutor } : {}),
82
85
  });
83
86
  const coreController = new Controller({
84
87
  qrAuth,
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installWeixinRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-weixin-host';
5
- export const inject = ['connection', 'credentials', 'webServer'];
5
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
18
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
18
19
 
19
20
  function harnessOrigin(webServer, configured) {
20
21
  if (configured !== undefined) return new URL(configured);
@@ -73,12 +74,14 @@ export async function createProductionController(ctx, config = {}, internals = {
73
74
  }
74
75
  return state;
75
76
  };
77
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
76
78
  const harness = new Harness({
77
79
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
78
80
  workspace: defaultWorkspace,
79
81
  agentPreset: config.agentPreset ?? 'standard',
80
82
  autostart: false,
81
83
  dshBin: config.dshBin ?? 'dsh',
84
+ ...(commandExecutor ? { commandExecutor } : {}),
82
85
  });
83
86
  const coreController = new Controller({
84
87
  api,
@@ -2,7 +2,7 @@ import { createProductionController } from './production.mjs';
2
2
  import { installWhatsappRpc } from './rpc.mjs';
3
3
 
4
4
  export const name = 'dsh-im-whatsapp-host';
5
- export const inject = ['connection', 'webServer'];
5
+ export const inject = ['connection', 'webServer', 'typertGateway'];
6
6
 
7
7
  export async function apply(ctx, config = {}) {
8
8
  if (config?.controller) {
@@ -15,6 +15,7 @@ import {
15
15
  observeBotWorkspaceRemovals,
16
16
  } from '../../../../src/channels/shared/bot-workspace-store.mjs';
17
17
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
18
+ import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
18
19
 
19
20
  const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
20
21
 
@@ -76,12 +77,14 @@ export async function createProductionController(ctx, config = {}, internals = {
76
77
  }
77
78
  return state;
78
79
  };
80
+ const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
79
81
  const harness = new Harness({
80
82
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
81
83
  workspace: defaultWorkspace,
82
84
  agentPreset: config.agentPreset ?? 'standard',
83
85
  autostart: false,
84
86
  dshBin: config.dshBin ?? 'dsh',
87
+ ...(commandExecutor ? { commandExecutor } : {}),
85
88
  });
86
89
  const coreController = new Controller({
87
90
  configStore: observedConfigStore,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Adapt the Host's Typert command gateway to the channel Harness client.
3
+ * Programmatic production fixtures may omit the gateway and exercise other assembly paths.
4
+ */
5
+ export function createHarnessCommandExecutor(ctx, provided) {
6
+ if (provided !== undefined) {
7
+ if (typeof provided !== 'function') throw new TypeError('commandExecutor must be a function');
8
+ return provided;
9
+ }
10
+ const gateway = ctx?.typertGateway;
11
+ if (!gateway) return undefined;
12
+ if (typeof gateway.invoke !== 'function') {
13
+ throw new TypeError('dsh-im requires a callable ctx.typertGateway');
14
+ }
15
+ return (sessionId, line, options = {}) => gateway.invoke({
16
+ namespace: 'commands',
17
+ method: 'execute',
18
+ args: { agentId: sessionId, line },
19
+ signal: options.signal,
20
+ });
21
+ }
@@ -9,7 +9,7 @@ import { apply as applyWeixin } from './channels/weixin/index.mjs';
9
9
  import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
10
10
 
11
11
  export const name = 'dsh-im-host';
12
- export const inject = ['connection', 'credentials', 'webServer'];
12
+ export const inject = ['connection', 'credentials', 'webServer', 'typertGateway'];
13
13
 
14
14
  function channelConfig(config, name) {
15
15
  const channel = config[name] ?? {};
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
+ import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
4
+
3
5
  export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
4
6
  export const DINGTALK_API_BASE_URL = 'https://api.dingtalk.com/';
5
7
  export const DINGTALK_REGISTRATION_SOURCE = 'DING_DWS_CLAW';
@@ -14,6 +16,7 @@ export class DingtalkApiError extends Error {
14
16
  this.name = 'DingtalkApiError';
15
17
  this.code = code;
16
18
  this.status = options.status;
19
+ this.providerCode = options.providerCode;
17
20
  }
18
21
  }
19
22
 
@@ -21,6 +24,25 @@ function nonEmptyString(value) {
21
24
  return typeof value === 'string' && value.trim() ? value.trim() : null;
22
25
  }
23
26
 
27
+ function safeProviderCode(value) {
28
+ const code = nonEmptyString(value);
29
+ return code && /^[A-Za-z0-9_.:-]{1,160}$/.test(code) ? code : undefined;
30
+ }
31
+
32
+ function secureDingtalkDownloadUrl(value) {
33
+ let url;
34
+ try {
35
+ url = new URL(value);
36
+ } catch (error) {
37
+ throw new DingtalkApiError('invalid-image-download', '钉钉服务返回了无效的图片下载地址。', { cause: error });
38
+ }
39
+ if (url.protocol === 'http:' && (!url.port || url.port === '80')) {
40
+ url.protocol = 'https:';
41
+ url.port = '';
42
+ }
43
+ return url;
44
+ }
45
+
24
46
  function isDingtalkHost(hostname) {
25
47
  const normalized = hostname.toLowerCase().replace(/\.$/, '');
26
48
  return normalized === 'dingtalk.com' || normalized.endsWith('.dingtalk.com');
@@ -123,10 +145,17 @@ async function requestJson(fetchImpl, url, {
123
145
  signal: controller.signal,
124
146
  });
125
147
  if (!response.ok) {
148
+ let providerCode;
149
+ try {
150
+ const errorBody = await response.json();
151
+ providerCode = safeProviderCode(errorBody?.code ?? errorBody?.errcode);
152
+ } catch {
153
+ // DingTalk occasionally returns an empty or non-JSON error body.
154
+ }
126
155
  throw new DingtalkApiError(
127
156
  'http-error',
128
157
  `钉钉服务请求失败(HTTP ${response.status})。`,
129
- { status: response.status },
158
+ { status: response.status, providerCode },
130
159
  );
131
160
  }
132
161
  try {
@@ -403,6 +432,58 @@ export function createDingtalkApi({
403
432
 
404
433
  accessToken,
405
434
 
435
+ async downloadImage({
436
+ clientId,
437
+ clientSecret,
438
+ robotCode,
439
+ downloadCode,
440
+ signal,
441
+ maxBytes,
442
+ }) {
443
+ const botCode = nonEmptyString(robotCode);
444
+ const fileCode = nonEmptyString(downloadCode);
445
+ if (!botCode || !fileCode) throw new TypeError('robotCode and downloadCode are required');
446
+ const token = await accessToken({ clientId, clientSecret, signal });
447
+ let response;
448
+ try {
449
+ response = await requestJson(
450
+ fetchImpl,
451
+ endpoint(apiBase, 'v1.0/robot/messageFiles/download'),
452
+ {
453
+ body: { downloadCode: fileCode, robotCode: botCode },
454
+ headers: { 'x-acs-dingtalk-access-token': token },
455
+ signal,
456
+ action: '图片下载地址',
457
+ },
458
+ );
459
+ } catch (error) {
460
+ if (signal?.aborted) throw error;
461
+ throw new DingtalkApiError(
462
+ 'image-download-address-failed',
463
+ '钉钉图片下载地址获取失败。',
464
+ { cause: error, status: error?.status, providerCode: error?.providerCode },
465
+ );
466
+ }
467
+ const downloadUrl = nonEmptyString(response?.downloadUrl ?? response?.download_url);
468
+ if (!downloadUrl) {
469
+ throw new DingtalkApiError('invalid-image-download', '钉钉服务没有返回图片下载地址。');
470
+ }
471
+ try {
472
+ return await fetchImageBuffer(secureDingtalkDownloadUrl(downloadUrl), {
473
+ fetchImpl,
474
+ signal,
475
+ maxBytes,
476
+ });
477
+ } catch (error) {
478
+ if (signal?.aborted || error instanceof ImagePromptError) throw error;
479
+ throw new DingtalkApiError(
480
+ 'image-content-download-failed',
481
+ '钉钉图片内容下载失败。',
482
+ { cause: error },
483
+ );
484
+ }
485
+ },
486
+
406
487
  async createAiCard({ clientId, clientSecret, target, initialText, signal }) {
407
488
  const appKey = nonEmptyString(clientId);
408
489
  const appSecret = nonEmptyString(clientSecret);