@clawling/clawchat-plugin-openclaw 2026.9.10-1 → 2026.9.13-1
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 +66 -0
- package/dist/src/account-removal.js +21 -0
- package/dist/src/channel.js +23 -1
- package/dist/src/channel.setup.js +70 -42
- package/dist/src/commands.js +50 -27
- package/dist/src/config.js +83 -13
- package/dist/src/liveware-cli.js +38 -4
- package/dist/src/liveware-sample.js +16 -2
- package/dist/src/llm-context-debug.js +7 -1
- package/dist/src/login.runtime.js +27 -29
- package/dist/src/outbound.js +4 -4
- package/dist/src/permission-result.js +42 -12
- package/dist/src/runtime.js +50 -27
- package/dist/src/terminal-send.js +16 -1
- package/dist/src/tools.js +799 -685
- package/openclaw.plugin.json +150 -0
- package/package.json +1 -1
- package/src/account-config-writes.ts +76 -0
- package/src/account-removal.ts +28 -0
- package/src/channel.setup.ts +77 -43
- package/src/channel.ts +22 -1
- package/src/commands.ts +60 -26
- package/src/config.ts +113 -14
- package/src/liveware-cli.ts +40 -4
- package/src/liveware-sample.ts +20 -2
- package/src/llm-context-debug.ts +7 -1
- package/src/login.runtime.ts +32 -26
- package/src/outbound.ts +4 -4
- package/src/permission-result.ts +46 -11
- package/src/runtime.ts +75 -31
- package/src/terminal-send.ts +16 -1
- package/src/tools.ts +896 -768
|
@@ -11,6 +11,8 @@ import net from "node:net";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { DEFAULT_SKILLS_REF, OFFICIAL_SKILLS_BASE } from "./skill-update.js";
|
|
13
13
|
import { livewareCliEnv } from "./liveware-cli.js";
|
|
14
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
15
|
+
import { normalizeOpenclawClawlingAccountId } from "./config.js";
|
|
14
16
|
export const LIVEWARES_TARGET = "openclaw";
|
|
15
17
|
export const LIVEWARE_SAMPLE_ID = "liveware-sample";
|
|
16
18
|
export const LIVEWARE_SAMPLE_APP_NAME = "Liveware Sample";
|
|
@@ -127,6 +129,15 @@ async function localInstallMatches(appDir, entry) {
|
|
|
127
129
|
return false; // missing / unreadable — treat as "not installed"
|
|
128
130
|
}
|
|
129
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Suffix for the `app.tmp-…` / `app.old-…` staging dirs. The pid keeps two
|
|
134
|
+
* PROCESSES apart; the account id keeps two supervisors inside ONE process
|
|
135
|
+
* apart, which share a pid and could otherwise share a staging tree.
|
|
136
|
+
*/
|
|
137
|
+
function stagingSuffix(accountId) {
|
|
138
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
139
|
+
return id === DEFAULT_ACCOUNT_ID ? `${process.pid}` : `${process.pid}-${id}`;
|
|
140
|
+
}
|
|
130
141
|
/**
|
|
131
142
|
* Download + verify + atomically install the sample under `<sampleRoot>/app`.
|
|
132
143
|
* Preserves USER_DATA_FILES from an existing install. Throws on any failure;
|
|
@@ -155,7 +166,8 @@ export async function downloadLivewareSample(opts) {
|
|
|
155
166
|
await fs.promises.rm(path.join(opts.sampleRoot, name), { recursive: true, force: true }).catch(() => { });
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
|
-
const
|
|
169
|
+
const swapSuffix = stagingSuffix(opts.accountId);
|
|
170
|
+
const tmpDir = path.join(opts.sampleRoot, `app.tmp-${swapSuffix}`);
|
|
159
171
|
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
160
172
|
await fs.promises.mkdir(tmpDir, { recursive: true });
|
|
161
173
|
try {
|
|
@@ -182,7 +194,7 @@ export async function downloadLivewareSample(opts) {
|
|
|
182
194
|
// handle, and doing it first leaves no app/ at all when it fails. Renaming
|
|
183
195
|
// aside puts the new tree in place first and makes the delete best-effort:
|
|
184
196
|
// a leftover app.old-<pid> is swept on the next run, never a broken install.
|
|
185
|
-
const oldDir = path.join(opts.sampleRoot, `app.old-${
|
|
197
|
+
const oldDir = path.join(opts.sampleRoot, `app.old-${swapSuffix}`);
|
|
186
198
|
await fs.promises.rm(oldDir, { recursive: true, force: true }).catch(() => { });
|
|
187
199
|
if (fs.existsSync(appDir)) {
|
|
188
200
|
await withFsSwapRetry(() => fs.promises.rename(appDir, oldDir));
|
|
@@ -737,6 +749,7 @@ export class LivewareSampleSupervisor {
|
|
|
737
749
|
}
|
|
738
750
|
const { version, appDir } = await downloadLivewareSample({
|
|
739
751
|
fetchFn: deps.fetchFn, sampleRoot: deps.sampleRoot, ref: deps.ref,
|
|
752
|
+
accountId: deps.accountId,
|
|
740
753
|
});
|
|
741
754
|
if (this.bailIfStopped())
|
|
742
755
|
return;
|
|
@@ -826,6 +839,7 @@ export class LivewareSampleSupervisor {
|
|
|
826
839
|
try {
|
|
827
840
|
const dl = await downloadLivewareSample({
|
|
828
841
|
fetchFn: deps.fetchFn, sampleRoot: deps.sampleRoot, ref: deps.ref,
|
|
842
|
+
accountId: deps.accountId,
|
|
829
843
|
});
|
|
830
844
|
version = dl.version;
|
|
831
845
|
appDir = dl.appDir;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { resolveStateDirFromEnv } from "./state-paths.js";
|
|
3
4
|
function envEnabled(value) {
|
|
4
5
|
return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
|
|
5
6
|
}
|
|
@@ -10,7 +11,12 @@ function safeId(value) {
|
|
|
10
11
|
export function createOpenclawLlmContextDebug(params = {}) {
|
|
11
12
|
const env = params.env ?? process.env;
|
|
12
13
|
const enabled = envEnabled(env.CLAWCHAT_LLM_CONTEXT_DEBUG);
|
|
13
|
-
|
|
14
|
+
// Default under the per-instance OpenClaw state dir, never the process cwd:
|
|
15
|
+
// two profiles launched from one directory must not interleave runs and
|
|
16
|
+
// overwrite each other's `latest.json`.
|
|
17
|
+
const snapshotRoot = path.resolve(env.CLAWCHAT_LLM_CONTEXT_SNAPSHOT_DIR ||
|
|
18
|
+
params.defaultSnapshotRoot ||
|
|
19
|
+
path.join(resolveStateDirFromEnv(env), "clawchat", "llm-context-debug"));
|
|
14
20
|
return {
|
|
15
21
|
enabled,
|
|
16
22
|
captureFullInput: envEnabled(env.CLAWCHAT_LLM_CONTEXT_CAPTURE_FULL_INPUT),
|
|
@@ -2,7 +2,9 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
3
3
|
import { ClawlingApiError } from "./api-types.js";
|
|
4
4
|
import { resolvePluginVersion } from "./plugin-report.js";
|
|
5
|
-
import {
|
|
5
|
+
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
|
|
6
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
7
|
+
import { CHANNEL_ID, mergeOpenclawClawchatRuntimePluginActivation, mergeOpenclawClawchatToolAllow, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
6
8
|
import { getClawChatStore } from "./storage.js";
|
|
7
9
|
/**
|
|
8
10
|
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
@@ -24,8 +26,7 @@ export const AGENT_NOT_FOUND_CODE = 16001;
|
|
|
24
26
|
/**
|
|
25
27
|
* Thrown when redeeming an invite code would silently replace a live activation.
|
|
26
28
|
*
|
|
27
|
-
*
|
|
28
|
-
* slot and `listOpenclawClawlingAccountIds()` returns a single account. Because
|
|
29
|
+
* Each account slot holds exactly one ClawChat identity. Because
|
|
29
30
|
* the stored `user_id` is replayed as a re-pair hint, redeeming a *new* invite
|
|
30
31
|
* code here cannot produce a second agent: the server binds the code to the
|
|
31
32
|
* existing agent, consumes it, creates no second agent and no second contact
|
|
@@ -45,9 +46,9 @@ export class ExistingActivationError extends Error {
|
|
|
45
46
|
" - RESTORE the identity above (re-pairs that same agent; if it was " +
|
|
46
47
|
"deleted this brings it back with its history): " +
|
|
47
48
|
"/clawchat-activate <CODE> --repair\n" +
|
|
48
|
-
" - To run a SECOND agent alongside this one instead,
|
|
49
|
-
"
|
|
50
|
-
"
|
|
49
|
+
" - To run a SECOND agent alongside this one instead, activate it as a " +
|
|
50
|
+
"named account: /clawchat-activate <CODE> --account <name> (or: openclaw " +
|
|
51
|
+
"channels add --channel clawchat-plugin-openclaw --account <name> --token <CODE>)");
|
|
51
52
|
this.name = "ExistingActivationError";
|
|
52
53
|
this.userId = userId;
|
|
53
54
|
this.agentId = agentId;
|
|
@@ -113,37 +114,30 @@ async function promptActivationIntentFromStdin(runtime, who) {
|
|
|
113
114
|
rl?.close();
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
function buildLoginConfig(cfg, result) {
|
|
117
|
-
const
|
|
118
|
-
const existing = (channels[CHANNEL_ID] ?? {});
|
|
117
|
+
function buildLoginConfig(cfg, accountId, result) {
|
|
118
|
+
const existing = readOpenclawClawlingAccountSection(cfg, accountId);
|
|
119
119
|
const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
|
|
120
120
|
? existing.groupMode
|
|
121
121
|
: "all";
|
|
122
122
|
const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
|
|
123
123
|
? existing.groupCommandMode
|
|
124
124
|
: "owner";
|
|
125
|
-
const
|
|
126
|
-
...existing,
|
|
125
|
+
const patch = {
|
|
127
126
|
enabled: true,
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
// Group policy defaults are root-level shared keys; named accounts inherit
|
|
128
|
+
// them and only carry credentials.
|
|
129
|
+
...(accountId === DEFAULT_ACCOUNT_ID ? { groupMode, groupCommandMode } : {}),
|
|
130
130
|
token: result.access_token,
|
|
131
|
+
// `undefined` deletes the key so a login without a refresh token never
|
|
132
|
+
// leaves a stale one behind.
|
|
133
|
+
refreshToken: result.refresh_token ? result.refresh_token : undefined,
|
|
131
134
|
...(result.agent.id ? { agentId: result.agent.id } : {}),
|
|
132
135
|
userId: result.agent.user_id,
|
|
133
136
|
ownerUserId: result.agent.owner_id,
|
|
134
137
|
};
|
|
135
|
-
|
|
136
|
-
nextSection.refreshToken = result.refresh_token;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
delete nextSection.refreshToken;
|
|
140
|
-
}
|
|
141
|
-
return mergeOpenclawClawchatRuntimePluginActivation(mergeOpenclawClawchatToolAllow({
|
|
142
|
-
...cfg,
|
|
143
|
-
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
144
|
-
}));
|
|
138
|
+
return mergeOpenclawClawchatRuntimePluginActivation(mergeOpenclawClawchatToolAllow(patchOpenclawClawlingAccountSection(cfg, accountId, patch)));
|
|
145
139
|
}
|
|
146
|
-
async function persistLoginConfig(params, result) {
|
|
140
|
+
async function persistLoginConfig(accountId, params, result) {
|
|
147
141
|
if (params.mutateConfigFile) {
|
|
148
142
|
params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`);
|
|
149
143
|
await params.mutateConfigFile({
|
|
@@ -152,7 +146,7 @@ async function persistLoginConfig(params, result) {
|
|
|
152
146
|
reason: "clawchat-plugin-openclaw credentials changed",
|
|
153
147
|
},
|
|
154
148
|
mutate(draft) {
|
|
155
|
-
Object.assign(draft, buildLoginConfig(draft, result));
|
|
149
|
+
Object.assign(draft, buildLoginConfig(draft, accountId, result));
|
|
156
150
|
},
|
|
157
151
|
});
|
|
158
152
|
params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
|
|
@@ -160,7 +154,7 @@ async function persistLoginConfig(params, result) {
|
|
|
160
154
|
}
|
|
161
155
|
if (params.persistConfig) {
|
|
162
156
|
params.runtime.log(`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
|
|
163
|
-
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
157
|
+
await params.persistConfig(buildLoginConfig(params.cfg, accountId, result));
|
|
164
158
|
params.runtime.log(`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`);
|
|
165
159
|
return;
|
|
166
160
|
}
|
|
@@ -198,10 +192,11 @@ function readOptionalConnectString(value, fieldName) {
|
|
|
198
192
|
*/
|
|
199
193
|
export async function runOpenclawClawlingLogin(params) {
|
|
200
194
|
const { cfg, runtime } = params;
|
|
195
|
+
const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
|
|
201
196
|
// `resolveOpenclawClawlingAccount` falls back to the built-in
|
|
202
197
|
// `DEFAULT_BASE_URL` / `DEFAULT_WEBSOCKET_URL` when the operator has not
|
|
203
198
|
// overridden them, so login works without a prior `openclaw channels setup --channel clawchat-plugin-openclaw`.
|
|
204
|
-
const account = resolveOpenclawClawlingAccount(cfg);
|
|
199
|
+
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
205
200
|
// Fork before the invite code is even read, so neither branch spends the code
|
|
206
201
|
// before the outcome is settled. "Live" is decided by whether a usable token
|
|
207
202
|
// remains: auto-logout (§C) blanks the tokens but preserves the identity, so a
|
|
@@ -315,8 +310,11 @@ export async function runOpenclawClawlingLogin(params) {
|
|
|
315
310
|
}
|
|
316
311
|
: {}),
|
|
317
312
|
};
|
|
318
|
-
|
|
319
|
-
|
|
313
|
+
const configPath = accountId === DEFAULT_ACCOUNT_ID
|
|
314
|
+
? `channels.${CHANNEL_ID}`
|
|
315
|
+
: `channels.${CHANNEL_ID}.accounts.${accountId}`;
|
|
316
|
+
runtime.log(`Updating config: ${configPath}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`);
|
|
317
|
+
await persistLoginConfig(accountId, params, normalizedResult);
|
|
320
318
|
try {
|
|
321
319
|
const store = params.store ??
|
|
322
320
|
getClawChatStore({
|
package/dist/src/outbound.js
CHANGED
|
@@ -673,8 +673,8 @@ export const openclawClawlingOutbound = {
|
|
|
673
673
|
textChunkLimit: 4000,
|
|
674
674
|
...createAttachedChannelResultAdapter({
|
|
675
675
|
channel: CHANNEL_ID,
|
|
676
|
-
sendText: async ({ cfg, to, text }) => {
|
|
677
|
-
const account = resolveOpenclawClawlingAccount(cfg);
|
|
676
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
677
|
+
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
678
678
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
679
679
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
680
680
|
const target = parseOpenclawRecipient(to);
|
|
@@ -709,8 +709,8 @@ export const openclawClawlingOutbound = {
|
|
|
709
709
|
messageId: result?.messageId ?? messageId,
|
|
710
710
|
};
|
|
711
711
|
},
|
|
712
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile }) => {
|
|
713
|
-
const account = resolveOpenclawClawlingAccount(cfg);
|
|
712
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, mediaAccess, mediaLocalRoots, mediaReadFile, accountId }) => {
|
|
713
|
+
const account = resolveOpenclawClawlingAccount(cfg, accountId);
|
|
714
714
|
const client = getOpenclawClawlingClient(account.accountId) ??
|
|
715
715
|
(await waitForOpenclawClawlingClient(account.accountId));
|
|
716
716
|
if (!mediaUrl?.trim()) {
|
|
@@ -20,16 +20,42 @@
|
|
|
20
20
|
* The discriminator is `payload.metadata.kind === "permission_result"`.
|
|
21
21
|
*/
|
|
22
22
|
import { EVENT } from "./protocol-types.js";
|
|
23
|
-
// Module-level dedup set — `request_id` values are stable backend identifiers.
|
|
24
|
-
// A duplicate receipt (e.g. from WS replay on reconnect) must not re-trigger
|
|
25
|
-
// an agent turn.
|
|
26
|
-
const _seenRequestIds = new Set();
|
|
27
23
|
/**
|
|
28
|
-
*
|
|
24
|
+
* Upper bound on retained `request_id`s per account (FIFO eviction). Mirrors
|
|
25
|
+
* `createNotifySignalObserver` in `src/ws-alignment.ts`: permission receipts
|
|
26
|
+
* are rare (one per owner decision), so 512 covers any realistic replay window
|
|
27
|
+
* while keeping a long-running gateway's memory flat.
|
|
28
|
+
*/
|
|
29
|
+
export const PERMISSION_RESULT_DEDUP_MAX_SEEN = 512;
|
|
30
|
+
// Dedup state partitioned by accountId — `request_id` values are stable backend
|
|
31
|
+
// identifiers scoped to the agent identity that raised the request, so one
|
|
32
|
+
// account's history must never suppress another account's receipt with the
|
|
33
|
+
// same id. A duplicate receipt within one account (e.g. from WS replay on
|
|
34
|
+
// reconnect) must not re-trigger an agent turn.
|
|
35
|
+
const _seenRequestIdsByAccount = new Map();
|
|
36
|
+
function seenRequestIdsFor(accountId) {
|
|
37
|
+
let entry = _seenRequestIdsByAccount.get(accountId);
|
|
38
|
+
if (!entry) {
|
|
39
|
+
entry = { seen: new Set(), order: [] };
|
|
40
|
+
_seenRequestIdsByAccount.set(accountId, entry);
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
function markRequestIdSeen(entry, requestId) {
|
|
45
|
+
entry.seen.add(requestId);
|
|
46
|
+
entry.order.push(requestId);
|
|
47
|
+
while (entry.order.length > PERMISSION_RESULT_DEDUP_MAX_SEEN) {
|
|
48
|
+
const evicted = entry.order.shift();
|
|
49
|
+
if (evicted !== undefined)
|
|
50
|
+
entry.seen.delete(evicted);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @internal Test helper — reset the dedup state between isolated test runs.
|
|
29
55
|
* Never call this in production code.
|
|
30
56
|
*/
|
|
31
57
|
export function _resetPermissionResultDedup() {
|
|
32
|
-
|
|
58
|
+
_seenRequestIdsByAccount.clear();
|
|
33
59
|
}
|
|
34
60
|
function buildOutcomeNote(metadata) {
|
|
35
61
|
const { operation, outcome, reason } = metadata;
|
|
@@ -107,8 +133,10 @@ export function buildPermissionResultEnvelope(params) {
|
|
|
107
133
|
};
|
|
108
134
|
}
|
|
109
135
|
/**
|
|
110
|
-
* Handles a `permission_result` receipt. Deduplicates by `request_id`
|
|
111
|
-
* (idempotent — WS replay on reconnect may deliver the
|
|
136
|
+
* Handles a `permission_result` receipt. Deduplicates by `request_id` within
|
|
137
|
+
* the receiving account (idempotent — WS replay on reconnect may deliver the
|
|
138
|
+
* same receipt again), retaining at most {@link PERMISSION_RESULT_DEDUP_MAX_SEEN}
|
|
139
|
+
* ids per account (oldest evicted first).
|
|
112
140
|
*
|
|
113
141
|
* An empty/falsy `request_id` is never added to the dedup set — two receipts
|
|
114
142
|
* can legitimately share an empty id, and both must still be rendered — but
|
|
@@ -127,9 +155,11 @@ export function buildPermissionResultEnvelope(params) {
|
|
|
127
155
|
export function handlePermissionResult(metadata, account, ownerConversationId) {
|
|
128
156
|
if (!ownerConversationId)
|
|
129
157
|
return null;
|
|
130
|
-
if (metadata.request_id
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
if (metadata.request_id) {
|
|
159
|
+
const entry = seenRequestIdsFor(account.accountId);
|
|
160
|
+
if (entry.seen.has(metadata.request_id))
|
|
161
|
+
return null;
|
|
162
|
+
markRequestIdSeen(entry, metadata.request_id);
|
|
163
|
+
}
|
|
134
164
|
return buildPermissionResultEnvelope({ account, metadata, ownerConversationId });
|
|
135
165
|
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -6,13 +6,14 @@ import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from ".
|
|
|
6
6
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
7
7
|
import { buildActivationBootstrapText } from "./activation-greeting.js";
|
|
8
8
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
9
|
-
import
|
|
10
|
-
import { ensureLivewareCli, livewareCliHomeDir, resolveLivewarePath } from "./liveware-cli.js";
|
|
9
|
+
import { ensureLivewareCli, livewareCliHomeDir, livewareSampleRootDir, resolveLivewarePath, } from "./liveware-cli.js";
|
|
11
10
|
import { LivewareSampleSupervisor, } from "./liveware-sample.js";
|
|
12
11
|
import { ClawlingApiError } from "./api-types.js";
|
|
13
12
|
import { RefreshManager } from "./refresh-manager.js";
|
|
14
13
|
import { runOpenclawClawlingLogin, } from "./login.runtime.js";
|
|
15
|
-
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
14
|
+
import { CHANNEL_ID, effectiveOutputVisibility, effectiveGroupCommandMode, effectiveGroupMode, hasOpenclawClawlingConnectCredentials, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, resolveOpenclawClawlingAccount, CLAWCHAT_REFRESH_TOKEN_ENV, } from "./config.js";
|
|
15
|
+
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
|
|
16
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
16
17
|
import { isValidChatId } from "./ws-client.js";
|
|
17
18
|
import { dispatchOpenclawClawlingInbound } from "./inbound.js";
|
|
18
19
|
import { PendingConsentStore, cleanupTombstonedManagedSkills, handleOwnerConsentReply, managedSkillExists, readLocalSkillState, resolveBundledSkillsDir, resolveManagedSkillsDir, runSkillUpdateCheck, } from "./skill-update.js";
|
|
@@ -130,12 +131,26 @@ export function classifyHelloFailReason(reason) {
|
|
|
130
131
|
return "token-rejected";
|
|
131
132
|
return "generic";
|
|
132
133
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The refresh token paired with the account's access token. The account
|
|
136
|
+
* section's `refreshToken` wins. `CLAWCHAT_REFRESH_TOKEN` is process-wide, so
|
|
137
|
+
* it is honoured ONLY for the default account and ONLY when that section
|
|
138
|
+
* carries no `token` of its own (i.e. the access token itself came from
|
|
139
|
+
* `CLAWCHAT_TOKEN`) — an env-provisioned pair can rotate, while a login-written
|
|
140
|
+
* token is never paired with a stray refresh token left in the environment.
|
|
141
|
+
*/
|
|
142
|
+
function readConfigRefreshToken(cfg, accountId, env = process.env) {
|
|
143
|
+
const section = readOpenclawClawlingAccountSection(cfg, accountId);
|
|
144
|
+
const refreshToken = section.refreshToken;
|
|
145
|
+
if (typeof refreshToken === "string" && refreshToken.trim())
|
|
146
|
+
return refreshToken.trim();
|
|
147
|
+
if (normalizeOpenclawClawlingAccountId(accountId) !== DEFAULT_ACCOUNT_ID)
|
|
148
|
+
return null;
|
|
149
|
+
const configToken = section.token;
|
|
150
|
+
if (typeof configToken === "string" && configToken.trim())
|
|
151
|
+
return null;
|
|
152
|
+
const envRefreshToken = env[CLAWCHAT_REFRESH_TOKEN_ENV];
|
|
153
|
+
return typeof envRefreshToken === "string" && envRefreshToken.trim() ? envRefreshToken.trim() : null;
|
|
139
154
|
}
|
|
140
155
|
function withFullVerboseDispatchConfig(cfg, agentId) {
|
|
141
156
|
const cfgRecord = cfg;
|
|
@@ -443,14 +458,25 @@ export async function maybeAutoActivateFromEnv(params) {
|
|
|
443
458
|
const code = (params.connectCode ?? "").trim();
|
|
444
459
|
if (!code)
|
|
445
460
|
return params.account;
|
|
461
|
+
// The env var is process-wide: it can only ever describe ONE identity, and
|
|
462
|
+
// that is the default account. A named account never redeems it.
|
|
463
|
+
if (normalizeOpenclawClawlingAccountId(params.account.accountId) !== DEFAULT_ACCOUNT_ID) {
|
|
464
|
+
params.log?.info?.(`[${params.account.accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present but ignored: env activation is for the default account only`);
|
|
465
|
+
return params.account;
|
|
466
|
+
}
|
|
446
467
|
if (accountHasConnectCredentials(params.account))
|
|
447
468
|
return params.account;
|
|
469
|
+
const accountId = params.account.accountId;
|
|
470
|
+
const stored = params.store?.getActivationCredentials?.({ platform: "openclaw", accountId });
|
|
471
|
+
if (stored) {
|
|
472
|
+
params.log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present but a sqlite activation row already holds credentials; not redeeming`);
|
|
473
|
+
return params.account;
|
|
474
|
+
}
|
|
448
475
|
if (consumedAutoActivationCodes.has(code))
|
|
449
476
|
return params.account;
|
|
450
477
|
consumedAutoActivationCodes.add(code);
|
|
451
478
|
const runLogin = params.runLogin ?? runOpenclawClawlingLogin;
|
|
452
479
|
const resolveAccount = params.resolveAccount ?? resolveOpenclawClawlingAccount;
|
|
453
|
-
const accountId = params.account.accountId;
|
|
454
480
|
params.log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} present; attempting non-interactive activation`);
|
|
455
481
|
try {
|
|
456
482
|
await runLogin({
|
|
@@ -470,7 +496,7 @@ export async function maybeAutoActivateFromEnv(params) {
|
|
|
470
496
|
params.log?.error?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} activation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
471
497
|
return params.account;
|
|
472
498
|
}
|
|
473
|
-
const next = resolveAccount(params.cfg);
|
|
499
|
+
const next = resolveAccount(params.cfg, accountId);
|
|
474
500
|
params.log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${CONNECT_CODE_ENV} activation succeeded hasToken=${Boolean(next.token)}`);
|
|
475
501
|
return next;
|
|
476
502
|
}
|
|
@@ -722,6 +748,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
722
748
|
account,
|
|
723
749
|
accountId,
|
|
724
750
|
connectCode: envConnectCode,
|
|
751
|
+
store,
|
|
725
752
|
log,
|
|
726
753
|
});
|
|
727
754
|
}
|
|
@@ -749,7 +776,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
749
776
|
userId: account.userId,
|
|
750
777
|
ownerUserId: account.ownerUserId,
|
|
751
778
|
accessToken: account.token,
|
|
752
|
-
refreshToken: readConfigRefreshToken(cfg),
|
|
779
|
+
refreshToken: readConfigRefreshToken(cfg, accountId),
|
|
753
780
|
loginMethod: "config-seed",
|
|
754
781
|
deviceId: CHANNEL_ID,
|
|
755
782
|
});
|
|
@@ -831,13 +858,16 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
831
858
|
// `RuntimeConnectionStore` only `Pick`s a subset of its methods, so widen
|
|
832
859
|
// through the structural `LivewareSampleStoreLike` contract (Task 8/4).
|
|
833
860
|
store: store,
|
|
834
|
-
|
|
861
|
+
// Per account: two supervisors in one process otherwise install, swap and
|
|
862
|
+
// delete inside the SAME `app/` tree. `default` keeps the legacy path.
|
|
863
|
+
sampleRoot: livewareSampleRootDir(stateDirForSample ?? "", accountId),
|
|
835
864
|
resolveToken: () => account.token ?? "",
|
|
836
865
|
resolveAgentUserId: () => account.userId ?? null,
|
|
837
866
|
resolveLivewarePath: () => resolveLivewarePath(stateDirForSample),
|
|
838
|
-
// Per-instance HOME for the liveware CLI: its credentials
|
|
839
|
-
// in the host-global `$HOME/.clawling`, which a second
|
|
840
|
-
|
|
867
|
+
// Per-instance AND per-account HOME for the liveware CLI: its credentials
|
|
868
|
+
// otherwise live in the host-global `$HOME/.clawling`, which a second
|
|
869
|
+
// profile — or a second account in this very process — clobbers.
|
|
870
|
+
cliHome: livewareCliHomeDir(stateDirForSample, accountId),
|
|
841
871
|
cliReady: livewareCliReady,
|
|
842
872
|
listApps: async () => {
|
|
843
873
|
const client = getConversationApiClient();
|
|
@@ -886,7 +916,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
886
916
|
? runtimeConfig.mutateConfigFile
|
|
887
917
|
: undefined;
|
|
888
918
|
};
|
|
889
|
-
// §0/§C.1 — write the
|
|
919
|
+
// §0/§C.1 — write the account slot's `token`/`refreshToken` keys. `tokens=null`
|
|
890
920
|
// blanks them (auto-logout); otherwise persists the rotated pair.
|
|
891
921
|
const persistConfigTokens = async (tokens) => {
|
|
892
922
|
const mutateConfigFile = resolveMutateConfigFile();
|
|
@@ -897,17 +927,10 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
897
927
|
await mutateConfigFile({
|
|
898
928
|
afterWrite: { mode: "none", reason: "clawchat-plugin-openclaw token refresh" },
|
|
899
929
|
mutate(draft) {
|
|
900
|
-
|
|
901
|
-
const existing = (channels[CHANNEL_ID] ?? {});
|
|
902
|
-
const nextSection = {
|
|
903
|
-
...existing,
|
|
930
|
+
Object.assign(draft, patchOpenclawClawlingAccountSection(draft, accountId, {
|
|
904
931
|
token: tokens ? tokens.accessToken : "",
|
|
905
932
|
refreshToken: tokens ? tokens.refreshToken : "",
|
|
906
|
-
};
|
|
907
|
-
Object.assign(draft, {
|
|
908
|
-
...draft,
|
|
909
|
-
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
910
|
-
});
|
|
933
|
+
}));
|
|
911
934
|
},
|
|
912
935
|
});
|
|
913
936
|
};
|
|
@@ -996,7 +1019,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
996
1019
|
(activationAccount.source === "sqlite" && store?.getActivationCredentials
|
|
997
1020
|
? store.getActivationCredentials({ platform: "openclaw", accountId })?.refreshToken
|
|
998
1021
|
: null) ??
|
|
999
|
-
readConfigRefreshToken(cfg);
|
|
1022
|
+
readConfigRefreshToken(cfg, accountId);
|
|
1000
1023
|
const refreshManager = new RefreshManager({
|
|
1001
1024
|
baseUrl: account.baseUrl,
|
|
1002
1025
|
deviceId: refreshDeviceId,
|
|
@@ -8,13 +8,28 @@ function key(accountId, chatId, scopeId) {
|
|
|
8
8
|
export function runWithTerminalClawChatSendScope(scopeId, fn) {
|
|
9
9
|
return terminalSendScope.run(scopeId, fn);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Drop every marker whose TTL has lapsed. A marker is only otherwise deleted
|
|
13
|
+
* when the same dispatch scope consumes it, and a scope that never produces a
|
|
14
|
+
* follow-up reply for that chat (mention sent to another chat, `<clawchat:no-reply/>`,
|
|
15
|
+
* dispatch failure) leaves it behind; without this sweep the map would grow by
|
|
16
|
+
* one entry per such turn for the lifetime of the gateway.
|
|
17
|
+
*/
|
|
18
|
+
function sweepExpiredTerminalSends(nowMs) {
|
|
19
|
+
for (const [recordKey, record] of terminalSends) {
|
|
20
|
+
if (record.expiresAt <= nowMs)
|
|
21
|
+
terminalSends.delete(recordKey);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
11
24
|
export function markTerminalClawChatSend(params) {
|
|
12
25
|
const scopeId = params.scopeId ?? terminalSendScope.getStore();
|
|
13
26
|
if (!scopeId)
|
|
14
27
|
return;
|
|
28
|
+
const nowMs = params.nowMs ?? Date.now();
|
|
29
|
+
sweepExpiredTerminalSends(nowMs);
|
|
15
30
|
terminalSends.set(key(params.accountId, params.chatId, scopeId), {
|
|
16
31
|
messageId: params.messageId,
|
|
17
|
-
expiresAt:
|
|
32
|
+
expiresAt: nowMs + (params.ttlMs ?? DEFAULT_TTL_MS),
|
|
18
33
|
scopeId,
|
|
19
34
|
});
|
|
20
35
|
}
|