@xmanrui/dsh-im 4.17.0 → 4.18.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 (50) hide show
  1. package/README.en.md +20 -14
  2. package/README.md +19 -16
  3. package/lib/client.js +447 -257
  4. package/lib/index.js +285 -263
  5. package/package.json +10 -4
  6. package/plugin-src/client/access-policy-settings.js +5 -0
  7. package/plugin-src/client/channel-logos.js +10 -0
  8. package/plugin-src/client/channels/imessage/api.js +12 -0
  9. package/plugin-src/client/channels/imessage/index.js +45 -0
  10. package/plugin-src/client/channels/imessage/styles.js +19 -0
  11. package/plugin-src/client/channels/shared/token-api.js +1 -0
  12. package/plugin-src/client/channels/shared/token-channel.js +5 -1
  13. package/plugin-src/client/delivery-settings.js +5 -0
  14. package/plugin-src/client/i18n.js +23 -1
  15. package/plugin-src/client/index.js +35 -14
  16. package/plugin-src/client/session-channel-logos.js +2 -0
  17. package/plugin-src/client/styles.js +1 -0
  18. package/plugin-src/host/channels/dingtalk/rpc.mjs +2 -4
  19. package/plugin-src/host/channels/feishu/rpc.mjs +2 -4
  20. package/plugin-src/host/channels/imessage/index.mjs +21 -0
  21. package/plugin-src/host/channels/imessage/production.mjs +13 -0
  22. package/plugin-src/host/channels/imessage/rpc.mjs +70 -0
  23. package/plugin-src/host/channels/office/rpc.mjs +2 -1
  24. package/plugin-src/host/channels/qq/rpc.mjs +2 -4
  25. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  26. package/plugin-src/host/channels/shared/rpc.mjs +2 -4
  27. package/plugin-src/host/channels/shared/startup.mjs +2 -4
  28. package/plugin-src/host/channels/slack/rpc.mjs +2 -4
  29. package/plugin-src/host/channels/telegram/rpc.mjs +2 -4
  30. package/plugin-src/host/channels/wecom/rpc.mjs +2 -4
  31. package/plugin-src/host/channels/wecom-app/rpc.mjs +2 -4
  32. package/plugin-src/host/channels/weixin/rpc.mjs +2 -4
  33. package/plugin-src/host/channels/whatsapp/rpc.mjs +2 -4
  34. package/plugin-src/host/delivery-adapter.mjs +4 -0
  35. package/plugin-src/host/delivery-rpc.mjs +2 -4
  36. package/plugin-src/host/inbound-ttl-rpc.mjs +2 -4
  37. package/plugin-src/host/index.mjs +4 -1
  38. package/plugin-src/host/modern-harness-api.mjs +1 -1
  39. package/plugin-src/host/update-rpc.mjs +2 -1
  40. package/plugin-src/management-rpc.mjs +77 -0
  41. package/src/channels/imessage/config-store.mjs +24 -0
  42. package/src/channels/imessage/controller.mjs +37 -0
  43. package/src/channels/imessage/harness-client.mjs +7 -0
  44. package/src/channels/imessage/imessage-api.mjs +194 -0
  45. package/src/channels/imessage/imessage-bridge.mjs +16 -0
  46. package/src/channels/imessage/runtime.mjs +98 -0
  47. package/src/channels/imessage/state-store.mjs +3 -0
  48. package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
  49. package/src/channels/shared/message-failure.mjs +5 -5
  50. package/src/channels/shared/session-channel-labels.mjs +1 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "4.17.0",
4
- "description": "把十种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect ten IM channels and a public AI Office to a local DeepSeek Harness.",
3
+ "version": "4.18.0",
4
+ "description": "把十一种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect eleven IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -20,6 +20,7 @@
20
20
  "telegram",
21
21
  "discord",
22
22
  "whatsapp",
23
+ "imessage",
23
24
  "ai-office"
24
25
  ],
25
26
  "author": "xmanrui (https://github.com/xmanrui)",
@@ -43,6 +44,10 @@
43
44
  {
44
45
  "name": "masucc",
45
46
  "url": "https://github.com/masucc"
47
+ },
48
+ {
49
+ "name": "cherryFloris",
50
+ "url": "https://github.com/cherryFloris"
46
51
  }
47
52
  ],
48
53
  "license": "MIT",
@@ -97,12 +102,13 @@
97
102
  "platform": "web"
98
103
  },
99
104
  "compatibility": {
100
- "dsh": "0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.3-alpha.1",
105
+ "dsh": "0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.3-alpha.1 || 0.1.5-alpha.1",
101
106
  "dshReleases": {
102
107
  "0.1.2-alpha.4": "compatible",
103
108
  "0.1.2-alpha.5": "compatible",
104
109
  "0.1.2-rc.1": "compatible",
105
- "0.1.3-alpha.1": "compatible"
110
+ "0.1.3-alpha.1": "compatible",
111
+ "0.1.5-alpha.1": "compatible"
106
112
  },
107
113
  "profiles": [
108
114
  "web"
@@ -69,6 +69,11 @@ export const ACCESS_CHANNEL_DEFINITIONS = Object.freeze({
69
69
  groupUserLabel: '群成员电话号码或 JID',
70
70
  groupPlaceholder: '8613800000000 或完整 JID',
71
71
  }),
72
+ imessage: Object.freeze({
73
+ directUserLabel: 'iMessage Chat GUID',
74
+ directPlaceholder: 'iMessage;+;chat123',
75
+ groupSupported: false,
76
+ }),
72
77
  });
73
78
 
74
79
  function clonePolicy(policy) {
@@ -132,6 +132,16 @@ export function WhatsappLogoGlyph({ size } = {}) {
132
132
  }));
133
133
  }
134
134
 
135
+ export function IMessageLogoGlyph({ size } = {}) {
136
+ return h('svg', {
137
+ ...dimensions(size), viewBox: '0 0 24 24', focusable: 'false',
138
+ 'aria-hidden': 'true', 'data-im-channel-logo': 'imessage',
139
+ }, h('path', {
140
+ fill: 'currentColor',
141
+ d: 'M12 2.2c-5.52 0-10 3.75-10 8.38 0 2.63 1.52 4.98 3.9 6.52l-.92 3.43a.5.5 0 0 0 .72.56l3.54-1.96c.87.23 1.8.35 2.76.35 5.52 0 10-3.75 10-8.9S17.52 2.2 12 2.2Zm-3.4 8.2a1.2 1.2 0 1 1 0 2.4 1.2 1.2 0 0 1 0-2.4Zm3.4 0a1.2 1.2 0 1 1 0 2.4 1.2 1.2 0 0 1 0-2.4Zm3.4 0a1.2 1.2 0 1 1 0 2.4 1.2 1.2 0 0 1 0-2.4Z',
142
+ }));
143
+ }
144
+
135
145
  export function OfficeLogoGlyph({ size } = {}) {
136
146
  return h('svg', {
137
147
  ...dimensions(size),
@@ -0,0 +1,12 @@
1
+ import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
2
+
3
+ export const IMESSAGE_RPC_CHANNEL = '/imessage';
4
+ export const IMESSAGE_ENDPOINTS = Object.freeze({
5
+ ...TOKEN_BOT_ENDPOINTS,
6
+ bindCredentials: 'bot.bind-native',
7
+ });
8
+ const api = createTokenChannelApi('iMessage', ' macOS Messages 连接');
9
+ export { api as imessageClientApi };
10
+ export const unwrapRpcResult = api.unwrapRpcResult;
11
+ export const normalizeSnapshot = api.normalizeSnapshot;
12
+ export const presentError = api.presentError;
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+ import { IMessageLogoGlyph } from '../../channel-logos.js';
3
+ import { createTokenChannelSettings } from '../shared/token-channel.js';
4
+ import { IMESSAGE_ENDPOINTS, imessageClientApi } from './api.js';
5
+ import { installIMessageStyles } from './styles.js';
6
+ import { h } from '../../i18n.js';
7
+
8
+ function PermissionPanel({ permissions, busy, error, onSubmit, onCancel }) {
9
+ const databaseReady = permissions?.database === 'granted';
10
+ const automationReady = permissions?.automation === 'granted';
11
+ return h('section', { className: 'ddt-card dim-surfaceCard dim-imessagePermissionPanel' },
12
+ h('h3', null, '启用 macOS 原生 iMessage'),
13
+ h('p', null, '支持同账号自聊:在 iPhone 或 Mac 上给自己已登录的 iMessage 邮箱或号码发指令。AI 回复以 🤖 DSH 开头,不会再次触发机器人。'),
14
+ h('p', null, 'DeepSeek Harness 通过 macOS Messages.app 收发文本消息。首次使用需要授予以下权限:'),
15
+ h('ol', { className: 'dim-imessagePermissionSteps' },
16
+ h('li', null,
17
+ h('strong', null, databaseReady ? '✓ 完全磁盘访问权限已授予' : '1. 授予完全磁盘访问权限'),
18
+ databaseReady ? null : h('p', null, '在“系统设置 → 隐私与安全性 → 完全磁盘访问权限”中,打开运行 DeepSeek Harness 的终端或应用。'),
19
+ databaseReady ? null : h('a', { href: 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles' }, '打开完全磁盘访问权限设置')),
20
+ h('li', null,
21
+ h('strong', null, automationReady ? '✓ Messages 自动化权限已授予' : '2. 允许自动化控制 Messages'),
22
+ automationReady ? null : h('p', null, '在“系统设置 → 隐私与安全性 → 自动化”中,允许运行 DeepSeek Harness 的应用控制 Messages。'),
23
+ automationReady ? null : h('a', { href: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Automation' }, '打开自动化设置'))),
24
+ error ? h('p', { className: 'dim-inlineError', role: 'alert' }, error.message ?? String(error)) : null,
25
+ h('div', { className: 'ddt-actions dim-viewActions' },
26
+ h('button', { type: 'button', className: 'ddt-button', onClick: onCancel, disabled: busy }, '取消'),
27
+ h('button', { type: 'button', className: 'ddt-button', 'data-kind': 'primary', onClick: () => onSubmit({}), disabled: busy },
28
+ busy ? '正在检查…' : '检查权限并启用')));
29
+ }
30
+
31
+ const channel = createTokenChannelSettings({
32
+ channel: 'iMessage', endpoints: IMESSAGE_ENDPOINTS, api: imessageClientApi,
33
+ LogoGlyph: IMessageLogoGlyph, installStyles: installIMessageStyles,
34
+ pageClass: 'dim-pageIMessage', avatarClass: 'dim-avatarIMessage',
35
+ connectionLabel: ' macOS Messages 连接',
36
+ tokenPlaceholder: '', emptyTitle: '接入 macOS 原生 iMessage',
37
+ emptyDescription: '无需安装第三方服务;请按指引授予 macOS Messages 访问权限。',
38
+ platformLabel: 'macOS Messages', credentialPayload: () => ({}),
39
+ CredentialPanel: PermissionPanel,
40
+ credentialAriaLabel: '配置 macOS Messages 权限', credentialOpenLabel: '配置本机权限',
41
+ credentialNoun: 'macOS Messages 权限', emptyActionLabel: '配置本机权限',
42
+ });
43
+
44
+ export const IMessageSettingsTab = channel.SettingsTab;
45
+ export const IMessageAccountCard = channel.AccountCard;
@@ -0,0 +1,19 @@
1
+ export const IMESSAGE_STYLE_ID = 'xmanrui-dsh-im-imessage-settings';
2
+ const CSS = String.raw`
3
+ .dim-pageIMessage { --ddt-accent: #32a852; --ddt-accent-deep: #248a40; --ddt-accent-wash: #edf9f0; }
4
+ .dim-avatarIMessage { color: #fff; background: #32a852; }
5
+ .dim-avatarIMessage svg { display: block; }
6
+ .dim-imessagePermissionPanel { padding: 20px; }
7
+ .dim-imessagePermissionPanel p { color: var(--dsw-alias-label-secondary, #646a73); }
8
+ .dim-imessagePermissionSteps { margin: 16px 0; padding-left: 22px; }
9
+ .dim-imessagePermissionSteps li { margin: 12px 0; }
10
+ .dim-imessagePermissionSteps a { color: var(--ddt-accent-deep); }
11
+ `;
12
+ export function installIMessageStyles() {
13
+ if (typeof document === 'undefined') return () => {};
14
+ const existing = document.querySelector(`style[data-plugin-css="${IMESSAGE_STYLE_ID}"]`);
15
+ if (existing) return () => {};
16
+ const style = document.createElement('style'); style.dataset.plugin = '@xmanrui/dsh-im';
17
+ style.dataset.pluginCss = IMESSAGE_STYLE_ID; style.textContent = CSS; document.head.appendChild(style);
18
+ return () => style.remove();
19
+ }
@@ -101,6 +101,7 @@ export function createTokenChannelApi(channel, connectionSummary, {
101
101
  totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
102
102
  agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
103
103
  modelCatalog: normalizeModelCatalog(source.modelCatalog),
104
+ ...(isRecord(source.permissions) ? { permissions: source.permissions } : {}),
104
105
  };
105
106
  };
106
107
 
@@ -182,7 +182,7 @@ export function createTokenChannelSettings(definition) {
182
182
  const [model, setModel] = React.useState({
183
183
  phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
184
184
  agentPresetCatalog: EMPTY_AGENT_PRESET_CATALOG,
185
- modelCatalog: EMPTY_MODEL_CATALOG,
185
+ modelCatalog: EMPTY_MODEL_CATALOG, permissions: null,
186
186
  });
187
187
  const [credentialOpen, setCredentialOpen] = React.useState(false);
188
188
  const [credentialError, setCredentialError] = React.useState(null);
@@ -221,6 +221,7 @@ export function createTokenChannelSettings(definition) {
221
221
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
222
222
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
223
223
  modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
224
+ permissions: snapshot.permissions ?? null,
224
225
  });
225
226
  } catch (error) {
226
227
  if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted
@@ -268,6 +269,7 @@ export function createTokenChannelSettings(definition) {
268
269
  phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null,
269
270
  agentPresetCatalog: snapshot.agentPresetCatalog ?? EMPTY_AGENT_PRESET_CATALOG,
270
271
  modelCatalog: snapshot.modelCatalog ?? EMPTY_MODEL_CATALOG,
272
+ permissions: snapshot.permissions ?? null,
271
273
  });
272
274
  }
273
275
  setCredentialOpen(false);
@@ -418,6 +420,8 @@ export function createTokenChannelSettings(definition) {
418
420
  : h(React.Fragment, null,
419
421
  credentialOpen ? (CredentialPanel
420
422
  ? h(CredentialPanel, {
423
+ channel,
424
+ permissions: model.permissions,
421
425
  busy,
422
426
  error: credentialError,
423
427
  onSubmit: bindCredentials,
@@ -115,6 +115,11 @@ const CHANNEL_DEFINITIONS = Object.freeze({
115
115
  group: [{ key: 'jid', label: '群 JID', placeholder: '1234567890@g.us' }],
116
116
  },
117
117
  },
118
+ imessage: {
119
+ label: 'iMessage',
120
+ kinds: [{ value: 'user', label: '私聊' }],
121
+ fields: { user: [{ key: 'chatGuid', label: 'Chat GUID', placeholder: 'iMessage;+;chat123' }] },
122
+ },
118
123
  });
119
124
 
120
125
  export const DELIVERY_CHANNEL_DEFINITIONS = CHANNEL_DEFINITIONS;
@@ -4,6 +4,27 @@ export const IM_LOCALE_NAMESPACE = 'dsh-im';
4
4
 
5
5
  const EN = Object.freeze({
6
6
  '$locale': 'en',
7
+ ' macOS Messages 连接': ' macOS Messages connection',
8
+ '接入 iMessage': 'Connect iMessage',
9
+ '先在 macOS 系统设置中授予 Messages 权限。': 'Grant Messages permissions in macOS System Settings first.',
10
+ '配置 macOS Messages 权限': 'Configure macOS Messages permissions',
11
+ '配置本机权限': 'Configure local permissions',
12
+ '启用 macOS 原生 iMessage': 'Enable native macOS iMessage',
13
+ '支持同账号自聊:在 iPhone 或 Mac 上给自己已登录的 iMessage 邮箱或号码发指令。AI 回复以 🤖 DSH 开头,不会再次触发机器人。': 'Self-chat is supported: send instructions from your iPhone or Mac to your signed-in iMessage email or phone number. AI replies start with 🤖 DSH and do not trigger the bot again.',
14
+ '检查权限并启用': 'Check permissions and enable',
15
+ 'DeepSeek Harness 通过 macOS Messages.app 收发文本消息。首次使用需要授予以下权限:': 'DeepSeek Harness sends and receives messages through macOS Messages.app. The following permissions are required the first time:',
16
+ '✓ 完全磁盘访问权限已授予': '✓ Full Disk Access granted',
17
+ '1. 授予完全磁盘访问权限': '1. Grant Full Disk Access',
18
+ '在“系统设置 → 隐私与安全性 → 完全磁盘访问权限”中,打开运行 DeepSeek Harness 的终端或应用。': 'In System Settings → Privacy & Security → Full Disk Access, enable the terminal or app running DeepSeek Harness.',
19
+ '打开完全磁盘访问权限设置': 'Open Full Disk Access settings',
20
+ '✓ Messages 自动化权限已授予': '✓ Messages automation granted',
21
+ '2. 允许自动化控制 Messages': '2. Allow automation control of Messages',
22
+ '在“系统设置 → 隐私与安全性 → 自动化”中,允许运行 DeepSeek Harness 的应用控制 Messages。': 'In System Settings → Privacy & Security → Automation, allow the app running DeepSeek Harness to control Messages.',
23
+ '打开自动化设置': 'Open Automation settings',
24
+ '正在检查…': 'Checking…',
25
+ '接入 macOS 原生 iMessage': 'Connect native macOS iMessage',
26
+ '无需安装第三方服务;请按指引授予 macOS Messages 访问权限。': 'No third-party service is required; follow the guide to grant macOS Messages access.',
27
+ 'macOS Messages 权限': 'macOS Messages permissions',
7
28
  'IM机器人': 'IM bots',
8
29
  'IM机器人设置': 'IM bot settings',
9
30
  '更多机器人设置': 'More bot settings',
@@ -1062,7 +1083,8 @@ function translateDynamic(text) {
1062
1083
 
1063
1084
  const phrases = [
1064
1085
  ['企业微信', 'WeCom'], ['DeepSeek Harness', 'DeepSeek Harness'],
1065
- ['WhatsApp', 'WhatsApp'], ['Telegram', 'Telegram'], ['Discord', 'Discord'],
1086
+ ['WhatsApp', 'WhatsApp'], ['iMessage', 'iMessage'], ['macOS Messages', 'macOS Messages'],
1087
+ ['Telegram', 'Telegram'], ['Discord', 'Discord'],
1066
1088
  ['Slack', 'Slack'], ['飞书', 'Feishu'], ['钉钉', 'DingTalk'], ['微信', 'WeChat'],
1067
1089
  ['机器人', 'bot'], ['账号', 'account'], ['应用', 'app'], ['凭据', 'credentials'],
1068
1090
  ['服务返回了无法识别的响应', 'service returned an unrecognized response'],
@@ -1,3 +1,4 @@
1
+ import { callManagementRpc } from '../management-rpc.mjs';
1
2
  import * as React from 'react';
2
3
  import manifest from '../../package.json' with { type: 'json' };
3
4
 
@@ -12,6 +13,7 @@ import {
12
13
  WecomLogoGlyph,
13
14
  WeixinLogoGlyph,
14
15
  WhatsappLogoGlyph,
16
+ IMessageLogoGlyph,
15
17
  } from './channel-logos.js';
16
18
  import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
17
19
  import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
@@ -45,6 +47,9 @@ import { installWeixinStyles } from './channels/weixin/styles.js';
45
47
  import { WHATSAPP_RPC_CHANNEL } from './channels/whatsapp/api.js';
46
48
  import { WhatsappSettingsTab } from './channels/whatsapp/index.js';
47
49
  import { installWhatsappStyles } from './channels/whatsapp/styles.js';
50
+ import { IMESSAGE_RPC_CHANNEL } from './channels/imessage/api.js';
51
+ import { IMessageSettingsTab } from './channels/imessage/index.js';
52
+ import { installIMessageStyles } from './channels/imessage/styles.js';
48
53
  import { en, h, IM_LOCALE_NAMESPACE, setImTranslator, zh } from './i18n.js';
49
54
  import { BotSettingsContext } from './channel-card-meta.js';
50
55
  import {
@@ -91,6 +96,7 @@ const CHANNELS = Object.freeze([
91
96
  { id: 'discord', label: 'Discord' },
92
97
  { id: 'whatsapp', label: 'WhatsApp' },
93
98
  { id: 'wecomApp', label: '企业微信应用', note: '(实验功能)' },
99
+ { id: 'imessage', label: 'iMessage', note: '(实验功能)' },
94
100
  { id: 'office', label: 'AI Office', note: '(实验功能)' },
95
101
  ]);
96
102
 
@@ -141,6 +147,11 @@ function WhatsappLogo() {
141
147
  h(WhatsappLogoGlyph));
142
148
  }
143
149
 
150
+ function IMessageLogo() {
151
+ return h('span', { className: 'dim-logo dim-logoIMessage', 'aria-hidden': 'true' },
152
+ h(IMessageLogoGlyph));
153
+ }
154
+
144
155
  function OfficeLogo() {
145
156
  return h('span', { className: 'dim-logo dim-logoOffice', 'aria-hidden': 'true' },
146
157
  h(OfficeLogoGlyph));
@@ -157,6 +168,7 @@ function ChannelLogo({ channel }) {
157
168
  if (channel === 'telegram') return h(TelegramLogo);
158
169
  if (channel === 'discord') return h(DiscordLogo);
159
170
  if (channel === 'whatsapp') return h(WhatsappLogo);
171
+ if (channel === 'imessage') return h(IMessageLogo);
160
172
  return h(OfficeLogo);
161
173
  }
162
174
 
@@ -180,6 +192,7 @@ export function IMSettingsTab({
180
192
  dingtalkRpcCall,
181
193
  discordRpcCall,
182
194
  feishuRpcCall,
195
+ imessageRpcCall,
183
196
  qqRpcCall,
184
197
  slackRpcCall,
185
198
  telegramRpcCall,
@@ -230,6 +243,7 @@ export function IMSettingsTab({
230
243
  updateRpcCall,
231
244
  deliveryRpcCall,
232
245
  globalSettingsRpcCall,
246
+ imessageRpcCall,
233
247
  }, {
234
248
  location: browserLocation,
235
249
  onRecovery: reportLoopbackRecovery,
@@ -240,6 +254,7 @@ export function IMSettingsTab({
240
254
  deliveryRpcCall,
241
255
  feishuRpcCall,
242
256
  globalSettingsRpcCall,
257
+ imessageRpcCall,
243
258
  officeRpcCall,
244
259
  qqRpcCall,
245
260
  reportLoopbackRecovery,
@@ -368,6 +383,8 @@ export function IMSettingsTab({
368
383
  ? h(DiscordSettingsTab, { rpcCall: rpcCalls.discordRpcCall })
369
384
  : active.id === 'whatsapp'
370
385
  ? h(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall })
386
+ : active.id === 'imessage'
387
+ ? h(IMessageSettingsTab, { rpcCall: rpcCalls.imessageRpcCall })
371
388
  : h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
372
389
  ),
373
390
  ));
@@ -394,6 +411,7 @@ export function apply(ctx) {
394
411
  installTelegramStyles(),
395
412
  installDiscordStyles(),
396
413
  installWhatsappStyles(),
414
+ installIMessageStyles(),
397
415
  installOfficeStyles(),
398
416
  installImStyles(),
399
417
  ];
@@ -403,33 +421,35 @@ export function apply(ctx) {
403
421
  }, 'im-settings: install combined channel styles');
404
422
 
405
423
  const feishuRpcCall = (endpoint, payload, signal) =>
406
- ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal);
424
+ callManagementRpc(ctx.connection, FEISHU_RPC_CHANNEL, endpoint, payload, signal);
407
425
  const weixinRpcCall = (endpoint, payload, signal) =>
408
- ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
426
+ callManagementRpc(ctx.connection, WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
409
427
  const dingtalkRpcCall = (endpoint, payload, signal) =>
410
- ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
428
+ callManagementRpc(ctx.connection, DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
411
429
  const qqRpcCall = (endpoint, payload, signal) =>
412
- ctx.connection.rpc.call(QQ_RPC_CHANNEL, endpoint, payload, signal);
430
+ callManagementRpc(ctx.connection, QQ_RPC_CHANNEL, endpoint, payload, signal);
413
431
  const wecomRpcCall = (endpoint, payload, signal) =>
414
- ctx.connection.rpc.call(WECOM_RPC_CHANNEL, endpoint, payload, signal);
432
+ callManagementRpc(ctx.connection, WECOM_RPC_CHANNEL, endpoint, payload, signal);
415
433
  const wecomAppRpcCall = (endpoint, payload, signal) =>
416
- ctx.connection.rpc.call(WECOM_APP_RPC_CHANNEL, endpoint, payload, signal);
434
+ callManagementRpc(ctx.connection, WECOM_APP_RPC_CHANNEL, endpoint, payload, signal);
417
435
  const telegramRpcCall = (endpoint, payload, signal) =>
418
- ctx.connection.rpc.call(TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
436
+ callManagementRpc(ctx.connection, TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
419
437
  const discordRpcCall = (endpoint, payload, signal) =>
420
- ctx.connection.rpc.call(DISCORD_RPC_CHANNEL, endpoint, payload, signal);
438
+ callManagementRpc(ctx.connection, DISCORD_RPC_CHANNEL, endpoint, payload, signal);
421
439
  const whatsappRpcCall = (endpoint, payload, signal) =>
422
- ctx.connection.rpc.call(WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
440
+ callManagementRpc(ctx.connection, WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
441
+ const imessageRpcCall = (endpoint, payload, signal) =>
442
+ callManagementRpc(ctx.connection, IMESSAGE_RPC_CHANNEL, endpoint, payload, signal);
423
443
  const slackRpcCall = (endpoint, payload, signal) =>
424
- ctx.connection.rpc.call(SLACK_RPC_CHANNEL, endpoint, payload, signal);
444
+ callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
425
445
  const officeRpcCall = (endpoint, payload, signal) =>
426
- ctx.connection.rpc.call(OFFICE_RPC_CHANNEL, endpoint, payload, signal);
446
+ callManagementRpc(ctx.connection, OFFICE_RPC_CHANNEL, endpoint, payload, signal);
427
447
  const updateRpcCall = (endpoint, payload, signal) =>
428
- ctx.connection.rpc.call(UPDATE_RPC_CHANNEL, endpoint, payload, signal);
448
+ callManagementRpc(ctx.connection, UPDATE_RPC_CHANNEL, endpoint, payload, signal);
429
449
  const deliveryRpcCall = (endpoint, payload, signal) =>
430
- ctx.connection.rpc.call(DELIVERY_RPC_CHANNEL, endpoint, payload, signal);
450
+ callManagementRpc(ctx.connection, DELIVERY_RPC_CHANNEL, endpoint, payload, signal);
431
451
  const globalSettingsRpcCall = (endpoint, payload, signal) =>
432
- ctx.connection.rpc.call(GLOBAL_SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
452
+ callManagementRpc(ctx.connection, GLOBAL_SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
433
453
  const workspaceDirectoryPicker = Object.freeze({
434
454
  listDirectory: (path, signal) =>
435
455
  callWorkspaceDirectoryApi(ctx, 'listDirectory', path, signal),
@@ -453,6 +473,7 @@ export function apply(ctx) {
453
473
  wecomAppRpcCall,
454
474
  weixinRpcCall,
455
475
  whatsappRpcCall,
476
+ imessageRpcCall,
456
477
  officeRpcCall,
457
478
  updateRpcCall,
458
479
  deliveryRpcCall,
@@ -2,6 +2,7 @@ import { parseSessionChannelTitle } from '../../src/channels/shared/session-chan
2
2
  import {
3
3
  WeixinLogoGlyph, FeishuLogoGlyph, DingtalkLogoGlyph, WecomLogoGlyph, QqLogoGlyph,
4
4
  SlackLogoGlyph, TelegramLogoGlyph, DiscordLogoGlyph, WhatsappLogoGlyph, OfficeLogoGlyph,
5
+ IMessageLogoGlyph,
5
6
  } from './channel-logos.js';
6
7
 
7
8
  const GLYPHS = Object.freeze({
@@ -14,6 +15,7 @@ const GLYPHS = Object.freeze({
14
15
  telegram: [TelegramLogoGlyph, '#229ed9'],
15
16
  discord: [DiscordLogoGlyph, '#5865f2'],
16
17
  whatsapp: [WhatsappLogoGlyph, '#25d366'],
18
+ imessage: [IMessageLogoGlyph, '#3478f6'],
17
19
  office: [OfficeLogoGlyph, '#f97316'],
18
20
  });
19
21
  const CHANNEL_ATTR = 'data-dsh-im-session-channel';
@@ -97,6 +97,7 @@ const CSS = String.raw`
97
97
  .dim-logoSlack { color: white; background: #4a154b; }
98
98
  .dim-logoSlack svg { width: 21px; height: 21px; }
99
99
  .dim-logoWhatsapp { color: white; background: #25d366; }
100
+ .dim-logoIMessage { color: white; background: #32a852; }
100
101
  .dim-logoWhatsapp svg { width: 21px; height: 21px; }
101
102
  .dim-channelCopy { min-width: 0; display: grid; }
102
103
  .dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from '../shared/context-enhancement-rpc.mjs';
3
4
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from '../shared/access-policy-rpc.mjs';
@@ -336,10 +337,7 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
336
337
  }
337
338
 
338
339
  export function installDingtalkRpc(ctx, controller, options, authority) {
339
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
340
- throw new TypeError('DSH Host Connection RPC is required');
341
- }
342
- return ctx.connection.rpc.handle(
340
+ return registerManagementRpc(ctx,
343
341
  DINGTALK_RPC_CHANNEL,
344
342
  createDingtalkRpcHandler(controller, options),
345
343
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import {
3
4
  normalizeAgentPresetCatalog,
@@ -795,10 +796,7 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
795
796
 
796
797
  /** Register the `/feishu` logical channel with its configured browser authority. */
797
798
  export function installFeishuRpc(ctx, controller, options, authority) {
798
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
799
- throw new TypeError('DSH Host Connection RPC is required');
800
- }
801
- return ctx.connection.rpc.handle(
799
+ return registerManagementRpc(ctx,
802
800
  FEISHU_RPC_CHANNEL,
803
801
  createFeishuRpcHandler(controller, options),
804
802
  { authority: resolveRpcAuthority(authority) },
@@ -0,0 +1,21 @@
1
+ import { createProductionController } from './production.mjs';
2
+ import { installIMessageRpc } from './rpc.mjs';
3
+
4
+ export const name = 'dsh-im-imessage-host';
5
+ export const inject = ['connection', 'credentials', 'typertGateway'];
6
+
7
+ export async function apply(ctx, config = {}) {
8
+ if (config?.controller) return installIMessageRpc(ctx, config.controller, config.rpcAuthority);
9
+ const production = await createProductionController(ctx, config, config.internals ?? {});
10
+ const unregisterDelivery = config.deliveryService && production.deliveryAdapter
11
+ ? config.deliveryService.registerAdapter(production.deliveryAdapter) : undefined;
12
+ const disposeRpc = installIMessageRpc(ctx, production.controller, config.rpcAuthority);
13
+ ctx.effect(() => async () => {
14
+ await unregisterDelivery?.(); await production.close();
15
+ }, 'dsh-im: close iMessage connections');
16
+ return disposeRpc;
17
+ }
18
+
19
+ export { createProductionController } from './production.mjs';
20
+ export { IMESSAGE_ENDPOINTS, IMESSAGE_RPC_CHANNEL, IMESSAGE_RPC_ENDPOINTS,
21
+ createIMessageRpcHandler, installIMessageRpc } from './rpc.mjs';
@@ -0,0 +1,13 @@
1
+ import { IMessageConfigStore } from '../../../../src/channels/imessage/config-store.mjs';
2
+ import { IMessageController } from '../../../../src/channels/imessage/controller.mjs';
3
+ import { IMessageHarnessClient } from '../../../../src/channels/imessage/harness-client.mjs';
4
+ import { IMessageRuntime } from '../../../../src/channels/imessage/runtime.mjs';
5
+ import { IMessageStateStore } from '../../../../src/channels/imessage/state-store.mjs';
6
+ import { createTokenProductionController } from '../shared/production.mjs';
7
+
8
+ export function createProductionController(ctx, config = {}, internals = {}) {
9
+ return createTokenProductionController(ctx, config, internals, {
10
+ channel: 'imessage', ConfigStore: IMessageConfigStore, StateStore: IMessageStateStore,
11
+ HarnessClient: IMessageHarnessClient, Controller: IMessageController, Runtime: IMessageRuntime,
12
+ });
13
+ }
@@ -0,0 +1,70 @@
1
+ import { createTokenBotRpcHandler } from '../shared/rpc.mjs';
2
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
3
+ import { resolveRpcAuthority } from '../../rpc-authority.mjs';
4
+
5
+ export const IMESSAGE_RPC_CHANNEL = '/imessage';
6
+ export const IMESSAGE_ENDPOINTS = Object.freeze({
7
+ status: 'connection.status',
8
+ bindNative: 'bot.bind-native',
9
+ bindCredentials: 'bot.bind-native',
10
+ permissions: 'permissions.status',
11
+ reconnectBot: 'bot.reconnect',
12
+ deleteBot: 'bot.delete',
13
+ setWorkspace: 'bot.workspace.set',
14
+ setModel: 'bot.model.set',
15
+ setAgentPreset: 'bot.agent-preset.set',
16
+ setContextEnhancement: 'bot.context-enhancement.set',
17
+ setAccessPolicy: 'bot.access-policy.set',
18
+ });
19
+ export const IMESSAGE_RPC_ENDPOINTS = Object.freeze(Object.values(IMESSAGE_ENDPOINTS));
20
+
21
+ function withRpcDetails(result) {
22
+ if (result?.ok !== false) return result;
23
+ return {
24
+ ...result,
25
+ error: { ...result.error, details: result.error?.details ?? {} },
26
+ };
27
+ }
28
+
29
+ export function createIMessageRpcHandler(controller) {
30
+ const tokenHandler = createTokenBotRpcHandler(controller, {
31
+ channel: 'iMessage',
32
+ });
33
+ return async (endpoint, payload, signal) => {
34
+ if (endpoint === IMESSAGE_ENDPOINTS.status) {
35
+ const value = await controller.status();
36
+ return { ok: true, value: { ...value, permissions: await controller.permissions() } };
37
+ }
38
+ if (endpoint === IMESSAGE_ENDPOINTS.permissions) {
39
+ try {
40
+ return { ok: true, value: await controller.permissions() };
41
+ } catch (error) {
42
+ return withRpcDetails({
43
+ ok: false,
44
+ error: { code: 'permissions-check-failed', message: error.message },
45
+ });
46
+ }
47
+ }
48
+ if (endpoint === IMESSAGE_ENDPOINTS.bindNative) {
49
+ try {
50
+ return { ok: true, value: await controller.bindNative() };
51
+ } catch (error) {
52
+ return withRpcDetails({
53
+ ok: false,
54
+ error: { code: error.code ?? 'imessage-bind-failed', message: error.message },
55
+ });
56
+ }
57
+ }
58
+ const translated = endpoint === IMESSAGE_ENDPOINTS.setModel ? 'bot.model.set' : endpoint;
59
+ return withRpcDetails(await tokenHandler(translated, payload, signal));
60
+ };
61
+ }
62
+
63
+ export function installIMessageRpc(ctx, controller, authority) {
64
+ return registerManagementRpc(
65
+ ctx,
66
+ IMESSAGE_RPC_CHANNEL,
67
+ createIMessageRpcHandler(controller),
68
+ { authority: resolveRpcAuthority(authority) },
69
+ );
70
+ }
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
3
  import { OFFICE_RPC_CHANNEL, OFFICE_RPC_ENDPOINTS } from '../../../../src/channels/office/protocol.mjs';
3
4
 
@@ -40,7 +41,7 @@ export function createOfficeRpcHandler(controller) {
40
41
  }
41
42
 
42
43
  export function installOfficeRpc(ctx, controller, authority) {
43
- return ctx.connection.rpc.handle(
44
+ return registerManagementRpc(ctx,
44
45
  OFFICE_RPC_CHANNEL,
45
46
  createOfficeRpcHandler(controller),
46
47
  { authority: resolveRpcAuthority(authority) },
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import QRCode from 'qrcode';
2
3
  import {
3
4
  connectionTestTargetUnavailable,
@@ -227,10 +228,7 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
227
228
  }
228
229
 
229
230
  export function installQqRpc(ctx, controller, options, authority) {
230
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
231
- throw new TypeError('DSH Host Connection RPC is required');
232
- }
233
- return ctx.connection.rpc.handle(
231
+ return registerManagementRpc(ctx,
234
232
  QQ_RPC_CHANNEL,
235
233
  createQqRpcHandler(controller, options),
236
234
  { authority: resolveRpcAuthority(authority) },
@@ -91,7 +91,7 @@ export function initialAccessPolicyFor(channel, config = {}) {
91
91
  group: allowlistScope(),
92
92
  });
93
93
  }
94
- if (['dingtalk', 'wecom', 'wecom-app', 'slack', 'discord'].includes(key)) {
94
+ if (['dingtalk', 'wecom', 'wecom-app', 'slack', 'discord', 'imessage'].includes(key)) {
95
95
  return createAccessPolicy({ direct: openScope(), group: openScope() });
96
96
  }
97
97
  throw new TypeError(`Unsupported access-policy channel: ${channel}`);
@@ -1,3 +1,4 @@
1
+ import { registerManagementRpc } from '../../../management-rpc.mjs';
1
2
  import { SET_CONTEXT_ENHANCEMENT_ENDPOINT, validContextEnhancementPayload } from './context-enhancement-rpc.mjs';
2
3
  import { SET_ACCESS_POLICY_ENDPOINT, validAccessPolicyPayload } from './access-policy-rpc.mjs';
3
4
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
@@ -197,10 +198,7 @@ export function createTokenBotRpcHandler(controller, { channel }) {
197
198
  }
198
199
 
199
200
  export function installTokenBotRpc(ctx, controller, { channel, rpcChannel, authority }) {
200
- if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
201
- throw new TypeError('DSH Host Connection RPC is required');
202
- }
203
- return ctx.connection.rpc.handle(
201
+ return registerManagementRpc(ctx,
204
202
  rpcChannel,
205
203
  createTokenBotRpcHandler(controller, { channel }),
206
204
  { authority: resolveRpcAuthority(authority) },