


























1+import { spawn } from "node:child_process";
12import {
23createMessageReceiptFromOutboundResults,
34type MessageReceipt,
@@ -52,6 +53,7 @@ type IMessageSendOpts = {
5253},
5354) => Promise<{ path: string; contentType?: string }>;
5455createClient?: (params: { cliPath: string; dbPath?: string }) => Promise<IMessageRpcClient>;
56+runCliJson?: (args: readonly string[]) => Promise<Record<string, unknown>>;
5557};
56585759export type IMessageSendResult = {
@@ -210,6 +212,110 @@ function resolveOutboundEchoScope(params: {
210212return `${params.accountId}:imessage:${params.target.to}`;
211213}
212214215+function buildIMessageCliJsonArgs(args: readonly string[], dbPath?: string): string[] {
216+const trimmedDbPath = dbPath?.trim();
217+return [...args, ...(trimmedDbPath ? ["--db", trimmedDbPath] : []), "--json"];
218+}
219+220+async function runIMessageCliJson(
221+cliPath: string,
222+dbPath: string | undefined,
223+args: readonly string[],
224+timeoutMs?: number,
225+): Promise<Record<string, unknown>> {
226+return await new Promise((resolve, reject) => {
227+const child = spawn(cliPath, buildIMessageCliJsonArgs(args, dbPath), {
228+stdio: ["ignore", "pipe", "pipe"],
229+});
230+let stdout = "";
231+let stderr = "";
232+let killEscalation: ReturnType<typeof setTimeout> | null = null;
233+const timer =
234+timeoutMs && timeoutMs > 0
235+ ? setTimeout(() => {
236+child.kill("SIGTERM");
237+killEscalation = setTimeout(() => {
238+try {
239+child.kill("SIGKILL");
240+} catch {
241+// best-effort
242+}
243+}, 2000);
244+reject(new Error(`iMessage action timed out after ${timeoutMs}ms`));
245+}, timeoutMs)
246+ : null;
247+child.stdout.setEncoding("utf8");
248+child.stderr.setEncoding("utf8");
249+child.stdout.on("data", (chunk) => {
250+stdout += chunk;
251+});
252+child.stderr.on("data", (chunk) => {
253+stderr += chunk;
254+});
255+child.on("error", (error) => {
256+if (timer) {
257+clearTimeout(timer);
258+}
259+if (killEscalation) {
260+clearTimeout(killEscalation);
261+}
262+reject(error);
263+});
264+child.on("close", (code) => {
265+if (timer) {
266+clearTimeout(timer);
267+}
268+if (killEscalation) {
269+clearTimeout(killEscalation);
270+}
271+const lines = stdout
272+.split(/\r?\n/u)
273+.map((line) => line.trim())
274+.filter(Boolean);
275+const last = lines.at(-1);
276+let parsed: Record<string, unknown> | null = null;
277+if (last) {
278+try {
279+const json = JSON.parse(last) as unknown;
280+if (json && typeof json === "object" && !Array.isArray(json)) {
281+parsed = json as Record<string, unknown>;
282+}
283+} catch {
284+// handled below
285+}
286+}
287+if (code === 0 && parsed) {
288+resolve(parsed);
289+return;
290+}
291+if (parsed && typeof parsed.error === "string" && parsed.error.trim()) {
292+reject(new Error(parsed.error.trim()));
293+return;
294+}
295+const detail = stderr.trim() || stdout.trim() || `imsg exited with code ${code}`;
296+reject(new Error(detail));
297+});
298+});
299+}
300+301+function stringValue(value: unknown): string | undefined {
302+return typeof value === "string" && value.trim() ? value.trim() : undefined;
303+}
304+305+async function resolveAttachmentChatGuid(params: {
306+target: ReturnType<typeof parseIMessageTarget>;
307+runCliJson: (args: readonly string[]) => Promise<Record<string, unknown>>;
308+}): Promise<string | null> {
309+if (params.target.kind === "chat_guid") {
310+return params.target.chatGuid;
311+}
312+if (params.target.kind !== "chat_id") {
313+return null;
314+}
315+const result = await params.runCliJson(["group", "--chat-id", String(params.target.chatId)]);
316+return stringValue(result.guid) ?? stringValue(result.chat_guid) ?? null;
317+}
318+213319export async function sendMessageIMessage(
214320to: string,
215321text: string,
@@ -278,6 +384,56 @@ export async function sendMessageIMessage(
278384}
279385const echoText = resolveOutboundEchoText(message, filePath ? mediaContentType : undefined);
280386const resolvedReplyToId = sanitizeReplyToId(opts.replyToId);
387+const runCliJson =
388+opts.runCliJson ??
389+((args: readonly string[]) => runIMessageCliJson(cliPath, dbPath, args, opts.timeoutMs));
390+391+if (filePath && !message.trim() && !resolvedReplyToId) {
392+const attachmentChatGuid = await resolveAttachmentChatGuid({ target, runCliJson });
393+if (attachmentChatGuid) {
394+const result = await runCliJson([
395+"send-attachment",
396+"--chat",
397+attachmentChatGuid,
398+"--file",
399+filePath,
400+"--transport",
401+"auto",
402+]);
403+const resolvedId = resolveMessageId(result);
404+const approvalBindingMessageId = resolveOutboundMessageGuid(result);
405+const messageId = resolvedId ?? (result?.ok || result?.success ? "ok" : "unknown");
406+const echoScope = resolveOutboundEchoScope({ accountId: account.accountId, target });
407+if (echoScope) {
408+rememberPersistedIMessageEcho({
409+scope: echoScope,
410+text: echoText,
411+messageId: resolvedId ?? undefined,
412+});
413+}
414+if (resolvedId) {
415+rememberIMessageReplyCache({
416+accountId: account.accountId,
417+messageId: resolvedId,
418+chatGuid: target.kind === "chat_guid" ? target.chatGuid : attachmentChatGuid,
419+chatId: target.kind === "chat_id" ? target.chatId : undefined,
420+timestamp: Date.now(),
421+isFromMe: true,
422+});
423+}
424+return {
425+ messageId,
426+ ...(approvalBindingMessageId ? { guid: approvalBindingMessageId } : {}),
427+sentText: message,
428+ ...(echoText ? { echoText } : {}),
429+receipt: createIMessageSendReceipt({
430+ messageId,
431+ target,
432+kind: "media",
433+}),
434+};
435+}
436+}
281437const params: Record<string, unknown> = {
282438text: message,
283439service: service || "auto",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。