feat(tools): fabric-send-message + fabric-channel-list + fabric-message-history
Plugin previously had no way for an agent to send text into a specific
channel proactively — outbound went only through the channel-reply
path (responds to the channel that woke the agent). discussion-complete
internally called client.postMessage but only for the close-time
summary, no general-purpose surface.
Three new tools (+ declare existing fabric-canvas / fabric-channel that
were registered but missing from contracts.tools so agents couldn't
see them per the openclaw plugin contract):
* fabric-send-message {guildNodeId, channelId, content}
→ {ok, messageId, seq}
Author = calling agent. Use for ARD broadcasts, follow-ups in a
different channel, etc.
* fabric-channel-list {guildNodeId, nameFilter?, xType?, includeClosed?}
→ {ok, count, channels[]}
Backend filters to public + member channels; nameFilter is client-
side case-insensitive substring; xType / includeClosed apply post-
fetch. Returns id/name/xType/lastSeq so callers can pipe into the
other tools.
* fabric-message-history {guildNodeId, channelId, seqFrom?, seqTo?, limit?}
→ {ok, page, messages[]}
Tail-by-default: omit seqFrom/seqTo and the tool fetches the
channel head from listChannels then asks for [head-limit+1, head].
Limit default 20, max 200. Backend rejects non-participants.
Plus 3 supporting client methods (listChannels, listMessages — both
GET via existing req helper).
contracts.tools updated to declare these 5 (3 new + 2 previously-
silent ones). Verified earlier in sim restart logs: openclaw warned
'plugin tool is undeclared (fabric): fabric-canvas / fabric-channel'
so agents couldn't use them despite registerTool firing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -190,6 +190,76 @@ export class FabricClient {
|
||||
removeCanvas(endpoint: string, token: string, channelId: string): Promise<unknown> {
|
||||
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
||||
}
|
||||
|
||||
// ---- channel discovery + message read (used by the agent-facing
|
||||
// fabric-channel-list / fabric-message-history tools) ----
|
||||
|
||||
/**
|
||||
* List channels in a guild visible to the calling user. Backend
|
||||
* filters to public + channels the user is a member of.
|
||||
*/
|
||||
listChannels(
|
||||
guildEndpoint: string,
|
||||
guildToken: string,
|
||||
guildNodeId: string,
|
||||
): Promise<Array<{
|
||||
id: string;
|
||||
guildId: string;
|
||||
name: string;
|
||||
xType: string;
|
||||
kind: string;
|
||||
isPublic: boolean;
|
||||
closed: boolean;
|
||||
lastSeq: number;
|
||||
createdAt: string;
|
||||
}>> {
|
||||
return this.req(
|
||||
'GET',
|
||||
`${guildEndpoint}/api/channels?guildId=${encodeURIComponent(guildNodeId)}`,
|
||||
guildToken,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page through a channel's message history by `seq`.
|
||||
*
|
||||
* Backend defaults: 50 / call, max 200. The `seq` field starts at 1
|
||||
* per channel; pass `seqFrom=channel.lastSeq - N + 1` to get the
|
||||
* tail. Page metadata in the response describes what to ask next.
|
||||
*/
|
||||
listMessages(
|
||||
guildEndpoint: string,
|
||||
guildToken: string,
|
||||
channelId: string,
|
||||
opts: { seqFrom?: number; seqTo?: number; limit?: number } = {},
|
||||
): Promise<{
|
||||
items: Array<{
|
||||
messageId: string;
|
||||
seq: number;
|
||||
content: string;
|
||||
authorUserId: string;
|
||||
createdAt: string;
|
||||
editedAt: string | null;
|
||||
deletedAt: string | null;
|
||||
isDeleted: boolean;
|
||||
}>;
|
||||
page: {
|
||||
seqFrom: number;
|
||||
seqTo: number;
|
||||
limit: number;
|
||||
returned: number;
|
||||
hasMore: boolean;
|
||||
nextExpectedSeq: number;
|
||||
highestCommittedSeq: number;
|
||||
};
|
||||
}> {
|
||||
const qs = new URLSearchParams();
|
||||
if (opts.seqFrom !== undefined) qs.set('seq_from', String(opts.seqFrom));
|
||||
if (opts.seqTo !== undefined) qs.set('seq_to', String(opts.seqTo));
|
||||
if (opts.limit !== undefined) qs.set('limit', String(opts.limit));
|
||||
const url = `${guildEndpoint}/api/channels/${channelId}/messages` + (qs.toString() ? `?${qs}` : '');
|
||||
return this.req('GET', url, guildToken);
|
||||
}
|
||||
}
|
||||
|
||||
export type CanvasFormat = 'md' | 'html' | 'text';
|
||||
|
||||
Reference in New Issue
Block a user