@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/src/config.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAccountListHelpers,
|
|
3
|
+
mergeAccountConfig,
|
|
4
|
+
normalizeAccountId,
|
|
5
|
+
resolveNormalizedAccountEntry,
|
|
6
|
+
} from "openclaw/plugin-sdk/account-core";
|
|
1
7
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
2
8
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
3
9
|
|
|
@@ -85,7 +91,7 @@ export type OpenclawClawlingAckConfig = {
|
|
|
85
91
|
autoResendOnTimeout?: boolean;
|
|
86
92
|
};
|
|
87
93
|
|
|
88
|
-
export type
|
|
94
|
+
export type OpenclawClawlingAccountConfig = {
|
|
89
95
|
enabled?: boolean;
|
|
90
96
|
websocketUrl?: string;
|
|
91
97
|
baseUrl?: string;
|
|
@@ -115,7 +121,18 @@ export type OpenclawClawlingConfig = {
|
|
|
115
121
|
ack?: OpenclawClawlingAckConfig;
|
|
116
122
|
};
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Channel section = the `default` account (and the inheritance base for every
|
|
126
|
+
* named account) plus the account map. Named accounts live under
|
|
127
|
+
* `accounts.<id>` and accept every key except the two map keys.
|
|
128
|
+
*/
|
|
129
|
+
export type OpenclawClawlingConfig = OpenclawClawlingAccountConfig & {
|
|
130
|
+
/** Which account `channels list/status` treats as default when several exist. */
|
|
131
|
+
defaultAccount?: string;
|
|
132
|
+
accounts?: Record<string, OpenclawClawlingAccountConfig>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const openclawClawlingAccountConfigSchema = {
|
|
119
136
|
type: "object",
|
|
120
137
|
additionalProperties: false,
|
|
121
138
|
properties: {
|
|
@@ -187,6 +204,19 @@ export const openclawClawlingConfigSchema = {
|
|
|
187
204
|
},
|
|
188
205
|
} as const;
|
|
189
206
|
|
|
207
|
+
export const openclawClawlingConfigSchema = {
|
|
208
|
+
type: "object",
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
properties: {
|
|
211
|
+
...openclawClawlingAccountConfigSchema.properties,
|
|
212
|
+
defaultAccount: { type: "string" },
|
|
213
|
+
accounts: {
|
|
214
|
+
type: "object",
|
|
215
|
+
additionalProperties: openclawClawlingAccountConfigSchema,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
} as const;
|
|
219
|
+
|
|
190
220
|
function isOpenclawClawchatToolAllowEntry(entry: unknown): boolean {
|
|
191
221
|
return entry === CHANNEL_ID || entry === "group:plugins";
|
|
192
222
|
}
|
|
@@ -309,6 +339,66 @@ function readChannelSection(cfg: OpenClawConfig): Record<string, unknown> {
|
|
|
309
339
|
return channel && typeof channel === "object" ? (channel as Record<string, unknown>) : {};
|
|
310
340
|
}
|
|
311
341
|
|
|
342
|
+
/** Keys that identify a ClawChat account. Never inherited by a named account from the root. */
|
|
343
|
+
export const ACCOUNT_CREDENTIAL_KEYS = ["token", "refreshToken", "agentId", "userId", "ownerUserId"] as const;
|
|
344
|
+
const ACCOUNT_MAP_KEYS = ["accounts", "defaultAccount"] as const;
|
|
345
|
+
const NESTED_ACCOUNT_OBJECT_KEYS = ["reconnect", "heartbeat", "ack"] as const;
|
|
346
|
+
|
|
347
|
+
/** Host-identical account id normalization (lowercase, `[^a-z0-9_-]` → `-`, blank → `default`). */
|
|
348
|
+
export function normalizeOpenclawClawlingAccountId(value?: string | null): string {
|
|
349
|
+
return normalizeAccountId(value);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function readAccountsMap(section: Record<string, unknown>): Record<string, Record<string, unknown>> | undefined {
|
|
353
|
+
const accounts = section.accounts;
|
|
354
|
+
return accounts && typeof accounts === "object" && !Array.isArray(accounts)
|
|
355
|
+
? (accounts as Record<string, Record<string, unknown>>)
|
|
356
|
+
: undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The single config read point. Root = the `default` account and the
|
|
361
|
+
* inheritance base; `accounts.<id>` overrides it. Credential keys are omitted
|
|
362
|
+
* from the base for named ids so an empty `accounts.work: {}` is unconfigured
|
|
363
|
+
* rather than a clone of the default identity. `accounts.default`, when the
|
|
364
|
+
* host's single-account promotion created one, merges over the root.
|
|
365
|
+
*/
|
|
366
|
+
export function readOpenclawClawlingAccountSection(
|
|
367
|
+
cfg: OpenClawConfig,
|
|
368
|
+
accountId?: string | null,
|
|
369
|
+
): Record<string, unknown> {
|
|
370
|
+
const root = readChannelSection(cfg);
|
|
371
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
372
|
+
const resolved = resolveNormalizedAccountEntry(readAccountsMap(root), id, normalizeAccountId);
|
|
373
|
+
// The schema says every entry is an object; the file on disk may disagree
|
|
374
|
+
// (a hand-edited `accounts.work: "work"`). Spreading a non-record merges it
|
|
375
|
+
// in character by character, so treat it as absent.
|
|
376
|
+
const entry =
|
|
377
|
+
resolved && typeof resolved === "object" && !Array.isArray(resolved) ? resolved : undefined;
|
|
378
|
+
const omitKeys: string[] = [...ACCOUNT_MAP_KEYS];
|
|
379
|
+
if (id !== DEFAULT_ACCOUNT_ID) omitKeys.push(...ACCOUNT_CREDENTIAL_KEYS);
|
|
380
|
+
return mergeAccountConfig<Record<string, unknown>>({
|
|
381
|
+
channelConfig: root,
|
|
382
|
+
accountConfig: entry ?? {},
|
|
383
|
+
omitKeys,
|
|
384
|
+
nestedObjectKeys: [...NESTED_ACCOUNT_OBJECT_KEYS],
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const accountListHelpers = createAccountListHelpers(CHANNEL_ID, {
|
|
389
|
+
normalizeAccountId,
|
|
390
|
+
// The root section is always an account, even when unconfigured.
|
|
391
|
+
hasImplicitDefaultAccount: () => true,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
export function listOpenclawClawlingAccountIds(cfg: OpenClawConfig): string[] {
|
|
395
|
+
return accountListHelpers.listAccountIds(cfg);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function resolveOpenclawClawlingDefaultAccountId(cfg: OpenClawConfig): string {
|
|
399
|
+
return accountListHelpers.resolveDefaultAccountId(cfg);
|
|
400
|
+
}
|
|
401
|
+
|
|
312
402
|
function readOptionalString(value: unknown): string {
|
|
313
403
|
return typeof value === "string" ? value.trim() : "";
|
|
314
404
|
}
|
|
@@ -501,9 +591,19 @@ function readAck(raw: unknown): Required<OpenclawClawlingAckConfig> {
|
|
|
501
591
|
|
|
502
592
|
export function resolveOpenclawClawlingAccount(
|
|
503
593
|
cfg: OpenClawConfig,
|
|
594
|
+
accountId?: string | null,
|
|
504
595
|
env: Record<string, string | undefined> = process.env,
|
|
505
596
|
): ResolvedOpenclawClawlingAccount {
|
|
506
|
-
const
|
|
597
|
+
const resolvedAccountId =
|
|
598
|
+
accountId === undefined || accountId === null || !accountId.trim()
|
|
599
|
+
? resolveOpenclawClawlingDefaultAccountId(cfg)
|
|
600
|
+
: normalizeOpenclawClawlingAccountId(accountId);
|
|
601
|
+
const isDefault = resolvedAccountId === DEFAULT_ACCOUNT_ID;
|
|
602
|
+
const root = readChannelSection(cfg);
|
|
603
|
+
const channel = readOpenclawClawlingAccountSection(cfg, resolvedAccountId);
|
|
604
|
+
// Credential env vars are process-wide and can only ever describe ONE
|
|
605
|
+
// identity — they belong to the default account. Endpoint env vars are shared.
|
|
606
|
+
const credentialEnv: Record<string, string | undefined> = isDefault ? env : {};
|
|
507
607
|
// Apply built-in defaults so login/gateway work without prior setup.
|
|
508
608
|
const websocketUrl =
|
|
509
609
|
readOptionalString(channel.websocketUrl) ||
|
|
@@ -515,16 +615,17 @@ export function resolveOpenclawClawlingAccount(
|
|
|
515
615
|
DEFAULT_BASE_URL;
|
|
516
616
|
const mediaBaseUrl =
|
|
517
617
|
readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
|
|
518
|
-
const token = readOptionalString(channel.token) || readEnvString(
|
|
519
|
-
const agentId =
|
|
618
|
+
const token = readOptionalString(channel.token) || readEnvString(credentialEnv, CLAWCHAT_TOKEN_ENV);
|
|
619
|
+
const agentId =
|
|
620
|
+
readOptionalString(channel.agentId) || readEnvString(credentialEnv, CLAWCHAT_AGENT_ID_ENV);
|
|
520
621
|
const userId = resolveUserId(
|
|
521
622
|
token,
|
|
522
|
-
readOptionalString(channel.userId) || readEnvString(
|
|
623
|
+
readOptionalString(channel.userId) || readEnvString(credentialEnv, CLAWCHAT_USER_ID_ENV),
|
|
523
624
|
);
|
|
524
625
|
const ownerUserId =
|
|
525
626
|
rejectPlaceholderOwnerUserId(readOptionalString(channel.ownerUserId), "channel ownerUserId") ||
|
|
526
627
|
rejectPlaceholderOwnerUserId(
|
|
527
|
-
readEnvString(
|
|
628
|
+
readEnvString(credentialEnv, CLAWCHAT_OWNER_USER_ID_ENV),
|
|
528
629
|
CLAWCHAT_OWNER_USER_ID_ENV,
|
|
529
630
|
) ||
|
|
530
631
|
// Fall back to the access token's `oid` (owner) claim. A provisioner may
|
|
@@ -532,7 +633,9 @@ export function resolveOpenclawClawlingAccount(
|
|
|
532
633
|
// (hasOpenclawClawlingConnectCredentials) never passes and the plugin sits
|
|
533
634
|
// in the wait-for-activation loop. The owner is always carried in the token.
|
|
534
635
|
decodeJwtStringClaim(token, "oid");
|
|
535
|
-
const
|
|
636
|
+
const rootEnabled = typeof root.enabled === "boolean" ? root.enabled : true;
|
|
637
|
+
const sectionEnabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
638
|
+
const enabled = rootEnabled && sectionEnabled;
|
|
536
639
|
const outputVisibility = readOutputVisibility(channel.outputVisibility);
|
|
537
640
|
const chats = readChats(channel.chats);
|
|
538
641
|
const groupMode = readGroupMode(channel.groupMode);
|
|
@@ -550,8 +653,8 @@ export function resolveOpenclawClawlingAccount(
|
|
|
550
653
|
typeof channel.livewareSample === "boolean" ? channel.livewareSample : true;
|
|
551
654
|
|
|
552
655
|
return {
|
|
553
|
-
accountId:
|
|
554
|
-
name: CHANNEL_ID,
|
|
656
|
+
accountId: resolvedAccountId,
|
|
657
|
+
name: isDefault ? CHANNEL_ID : resolvedAccountId,
|
|
555
658
|
enabled,
|
|
556
659
|
configured: hasOpenclawClawlingConnectCredentials({
|
|
557
660
|
websocketUrl,
|
|
@@ -582,7 +685,3 @@ export function resolveOpenclawClawlingAccount(
|
|
|
582
685
|
ack: readAck(channel.ack),
|
|
583
686
|
};
|
|
584
687
|
}
|
|
585
|
-
|
|
586
|
-
export function listOpenclawClawlingAccountIds(): string[] {
|
|
587
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
588
|
-
}
|
package/src/liveware-cli.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
5
|
+
import { normalizeOpenclawClawlingAccountId } from "./config.ts";
|
|
4
6
|
|
|
5
7
|
export const LIVEWARE_BASE_URL = "https://media.clawling.chat/liveware/";
|
|
6
8
|
|
|
@@ -21,7 +23,36 @@ export function livewareDir(stateDir?: string): string {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
26
|
+
* Root for one account's liveware state: `<stateDir>/clawchat` for the
|
|
27
|
+
* `default` account, `<stateDir>/clawchat/accounts/<id>` for a named one.
|
|
28
|
+
*
|
|
29
|
+
* The `default` account keeps the pre-multi-account layout exactly, so an
|
|
30
|
+
* existing single-account install never has to migrate anything.
|
|
31
|
+
*/
|
|
32
|
+
function livewareAccountRoot(stateDir: string | undefined, accountId?: string | null): string {
|
|
33
|
+
const root = stateDir ?? resolveStateDir();
|
|
34
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
35
|
+
return id === DEFAULT_ACCOUNT_ID
|
|
36
|
+
? path.join(root, "clawchat")
|
|
37
|
+
: path.join(root, "clawchat", "accounts", id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Root of a NAMED account's private liveware state
|
|
42
|
+
* (`<stateDir>/clawchat/accounts/<id>`), the directory account removal
|
|
43
|
+
* deletes wholesale. Returns null for `default`, whose root is the shared
|
|
44
|
+
* `<stateDir>/clawchat` and must never be removed this way.
|
|
45
|
+
*/
|
|
46
|
+
export function livewareNamedAccountRootDir(stateDir: string, accountId?: string | null): string | null {
|
|
47
|
+
return normalizeOpenclawClawlingAccountId(accountId) === DEFAULT_ACCOUNT_ID
|
|
48
|
+
? null
|
|
49
|
+
: livewareAccountRoot(stateDir, accountId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Private HOME for the liveware CLI: `<stateDir>/clawchat/liveware-home` for
|
|
54
|
+
* the default account, `<stateDir>/clawchat/accounts/<id>/liveware-home` for a
|
|
55
|
+
* named one.
|
|
25
56
|
*
|
|
26
57
|
* The CLI keeps its login credentials in `$HOME/.clawling`, which is
|
|
27
58
|
* host-global. Two OpenClaw instances on one box (`openclaw --profile <name>`,
|
|
@@ -32,13 +63,30 @@ export function livewareDir(stateDir?: string): string {
|
|
|
32
63
|
* credential store per-instance, which is the same isolation the rest of the
|
|
33
64
|
* plugin already gets for free.
|
|
34
65
|
*
|
|
66
|
+
* Two ACCOUNTS in ONE instance have the same problem and no state dir to tell
|
|
67
|
+
* them apart, so a named account gets its own sub-root: otherwise `work`'s
|
|
68
|
+
* `liveware login` would overwrite `default`'s session in the shared home and
|
|
69
|
+
* `default`'s next `tunnel bind` / `app create` would run as `work`'s user.
|
|
70
|
+
*
|
|
35
71
|
* Migration is automatic: `livewareLogin` runs on every bootstrap AND (best
|
|
36
72
|
* effort) on every relaunch, so an existing install simply re-logs into its new
|
|
37
73
|
* private home on the next start.
|
|
38
74
|
*/
|
|
39
|
-
export function livewareCliHomeDir(stateDir?: string): string {
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
export function livewareCliHomeDir(stateDir?: string, accountId?: string | null): string {
|
|
76
|
+
return path.join(livewareAccountRoot(stateDir, accountId), "liveware-home");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Where the Liveware Sample app tree is installed for one account:
|
|
81
|
+
* `<stateDir>/clawchat/liveware-sample` (default) or
|
|
82
|
+
* `<stateDir>/clawchat/accounts/<id>/liveware-sample` (named).
|
|
83
|
+
*
|
|
84
|
+
* Per account for the same reason as {@link livewareCliHomeDir}: two
|
|
85
|
+
* supervisors in one process otherwise install, swap and delete inside the
|
|
86
|
+
* same `app/` tree concurrently.
|
|
87
|
+
*/
|
|
88
|
+
export function livewareSampleRootDir(stateDir?: string, accountId?: string | null): string {
|
|
89
|
+
return path.join(livewareAccountRoot(stateDir, accountId), "liveware-sample");
|
|
42
90
|
}
|
|
43
91
|
|
|
44
92
|
/**
|
package/src/liveware-sample.ts
CHANGED
|
@@ -12,6 +12,8 @@ import path from "node:path";
|
|
|
12
12
|
import { DEFAULT_SKILLS_REF, OFFICIAL_SKILLS_BASE, type FetchLike } from "./skill-update.ts";
|
|
13
13
|
import type { LivewareSampleRow, LivewareSampleUpsert } from "./storage.ts";
|
|
14
14
|
import { livewareCliEnv, type LivewareLogger } from "./liveware-cli.ts";
|
|
15
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
16
|
+
import { normalizeOpenclawClawlingAccountId } from "./config.ts";
|
|
15
17
|
|
|
16
18
|
export const LIVEWARES_TARGET = "openclaw";
|
|
17
19
|
export const LIVEWARE_SAMPLE_ID = "liveware-sample";
|
|
@@ -154,6 +156,16 @@ async function localInstallMatches(
|
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Suffix for the `app.tmp-…` / `app.old-…` staging dirs. The pid keeps two
|
|
161
|
+
* PROCESSES apart; the account id keeps two supervisors inside ONE process
|
|
162
|
+
* apart, which share a pid and could otherwise share a staging tree.
|
|
163
|
+
*/
|
|
164
|
+
function stagingSuffix(accountId?: string): string {
|
|
165
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
166
|
+
return id === DEFAULT_ACCOUNT_ID ? `${process.pid}` : `${process.pid}-${id}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
/**
|
|
158
170
|
* Download + verify + atomically install the sample under `<sampleRoot>/app`.
|
|
159
171
|
* Preserves USER_DATA_FILES from an existing install. Throws on any failure;
|
|
@@ -172,6 +184,9 @@ export async function downloadLivewareSample(opts: {
|
|
|
172
184
|
sampleRoot: string;
|
|
173
185
|
ref?: string;
|
|
174
186
|
base?: string;
|
|
187
|
+
/** Disambiguates the staging/rollback dirs when two accounts run in one
|
|
188
|
+
* process (same pid). Omitted/`default` keeps the pid-only names. */
|
|
189
|
+
accountId?: string;
|
|
175
190
|
}): Promise<{ version: string; appDir: string; reused?: boolean }> {
|
|
176
191
|
const ref = opts.ref ?? DEFAULT_SKILLS_REF;
|
|
177
192
|
const entry = parseLivewaresManifest(
|
|
@@ -191,7 +206,8 @@ export async function downloadLivewareSample(opts: {
|
|
|
191
206
|
}
|
|
192
207
|
}
|
|
193
208
|
|
|
194
|
-
const
|
|
209
|
+
const swapSuffix = stagingSuffix(opts.accountId);
|
|
210
|
+
const tmpDir = path.join(opts.sampleRoot, `app.tmp-${swapSuffix}`);
|
|
195
211
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
196
212
|
await fs.promises.mkdir(tmpDir, { recursive: true });
|
|
197
213
|
try {
|
|
@@ -218,7 +234,7 @@ export async function downloadLivewareSample(opts: {
|
|
|
218
234
|
// handle, and doing it first leaves no app/ at all when it fails. Renaming
|
|
219
235
|
// aside puts the new tree in place first and makes the delete best-effort:
|
|
220
236
|
// a leftover app.old-<pid> is swept on the next run, never a broken install.
|
|
221
|
-
const oldDir = path.join(opts.sampleRoot, `app.old-${
|
|
237
|
+
const oldDir = path.join(opts.sampleRoot, `app.old-${swapSuffix}`);
|
|
222
238
|
await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => {});
|
|
223
239
|
if (fs.existsSync(appDir)) {
|
|
224
240
|
await withFsSwapRetry(() => fs.promises.rename(appDir, oldDir));
|
|
@@ -700,9 +716,10 @@ export type LivewareSampleSupervisorDeps = {
|
|
|
700
716
|
ref?: string;
|
|
701
717
|
spawnFn?: SpawnLike;
|
|
702
718
|
execFileFn?: ExecFileLike;
|
|
703
|
-
/** Private HOME for the liveware CLI (`livewareCliHomeDir(stateDir)`),
|
|
704
|
-
* instances on one host
|
|
705
|
-
* inherits the ambient HOME, which
|
|
719
|
+
/** Private HOME for the liveware CLI (`livewareCliHomeDir(stateDir, accountId)`),
|
|
720
|
+
* so two instances on one host — or two accounts in one gateway — do not
|
|
721
|
+
* share `$HOME/.clawling`. Absent → the CLI inherits the ambient HOME, which
|
|
722
|
+
* is the pre-isolation behavior. */
|
|
706
723
|
cliHome?: string;
|
|
707
724
|
probePortFn?: ProbePortLike;
|
|
708
725
|
log?: LivewareLogger;
|
|
@@ -894,6 +911,7 @@ export class LivewareSampleSupervisor {
|
|
|
894
911
|
|
|
895
912
|
const { version, appDir } = await downloadLivewareSample({
|
|
896
913
|
fetchFn: deps.fetchFn, sampleRoot: deps.sampleRoot, ref: deps.ref,
|
|
914
|
+
accountId: deps.accountId,
|
|
897
915
|
});
|
|
898
916
|
if (this.bailIfStopped()) return;
|
|
899
917
|
const cliEnv = await this.ensureCliEnv();
|
|
@@ -977,6 +995,7 @@ export class LivewareSampleSupervisor {
|
|
|
977
995
|
try {
|
|
978
996
|
const dl = await downloadLivewareSample({
|
|
979
997
|
fetchFn: deps.fetchFn, sampleRoot: deps.sampleRoot, ref: deps.ref,
|
|
998
|
+
accountId: deps.accountId,
|
|
980
999
|
});
|
|
981
1000
|
version = dl.version;
|
|
982
1001
|
appDir = dl.appDir;
|
package/src/login.runtime.ts
CHANGED
|
@@ -3,10 +3,14 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
|
3
3
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
4
4
|
import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
|
|
5
5
|
import { resolvePluginVersion } from "./plugin-report.ts";
|
|
6
|
+
import { openclawClawlingAccountSlotPath, patchOpenclawClawlingAccountSection } from "./account-config-writes.ts";
|
|
7
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
6
8
|
import {
|
|
7
9
|
CHANNEL_ID,
|
|
8
10
|
mergeOpenclawClawchatRuntimePluginActivation,
|
|
9
11
|
mergeOpenclawClawchatToolAllow,
|
|
12
|
+
normalizeOpenclawClawlingAccountId,
|
|
13
|
+
readOpenclawClawlingAccountSection,
|
|
10
14
|
resolveOpenclawClawlingAccount,
|
|
11
15
|
} from "./config.ts";
|
|
12
16
|
import { getClawChatStore, type ClawChatStore } from "./storage.ts";
|
|
@@ -80,8 +84,7 @@ export type ActivationIntent = "new-account" | "repair";
|
|
|
80
84
|
/**
|
|
81
85
|
* Thrown when redeeming an invite code would silently replace a live activation.
|
|
82
86
|
*
|
|
83
|
-
*
|
|
84
|
-
* slot and `listOpenclawClawlingAccountIds()` returns a single account. Because
|
|
87
|
+
* Each account slot holds exactly one ClawChat identity. Because
|
|
85
88
|
* the stored `user_id` is replayed as a re-pair hint, redeeming a *new* invite
|
|
86
89
|
* code here cannot produce a second agent: the server binds the code to the
|
|
87
90
|
* existing agent, consumes it, creates no second agent and no second contact
|
|
@@ -103,9 +106,9 @@ export class ExistingActivationError extends Error {
|
|
|
103
106
|
" - RESTORE the identity above (re-pairs that same agent; if it was " +
|
|
104
107
|
"deleted this brings it back with its history): " +
|
|
105
108
|
"/clawchat-activate <CODE> --repair\n" +
|
|
106
|
-
" - To run a SECOND agent alongside this one instead,
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
+
" - To run a SECOND agent alongside this one instead, activate it as a " +
|
|
110
|
+
"named account: /clawchat-activate <CODE> --account <name> (or: openclaw " +
|
|
111
|
+
"channels add --channel clawchat-plugin-openclaw --account <name> --token <CODE>)",
|
|
109
112
|
);
|
|
110
113
|
this.name = "ExistingActivationError";
|
|
111
114
|
this.userId = userId;
|
|
@@ -177,39 +180,38 @@ async function promptActivationIntentFromStdin(
|
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
|
|
180
|
-
function buildLoginConfig(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
function buildLoginConfig(
|
|
184
|
+
cfg: OpenClawConfig,
|
|
185
|
+
accountId: string,
|
|
186
|
+
result: AgentConnectResult,
|
|
187
|
+
): OpenClawConfig {
|
|
188
|
+
const existing = readOpenclawClawlingAccountSection(cfg, accountId);
|
|
183
189
|
const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
|
|
184
190
|
? existing.groupMode
|
|
185
191
|
: "all";
|
|
186
192
|
const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
|
|
187
193
|
? existing.groupCommandMode
|
|
188
194
|
: "owner";
|
|
189
|
-
const
|
|
190
|
-
...existing,
|
|
195
|
+
const patch: Record<string, unknown> = {
|
|
191
196
|
enabled: true,
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
// Group policy defaults are root-level shared keys; named accounts inherit
|
|
198
|
+
// them and only carry credentials.
|
|
199
|
+
...(accountId === DEFAULT_ACCOUNT_ID ? { groupMode, groupCommandMode } : {}),
|
|
194
200
|
token: result.access_token,
|
|
201
|
+
// `undefined` deletes the key so a login without a refresh token never
|
|
202
|
+
// leaves a stale one behind.
|
|
203
|
+
refreshToken: result.refresh_token ? result.refresh_token : undefined,
|
|
195
204
|
...(result.agent.id ? { agentId: result.agent.id } : {}),
|
|
196
205
|
userId: result.agent.user_id,
|
|
197
206
|
ownerUserId: result.agent.owner_id,
|
|
198
207
|
};
|
|
199
|
-
if (result.refresh_token) {
|
|
200
|
-
nextSection.refreshToken = result.refresh_token;
|
|
201
|
-
} else {
|
|
202
|
-
delete nextSection.refreshToken;
|
|
203
|
-
}
|
|
204
208
|
return mergeOpenclawClawchatRuntimePluginActivation(
|
|
205
|
-
mergeOpenclawClawchatToolAllow(
|
|
206
|
-
...cfg,
|
|
207
|
-
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
208
|
-
}),
|
|
209
|
+
mergeOpenclawClawchatToolAllow(patchOpenclawClawlingAccountSection(cfg, accountId, patch)),
|
|
209
210
|
);
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
async function persistLoginConfig(
|
|
214
|
+
accountId: string,
|
|
213
215
|
params: LoginParams,
|
|
214
216
|
result: AgentConnectResult,
|
|
215
217
|
): Promise<void> {
|
|
@@ -223,7 +225,7 @@ async function persistLoginConfig(
|
|
|
223
225
|
reason: "clawchat-plugin-openclaw credentials changed",
|
|
224
226
|
},
|
|
225
227
|
mutate(draft) {
|
|
226
|
-
Object.assign(draft, buildLoginConfig(draft, result));
|
|
228
|
+
Object.assign(draft, buildLoginConfig(draft, accountId, result));
|
|
227
229
|
},
|
|
228
230
|
});
|
|
229
231
|
params.runtime.log(
|
|
@@ -236,7 +238,7 @@ async function persistLoginConfig(
|
|
|
236
238
|
params.runtime.log(
|
|
237
239
|
`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
|
|
238
240
|
);
|
|
239
|
-
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
241
|
+
await params.persistConfig(buildLoginConfig(params.cfg, accountId, result));
|
|
240
242
|
params.runtime.log(
|
|
241
243
|
`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
|
|
242
244
|
);
|
|
@@ -280,11 +282,12 @@ function readOptionalConnectString(value: unknown, fieldName: string): string |
|
|
|
280
282
|
*/
|
|
281
283
|
export async function runOpenclawClawlingLogin(params: LoginParams): Promise<void> {
|
|
282
284
|
const { cfg, runtime } = params;
|
|
285
|
+
const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
|
|
283
286
|
|
|
284
287
|
// `resolveOpenclawClawlingAccount` falls back to the built-in
|
|
285
288
|
// `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
|
|
286
289
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
287
|
-
const account = resolveOpenclawClawlingAccount(cfg);
|
|
290
|
+
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
288
291
|
|
|
289
292
|
// Fork before the invite code is even read, so neither branch spends the code
|
|
290
293
|
// before the outcome is settled. "Live" is decided by whether a usable token
|
|
@@ -405,12 +408,13 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
405
408
|
: {}),
|
|
406
409
|
};
|
|
407
410
|
|
|
411
|
+
const configPath = openclawClawlingAccountSlotPath(params.cfg, accountId);
|
|
408
412
|
runtime.log(
|
|
409
|
-
`Updating config:
|
|
413
|
+
`Updating config: ${configPath}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${
|
|
410
414
|
normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""
|
|
411
415
|
} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`,
|
|
412
416
|
);
|
|
413
|
-
await persistLoginConfig(params, normalizedResult);
|
|
417
|
+
await persistLoginConfig(accountId, params, normalizedResult);
|
|
414
418
|
try {
|
|
415
419
|
const store =
|
|
416
420
|
params.store ??
|
package/src/outbound.ts
CHANGED
|
@@ -899,8 +899,8 @@ export const openclawClawlingOutbound: ChannelOutboundAdapter = {
|
|
|
899
899
|
textChunkLimit: 4000,
|
|
900
900
|
...createAttachedChannelResultAdapter({
|
|
901
901
|
channel: CHANNEL_ID,
|
|
902
|
-
sendText: async ({ cfg, to, text }) => {
|
|
903
|
-
const account = resolveOpenclawClawlingAccount(cfg);
|
|
902
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
903
|
+
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
904
904
|
const client =
|
|
905
905
|
getOpenclawClawlingClient(account.accountId) ??
|
|
906
906
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
@@ -936,8 +936,8 @@ export const openclawClawlingOutbound: ChannelOutboundAdapter = {
|
|
|
936
936
|
messageId: result?.messageId ?? messageId,
|
|
937
937
|
};
|
|
938
938
|
},
|
|
939
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
940
|
-
const account = resolveOpenclawClawlingAccount(cfg);
|
|
939
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId }) => {
|
|
940
|
+
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
941
941
|
const client =
|
|
942
942
|
getOpenclawClawlingClient(account.accountId) ??
|
|
943
943
|
(await waitForOpenclawClawlingClient(account.accountId));
|