@alfe.ai/openclaw-chat 0.7.0 → 0.8.1
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/dist/plugin.d.cts +29 -0
- package/dist/plugin.d.ts +29 -0
- package/dist/plugin2.cjs +160 -23
- package/dist/plugin2.js +160 -23
- package/package.json +2 -2
package/dist/plugin.d.cts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* DEVELOPING.md → "Publishing public @alfe.ai/* packages"). Keep in sync with
|
|
22
22
|
* the transport union.
|
|
23
23
|
*/
|
|
24
|
+
interface StoredSelectOption {
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
24
28
|
type StoredMessageComponent = {
|
|
25
29
|
type: 'link_button';
|
|
26
30
|
id: string;
|
|
@@ -34,6 +38,31 @@ type StoredMessageComponent = {
|
|
|
34
38
|
label: string;
|
|
35
39
|
value: string;
|
|
36
40
|
style?: 'primary' | 'secondary' | 'danger';
|
|
41
|
+
} | {
|
|
42
|
+
type: 'select';
|
|
43
|
+
id: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
options: StoredSelectOption[];
|
|
47
|
+
} | {
|
|
48
|
+
type: 'multi_select';
|
|
49
|
+
id: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
options: StoredSelectOption[];
|
|
52
|
+
submitLabel?: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'confirm';
|
|
55
|
+
id: string;
|
|
56
|
+
label?: string;
|
|
57
|
+
confirmLabel: string;
|
|
58
|
+
confirmValue: string;
|
|
59
|
+
cancelLabel?: string;
|
|
60
|
+
cancelValue?: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'copy_button';
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
value: string;
|
|
37
66
|
};
|
|
38
67
|
interface ChatMessage {
|
|
39
68
|
role: 'user' | 'assistant';
|
package/dist/plugin.d.ts
CHANGED
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* DEVELOPING.md → "Publishing public @alfe.ai/* packages"). Keep in sync with
|
|
22
22
|
* the transport union.
|
|
23
23
|
*/
|
|
24
|
+
interface StoredSelectOption {
|
|
25
|
+
label: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}
|
|
24
28
|
type StoredMessageComponent = {
|
|
25
29
|
type: 'link_button';
|
|
26
30
|
id: string;
|
|
@@ -34,6 +38,31 @@ type StoredMessageComponent = {
|
|
|
34
38
|
label: string;
|
|
35
39
|
value: string;
|
|
36
40
|
style?: 'primary' | 'secondary' | 'danger';
|
|
41
|
+
} | {
|
|
42
|
+
type: 'select';
|
|
43
|
+
id: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
options: StoredSelectOption[];
|
|
47
|
+
} | {
|
|
48
|
+
type: 'multi_select';
|
|
49
|
+
id: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
options: StoredSelectOption[];
|
|
52
|
+
submitLabel?: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'confirm';
|
|
55
|
+
id: string;
|
|
56
|
+
label?: string;
|
|
57
|
+
confirmLabel: string;
|
|
58
|
+
confirmValue: string;
|
|
59
|
+
cancelLabel?: string;
|
|
60
|
+
cancelValue?: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'copy_button';
|
|
63
|
+
id: string;
|
|
64
|
+
label: string;
|
|
65
|
+
value: string;
|
|
37
66
|
};
|
|
38
67
|
interface ChatMessage {
|
|
39
68
|
role: 'user' | 'assistant';
|
package/dist/plugin2.cjs
CHANGED
|
@@ -3,8 +3,8 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_os = require("node:os");
|
|
5
5
|
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
6
|
-
let node_crypto = require("node:crypto");
|
|
7
6
|
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
8
8
|
let _alfe_ai_config = require("@alfe.ai/config");
|
|
9
9
|
let node_fs = require("node:fs");
|
|
10
10
|
//#region src/outbound-media.ts
|
|
@@ -376,24 +376,58 @@ function isSafeComponentUrl(raw) {
|
|
|
376
376
|
}
|
|
377
377
|
const MAX_COMPONENTS_PER_MESSAGE = 10;
|
|
378
378
|
const MAX_COMPONENT_LABEL_CHARS = 120;
|
|
379
|
+
const MAX_COMPONENT_VALUE_CHARS = 400;
|
|
380
|
+
const MAX_OPTIONS_PER_COMPONENT = 25;
|
|
381
|
+
/** Cap a maybe-string; returns '' for non-strings so callers can `if (!x)`. */
|
|
382
|
+
function capStr(v, max) {
|
|
383
|
+
return typeof v === "string" ? v.slice(0, max) : "";
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Validate + cap the {label,value} option list of a select / multi_select.
|
|
387
|
+
* Drops malformed / empty entries; caps the count. Empty result → the caller
|
|
388
|
+
* drops the whole component (a chooser with no choices is useless).
|
|
389
|
+
*/
|
|
390
|
+
function sanitizeOptions(raw) {
|
|
391
|
+
if (!Array.isArray(raw)) return [];
|
|
392
|
+
const out = [];
|
|
393
|
+
for (const item of raw) {
|
|
394
|
+
if (out.length >= MAX_OPTIONS_PER_COMPONENT) break;
|
|
395
|
+
if (!item || typeof item !== "object") continue;
|
|
396
|
+
const o = item;
|
|
397
|
+
const label = capStr(o.label, MAX_COMPONENT_LABEL_CHARS);
|
|
398
|
+
const value = capStr(o.value, MAX_COMPONENT_VALUE_CHARS);
|
|
399
|
+
if (!label || !value) continue;
|
|
400
|
+
out.push({
|
|
401
|
+
label,
|
|
402
|
+
value
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
379
407
|
/**
|
|
380
408
|
* Validate + normalize agent-supplied components down to the persisted wire
|
|
381
|
-
* shape. Drops anything malformed or unsafe (unsafe URL, missing
|
|
382
|
-
* over-cap). Returns at most
|
|
383
|
-
*
|
|
409
|
+
* shape. Drops anything malformed or unsafe (unsafe URL, missing required
|
|
410
|
+
* fields, no valid options, over-cap). Returns at most
|
|
411
|
+
* MAX_COMPONENTS_PER_MESSAGE. Handles the full core set: `link_button`,
|
|
412
|
+
* `quick_reply`, `select`, `multi_select`, `confirm`, `copy_button`. Only
|
|
413
|
+
* `link_button` is URL-safety-gated — the rest carry no URL, so their values
|
|
414
|
+
* are plain text (capped, never navigated).
|
|
384
415
|
*/
|
|
385
416
|
function sanitizeComponents(raw) {
|
|
386
417
|
if (!Array.isArray(raw)) return [];
|
|
387
418
|
const out = [];
|
|
419
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
388
420
|
for (const item of raw) {
|
|
389
421
|
if (out.length >= MAX_COMPONENTS_PER_MESSAGE) break;
|
|
390
422
|
if (!item || typeof item !== "object") continue;
|
|
391
423
|
const c = item;
|
|
392
|
-
const label =
|
|
393
|
-
|
|
394
|
-
|
|
424
|
+
const label = capStr(c.label, MAX_COMPONENT_LABEL_CHARS);
|
|
425
|
+
let id = typeof c.id === "string" && c.id ? c.id : (0, node_crypto.randomUUID)();
|
|
426
|
+
if (usedIds.has(id)) id = (0, node_crypto.randomUUID)();
|
|
427
|
+
usedIds.add(id);
|
|
395
428
|
const style = c.style === "primary" || c.style === "secondary" || c.style === "danger" ? c.style : void 0;
|
|
396
429
|
if (c.type === "link_button") {
|
|
430
|
+
if (!label) continue;
|
|
397
431
|
if (typeof c.url !== "string" || !isSafeComponentUrl(c.url)) continue;
|
|
398
432
|
const target = c.target === "same-tab" || c.target === "new-tab" || c.target === "popup" ? c.target : void 0;
|
|
399
433
|
out.push({
|
|
@@ -405,14 +439,63 @@ function sanitizeComponents(raw) {
|
|
|
405
439
|
...style ? { style } : {}
|
|
406
440
|
});
|
|
407
441
|
} else if (c.type === "quick_reply") {
|
|
408
|
-
if (
|
|
442
|
+
if (!label) continue;
|
|
443
|
+
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
|
|
444
|
+
if (!value) continue;
|
|
409
445
|
out.push({
|
|
410
446
|
type: "quick_reply",
|
|
411
447
|
id,
|
|
412
448
|
label,
|
|
413
|
-
value
|
|
449
|
+
value,
|
|
414
450
|
...style ? { style } : {}
|
|
415
451
|
});
|
|
452
|
+
} else if (c.type === "select") {
|
|
453
|
+
const options = sanitizeOptions(c.options);
|
|
454
|
+
if (options.length === 0) continue;
|
|
455
|
+
const placeholder = capStr(c.placeholder, MAX_COMPONENT_LABEL_CHARS);
|
|
456
|
+
out.push({
|
|
457
|
+
type: "select",
|
|
458
|
+
id,
|
|
459
|
+
...label ? { label } : {},
|
|
460
|
+
...placeholder ? { placeholder } : {},
|
|
461
|
+
options
|
|
462
|
+
});
|
|
463
|
+
} else if (c.type === "multi_select") {
|
|
464
|
+
const options = sanitizeOptions(c.options);
|
|
465
|
+
if (options.length === 0) continue;
|
|
466
|
+
const submitLabel = capStr(c.submitLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
467
|
+
out.push({
|
|
468
|
+
type: "multi_select",
|
|
469
|
+
id,
|
|
470
|
+
...label ? { label } : {},
|
|
471
|
+
options,
|
|
472
|
+
...submitLabel ? { submitLabel } : {}
|
|
473
|
+
});
|
|
474
|
+
} else if (c.type === "confirm") {
|
|
475
|
+
const confirmLabel = capStr(c.confirmLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
476
|
+
const confirmValue = capStr(c.confirmValue, MAX_COMPONENT_VALUE_CHARS);
|
|
477
|
+
if (!confirmLabel || !confirmValue) continue;
|
|
478
|
+
const cancelLabel = capStr(c.cancelLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
479
|
+
const cancelValue = capStr(c.cancelValue, MAX_COMPONENT_VALUE_CHARS);
|
|
480
|
+
out.push({
|
|
481
|
+
type: "confirm",
|
|
482
|
+
id,
|
|
483
|
+
...label ? { label } : {},
|
|
484
|
+
confirmLabel,
|
|
485
|
+
confirmValue,
|
|
486
|
+
...cancelLabel ? { cancelLabel } : {},
|
|
487
|
+
...cancelValue ? { cancelValue } : {}
|
|
488
|
+
});
|
|
489
|
+
} else if (c.type === "copy_button") {
|
|
490
|
+
if (!label) continue;
|
|
491
|
+
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
|
|
492
|
+
if (!value) continue;
|
|
493
|
+
out.push({
|
|
494
|
+
type: "copy_button",
|
|
495
|
+
id,
|
|
496
|
+
label,
|
|
497
|
+
value
|
|
498
|
+
});
|
|
416
499
|
}
|
|
417
500
|
}
|
|
418
501
|
return out;
|
|
@@ -1496,7 +1579,7 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1496
1579
|
];
|
|
1497
1580
|
if (present) tools.push(defineTool({
|
|
1498
1581
|
name: "chat_present_components",
|
|
1499
|
-
description: "Attach interactive UI components
|
|
1582
|
+
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\" } ] }",
|
|
1500
1583
|
parameters: {
|
|
1501
1584
|
type: "object",
|
|
1502
1585
|
properties: {
|
|
@@ -1506,26 +1589,37 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1506
1589
|
},
|
|
1507
1590
|
text: {
|
|
1508
1591
|
type: "string",
|
|
1509
|
-
description: "Optional message text rendered above the components."
|
|
1592
|
+
description: "Optional message text rendered above the components. Keep the key info here too — it is the fallback on clients that cannot render components."
|
|
1510
1593
|
},
|
|
1511
1594
|
components: {
|
|
1512
1595
|
type: "array",
|
|
1513
|
-
description: "Interactive components to attach (at least one).",
|
|
1596
|
+
description: "Interactive components to attach (at least one, at most 10).",
|
|
1514
1597
|
items: {
|
|
1515
1598
|
type: "object",
|
|
1516
1599
|
properties: {
|
|
1517
1600
|
type: {
|
|
1518
1601
|
type: "string",
|
|
1519
|
-
enum: [
|
|
1520
|
-
|
|
1602
|
+
enum: [
|
|
1603
|
+
"link_button",
|
|
1604
|
+
"quick_reply",
|
|
1605
|
+
"select",
|
|
1606
|
+
"multi_select",
|
|
1607
|
+
"confirm",
|
|
1608
|
+
"copy_button"
|
|
1609
|
+
],
|
|
1610
|
+
description: "Component kind — see the tool description for when to use each."
|
|
1611
|
+
},
|
|
1612
|
+
id: {
|
|
1613
|
+
type: "string",
|
|
1614
|
+
description: "Stable id (optional — auto-generated when omitted)."
|
|
1521
1615
|
},
|
|
1522
1616
|
label: {
|
|
1523
1617
|
type: "string",
|
|
1524
|
-
description: "Button text."
|
|
1618
|
+
description: "Button/control text. Required for link_button, quick_reply, copy_button; optional heading for select / multi_select."
|
|
1525
1619
|
},
|
|
1526
1620
|
url: {
|
|
1527
1621
|
type: "string",
|
|
1528
|
-
description: "https URL on an Alfe-owned host to open when clicked."
|
|
1622
|
+
description: "link_button only: https URL on an Alfe-owned host to open when clicked."
|
|
1529
1623
|
},
|
|
1530
1624
|
target: {
|
|
1531
1625
|
type: "string",
|
|
@@ -1534,7 +1628,53 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1534
1628
|
"new-tab",
|
|
1535
1629
|
"popup"
|
|
1536
1630
|
],
|
|
1537
|
-
description: "
|
|
1631
|
+
description: "link_button only: how the URL opens (default same-tab)."
|
|
1632
|
+
},
|
|
1633
|
+
value: {
|
|
1634
|
+
type: "string",
|
|
1635
|
+
description: "quick_reply: the text submitted as the user's next message when tapped. copy_button: the value copied to the clipboard."
|
|
1636
|
+
},
|
|
1637
|
+
placeholder: {
|
|
1638
|
+
type: "string",
|
|
1639
|
+
description: "select only: the empty-state prompt shown before a choice is made."
|
|
1640
|
+
},
|
|
1641
|
+
options: {
|
|
1642
|
+
type: "array",
|
|
1643
|
+
description: "select / multi_select only: the choices (max 25). Each is { label, value }; `value` is what gets submitted (multi_select joins checked values with \", \").",
|
|
1644
|
+
items: {
|
|
1645
|
+
type: "object",
|
|
1646
|
+
properties: {
|
|
1647
|
+
label: {
|
|
1648
|
+
type: "string",
|
|
1649
|
+
description: "Option text shown to the user."
|
|
1650
|
+
},
|
|
1651
|
+
value: {
|
|
1652
|
+
type: "string",
|
|
1653
|
+
description: "Value submitted when chosen."
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
required: ["label", "value"]
|
|
1657
|
+
}
|
|
1658
|
+
},
|
|
1659
|
+
submitLabel: {
|
|
1660
|
+
type: "string",
|
|
1661
|
+
description: "multi_select only: submit button caption (default \"Submit\")."
|
|
1662
|
+
},
|
|
1663
|
+
confirmLabel: {
|
|
1664
|
+
type: "string",
|
|
1665
|
+
description: "confirm only: primary button text (e.g. \"Approve\")."
|
|
1666
|
+
},
|
|
1667
|
+
confirmValue: {
|
|
1668
|
+
type: "string",
|
|
1669
|
+
description: "confirm only: value submitted when the primary button is tapped."
|
|
1670
|
+
},
|
|
1671
|
+
cancelLabel: {
|
|
1672
|
+
type: "string",
|
|
1673
|
+
description: "confirm only: secondary button text (omit to show only the primary)."
|
|
1674
|
+
},
|
|
1675
|
+
cancelValue: {
|
|
1676
|
+
type: "string",
|
|
1677
|
+
description: "confirm only: value submitted when the secondary button is tapped."
|
|
1538
1678
|
},
|
|
1539
1679
|
style: {
|
|
1540
1680
|
type: "string",
|
|
@@ -1543,14 +1683,10 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1543
1683
|
"secondary",
|
|
1544
1684
|
"danger"
|
|
1545
1685
|
],
|
|
1546
|
-
description: "
|
|
1686
|
+
description: "link_button / quick_reply only: visual style hint."
|
|
1547
1687
|
}
|
|
1548
1688
|
},
|
|
1549
|
-
required: [
|
|
1550
|
-
"type",
|
|
1551
|
-
"label",
|
|
1552
|
-
"url"
|
|
1553
|
-
]
|
|
1689
|
+
required: ["type"]
|
|
1554
1690
|
}
|
|
1555
1691
|
}
|
|
1556
1692
|
},
|
|
@@ -2516,6 +2652,7 @@ const plugin = {
|
|
|
2516
2652
|
api.registerChannel(alfeChannel);
|
|
2517
2653
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
2518
2654
|
if (typeof api.registerTool === "function") {
|
|
2655
|
+
(0, _alfe_ai_agent_api_client.installToolErrorCapture)(api, { plugin: "openclaw-chat" });
|
|
2519
2656
|
const present = async (args) => {
|
|
2520
2657
|
const clean = sanitizeComponents(args.components);
|
|
2521
2658
|
if (clean.length === 0) throw new Error("No valid components: link_button requires a `label` and an https `url` on an Alfe-owned host");
|
package/dist/plugin2.js
CHANGED
|
@@ -3,8 +3,8 @@ import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs
|
|
|
3
3
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
6
|
+
import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
|
|
6
7
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { AgentApiClient } from "@alfe.ai/agent-api-client";
|
|
8
8
|
import { resolveConfig } from "@alfe.ai/config";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
//#region src/outbound-media.ts
|
|
@@ -376,24 +376,58 @@ function isSafeComponentUrl(raw) {
|
|
|
376
376
|
}
|
|
377
377
|
const MAX_COMPONENTS_PER_MESSAGE = 10;
|
|
378
378
|
const MAX_COMPONENT_LABEL_CHARS = 120;
|
|
379
|
+
const MAX_COMPONENT_VALUE_CHARS = 400;
|
|
380
|
+
const MAX_OPTIONS_PER_COMPONENT = 25;
|
|
381
|
+
/** Cap a maybe-string; returns '' for non-strings so callers can `if (!x)`. */
|
|
382
|
+
function capStr(v, max) {
|
|
383
|
+
return typeof v === "string" ? v.slice(0, max) : "";
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Validate + cap the {label,value} option list of a select / multi_select.
|
|
387
|
+
* Drops malformed / empty entries; caps the count. Empty result → the caller
|
|
388
|
+
* drops the whole component (a chooser with no choices is useless).
|
|
389
|
+
*/
|
|
390
|
+
function sanitizeOptions(raw) {
|
|
391
|
+
if (!Array.isArray(raw)) return [];
|
|
392
|
+
const out = [];
|
|
393
|
+
for (const item of raw) {
|
|
394
|
+
if (out.length >= MAX_OPTIONS_PER_COMPONENT) break;
|
|
395
|
+
if (!item || typeof item !== "object") continue;
|
|
396
|
+
const o = item;
|
|
397
|
+
const label = capStr(o.label, MAX_COMPONENT_LABEL_CHARS);
|
|
398
|
+
const value = capStr(o.value, MAX_COMPONENT_VALUE_CHARS);
|
|
399
|
+
if (!label || !value) continue;
|
|
400
|
+
out.push({
|
|
401
|
+
label,
|
|
402
|
+
value
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
379
407
|
/**
|
|
380
408
|
* Validate + normalize agent-supplied components down to the persisted wire
|
|
381
|
-
* shape. Drops anything malformed or unsafe (unsafe URL, missing
|
|
382
|
-
* over-cap). Returns at most
|
|
383
|
-
*
|
|
409
|
+
* shape. Drops anything malformed or unsafe (unsafe URL, missing required
|
|
410
|
+
* fields, no valid options, over-cap). Returns at most
|
|
411
|
+
* MAX_COMPONENTS_PER_MESSAGE. Handles the full core set: `link_button`,
|
|
412
|
+
* `quick_reply`, `select`, `multi_select`, `confirm`, `copy_button`. Only
|
|
413
|
+
* `link_button` is URL-safety-gated — the rest carry no URL, so their values
|
|
414
|
+
* are plain text (capped, never navigated).
|
|
384
415
|
*/
|
|
385
416
|
function sanitizeComponents(raw) {
|
|
386
417
|
if (!Array.isArray(raw)) return [];
|
|
387
418
|
const out = [];
|
|
419
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
388
420
|
for (const item of raw) {
|
|
389
421
|
if (out.length >= MAX_COMPONENTS_PER_MESSAGE) break;
|
|
390
422
|
if (!item || typeof item !== "object") continue;
|
|
391
423
|
const c = item;
|
|
392
|
-
const label =
|
|
393
|
-
|
|
394
|
-
|
|
424
|
+
const label = capStr(c.label, MAX_COMPONENT_LABEL_CHARS);
|
|
425
|
+
let id = typeof c.id === "string" && c.id ? c.id : randomUUID();
|
|
426
|
+
if (usedIds.has(id)) id = randomUUID();
|
|
427
|
+
usedIds.add(id);
|
|
395
428
|
const style = c.style === "primary" || c.style === "secondary" || c.style === "danger" ? c.style : void 0;
|
|
396
429
|
if (c.type === "link_button") {
|
|
430
|
+
if (!label) continue;
|
|
397
431
|
if (typeof c.url !== "string" || !isSafeComponentUrl(c.url)) continue;
|
|
398
432
|
const target = c.target === "same-tab" || c.target === "new-tab" || c.target === "popup" ? c.target : void 0;
|
|
399
433
|
out.push({
|
|
@@ -405,14 +439,63 @@ function sanitizeComponents(raw) {
|
|
|
405
439
|
...style ? { style } : {}
|
|
406
440
|
});
|
|
407
441
|
} else if (c.type === "quick_reply") {
|
|
408
|
-
if (
|
|
442
|
+
if (!label) continue;
|
|
443
|
+
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
|
|
444
|
+
if (!value) continue;
|
|
409
445
|
out.push({
|
|
410
446
|
type: "quick_reply",
|
|
411
447
|
id,
|
|
412
448
|
label,
|
|
413
|
-
value
|
|
449
|
+
value,
|
|
414
450
|
...style ? { style } : {}
|
|
415
451
|
});
|
|
452
|
+
} else if (c.type === "select") {
|
|
453
|
+
const options = sanitizeOptions(c.options);
|
|
454
|
+
if (options.length === 0) continue;
|
|
455
|
+
const placeholder = capStr(c.placeholder, MAX_COMPONENT_LABEL_CHARS);
|
|
456
|
+
out.push({
|
|
457
|
+
type: "select",
|
|
458
|
+
id,
|
|
459
|
+
...label ? { label } : {},
|
|
460
|
+
...placeholder ? { placeholder } : {},
|
|
461
|
+
options
|
|
462
|
+
});
|
|
463
|
+
} else if (c.type === "multi_select") {
|
|
464
|
+
const options = sanitizeOptions(c.options);
|
|
465
|
+
if (options.length === 0) continue;
|
|
466
|
+
const submitLabel = capStr(c.submitLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
467
|
+
out.push({
|
|
468
|
+
type: "multi_select",
|
|
469
|
+
id,
|
|
470
|
+
...label ? { label } : {},
|
|
471
|
+
options,
|
|
472
|
+
...submitLabel ? { submitLabel } : {}
|
|
473
|
+
});
|
|
474
|
+
} else if (c.type === "confirm") {
|
|
475
|
+
const confirmLabel = capStr(c.confirmLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
476
|
+
const confirmValue = capStr(c.confirmValue, MAX_COMPONENT_VALUE_CHARS);
|
|
477
|
+
if (!confirmLabel || !confirmValue) continue;
|
|
478
|
+
const cancelLabel = capStr(c.cancelLabel, MAX_COMPONENT_LABEL_CHARS);
|
|
479
|
+
const cancelValue = capStr(c.cancelValue, MAX_COMPONENT_VALUE_CHARS);
|
|
480
|
+
out.push({
|
|
481
|
+
type: "confirm",
|
|
482
|
+
id,
|
|
483
|
+
...label ? { label } : {},
|
|
484
|
+
confirmLabel,
|
|
485
|
+
confirmValue,
|
|
486
|
+
...cancelLabel ? { cancelLabel } : {},
|
|
487
|
+
...cancelValue ? { cancelValue } : {}
|
|
488
|
+
});
|
|
489
|
+
} else if (c.type === "copy_button") {
|
|
490
|
+
if (!label) continue;
|
|
491
|
+
const value = capStr(c.value, MAX_COMPONENT_VALUE_CHARS);
|
|
492
|
+
if (!value) continue;
|
|
493
|
+
out.push({
|
|
494
|
+
type: "copy_button",
|
|
495
|
+
id,
|
|
496
|
+
label,
|
|
497
|
+
value
|
|
498
|
+
});
|
|
416
499
|
}
|
|
417
500
|
}
|
|
418
501
|
return out;
|
|
@@ -1496,7 +1579,7 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1496
1579
|
];
|
|
1497
1580
|
if (present) tools.push(defineTool({
|
|
1498
1581
|
name: "chat_present_components",
|
|
1499
|
-
description: "Attach interactive UI components
|
|
1582
|
+
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\" } ] }",
|
|
1500
1583
|
parameters: {
|
|
1501
1584
|
type: "object",
|
|
1502
1585
|
properties: {
|
|
@@ -1506,26 +1589,37 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1506
1589
|
},
|
|
1507
1590
|
text: {
|
|
1508
1591
|
type: "string",
|
|
1509
|
-
description: "Optional message text rendered above the components."
|
|
1592
|
+
description: "Optional message text rendered above the components. Keep the key info here too — it is the fallback on clients that cannot render components."
|
|
1510
1593
|
},
|
|
1511
1594
|
components: {
|
|
1512
1595
|
type: "array",
|
|
1513
|
-
description: "Interactive components to attach (at least one).",
|
|
1596
|
+
description: "Interactive components to attach (at least one, at most 10).",
|
|
1514
1597
|
items: {
|
|
1515
1598
|
type: "object",
|
|
1516
1599
|
properties: {
|
|
1517
1600
|
type: {
|
|
1518
1601
|
type: "string",
|
|
1519
|
-
enum: [
|
|
1520
|
-
|
|
1602
|
+
enum: [
|
|
1603
|
+
"link_button",
|
|
1604
|
+
"quick_reply",
|
|
1605
|
+
"select",
|
|
1606
|
+
"multi_select",
|
|
1607
|
+
"confirm",
|
|
1608
|
+
"copy_button"
|
|
1609
|
+
],
|
|
1610
|
+
description: "Component kind — see the tool description for when to use each."
|
|
1611
|
+
},
|
|
1612
|
+
id: {
|
|
1613
|
+
type: "string",
|
|
1614
|
+
description: "Stable id (optional — auto-generated when omitted)."
|
|
1521
1615
|
},
|
|
1522
1616
|
label: {
|
|
1523
1617
|
type: "string",
|
|
1524
|
-
description: "Button text."
|
|
1618
|
+
description: "Button/control text. Required for link_button, quick_reply, copy_button; optional heading for select / multi_select."
|
|
1525
1619
|
},
|
|
1526
1620
|
url: {
|
|
1527
1621
|
type: "string",
|
|
1528
|
-
description: "https URL on an Alfe-owned host to open when clicked."
|
|
1622
|
+
description: "link_button only: https URL on an Alfe-owned host to open when clicked."
|
|
1529
1623
|
},
|
|
1530
1624
|
target: {
|
|
1531
1625
|
type: "string",
|
|
@@ -1534,7 +1628,53 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1534
1628
|
"new-tab",
|
|
1535
1629
|
"popup"
|
|
1536
1630
|
],
|
|
1537
|
-
description: "
|
|
1631
|
+
description: "link_button only: how the URL opens (default same-tab)."
|
|
1632
|
+
},
|
|
1633
|
+
value: {
|
|
1634
|
+
type: "string",
|
|
1635
|
+
description: "quick_reply: the text submitted as the user's next message when tapped. copy_button: the value copied to the clipboard."
|
|
1636
|
+
},
|
|
1637
|
+
placeholder: {
|
|
1638
|
+
type: "string",
|
|
1639
|
+
description: "select only: the empty-state prompt shown before a choice is made."
|
|
1640
|
+
},
|
|
1641
|
+
options: {
|
|
1642
|
+
type: "array",
|
|
1643
|
+
description: "select / multi_select only: the choices (max 25). Each is { label, value }; `value` is what gets submitted (multi_select joins checked values with \", \").",
|
|
1644
|
+
items: {
|
|
1645
|
+
type: "object",
|
|
1646
|
+
properties: {
|
|
1647
|
+
label: {
|
|
1648
|
+
type: "string",
|
|
1649
|
+
description: "Option text shown to the user."
|
|
1650
|
+
},
|
|
1651
|
+
value: {
|
|
1652
|
+
type: "string",
|
|
1653
|
+
description: "Value submitted when chosen."
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
required: ["label", "value"]
|
|
1657
|
+
}
|
|
1658
|
+
},
|
|
1659
|
+
submitLabel: {
|
|
1660
|
+
type: "string",
|
|
1661
|
+
description: "multi_select only: submit button caption (default \"Submit\")."
|
|
1662
|
+
},
|
|
1663
|
+
confirmLabel: {
|
|
1664
|
+
type: "string",
|
|
1665
|
+
description: "confirm only: primary button text (e.g. \"Approve\")."
|
|
1666
|
+
},
|
|
1667
|
+
confirmValue: {
|
|
1668
|
+
type: "string",
|
|
1669
|
+
description: "confirm only: value submitted when the primary button is tapped."
|
|
1670
|
+
},
|
|
1671
|
+
cancelLabel: {
|
|
1672
|
+
type: "string",
|
|
1673
|
+
description: "confirm only: secondary button text (omit to show only the primary)."
|
|
1674
|
+
},
|
|
1675
|
+
cancelValue: {
|
|
1676
|
+
type: "string",
|
|
1677
|
+
description: "confirm only: value submitted when the secondary button is tapped."
|
|
1538
1678
|
},
|
|
1539
1679
|
style: {
|
|
1540
1680
|
type: "string",
|
|
@@ -1543,14 +1683,10 @@ function buildA2ATools(getChatClient, log, present) {
|
|
|
1543
1683
|
"secondary",
|
|
1544
1684
|
"danger"
|
|
1545
1685
|
],
|
|
1546
|
-
description: "
|
|
1686
|
+
description: "link_button / quick_reply only: visual style hint."
|
|
1547
1687
|
}
|
|
1548
1688
|
},
|
|
1549
|
-
required: [
|
|
1550
|
-
"type",
|
|
1551
|
-
"label",
|
|
1552
|
-
"url"
|
|
1553
|
-
]
|
|
1689
|
+
required: ["type"]
|
|
1554
1690
|
}
|
|
1555
1691
|
}
|
|
1556
1692
|
},
|
|
@@ -2516,6 +2652,7 @@ const plugin = {
|
|
|
2516
2652
|
api.registerChannel(alfeChannel);
|
|
2517
2653
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
2518
2654
|
if (typeof api.registerTool === "function") {
|
|
2655
|
+
installToolErrorCapture(api, { plugin: "openclaw-chat" });
|
|
2519
2656
|
const present = async (args) => {
|
|
2520
2657
|
const clean = sanitizeComponents(args.components);
|
|
2521
2658
|
if (clean.length === 0) throw new Error("No valid components: link_button requires a `label` and an https `url` on an Alfe-owned host");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"openclaw.plugin.json"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@alfe.ai/agent-api-client": "^0.
|
|
30
|
+
"@alfe.ai/agent-api-client": "^0.8.0",
|
|
31
31
|
"@alfe.ai/chat": "^0.0.14",
|
|
32
32
|
"@alfe.ai/config": "^0.3.0"
|
|
33
33
|
},
|