@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
package/src/inbound.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EVENT,
|
|
3
|
+
type ChatType,
|
|
4
|
+
type Envelope,
|
|
5
|
+
} from "./protocol-types.ts";
|
|
6
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { effectiveGroupMode, type ResolvedOpenclawClawlingAccount } from "./config.ts";
|
|
8
|
+
import type { MediaItem } from "./media-runtime.ts";
|
|
9
|
+
import { extractMediaFragments, fragmentsToText, type MentionedUser } from "./message-mapper.ts";
|
|
10
|
+
import { hasRenderableText, isInboundMessagePayload } from "./protocol.ts";
|
|
11
|
+
|
|
12
|
+
export interface IngestTurnParams {
|
|
13
|
+
channel: "clawchat-plugin-openclaw";
|
|
14
|
+
accountId: string;
|
|
15
|
+
/**
|
|
16
|
+
* The conversational subject. `peer.id` mirrors the inbound envelope's
|
|
17
|
+
* `chat_id` (new-protocol routing key); `peer.kind` is `chat_type`
|
|
18
|
+
* (direct / group).
|
|
19
|
+
*/
|
|
20
|
+
peer: { kind: "direct" | "group"; id: string };
|
|
21
|
+
senderId: string;
|
|
22
|
+
senderNickName: string;
|
|
23
|
+
senderRelation?: "self_agent" | "owner" | "peer_agent" | "peer_user";
|
|
24
|
+
senderProfileType?: string | null;
|
|
25
|
+
senderIsOwner?: boolean;
|
|
26
|
+
senderIsGroupOwner?: boolean;
|
|
27
|
+
rawBody: string;
|
|
28
|
+
messageId: string;
|
|
29
|
+
traceId: string;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
wasMentioned: boolean;
|
|
32
|
+
mentionedUserIds: string[];
|
|
33
|
+
mentionedUsers: MentionedUser[];
|
|
34
|
+
coalescedGroupBatch?: boolean;
|
|
35
|
+
mediaItems: MediaItem[];
|
|
36
|
+
replyCtx?: {
|
|
37
|
+
replyToMessageId: string;
|
|
38
|
+
replyPreviewChatId: string;
|
|
39
|
+
replyPreviewSenderId: string;
|
|
40
|
+
replyPreviewNickName: string;
|
|
41
|
+
replyPreviewText: string;
|
|
42
|
+
};
|
|
43
|
+
cfg: OpenClawConfig;
|
|
44
|
+
runtime: PluginRuntime;
|
|
45
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
46
|
+
envelope: Envelope<unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DispatchInboundParams {
|
|
50
|
+
envelope: Envelope<unknown>;
|
|
51
|
+
cfg: OpenClawConfig;
|
|
52
|
+
runtime: PluginRuntime;
|
|
53
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
54
|
+
ingest: (params: IngestTurnParams) => Promise<void>;
|
|
55
|
+
log?: { info?: (m: string) => void; error?: (m: string) => void };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type SenderLike = {
|
|
59
|
+
id?: unknown;
|
|
60
|
+
nick_name?: unknown;
|
|
61
|
+
type?: unknown;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function normalizeSender(sender: unknown): { id: string; nickName: string; profileType: string | null } | null {
|
|
65
|
+
if (!sender || typeof sender !== "object") return null;
|
|
66
|
+
const s = sender as SenderLike;
|
|
67
|
+
const id = typeof s.id === "string" ? s.id : "";
|
|
68
|
+
if (!id) return null;
|
|
69
|
+
const nickName = typeof s.nick_name === "string" ? s.nick_name : id;
|
|
70
|
+
const profileType = s.type === "agent" || s.type === "user" ? s.type : null;
|
|
71
|
+
return { id, nickName, profileType };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requireChatId(envelope: Envelope<unknown>): string | null {
|
|
75
|
+
const chatId = (envelope as Envelope<unknown> & { chat_id?: unknown }).chat_id;
|
|
76
|
+
return typeof chatId === "string" && chatId.trim() ? chatId : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mentionIdFromRecord(record: Record<string, unknown>): string | undefined {
|
|
80
|
+
const userId = record.user_id ?? record.userId ?? record.id;
|
|
81
|
+
return typeof userId === "string" && userId.length > 0 ? userId : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function mentionDisplayFromRecord(record: Record<string, unknown>): string | undefined {
|
|
85
|
+
const display = record.display ?? record.label ?? record.name ?? record.nick_name ?? record.nickname;
|
|
86
|
+
return typeof display === "string" && display.trim() ? display.trim().replace(/^@/, "") : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractMentionUsers(fragments: Array<Record<string, unknown>>): MentionedUser[] {
|
|
90
|
+
return fragments
|
|
91
|
+
.filter((fragment) => fragment.kind === "mention")
|
|
92
|
+
.map((fragment) => {
|
|
93
|
+
const id = mentionIdFromRecord(fragment);
|
|
94
|
+
if (!id) return null;
|
|
95
|
+
const display = mentionDisplayFromRecord(fragment);
|
|
96
|
+
return display ? { id, display } : { id };
|
|
97
|
+
})
|
|
98
|
+
.filter((mention): mention is MentionedUser => mention !== null);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeMentionUsers(mentions: unknown[]): MentionedUser[] {
|
|
102
|
+
return mentions
|
|
103
|
+
.map((mention) => {
|
|
104
|
+
if (typeof mention === "string" && mention.length > 0) return { id: mention };
|
|
105
|
+
if (mention && typeof mention === "object") {
|
|
106
|
+
const record = mention as Record<string, unknown>;
|
|
107
|
+
const id = mentionIdFromRecord(record);
|
|
108
|
+
if (!id) return null;
|
|
109
|
+
const display = mentionDisplayFromRecord(record);
|
|
110
|
+
return display ? { id, display } : { id };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
})
|
|
114
|
+
.filter((mention): mention is MentionedUser => mention !== null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function mergeMentionUsers(...sources: MentionedUser[][]): MentionedUser[] {
|
|
118
|
+
const merged = new Map<string, MentionedUser>();
|
|
119
|
+
for (const source of sources) {
|
|
120
|
+
for (const mention of source) {
|
|
121
|
+
const existing = merged.get(mention.id);
|
|
122
|
+
if (!existing || (!existing.display && mention.display)) {
|
|
123
|
+
merged.set(mention.id, mention);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return Array.from(merged.values());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mentionedUserIds(mentions: MentionedUser[]): string[] {
|
|
131
|
+
return mentions.map((mention) => mention.id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Exported for direct unit testing. Direct chats always count as addressed;
|
|
136
|
+
* group chats require a mention unless config opts into all group messages.
|
|
137
|
+
*/
|
|
138
|
+
export function detectMention(params: {
|
|
139
|
+
mentions: unknown[];
|
|
140
|
+
chatType: "direct" | "group";
|
|
141
|
+
userId: string;
|
|
142
|
+
}): boolean {
|
|
143
|
+
if (params.chatType === "direct") return true;
|
|
144
|
+
return mentionedUserIds(normalizeMentionUsers(params.mentions)).includes(params.userId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function dispatchOpenclawClawlingInbound(
|
|
148
|
+
params: DispatchInboundParams,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const { envelope, account, log } = params;
|
|
151
|
+
const isMaterializedMessage = envelope.event === EVENT.MESSAGE_SEND || envelope.event === EVENT.MESSAGE_REPLY;
|
|
152
|
+
if (!isMaterializedMessage) {
|
|
153
|
+
log?.info?.(
|
|
154
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip non-business event=${envelope.event} trace=${envelope.trace_id}`,
|
|
155
|
+
);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isMaterializedMessage && !isInboundMessagePayload(envelope.payload)) {
|
|
159
|
+
log?.info?.(
|
|
160
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip: invalid payload trace=${envelope.trace_id}`,
|
|
161
|
+
);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const chatId = requireChatId(envelope);
|
|
165
|
+
if (!chatId) {
|
|
166
|
+
log?.info?.(
|
|
167
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip: missing chat_id trace=${envelope.trace_id}`,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const payload = envelope.payload as {
|
|
172
|
+
message_id: string;
|
|
173
|
+
message_mode?: string;
|
|
174
|
+
message?: unknown;
|
|
175
|
+
};
|
|
176
|
+
const message = payload.message as {
|
|
177
|
+
body: { fragments: Array<Record<string, unknown>> };
|
|
178
|
+
context: {
|
|
179
|
+
mentions: unknown[];
|
|
180
|
+
reply: {
|
|
181
|
+
reply_to_msg_id: string;
|
|
182
|
+
reply_preview: {
|
|
183
|
+
id?: string;
|
|
184
|
+
nick_name?: string;
|
|
185
|
+
fragments: Array<Record<string, unknown>>;
|
|
186
|
+
};
|
|
187
|
+
} | null;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const sender = normalizeSender(envelope.sender);
|
|
192
|
+
if (!sender) {
|
|
193
|
+
log?.info?.(
|
|
194
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip: missing sender trace=${envelope.trace_id}`,
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (account.userId && sender.id === account.userId) {
|
|
199
|
+
log?.info?.(
|
|
200
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip self-echo msg=${payload.message_id} user=${sender.id}`,
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const chatType: ChatType = envelope.chat_type === "group" ? "group" : "direct";
|
|
205
|
+
const isGroup = chatType === "group";
|
|
206
|
+
if (isMaterializedMessage && payload.message_mode !== "normal") {
|
|
207
|
+
log?.info?.(
|
|
208
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip non-normal mode=${payload.message_mode}`,
|
|
209
|
+
);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!hasRenderableText(message)) {
|
|
213
|
+
log?.info?.(
|
|
214
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip empty msg=${payload.message_id}`,
|
|
215
|
+
);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const fragmentMentionUsers = extractMentionUsers(message.body.fragments as Array<Record<string, unknown>>);
|
|
219
|
+
const contextMentionUsers = normalizeMentionUsers(message.context.mentions);
|
|
220
|
+
const mentionedUsers = mergeMentionUsers(fragmentMentionUsers, contextMentionUsers);
|
|
221
|
+
const mentionIds = mentionedUserIds(mentionedUsers);
|
|
222
|
+
const rawBody = fragmentsToText(message.body.fragments as never, {
|
|
223
|
+
mentionFallbackIds: mentionIds,
|
|
224
|
+
});
|
|
225
|
+
const mediaItems = extractMediaFragments(message.body.fragments as never);
|
|
226
|
+
const wasMentioned = detectMention({
|
|
227
|
+
mentions: mentionIds,
|
|
228
|
+
chatType,
|
|
229
|
+
userId: account.userId,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Group trigger policy: in "mention" mode we only handle group messages
|
|
233
|
+
// that @-mention us; "all" listens open and processes every group msg.
|
|
234
|
+
// Direct chats are unaffected (detectMention returns true).
|
|
235
|
+
const groupMode = isGroup ? effectiveGroupMode(account, chatId) : account.groupMode;
|
|
236
|
+
if (isGroup && groupMode === "mention" && !wasMentioned) {
|
|
237
|
+
log?.info?.(
|
|
238
|
+
`[${account.accountId}] clawchat-plugin-openclaw skip group (no mention) msg=${payload.message_id}`,
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
log?.info?.(
|
|
244
|
+
`[${account.accountId}] clawchat-plugin-openclaw inbound event=${envelope.event} msg=${payload.message_id} from=${sender.id} text_len=${rawBody.length} mentioned=${wasMentioned}`,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const replyCtx = message.context.reply
|
|
248
|
+
? {
|
|
249
|
+
replyToMessageId: message.context.reply.reply_to_msg_id,
|
|
250
|
+
replyPreviewChatId: chatId,
|
|
251
|
+
replyPreviewSenderId: message.context.reply.reply_preview.id ?? "",
|
|
252
|
+
replyPreviewNickName: message.context.reply.reply_preview.nick_name ?? "",
|
|
253
|
+
replyPreviewText: fragmentsToText(message.context.reply.reply_preview.fragments as never),
|
|
254
|
+
}
|
|
255
|
+
: undefined;
|
|
256
|
+
|
|
257
|
+
await params.ingest({
|
|
258
|
+
channel: "clawchat-plugin-openclaw",
|
|
259
|
+
accountId: account.accountId,
|
|
260
|
+
peer: { kind: isGroup ? "group" : "direct", id: chatId },
|
|
261
|
+
senderId: sender.id,
|
|
262
|
+
senderNickName: sender.nickName,
|
|
263
|
+
...(sender.profileType ? { senderProfileType: sender.profileType } : {}),
|
|
264
|
+
rawBody,
|
|
265
|
+
messageId: payload.message_id,
|
|
266
|
+
traceId: envelope.trace_id,
|
|
267
|
+
timestamp: envelope.emitted_at,
|
|
268
|
+
wasMentioned,
|
|
269
|
+
mentionedUserIds: mentionIds,
|
|
270
|
+
mentionedUsers,
|
|
271
|
+
mediaItems,
|
|
272
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
273
|
+
cfg: params.cfg,
|
|
274
|
+
runtime: params.runtime,
|
|
275
|
+
account,
|
|
276
|
+
envelope,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createOpenclawLlmContextDebug } from "./llm-context-debug.ts";
|
|
6
|
+
|
|
7
|
+
const dirs: string[] = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function tempDir(): string {
|
|
14
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-llm-context-"));
|
|
15
|
+
dirs.push(dir);
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("OpenClaw LLM context debug", () => {
|
|
20
|
+
it("is disabled by default and writes no snapshot", () => {
|
|
21
|
+
const root = tempDir();
|
|
22
|
+
const debug = createOpenclawLlmContextDebug({ env: {}, defaultSnapshotRoot: root });
|
|
23
|
+
|
|
24
|
+
expect(debug.enabled).toBe(false);
|
|
25
|
+
expect(debug.writeSnapshot({
|
|
26
|
+
visibility: "injected_only",
|
|
27
|
+
trace: { messageId: "m1" },
|
|
28
|
+
input: { injectedPrompt: "p", eventText: "e" },
|
|
29
|
+
})).toBeNull();
|
|
30
|
+
expect(fs.existsSync(path.join(root, "openclaw"))).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("writes a v1 snapshot when enabled", () => {
|
|
34
|
+
const root = tempDir();
|
|
35
|
+
const debug = createOpenclawLlmContextDebug({
|
|
36
|
+
env: {
|
|
37
|
+
CLAWCHAT_LLM_CONTEXT_DEBUG: "1",
|
|
38
|
+
CLAWCHAT_LLM_CONTEXT_SNAPSHOT_DIR: root,
|
|
39
|
+
},
|
|
40
|
+
defaultSnapshotRoot: root,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const filePath = debug.writeSnapshot({
|
|
44
|
+
visibility: "injected_only",
|
|
45
|
+
trace: { messageId: "m1", chatType: "dm" },
|
|
46
|
+
input: { injectedPrompt: "prompt", eventText: "hello" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(filePath).toMatch(/openclaw\/runs\/.*\.json$/);
|
|
50
|
+
const raw = JSON.parse(fs.readFileSync(filePath!, "utf8"));
|
|
51
|
+
expect(raw.schemaVersion).toBe(1);
|
|
52
|
+
expect(raw.source).toBe("openclaw");
|
|
53
|
+
expect(raw.input.injectedPrompt).toBe("prompt");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type OpenclawLlmContextVisibility =
|
|
5
|
+
| "full_llm_input"
|
|
6
|
+
| "host_event"
|
|
7
|
+
| "host_descriptor"
|
|
8
|
+
| "injected_only"
|
|
9
|
+
| "static_only";
|
|
10
|
+
|
|
11
|
+
export type OpenclawLlmContextSnapshotInput = {
|
|
12
|
+
visibility: OpenclawLlmContextVisibility;
|
|
13
|
+
trace: Record<string, unknown>;
|
|
14
|
+
input: {
|
|
15
|
+
injectedPrompt?: string;
|
|
16
|
+
eventText?: string;
|
|
17
|
+
fullLlmInput?: unknown;
|
|
18
|
+
sections?: unknown[];
|
|
19
|
+
};
|
|
20
|
+
context?: {
|
|
21
|
+
promptParts?: unknown[];
|
|
22
|
+
tools?: unknown[];
|
|
23
|
+
skills?: unknown[];
|
|
24
|
+
};
|
|
25
|
+
output?: {
|
|
26
|
+
rawModelOutput?: string | null;
|
|
27
|
+
streamChunks?: unknown[];
|
|
28
|
+
toolCalls?: unknown[];
|
|
29
|
+
toolResults?: unknown[];
|
|
30
|
+
finalAssistantText?: string | null;
|
|
31
|
+
adapterFilteredText?: string | null;
|
|
32
|
+
outboundClawChatMessage?: unknown;
|
|
33
|
+
suppressed?: boolean;
|
|
34
|
+
suppressionReason?: string | null;
|
|
35
|
+
};
|
|
36
|
+
warnings?: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type OpenclawLlmContextDebug = {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
captureFullInput: boolean;
|
|
42
|
+
captureOutput: boolean;
|
|
43
|
+
reloadPrompts: boolean;
|
|
44
|
+
snapshotRoot: string;
|
|
45
|
+
writeSnapshot(snapshot: OpenclawLlmContextSnapshotInput): string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function envEnabled(value: string | undefined): boolean {
|
|
49
|
+
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function safeId(value: unknown): string {
|
|
53
|
+
const text = String(value ?? "unknown").replace(/[^A-Za-z0-9_.-]+/g, "-");
|
|
54
|
+
return text.length > 0 ? text.slice(0, 80) : "unknown";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createOpenclawLlmContextDebug(params: {
|
|
58
|
+
env?: Record<string, string | undefined>;
|
|
59
|
+
defaultSnapshotRoot?: string;
|
|
60
|
+
} = {}): OpenclawLlmContextDebug {
|
|
61
|
+
const env = params.env ?? process.env;
|
|
62
|
+
const enabled = envEnabled(env.CLAWCHAT_LLM_CONTEXT_DEBUG);
|
|
63
|
+
const snapshotRoot = path.resolve(
|
|
64
|
+
env.CLAWCHAT_LLM_CONTEXT_SNAPSHOT_DIR || params.defaultSnapshotRoot || ".clawchat-llm-context-debug",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
enabled,
|
|
69
|
+
captureFullInput: envEnabled(env.CLAWCHAT_LLM_CONTEXT_CAPTURE_FULL_INPUT),
|
|
70
|
+
captureOutput: envEnabled(env.CLAWCHAT_LLM_CONTEXT_CAPTURE_OUTPUT),
|
|
71
|
+
reloadPrompts: envEnabled(env.CLAWCHAT_LLM_CONTEXT_RELOAD_PROMPTS),
|
|
72
|
+
snapshotRoot,
|
|
73
|
+
writeSnapshot(snapshot) {
|
|
74
|
+
if (!enabled) return null;
|
|
75
|
+
const createdAt = new Date().toISOString();
|
|
76
|
+
const targetRoot = path.join(snapshotRoot, "openclaw");
|
|
77
|
+
const runsRoot = path.join(targetRoot, "runs");
|
|
78
|
+
fs.mkdirSync(runsRoot, { recursive: true });
|
|
79
|
+
const messageId = safeId(snapshot.trace.messageId);
|
|
80
|
+
const traceId = safeId(snapshot.trace.traceId);
|
|
81
|
+
const filePath = path.join(runsRoot, `${createdAt.replace(/[:.]/g, "-")}-${messageId}-${traceId}.json`);
|
|
82
|
+
const body = {
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
source: "openclaw",
|
|
85
|
+
visibility: snapshot.visibility,
|
|
86
|
+
createdAt,
|
|
87
|
+
trace: snapshot.trace,
|
|
88
|
+
context: {
|
|
89
|
+
promptParts: snapshot.context?.promptParts ?? [],
|
|
90
|
+
tools: snapshot.context?.tools ?? [],
|
|
91
|
+
skills: snapshot.context?.skills ?? [],
|
|
92
|
+
},
|
|
93
|
+
input: {
|
|
94
|
+
injectedPrompt: snapshot.input.injectedPrompt ?? "",
|
|
95
|
+
eventText: snapshot.input.eventText ?? "",
|
|
96
|
+
fullLlmInput: snapshot.input.fullLlmInput ?? null,
|
|
97
|
+
sections: snapshot.input.sections ?? [],
|
|
98
|
+
},
|
|
99
|
+
output: {
|
|
100
|
+
rawModelOutput: snapshot.output?.rawModelOutput ?? null,
|
|
101
|
+
streamChunks: snapshot.output?.streamChunks ?? [],
|
|
102
|
+
toolCalls: snapshot.output?.toolCalls ?? [],
|
|
103
|
+
toolResults: snapshot.output?.toolResults ?? [],
|
|
104
|
+
finalAssistantText: snapshot.output?.finalAssistantText ?? null,
|
|
105
|
+
adapterFilteredText: snapshot.output?.adapterFilteredText ?? null,
|
|
106
|
+
outboundClawChatMessage: snapshot.output?.outboundClawChatMessage ?? null,
|
|
107
|
+
suppressed: snapshot.output?.suppressed ?? false,
|
|
108
|
+
suppressionReason: snapshot.output?.suppressionReason ?? null,
|
|
109
|
+
},
|
|
110
|
+
warnings: snapshot.warnings ?? [],
|
|
111
|
+
};
|
|
112
|
+
const text = `${JSON.stringify(body, null, 2)}\n`;
|
|
113
|
+
fs.writeFileSync(filePath, text, "utf8");
|
|
114
|
+
fs.writeFileSync(path.join(targetRoot, "latest.json"), text, "utf8");
|
|
115
|
+
return filePath;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const openclawLlmContextDebug: OpenclawLlmContextDebug = {
|
|
121
|
+
get enabled() {
|
|
122
|
+
return createOpenclawLlmContextDebug().enabled;
|
|
123
|
+
},
|
|
124
|
+
get captureFullInput() {
|
|
125
|
+
return createOpenclawLlmContextDebug().captureFullInput;
|
|
126
|
+
},
|
|
127
|
+
get captureOutput() {
|
|
128
|
+
return createOpenclawLlmContextDebug().captureOutput;
|
|
129
|
+
},
|
|
130
|
+
get reloadPrompts() {
|
|
131
|
+
return createOpenclawLlmContextDebug().reloadPrompts;
|
|
132
|
+
},
|
|
133
|
+
get snapshotRoot() {
|
|
134
|
+
return createOpenclawLlmContextDebug().snapshotRoot;
|
|
135
|
+
},
|
|
136
|
+
writeSnapshot(snapshot) {
|
|
137
|
+
return createOpenclawLlmContextDebug().writeSnapshot(snapshot);
|
|
138
|
+
},
|
|
139
|
+
};
|