@xmanrui/dsh-im 4.17.1 → 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 (34) hide show
  1. package/README.en.md +17 -13
  2. package/README.md +16 -15
  3. package/lib/client.js +424 -246
  4. package/lib/index.js +280 -258
  5. package/package.json +7 -2
  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 +20 -0
  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/imessage/index.mjs +21 -0
  19. package/plugin-src/host/channels/imessage/production.mjs +13 -0
  20. package/plugin-src/host/channels/imessage/rpc.mjs +70 -0
  21. package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
  22. package/plugin-src/host/delivery-adapter.mjs +4 -0
  23. package/plugin-src/host/index.mjs +3 -0
  24. package/plugin-src/host/modern-harness-api.mjs +1 -1
  25. package/src/channels/imessage/config-store.mjs +24 -0
  26. package/src/channels/imessage/controller.mjs +37 -0
  27. package/src/channels/imessage/harness-client.mjs +7 -0
  28. package/src/channels/imessage/imessage-api.mjs +194 -0
  29. package/src/channels/imessage/imessage-bridge.mjs +16 -0
  30. package/src/channels/imessage/runtime.mjs +98 -0
  31. package/src/channels/imessage/state-store.mjs +3 -0
  32. package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
  33. package/src/channels/shared/message-failure.mjs +5 -5
  34. 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.1",
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",
@@ -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'],
@@ -13,6 +13,7 @@ import {
13
13
  WecomLogoGlyph,
14
14
  WeixinLogoGlyph,
15
15
  WhatsappLogoGlyph,
16
+ IMessageLogoGlyph,
16
17
  } from './channel-logos.js';
17
18
  import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
18
19
  import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
@@ -46,6 +47,9 @@ import { installWeixinStyles } from './channels/weixin/styles.js';
46
47
  import { WHATSAPP_RPC_CHANNEL } from './channels/whatsapp/api.js';
47
48
  import { WhatsappSettingsTab } from './channels/whatsapp/index.js';
48
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';
49
53
  import { en, h, IM_LOCALE_NAMESPACE, setImTranslator, zh } from './i18n.js';
50
54
  import { BotSettingsContext } from './channel-card-meta.js';
51
55
  import {
@@ -92,6 +96,7 @@ const CHANNELS = Object.freeze([
92
96
  { id: 'discord', label: 'Discord' },
93
97
  { id: 'whatsapp', label: 'WhatsApp' },
94
98
  { id: 'wecomApp', label: '企业微信应用', note: '(实验功能)' },
99
+ { id: 'imessage', label: 'iMessage', note: '(实验功能)' },
95
100
  { id: 'office', label: 'AI Office', note: '(实验功能)' },
96
101
  ]);
97
102
 
@@ -142,6 +147,11 @@ function WhatsappLogo() {
142
147
  h(WhatsappLogoGlyph));
143
148
  }
144
149
 
150
+ function IMessageLogo() {
151
+ return h('span', { className: 'dim-logo dim-logoIMessage', 'aria-hidden': 'true' },
152
+ h(IMessageLogoGlyph));
153
+ }
154
+
145
155
  function OfficeLogo() {
146
156
  return h('span', { className: 'dim-logo dim-logoOffice', 'aria-hidden': 'true' },
147
157
  h(OfficeLogoGlyph));
@@ -158,6 +168,7 @@ function ChannelLogo({ channel }) {
158
168
  if (channel === 'telegram') return h(TelegramLogo);
159
169
  if (channel === 'discord') return h(DiscordLogo);
160
170
  if (channel === 'whatsapp') return h(WhatsappLogo);
171
+ if (channel === 'imessage') return h(IMessageLogo);
161
172
  return h(OfficeLogo);
162
173
  }
163
174
 
@@ -181,6 +192,7 @@ export function IMSettingsTab({
181
192
  dingtalkRpcCall,
182
193
  discordRpcCall,
183
194
  feishuRpcCall,
195
+ imessageRpcCall,
184
196
  qqRpcCall,
185
197
  slackRpcCall,
186
198
  telegramRpcCall,
@@ -231,6 +243,7 @@ export function IMSettingsTab({
231
243
  updateRpcCall,
232
244
  deliveryRpcCall,
233
245
  globalSettingsRpcCall,
246
+ imessageRpcCall,
234
247
  }, {
235
248
  location: browserLocation,
236
249
  onRecovery: reportLoopbackRecovery,
@@ -241,6 +254,7 @@ export function IMSettingsTab({
241
254
  deliveryRpcCall,
242
255
  feishuRpcCall,
243
256
  globalSettingsRpcCall,
257
+ imessageRpcCall,
244
258
  officeRpcCall,
245
259
  qqRpcCall,
246
260
  reportLoopbackRecovery,
@@ -369,6 +383,8 @@ export function IMSettingsTab({
369
383
  ? h(DiscordSettingsTab, { rpcCall: rpcCalls.discordRpcCall })
370
384
  : active.id === 'whatsapp'
371
385
  ? h(WhatsappSettingsTab, { rpcCall: rpcCalls.whatsappRpcCall })
386
+ : active.id === 'imessage'
387
+ ? h(IMessageSettingsTab, { rpcCall: rpcCalls.imessageRpcCall })
372
388
  : h(OfficeSettingsTab, { rpcCall: rpcCalls.officeRpcCall }))),
373
389
  ),
374
390
  ));
@@ -395,6 +411,7 @@ export function apply(ctx) {
395
411
  installTelegramStyles(),
396
412
  installDiscordStyles(),
397
413
  installWhatsappStyles(),
414
+ installIMessageStyles(),
398
415
  installOfficeStyles(),
399
416
  installImStyles(),
400
417
  ];
@@ -421,6 +438,8 @@ export function apply(ctx) {
421
438
  callManagementRpc(ctx.connection, DISCORD_RPC_CHANNEL, endpoint, payload, signal);
422
439
  const whatsappRpcCall = (endpoint, payload, signal) =>
423
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);
424
443
  const slackRpcCall = (endpoint, payload, signal) =>
425
444
  callManagementRpc(ctx.connection, SLACK_RPC_CHANNEL, endpoint, payload, signal);
426
445
  const officeRpcCall = (endpoint, payload, signal) =>
@@ -454,6 +473,7 @@ export function apply(ctx) {
454
473
  wecomAppRpcCall,
455
474
  weixinRpcCall,
456
475
  whatsappRpcCall,
476
+ imessageRpcCall,
457
477
  officeRpcCall,
458
478
  updateRpcCall,
459
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; }
@@ -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
+ }
@@ -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}`);
@@ -15,6 +15,7 @@ const CHANNELS = new Set([
15
15
  'telegram',
16
16
  'discord',
17
17
  'whatsapp',
18
+ 'imessage',
18
19
  ]);
19
20
 
20
21
  export function supportsDeliveryChannel(channel) {
@@ -122,6 +123,9 @@ function normalizeRoute(channel, kind, route) {
122
123
  }
123
124
  return normalized;
124
125
  }
126
+ case 'imessage':
127
+ oneOf(kind, ['user']);
128
+ return routeWithStrings(route, ['chatGuid']);
125
129
  default:
126
130
  throw new TypeError(`Unsupported delivery channel: ${channel}`);
127
131
  }
@@ -9,6 +9,7 @@ import { apply as applyWecom } from './channels/wecom/index.mjs';
9
9
  import { apply as applyWecomApp } from './channels/wecom-app/index.mjs';
10
10
  import { apply as applyWeixin } from './channels/weixin/index.mjs';
11
11
  import { apply as applyWhatsapp } from './channels/whatsapp/index.mjs';
12
+ import { apply as applyIMessage } from './channels/imessage/index.mjs';
12
13
  import { installOutboundArtifactTool } from '../../src/channels/shared/semantic/artifact.mjs';
13
14
  import { setImHostLanguage } from '../../src/channels/shared/i18n.mjs';
14
15
  import { installDeliveryRpc } from './delivery-rpc.mjs';
@@ -53,6 +54,7 @@ export function createImHostPlugin(internals = {}) {
53
54
  const startDiscord = internals.applyDiscord ?? applyDiscord;
54
55
  const startOffice = internals.applyOffice ?? applyOffice;
55
56
  const startWhatsapp = internals.applyWhatsapp ?? applyWhatsapp;
57
+ const startIMessage = internals.applyIMessage ?? applyIMessage;
56
58
  const channels = [
57
59
  ['feishu', startFeishu],
58
60
  ['weixin', startWeixin],
@@ -64,6 +66,7 @@ export function createImHostPlugin(internals = {}) {
64
66
  ['telegram', startTelegram],
65
67
  ['discord', startDiscord],
66
68
  ['whatsapp', startWhatsapp],
69
+ ['imessage', startIMessage],
67
70
  ['office', startOffice],
68
71
  ];
69
72
  return Object.freeze({
@@ -5,7 +5,7 @@ import { hasActiveHarnessInteractionOwner } from '../../src/channels/shared/harn
5
5
  const modernApis = new WeakMap();
6
6
 
7
7
  function failureOf(error) {
8
- const failure = error?.failure;
8
+ const failure = error?.failure ?? (error?.isDSHRemoteError === true ? error : undefined);
9
9
  if (failure && typeof failure === 'object'
10
10
  && typeof failure.code === 'string'
11
11
  && typeof failure.message === 'string') {
@@ -0,0 +1,24 @@
1
+ import {
2
+ deriveTokenBotIdentity,
3
+ maskPlatformId,
4
+ TokenBotConfigStore,
5
+ } from '../shared/token-config-store.mjs';
6
+
7
+ const IDENTITY_OPTIONS = Object.freeze({
8
+ botPrefix: 'imessage',
9
+ tokenRefPrefix: 'DSH_IMESSAGE_PASSWORD',
10
+ });
11
+
12
+ export function deriveIMessageBotIdentity(platformId) {
13
+ return deriveTokenBotIdentity(platformId, IDENTITY_OPTIONS);
14
+ }
15
+
16
+ export function maskIMessageBotId(platformId) {
17
+ return maskPlatformId(platformId, 'iMessage 网关');
18
+ }
19
+
20
+ export class IMessageConfigStore extends TokenBotConfigStore {
21
+ constructor(path) {
22
+ super(path, { channel: 'iMessage', ...IDENTITY_OPTIONS });
23
+ }
24
+ }
@@ -0,0 +1,37 @@
1
+ import { TokenBotController } from '../shared/token-bot-controller.mjs';
2
+ import { MacOSMessagesApi } from './imessage-api.mjs';
3
+ import {
4
+ deriveIMessageBotIdentity,
5
+ maskIMessageBotId,
6
+ } from './config-store.mjs';
7
+ import { IMESSAGE_DESCRIPTOR } from './imessage-bridge.mjs';
8
+
9
+ const NATIVE_CREDENTIAL = 'macos-messages-native';
10
+
11
+ export async function inspectIMessageCredential(value, { api = new MacOSMessagesApi() } = {}) {
12
+ if (value !== NATIVE_CREDENTIAL) throw new TypeError('Invalid native iMessage credential');
13
+ const permissions = await api.getPermissions();
14
+ return { platformId: 'macos-messages', name: 'Mac Messages', permissions };
15
+ }
16
+
17
+ export class IMessageController extends TokenBotController {
18
+ constructor(options) {
19
+ super({
20
+ ...options,
21
+ descriptor: IMESSAGE_DESCRIPTOR,
22
+ inspectToken: options.inspectToken ?? inspectIMessageCredential,
23
+ deriveIdentity: deriveIMessageBotIdentity,
24
+ maskPlatformId: maskIMessageBotId,
25
+ });
26
+ }
27
+
28
+ async bindNative() {
29
+ return super.bindCredentials({ token: NATIVE_CREDENTIAL });
30
+ }
31
+
32
+ async permissions() {
33
+ return new MacOSMessagesApi().getPermissions();
34
+ }
35
+ }
36
+
37
+ export { NATIVE_CREDENTIAL };
@@ -0,0 +1,7 @@
1
+ import { HarnessClient } from '../shared/harness-client.mjs';
2
+
3
+ export class IMessageHarnessClient extends HarnessClient {
4
+ constructor(options) {
5
+ super({ ...options, rpcIdPrefix: 'imessage', logPrefix: 'dsh-imessage' });
6
+ }
7
+ }