@clawling/clawchat-plugin-openclaw 2026.9.11-1 → 2026.9.13-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/account-config-writes.js +67 -0
- package/dist/src/account-removal.js +44 -0
- package/dist/src/channel.js +30 -1
- package/dist/src/channel.setup.js +79 -42
- package/dist/src/commands.js +50 -27
- package/dist/src/config.js +83 -13
- package/dist/src/liveware-cli.js +49 -4
- package/dist/src/liveware-sample.js +16 -2
- package/dist/src/login.runtime.js +25 -29
- package/dist/src/outbound.js +4 -4
- package/dist/src/runtime.js +35 -32
- package/dist/src/tools.js +799 -685
- package/openclaw.plugin.json +150 -0
- package/package.json +1 -1
- package/src/account-config-writes.ts +74 -0
- package/src/account-removal.ts +52 -0
- package/src/channel.setup.ts +86 -43
- package/src/channel.ts +28 -1
- package/src/commands.ts +60 -26
- package/src/config.ts +113 -14
- package/src/liveware-cli.ts +52 -4
- package/src/liveware-sample.ts +24 -5
- package/src/login.runtime.ts +30 -26
- package/src/outbound.ts +4 -4
- package/src/runtime.ts +52 -36
- package/src/tools.ts +896 -768
package/openclaw.plugin.json
CHANGED
|
@@ -138,6 +138,81 @@
|
|
|
138
138
|
"timeout": { "type": "integer", "minimum": 100 },
|
|
139
139
|
"autoResendOnTimeout": { "type": "boolean" }
|
|
140
140
|
}
|
|
141
|
+
},
|
|
142
|
+
"defaultAccount": { "type": "string" },
|
|
143
|
+
"accounts": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"additionalProperties": false,
|
|
148
|
+
"properties": {
|
|
149
|
+
"enabled": { "type": "boolean" },
|
|
150
|
+
"websocketUrl": { "type": "string" },
|
|
151
|
+
"baseUrl": { "type": "string" },
|
|
152
|
+
"mediaBaseUrl": { "type": "string" },
|
|
153
|
+
"token": { "type": "string" },
|
|
154
|
+
"refreshToken": { "type": "string" },
|
|
155
|
+
"agentId": { "type": "string" },
|
|
156
|
+
"userId": { "type": "string" },
|
|
157
|
+
"ownerUserId": { "type": "string" },
|
|
158
|
+
"outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] },
|
|
159
|
+
"chats": {
|
|
160
|
+
"type": "object",
|
|
161
|
+
"additionalProperties": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"additionalProperties": false,
|
|
164
|
+
"properties": {
|
|
165
|
+
"outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"groupMode": { "type": "string", "enum": ["mention", "all"] },
|
|
170
|
+
"groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
|
|
171
|
+
"groups": {
|
|
172
|
+
"type": "object",
|
|
173
|
+
"additionalProperties": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"additionalProperties": false,
|
|
176
|
+
"properties": {
|
|
177
|
+
"groupMode": { "type": "string", "enum": ["mention", "all"] },
|
|
178
|
+
"groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
|
|
179
|
+
"outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
"forwardThinking": { "type": "boolean" },
|
|
184
|
+
"forwardToolCalls": { "type": "boolean" },
|
|
185
|
+
"richInteractions": { "type": "boolean" },
|
|
186
|
+
"awarenessNote": { "type": "boolean" },
|
|
187
|
+
"livewareSample": { "type": "boolean" },
|
|
188
|
+
"reconnect": {
|
|
189
|
+
"type": "object",
|
|
190
|
+
"additionalProperties": false,
|
|
191
|
+
"properties": {
|
|
192
|
+
"initialDelay": { "type": "integer", "minimum": 100 },
|
|
193
|
+
"maxDelay": { "type": "integer", "minimum": 100 },
|
|
194
|
+
"jitterRatio": { "type": "number", "minimum": 0 },
|
|
195
|
+
"maxRetries": { "type": "integer", "minimum": 0 }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"heartbeat": {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"additionalProperties": false,
|
|
201
|
+
"properties": {
|
|
202
|
+
"interval": { "type": "integer", "minimum": 1000 },
|
|
203
|
+
"timeout": { "type": "integer", "minimum": 1000 }
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
"ack": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"additionalProperties": false,
|
|
209
|
+
"properties": {
|
|
210
|
+
"timeout": { "type": "integer", "minimum": 100 },
|
|
211
|
+
"autoResendOnTimeout": { "type": "boolean" }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
141
216
|
}
|
|
142
217
|
}
|
|
143
218
|
},
|
|
@@ -213,6 +288,81 @@
|
|
|
213
288
|
"timeout": { "type": "integer", "minimum": 100 },
|
|
214
289
|
"autoResendOnTimeout": { "type": "boolean" }
|
|
215
290
|
}
|
|
291
|
+
},
|
|
292
|
+
"defaultAccount": { "type": "string" },
|
|
293
|
+
"accounts": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"additionalProperties": {
|
|
296
|
+
"type": "object",
|
|
297
|
+
"additionalProperties": false,
|
|
298
|
+
"properties": {
|
|
299
|
+
"enabled": { "type": "boolean" },
|
|
300
|
+
"websocketUrl": { "type": "string" },
|
|
301
|
+
"baseUrl": { "type": "string" },
|
|
302
|
+
"mediaBaseUrl": { "type": "string" },
|
|
303
|
+
"token": { "type": "string" },
|
|
304
|
+
"refreshToken": { "type": "string" },
|
|
305
|
+
"agentId": { "type": "string" },
|
|
306
|
+
"userId": { "type": "string" },
|
|
307
|
+
"ownerUserId": { "type": "string" },
|
|
308
|
+
"outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] },
|
|
309
|
+
"chats": {
|
|
310
|
+
"type": "object",
|
|
311
|
+
"additionalProperties": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"additionalProperties": false,
|
|
314
|
+
"properties": {
|
|
315
|
+
"outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
"groupMode": { "type": "string", "enum": ["mention", "all"] },
|
|
320
|
+
"groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
|
|
321
|
+
"groups": {
|
|
322
|
+
"type": "object",
|
|
323
|
+
"additionalProperties": {
|
|
324
|
+
"type": "object",
|
|
325
|
+
"additionalProperties": false,
|
|
326
|
+
"properties": {
|
|
327
|
+
"groupMode": { "type": "string", "enum": ["mention", "all"] },
|
|
328
|
+
"groupCommandMode": { "type": "string", "enum": ["owner", "all", "off"] },
|
|
329
|
+
"outputVisibility": { "type": "string", "enum": ["minimal", "normal", "full"] }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
"forwardThinking": { "type": "boolean" },
|
|
334
|
+
"forwardToolCalls": { "type": "boolean" },
|
|
335
|
+
"richInteractions": { "type": "boolean" },
|
|
336
|
+
"awarenessNote": { "type": "boolean" },
|
|
337
|
+
"livewareSample": { "type": "boolean" },
|
|
338
|
+
"reconnect": {
|
|
339
|
+
"type": "object",
|
|
340
|
+
"additionalProperties": false,
|
|
341
|
+
"properties": {
|
|
342
|
+
"initialDelay": { "type": "integer", "minimum": 100 },
|
|
343
|
+
"maxDelay": { "type": "integer", "minimum": 100 },
|
|
344
|
+
"jitterRatio": { "type": "number", "minimum": 0 },
|
|
345
|
+
"maxRetries": { "type": "integer", "minimum": 0 }
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
"heartbeat": {
|
|
349
|
+
"type": "object",
|
|
350
|
+
"additionalProperties": false,
|
|
351
|
+
"properties": {
|
|
352
|
+
"interval": { "type": "integer", "minimum": 1000 },
|
|
353
|
+
"timeout": { "type": "integer", "minimum": 1000 }
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
"ack": {
|
|
357
|
+
"type": "object",
|
|
358
|
+
"additionalProperties": false,
|
|
359
|
+
"properties": {
|
|
360
|
+
"timeout": { "type": "integer", "minimum": 100 },
|
|
361
|
+
"autoResendOnTimeout": { "type": "boolean" }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
216
366
|
}
|
|
217
367
|
}
|
|
218
368
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
3
|
+
import { CHANNEL_ID, normalizeOpenclawClawlingAccountId } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
function readRecord(value: unknown): Record<string, unknown> {
|
|
6
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? (value as Record<string, unknown>)
|
|
8
|
+
: {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findAccountKey(accounts: Record<string, unknown>, accountId: string): string | undefined {
|
|
12
|
+
return Object.keys(accounts).find((key) => normalizeOpenclawClawlingAccountId(key) === accountId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function applyPatch(target: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
|
16
|
+
const next: Record<string, unknown> = { ...target };
|
|
17
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
18
|
+
if (value === undefined) delete next[key];
|
|
19
|
+
else next[key] = value;
|
|
20
|
+
}
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Dotted config path of the slot {@link patchOpenclawClawlingAccountSection}
|
|
26
|
+
* writes `accountId` into, for operator-facing logs. Default:
|
|
27
|
+
* `channels.<channel>` — or `channels.<channel>.accounts.default` once the
|
|
28
|
+
* host's single-account promotion created that entry. Named:
|
|
29
|
+
* `channels.<channel>.accounts.<key>` with the existing key spelling.
|
|
30
|
+
*/
|
|
31
|
+
export function openclawClawlingAccountSlotPath(cfg: OpenClawConfig, accountId?: string | null): string {
|
|
32
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
33
|
+
const section = readRecord(readRecord((cfg as { channels?: unknown }).channels)[CHANNEL_ID]);
|
|
34
|
+
const key = findAccountKey(readRecord(section.accounts), id);
|
|
35
|
+
const base = `channels.${CHANNEL_ID}`;
|
|
36
|
+
if (key !== undefined) return `${base}.accounts.${key}`;
|
|
37
|
+
return id === DEFAULT_ACCOUNT_ID ? base : `${base}.accounts.${id}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The single config write point. Pure. Default → root section (or
|
|
42
|
+
* `accounts.default` when that entry exists, so a host-promoted token is never
|
|
43
|
+
* shadowed by a stale copy); named → `accounts.<id>` keeping the existing key
|
|
44
|
+
* spelling. A patch value of `undefined` deletes the key. `enabled` is only
|
|
45
|
+
* touched when the patch carries it.
|
|
46
|
+
*/
|
|
47
|
+
export function patchOpenclawClawlingAccountSection(
|
|
48
|
+
cfg: OpenClawConfig,
|
|
49
|
+
accountId: string | null | undefined,
|
|
50
|
+
patch: Record<string, unknown>,
|
|
51
|
+
): OpenClawConfig {
|
|
52
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
53
|
+
const channels = readRecord((cfg as { channels?: unknown }).channels);
|
|
54
|
+
const section = readRecord(channels[CHANNEL_ID]);
|
|
55
|
+
const accounts = readRecord(section.accounts);
|
|
56
|
+
const existingKey = findAccountKey(accounts, id);
|
|
57
|
+
if (id === DEFAULT_ACCOUNT_ID && existingKey === undefined) {
|
|
58
|
+
return {
|
|
59
|
+
...cfg,
|
|
60
|
+
channels: { ...channels, [CHANNEL_ID]: applyPatch(section, patch) },
|
|
61
|
+
} as OpenClawConfig;
|
|
62
|
+
}
|
|
63
|
+
const key = existingKey ?? id;
|
|
64
|
+
return {
|
|
65
|
+
...cfg,
|
|
66
|
+
channels: {
|
|
67
|
+
...channels,
|
|
68
|
+
[CHANNEL_ID]: {
|
|
69
|
+
...section,
|
|
70
|
+
accounts: { ...accounts, [key]: applyPatch(readRecord(accounts[key]), patch) },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
} as OpenClawConfig;
|
|
74
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { normalizeOpenclawClawlingAccountId } from "./config.ts";
|
|
3
|
+
import { livewareNamedAccountRootDir } from "./liveware-cli.ts";
|
|
4
|
+
import type { ClawChatStore } from "./storage.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `openclaw channels remove --account <id> [--delete]` — the host has already
|
|
8
|
+
* rewritten the config (hybrid adapter) and stopped the account. Blank that
|
|
9
|
+
* account's SQLite credentials so a later start of the same id cannot resume
|
|
10
|
+
* the old identity from the row. Identity columns are kept (re-pair mode).
|
|
11
|
+
*
|
|
12
|
+
* A NAMED account also owns a private liveware root
|
|
13
|
+
* (`<stateDir>/clawchat/accounts/<id>/`: the CLI home holding the
|
|
14
|
+
* `$HOME/.clawling` login session, and the sample tree); the whole root is
|
|
15
|
+
* removed so the identity does not linger on disk. The `default` account's
|
|
16
|
+
* paths are the shared pre-multi-account layout and are never touched here.
|
|
17
|
+
* The host stops the account before this hook (best effort), so a still-open
|
|
18
|
+
* file only costs a log line.
|
|
19
|
+
*/
|
|
20
|
+
export function clearOpenclawClawlingAccountOnRemoval(params: {
|
|
21
|
+
accountId: string;
|
|
22
|
+
store: Pick<ClawChatStore, "clearActivationCredentials"> | null;
|
|
23
|
+
/** Resolved OpenClaw state dir; absent → liveware directories are left alone. */
|
|
24
|
+
stateDir?: string | null;
|
|
25
|
+
log?: (message: string) => void;
|
|
26
|
+
rmFn?: (dir: string) => void;
|
|
27
|
+
}): void {
|
|
28
|
+
const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
|
|
29
|
+
if (!params.store) {
|
|
30
|
+
params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite unavailable, nothing to clear`);
|
|
31
|
+
} else {
|
|
32
|
+
try {
|
|
33
|
+
params.store.clearActivationCredentials({ platform: "openclaw", accountId });
|
|
34
|
+
params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite credentials cleared`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
params.log?.(
|
|
37
|
+
`[${accountId}] clawchat-plugin-openclaw account removed; sqlite clear failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!params.stateDir) return;
|
|
42
|
+
const dir = livewareNamedAccountRootDir(params.stateDir, accountId);
|
|
43
|
+
if (dir === null) return;
|
|
44
|
+
const rm = params.rmFn ?? ((target: string) => fs.rmSync(target, { recursive: true, force: true }));
|
|
45
|
+
try {
|
|
46
|
+
rm(dir);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
params.log?.(
|
|
49
|
+
`[${accountId}] clawchat-plugin-openclaw account removed; liveware directory ${dir} not removed: ${err instanceof Error ? err.message : String(err)}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/channel.setup.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
2
2
|
import type { ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
|
|
3
3
|
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
|
5
|
+
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
|
|
5
6
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
6
7
|
import {
|
|
7
8
|
createComputedAccountStatusAdapter,
|
|
@@ -11,79 +12,121 @@ import {
|
|
|
11
12
|
CHANNEL_ID,
|
|
12
13
|
canStartOpenclawClawlingAccount,
|
|
13
14
|
listOpenclawClawlingAccountIds,
|
|
15
|
+
normalizeOpenclawClawlingAccountId,
|
|
14
16
|
openclawClawlingConfigSchema,
|
|
15
17
|
resolveOpenclawClawlingAccount,
|
|
18
|
+
resolveOpenclawClawlingDefaultAccountId,
|
|
16
19
|
type ResolvedOpenclawClawlingAccount,
|
|
17
20
|
} from "./config.ts";
|
|
18
21
|
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
/** Keys a `channels remove --account default --delete` clears from the section. */
|
|
24
|
+
const CLEAR_BASE_FIELDS = [
|
|
25
|
+
"websocketUrl",
|
|
26
|
+
"baseUrl",
|
|
27
|
+
"token",
|
|
28
|
+
"refreshToken",
|
|
29
|
+
"agentId",
|
|
30
|
+
"userId",
|
|
31
|
+
"ownerUserId",
|
|
32
|
+
"forwardThinking",
|
|
33
|
+
"forwardToolCalls",
|
|
34
|
+
"richInteractions",
|
|
35
|
+
"enabled",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hybrid layout: the channel root IS the `default` account (and the
|
|
40
|
+
* inheritance base); named accounts live under `accounts.<id>`. The SDK
|
|
41
|
+
* adapter branches `setAccountEnabled` / `deleteAccount` on the id for us.
|
|
42
|
+
*/
|
|
43
|
+
const hybridConfigAdapter = createHybridChannelConfigAdapter<ResolvedOpenclawClawlingAccount>({
|
|
21
44
|
sectionKey: CHANNEL_ID,
|
|
22
|
-
resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
|
|
23
|
-
listAccountIds: () => listOpenclawClawlingAccountIds(),
|
|
24
|
-
defaultAccountId: () =>
|
|
25
|
-
|
|
26
|
-
clearBaseFields: [
|
|
27
|
-
"websocketUrl",
|
|
28
|
-
"baseUrl",
|
|
29
|
-
"token",
|
|
30
|
-
"userId",
|
|
31
|
-
"forwardThinking",
|
|
32
|
-
"forwardToolCalls",
|
|
33
|
-
"richInteractions",
|
|
34
|
-
"enabled",
|
|
35
|
-
],
|
|
45
|
+
resolveAccount: (cfg, accountId) => resolveOpenclawClawlingAccount(cfg, accountId),
|
|
46
|
+
listAccountIds: (cfg) => listOpenclawClawlingAccountIds(cfg),
|
|
47
|
+
defaultAccountId: (cfg) => resolveOpenclawClawlingDefaultAccountId(cfg),
|
|
48
|
+
preserveSectionOnDefaultDelete: true,
|
|
49
|
+
clearBaseFields: [...CLEAR_BASE_FIELDS],
|
|
36
50
|
resolveAllowFrom: (account) => account.allowFrom,
|
|
37
51
|
formatAllowFrom: () => [],
|
|
38
52
|
});
|
|
39
53
|
|
|
54
|
+
/** `{ token: undefined, refreshToken: undefined, … }` — `undefined` deletes. */
|
|
55
|
+
const CLEAR_BASE_FIELDS_PATCH: Record<string, undefined> = Object.fromEntries(
|
|
56
|
+
CLEAR_BASE_FIELDS.map((key) => [key, undefined]),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const configAdapter: typeof hybridConfigAdapter = {
|
|
60
|
+
...hybridConfigAdapter,
|
|
61
|
+
/**
|
|
62
|
+
* The SDK's default branch clears `clearBaseFields` from the ROOT only. Once
|
|
63
|
+
* the host's single-account promotion has moved the identity into
|
|
64
|
+
* `accounts.default`, that leaves the deleted default fully credentialed:
|
|
65
|
+
* the default reader merges the promoted entry back over the blanked root,
|
|
66
|
+
* `ownerUserId` returns via the token's `oid` claim, and the account resolves
|
|
67
|
+
* `configured: true` again on the next gateway start. Clear the same keys
|
|
68
|
+
* from the promoted entry so `--delete` actually deletes. Named ids are the
|
|
69
|
+
* SDK's alone — it removes the whole `accounts.<id>` entry.
|
|
70
|
+
*
|
|
71
|
+
* Guard: a config with no channel section has nothing to clear, and the
|
|
72
|
+
* patch write would otherwise materialize an empty section out of nothing.
|
|
73
|
+
*/
|
|
74
|
+
deleteAccount: (args) => {
|
|
75
|
+
const next = hybridConfigAdapter.deleteAccount?.(args) ?? args.cfg;
|
|
76
|
+
if (normalizeOpenclawClawlingAccountId(args.accountId) !== DEFAULT_ACCOUNT_ID) return next;
|
|
77
|
+
if (!hasOpenclawClawlingChannelSection(next)) return next;
|
|
78
|
+
return patchOpenclawClawlingAccountSection(next, DEFAULT_ACCOUNT_ID, CLEAR_BASE_FIELDS_PATCH);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function hasOpenclawClawlingChannelSection(cfg: OpenClawConfig): boolean {
|
|
83
|
+
const section = (cfg as { channels?: Record<string, unknown> }).channels?.[CHANNEL_ID];
|
|
84
|
+
return Boolean(section) && typeof section === "object" && !Array.isArray(section);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readInviteCode(input: ChannelSetupInput): string {
|
|
88
|
+
return typeof input.code === "string" && input.code.trim()
|
|
89
|
+
? input.code.trim()
|
|
90
|
+
: typeof input.token === "string"
|
|
91
|
+
? input.token.trim()
|
|
92
|
+
: "";
|
|
93
|
+
}
|
|
94
|
+
|
|
40
95
|
/**
|
|
41
96
|
* Invite-code setup adapter used by OpenClaw setup surfaces.
|
|
42
97
|
*
|
|
43
|
-
* `channels add --token
|
|
44
|
-
*
|
|
45
|
-
*
|
|
98
|
+
* `channels add [--account <id>] --token <code>` passes the invite code as
|
|
99
|
+
* setup input and the (optional) account id from the host. The setup write
|
|
100
|
+
* leaves channel config unchanged; `afterAccountConfigWritten` exchanges the
|
|
101
|
+
* invite code and persists credentials for THAT account through the host
|
|
102
|
+
* runtime mutator.
|
|
46
103
|
*/
|
|
47
104
|
const setupAdapter: NonNullable<ChannelPlugin<ResolvedOpenclawClawlingAccount>["setup"]> = {
|
|
48
|
-
resolveAccountId: () =>
|
|
49
|
-
validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) =>
|
|
50
|
-
|
|
51
|
-
typeof input.code === "string" && input.code.trim()
|
|
52
|
-
? input.code.trim()
|
|
53
|
-
: typeof input.token === "string"
|
|
54
|
-
? input.token.trim()
|
|
55
|
-
: "";
|
|
56
|
-
if (!inviteCode) {
|
|
57
|
-
return "ClawChat invite code is required.";
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
},
|
|
105
|
+
resolveAccountId: ({ accountId }) => normalizeOpenclawClawlingAccountId(accountId),
|
|
106
|
+
validateInput: ({ input }: { cfg: OpenClawConfig; accountId: string; input: ChannelSetupInput }) =>
|
|
107
|
+
readInviteCode(input) ? null : "ClawChat invite code is required.",
|
|
61
108
|
applyAccountConfig: ({ cfg }: {
|
|
62
109
|
cfg: OpenClawConfig;
|
|
63
110
|
accountId: string;
|
|
64
111
|
input: ChannelSetupInput;
|
|
65
112
|
}) => cfg,
|
|
66
|
-
afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
? input.code.trim()
|
|
71
|
-
: typeof input.token === "string"
|
|
72
|
-
? input.token.trim()
|
|
73
|
-
: "";
|
|
113
|
+
afterAccountConfigWritten: async ({ cfg, accountId, input, runtime }) => {
|
|
114
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
115
|
+
runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten invoked`);
|
|
116
|
+
const code = readInviteCode(input);
|
|
74
117
|
if (!code) {
|
|
75
|
-
runtime.log(
|
|
118
|
+
runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten skipped: empty invite code`);
|
|
76
119
|
return;
|
|
77
120
|
}
|
|
78
121
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
|
|
79
122
|
await runOpenclawClawlingLogin({
|
|
80
123
|
cfg,
|
|
81
|
-
accountId:
|
|
124
|
+
accountId: id,
|
|
82
125
|
runtime: { log: (message: string) => runtime.log(message) },
|
|
83
126
|
readInviteCode: async () => code,
|
|
84
127
|
mutateConfigFile: mutateConfigFile as OpenclawClawchatMutateConfigFile,
|
|
85
128
|
});
|
|
86
|
-
runtime.log(
|
|
129
|
+
runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten completed`);
|
|
87
130
|
},
|
|
88
131
|
};
|
|
89
132
|
|
package/src/channel.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createChatChannelPlugin, type ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
3
|
+
import { clearOpenclawClawlingAccountOnRemoval } from "./account-removal.ts";
|
|
3
4
|
import {
|
|
4
5
|
CHANNEL_ID,
|
|
5
6
|
canStartOpenclawClawlingAccount,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
10
11
|
import { openclawClawlingOutbound } from "./outbound.ts";
|
|
11
12
|
import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.ts";
|
|
13
|
+
import { clawChatDbPathForStateDir, getClawChatStore } from "./storage.ts";
|
|
12
14
|
import { openclawClawlingSetupPlugin } from "./channel.setup.ts";
|
|
13
15
|
import { getClawChatPlatformPrompt } from "./plugin-prompts.ts";
|
|
14
16
|
|
|
@@ -33,9 +35,34 @@ export const openclawClawlingPlugin: ChannelPlugin<ResolvedOpenclawClawlingAccou
|
|
|
33
35
|
});
|
|
34
36
|
},
|
|
35
37
|
},
|
|
38
|
+
lifecycle: {
|
|
39
|
+
onAccountRemoved: ({ accountId, runtime }) => {
|
|
40
|
+
let store: ReturnType<typeof getClawChatStore> | null = null;
|
|
41
|
+
let stateDir: string | undefined;
|
|
42
|
+
try {
|
|
43
|
+
stateDir = getOpenclawClawlingRuntime().state?.resolveStateDir?.();
|
|
44
|
+
} catch {
|
|
45
|
+
stateDir = undefined;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
store = getClawChatStore({
|
|
49
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
50
|
+
log: { error: (message: string) => runtime.error(message) },
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
store = null;
|
|
54
|
+
}
|
|
55
|
+
clearOpenclawClawlingAccountOnRemoval({
|
|
56
|
+
accountId,
|
|
57
|
+
store,
|
|
58
|
+
stateDir,
|
|
59
|
+
log: (message) => runtime.log(message),
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
},
|
|
36
63
|
gateway: {
|
|
37
64
|
startAccount: async (ctx) => {
|
|
38
|
-
const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
|
|
65
|
+
const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg, ctx.accountId);
|
|
39
66
|
ctx.log?.info?.(
|
|
40
67
|
`[${account.accountId}] clawchat-plugin-openclaw lifecycle START_ACCOUNT_CALLED configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`,
|
|
41
68
|
);
|
package/src/commands.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import type { OpenClawConfig, PluginCommandContext } from "openclaw/plugin-sdk/core";
|
|
3
|
-
import {
|
|
3
|
+
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
|
|
4
|
+
import {
|
|
5
|
+
CHANNEL_ID,
|
|
6
|
+
normalizeOpenclawClawlingAccountId,
|
|
7
|
+
readOpenclawClawlingAccountSection,
|
|
8
|
+
type OutputVisibility,
|
|
9
|
+
} from "./config.ts";
|
|
4
10
|
import type { OpenclawClawchatMutateConfigFile } from "./login.runtime.ts";
|
|
5
11
|
|
|
6
12
|
/** Activation intent flags — the two escape hatches out of the live-activation guard. */
|
|
@@ -10,6 +16,32 @@ function stripActivationFlags(raw: string): string {
|
|
|
10
16
|
return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
/** `--account <id>` / `--account=<id>`; the value is removed before the code scan so it can never be mistaken for a code. */
|
|
20
|
+
const ACCOUNT_FLAG_PATTERN = /(?:^|\s)--account(?:=|\s+)(\S+)/u;
|
|
21
|
+
/** `--account` with nothing after it — including a dangling `--account=`, which
|
|
22
|
+
* is a typo rather than "use the default account". */
|
|
23
|
+
const BARE_ACCOUNT_FLAG_PATTERN = /(?:^|\s)--account=?(?=\s|$)/u;
|
|
24
|
+
|
|
25
|
+
function extractAccountFlag(value: unknown): { accountId: string | null; rest: string; error: string | null } {
|
|
26
|
+
const raw = typeof value === "string" ? value : "";
|
|
27
|
+
const match = ACCOUNT_FLAG_PATTERN.exec(raw);
|
|
28
|
+
if (match) {
|
|
29
|
+
const id = match[1] ?? "";
|
|
30
|
+
if (!id || id.startsWith("--")) {
|
|
31
|
+
return { accountId: null, rest: raw, error: "--account requires an account id" };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
accountId: normalizeOpenclawClawlingAccountId(id),
|
|
35
|
+
rest: raw.replace(match[0], " "),
|
|
36
|
+
error: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (BARE_ACCOUNT_FLAG_PATTERN.test(raw)) {
|
|
40
|
+
return { accountId: null, rest: raw, error: "--account requires an account id" };
|
|
41
|
+
}
|
|
42
|
+
return { accountId: null, rest: raw, error: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
/**
|
|
14
46
|
* A whole argument token that looks like a ClawChat connect code.
|
|
15
47
|
*
|
|
@@ -95,29 +127,25 @@ function resolveCommandChatId(ctx: PluginCommandContext): string {
|
|
|
95
127
|
|
|
96
128
|
function persistOutputVisibility(
|
|
97
129
|
draft: OpenClawConfig,
|
|
130
|
+
accountId: string,
|
|
98
131
|
chatId: string,
|
|
99
132
|
outputVisibility: OutputVisibility,
|
|
100
133
|
): void {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
134
|
+
// Seeded from the MERGED view, never the raw target entry: `mergeAccountConfig`
|
|
135
|
+
// REPLACES `chats` rather than deep-merging it, so a first write seeded from an
|
|
136
|
+
// entry that has no `chats` of its own (a named account, or a `default` the
|
|
137
|
+
// host has promoted into `accounts.default`) would shadow every inherited
|
|
138
|
+
// conversation. Copying the inherited map forward and overriding one key keeps
|
|
139
|
+
// the resolved view additive.
|
|
140
|
+
const merged = readOpenclawClawlingAccountSection(draft, accountId);
|
|
141
|
+
const chats = readRecord(merged.chats);
|
|
104
142
|
const chat = readRecord(chats[chatId]);
|
|
105
|
-
Object.assign(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
...
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
chats: {
|
|
112
|
-
...chats,
|
|
113
|
-
[chatId]: {
|
|
114
|
-
...chat,
|
|
115
|
-
outputVisibility,
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
});
|
|
143
|
+
Object.assign(
|
|
144
|
+
draft,
|
|
145
|
+
patchOpenclawClawlingAccountSection(draft, accountId, {
|
|
146
|
+
chats: { ...chats, [chatId]: { ...chat, outputVisibility } },
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
121
149
|
}
|
|
122
150
|
|
|
123
151
|
function formatOutputVisibilityResult(outputVisibility: OutputVisibility): string {
|
|
@@ -139,21 +167,26 @@ function formatOutputVisibilityResult(outputVisibility: OutputVisibility): strin
|
|
|
139
167
|
export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "registerCommand" | "logger" | "runtime">): void {
|
|
140
168
|
api.registerCommand({
|
|
141
169
|
name: "clawchat-activate",
|
|
142
|
-
description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP.",
|
|
170
|
+
description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP [--account <id>].",
|
|
143
171
|
acceptsArgs: true,
|
|
144
172
|
requireAuth: true,
|
|
145
173
|
async handler(ctx) {
|
|
146
174
|
const rawArgs = ctx.args ?? ctx.commandBody;
|
|
147
|
-
const
|
|
175
|
+
const accountFlag = extractAccountFlag(rawArgs);
|
|
176
|
+
if (accountFlag.error) {
|
|
177
|
+
return { text: `${accountFlag.error}. Usage: /clawchat-activate K7RM4TQP [--account <id>]` };
|
|
178
|
+
}
|
|
179
|
+
const code = extractInviteCode(accountFlag.rest);
|
|
148
180
|
if (!code) {
|
|
149
|
-
return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP" };
|
|
181
|
+
return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP [--account <id>]" };
|
|
150
182
|
}
|
|
151
|
-
const intent = extractActivationIntent(
|
|
183
|
+
const intent = extractActivationIntent(accountFlag.rest);
|
|
184
|
+
const accountId = accountFlag.accountId ?? normalizeOpenclawClawlingAccountId(ctx.accountId);
|
|
152
185
|
try {
|
|
153
186
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.ts");
|
|
154
187
|
await runOpenclawClawlingLogin({
|
|
155
188
|
cfg: ctx.config,
|
|
156
|
-
accountId
|
|
189
|
+
accountId,
|
|
157
190
|
runtime: { log: (message: string) => api.logger?.info?.(message) },
|
|
158
191
|
readInviteCode: async () => code,
|
|
159
192
|
mutateConfigFile: (api.runtime.config as unknown as {
|
|
@@ -189,10 +222,11 @@ export function registerOpenclawClawlingCommands(api: Pick<OpenClawPluginApi, "r
|
|
|
189
222
|
if (!mutateConfigFile) {
|
|
190
223
|
return { text: "OpenClaw runtime config mutation is unavailable for /clawchat-output." };
|
|
191
224
|
}
|
|
225
|
+
const accountId = normalizeOpenclawClawlingAccountId(ctx.accountId);
|
|
192
226
|
await mutateConfigFile({
|
|
193
227
|
afterWrite: { mode: "auto" },
|
|
194
228
|
mutate(draft) {
|
|
195
|
-
persistOutputVisibility(draft, chatId, outputVisibility);
|
|
229
|
+
persistOutputVisibility(draft, accountId, chatId, outputVisibility);
|
|
196
230
|
},
|
|
197
231
|
});
|
|
198
232
|
return { text: formatOutputVisibilityResult(outputVisibility) };
|