@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
@@ -0,0 +1,98 @@
1
+ import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
2
+ import { MacOSMessagesApi, normalizeIMessage } from './imessage-api.mjs';
3
+ import { createIMessageBridgeStatus, IMessageHarnessBridge } from './imessage-bridge.mjs';
4
+
5
+ const DEFAULT_POLL_INTERVAL_MS = 2_000;
6
+
7
+ class IMessageBotClient {
8
+ #api;
9
+ #signal;
10
+ constructor(api, signal) { this.#api = api; this.#signal = signal; }
11
+ sendText(target, text) {
12
+ return this.#api.sendText({
13
+ chatGuid: target.chatGuid,
14
+ address: target.address,
15
+ text,
16
+ signal: this.#signal,
17
+ });
18
+ }
19
+ sendTyping() { return Promise.resolve(); }
20
+ }
21
+
22
+ export function createIMessageRuntimeStatus() {
23
+ return { startedAt: null, ready: false, connectionState: 'idle', harnessReachable: false,
24
+ lastCheckedAt: null, lastConnectedAt: null, lastError: null, ...createIMessageBridgeStatus() };
25
+ }
26
+
27
+ export class IMessageRuntime {
28
+ #config; #token; #harness; #state; #contextEnhancement; #accessPolicy; #logger;
29
+ #replyTimeoutMs; #pollIntervalMs; #createApi; #status = createIMessageRuntimeStatus();
30
+ #api; #bridge; #abortController; #timer; #polling; #stopped = true;
31
+ constructor({ config, token, harness, state, contextEnhancement, accessPolicy, logger = console,
32
+ replyTimeoutMs = 600_000, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
33
+ createApi = (options) => new MacOSMessagesApi(options) }) {
34
+ if (!config || !token || !harness || !state) throw new TypeError('IMessageRuntime requires config, token, Harness, and state');
35
+ this.#config = config; this.#token = token; this.#harness = harness; this.#state = state;
36
+ this.#contextEnhancement = contextEnhancement; this.#accessPolicy = accessPolicy; this.#logger = logger;
37
+ this.#replyTimeoutMs = replyTimeoutMs; this.#pollIntervalMs = pollIntervalMs; this.#createApi = createApi;
38
+ }
39
+ get status() { return structuredClone(this.#status); }
40
+ async sendConnectionTest(text) {
41
+ if (!this.#status.ready || !this.#api) { const error = new Error('iMessage gateway is not connected'); error.code = 'test-target-unavailable'; throw error; }
42
+ await sendRememberedConnectionTest({
43
+ state: this.#state,
44
+ text,
45
+ channelLabel: 'iMessage',
46
+ send: (target, value) => this.#api.sendText({
47
+ chatGuid: target.chatGuid, address: target.address, text: value,
48
+ }),
49
+ });
50
+ }
51
+ async sendProactiveText(target, text, options = {}) {
52
+ if (!this.#status.ready || !this.#api) { const error = new Error('iMessage gateway is not connected'); error.code = 'bot-not-connected'; throw error; }
53
+ const chatGuid = target?.route?.chatGuid;
54
+ if (!chatGuid) { const error = new TypeError('Invalid iMessage proactive delivery target'); error.code = 'invalid-target'; throw error; }
55
+ await this.#api.sendText({ chatGuid, text, signal: options.signal });
56
+ return { sent: true };
57
+ }
58
+ async start() {
59
+ if (this.#status.ready) return this.status;
60
+ await this.stop(); this.#stopped = false; this.#status.startedAt = new Date().toISOString(); this.#status.connectionState = 'connecting';
61
+ this.#abortController = new AbortController();
62
+ try {
63
+ await this.#harness.ensureRunning(); this.#status.harnessReachable = true;
64
+ this.#api = this.#createApi({ signal: this.#abortController.signal });
65
+ const permissions = await this.#api.getPermissions();
66
+ if (permissions.database !== 'granted' || permissions.automation !== 'granted') {
67
+ const error = new Error('macOS Messages permissions are required');
68
+ error.code = 'messages-permission-required';
69
+ error.permissions = permissions;
70
+ throw error;
71
+ }
72
+ if (this.#state.cursor() === null && typeof this.#api.getLatestMessageRowId === 'function') {
73
+ await this.#state.setCursor(await this.#api.getLatestMessageRowId());
74
+ }
75
+ const client = new IMessageBotClient(this.#api, this.#abortController.signal);
76
+ this.#bridge = new IMessageHarnessBridge({ bot: client, harness: this.#harness, state: this.#state,
77
+ contextEnhancement: this.#contextEnhancement, accessPolicy: this.#accessPolicy, status: this.#status,
78
+ logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs, signal: this.#abortController.signal });
79
+ this.#status.ready = true; this.#status.connectionState = 'connected'; this.#status.lastConnectedAt = Date.now();
80
+ this.#schedulePoll(0); return this.status;
81
+ } catch (error) { this.#status.ready = false; this.#status.connectionState = 'failed'; this.#status.lastError = error.message; await this.stop(); throw error; }
82
+ }
83
+ async stop() { this.#stopped = true; clearTimeout(this.#timer); this.#timer = null; this.#abortController?.abort(); await this.#polling?.catch(() => {}); this.#polling = null; this.#bridge = null; this.#api = null; this.#status.ready = false; }
84
+ #schedulePoll(delay) { if (this.#stopped) return; this.#timer = setTimeout(() => { this.#polling = this.#poll().finally(() => this.#schedulePoll(this.#pollIntervalMs)); }, delay); this.#timer.unref?.(); }
85
+ async #poll() {
86
+ if (!this.#api || !this.#bridge || this.#stopped) return;
87
+ try {
88
+ const rows = await this.#api.listMessages({ after: this.#state.cursor() ?? 0, limit: 100 });
89
+ const messages = Array.isArray(rows) ? rows : rows?.messages ?? rows?.data ?? [];
90
+ for (const raw of messages) {
91
+ const message = normalizeIMessage(raw, { botId: this.#config.platformId });
92
+ if (message) await this.#bridge.accept(message);
93
+ if (Number.isSafeInteger(raw.rowid)) await this.#state.setCursor(raw.rowid);
94
+ }
95
+ this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null;
96
+ } catch (error) { if (!this.#stopped) { this.#status.lastError = error.message; this.#logger.warn?.('[dsh-im:imessage] polling failed', error); } }
97
+ }
98
+ }
@@ -0,0 +1,3 @@
1
+ import { ConversationStateStore } from '../shared/conversation-state-store.mjs';
2
+
3
+ export class IMessageStateStore extends ConversationStateStore {}
@@ -89,8 +89,8 @@ export default {
89
89
  'The Workspace or Session state just changed. Please send this message again.',
90
90
  '当前工作区不存在或暂不可用。请重新选择工作区后重试。':
91
91
  'The current Workspace does not exist or is unavailable. Select another Workspace and try again.',
92
- '当前 Agent Preset 不存在或暂不可用。请发送 /presetlist 后重新选择。':
93
- 'The current Agent Preset does not exist or is unavailable. Send /presetlist and select another one.',
92
+ '当前 Agent Preset 无法使用。请发送 /presetlist 查看可用项,使用 /preset <序号或 ID> 重新选择,再发送 /new 创建新会话后重试。如需继续原会话,请联系管理员恢复原 Preset。':
93
+ 'The current Agent Preset is unavailable. Send /presetlist to see available options, select one with /preset <index or ID>, then send /new to create a new Session and try again. To continue the original Session, ask an administrator to restore the original Preset.',
94
94
  '回复已经生成,但机器人没有发送权限。请联系管理员检查渠道权限或重新绑定机器人。':
95
95
  'The reply was generated, but the bot cannot send it. Ask an administrator to check channel permissions or reconnect the bot.',
96
96
  '回复已经生成,但当前渠道正在限流,暂时无法发送。请稍后重试。':
@@ -77,7 +77,7 @@ const FAILURE_MESSAGES = Object.freeze({
77
77
  WORKSPACE_UNAVAILABLE:
78
78
  '当前工作区不存在或暂不可用。请重新选择工作区后重试。',
79
79
  PRESET_UNAVAILABLE:
80
- '当前 Agent Preset 不存在或暂不可用。请发送 /presetlist 后重新选择。',
80
+ '当前 Agent Preset 无法使用。请发送 /presetlist 查看可用项,使用 /preset <序号或 ID> 重新选择,再发送 /new 创建新会话后重试。如需继续原会话,请联系管理员恢复原 Preset。',
81
81
  CHANNEL_PERMISSION:
82
82
  '回复已经生成,但机器人没有发送权限。请联系管理员检查渠道权限或重新绑定机器人。',
83
83
  CHANNEL_RATE_LIMIT:
@@ -133,7 +133,7 @@ function failureCode(error) {
133
133
  if (code === 'agent-busy') return 'SESSION_BUSY';
134
134
  if (code === 'workspace-session-stale') return 'SESSION_STALE';
135
135
  if (code.startsWith('workspace-')) return 'WORKSPACE_UNAVAILABLE';
136
- if (code.startsWith('agent-preset-')) return 'PRESET_UNAVAILABLE';
136
+ if (/^agent-preset[-/]/u.test(code)) return 'PRESET_UNAVAILABLE';
137
137
  if (code.startsWith('image-') || code.startsWith('inbound-file-')
138
138
  || code === 'attachment-error') return 'INPUT_INVALID';
139
139
 
@@ -170,8 +170,8 @@ function safeReferenceId(value) {
170
170
  }
171
171
 
172
172
  function safeFailureReason(value) {
173
- if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/u.test(value)) return null;
174
- return value.toUpperCase().replaceAll('-', '_');
173
+ if (typeof value !== 'string' || !/^[A-Za-z0-9_/-]{1,64}$/u.test(value)) return null;
174
+ return value.toUpperCase().replace(/[-/]/gu, '_');
175
175
  }
176
176
 
177
177
  export function classifyMessageFailure(error, {
@@ -190,7 +190,7 @@ export function classifyMessageFailure(error, {
190
190
  : classifiedCode;
191
191
  return Object.freeze({
192
192
  code,
193
- reason: safeReason ?? code,
193
+ reason: safeReason ?? safeFailureReason(error?.code) ?? code,
194
194
  message: typeof userMessage === 'string' && userMessage.trim()
195
195
  ? userMessage.trim()
196
196
  : t(FAILURE_MESSAGES[code]),
@@ -8,6 +8,7 @@ export const SESSION_CHANNEL_LABELS = Object.freeze(Object.fromEntries(Object.en
8
8
  telegram: ['Telegram', 'Telegram'],
9
9
  discord: ['Discord', 'Discord'],
10
10
  whatsapp: ['WhatsApp', 'WhatsApp'],
11
+ imessage: ['iMessage', 'iMessage'],
11
12
  office: ['AI Office', 'AI Office'],
12
13
  }).map(([channel, labels]) => [channel, Object.freeze(labels)])));
13
14