@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.
- package/README.en.md +20 -14
- package/README.md +19 -16
- package/lib/client.js +447 -257
- package/lib/index.js +285 -263
- package/package.json +10 -4
- package/plugin-src/client/access-policy-settings.js +5 -0
- package/plugin-src/client/channel-logos.js +10 -0
- package/plugin-src/client/channels/imessage/api.js +12 -0
- package/plugin-src/client/channels/imessage/index.js +45 -0
- package/plugin-src/client/channels/imessage/styles.js +19 -0
- package/plugin-src/client/channels/shared/token-api.js +1 -0
- package/plugin-src/client/channels/shared/token-channel.js +5 -1
- package/plugin-src/client/delivery-settings.js +5 -0
- package/plugin-src/client/i18n.js +23 -1
- package/plugin-src/client/index.js +35 -14
- package/plugin-src/client/session-channel-logos.js +2 -0
- package/plugin-src/client/styles.js +1 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +2 -4
- package/plugin-src/host/channels/feishu/rpc.mjs +2 -4
- package/plugin-src/host/channels/imessage/index.mjs +21 -0
- package/plugin-src/host/channels/imessage/production.mjs +13 -0
- package/plugin-src/host/channels/imessage/rpc.mjs +70 -0
- package/plugin-src/host/channels/office/rpc.mjs +2 -1
- package/plugin-src/host/channels/qq/rpc.mjs +2 -4
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/rpc.mjs +2 -4
- package/plugin-src/host/channels/shared/startup.mjs +2 -4
- package/plugin-src/host/channels/slack/rpc.mjs +2 -4
- package/plugin-src/host/channels/telegram/rpc.mjs +2 -4
- package/plugin-src/host/channels/wecom/rpc.mjs +2 -4
- package/plugin-src/host/channels/wecom-app/rpc.mjs +2 -4
- package/plugin-src/host/channels/weixin/rpc.mjs +2 -4
- package/plugin-src/host/channels/whatsapp/rpc.mjs +2 -4
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/delivery-rpc.mjs +2 -4
- package/plugin-src/host/inbound-ttl-rpc.mjs +2 -4
- package/plugin-src/host/index.mjs +4 -1
- package/plugin-src/host/modern-harness-api.mjs +1 -1
- package/plugin-src/host/update-rpc.mjs +2 -1
- package/plugin-src/management-rpc.mjs +77 -0
- package/src/channels/imessage/config-store.mjs +24 -0
- package/src/channels/imessage/controller.mjs +37 -0
- package/src/channels/imessage/harness-client.mjs +7 -0
- package/src/channels/imessage/imessage-api.mjs +194 -0
- package/src/channels/imessage/imessage-bridge.mjs +16 -0
- package/src/channels/imessage/runtime.mjs +98 -0
- package/src/channels/imessage/state-store.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -2
- package/src/channels/shared/message-failure.mjs +5 -5
- 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
|
+
}
|
|
@@ -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
|
|
93
|
-
'The current Agent Preset
|
|
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
|
|
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 (
|
|
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_
|
|
174
|
-
return value.toUpperCase().
|
|
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
|
|