feat: Fabric channel plugin for OpenClaw (Phase 1, B1)
OpenClaw channel plugin: defineChannelPluginEntry + createChatChannelPlugin.
Inbound via channel-turn kernel (wakeup -> admission: true=dispatch,
else drop+recordHistory). One Fabric socket per agent identity in the
plugin runtime (no sidecar). Center API-key agent auth. Tools:
fabric-register, create-{chat,work,report,discussion}-channel,
discussion-complete (post summary + close channel).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
src/fabric-client.ts
Normal file
89
src/fabric-client.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// Thin Fabric REST client. One auth concept: an agent's Center API key is
|
||||
// exchanged for a normal user session (POST /auth/agent/login); the returned
|
||||
// guild access tokens are used to post messages and call channel APIs.
|
||||
|
||||
export type FabricSession = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: { id: string; email: string; name: string };
|
||||
guilds: Array<{ nodeId: string; name: string; endpoint: string; status: string }>;
|
||||
guildAccessTokens: Array<{ guildNodeId: string; token: string }>;
|
||||
};
|
||||
|
||||
export class FabricClient {
|
||||
constructor(private readonly centerApiBase: string) {}
|
||||
|
||||
private async post<T>(url: string, body: unknown, auth?: string): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
...(auth ? { authorization: `Bearer ${auth}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`POST ${url} -> ${res.status} ${text}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
||||
agentLogin(apiKey: string): Promise<FabricSession> {
|
||||
return this.post<FabricSession>(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
||||
}
|
||||
|
||||
// Refresh the center access token (guild tokens are re-fetched via /auth/me/guilds).
|
||||
async refresh(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> {
|
||||
return this.post(`${this.centerApiBase}/auth/refresh`, { refreshToken });
|
||||
}
|
||||
|
||||
async meGuilds(accessToken: string): Promise<Pick<FabricSession, 'guilds' | 'guildAccessTokens'>> {
|
||||
const res = await fetch(`${this.centerApiBase}/auth/me/guilds`, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`me/guilds -> ${res.status}`);
|
||||
return (await res.json()) as Pick<FabricSession, 'guilds' | 'guildAccessTokens'>;
|
||||
}
|
||||
|
||||
// ---- guild-scoped (use the per-guild access token) ----
|
||||
|
||||
postMessage(
|
||||
guildEndpoint: string,
|
||||
guildToken: string,
|
||||
channelId: string,
|
||||
content: string,
|
||||
authorUserId: string,
|
||||
): Promise<unknown> {
|
||||
return this.post(
|
||||
`${guildEndpoint}/api/channels/${channelId}/messages`,
|
||||
{ content, authorUserId },
|
||||
guildToken,
|
||||
);
|
||||
}
|
||||
|
||||
createChannel(
|
||||
guildEndpoint: string,
|
||||
guildToken: string,
|
||||
body: {
|
||||
guildId: string;
|
||||
name: string;
|
||||
xType: string;
|
||||
isPublic?: boolean;
|
||||
memberUserIds?: string[];
|
||||
onDuty?: string;
|
||||
listeners?: string[];
|
||||
},
|
||||
): Promise<{ id: string }> {
|
||||
return this.post(`${guildEndpoint}/api/channels`, body, guildToken);
|
||||
}
|
||||
|
||||
closeChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise<unknown> {
|
||||
return this.post(`${guildEndpoint}/api/channels/${channelId}/close`, {}, guildToken);
|
||||
}
|
||||
|
||||
joinChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise<unknown> {
|
||||
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user