





















@@ -0,0 +1,166 @@
1+import {
2+formatInboundEnvelope,
3+type resolveEnvelopeFormatOptions,
4+} from "openclaw/plugin-sdk/channel-inbound";
5+import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
6+import type { IMessageRpcClient } from "../client.js";
7+import { normalizeIMessageHandle } from "../targets.js";
8+import { parseIMessageNotification } from "./parse-notification.js";
9+import type { IMessagePayload } from "./types.js";
10+11+const DM_HISTORY_RPC_TIMEOUT_MS = 10_000;
12+13+type IMessageHistoryResult = {
14+messages?: unknown[];
15+};
16+17+type IMessageDmHistoryConfig = {
18+dmHistoryLimit?: number;
19+dms?: Record<string, { historyLimit?: number }>;
20+};
21+22+export type IMessageDmHistoryEntry = {
23+sender: string;
24+body: string;
25+timestamp?: number;
26+};
27+28+export type IMessageDmHistoryContext = {
29+body?: string;
30+inboundHistory?: IMessageDmHistoryEntry[];
31+};
32+33+export function resolveIMessageDmHistoryLimit(params: {
34+config: IMessageDmHistoryConfig;
35+sender?: string;
36+senderNormalized?: string;
37+}): number {
38+const senderCandidates = [
39+normalizeOptionalString(params.senderNormalized),
40+normalizeOptionalString(params.sender),
41+params.sender ? normalizeIMessageHandle(params.sender) : undefined,
42+].filter((candidate): candidate is string => Boolean(candidate));
43+44+for (const candidate of senderCandidates) {
45+const override = params.config.dms?.[candidate]?.historyLimit;
46+if (override !== undefined) {
47+return Math.max(0, override);
48+}
49+}
50+51+return Math.max(0, params.config.dmHistoryLimit ?? 0);
52+}
53+54+function historyRowSortValue(message: IMessagePayload): number {
55+if (typeof message.id === "number" && Number.isFinite(message.id)) {
56+return message.id;
57+}
58+const createdAtMs =
59+typeof message.created_at === "string" ? Date.parse(message.created_at) : Number.NaN;
60+return Number.isFinite(createdAtMs) ? createdAtMs : 0;
61+}
62+63+function isBeforeCurrentMessage(params: {
64+message: IMessagePayload;
65+currentMessage: IMessagePayload;
66+}): boolean {
67+const { message, currentMessage } = params;
68+if (
69+typeof message.id === "number" &&
70+typeof currentMessage.id === "number" &&
71+Number.isFinite(message.id) &&
72+Number.isFinite(currentMessage.id)
73+) {
74+return message.id < currentMessage.id;
75+}
76+const guid = normalizeOptionalString(message.guid);
77+const currentGuid = normalizeOptionalString(currentMessage.guid);
78+if (guid && currentGuid) {
79+return guid !== currentGuid;
80+}
81+return true;
82+}
83+84+function historyEntryFromMessage(message: IMessagePayload, fallbackSender: string) {
85+const body = normalizeOptionalString(message.text);
86+if (!body) {
87+return null;
88+}
89+const timestamp =
90+typeof message.created_at === "string" ? Date.parse(message.created_at) : Number.NaN;
91+return {
92+sender:
93+message.is_from_me === true
94+ ? "Me"
95+ : normalizeIMessageHandle(normalizeOptionalString(message.sender) ?? fallbackSender) ||
96+fallbackSender,
97+ body,
98+ ...(Number.isFinite(timestamp) ? { timestamp } : {}),
99+};
100+}
101+102+export async function resolveIMessageDmHistoryContext(params: {
103+client: IMessageRpcClient;
104+message: IMessagePayload;
105+senderNormalized: string;
106+limit: number;
107+envelopeOptions: ReturnType<typeof resolveEnvelopeFormatOptions>;
108+logVerbose?: (msg: string) => void;
109+}): Promise<IMessageDmHistoryContext> {
110+const maxMessages = Math.max(0, Math.floor(params.limit));
111+const chatId =
112+typeof params.message.chat_id === "number" && Number.isFinite(params.message.chat_id)
113+ ? params.message.chat_id
114+ : undefined;
115+if (maxMessages <= 0 || chatId === undefined) {
116+return {};
117+}
118+119+let result: IMessageHistoryResult | undefined;
120+try {
121+result = await params.client.request<IMessageHistoryResult>(
122+"messages.history",
123+{
124+chat_id: chatId,
125+limit: maxMessages + 1,
126+attachments: false,
127+},
128+{ timeoutMs: DM_HISTORY_RPC_TIMEOUT_MS },
129+);
130+} catch (err) {
131+params.logVerbose?.(`imessage: DM history fetch failed for chat_id=${chatId}: ${String(err)}`);
132+return {};
133+}
134+135+const rows = Array.isArray(result?.messages) ? result.messages : [];
136+const history = rows
137+.map((row) => parseIMessageNotification({ message: row }))
138+.filter((message): message is IMessagePayload => Boolean(message))
139+.filter((message) => message.is_group !== true)
140+.filter((message) => isBeforeCurrentMessage({ message, currentMessage: params.message }))
141+.toSorted((a, b) => historyRowSortValue(a) - historyRowSortValue(b))
142+.map((message) => historyEntryFromMessage(message, params.senderNormalized))
143+.filter((entry): entry is IMessageDmHistoryEntry => Boolean(entry))
144+.slice(-maxMessages);
145+146+if (history.length === 0) {
147+return {};
148+}
149+150+return {
151+inboundHistory: history,
152+body: history
153+.map((entry) =>
154+formatInboundEnvelope({
155+channel: "iMessage",
156+from: entry.sender,
157+timestamp: entry.timestamp,
158+body: entry.body,
159+chatType: "direct",
160+senderLabel: entry.sender,
161+envelope: params.envelopeOptions,
162+}),
163+)
164+.join("\n\n"),
165+};
166+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。