@alfe.ai/openclaw-chat 0.9.8 → 0.9.10
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.md +20 -11
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +5 -0
- package/dist/plugin.d.cts +30 -11
- package/dist/plugin.d.ts +30 -11
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +478 -139
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +447 -138
- package/package.json +5 -4
package/dist/plugin2.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
let node_module = require("node:module");
|
|
2
|
+
let node_crypto = require("node:crypto");
|
|
2
3
|
let node_fs_promises = require("node:fs/promises");
|
|
3
4
|
let node_fs = require("node:fs");
|
|
4
5
|
let node_child_process = require("node:child_process");
|
|
@@ -7,7 +8,7 @@ let node_url = require("node:url");
|
|
|
7
8
|
let node_os = require("node:os");
|
|
8
9
|
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
9
10
|
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
10
|
-
let
|
|
11
|
+
let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
|
|
11
12
|
let _alfe_ai_config = require("@alfe.ai/config");
|
|
12
13
|
//#region src/outbound-media.ts
|
|
13
14
|
/**
|
|
@@ -154,29 +155,40 @@ async function uploadLocalFile(localPath, log, deps) {
|
|
|
154
155
|
const client = deps.getClient ? deps.getClient() : getUploadClient(log);
|
|
155
156
|
if (!client) return null;
|
|
156
157
|
let size;
|
|
158
|
+
let body;
|
|
157
159
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const handle = await (0, node_fs_promises.open)(localPath, node_fs.constants.O_RDONLY | node_fs.constants.O_NOFOLLOW);
|
|
161
|
+
try {
|
|
162
|
+
const before = await handle.stat();
|
|
163
|
+
if (!before.isFile()) {
|
|
164
|
+
log.warn(`Outbound media ref is not a file — skipping: ${localPath}`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
size = before.size;
|
|
168
|
+
if (size <= 0) {
|
|
169
|
+
log.warn(`Outbound media file is empty — skipping: ${localPath}`);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (size > 26214400) {
|
|
173
|
+
log.warn(`Outbound media exceeds ${String(MAX_OUTBOUND_MEDIA_SIZE)} bytes (${String(size)}) — skipping: ${localPath}`);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
body = Uint8Array.from(await handle.readFile());
|
|
177
|
+
const after = await handle.stat();
|
|
178
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || body.length !== before.size) {
|
|
179
|
+
log.warn(`Outbound media changed while being read — skipping: ${localPath}`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
await handle.close();
|
|
162
184
|
}
|
|
163
|
-
size = st.size;
|
|
164
185
|
} catch {
|
|
165
186
|
log.warn(`Outbound media file not found — skipping: ${localPath}`);
|
|
166
187
|
return null;
|
|
167
188
|
}
|
|
168
|
-
if (size <= 0) {
|
|
169
|
-
log.warn(`Outbound media file is empty — skipping: ${localPath}`);
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
if (size > 26214400) {
|
|
173
|
-
log.warn(`Outbound media exceeds ${String(MAX_OUTBOUND_MEDIA_SIZE)} bytes (${String(size)}) — skipping: ${localPath}`);
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
189
|
const filename = (0, node_path.basename)(localPath);
|
|
177
190
|
const mimeType = mimeFromPath(localPath);
|
|
178
191
|
try {
|
|
179
|
-
const body = await (0, node_fs_promises.readFile)(localPath);
|
|
180
192
|
const { attachments } = await client.presignAttachments([{
|
|
181
193
|
filename,
|
|
182
194
|
mimeType,
|
|
@@ -407,13 +419,17 @@ function isSafeComponentUrl(raw) {
|
|
|
407
419
|
return false;
|
|
408
420
|
}
|
|
409
421
|
if (u.protocol !== "https:") return false;
|
|
422
|
+
if (u.username || u.password) return false;
|
|
410
423
|
const host = u.hostname.toLowerCase();
|
|
411
424
|
return host === "alfe.ai" || host.endsWith(".alfe.ai");
|
|
412
425
|
}
|
|
413
426
|
const MAX_COMPONENTS_PER_MESSAGE = 10;
|
|
414
|
-
const MAX_COMPONENT_LABEL_CHARS = 120;
|
|
415
|
-
const MAX_COMPONENT_VALUE_CHARS = 400;
|
|
427
|
+
const MAX_COMPONENT_LABEL_CHARS$1 = 120;
|
|
428
|
+
const MAX_COMPONENT_VALUE_CHARS$1 = 400;
|
|
416
429
|
const MAX_OPTIONS_PER_COMPONENT = 25;
|
|
430
|
+
const MAX_COMPONENT_ID_CHARS$1 = 128;
|
|
431
|
+
const MAX_CONVERSATION_ID_CHARS = 512;
|
|
432
|
+
const MAX_USER_ID_CHARS = 256;
|
|
417
433
|
/** Cap a maybe-string; returns '' for non-strings so callers can `if (!x)`. */
|
|
418
434
|
function capStr(v, max) {
|
|
419
435
|
return typeof v === "string" ? v.slice(0, max) : "";
|
|
@@ -430,8 +446,8 @@ function sanitizeOptions(raw) {
|
|
|
430
446
|
if (out.length >= MAX_OPTIONS_PER_COMPONENT) break;
|
|
431
447
|
if (!item || typeof item !== "object") continue;
|
|
432
448
|
const o = item;
|
|
433
|
-
const label = capStr(o.label, MAX_COMPONENT_LABEL_CHARS);
|
|
434
|
-
const value = capStr(o.value, MAX_COMPONENT_VALUE_CHARS);
|
|
449
|
+
const label = capStr(o.label, MAX_COMPONENT_LABEL_CHARS$1);
|
|
450
|
+
const value = capStr(o.value, MAX_COMPONENT_VALUE_CHARS$1);
|
|
435
451
|
if (!label || !value) continue;
|
|
436
452
|
out.push({
|
|
437
453
|
label,
|
|
@@ -457,8 +473,8 @@ function sanitizeComponents(raw) {
|
|
|
457
473
|
if (out.length >= MAX_COMPONENTS_PER_MESSAGE) break;
|
|
458
474
|
if (!item || typeof item !== "object") continue;
|
|
459
475
|
const c = item;
|
|
460
|
-
const label = capStr(c.label, MAX_COMPONENT_LABEL_CHARS);
|
|
461
|
-
let id = typeof c.id === "string" && c.id ? c.id : (0, node_crypto.randomUUID)();
|
|
476
|
+
const label = capStr(c.label, MAX_COMPONENT_LABEL_CHARS$1);
|
|
477
|
+
let id = typeof c.id === "string" && c.id ? c.id.slice(0, MAX_COMPONENT_ID_CHARS$1) : (0, node_crypto.randomUUID)();
|
|
462
478
|
if (usedIds.has(id)) id = (0, node_crypto.randomUUID)();
|
|
463
479
|
usedIds.add(id);
|
|
464
480
|
const style = c.style === "primary" || c.style === "secondary" || c.style === "danger" ? c.style : void 0;
|
|
@@ -476,7 +492,7 @@ function sanitizeComponents(raw) {
|
|
|
476
492
|
});
|
|
477
493
|
} else if (c.type === "quick_reply") {
|
|
478
494
|
if (!label) continue;
|
|
479
|
-
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
|
|
495
|
+
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS$1);
|
|
480
496
|
if (!value) continue;
|
|
481
497
|
out.push({
|
|
482
498
|
type: "quick_reply",
|
|
@@ -488,7 +504,7 @@ function sanitizeComponents(raw) {
|
|
|
488
504
|
} else if (c.type === "select") {
|
|
489
505
|
const options = sanitizeOptions(c.options);
|
|
490
506
|
if (options.length === 0) continue;
|
|
491
|
-
const placeholder = capStr(c.placeholder, MAX_COMPONENT_LABEL_CHARS);
|
|
507
|
+
const placeholder = capStr(c.placeholder, MAX_COMPONENT_LABEL_CHARS$1);
|
|
492
508
|
out.push({
|
|
493
509
|
type: "select",
|
|
494
510
|
id,
|
|
@@ -499,7 +515,7 @@ function sanitizeComponents(raw) {
|
|
|
499
515
|
} else if (c.type === "multi_select") {
|
|
500
516
|
const options = sanitizeOptions(c.options);
|
|
501
517
|
if (options.length === 0) continue;
|
|
502
|
-
const submitLabel = capStr(c.submitLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
518
|
+
const submitLabel = capStr(c.submitLabel, MAX_COMPONENT_LABEL_CHARS$1);
|
|
503
519
|
out.push({
|
|
504
520
|
type: "multi_select",
|
|
505
521
|
id,
|
|
@@ -508,11 +524,11 @@ function sanitizeComponents(raw) {
|
|
|
508
524
|
...submitLabel ? { submitLabel } : {}
|
|
509
525
|
});
|
|
510
526
|
} else if (c.type === "confirm") {
|
|
511
|
-
const confirmLabel = capStr(c.confirmLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
512
|
-
const confirmValue = capStr(c.confirmValue, MAX_COMPONENT_VALUE_CHARS);
|
|
527
|
+
const confirmLabel = capStr(c.confirmLabel, MAX_COMPONENT_LABEL_CHARS$1);
|
|
528
|
+
const confirmValue = capStr(c.confirmValue, MAX_COMPONENT_VALUE_CHARS$1);
|
|
513
529
|
if (!confirmLabel || !confirmValue) continue;
|
|
514
|
-
const cancelLabel = capStr(c.cancelLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
515
|
-
const cancelValue = capStr(c.cancelValue, MAX_COMPONENT_VALUE_CHARS);
|
|
530
|
+
const cancelLabel = capStr(c.cancelLabel, MAX_COMPONENT_LABEL_CHARS$1);
|
|
531
|
+
const cancelValue = capStr(c.cancelValue, MAX_COMPONENT_VALUE_CHARS$1);
|
|
516
532
|
out.push({
|
|
517
533
|
type: "confirm",
|
|
518
534
|
id,
|
|
@@ -520,11 +536,11 @@ function sanitizeComponents(raw) {
|
|
|
520
536
|
confirmLabel,
|
|
521
537
|
confirmValue,
|
|
522
538
|
...cancelLabel ? { cancelLabel } : {},
|
|
523
|
-
...cancelValue ? { cancelValue } : {}
|
|
539
|
+
...cancelLabel && cancelValue ? { cancelValue } : {}
|
|
524
540
|
});
|
|
525
541
|
} else if (c.type === "copy_button") {
|
|
526
542
|
if (!label) continue;
|
|
527
|
-
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
|
|
543
|
+
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS$1);
|
|
528
544
|
if (!value) continue;
|
|
529
545
|
out.push({
|
|
530
546
|
type: "copy_button",
|
|
@@ -538,6 +554,23 @@ function sanitizeComponents(raw) {
|
|
|
538
554
|
}
|
|
539
555
|
const CHANNEL_ID = "alfe";
|
|
540
556
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
557
|
+
/** Persist only bucket-backed media; passthrough URLs have no stable id. */
|
|
558
|
+
function mediaToStoredAttachments(media) {
|
|
559
|
+
const stored = [];
|
|
560
|
+
for (const item of media) {
|
|
561
|
+
if (!item.attachmentId) continue;
|
|
562
|
+
const mimeType = item.mimeType ?? "application/octet-stream";
|
|
563
|
+
const type = mimeType.startsWith("image/") ? "image" : mimeType.startsWith("video/") ? "video" : mimeType.startsWith("audio/") ? "audio" : mimeType === "application/pdf" ? "document" : "file";
|
|
564
|
+
stored.push({
|
|
565
|
+
attachmentId: item.attachmentId,
|
|
566
|
+
type,
|
|
567
|
+
mimeType,
|
|
568
|
+
filename: item.filename ?? "file",
|
|
569
|
+
...typeof item.size === "number" && item.size > 0 ? { size: item.size } : {}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return stored.length ? stored : void 0;
|
|
573
|
+
}
|
|
541
574
|
async function sendViaChat(deps, ctx, mediaUrl, components) {
|
|
542
575
|
const client = deps.getChatClient();
|
|
543
576
|
if (!client) throw new Error("Chat service not connected — cannot deliver");
|
|
@@ -563,7 +596,8 @@ async function sendViaChat(deps, ctx, mediaUrl, components) {
|
|
|
563
596
|
await deps.createSession(conversationId, "", "alfe", void 0, userId);
|
|
564
597
|
}
|
|
565
598
|
}
|
|
566
|
-
|
|
599
|
+
const storedMedia = mediaToStoredAttachments(media.attachments);
|
|
600
|
+
if (components?.length || storedMedia?.length) await deps.addMessage(conversationId, "assistant", text, void 0, void 0, components, storedMedia);
|
|
567
601
|
else await deps.addMessage(conversationId, "assistant", text);
|
|
568
602
|
const messageId = (0, node_crypto.randomUUID)();
|
|
569
603
|
client.notify("agent-message", {
|
|
@@ -701,17 +735,22 @@ function createAlfeChannelPlugin(deps) {
|
|
|
701
735
|
error: /* @__PURE__ */ new Error("Missing target — use user:{userId} or conv:{conversationId}")
|
|
702
736
|
};
|
|
703
737
|
if (to.startsWith("conv:")) {
|
|
704
|
-
|
|
738
|
+
const convId = to.slice(5);
|
|
739
|
+
if (!convId) return {
|
|
705
740
|
ok: false,
|
|
706
741
|
error: /* @__PURE__ */ new Error("Empty conversation ID")
|
|
707
742
|
};
|
|
743
|
+
if (convId.length > MAX_CONVERSATION_ID_CHARS || /\s/.test(convId)) return {
|
|
744
|
+
ok: false,
|
|
745
|
+
error: /* @__PURE__ */ new Error("Invalid conversation ID")
|
|
746
|
+
};
|
|
708
747
|
return {
|
|
709
748
|
ok: true,
|
|
710
749
|
to
|
|
711
750
|
};
|
|
712
751
|
}
|
|
713
752
|
const userId = to.startsWith("user:") ? to.slice(5) : to;
|
|
714
|
-
if (!userId || userId === "anon") return {
|
|
753
|
+
if (!userId || userId === "anon" || userId.length > MAX_USER_ID_CHARS || /\s/.test(userId)) return {
|
|
715
754
|
ok: false,
|
|
716
755
|
error: /* @__PURE__ */ new Error("Invalid target: userId is required")
|
|
717
756
|
};
|
|
@@ -749,18 +788,27 @@ function createAlfeChannelPlugin(deps) {
|
|
|
749
788
|
return params.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
750
789
|
},
|
|
751
790
|
applyAccountConfig(params) {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
791
|
+
const previousChannels = params.cfg.channels ?? {};
|
|
792
|
+
const previousSection = previousChannels.alfe ?? {};
|
|
793
|
+
const section = {
|
|
794
|
+
...previousSection,
|
|
795
|
+
...previousSection.accounts ? { accounts: { ...previousSection.accounts } } : {}
|
|
796
|
+
};
|
|
797
|
+
const cfg = {
|
|
798
|
+
...params.cfg,
|
|
799
|
+
channels: {
|
|
800
|
+
...previousChannels,
|
|
801
|
+
alfe: section
|
|
802
|
+
}
|
|
803
|
+
};
|
|
756
804
|
if (params.accountId === DEFAULT_ACCOUNT_ID) section.enabled = true;
|
|
757
|
-
else {
|
|
758
|
-
section.accounts
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
}
|
|
805
|
+
else section.accounts = {
|
|
806
|
+
...section.accounts,
|
|
807
|
+
[params.accountId]: {
|
|
808
|
+
...params.input,
|
|
809
|
+
enabled: true
|
|
810
|
+
}
|
|
811
|
+
};
|
|
764
812
|
return cfg;
|
|
765
813
|
}
|
|
766
814
|
}
|
|
@@ -775,8 +823,7 @@ function createAlfeChannelPlugin(deps) {
|
|
|
775
823
|
function isAlfeSessionKey(key) {
|
|
776
824
|
if (key.startsWith("alfe:")) return true;
|
|
777
825
|
if (key.includes(":alfe:")) return true;
|
|
778
|
-
|
|
779
|
-
return false;
|
|
826
|
+
return /^(?:agent:[^:]+:)?(?:chat-|sms-|wa-)/.test(key);
|
|
780
827
|
}
|
|
781
828
|
/**
|
|
782
829
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
@@ -786,6 +833,25 @@ function extractChannelMode(conversationId, fallback = "chat") {
|
|
|
786
833
|
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
787
834
|
}
|
|
788
835
|
//#endregion
|
|
836
|
+
//#region src/inbound-hook-routing.ts
|
|
837
|
+
/**
|
|
838
|
+
* Preserve Alfe's authoritative identity route on OpenClaw's canonical inbound
|
|
839
|
+
* fields. Arbitrary `extraContext` values are not copied into plugin message
|
|
840
|
+
* hooks, while these fields become `metadata.provider`, `ctx.channelId`, and
|
|
841
|
+
* `ctx.conversationId` respectively.
|
|
842
|
+
*/
|
|
843
|
+
function buildInboundHookRouting(identityProvider, channelMode, conversationId) {
|
|
844
|
+
const explicitProvider = identityProvider?.trim();
|
|
845
|
+
const modeProvider = channelMode.trim();
|
|
846
|
+
const provider = explicitProvider && explicitProvider.length > 0 ? explicitProvider : modeProvider.length > 0 ? modeProvider : "chat";
|
|
847
|
+
return {
|
|
848
|
+
provider,
|
|
849
|
+
surface: "alfe",
|
|
850
|
+
originatingChannel: provider,
|
|
851
|
+
...conversationId ? { originatingTo: conversationId } : {}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
789
855
|
//#region src/session-store.ts
|
|
790
856
|
/**
|
|
791
857
|
* Session Store — persists chat sessions to the local filesystem.
|
|
@@ -812,6 +878,94 @@ async function ensureDir() {
|
|
|
812
878
|
function sessionPath(sessionId) {
|
|
813
879
|
return (0, node_path.join)(SESSIONS_DIR, `${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
814
880
|
}
|
|
881
|
+
function isRecord(value) {
|
|
882
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
883
|
+
}
|
|
884
|
+
function isOptionalString(value) {
|
|
885
|
+
return value === void 0 || typeof value === "string";
|
|
886
|
+
}
|
|
887
|
+
function isOptionalBoolean(value) {
|
|
888
|
+
return value === void 0 || typeof value === "boolean";
|
|
889
|
+
}
|
|
890
|
+
function isStoredSelectOption(value) {
|
|
891
|
+
return isRecord(value) && typeof value.label === "string" && typeof value.value === "string";
|
|
892
|
+
}
|
|
893
|
+
function isStoredMessageComponent(value) {
|
|
894
|
+
if (!isRecord(value) || typeof value.type !== "string" || typeof value.id !== "string") return false;
|
|
895
|
+
if (!(value.style === void 0 || typeof value.style === "string" && [
|
|
896
|
+
"primary",
|
|
897
|
+
"secondary",
|
|
898
|
+
"danger"
|
|
899
|
+
].includes(value.style))) return false;
|
|
900
|
+
switch (value.type) {
|
|
901
|
+
case "link_button": return typeof value.label === "string" && typeof value.url === "string" && (value.target === void 0 || typeof value.target === "string" && [
|
|
902
|
+
"same-tab",
|
|
903
|
+
"new-tab",
|
|
904
|
+
"popup"
|
|
905
|
+
].includes(value.target));
|
|
906
|
+
case "quick_reply": return typeof value.label === "string" && typeof value.value === "string";
|
|
907
|
+
case "select": return isOptionalString(value.label) && isOptionalString(value.placeholder) && Array.isArray(value.options) && value.options.every(isStoredSelectOption);
|
|
908
|
+
case "multi_select": return isOptionalString(value.label) && isOptionalString(value.submitLabel) && Array.isArray(value.options) && value.options.every(isStoredSelectOption);
|
|
909
|
+
case "confirm": return isOptionalString(value.label) && typeof value.confirmLabel === "string" && typeof value.confirmValue === "string" && isOptionalString(value.cancelLabel) && isOptionalString(value.cancelValue);
|
|
910
|
+
case "copy_button": return typeof value.label === "string" && typeof value.value === "string";
|
|
911
|
+
default: return false;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function isStoredAttachment(value) {
|
|
915
|
+
return isRecord(value) && typeof value.attachmentId === "string" && typeof value.type === "string" && [
|
|
916
|
+
"image",
|
|
917
|
+
"video",
|
|
918
|
+
"audio",
|
|
919
|
+
"document",
|
|
920
|
+
"file"
|
|
921
|
+
].includes(value.type) && typeof value.mimeType === "string" && typeof value.filename === "string" && (value.size === void 0 || typeof value.size === "number" && Number.isFinite(value.size) && value.size >= 0);
|
|
922
|
+
}
|
|
923
|
+
function isChatActivityRecord(value) {
|
|
924
|
+
if (!isRecord(value) || typeof value.ts !== "number" || !Number.isFinite(value.ts)) return false;
|
|
925
|
+
if (value.kind === "thinking") return typeof value.text === "string";
|
|
926
|
+
if (value.kind !== "tool") return false;
|
|
927
|
+
if (typeof value.toolCallId !== "string" || typeof value.name !== "string" || typeof value.status !== "string" || ![
|
|
928
|
+
"done",
|
|
929
|
+
"failed",
|
|
930
|
+
"interrupted"
|
|
931
|
+
].includes(value.status) || !isOptionalString(value.summary) || !isOptionalString(value.argsText) || !isOptionalString(value.resultText) || !isOptionalBoolean(value.isError) || value.durationMs !== void 0 && (typeof value.durationMs !== "number" || !Number.isFinite(value.durationMs) || value.durationMs < 0)) return false;
|
|
932
|
+
if (value.truncated === void 0) return true;
|
|
933
|
+
return isRecord(value.truncated) && isOptionalBoolean(value.truncated.args) && isOptionalBoolean(value.truncated.progress) && isOptionalBoolean(value.truncated.result);
|
|
934
|
+
}
|
|
935
|
+
function isStoredRoute(value) {
|
|
936
|
+
return isRecord(value) && typeof value.sessionKey === "string" && typeof value.storePath === "string";
|
|
937
|
+
}
|
|
938
|
+
/** Parse the local persistence boundary without trusting a JSON cast. */
|
|
939
|
+
function asSessionData(value, expectedSessionId) {
|
|
940
|
+
if (!isRecord(value)) return null;
|
|
941
|
+
const record = value;
|
|
942
|
+
if (typeof record.sessionId !== "string" || expectedSessionId !== void 0 && record.sessionId !== expectedSessionId || typeof record.agentId !== "string" || typeof record.channel !== "string" || !isOptionalString(record.tenantId) || !isOptionalString(record.userId) || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string" || !Array.isArray(record.messages)) return null;
|
|
943
|
+
if (!record.messages.every((message) => {
|
|
944
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return false;
|
|
945
|
+
const item = message;
|
|
946
|
+
return (item.role === "user" || item.role === "assistant") && typeof item.content === "string" && typeof item.timestamp === "number" && Number.isFinite(item.timestamp) && isOptionalString(item.senderId) && isOptionalString(item.senderName) && (item.components === void 0 || Array.isArray(item.components) && item.components.every(isStoredMessageComponent)) && (item.attachments === void 0 || Array.isArray(item.attachments) && item.attachments.every(isStoredAttachment));
|
|
947
|
+
})) return null;
|
|
948
|
+
if (record.activity !== void 0 && (!Array.isArray(record.activity) || !record.activity.every(isChatActivityRecord))) return null;
|
|
949
|
+
if (record.routes !== void 0 && (!Array.isArray(record.routes) || !record.routes.every(isStoredRoute))) return null;
|
|
950
|
+
return record;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Replace a session file atomically with owner-only permissions. Temporary
|
|
954
|
+
* names never end in `.json`, so list/backfill scanners ignore crash debris.
|
|
955
|
+
*/
|
|
956
|
+
async function writeTextAtomically(path, contents) {
|
|
957
|
+
const temporary = (0, node_path.join)((0, node_path.dirname)(path), `.${(0, node_path.basename)(path)}.${String(process.pid)}.${(0, node_crypto.randomUUID)()}.tmp`);
|
|
958
|
+
try {
|
|
959
|
+
await (0, node_fs_promises.writeFile)(temporary, contents, {
|
|
960
|
+
encoding: "utf-8",
|
|
961
|
+
mode: 384
|
|
962
|
+
});
|
|
963
|
+
await (0, node_fs_promises.rename)(temporary, path);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
await (0, node_fs_promises.unlink)(temporary).catch(() => void 0);
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
815
969
|
async function cleanupOldSessions() {
|
|
816
970
|
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
817
971
|
try {
|
|
@@ -822,6 +976,7 @@ async function cleanupOldSessions() {
|
|
|
822
976
|
const filePath = (0, node_path.join)(SESSIONS_DIR, file);
|
|
823
977
|
if (now - (await (0, node_fs_promises.stat)(filePath)).mtimeMs > MAX_AGE_MS) await (0, node_fs_promises.unlink)(filePath);
|
|
824
978
|
} catch {}
|
|
979
|
+
lastCleanupAt = Date.now();
|
|
825
980
|
return;
|
|
826
981
|
}
|
|
827
982
|
const fileStats = [];
|
|
@@ -849,7 +1004,7 @@ async function cleanupOldSessions() {
|
|
|
849
1004
|
async function getSession(sessionId) {
|
|
850
1005
|
try {
|
|
851
1006
|
const data = await (0, node_fs_promises.readFile)(sessionPath(sessionId), "utf-8");
|
|
852
|
-
return JSON.parse(data);
|
|
1007
|
+
return asSessionData(JSON.parse(data), sessionId);
|
|
853
1008
|
} catch {
|
|
854
1009
|
return null;
|
|
855
1010
|
}
|
|
@@ -857,7 +1012,7 @@ async function getSession(sessionId) {
|
|
|
857
1012
|
async function saveSession(session) {
|
|
858
1013
|
await ensureDir();
|
|
859
1014
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
860
|
-
await (
|
|
1015
|
+
await writeTextAtomically(sessionPath(session.sessionId), JSON.stringify(session, null, 2));
|
|
861
1016
|
}
|
|
862
1017
|
async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
863
1018
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -953,7 +1108,8 @@ async function listSessions(filters, limit = 50) {
|
|
|
953
1108
|
const summaries = [];
|
|
954
1109
|
for (const file of jsonFiles) try {
|
|
955
1110
|
const data = await (0, node_fs_promises.readFile)((0, node_path.join)(SESSIONS_DIR, file), "utf-8");
|
|
956
|
-
const session = JSON.parse(data);
|
|
1111
|
+
const session = asSessionData(JSON.parse(data));
|
|
1112
|
+
if (!session) continue;
|
|
957
1113
|
if (filters?.agentId && session.agentId !== filters.agentId) continue;
|
|
958
1114
|
if (filters?.channel && session.channel !== filters.channel) continue;
|
|
959
1115
|
if (filters?.tenantId && session.tenantId !== filters.tenantId) continue;
|
|
@@ -975,7 +1131,8 @@ async function listSessions(filters, limit = 50) {
|
|
|
975
1131
|
const aTime = a.lastMessageAt ?? a.createdAt;
|
|
976
1132
|
return (b.lastMessageAt ?? b.createdAt).localeCompare(aTime);
|
|
977
1133
|
});
|
|
978
|
-
|
|
1134
|
+
const boundedLimit = Number.isFinite(limit) ? Math.min(100, Math.max(1, Math.trunc(limit))) : 50;
|
|
1135
|
+
return summaries.slice(0, boundedLimit);
|
|
979
1136
|
}
|
|
980
1137
|
//#endregion
|
|
981
1138
|
//#region src/activity-serialize.ts
|
|
@@ -1336,6 +1493,7 @@ function stripThinkSpans(text) {
|
|
|
1336
1493
|
const MAX_ENTRIES = 500;
|
|
1337
1494
|
const MAX_THINKING_CHARS = 16e3;
|
|
1338
1495
|
const MAX_TRANSCRIPT_BYTES = 4 * 1024 * 1024;
|
|
1496
|
+
const MAX_TRANSCRIPT_INDEX_BYTES = 4 * 1024 * 1024;
|
|
1339
1497
|
/** Default OpenClaw store for the daemon's single agent ("main"). */
|
|
1340
1498
|
function defaultStorePath() {
|
|
1341
1499
|
return (0, node_path.join)((0, node_os.homedir)(), ".openclaw", "agents", "main", "sessions", "sessions.json");
|
|
@@ -1356,8 +1514,8 @@ async function readTranscriptTail(path) {
|
|
|
1356
1514
|
const { size } = await handle.stat();
|
|
1357
1515
|
if (size <= MAX_TRANSCRIPT_BYTES) return await handle.readFile({ encoding: "utf-8" });
|
|
1358
1516
|
const buf = Buffer.alloc(MAX_TRANSCRIPT_BYTES);
|
|
1359
|
-
await handle.read(buf, 0, MAX_TRANSCRIPT_BYTES, size - MAX_TRANSCRIPT_BYTES);
|
|
1360
|
-
const text = buf.toString("utf-8");
|
|
1517
|
+
const { bytesRead } = await handle.read(buf, 0, MAX_TRANSCRIPT_BYTES, size - MAX_TRANSCRIPT_BYTES);
|
|
1518
|
+
const text = buf.subarray(0, bytesRead).toString("utf-8");
|
|
1361
1519
|
const firstNewline = text.indexOf("\n");
|
|
1362
1520
|
return firstNewline === -1 ? "" : text.slice(firstNewline + 1);
|
|
1363
1521
|
} finally {
|
|
@@ -1367,30 +1525,44 @@ async function readTranscriptTail(path) {
|
|
|
1367
1525
|
return null;
|
|
1368
1526
|
}
|
|
1369
1527
|
}
|
|
1528
|
+
/** Read a complete JSON index only when it stays inside the local memory cap. */
|
|
1529
|
+
async function readTranscriptIndex(path) {
|
|
1530
|
+
try {
|
|
1531
|
+
const handle = await (0, node_fs_promises.open)(path, "r");
|
|
1532
|
+
try {
|
|
1533
|
+
const { size } = await handle.stat();
|
|
1534
|
+
if (size < 1 || size > MAX_TRANSCRIPT_INDEX_BYTES) return null;
|
|
1535
|
+
return JSON.parse(await handle.readFile({ encoding: "utf-8" }));
|
|
1536
|
+
} finally {
|
|
1537
|
+
await handle.close();
|
|
1538
|
+
}
|
|
1539
|
+
} catch {
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1370
1543
|
/**
|
|
1371
1544
|
* Find the transcript files for a conversation: exact route matches first,
|
|
1372
1545
|
* then an index scan for `:conv:{conversationId}` keys (retroactive path).
|
|
1373
1546
|
* Returns absolute jsonl paths, deduped.
|
|
1374
1547
|
*/
|
|
1375
1548
|
async function resolveTranscriptPaths(conversationId, routes) {
|
|
1376
|
-
const
|
|
1549
|
+
const validRoutes = (routes ?? []).filter((route) => Boolean(route) && typeof route === "object" && typeof route.sessionKey === "string" && route.sessionKey.length > 0 && typeof route.storePath === "string" && route.storePath.length > 0 && route.storePath.length <= 4096);
|
|
1550
|
+
const storePaths = new Set(validRoutes.map((r) => r.storePath));
|
|
1377
1551
|
if (storePaths.size === 0) storePaths.add(defaultStorePath());
|
|
1378
|
-
const routeKeys = new Set(
|
|
1552
|
+
const routeKeys = new Set(validRoutes.map((r) => r.sessionKey));
|
|
1379
1553
|
const convSegment = `:conv:${conversationId}`;
|
|
1380
1554
|
const files = /* @__PURE__ */ new Set();
|
|
1381
1555
|
for (const storePath of storePaths) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
index = JSON.parse(await (0, node_fs_promises.readFile)(storePath, "utf-8"));
|
|
1385
|
-
} catch {
|
|
1386
|
-
continue;
|
|
1387
|
-
}
|
|
1556
|
+
const parsed = await readTranscriptIndex(storePath);
|
|
1557
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
1388
1558
|
const dir = (0, node_path.dirname)(storePath);
|
|
1389
|
-
for (const [key,
|
|
1390
|
-
if (!
|
|
1559
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1560
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
1561
|
+
const sessionId = value.sessionId;
|
|
1562
|
+
if (typeof sessionId !== "string" || !sessionId) continue;
|
|
1391
1563
|
if (!(routeKeys.has(key) || key.endsWith(convSegment) || key.includes(`${convSegment}:`))) continue;
|
|
1392
|
-
if (!/^[A-Za-z0-9._-]+$/.test(
|
|
1393
|
-
files.add((0, node_path.join)(dir, `${
|
|
1564
|
+
if (!/^[A-Za-z0-9._-]+$/.test(sessionId)) continue;
|
|
1565
|
+
files.add((0, node_path.join)(dir, `${sessionId}.jsonl`));
|
|
1394
1566
|
}
|
|
1395
1567
|
}
|
|
1396
1568
|
return [...files];
|
|
@@ -1480,6 +1652,18 @@ async function collectTranscriptActivity(conversationId, routes) {
|
|
|
1480
1652
|
}
|
|
1481
1653
|
//#endregion
|
|
1482
1654
|
//#region src/a2a-tools.ts
|
|
1655
|
+
const MAX_AGENT_ID_CHARS = 256;
|
|
1656
|
+
const MAX_A2A_MESSAGE_CHARS = 32e3;
|
|
1657
|
+
const MAX_THREAD_ID_CHARS = 128;
|
|
1658
|
+
const MAX_A2A_DEPTH = 20;
|
|
1659
|
+
const MAX_COMPONENT_TARGET_CHARS = 512;
|
|
1660
|
+
const MAX_COMPONENT_TEXT_CHARS = 32e3;
|
|
1661
|
+
const MAX_COMPONENT_ID_CHARS = 128;
|
|
1662
|
+
const MAX_COMPONENT_LABEL_CHARS = 120;
|
|
1663
|
+
const MAX_COMPONENT_VALUE_CHARS = 400;
|
|
1664
|
+
const MAX_COMPONENT_URL_CHARS = 2048;
|
|
1665
|
+
const MAX_COMPONENTS = 10;
|
|
1666
|
+
const MAX_COMPONENT_OPTIONS = 25;
|
|
1483
1667
|
let conversationEndRequested = false;
|
|
1484
1668
|
let a2aTurnArmed = false;
|
|
1485
1669
|
/**
|
|
@@ -1505,41 +1689,14 @@ function disarmA2AEndSignal() {
|
|
|
1505
1689
|
function isA2AEndSignalled() {
|
|
1506
1690
|
return conversationEndRequested;
|
|
1507
1691
|
}
|
|
1508
|
-
function ok(result) {
|
|
1509
|
-
return { content: [{
|
|
1510
|
-
type: "text",
|
|
1511
|
-
text: JSON.stringify(result)
|
|
1512
|
-
}] };
|
|
1513
|
-
}
|
|
1514
|
-
function errResult(message) {
|
|
1515
|
-
return { content: [{
|
|
1516
|
-
type: "text",
|
|
1517
|
-
text: JSON.stringify({ error: message })
|
|
1518
|
-
}] };
|
|
1519
|
-
}
|
|
1520
|
-
function defineTool(def) {
|
|
1521
|
-
return {
|
|
1522
|
-
name: def.name,
|
|
1523
|
-
description: def.description,
|
|
1524
|
-
label: def.name,
|
|
1525
|
-
parameters: def.parameters,
|
|
1526
|
-
execute: async (_toolCallId, params) => {
|
|
1527
|
-
try {
|
|
1528
|
-
return ok(await def.handler(params));
|
|
1529
|
-
} catch (e) {
|
|
1530
|
-
return errResult(e.message);
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
1692
|
function buildA2ATools(getChatClient, log, present) {
|
|
1536
1693
|
const requireClient = () => {
|
|
1537
1694
|
const client = getChatClient();
|
|
1538
|
-
if (!client) throw
|
|
1695
|
+
if (!client) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("chat service not connected yet — try again in a moment");
|
|
1539
1696
|
return client;
|
|
1540
1697
|
};
|
|
1541
1698
|
const tools = [
|
|
1542
|
-
defineTool({
|
|
1699
|
+
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
1543
1700
|
name: "list_agents",
|
|
1544
1701
|
description: "List other agents in your organization that you can communicate with.",
|
|
1545
1702
|
parameters: {
|
|
@@ -1552,26 +1709,35 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1552
1709
|
return await requireClient().sendRequest("a2a.list-agents", {});
|
|
1553
1710
|
}
|
|
1554
1711
|
}),
|
|
1555
|
-
defineTool({
|
|
1712
|
+
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
1556
1713
|
name: "message_agent",
|
|
1557
|
-
description: "Send a message to another agent. Starts a new conversation thread, or continues an existing one. The conversation will bounce back and forth automatically until
|
|
1714
|
+
description: "Send a message to another agent. Starts a new conversation thread, or continues an existing one. The conversation will bounce back and forth automatically until an agent calls end_conversation() or the server depth limit is reached.",
|
|
1558
1715
|
parameters: {
|
|
1559
1716
|
type: "object",
|
|
1560
1717
|
properties: {
|
|
1561
1718
|
agent_id: {
|
|
1562
1719
|
type: "string",
|
|
1720
|
+
minLength: 1,
|
|
1721
|
+
maxLength: MAX_AGENT_ID_CHARS,
|
|
1563
1722
|
description: "Target agent ID (use list_agents to find available agents)"
|
|
1564
1723
|
},
|
|
1565
1724
|
message: {
|
|
1566
1725
|
type: "string",
|
|
1726
|
+
minLength: 1,
|
|
1727
|
+
maxLength: MAX_A2A_MESSAGE_CHARS,
|
|
1567
1728
|
description: "Message to send to the agent"
|
|
1568
1729
|
},
|
|
1569
1730
|
thread_id: {
|
|
1570
1731
|
type: "string",
|
|
1732
|
+
minLength: 1,
|
|
1733
|
+
maxLength: MAX_THREAD_ID_CHARS,
|
|
1734
|
+
pattern: "^[A-Za-z0-9_-]+$",
|
|
1571
1735
|
description: "Continue an existing conversation thread (omit to start a new one)"
|
|
1572
1736
|
},
|
|
1573
1737
|
max_depth: {
|
|
1574
|
-
type: "
|
|
1738
|
+
type: "integer",
|
|
1739
|
+
minimum: 1,
|
|
1740
|
+
maximum: MAX_A2A_DEPTH,
|
|
1575
1741
|
description: "Maximum number of back-and-forth exchanges (default: 10)"
|
|
1576
1742
|
}
|
|
1577
1743
|
},
|
|
@@ -1582,7 +1748,7 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1582
1748
|
const message = params.message;
|
|
1583
1749
|
const threadId = params.thread_id;
|
|
1584
1750
|
const maxDepth = params.max_depth ?? 10;
|
|
1585
|
-
if (
|
|
1751
|
+
if (typeof agentId !== "string" || agentId.length < 1 || agentId.length > MAX_AGENT_ID_CHARS || typeof message !== "string" || message.length < 1 || message.length > MAX_A2A_MESSAGE_CHARS || threadId !== void 0 && (typeof threadId !== "string" || threadId.length < 1 || threadId.length > MAX_THREAD_ID_CHARS || !/^[A-Za-z0-9_-]+$/.test(threadId)) || !Number.isInteger(maxDepth) || maxDepth < 1 || maxDepth > MAX_A2A_DEPTH) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Invalid message_agent parameters");
|
|
1586
1752
|
log.info(`message_agent tool: sending to ${agentId}`);
|
|
1587
1753
|
return await requireClient().sendRequest("a2a.send", {
|
|
1588
1754
|
targetAgentId: agentId,
|
|
@@ -1592,13 +1758,14 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1592
1758
|
});
|
|
1593
1759
|
}
|
|
1594
1760
|
}),
|
|
1595
|
-
defineTool({
|
|
1761
|
+
(0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
1596
1762
|
name: "end_conversation",
|
|
1597
1763
|
description: "End the current agent-to-agent conversation. Call this when the discussion is resolved and no further exchanges are needed.",
|
|
1598
1764
|
parameters: {
|
|
1599
1765
|
type: "object",
|
|
1600
1766
|
properties: { summary: {
|
|
1601
1767
|
type: "string",
|
|
1768
|
+
maxLength: 1e3,
|
|
1602
1769
|
description: "Brief summary of what was discussed or resolved"
|
|
1603
1770
|
} },
|
|
1604
1771
|
required: []
|
|
@@ -1614,7 +1781,7 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1614
1781
|
}
|
|
1615
1782
|
})
|
|
1616
1783
|
];
|
|
1617
|
-
if (present) tools.push(defineTool({
|
|
1784
|
+
if (present) tools.push((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
|
|
1618
1785
|
name: "chat_present_components",
|
|
1619
1786
|
description: "Attach interactive UI components to a chat message so the user can act with a tap instead of typing. PREFER components whenever you offer the user a discrete set of choices — a quick_reply or select reads far better than \"reply 1, 2, or 3\". Use:\n • quick_reply — one-tap canned replies for a short set of options.\n • select — a single choice from a longer list (dropdown).\n • multi_select — let the user pick MANY options, then submit.\n • confirm — an approval / yes-no decision (primary + secondary button).\n • link_button — an actionable LINK (open a dashboard, a browser-takeover deep link). URL must be https on an Alfe-owned host.\n • copy_button — let the user copy a value (token, id, snippet) to their clipboard.\nChoosing/submitting an interactive component sends the chosen value back AS THE USER'S NEXT MESSAGE, so the conversation continues normally — you will see it as ordinary user input. Don't button-spam: components are for real interactions, not decoration, and each message allows at most 10. Components degrade to plain text on clients that don't render them, so keep the essentials in `text` too.\nExamples:\n Approval: { to:\"conv:abc\", text:\"Deploy to production?\", components:[ { type:\"confirm\", id:\"c1\", confirmLabel:\"Deploy\", confirmValue:\"yes, deploy\", cancelLabel:\"Cancel\", cancelValue:\"no, hold off\" } ] }\n Pick one: { to:\"user:u_123\", text:\"Which environment?\", components:[ { type:\"select\", id:\"s1\", placeholder:\"Choose an environment\", options:[ { label:\"Dev\", value:\"dev\" }, { label:\"Staging\", value:\"staging\" }, { label:\"Prod\", value:\"prod\" } ] } ] }\n Link: { to:\"conv:abc\", text:\"I need you to finish the login step.\", components:[ { type:\"link_button\", id:\"b1\", label:\"Open browser session\", url:\"https://app.alfe.ai/agents/x?tab=browser\", target:\"new-tab\", style:\"primary\" } ] }",
|
|
1620
1787
|
parameters: {
|
|
@@ -1622,14 +1789,20 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1622
1789
|
properties: {
|
|
1623
1790
|
to: {
|
|
1624
1791
|
type: "string",
|
|
1792
|
+
minLength: 6,
|
|
1793
|
+
maxLength: MAX_COMPONENT_TARGET_CHARS + 5,
|
|
1794
|
+
pattern: "^(conv|user):.+$",
|
|
1625
1795
|
description: "Target conversation: \"conv:{conversationId}\" or \"user:{userId}\"."
|
|
1626
1796
|
},
|
|
1627
1797
|
text: {
|
|
1628
1798
|
type: "string",
|
|
1799
|
+
maxLength: MAX_COMPONENT_TEXT_CHARS,
|
|
1629
1800
|
description: "Optional message text rendered above the components. Keep the key info here too — it is the fallback on clients that cannot render components."
|
|
1630
1801
|
},
|
|
1631
1802
|
components: {
|
|
1632
1803
|
type: "array",
|
|
1804
|
+
minItems: 1,
|
|
1805
|
+
maxItems: MAX_COMPONENTS,
|
|
1633
1806
|
description: "Interactive components to attach (at least one, at most 10).",
|
|
1634
1807
|
items: {
|
|
1635
1808
|
type: "object",
|
|
@@ -1648,14 +1821,18 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1648
1821
|
},
|
|
1649
1822
|
id: {
|
|
1650
1823
|
type: "string",
|
|
1824
|
+
minLength: 1,
|
|
1825
|
+
maxLength: MAX_COMPONENT_ID_CHARS,
|
|
1651
1826
|
description: "Stable id (optional — auto-generated when omitted)."
|
|
1652
1827
|
},
|
|
1653
1828
|
label: {
|
|
1654
1829
|
type: "string",
|
|
1830
|
+
maxLength: MAX_COMPONENT_LABEL_CHARS,
|
|
1655
1831
|
description: "Button/control text. Required for link_button, quick_reply, copy_button; optional heading for select / multi_select."
|
|
1656
1832
|
},
|
|
1657
1833
|
url: {
|
|
1658
1834
|
type: "string",
|
|
1835
|
+
maxLength: MAX_COMPONENT_URL_CHARS,
|
|
1659
1836
|
description: "link_button only: https URL on an Alfe-owned host to open when clicked."
|
|
1660
1837
|
},
|
|
1661
1838
|
target: {
|
|
@@ -1669,24 +1846,32 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1669
1846
|
},
|
|
1670
1847
|
value: {
|
|
1671
1848
|
type: "string",
|
|
1849
|
+
maxLength: MAX_COMPONENT_VALUE_CHARS,
|
|
1672
1850
|
description: "quick_reply: the text submitted as the user's next message when tapped. copy_button: the value copied to the clipboard."
|
|
1673
1851
|
},
|
|
1674
1852
|
placeholder: {
|
|
1675
1853
|
type: "string",
|
|
1854
|
+
maxLength: MAX_COMPONENT_LABEL_CHARS,
|
|
1676
1855
|
description: "select only: the empty-state prompt shown before a choice is made."
|
|
1677
1856
|
},
|
|
1678
1857
|
options: {
|
|
1679
1858
|
type: "array",
|
|
1859
|
+
minItems: 1,
|
|
1860
|
+
maxItems: MAX_COMPONENT_OPTIONS,
|
|
1680
1861
|
description: "select / multi_select only: the choices (max 25). Each is { label, value }; `value` is what gets submitted (multi_select joins checked values with \", \").",
|
|
1681
1862
|
items: {
|
|
1682
1863
|
type: "object",
|
|
1683
1864
|
properties: {
|
|
1684
1865
|
label: {
|
|
1685
1866
|
type: "string",
|
|
1867
|
+
minLength: 1,
|
|
1868
|
+
maxLength: MAX_COMPONENT_LABEL_CHARS,
|
|
1686
1869
|
description: "Option text shown to the user."
|
|
1687
1870
|
},
|
|
1688
1871
|
value: {
|
|
1689
1872
|
type: "string",
|
|
1873
|
+
minLength: 1,
|
|
1874
|
+
maxLength: MAX_COMPONENT_VALUE_CHARS,
|
|
1690
1875
|
description: "Value submitted when chosen."
|
|
1691
1876
|
}
|
|
1692
1877
|
},
|
|
@@ -1695,22 +1880,27 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1695
1880
|
},
|
|
1696
1881
|
submitLabel: {
|
|
1697
1882
|
type: "string",
|
|
1883
|
+
maxLength: MAX_COMPONENT_LABEL_CHARS,
|
|
1698
1884
|
description: "multi_select only: submit button caption (default \"Submit\")."
|
|
1699
1885
|
},
|
|
1700
1886
|
confirmLabel: {
|
|
1701
1887
|
type: "string",
|
|
1888
|
+
maxLength: MAX_COMPONENT_LABEL_CHARS,
|
|
1702
1889
|
description: "confirm only: primary button text (e.g. \"Approve\")."
|
|
1703
1890
|
},
|
|
1704
1891
|
confirmValue: {
|
|
1705
1892
|
type: "string",
|
|
1893
|
+
maxLength: MAX_COMPONENT_VALUE_CHARS,
|
|
1706
1894
|
description: "confirm only: value submitted when the primary button is tapped."
|
|
1707
1895
|
},
|
|
1708
1896
|
cancelLabel: {
|
|
1709
1897
|
type: "string",
|
|
1898
|
+
maxLength: MAX_COMPONENT_LABEL_CHARS,
|
|
1710
1899
|
description: "confirm only: secondary button text (omit to show only the primary)."
|
|
1711
1900
|
},
|
|
1712
1901
|
cancelValue: {
|
|
1713
1902
|
type: "string",
|
|
1903
|
+
maxLength: MAX_COMPONENT_VALUE_CHARS,
|
|
1714
1904
|
description: "confirm only: value submitted when the secondary button is tapped."
|
|
1715
1905
|
},
|
|
1716
1906
|
style: {
|
|
@@ -1731,14 +1921,20 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1731
1921
|
},
|
|
1732
1922
|
handler: async (params) => {
|
|
1733
1923
|
const to = params.to;
|
|
1734
|
-
if (!to || !to.startsWith("conv:") && !to.startsWith("user:")) throw
|
|
1924
|
+
if (!to || !to.startsWith("conv:") && !to.startsWith("user:") || to.length > MAX_COMPONENT_TARGET_CHARS + 5 || to.slice(to.indexOf(":") + 1).length < 1) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("`to` must be \"conv:{conversationId}\" or \"user:{userId}\"");
|
|
1735
1925
|
const text = params.text;
|
|
1926
|
+
if (text !== void 0 && (typeof text !== "string" || text.length > MAX_COMPONENT_TEXT_CHARS)) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("`text` exceeds the 32000-character limit");
|
|
1736
1927
|
log.info("chat_present_components tool called");
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1928
|
+
try {
|
|
1929
|
+
return await present({
|
|
1930
|
+
to,
|
|
1931
|
+
text,
|
|
1932
|
+
components: params.components
|
|
1933
|
+
});
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (error instanceof Error && error.message === "At least one valid component is required") throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)(error.message);
|
|
1936
|
+
throw error;
|
|
1937
|
+
}
|
|
1742
1938
|
}
|
|
1743
1939
|
}));
|
|
1744
1940
|
return tools;
|
|
@@ -1771,14 +1967,27 @@ const DEFAULT_EXACT_HOSTS = [
|
|
|
1771
1967
|
"mmg.whatsapp.net"
|
|
1772
1968
|
];
|
|
1773
1969
|
const DEFAULT_SUFFIX_HOSTS = [
|
|
1774
|
-
".s3.amazonaws.com",
|
|
1775
|
-
".amazonaws.com",
|
|
1776
1970
|
".twiliocdn.com",
|
|
1777
1971
|
".cdn.discordapp.com",
|
|
1778
1972
|
".discordapp.net",
|
|
1779
1973
|
".telegram.org",
|
|
1780
1974
|
".alfe.ai"
|
|
1781
1975
|
];
|
|
1976
|
+
const AWS_REGION = /^(?:[a-z]{2}(?:-gov)?|us-iso[a-z]?|eusc(?:-[a-z]{2})?)-[a-z0-9-]+-\d$/;
|
|
1977
|
+
/** Keep byte-for-byte policy parity with services/chat/src/lib/media-url. */
|
|
1978
|
+
function isAmazonS3Host(host) {
|
|
1979
|
+
if (host === "s3.amazonaws.com" || host.endsWith(".s3.amazonaws.com")) return true;
|
|
1980
|
+
if (!host.endsWith(".amazonaws.com")) return false;
|
|
1981
|
+
const labels = host.slice(0, -14).split(".");
|
|
1982
|
+
return labels.some((service, index) => {
|
|
1983
|
+
const tail = labels.slice(index + 1);
|
|
1984
|
+
if (/^s3-(?:[a-z]{2}(?:-gov)?|us-iso[a-z]?|eusc(?:-[a-z]{2})?)-[a-z0-9-]+-\d$/.test(service)) return tail.length === 0;
|
|
1985
|
+
if (service === "s3-accelerate") return tail.length === 0 || tail.length === 1 && tail[0] === "dualstack";
|
|
1986
|
+
if (service !== "s3" && service !== "s3-fips" && service !== "s3-accesspoint" && service !== "s3-object-lambda" && service !== "s3-outposts") return false;
|
|
1987
|
+
if (tail.length === 1) return AWS_REGION.test(tail[0] ?? "");
|
|
1988
|
+
return tail.length === 2 && tail[0] === "dualstack" && AWS_REGION.test(tail[1] ?? "");
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1782
1991
|
function parseExtraHosts(raw) {
|
|
1783
1992
|
const exact = /* @__PURE__ */ new Set();
|
|
1784
1993
|
const suffix = [];
|
|
@@ -1837,7 +2046,9 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
1837
2046
|
reason: "blocked_host"
|
|
1838
2047
|
};
|
|
1839
2048
|
const extra = parseExtraHosts(opts.extraHosts ?? process.env.ALFE_ATTACHMENT_ALLOWED_HOSTS);
|
|
1840
|
-
|
|
2049
|
+
const exact = new Set([...DEFAULT_EXACT_HOSTS.map((h) => h.toLowerCase()), ...extra.exact]);
|
|
2050
|
+
if (isAmazonS3Host(host)) return { ok: true };
|
|
2051
|
+
if (exact.has(host)) return { ok: true };
|
|
1841
2052
|
if ([...DEFAULT_SUFFIX_HOSTS.map((h) => h.toLowerCase()), ...extra.suffix].some((rule) => host === rule.slice(1) || host.endsWith(rule))) return { ok: true };
|
|
1842
2053
|
return {
|
|
1843
2054
|
ok: false,
|
|
@@ -1845,6 +2056,40 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
1845
2056
|
};
|
|
1846
2057
|
}
|
|
1847
2058
|
//#endregion
|
|
2059
|
+
//#region src/local-ai-proxy-identity.ts
|
|
2060
|
+
const LOCAL_AI_PROXY_IDENTITY_URL = "http://127.0.0.1:18193/__alfe/set-identity";
|
|
2061
|
+
const LOCAL_AI_PROXY_CONTROL_TIMEOUT_MS = 1e3;
|
|
2062
|
+
const MAX_IDENTITY_ID_CHARS = 256;
|
|
2063
|
+
/**
|
|
2064
|
+
* Best-effort loopback identity handshake for the local AI proxy.
|
|
2065
|
+
*
|
|
2066
|
+
* A turn awaits this before model dispatch so attribution cannot lose a race
|
|
2067
|
+
* with the first LLM request. Failure never rejects the chat turn.
|
|
2068
|
+
*/
|
|
2069
|
+
async function syncLocalAiProxyIdentity(identityId, fetchFn = fetch) {
|
|
2070
|
+
if (identityId !== void 0 && !isValidIdentityId(identityId)) return false;
|
|
2071
|
+
try {
|
|
2072
|
+
const response = await fetchFn(LOCAL_AI_PROXY_IDENTITY_URL, {
|
|
2073
|
+
method: "POST",
|
|
2074
|
+
headers: { "Content-Type": "application/json" },
|
|
2075
|
+
body: JSON.stringify({ identityId: identityId ?? null }),
|
|
2076
|
+
signal: AbortSignal.timeout(LOCAL_AI_PROXY_CONTROL_TIMEOUT_MS)
|
|
2077
|
+
});
|
|
2078
|
+
try {
|
|
2079
|
+
await response.body?.cancel();
|
|
2080
|
+
} catch {}
|
|
2081
|
+
return response.ok;
|
|
2082
|
+
} catch {
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
function isValidIdentityId(value) {
|
|
2087
|
+
return value.length > 0 && value.length <= MAX_IDENTITY_ID_CHARS && !Array.from(value).some((character) => {
|
|
2088
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
2089
|
+
return codePoint < 32 || codePoint === 127;
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
//#endregion
|
|
1848
2093
|
//#region src/plugin.ts
|
|
1849
2094
|
/**
|
|
1850
2095
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -2233,6 +2478,7 @@ async function resolveOpenClawSdk(log) {
|
|
|
2233
2478
|
} catch {}
|
|
2234
2479
|
log.warn(`OpenClaw SDK not resolvable — chat dispatch will not work. argv1=${process.argv[1] ?? "<none>"} execPath=${process.execPath} PATH=${pathEnv ? "set" : "EMPTY"} anchorsTried=[${anchors.join(", ")}]`);
|
|
2235
2480
|
}
|
|
2481
|
+
const CHAT_ACTIVATION_KEY = (0, _alfe_ai_openclaw_plugin_kit.getActivationKey)("chat");
|
|
2236
2482
|
let pluginRuntime = null;
|
|
2237
2483
|
let chatClient = null;
|
|
2238
2484
|
let connectingPromise = null;
|
|
@@ -2335,7 +2581,7 @@ function replayAttachments(atts) {
|
|
|
2335
2581
|
...typeof a.size === "number" ? { size: a.size } : {}
|
|
2336
2582
|
}));
|
|
2337
2583
|
}
|
|
2338
|
-
const
|
|
2584
|
+
const MAX_INBOUND_ATTACHMENT_SIZE = 50 * 1024 * 1024;
|
|
2339
2585
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
2340
2586
|
const MAX_REDIRECTS = 5;
|
|
2341
2587
|
/**
|
|
@@ -2368,13 +2614,62 @@ async function fetchAttachmentWithValidation(url, signal) {
|
|
|
2368
2614
|
}
|
|
2369
2615
|
throw new Error("too_many_redirects");
|
|
2370
2616
|
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Read a response body without ever retaining more than the attachment cap.
|
|
2619
|
+
* `arrayBuffer()` cannot enforce a cap until after the entire response has
|
|
2620
|
+
* already been allocated, so both declared and streamed sizes are checked.
|
|
2621
|
+
*/
|
|
2622
|
+
async function readAttachmentBodyBounded(response, maxBytes = MAX_INBOUND_ATTACHMENT_SIZE) {
|
|
2623
|
+
const declaredRaw = response.headers.get("content-length");
|
|
2624
|
+
if (declaredRaw !== null) {
|
|
2625
|
+
const declared = Number(declaredRaw);
|
|
2626
|
+
if (Number.isFinite(declared) && declared > maxBytes) throw new Error("attachment_too_large");
|
|
2627
|
+
}
|
|
2628
|
+
if (!response.body) return new Uint8Array();
|
|
2629
|
+
const reader = response.body.getReader();
|
|
2630
|
+
const chunks = [];
|
|
2631
|
+
let total = 0;
|
|
2632
|
+
try {
|
|
2633
|
+
for (;;) {
|
|
2634
|
+
const { done, value } = await reader.read();
|
|
2635
|
+
if (done) break;
|
|
2636
|
+
total += value.byteLength;
|
|
2637
|
+
if (total > maxBytes) {
|
|
2638
|
+
await reader.cancel("attachment_too_large").catch(() => void 0);
|
|
2639
|
+
throw new Error("attachment_too_large");
|
|
2640
|
+
}
|
|
2641
|
+
chunks.push(value);
|
|
2642
|
+
}
|
|
2643
|
+
} finally {
|
|
2644
|
+
reader.releaseLock();
|
|
2645
|
+
}
|
|
2646
|
+
const body = new Uint8Array(total);
|
|
2647
|
+
let offset = 0;
|
|
2648
|
+
for (const chunk of chunks) {
|
|
2649
|
+
body.set(chunk, offset);
|
|
2650
|
+
offset += chunk.byteLength;
|
|
2651
|
+
}
|
|
2652
|
+
return body;
|
|
2653
|
+
}
|
|
2654
|
+
/** Build one safe, collision-resistant filename inside the attachment dir. */
|
|
2655
|
+
function buildAttachmentLocalFilename(att) {
|
|
2656
|
+
const safeId = att.id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 256) || "attachment";
|
|
2657
|
+
const safeName = Array.from(att.filename ?? "file", (character) => {
|
|
2658
|
+
const code = character.codePointAt(0) ?? 0;
|
|
2659
|
+
return code <= 31 || code === 127 ? "_" : character;
|
|
2660
|
+
}).join("").replace(/[\\/]/g, "_").replace(/\.\./g, "_").slice(0, 512) || "file";
|
|
2661
|
+
return `${safeId}_${(0, node_crypto.randomUUID)()}_${safeName}`;
|
|
2662
|
+
}
|
|
2371
2663
|
async function downloadAttachments(attachments, log) {
|
|
2372
2664
|
const attachDir = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "attachments");
|
|
2373
|
-
await (0, node_fs_promises.mkdir)(attachDir, {
|
|
2665
|
+
await (0, node_fs_promises.mkdir)(attachDir, {
|
|
2666
|
+
recursive: true,
|
|
2667
|
+
mode: 448
|
|
2668
|
+
});
|
|
2374
2669
|
const results = [];
|
|
2375
|
-
for (const att of attachments) {
|
|
2376
|
-
const filename = (att.filename ??
|
|
2377
|
-
const localPath = (0, node_path.join)(attachDir,
|
|
2670
|
+
for (const att of attachments.slice(0, 10)) {
|
|
2671
|
+
const filename = (att.filename ?? "file").slice(0, 512) || "file";
|
|
2672
|
+
const localPath = (0, node_path.join)(attachDir, buildAttachmentLocalFilename(att));
|
|
2378
2673
|
const controller = new AbortController();
|
|
2379
2674
|
const timeout = setTimeout(() => {
|
|
2380
2675
|
controller.abort();
|
|
@@ -2385,12 +2680,11 @@ async function downloadAttachments(attachments, log) {
|
|
|
2385
2680
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
2386
2681
|
continue;
|
|
2387
2682
|
}
|
|
2388
|
-
const buffer =
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
}
|
|
2393
|
-
await (0, node_fs_promises.writeFile)(localPath, buffer);
|
|
2683
|
+
const buffer = await readAttachmentBodyBounded(res);
|
|
2684
|
+
await (0, node_fs_promises.writeFile)(localPath, buffer, {
|
|
2685
|
+
flag: "wx",
|
|
2686
|
+
mode: 384
|
|
2687
|
+
});
|
|
2394
2688
|
results.push({
|
|
2395
2689
|
localPath,
|
|
2396
2690
|
filename,
|
|
@@ -2398,6 +2692,7 @@ async function downloadAttachments(attachments, log) {
|
|
|
2398
2692
|
});
|
|
2399
2693
|
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
2400
2694
|
} catch (err) {
|
|
2695
|
+
if ((err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : void 0) !== "EEXIST") await (0, node_fs_promises.unlink)(localPath).catch(() => void 0);
|
|
2401
2696
|
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2402
2697
|
} finally {
|
|
2403
2698
|
clearTimeout(timeout);
|
|
@@ -2537,6 +2832,20 @@ const VOICE_REPLY_SYSTEM_PROMPT = [
|
|
|
2537
2832
|
*/
|
|
2538
2833
|
const CROSS_AGENT_TOOLS_SYSTEM_PROMPT = ["Cross-agent messaging: to contact another Alfe agent (IDs like agt_…), use list_agents to discover and message_agent to send.", "The runtime's sessions_send / sessions_spawn tools do NOT know Alfe agent IDs and will fail with \"agent not found\" — never use them to reach another agent."].join("\n");
|
|
2539
2834
|
/**
|
|
2835
|
+
* Rules for an inbound agent-to-agent turn. This must ride the runtime-rendered
|
|
2836
|
+
* `GroupSystemPrompt` field; arbitrary extra-context keys are not shown to the
|
|
2837
|
+
* model. Keep the instruction structural: `end_conversation` is the plugin's
|
|
2838
|
+
* authoritative turn-local completion signal. The relay still accepts the
|
|
2839
|
+
* legacy trailing `[RESOLVED]` sentinel for older plugin versions.
|
|
2840
|
+
*/
|
|
2841
|
+
const A2A_REPLY_SYSTEM_PROMPT = [
|
|
2842
|
+
"This is an agent-to-agent conversation with another Alfe agent.",
|
|
2843
|
+
"Rules:",
|
|
2844
|
+
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
2845
|
+
"- When the discussion is complete, call the end_conversation tool.",
|
|
2846
|
+
"- Do not respond just to acknowledge receipt; that creates unproductive reply loops."
|
|
2847
|
+
].join("\n");
|
|
2848
|
+
/**
|
|
2540
2849
|
* Per-turn envelope context derived from the RPC `origin` flag, returned as a
|
|
2541
2850
|
* `GroupSystemPrompt` block — the PROVEN runtime-rendered seam for per-turn
|
|
2542
2851
|
* system-prompt guidance: dispatchInbound spreads `extraContext` into
|
|
@@ -2564,6 +2873,11 @@ function buildOriginEnvelopeContext(origin) {
|
|
|
2564
2873
|
if (origin === "voice") return { GroupSystemPrompt: `${VOICE_REPLY_SYSTEM_PROMPT}\n\n${CROSS_AGENT_TOOLS_SYSTEM_PROMPT}` };
|
|
2565
2874
|
return { GroupSystemPrompt: CROSS_AGENT_TOOLS_SYSTEM_PROMPT };
|
|
2566
2875
|
}
|
|
2876
|
+
/** Build the single live per-turn prompt block without competing object keys. */
|
|
2877
|
+
function buildTurnEnvelopeContext(origin, isA2A) {
|
|
2878
|
+
const originPrompt = String(buildOriginEnvelopeContext(origin).GroupSystemPrompt);
|
|
2879
|
+
return { GroupSystemPrompt: isA2A ? `${originPrompt}\n\n${A2A_REPLY_SYSTEM_PROMPT}` : originPrompt };
|
|
2880
|
+
}
|
|
2567
2881
|
async function handleAgentRequest(request, log) {
|
|
2568
2882
|
const runtime = pluginRuntime;
|
|
2569
2883
|
if (!runtime) {
|
|
@@ -2574,7 +2888,7 @@ async function handleAgentRequest(request, log) {
|
|
|
2574
2888
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
2575
2889
|
return;
|
|
2576
2890
|
}
|
|
2577
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId,
|
|
2891
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, attachments: rawAttachments, a2a, chatMessageId } = request.params;
|
|
2578
2892
|
const isA2A = !!a2a;
|
|
2579
2893
|
if (!message && !rawAttachments?.length) {
|
|
2580
2894
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
@@ -2732,15 +3046,15 @@ async function handleAgentRequest(request, log) {
|
|
|
2732
3046
|
return;
|
|
2733
3047
|
}
|
|
2734
3048
|
});
|
|
3049
|
+
let localProxyIdentityActive = false;
|
|
2735
3050
|
try {
|
|
2736
3051
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
2737
3052
|
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
body: JSON.stringify({ identityId })
|
|
2742
|
-
}).catch(() => {});
|
|
3053
|
+
const identitySynced = await syncLocalAiProxyIdentity(identityId);
|
|
3054
|
+
localProxyIdentityActive = identitySynced && identityId !== void 0;
|
|
3055
|
+
if (!identitySynced) log.debug("Local AI proxy identity attribution unavailable");
|
|
2743
3056
|
const channelMode = isA2A ? "a2a" : extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
3057
|
+
const inboundHookRouting = buildInboundHookRouting(identityProvider, channelMode, conversationId);
|
|
2744
3058
|
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : channelMode === "a2a" ? "Agent" : "Alfe";
|
|
2745
3059
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
2746
3060
|
const userLabel = displayName ?? userId ?? senderId;
|
|
@@ -2787,6 +3101,7 @@ async function handleAgentRequest(request, log) {
|
|
|
2787
3101
|
bodyForAgent,
|
|
2788
3102
|
messageId: request.id,
|
|
2789
3103
|
timestamp: Date.now(),
|
|
3104
|
+
...inboundHookRouting,
|
|
2790
3105
|
extraContext: {
|
|
2791
3106
|
...tenantId ? { TenantId: tenantId } : {},
|
|
2792
3107
|
...clientType ? { ClientType: clientType } : {},
|
|
@@ -2794,22 +3109,13 @@ async function handleAgentRequest(request, log) {
|
|
|
2794
3109
|
...displayName ? { SenderName: displayName } : {},
|
|
2795
3110
|
...identityId ? { IdentityId: identityId } : {},
|
|
2796
3111
|
...userId ? { UserId: userId } : {},
|
|
2797
|
-
...senderPermissions?.length ? { SenderPermissions: senderPermissions } : {},
|
|
2798
3112
|
ChannelMode: channelMode,
|
|
2799
|
-
...
|
|
3113
|
+
...buildTurnEnvelopeContext(origin, isA2A),
|
|
2800
3114
|
...isA2A ? {
|
|
2801
3115
|
CallerType: "agent",
|
|
2802
3116
|
CallerAgentId: a2a.sourceAgentId,
|
|
2803
3117
|
CallerAgentName: a2a.sourceAgentName,
|
|
2804
|
-
InteractionDepth: String(a2a.depth)
|
|
2805
|
-
A2ASystemPrompt: [
|
|
2806
|
-
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
2807
|
-
"Rules:",
|
|
2808
|
-
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
2809
|
-
"- When the discussion is complete, call the end_conversation() tool AND end your final message with the literal token [RESOLVED] (uppercase, in square brackets, as the last thing in the message).",
|
|
2810
|
-
"- Do NOT write the word \"resolved\" in prose unless you mean to end — only the exact token [RESOLVED] ends the thread.",
|
|
2811
|
-
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
2812
|
-
].join("\n")
|
|
3118
|
+
InteractionDepth: String(a2a.depth)
|
|
2813
3119
|
} : {}
|
|
2814
3120
|
},
|
|
2815
3121
|
deliver: async (payload) => {
|
|
@@ -2866,6 +3172,9 @@ async function handleAgentRequest(request, log) {
|
|
|
2866
3172
|
activeRun = null;
|
|
2867
3173
|
unsubscribe();
|
|
2868
3174
|
clearAllToolUpdates();
|
|
3175
|
+
if (localProxyIdentityActive) {
|
|
3176
|
+
if (!await syncLocalAiProxyIdentity(void 0)) log.debug("Local AI proxy identity attribution clear failed");
|
|
3177
|
+
}
|
|
2869
3178
|
try {
|
|
2870
3179
|
await flushTurnActivity();
|
|
2871
3180
|
} catch (err) {
|
|
@@ -3009,7 +3318,7 @@ const plugin = {
|
|
|
3009
3318
|
chatWsUrl: pluginConfig.chatWsUrl
|
|
3010
3319
|
});
|
|
3011
3320
|
if (chatWsUrl && apiKey) {
|
|
3012
|
-
log.info(
|
|
3321
|
+
log.info("Connecting to chat service relay");
|
|
3013
3322
|
chatClient = new _alfe_ai_chat.ChatServiceClient({
|
|
3014
3323
|
wsUrl: chatWsUrl,
|
|
3015
3324
|
apiKey,
|
|
@@ -3056,7 +3365,6 @@ const plugin = {
|
|
|
3056
3365
|
connectingPromise = thisConnect;
|
|
3057
3366
|
};
|
|
3058
3367
|
const stopChatService = async () => {
|
|
3059
|
-
globalThis.__alfeChatPluginActivated = false;
|
|
3060
3368
|
if (connectingPromise) {
|
|
3061
3369
|
await connectingPromise.catch((err) => {
|
|
3062
3370
|
log.debug(`Connection attempt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -3070,6 +3378,7 @@ const plugin = {
|
|
|
3070
3378
|
}
|
|
3071
3379
|
pluginRuntime = null;
|
|
3072
3380
|
dispatchInbound = null;
|
|
3381
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(CHAT_ACTIVATION_KEY);
|
|
3073
3382
|
log.info("Chat plugin deactivated");
|
|
3074
3383
|
};
|
|
3075
3384
|
const gw = globalThis;
|
|
@@ -3147,7 +3456,6 @@ const plugin = {
|
|
|
3147
3456
|
log.info("Chat plugin registered");
|
|
3148
3457
|
},
|
|
3149
3458
|
async deactivate(api) {
|
|
3150
|
-
globalThis.__alfeChatPluginActivated = false;
|
|
3151
3459
|
const log = api.logger;
|
|
3152
3460
|
log.info("Chat plugin deactivating...");
|
|
3153
3461
|
if (connectingPromise) {
|
|
@@ -3163,16 +3471,29 @@ const plugin = {
|
|
|
3163
3471
|
}
|
|
3164
3472
|
pluginRuntime = null;
|
|
3165
3473
|
dispatchInbound = null;
|
|
3474
|
+
(0, _alfe_ai_openclaw_plugin_kit.resetActivation)(CHAT_ACTIVATION_KEY);
|
|
3166
3475
|
log.info("Chat plugin deactivated");
|
|
3167
3476
|
}
|
|
3168
3477
|
};
|
|
3169
3478
|
//#endregion
|
|
3479
|
+
Object.defineProperty(exports, "A2A_REPLY_SYSTEM_PROMPT", {
|
|
3480
|
+
enumerable: true,
|
|
3481
|
+
get: function() {
|
|
3482
|
+
return A2A_REPLY_SYSTEM_PROMPT;
|
|
3483
|
+
}
|
|
3484
|
+
});
|
|
3170
3485
|
Object.defineProperty(exports, "CROSS_AGENT_TOOLS_SYSTEM_PROMPT", {
|
|
3171
3486
|
enumerable: true,
|
|
3172
3487
|
get: function() {
|
|
3173
3488
|
return CROSS_AGENT_TOOLS_SYSTEM_PROMPT;
|
|
3174
3489
|
}
|
|
3175
3490
|
});
|
|
3491
|
+
Object.defineProperty(exports, "MAX_INBOUND_ATTACHMENT_SIZE", {
|
|
3492
|
+
enumerable: true,
|
|
3493
|
+
get: function() {
|
|
3494
|
+
return MAX_INBOUND_ATTACHMENT_SIZE;
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3176
3497
|
Object.defineProperty(exports, "VOICE_REPLY_SYSTEM_PROMPT", {
|
|
3177
3498
|
enumerable: true,
|
|
3178
3499
|
get: function() {
|
|
@@ -3209,6 +3530,12 @@ Object.defineProperty(exports, "buildA2ACompletePayload", {
|
|
|
3209
3530
|
return buildA2ACompletePayload;
|
|
3210
3531
|
}
|
|
3211
3532
|
});
|
|
3533
|
+
Object.defineProperty(exports, "buildAttachmentLocalFilename", {
|
|
3534
|
+
enumerable: true,
|
|
3535
|
+
get: function() {
|
|
3536
|
+
return buildAttachmentLocalFilename;
|
|
3537
|
+
}
|
|
3538
|
+
});
|
|
3212
3539
|
Object.defineProperty(exports, "buildOriginEnvelopeContext", {
|
|
3213
3540
|
enumerable: true,
|
|
3214
3541
|
get: function() {
|
|
@@ -3221,6 +3548,12 @@ Object.defineProperty(exports, "buildToolActivity", {
|
|
|
3221
3548
|
return buildToolActivity;
|
|
3222
3549
|
}
|
|
3223
3550
|
});
|
|
3551
|
+
Object.defineProperty(exports, "buildTurnEnvelopeContext", {
|
|
3552
|
+
enumerable: true,
|
|
3553
|
+
get: function() {
|
|
3554
|
+
return buildTurnEnvelopeContext;
|
|
3555
|
+
}
|
|
3556
|
+
});
|
|
3224
3557
|
Object.defineProperty(exports, "computeOpenClawSdkAnchors", {
|
|
3225
3558
|
enumerable: true,
|
|
3226
3559
|
get: function() {
|
|
@@ -3251,6 +3584,12 @@ Object.defineProperty(exports, "plugin", {
|
|
|
3251
3584
|
return plugin;
|
|
3252
3585
|
}
|
|
3253
3586
|
});
|
|
3587
|
+
Object.defineProperty(exports, "readAttachmentBodyBounded", {
|
|
3588
|
+
enumerable: true,
|
|
3589
|
+
get: function() {
|
|
3590
|
+
return readAttachmentBodyBounded;
|
|
3591
|
+
}
|
|
3592
|
+
});
|
|
3254
3593
|
Object.defineProperty(exports, "resolveAbortTargetKeys", {
|
|
3255
3594
|
enumerable: true,
|
|
3256
3595
|
get: function() {
|