@clawling/clawchat-plugin-openclaw 2026.10.7-1 → 2026.10.7-2
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/src/framework-error-copy.js +172 -0
- package/dist/src/liveware-sample.js +106 -19
- package/dist/src/permission-result.js +116 -27
- package/dist/src/profile-prompt.js +17 -2
- package/dist/src/reply-dispatcher.js +12 -2
- package/dist/src/runtime.js +116 -2
- package/dist/src/skill-update.js +1 -1
- package/dist/src/tools-schema.js +1 -1
- package/dist/src/tools.js +90 -8
- package/package.json +3 -8
- package/skills/clawchat-core/SKILL.md +12 -3
- package/skills/clawchat-orchestration/SKILL.md +2 -1
- package/skills/clawchat-set-greeting/SKILL.md +5 -4
- package/skills/manifest.json +25 -25
- package/src/framework-error-copy.ts +185 -0
- package/src/liveware-sample.ts +106 -19
- package/src/permission-result.ts +134 -30
- package/src/profile-prompt.ts +21 -2
- package/src/reply-dispatcher.ts +24 -1
- package/src/runtime.ts +119 -3
- package/src/skill-update.ts +1 -1
- package/src/tools-schema.ts +1 -1
- package/src/tools.ts +99 -8
package/src/profile-prompt.ts
CHANGED
|
@@ -331,10 +331,24 @@ function renderGroupMessageMetadata(turn: ClawChatTurnPrompt, groupMetadata?: Cl
|
|
|
331
331
|
return lines.join("\n");
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
function
|
|
334
|
+
function renderCurrentAgent(agentId: string, nickname: string | null | undefined): string {
|
|
335
|
+
const self = nickname ? `${formatValue(agentId)} (${formatValue(nickname)})` : formatValue(agentId);
|
|
336
|
+
return [
|
|
337
|
+
"## ClawChat Current Agent",
|
|
338
|
+
`current_agent_id: ${formatValue(agentId)}`,
|
|
339
|
+
`current_agent_nickname: ${formatValue(nickname)}`,
|
|
340
|
+
`You are ${self}. A sender_id of ${formatValue(agentId)} is you. In a group, mentions_current_agent=true (mention_routing addressed_to_current_agent) means the message is addressed to you, whatever name the text shows.`,
|
|
341
|
+
].join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function renderGroupParticipants(
|
|
345
|
+
participants: ClawChatGroupParticipantPrompt[],
|
|
346
|
+
currentAgentId: string | null,
|
|
347
|
+
): string | null {
|
|
335
348
|
if (participants.length === 0) return null;
|
|
336
349
|
const lines = participants.map((participant) => {
|
|
337
350
|
const labels = [participant.profileType || "user"];
|
|
351
|
+
if (currentAgentId && participant.id === currentAgentId) labels.push("current_agent");
|
|
338
352
|
if (participant.isAgentOwner) labels.push("agent_owner");
|
|
339
353
|
if (participant.isGroupOwner) labels.push("group_owner");
|
|
340
354
|
const type = labels.join(", ");
|
|
@@ -347,6 +361,7 @@ export function renderClawChatProfilePrompt(params: {
|
|
|
347
361
|
basePrompt: string;
|
|
348
362
|
ownerMetadata?: ClawChatPromptMetadata | null;
|
|
349
363
|
ownerIdFallback?: string | null;
|
|
364
|
+
currentAgentIdFallback?: string | null;
|
|
350
365
|
userMetadata?: ClawChatPromptMetadata | null;
|
|
351
366
|
groupMetadata?: ClawChatPromptMetadata | null;
|
|
352
367
|
groupParticipants?: ClawChatGroupParticipantPrompt[];
|
|
@@ -365,6 +380,10 @@ export function renderClawChatProfilePrompt(params: {
|
|
|
365
380
|
if (Object.keys(ownerMetadata).length > 0) {
|
|
366
381
|
sections.push(renderMetadataSection("ClawChat Agent Owner Metadata", ownerMetadata));
|
|
367
382
|
}
|
|
383
|
+
const currentAgentId = ownerMetadataSource.agent_id || params.currentAgentIdFallback || null;
|
|
384
|
+
if (currentAgentId) {
|
|
385
|
+
sections.push(renderCurrentAgent(currentAgentId, ownerMetadataSource.agent_nickname));
|
|
386
|
+
}
|
|
368
387
|
if (
|
|
369
388
|
params.turn.chatType === "dm" &&
|
|
370
389
|
!params.turn.senderIsOwner &&
|
|
@@ -387,7 +406,7 @@ export function renderClawChatProfilePrompt(params: {
|
|
|
387
406
|
}
|
|
388
407
|
}
|
|
389
408
|
if (params.turn.chatType === "group") {
|
|
390
|
-
const participantSection = renderGroupParticipants(params.groupParticipants ?? []);
|
|
409
|
+
const participantSection = renderGroupParticipants(params.groupParticipants ?? [], currentAgentId);
|
|
391
410
|
if (participantSection) sections.push(participantSection);
|
|
392
411
|
}
|
|
393
412
|
sections.push(
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -11,6 +11,11 @@ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
|
11
11
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
12
12
|
import { resolveOutboundMediaUrls } from "openclaw/plugin-sdk/reply-payload";
|
|
13
13
|
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
|
14
|
+
import {
|
|
15
|
+
classifyFrameworkError,
|
|
16
|
+
formatFrameworkErrorForOwner,
|
|
17
|
+
isFrameworkErrorReply,
|
|
18
|
+
} from "./framework-error-copy.ts";
|
|
14
19
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
15
20
|
import type { ChatType } from "./client.ts";
|
|
16
21
|
import {
|
|
@@ -77,6 +82,12 @@ export interface ReplyDispatcherOptions {
|
|
|
77
82
|
* dispatcher was constructed is still picked up.
|
|
78
83
|
*/
|
|
79
84
|
resolveOwnerConversationId?: () => string | null;
|
|
85
|
+
/**
|
|
86
|
+
* The owner's reported app locale (`agent_owner_locale`), read lazily at
|
|
87
|
+
* send time. Picks the language of the sentence that leads a rewritten
|
|
88
|
+
* framework error final; null/absent means English.
|
|
89
|
+
*/
|
|
90
|
+
resolveOwnerLocale?: () => string | null;
|
|
80
91
|
log?: { info?: (m: string) => void; error?: (m: string) => void };
|
|
81
92
|
}
|
|
82
93
|
|
|
@@ -418,6 +429,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
418
429
|
inboundMessageId,
|
|
419
430
|
store,
|
|
420
431
|
resolveOwnerConversationId,
|
|
432
|
+
resolveOwnerLocale,
|
|
421
433
|
log,
|
|
422
434
|
} = options;
|
|
423
435
|
const isGroupTarget = target.chatType === "group";
|
|
@@ -933,7 +945,18 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
933
945
|
});
|
|
934
946
|
return;
|
|
935
947
|
}
|
|
936
|
-
|
|
948
|
+
// OpenClaw's own failure copy (isError, or a leading ⚠️) is English
|
|
949
|
+
// and often operator-facing; lead it with an owner-language sentence
|
|
950
|
+
// and keep the original at the end.
|
|
951
|
+
const deliveredText = isFrameworkErrorReply(payload, text)
|
|
952
|
+
? formatFrameworkErrorForOwner(text, resolveOwnerLocale?.() ?? null)
|
|
953
|
+
: text;
|
|
954
|
+
if (deliveredText !== text) {
|
|
955
|
+
log?.info?.(
|
|
956
|
+
`[${account.accountId}] clawchat-plugin-openclaw final framework error localized category=${classifyFrameworkError(text)}`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
const finalText = richFragment && account.richInteractions ? mergeFinalText("") : mergeFinalText(deliveredText);
|
|
937
960
|
const finalUrls = mergeFinalUrls(urls);
|
|
938
961
|
if (
|
|
939
962
|
isClawChatNoopResponseText(finalText) &&
|
package/src/runtime.ts
CHANGED
|
@@ -1210,6 +1210,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1210
1210
|
// `sendOwnerSkillMessage` below) — both live in this same function closure, so
|
|
1211
1211
|
// the forward reference here just resolves to the fresh value at call time.
|
|
1212
1212
|
let livewareSampleNotify: (text: string) => Promise<boolean> = async () => false;
|
|
1213
|
+
// The supervisor this gateway call shares (new or adopted). The abort cleanup
|
|
1214
|
+
// below awaits its stop() so the host's channel stop (which waits on this
|
|
1215
|
+
// task) does not return while liveware children are still being killed.
|
|
1216
|
+
let livewareSampleForShutdown: LivewareSampleSupervisor | null = null;
|
|
1213
1217
|
if (store) {
|
|
1214
1218
|
const stateDirForSample = runtime.state?.resolveStateDir?.();
|
|
1215
1219
|
const livewareSampleDeps: LivewareSampleSupervisorDeps = {
|
|
@@ -1253,6 +1257,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1253
1257
|
livewareSampleSupervisors.set(abortSignal, supervisorsForSignal);
|
|
1254
1258
|
const supervisorKey = `openclaw:${accountId}`;
|
|
1255
1259
|
const runningSupervisor = supervisorsForSignal.get(supervisorKey);
|
|
1260
|
+
livewareSampleForShutdown = runningSupervisor ?? null;
|
|
1256
1261
|
if (runningSupervisor) {
|
|
1257
1262
|
// A gateway re-entry (refresh/backoff reconnect), not a fresh channel start:
|
|
1258
1263
|
// hand the live supervisor this closure's token + REST client + notify path
|
|
@@ -1263,9 +1268,12 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1263
1268
|
runningSupervisor.adoptDeps(livewareSampleDeps);
|
|
1264
1269
|
} else {
|
|
1265
1270
|
const livewareSampleSupervisor = new LivewareSampleSupervisor(livewareSampleDeps);
|
|
1271
|
+
livewareSampleForShutdown = livewareSampleSupervisor;
|
|
1266
1272
|
supervisorsForSignal.set(supervisorKey, livewareSampleSupervisor);
|
|
1267
1273
|
abortSignal.addEventListener("abort", () => {
|
|
1268
|
-
|
|
1274
|
+
// Starts the SIGTERM → SIGKILL teardown at once; the gateway's own
|
|
1275
|
+
// abort cleanup awaits the same (idempotent) stop() promise.
|
|
1276
|
+
void livewareSampleSupervisor.stop();
|
|
1269
1277
|
if (supervisorsForSignal.get(supervisorKey) === livewareSampleSupervisor) {
|
|
1270
1278
|
supervisorsForSignal.delete(supervisorKey);
|
|
1271
1279
|
}
|
|
@@ -1576,6 +1584,25 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1576
1584
|
const persistOwnerProfile = upsertOwnerProfile
|
|
1577
1585
|
? makeOwnerProfilePersister({ upsertOwnerProfile }, { accountId })
|
|
1578
1586
|
: undefined;
|
|
1587
|
+
// The owner_profile row is where owner-language copy reads its locale
|
|
1588
|
+
// (framework errors, greetings, liveware intros). It was only refreshed on
|
|
1589
|
+
// owner metadata pulls, so an owner who switched app language kept getting
|
|
1590
|
+
// the old one. friend.profile_updated also reaches the agent when the
|
|
1591
|
+
// owner edits their profile; refetch just the owner row then.
|
|
1592
|
+
const refreshOwnerProfileOnUpdate = (entityId: unknown): void => {
|
|
1593
|
+
const subject = typeof entityId === "string" ? entityId.trim() : "";
|
|
1594
|
+
if (!persistOwnerProfile || !subject || subject !== account.ownerUserId) return;
|
|
1595
|
+
void (async () => {
|
|
1596
|
+
try {
|
|
1597
|
+
const owner = await getConversationApiClient().getAgentOwner();
|
|
1598
|
+
persistOwnerProfile(owner as unknown as Record<string, unknown>);
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
log?.error?.(
|
|
1601
|
+
`[${accountId}] clawchat-plugin-openclaw owner profile refresh failed source=friend.profile_updated: ${err instanceof Error ? err.message : String(err)}`,
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
})();
|
|
1605
|
+
};
|
|
1579
1606
|
const refreshAgentBehavior = async (options: {
|
|
1580
1607
|
metadataVersion?: number;
|
|
1581
1608
|
source: string;
|
|
@@ -2791,6 +2818,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2791
2818
|
if (type === "friend.added") {
|
|
2792
2819
|
scheduleFriendGreeting(payload as Record<string, unknown> | undefined);
|
|
2793
2820
|
}
|
|
2821
|
+
if (type === "friend.profile_updated") {
|
|
2822
|
+
refreshOwnerProfileOnUpdate(payload?.entity_id);
|
|
2823
|
+
}
|
|
2794
2824
|
}
|
|
2795
2825
|
}
|
|
2796
2826
|
});
|
|
@@ -2903,6 +2933,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2903
2933
|
? groupFile.metadata.group_owner_nickname ?? metadata?.nickname ?? userId
|
|
2904
2934
|
: metadata?.nickname ?? userId;
|
|
2905
2935
|
const profileType = metadata?.profile_type ?? (userId === account.userId ? "agent" : "user");
|
|
2936
|
+
if (name !== userId) knownNicknames.set(userId, name);
|
|
2906
2937
|
participants.push({ id: userId, name, profileType, isAgentOwner, isGroupOwner });
|
|
2907
2938
|
}
|
|
2908
2939
|
return participants;
|
|
@@ -2941,6 +2972,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2941
2972
|
basePrompt: turn.peer.kind === "group" ? getClawChatGroupPrompt() : getClawChatUserPrompt(),
|
|
2942
2973
|
...promptMetadata,
|
|
2943
2974
|
ownerIdFallback: account.ownerUserId,
|
|
2975
|
+
currentAgentIdFallback: account.userId,
|
|
2944
2976
|
groupParticipants,
|
|
2945
2977
|
turn: {
|
|
2946
2978
|
chatType: promptChatType,
|
|
@@ -2961,8 +2993,76 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2961
2993
|
});
|
|
2962
2994
|
};
|
|
2963
2995
|
|
|
2996
|
+
// Group frames often carry the sender's id as nick_name. Prefer the nickname
|
|
2997
|
+
// already cached in ClawChat memory (owner.md for the owner and this agent,
|
|
2998
|
+
// users/<id>.md for everyone else, the group's group_owner_nickname for its
|
|
2999
|
+
// owner); keep the id when nothing is cached.
|
|
3000
|
+
const cachedNicknameFor = async (memoryRoot: string, userId: string): Promise<string | null> => {
|
|
3001
|
+
try {
|
|
3002
|
+
if (userId === account.ownerUserId || userId === account.userId) {
|
|
3003
|
+
const ownerFile = await readClawChatMemoryFile(memoryRoot, { targetType: "owner", targetId: "owner" });
|
|
3004
|
+
const nickname = userId === account.userId
|
|
3005
|
+
? ownerFile.metadata.agent_nickname
|
|
3006
|
+
: ownerFile.metadata.agent_owner_nickname;
|
|
3007
|
+
if (nickname) return nickname;
|
|
3008
|
+
}
|
|
3009
|
+
const userFile = await readClawChatMemoryFile(memoryRoot, { targetType: "user", targetId: userId });
|
|
3010
|
+
return userFile.exists && userFile.metadata.nickname ? userFile.metadata.nickname : null;
|
|
3011
|
+
} catch {
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
|
|
3016
|
+
// Nicknames seen while building group prompts, so ingest can label a
|
|
3017
|
+
// coalesced batch's transcript ("Alice:" rather than "usr_…:") without
|
|
3018
|
+
// touching disk.
|
|
3019
|
+
const knownNicknames = new Map<string, string>();
|
|
3020
|
+
|
|
3021
|
+
const withCachedGroupSenderNames = async <T extends IngestTurnParams>(
|
|
3022
|
+
turn: T,
|
|
3023
|
+
memoryRoot: string | null,
|
|
3024
|
+
): Promise<T> => {
|
|
3025
|
+
if (turn.peer.kind !== "group" || !memoryRoot) return turn;
|
|
3026
|
+
const isBareId = (name: string | null | undefined, id: string): boolean => !name || name === id;
|
|
3027
|
+
const cache = new Map<string, string | null>();
|
|
3028
|
+
let groupOwner: { id?: string; nickname?: string } | null = null;
|
|
3029
|
+
const lookup = async (userId: string): Promise<string | null> => {
|
|
3030
|
+
if (!cache.has(userId)) {
|
|
3031
|
+
let nickname = await cachedNicknameFor(memoryRoot, userId);
|
|
3032
|
+
if (!nickname) {
|
|
3033
|
+
if (!groupOwner) {
|
|
3034
|
+
try {
|
|
3035
|
+
const groupFile = await readClawChatMemoryFile(memoryRoot, { targetType: "group", targetId: turn.peer.id });
|
|
3036
|
+
groupOwner = { id: groupFile.metadata.group_owner_id, nickname: groupFile.metadata.group_owner_nickname };
|
|
3037
|
+
} catch {
|
|
3038
|
+
groupOwner = {};
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (groupOwner.id === userId && groupOwner.nickname) nickname = groupOwner.nickname;
|
|
3042
|
+
}
|
|
3043
|
+
cache.set(userId, nickname);
|
|
3044
|
+
if (nickname) knownNicknames.set(userId, nickname);
|
|
3045
|
+
}
|
|
3046
|
+
return cache.get(userId) ?? null;
|
|
3047
|
+
};
|
|
3048
|
+
const senderNickName = isBareId(turn.senderNickName, turn.senderId)
|
|
3049
|
+
? (await lookup(turn.senderId)) ?? turn.senderNickName
|
|
3050
|
+
: turn.senderNickName;
|
|
3051
|
+
const groupMessages = turn.groupMessages
|
|
3052
|
+
? await Promise.all(turn.groupMessages.map(async (message) =>
|
|
3053
|
+
isBareId(message.senderName, message.senderId)
|
|
3054
|
+
? { ...message, senderName: (await lookup(message.senderId)) ?? message.senderName }
|
|
3055
|
+
: message
|
|
3056
|
+
))
|
|
3057
|
+
: turn.groupMessages;
|
|
3058
|
+
return { ...turn, senderNickName, ...(groupMessages ? { groupMessages } : {}) };
|
|
3059
|
+
};
|
|
3060
|
+
|
|
2964
3061
|
const resolveSenderNickNameForTurn = (turn: IngestTurnParams): string => {
|
|
2965
3062
|
if (turn.senderNickName && turn.senderNickName !== turn.senderId) return turn.senderNickName;
|
|
3063
|
+
// Synchronous on purpose: an await here would reorder same-group frames.
|
|
3064
|
+
const known = turn.peer.kind === "group" ? knownNicknames.get(turn.senderId) : undefined;
|
|
3065
|
+
if (known) return known;
|
|
2966
3066
|
return turn.senderNickName || turn.senderId;
|
|
2967
3067
|
};
|
|
2968
3068
|
|
|
@@ -2970,7 +3070,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2970
3070
|
senderRelation: NonNullable<IngestTurnParams["senderRelation"]>;
|
|
2971
3071
|
senderProfileType: string;
|
|
2972
3072
|
senderIsOwner: boolean;
|
|
2973
|
-
senderIsGroupOwner
|
|
3073
|
+
senderIsGroupOwner?: boolean;
|
|
2974
3074
|
} => {
|
|
2975
3075
|
const senderRelation = resolveSenderRelation({
|
|
2976
3076
|
senderId: turn.senderId,
|
|
@@ -2983,7 +3083,9 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2983
3083
|
senderProfileType: turn.senderProfileType ??
|
|
2984
3084
|
(senderRelation === "self_agent" || senderRelation === "peer_agent" ? "agent" : "user"),
|
|
2985
3085
|
senderIsOwner: senderRelation === "owner",
|
|
2986
|
-
|
|
3086
|
+
// Left undefined when the frame does not say, so the prompt falls back
|
|
3087
|
+
// to the group metadata's group_owner_id instead of a forced false.
|
|
3088
|
+
...(turn.senderIsGroupOwner !== undefined ? { senderIsGroupOwner: turn.senderIsGroupOwner } : {}),
|
|
2987
3089
|
};
|
|
2988
3090
|
};
|
|
2989
3091
|
|
|
@@ -3062,6 +3164,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3062
3164
|
`[${accountId}] clawchat-plugin-openclaw message metadata refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3063
3165
|
);
|
|
3064
3166
|
}
|
|
3167
|
+
turn = await withCachedGroupSenderNames(turn, memoryRoot);
|
|
3065
3168
|
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
3066
3169
|
const turnPrompt = await buildPromptForTurn(turn, memoryRoot);
|
|
3067
3170
|
const senderIsOwner = turn.senderIsOwner === true || turn.senderId === account.ownerUserId;
|
|
@@ -3202,6 +3305,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3202
3305
|
runtime,
|
|
3203
3306
|
account,
|
|
3204
3307
|
client,
|
|
3308
|
+
resolveOwnerLocale: () =>
|
|
3309
|
+
store?.getOwnerProfile?.({ platform: "openclaw", accountId })?.locale ?? null,
|
|
3205
3310
|
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
3206
3311
|
...(replyCtx ? { replyCtx } : {}),
|
|
3207
3312
|
inboundMessageId: turn.messageId,
|
|
@@ -3871,6 +3976,17 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3871
3976
|
closeReason: "client close",
|
|
3872
3977
|
});
|
|
3873
3978
|
client.close();
|
|
3979
|
+
if (livewareSampleForShutdown) {
|
|
3980
|
+
// SIGTERM, then SIGKILL after KILL_GRACE_MS (3 s): inside the host's
|
|
3981
|
+
// 5 s wait for this task, so a stuck child no longer outlives the stop.
|
|
3982
|
+
try {
|
|
3983
|
+
await livewareSampleForShutdown.stop();
|
|
3984
|
+
} catch (err) {
|
|
3985
|
+
log?.error?.(
|
|
3986
|
+
`[${accountId}] clawchat-plugin-openclaw liveware-sample stop failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3874
3990
|
setStatus({
|
|
3875
3991
|
...getStatus(),
|
|
3876
3992
|
connected: false,
|
package/src/skill-update.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const OFFICIAL_SKILLS_BASE =
|
|
|
71
71
|
* in the install-cli repo, bump this constant, ship it. `liveware-sample.ts`
|
|
72
72
|
* imports the same ref, so the `livewares` tree at that tag is pinned too.
|
|
73
73
|
*/
|
|
74
|
-
export const DEFAULT_SKILLS_REF = "skills-v1.
|
|
74
|
+
export const DEFAULT_SKILLS_REF = "skills-v1.17.0";
|
|
75
75
|
|
|
76
76
|
/** Refuse to treat an absurdly large response as a skill file (defence in depth). */
|
|
77
77
|
export const MAX_SKILL_BYTES = 256 * 1024;
|
package/src/tools-schema.ts
CHANGED
|
@@ -328,7 +328,7 @@ export const ClawchatCreateMomentSchema = Type.Object({
|
|
|
328
328
|
images: Type.Optional(
|
|
329
329
|
Type.Array(
|
|
330
330
|
Type.String({
|
|
331
|
-
description: "
|
|
331
|
+
description: "An http(s) image URL, or an absolute local image path (the tool uploads it and uses the hosted URL).",
|
|
332
332
|
}),
|
|
333
333
|
),
|
|
334
334
|
),
|
package/src/tools.ts
CHANGED
|
@@ -148,6 +148,17 @@ function extractRequestId(err: ClawlingApiError): string | undefined {
|
|
|
148
148
|
return undefined;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* What the agent must do with the later `permission_result` receipt
|
|
153
|
+
* (agent-protocol §2.8): on approval the SERVER executes the gated call, so a
|
|
154
|
+
* second call would duplicate it or raise a fresh card. Only `approved_retry`
|
|
155
|
+
* (reads, which are never replayed) asks for exactly one more call.
|
|
156
|
+
*/
|
|
157
|
+
const PERMISSION_RECEIPT_RULE =
|
|
158
|
+
"When the result arrives: outcome \"approved\" (or \"auto_allowed\") means the server has already done it for you — do not call this tool again; " +
|
|
159
|
+
"only outcome \"approved_retry\" means call this same tool once more with the same arguments. " +
|
|
160
|
+
"Anything the server produced (a new id, a code, read data) comes in that message's Result line.";
|
|
161
|
+
|
|
151
162
|
function permissionGateResult(err: ClawlingApiError) {
|
|
152
163
|
const code = (err.meta as Record<string, unknown> | undefined)?.code;
|
|
153
164
|
const requestId = extractRequestId(err);
|
|
@@ -158,7 +169,8 @@ function permissionGateResult(err: ClawlingApiError) {
|
|
|
158
169
|
"This operation requires the owner's approval and has been submitted for review" +
|
|
159
170
|
(requestId ? ` (request_id=${requestId})` : "") +
|
|
160
171
|
". It has NOT failed. Do not retry — the result will arrive later as a normal " +
|
|
161
|
-
"chat message; wait for it instead of calling this tool again."
|
|
172
|
+
"chat message; wait for it instead of calling this tool again. " +
|
|
173
|
+
PERMISSION_RECEIPT_RULE;
|
|
162
174
|
status = "pending";
|
|
163
175
|
} else {
|
|
164
176
|
message =
|
|
@@ -257,6 +269,50 @@ function inferMimeFromPath(filePath: string): string {
|
|
|
257
269
|
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
258
270
|
}
|
|
259
271
|
|
|
272
|
+
type MomentImageEntry = { kind: "url"; url: string } | { kind: "local"; path: string };
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Classify `clawchat_create_moment` image entries: http(s) URLs pass through;
|
|
276
|
+
* an existing absolute local regular file (≤ 20MB) is marked for upload; any
|
|
277
|
+
* other value is a validation error naming the offending index.
|
|
278
|
+
*/
|
|
279
|
+
function planMomentImages(
|
|
280
|
+
images: unknown[],
|
|
281
|
+
): { ok: true; entries: MomentImageEntry[] } | { ok: false; message: string } {
|
|
282
|
+
const entries: MomentImageEntry[] = [];
|
|
283
|
+
for (const [i, raw] of images.entries()) {
|
|
284
|
+
const where = `clawchat-plugin-openclaw: images[${i}]`;
|
|
285
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
286
|
+
if (!value) {
|
|
287
|
+
return { ok: false, message: `${where} must be an http(s) image URL or an absolute local image path` };
|
|
288
|
+
}
|
|
289
|
+
if (/^https?:\/\/\S+$/i.test(value)) {
|
|
290
|
+
entries.push({ kind: "url", url: value });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (!path.isAbsolute(value)) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
message: `${where} (${JSON.stringify(value)}) is neither an http(s) URL nor an absolute local file path; no moment was created`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
let stat: fs.Stats;
|
|
300
|
+
try {
|
|
301
|
+
stat = fs.statSync(value);
|
|
302
|
+
} catch {
|
|
303
|
+
return { ok: false, message: `${where}: local file ${value} does not exist; no moment was created` };
|
|
304
|
+
}
|
|
305
|
+
if (!stat.isFile()) {
|
|
306
|
+
return { ok: false, message: `${where}: ${value} is not a regular file; no moment was created` };
|
|
307
|
+
}
|
|
308
|
+
if (stat.size > MAX_UPLOAD_BYTES) {
|
|
309
|
+
return { ok: false, message: `${where}: ${value} is too large (${stat.size} bytes; max 20MB)` };
|
|
310
|
+
}
|
|
311
|
+
entries.push({ kind: "local", path: value });
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, entries };
|
|
314
|
+
}
|
|
315
|
+
|
|
260
316
|
function validateMentionMessageParams(params: unknown):
|
|
261
317
|
| { ok: true; params: ClawchatMentionMessageParams }
|
|
262
318
|
| { ok: false; message: string } {
|
|
@@ -442,6 +498,7 @@ export function registerOpenclawClawlingTools(
|
|
|
442
498
|
ok: true,
|
|
443
499
|
client: createOpenclawClawlingApiClient({
|
|
444
500
|
baseUrl: acct.baseUrl,
|
|
501
|
+
mediaBaseUrl: acct.mediaBaseUrl,
|
|
445
502
|
token: acct.token,
|
|
446
503
|
userId: acct.userId,
|
|
447
504
|
}),
|
|
@@ -490,7 +547,19 @@ export function registerOpenclawClawlingTools(
|
|
|
490
547
|
? (rawData as Record<string, unknown>)
|
|
491
548
|
: {};
|
|
492
549
|
const outcome = mapGateOutcome(code, gateData);
|
|
493
|
-
if (outcome !== null)
|
|
550
|
+
if (outcome !== null) {
|
|
551
|
+
// `next_step` is added here, not in mapGateOutcome: that mapping
|
|
552
|
+
// is pinned byte-for-byte against the Hermes parity fixtures.
|
|
553
|
+
return jsonResponse(
|
|
554
|
+
outcome.status === "pending_owner_approval"
|
|
555
|
+
? {
|
|
556
|
+
...outcome,
|
|
557
|
+
next_step:
|
|
558
|
+
"Waiting for the owner's approval; do not call this tool again now. " + PERMISSION_RECEIPT_RULE,
|
|
559
|
+
}
|
|
560
|
+
: outcome,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
494
563
|
}
|
|
495
564
|
return apiError(err);
|
|
496
565
|
}
|
|
@@ -1413,7 +1482,7 @@ export function registerOpenclawClawlingTools(
|
|
|
1413
1482
|
description: toolDescription(
|
|
1414
1483
|
"Create a new ClawChat moment/dynamic for the configured ClawChat account. " +
|
|
1415
1484
|
"TRIGGER - invoke when the user asks to publish, post, or send a ClawChat moment/dynamic, such as \"post a ClawChat moment saying ...\", \"publish a dynamic ...\", or \"send a friend moment ...\". " +
|
|
1416
|
-
"At least one of text or images must be present.
|
|
1485
|
+
"At least one of text or images must be present. Each images entry is either an http(s) image URL (used as-is) or an absolute local image path (max 20MB) — for a local path this tool uploads it to ClawChat media itself and uses the hosted URL, so no separate upload step is needed. Any other value (relative path, file:// or data: URI, missing file) is rejected and no moment is created.",
|
|
1417
1486
|
),
|
|
1418
1487
|
parameters: ClawchatCreateMomentSchema,
|
|
1419
1488
|
async execute(_callId, params) {
|
|
@@ -1424,12 +1493,34 @@ export function registerOpenclawClawlingTools(
|
|
|
1424
1493
|
if (!text && (!images || images.length === 0)) {
|
|
1425
1494
|
return validationError("clawchat-plugin-openclaw: at least one of text or images is required");
|
|
1426
1495
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1496
|
+
// Validate every entry before touching the network, so a bad value
|
|
1497
|
+
// never leaves a half-uploaded batch or a moment with a broken image.
|
|
1498
|
+
const plan = images !== undefined ? planMomentImages(images) : { ok: true as const, entries: [] };
|
|
1499
|
+
if (!plan.ok) return validationError(plan.message);
|
|
1500
|
+
return await withGatedClient(accountId, async (c) => {
|
|
1501
|
+
const resolved: string[] = [];
|
|
1502
|
+
for (const entry of plan.entries) {
|
|
1503
|
+
if (entry.kind === "url") {
|
|
1504
|
+
resolved.push(entry.url);
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
const uploaded = await c.uploadMedia({
|
|
1508
|
+
buffer: fs.readFileSync(entry.path),
|
|
1509
|
+
filename: path.basename(entry.path),
|
|
1510
|
+
mime: inferMimeFromPath(entry.path),
|
|
1511
|
+
});
|
|
1512
|
+
if (uploaded.kind !== "image") {
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`clawchat-plugin-openclaw: ${entry.path} is not an image (media service classified it as ${uploaded.kind}); no moment was created`,
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
resolved.push(uploaded.url);
|
|
1518
|
+
}
|
|
1519
|
+
return await c.createMoment({
|
|
1429
1520
|
...(text !== undefined ? { text } : {}),
|
|
1430
|
-
...(images !== undefined ? { images } : {}),
|
|
1431
|
-
})
|
|
1432
|
-
);
|
|
1521
|
+
...(images !== undefined ? { images: resolved } : {}),
|
|
1522
|
+
});
|
|
1523
|
+
});
|
|
1433
1524
|
});
|
|
1434
1525
|
},
|
|
1435
1526
|
};
|