feat(plugin): fabric-canvas tool; fabric-register env=AGENT_ID only
- bin/fabric-register.mjs: only AGENT_ID is read from the environment; --api-key is flag-only (no FABRIC_API_KEY); dropped FABRIC_CENTER_API_BASE / FABRIC_IDENTITY_FILE / OPENCLAW_PATH env fallbacks (flags + sensible defaults; --center still falls back to openclaw.json). - New fabric-canvas tool (one tool, four actions): read / share / update / close the channel's single pinned canvas. Backed by FabricClient get/share/update/removeCanvas (GET/PUT/PATCH/DELETE; empty 2xx body -> null). update/close are sharer-only server-side. - README updated. Verified: client-level smoke against the running guild — read(empty→null) → share(v1) → read → update(v2) → close(→null) all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
README.md
14
README.md
@@ -59,10 +59,11 @@ Two ways, both write the same identity registry the transport reads:
|
|||||||
It validates the key against Center, then writes
|
It validates the key against Center, then writes
|
||||||
`~/.openclaw/fabric-identity.json`. One-time and persistent — *not* per
|
`~/.openclaw/fabric-identity.json`. One-time and persistent — *not* per
|
||||||
login; the plugin's transport logs in and stays connected on its own.
|
login; the plugin's transport logs in and stays connected on its own.
|
||||||
`AGENT_ID` env wins; otherwise `--agent-id` is required. Other flags:
|
**Only `AGENT_ID` is read from the environment** — if unset, `--agent-id`
|
||||||
`--center`, `--identity-file`, `--openclaw-path` (env equivalents:
|
is required. `--api-key` is flag-only (never from the env). Other flags:
|
||||||
`FABRIC_API_KEY`, `FABRIC_CENTER_API_BASE`, `FABRIC_IDENTITY_FILE`,
|
`--center`, `--identity-file`, `--openclaw-path` (sensible defaults;
|
||||||
`OPENCLAW_PATH`). Restart the gateway afterwards.
|
`--center` also falls back to `openclaw.json`). Restart the gateway
|
||||||
|
afterwards.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
@@ -90,6 +91,11 @@ Then `openclaw gateway restart`.
|
|||||||
`create-report-channel` (report) / `create-discussion-channel` (discuss)
|
`create-report-channel` (report) / `create-discussion-channel` (discuss)
|
||||||
- `discussion-complete` — post a summary, then close the channel
|
- `discussion-complete` — post a summary, then close the channel
|
||||||
(closed → history readable; new posts → `409`)
|
(closed → history readable; new posts → `409`)
|
||||||
|
- `fabric-canvas` — manage a channel's single pinned canvas document; one
|
||||||
|
tool, four `action`s: `read` (current canvas or null) · `share`
|
||||||
|
(create/replace; caller becomes sharer) · `update` (edit in place;
|
||||||
|
sharer-only) · `close` (remove; sharer-only). `share` needs
|
||||||
|
`title`/`format`(`md`|`html`|`text`)/`source`.
|
||||||
|
|
||||||
## Install / build
|
## Install / build
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,15 @@
|
|||||||
* fabric-register --api-key fak_xxx # uses $AGENT_ID
|
* fabric-register --api-key fak_xxx # uses $AGENT_ID
|
||||||
* fabric-register --agent-id echo --api-key fak_xxx
|
* fabric-register --agent-id echo --api-key fak_xxx
|
||||||
*
|
*
|
||||||
* Flags / env:
|
* Only AGENT_ID is read from the environment; everything else is a flag.
|
||||||
* --agent-id <id> (or env AGENT_ID; one of them required)
|
*
|
||||||
* --api-key <fak_…> (or env FABRIC_API_KEY; required)
|
* Flags:
|
||||||
* --center <url> (or env FABRIC_CENTER_API_BASE; else openclaw.json;
|
* --agent-id <id> required unless the AGENT_ID env var is set
|
||||||
* else http://localhost:7001/api)
|
* --api-key <fak_…> required (flag only — never from the environment)
|
||||||
* --identity-file <path> (or env FABRIC_IDENTITY_FILE; else
|
* --center <url> else openclaw.json channels.fabric.centerApiBase;
|
||||||
* ~/.openclaw/fabric-identity.json)
|
* else http://localhost:7001/api
|
||||||
* --openclaw-path <dir> (or env OPENCLAW_PATH; else ~/.openclaw)
|
* --identity-file <path> default ~/.openclaw/fabric-identity.json
|
||||||
|
* --openclaw-path <dir> default ~/.openclaw
|
||||||
* -h | --help
|
* -h | --help
|
||||||
*/
|
*/
|
||||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||||
@@ -51,14 +52,16 @@ const HELP = `fabric-register — bind an OpenClaw agent to a Fabric Center API
|
|||||||
fabric-register --api-key fak_xxx # agent id from $AGENT_ID
|
fabric-register --api-key fak_xxx # agent id from $AGENT_ID
|
||||||
fabric-register --agent-id <id> --api-key fak_xxx
|
fabric-register --agent-id <id> --api-key fak_xxx
|
||||||
|
|
||||||
--agent-id <id> required unless $AGENT_ID is set
|
--agent-id <id> required unless the AGENT_ID env var is set
|
||||||
--api-key <fak_…> required (or env FABRIC_API_KEY)
|
--api-key <fak_…> required (flag only — never read from the env)
|
||||||
--center <url> Center API base (or env FABRIC_CENTER_API_BASE;
|
--center <url> Center API base (else openclaw.json
|
||||||
else openclaw.json channels.fabric.centerApiBase;
|
channels.fabric.centerApiBase; else
|
||||||
else http://localhost:7001/api)
|
http://localhost:7001/api)
|
||||||
--identity-file <path> default ~/.openclaw/fabric-identity.json
|
--identity-file <path> default ~/.openclaw/fabric-identity.json
|
||||||
--openclaw-path <dir> default ~/.openclaw
|
--openclaw-path <dir> default ~/.openclaw
|
||||||
-h, --help
|
-h, --help
|
||||||
|
|
||||||
|
Only AGENT_ID is taken from the environment; everything else is a flag.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function fail(msg) {
|
function fail(msg) {
|
||||||
@@ -81,22 +84,17 @@ async function main() {
|
|||||||
fail('no agent id: set the AGENT_ID environment variable or pass --agent-id <id>');
|
fail('no agent id: set the AGENT_ID environment variable or pass --agent-id <id>');
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey =
|
// api key: flag ONLY — never from the environment.
|
||||||
(typeof a['api-key'] === 'string' && a['api-key'].trim()) ||
|
const apiKey = typeof a['api-key'] === 'string' && a['api-key'].trim();
|
||||||
(process.env.FABRIC_API_KEY && process.env.FABRIC_API_KEY.trim());
|
if (!apiKey) fail('missing --api-key <fak_…> (flag only)');
|
||||||
if (!apiKey) fail('missing --api-key <fak_…> (or env FABRIC_API_KEY)');
|
|
||||||
|
|
||||||
const openclawPath = resolve(
|
const openclawPath = resolve(
|
||||||
(typeof a['openclaw-path'] === 'string' && a['openclaw-path']) ||
|
(typeof a['openclaw-path'] === 'string' && a['openclaw-path']) ||
|
||||||
process.env.OPENCLAW_PATH ||
|
|
||||||
join(homedir(), '.openclaw'),
|
join(homedir(), '.openclaw'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// center api base: flag > env > openclaw.json > default
|
// center api base: flag > openclaw.json > default
|
||||||
let center =
|
let center = (typeof a.center === 'string' && a.center) || '';
|
||||||
(typeof a.center === 'string' && a.center) ||
|
|
||||||
process.env.FABRIC_CENTER_API_BASE ||
|
|
||||||
'';
|
|
||||||
if (!center) {
|
if (!center) {
|
||||||
try {
|
try {
|
||||||
const cfg = JSON.parse(readFileSync(join(openclawPath, 'openclaw.json'), 'utf8'));
|
const cfg = JSON.parse(readFileSync(join(openclawPath, 'openclaw.json'), 'utf8'));
|
||||||
@@ -110,7 +108,6 @@ async function main() {
|
|||||||
|
|
||||||
const identityFile = resolve(
|
const identityFile = resolve(
|
||||||
(typeof a['identity-file'] === 'string' && a['identity-file']) ||
|
(typeof a['identity-file'] === 'string' && a['identity-file']) ||
|
||||||
process.env.FABRIC_IDENTITY_FILE ||
|
|
||||||
join(openclawPath, 'fabric-identity.json'),
|
join(openclawPath, 'fabric-identity.json'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
38
dist/fabric/src/fabric-client.js
vendored
38
dist/fabric/src/fabric-client.js
vendored
@@ -21,6 +21,24 @@ export class FabricClient {
|
|||||||
}
|
}
|
||||||
return (await res.json());
|
return (await res.json());
|
||||||
}
|
}
|
||||||
|
// Generic JSON request (GET/PUT/PATCH/DELETE). Empty 2xx body -> null
|
||||||
|
// (Fabric returns an empty body when a channel has no canvas).
|
||||||
|
async req(method, url, auth, body) {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
||||||
|
...(auth ? { authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`${method} ${url} -> ${res.status} ${text}`);
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
return (text ? JSON.parse(text) : null);
|
||||||
|
}
|
||||||
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
||||||
agentLogin(apiKey) {
|
agentLogin(apiKey) {
|
||||||
return this.post(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
return this.post(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
||||||
@@ -50,4 +68,24 @@ export class FabricClient {
|
|||||||
joinChannel(guildEndpoint, guildToken, channelId) {
|
joinChannel(guildEndpoint, guildToken, channelId) {
|
||||||
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
||||||
}
|
}
|
||||||
|
// ---- channel canvas (one pinned doc per channel) ----
|
||||||
|
canvasUrl(endpoint, channelId) {
|
||||||
|
return `${endpoint}/api/channels/${channelId}/canvas`;
|
||||||
|
}
|
||||||
|
// null when the channel has no canvas
|
||||||
|
getCanvas(endpoint, token, channelId) {
|
||||||
|
return this.req('GET', this.canvasUrl(endpoint, channelId), token);
|
||||||
|
}
|
||||||
|
// share / replace (caller becomes the sharer)
|
||||||
|
shareCanvas(endpoint, token, channelId, body) {
|
||||||
|
return this.req('PUT', this.canvasUrl(endpoint, channelId), token, body);
|
||||||
|
}
|
||||||
|
// update in place (original sharer only — else the guild returns 403)
|
||||||
|
updateCanvas(endpoint, token, channelId, body) {
|
||||||
|
return this.req('PATCH', this.canvasUrl(endpoint, channelId), token, body);
|
||||||
|
}
|
||||||
|
// remove ("close") the canvas (original sharer only)
|
||||||
|
removeCanvas(endpoint, token, channelId) {
|
||||||
|
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
74
dist/fabric/src/tools.js
vendored
74
dist/fabric/src/tools.js
vendored
@@ -89,4 +89,78 @@ export function registerFabricTools(api, client, identity) {
|
|||||||
return { ok: true, closed: true };
|
return { ok: true, closed: true };
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
// fabric-canvas: share / update / read / close the channel's single
|
||||||
|
// pinned canvas document (one tool, four actions). update/close are
|
||||||
|
// sharer-only server-side (the guild returns 403 otherwise).
|
||||||
|
api.registerTool((ctx) => ({
|
||||||
|
name: 'fabric-canvas',
|
||||||
|
description: "Manage a channel's pinned canvas document. action: " +
|
||||||
|
"read (current canvas or null) | share (create/replace; you become " +
|
||||||
|
'the sharer) | update (edit in place; sharer only) | close (remove; ' +
|
||||||
|
'sharer only).',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['action', 'guildNodeId', 'channelId'],
|
||||||
|
properties: {
|
||||||
|
action: { type: 'string', enum: ['read', 'share', 'update', 'close'] },
|
||||||
|
guildNodeId: { type: 'string' },
|
||||||
|
channelId: { type: 'string' },
|
||||||
|
title: { type: 'string', description: 'share: required; update: optional' },
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['md', 'html', 'text'],
|
||||||
|
description: 'share: required; update: optional',
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'document body. share: required; update: optional',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (p) => {
|
||||||
|
const agentId = ctx.agentId;
|
||||||
|
if (!agentId)
|
||||||
|
return { ok: false, error: 'no agent context' };
|
||||||
|
const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
||||||
|
const ep = guild.endpoint;
|
||||||
|
switch (p.action) {
|
||||||
|
case 'read': {
|
||||||
|
const canvas = await client.getCanvas(ep, token, p.channelId);
|
||||||
|
return { ok: true, canvas };
|
||||||
|
}
|
||||||
|
case 'share': {
|
||||||
|
if (!p.title || !p.format || p.source === undefined) {
|
||||||
|
return { ok: false, error: 'share requires title, format, and source' };
|
||||||
|
}
|
||||||
|
const canvas = await client.shareCanvas(ep, token, p.channelId, {
|
||||||
|
title: p.title,
|
||||||
|
format: p.format,
|
||||||
|
source: p.source,
|
||||||
|
});
|
||||||
|
return { ok: true, canvas };
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const body = {};
|
||||||
|
if (p.title !== undefined)
|
||||||
|
body.title = p.title;
|
||||||
|
if (p.format !== undefined)
|
||||||
|
body.format = p.format;
|
||||||
|
if (p.source !== undefined)
|
||||||
|
body.source = p.source;
|
||||||
|
if (Object.keys(body).length === 0) {
|
||||||
|
return { ok: false, error: 'update needs at least one of title/format/source' };
|
||||||
|
}
|
||||||
|
const canvas = await client.updateCanvas(ep, token, p.channelId, body);
|
||||||
|
return { ok: true, canvas };
|
||||||
|
}
|
||||||
|
case 'close': {
|
||||||
|
await client.removeCanvas(ep, token, p.channelId);
|
||||||
|
return { ok: true, removed: true };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { ok: false, error: `unknown action ${String(p.action)}` };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,30 @@ export class FabricClient {
|
|||||||
return (await res.json()) as T;
|
return (await res.json()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generic JSON request (GET/PUT/PATCH/DELETE). Empty 2xx body -> null
|
||||||
|
// (Fabric returns an empty body when a channel has no canvas).
|
||||||
|
private async req<T>(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
auth?: string,
|
||||||
|
body?: unknown,
|
||||||
|
): Promise<T> {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
||||||
|
...(auth ? { authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`${method} ${url} -> ${res.status} ${text}`);
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
return (text ? JSON.parse(text) : null) as T;
|
||||||
|
}
|
||||||
|
|
||||||
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
// Exchange an agent API key for a Fabric user session (+ guild tokens).
|
||||||
agentLogin(apiKey: string): Promise<FabricSession> {
|
agentLogin(apiKey: string): Promise<FabricSession> {
|
||||||
return this.post<FabricSession>(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
return this.post<FabricSession>(`${this.centerApiBase}/auth/agent/login`, { apiKey });
|
||||||
@@ -86,4 +110,57 @@ export class FabricClient {
|
|||||||
joinChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise<unknown> {
|
joinChannel(guildEndpoint: string, guildToken: string, channelId: string): Promise<unknown> {
|
||||||
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
return this.post(`${guildEndpoint}/api/channels/${channelId}/join`, {}, guildToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- channel canvas (one pinned doc per channel) ----
|
||||||
|
|
||||||
|
private canvasUrl(endpoint: string, channelId: string): string {
|
||||||
|
return `${endpoint}/api/channels/${channelId}/canvas`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// null when the channel has no canvas
|
||||||
|
getCanvas(
|
||||||
|
endpoint: string,
|
||||||
|
token: string,
|
||||||
|
channelId: string,
|
||||||
|
): Promise<FabricCanvas | null> {
|
||||||
|
return this.req('GET', this.canvasUrl(endpoint, channelId), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// share / replace (caller becomes the sharer)
|
||||||
|
shareCanvas(
|
||||||
|
endpoint: string,
|
||||||
|
token: string,
|
||||||
|
channelId: string,
|
||||||
|
body: CanvasInput,
|
||||||
|
): Promise<FabricCanvas> {
|
||||||
|
return this.req('PUT', this.canvasUrl(endpoint, channelId), token, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update in place (original sharer only — else the guild returns 403)
|
||||||
|
updateCanvas(
|
||||||
|
endpoint: string,
|
||||||
|
token: string,
|
||||||
|
channelId: string,
|
||||||
|
body: Partial<CanvasInput>,
|
||||||
|
): Promise<FabricCanvas> {
|
||||||
|
return this.req('PATCH', this.canvasUrl(endpoint, channelId), token, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove ("close") the canvas (original sharer only)
|
||||||
|
removeCanvas(endpoint: string, token: string, channelId: string): Promise<unknown> {
|
||||||
|
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CanvasFormat = 'md' | 'html' | 'text';
|
||||||
|
export type CanvasInput = { title: string; format: CanvasFormat; source: string };
|
||||||
|
export type FabricCanvas = {
|
||||||
|
channelId: string;
|
||||||
|
sharerUserId: string;
|
||||||
|
title: string;
|
||||||
|
format: CanvasFormat;
|
||||||
|
source: string;
|
||||||
|
version: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|||||||
79
src/tools.ts
79
src/tools.ts
@@ -120,4 +120,83 @@ export function registerFabricTools(
|
|||||||
return { ok: true, closed: true };
|
return { ok: true, closed: true };
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// fabric-canvas: share / update / read / close the channel's single
|
||||||
|
// pinned canvas document (one tool, four actions). update/close are
|
||||||
|
// sharer-only server-side (the guild returns 403 otherwise).
|
||||||
|
api.registerTool((ctx: Ctx) => ({
|
||||||
|
name: 'fabric-canvas',
|
||||||
|
description:
|
||||||
|
"Manage a channel's pinned canvas document. action: " +
|
||||||
|
"read (current canvas or null) | share (create/replace; you become " +
|
||||||
|
'the sharer) | update (edit in place; sharer only) | close (remove; ' +
|
||||||
|
'sharer only).',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['action', 'guildNodeId', 'channelId'],
|
||||||
|
properties: {
|
||||||
|
action: { type: 'string', enum: ['read', 'share', 'update', 'close'] },
|
||||||
|
guildNodeId: { type: 'string' },
|
||||||
|
channelId: { type: 'string' },
|
||||||
|
title: { type: 'string', description: 'share: required; update: optional' },
|
||||||
|
format: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['md', 'html', 'text'],
|
||||||
|
description: 'share: required; update: optional',
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'document body. share: required; update: optional',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (p: {
|
||||||
|
action: 'read' | 'share' | 'update' | 'close';
|
||||||
|
guildNodeId: string;
|
||||||
|
channelId: string;
|
||||||
|
title?: string;
|
||||||
|
format?: 'md' | 'html' | 'text';
|
||||||
|
source?: string;
|
||||||
|
}) => {
|
||||||
|
const agentId = ctx.agentId;
|
||||||
|
if (!agentId) return { ok: false, error: 'no agent context' };
|
||||||
|
const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
||||||
|
const ep = guild.endpoint;
|
||||||
|
switch (p.action) {
|
||||||
|
case 'read': {
|
||||||
|
const canvas = await client.getCanvas(ep, token, p.channelId);
|
||||||
|
return { ok: true, canvas };
|
||||||
|
}
|
||||||
|
case 'share': {
|
||||||
|
if (!p.title || !p.format || p.source === undefined) {
|
||||||
|
return { ok: false, error: 'share requires title, format, and source' };
|
||||||
|
}
|
||||||
|
const canvas = await client.shareCanvas(ep, token, p.channelId, {
|
||||||
|
title: p.title,
|
||||||
|
format: p.format,
|
||||||
|
source: p.source,
|
||||||
|
});
|
||||||
|
return { ok: true, canvas };
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const body: Partial<{ title: string; format: 'md' | 'html' | 'text'; source: string }> = {};
|
||||||
|
if (p.title !== undefined) body.title = p.title;
|
||||||
|
if (p.format !== undefined) body.format = p.format;
|
||||||
|
if (p.source !== undefined) body.source = p.source;
|
||||||
|
if (Object.keys(body).length === 0) {
|
||||||
|
return { ok: false, error: 'update needs at least one of title/format/source' };
|
||||||
|
}
|
||||||
|
const canvas = await client.updateCanvas(ep, token, p.channelId, body);
|
||||||
|
return { ok: true, canvas };
|
||||||
|
}
|
||||||
|
case 'close': {
|
||||||
|
await client.removeCanvas(ep, token, p.channelId);
|
||||||
|
return { ok: true, removed: true };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return { ok: false, error: `unknown action ${String(p.action)}` };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user