@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.
- package/README.en.md +17 -13
- package/README.md +16 -15
- package/lib/client.js +424 -246
- package/lib/index.js +280 -258
- package/package.json +7 -2
- 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 +20 -0
- package/plugin-src/client/session-channel-logos.js +2 -0
- package/plugin-src/client/styles.js +1 -0
- 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/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/delivery-adapter.mjs +4 -0
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/modern-harness-api.mjs +1 -1
- 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,194 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const DEFAULT_DB_PATH = `${process.env.HOME ?? ''}/Library/Messages/chat.db`;
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
7
|
+
// Self-chat has no separate bot sender. Keep the reply marker in Messages itself
|
|
8
|
+
// so echoes are still recognizable after a Host restart or iCloud resync.
|
|
9
|
+
export const IMESSAGE_BOT_REPLY_PREFIX = '🤖 DSH\n';
|
|
10
|
+
|
|
11
|
+
function cleanString(value) {
|
|
12
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeChatGuid(value) {
|
|
16
|
+
const chatGuid = cleanString(value?.chatGuid ?? value);
|
|
17
|
+
if (!chatGuid || chatGuid.length > 512 || /[\r\n]/.test(chatGuid)) {
|
|
18
|
+
throw new TypeError('iMessage chatGuid is required');
|
|
19
|
+
}
|
|
20
|
+
return chatGuid;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeAddress(value) {
|
|
24
|
+
const address = cleanString(value);
|
|
25
|
+
if (!address || address.length > 512 || /[\r\n]/.test(address)) {
|
|
26
|
+
throw new TypeError('iMessage address is required');
|
|
27
|
+
}
|
|
28
|
+
return address;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function permissionError(kind, message, cause) {
|
|
32
|
+
const error = new Error(message, { cause });
|
|
33
|
+
error.code = kind;
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sqlString(value) {
|
|
38
|
+
return `'${String(value).replaceAll("'", "''")}'`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decodeRows(stdout) {
|
|
42
|
+
const text = String(stdout ?? '').trim();
|
|
43
|
+
if (!text) return [];
|
|
44
|
+
try {
|
|
45
|
+
const rows = JSON.parse(text);
|
|
46
|
+
return Array.isArray(rows) ? rows : [];
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error('macOS Messages returned invalid database output', { cause: error });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function appleScriptString(value) {
|
|
53
|
+
return JSON.stringify(String(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function normalizeIMessageTarget(value) {
|
|
57
|
+
return normalizeChatGuid(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeIMessage(value, { botId } = {}) {
|
|
61
|
+
if (!value || typeof value !== 'object') return null;
|
|
62
|
+
if (value.serviceName !== undefined && value.serviceName !== 'iMessage') return null;
|
|
63
|
+
const guid = cleanString(value.guid ?? value.id);
|
|
64
|
+
const chatGuid = cleanString(value.chatGuid ?? value.chat_guid);
|
|
65
|
+
const text = cleanString(value.text);
|
|
66
|
+
const sender = cleanString(value.sender ?? value.handle_id);
|
|
67
|
+
if (!guid || !chatGuid || !text || !sender) return null;
|
|
68
|
+
if (text.startsWith(IMESSAGE_BOT_REPLY_PREFIX)) return null;
|
|
69
|
+
if (value.isFromMe === 1 || value.isFromMe === true) return null;
|
|
70
|
+
if (botId && sender === botId) return null;
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
messageId: guid,
|
|
73
|
+
providerMessageId: guid,
|
|
74
|
+
conversationId: chatGuid,
|
|
75
|
+
kind: 'direct',
|
|
76
|
+
senderId: sender,
|
|
77
|
+
senderName: sender,
|
|
78
|
+
content: text,
|
|
79
|
+
addressed: true,
|
|
80
|
+
replyTarget: { chatGuid, address: sender, serviceName: 'iMessage' },
|
|
81
|
+
connectionTestTarget: { chatGuid, address: sender, serviceName: 'iMessage' },
|
|
82
|
+
...(value.receivedAt ? { receivedAt: value.receivedAt } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class MacOSMessagesApi {
|
|
87
|
+
#dbPath;
|
|
88
|
+
#execFile;
|
|
89
|
+
#execFileOptions;
|
|
90
|
+
#osascript;
|
|
91
|
+
|
|
92
|
+
constructor({ dbPath = DEFAULT_DB_PATH, execFileImpl = execFileAsync, osascriptImpl } = {}) {
|
|
93
|
+
this.#dbPath = dbPath;
|
|
94
|
+
this.#execFile = execFileImpl;
|
|
95
|
+
this.#execFileOptions = { timeout: DEFAULT_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 };
|
|
96
|
+
this.#osascript = osascriptImpl ?? ((script) => this.#execFile('/usr/bin/osascript', ['-e', script], this.#execFileOptions));
|
|
97
|
+
if (typeof this.#execFile !== 'function' || typeof this.#osascript !== 'function') {
|
|
98
|
+
throw new TypeError('MacOSMessagesApi requires command runners');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getPermissions() {
|
|
103
|
+
const result = { platform: process.platform, database: 'unknown', automation: 'unknown' };
|
|
104
|
+
if (process.platform !== 'darwin') {
|
|
105
|
+
return { ...result, database: 'unsupported', automation: 'unsupported' };
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await this.#execFile('/usr/bin/sqlite3', ['-json', this.#dbPath, 'SELECT 1 AS ok LIMIT 1;'], this.#execFileOptions);
|
|
109
|
+
result.database = 'granted';
|
|
110
|
+
} catch (error) {
|
|
111
|
+
result.database = /authorization denied|not authorized|unable to open database/i.test(String(error?.stderr ?? error))
|
|
112
|
+
? 'required' : 'error';
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await this.#osascript('tell application "Messages" to get name');
|
|
116
|
+
result.automation = 'granted';
|
|
117
|
+
} catch (error) {
|
|
118
|
+
result.automation = /not authorized|(-1743)|assistive/i.test(String(error?.stderr ?? error))
|
|
119
|
+
? 'required' : 'error';
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async listMessages({ after = 0, limit = 50, chatGuid } = {}) {
|
|
125
|
+
const cursor = Number.isSafeInteger(after) && after >= 0 ? after : 0;
|
|
126
|
+
const boundedLimit = Math.max(1, Math.min(100, Number(limit) || 50));
|
|
127
|
+
const chatFilter = chatGuid ? ` AND c.guid = ${sqlString(normalizeChatGuid(chatGuid))}` : '';
|
|
128
|
+
// Messages delivers self-chat back as an incoming copy. Read that copy once,
|
|
129
|
+
// keeping all outgoing rows excluded, and filter bot replies by their marker.
|
|
130
|
+
const query = `SELECT m.ROWID AS rowid, m.guid AS guid, m.text AS text,
|
|
131
|
+
h.id AS sender, c.guid AS chatGuid, c.service_name AS serviceName,
|
|
132
|
+
m.is_from_me AS isFromMe,
|
|
133
|
+
datetime((m.date / 1000000000) + 978307200, 'unixepoch') AS receivedAt
|
|
134
|
+
FROM message m
|
|
135
|
+
JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
|
|
136
|
+
JOIN chat c ON c.ROWID = cmj.chat_id
|
|
137
|
+
LEFT JOIN handle h ON h.ROWID = m.handle_id
|
|
138
|
+
WHERE m.ROWID > ${cursor} AND m.is_from_me = 0
|
|
139
|
+
AND m.text IS NOT NULL AND m.text != ''
|
|
140
|
+
AND c.service_name = 'iMessage'${chatFilter}
|
|
141
|
+
ORDER BY m.ROWID ASC LIMIT ${boundedLimit};`;
|
|
142
|
+
try {
|
|
143
|
+
const { stdout } = await this.#execFile('/usr/bin/sqlite3', ['-json', this.#dbPath, query], this.#execFileOptions);
|
|
144
|
+
return decodeRows(stdout);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (/authorization denied|not authorized|unable to open database/i.test(String(error?.stderr ?? error))) {
|
|
147
|
+
throw permissionError('messages-database-permission-required', '请在系统设置中授予 DeepSeek Harness 完全磁盘访问权限。', error);
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getLatestMessageRowId() {
|
|
154
|
+
if (process.platform !== 'darwin') return 0;
|
|
155
|
+
const query = `SELECT COALESCE(MAX(m.ROWID), 0) AS rowid
|
|
156
|
+
FROM message m
|
|
157
|
+
JOIN chat_message_join cmj ON cmj.message_id = m.ROWID
|
|
158
|
+
JOIN chat c ON c.ROWID = cmj.chat_id
|
|
159
|
+
WHERE c.service_name = 'iMessage';`;
|
|
160
|
+
const { stdout } = await this.#execFile(
|
|
161
|
+
'/usr/bin/sqlite3', ['-json', this.#dbPath, query], this.#execFileOptions,
|
|
162
|
+
);
|
|
163
|
+
const rows = decodeRows(stdout);
|
|
164
|
+
const rowid = Number(rows[0]?.rowid ?? 0);
|
|
165
|
+
return Number.isSafeInteger(rowid) && rowid >= 0 ? rowid : 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async sendText({ chatGuid, address, text } = {}) {
|
|
169
|
+
const target = normalizeChatGuid(chatGuid);
|
|
170
|
+
const recipient = address ? normalizeAddress(address) : target.split(';').at(-1) || target;
|
|
171
|
+
const content = cleanString(text);
|
|
172
|
+
if (!content) throw new TypeError('iMessage text is required');
|
|
173
|
+
const reply = content.startsWith(IMESSAGE_BOT_REPLY_PREFIX)
|
|
174
|
+
? content : `${IMESSAGE_BOT_REPLY_PREFIX}${content}`;
|
|
175
|
+
const script = `tell application "Messages"
|
|
176
|
+
set serviceList to every service whose service type = iMessage
|
|
177
|
+
if (count of serviceList) is 0 then error "No iMessage service is available"
|
|
178
|
+
set targetService to item 1 of serviceList
|
|
179
|
+
set targetBuddy to buddy ${appleScriptString(recipient)} of targetService
|
|
180
|
+
send ${appleScriptString(reply)} to targetBuddy
|
|
181
|
+
end tell`;
|
|
182
|
+
try {
|
|
183
|
+
await this.#osascript(script);
|
|
184
|
+
return { sent: true };
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (/not authorized|(-1743)|assistive/i.test(String(error?.stderr ?? error))) {
|
|
187
|
+
throw permissionError('messages-automation-permission-required', '请在系统设置中允许 DeepSeek Harness 自动化控制 Messages。', error);
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { DEFAULT_DB_PATH };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TextHarnessBridge, createTextBridgeStatus } from '../shared/text-harness-bridge.mjs';
|
|
2
|
+
|
|
3
|
+
export const IMESSAGE_DESCRIPTOR = Object.freeze({
|
|
4
|
+
key: 'imessage',
|
|
5
|
+
label: 'iMessage',
|
|
6
|
+
connectionLabel: ' macOS Messages 连接',
|
|
7
|
+
reactions: Object.freeze({ processing: '👀', success: '✅', error: '❌' }),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export class IMessageHarnessBridge extends TextHarnessBridge {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
super({ ...options, descriptor: IMESSAGE_DESCRIPTOR });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { createTextBridgeStatus as createIMessageBridgeStatus };
|
|
@@ -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
|
|