@hywkp/test-openclaw-sider 1.0.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/README.md +65 -0
- package/README.zh_CN.md +106 -0
- package/index.ts +80 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +51 -0
- package/setup-entry.ts +6 -0
- package/src/account.ts +350 -0
- package/src/auth.ts +292 -0
- package/src/channel.ts +3864 -0
- package/src/config.ts +29 -0
- package/src/inbound-media.ts +196 -0
- package/src/media-upload.ts +983 -0
- package/src/remote-browser-support.ts +64 -0
- package/src/setup-core.ts +431 -0
- package/src/user-agent.ts +17 -0
package/src/auth.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { type OpenClawConfig, type PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
|
3
|
+
import { type OpenClawPluginService } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
|
+
import {
|
|
5
|
+
SIDER_CHANNEL_ID,
|
|
6
|
+
SIDER_DEFAULT_BASE_URL,
|
|
7
|
+
readDefaultSiderSetupTokenEnv,
|
|
8
|
+
readSiderBaseUrlEnv,
|
|
9
|
+
} from "./config.js";
|
|
10
|
+
import { SIDER_USER_AGENT } from "./user-agent.js";
|
|
11
|
+
|
|
12
|
+
export const SIDER_AUTH_EXCHANGE_API_PATH = "/v1/claws/register";
|
|
13
|
+
export type SiderSetupConfigSnapshot = {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
setupToken?: string;
|
|
16
|
+
token?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SiderRegisterResponse = {
|
|
20
|
+
claw_id: string;
|
|
21
|
+
token: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SiderResolvedCredentials = {
|
|
25
|
+
token: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SiderAuthSetupServiceParams = {
|
|
29
|
+
listAccountIds: (cfg: OpenClawConfig) => string[];
|
|
30
|
+
getAccountSetupConfig: (
|
|
31
|
+
cfg: OpenClawConfig,
|
|
32
|
+
accountId: string,
|
|
33
|
+
) => SiderSetupConfigSnapshot;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let runtimeRef: PluginRuntime | null = null;
|
|
37
|
+
const pendingAccountOperations = new Map<string, Promise<OpenClawConfig>>();
|
|
38
|
+
|
|
39
|
+
function getSiderAuthRuntime(): PluginRuntime {
|
|
40
|
+
if (!runtimeRef) {
|
|
41
|
+
throw new Error("sider auth runtime not initialized");
|
|
42
|
+
}
|
|
43
|
+
return runtimeRef;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function trimMaybe(value: string | undefined): string | undefined {
|
|
47
|
+
const trimmed = value?.trim();
|
|
48
|
+
return trimmed || undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeGatewayUrl(raw?: string): string | undefined {
|
|
52
|
+
const trimmed = trimMaybe(raw);
|
|
53
|
+
return trimmed ? trimmed.replace(/\/+$/, "") : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveSiderBaseUrl(): string {
|
|
57
|
+
return normalizeGatewayUrl(readSiderBaseUrlEnv()) ?? SIDER_DEFAULT_BASE_URL;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getPendingOperation(accountId: string): Promise<OpenClawConfig> | undefined {
|
|
61
|
+
return pendingAccountOperations.get(accountId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function withPendingOperation(
|
|
65
|
+
accountId: string,
|
|
66
|
+
run: () => Promise<OpenClawConfig>,
|
|
67
|
+
): Promise<OpenClawConfig> {
|
|
68
|
+
const existing = getPendingOperation(accountId);
|
|
69
|
+
if (existing) {
|
|
70
|
+
return await existing;
|
|
71
|
+
}
|
|
72
|
+
const pending = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
return await run();
|
|
75
|
+
} finally {
|
|
76
|
+
pendingAccountOperations.delete(accountId);
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
pendingAccountOperations.set(accountId, pending);
|
|
80
|
+
return await pending;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildSiderApiUrl(pathname: string): string {
|
|
84
|
+
const url = new URL(resolveSiderBaseUrl());
|
|
85
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
86
|
+
const suffix = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
87
|
+
url.pathname = `${basePath}${suffix}`;
|
|
88
|
+
url.search = "";
|
|
89
|
+
url.hash = "";
|
|
90
|
+
return url.toString();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveSiderApiUrl(pathname: string): string {
|
|
94
|
+
return buildSiderApiUrl(pathname);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatAuthorizationHeader(token: string): string {
|
|
98
|
+
const trimmed = token.trim();
|
|
99
|
+
if (!trimmed) {
|
|
100
|
+
return trimmed;
|
|
101
|
+
}
|
|
102
|
+
return /^Bearer\s+/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function appendTokenQuery(url: string, token?: string): string {
|
|
106
|
+
const trimmed = token?.trim();
|
|
107
|
+
if (!trimmed) {
|
|
108
|
+
return url;
|
|
109
|
+
}
|
|
110
|
+
const next = new URL(url);
|
|
111
|
+
next.searchParams.set("token", trimmed);
|
|
112
|
+
return next.toString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseRegisterResponse(payload: unknown): SiderRegisterResponse {
|
|
116
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
117
|
+
throw new Error("selfclaw register response is not a JSON object");
|
|
118
|
+
}
|
|
119
|
+
const clawId = trimMaybe(
|
|
120
|
+
typeof (payload as Record<string, unknown>).claw_id === "string"
|
|
121
|
+
? ((payload as Record<string, unknown>).claw_id as string)
|
|
122
|
+
: undefined,
|
|
123
|
+
);
|
|
124
|
+
const token = trimMaybe(
|
|
125
|
+
typeof (payload as Record<string, unknown>).token === "string"
|
|
126
|
+
? ((payload as Record<string, unknown>).token as string)
|
|
127
|
+
: undefined,
|
|
128
|
+
);
|
|
129
|
+
if (!clawId || !token) {
|
|
130
|
+
throw new Error("selfclaw register response missing claw_id/token");
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
claw_id: clawId,
|
|
134
|
+
token,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function exchangeSetupToken(params: {
|
|
139
|
+
pathname: string;
|
|
140
|
+
authorization: string;
|
|
141
|
+
}): Promise<SiderResolvedCredentials> {
|
|
142
|
+
const url = buildSiderApiUrl(params.pathname);
|
|
143
|
+
const response = await fetch(url, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
Authorization: formatAuthorizationHeader(params.authorization),
|
|
147
|
+
"User-Agent": SIDER_USER_AGENT,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
let detail = "";
|
|
152
|
+
try {
|
|
153
|
+
const text = (await response.text()).trim();
|
|
154
|
+
if (text) {
|
|
155
|
+
detail = `: ${text}`;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore body read failures and keep the status-only error.
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`selfclaw request failed (${response.status} ${response.statusText})${detail}`);
|
|
161
|
+
}
|
|
162
|
+
let payload: unknown = null;
|
|
163
|
+
try {
|
|
164
|
+
payload = await response.json();
|
|
165
|
+
} catch {
|
|
166
|
+
payload = null;
|
|
167
|
+
}
|
|
168
|
+
const parsed = parseRegisterResponse(payload);
|
|
169
|
+
return { token: parsed.token };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function cloneOpenClawConfig(cfg: OpenClawConfig): OpenClawConfig {
|
|
173
|
+
return structuredClone(cfg);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ensureChannelConfig(cfg: OpenClawConfig): Record<string, unknown> {
|
|
177
|
+
const next = cfg as OpenClawConfig & {
|
|
178
|
+
channels?: Record<string, Record<string, unknown>>;
|
|
179
|
+
};
|
|
180
|
+
next.channels ??= {};
|
|
181
|
+
next.channels[SIDER_CHANNEL_ID] ??= {};
|
|
182
|
+
return next.channels[SIDER_CHANNEL_ID]!;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function resolveWritableSiderAccountConfig(
|
|
186
|
+
cfg: OpenClawConfig,
|
|
187
|
+
accountId: string,
|
|
188
|
+
): Record<string, unknown> {
|
|
189
|
+
const channelCfg = ensureChannelConfig(cfg);
|
|
190
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
191
|
+
return channelCfg;
|
|
192
|
+
}
|
|
193
|
+
const accounts =
|
|
194
|
+
typeof channelCfg.accounts === "object" && channelCfg.accounts && !Array.isArray(channelCfg.accounts)
|
|
195
|
+
? (channelCfg.accounts as Record<string, Record<string, unknown>>)
|
|
196
|
+
: ((channelCfg.accounts = {}) as Record<string, Record<string, unknown>>);
|
|
197
|
+
accounts[accountId] ??= {};
|
|
198
|
+
return accounts[accountId]!;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function persistSiderCredentials(params: {
|
|
202
|
+
accountId: string;
|
|
203
|
+
credentials: SiderResolvedCredentials;
|
|
204
|
+
}): Promise<OpenClawConfig> {
|
|
205
|
+
const runtime = getSiderAuthRuntime();
|
|
206
|
+
const latestCfg = cloneOpenClawConfig(runtime.config.loadConfig());
|
|
207
|
+
const accountCfg = resolveWritableSiderAccountConfig(latestCfg, params.accountId);
|
|
208
|
+
accountCfg.token = params.credentials.token;
|
|
209
|
+
delete accountCfg.setupToken;
|
|
210
|
+
if (accountCfg.enabled === undefined) {
|
|
211
|
+
accountCfg.enabled = true;
|
|
212
|
+
}
|
|
213
|
+
await runtime.config.writeConfigFile(latestCfg);
|
|
214
|
+
return latestCfg;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveConfiguredSetupToken(snapshot: SiderSetupConfigSnapshot): string | undefined {
|
|
218
|
+
return trimMaybe(snapshot.setupToken);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function setSiderAuthRuntime(runtime: PluginRuntime): void {
|
|
222
|
+
runtimeRef = runtime;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function resolveSiderSetupToken(
|
|
226
|
+
accountId: string,
|
|
227
|
+
snapshot?: SiderSetupConfigSnapshot,
|
|
228
|
+
): string | undefined {
|
|
229
|
+
const configuredToken = snapshot ? resolveConfiguredSetupToken(snapshot) : undefined;
|
|
230
|
+
if (configuredToken) {
|
|
231
|
+
return configuredToken;
|
|
232
|
+
}
|
|
233
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
234
|
+
return trimMaybe(readDefaultSiderSetupTokenEnv());
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function isSiderAccountSetupPending(accountId: string): boolean {
|
|
240
|
+
return pendingAccountOperations.has(accountId);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function ensureSiderAccountSetup(params: {
|
|
244
|
+
cfg: OpenClawConfig;
|
|
245
|
+
accountId: string;
|
|
246
|
+
getAccountSetupConfig: (
|
|
247
|
+
cfg: OpenClawConfig,
|
|
248
|
+
accountId: string,
|
|
249
|
+
) => SiderSetupConfigSnapshot;
|
|
250
|
+
}): Promise<OpenClawConfig> {
|
|
251
|
+
const snapshot = params.getAccountSetupConfig(params.cfg, params.accountId);
|
|
252
|
+
if (snapshot.enabled === false) {
|
|
253
|
+
return params.cfg;
|
|
254
|
+
}
|
|
255
|
+
const setupToken = resolveSiderSetupToken(params.accountId, snapshot);
|
|
256
|
+
if (!setupToken) {
|
|
257
|
+
return params.cfg;
|
|
258
|
+
}
|
|
259
|
+
return await withPendingOperation(params.accountId, async () => {
|
|
260
|
+
const credentials = await exchangeSetupToken({
|
|
261
|
+
pathname: SIDER_AUTH_EXCHANGE_API_PATH,
|
|
262
|
+
authorization: setupToken,
|
|
263
|
+
});
|
|
264
|
+
return await persistSiderCredentials({
|
|
265
|
+
accountId: params.accountId,
|
|
266
|
+
credentials,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function createSiderAuthSetupService(
|
|
272
|
+
params: SiderAuthSetupServiceParams,
|
|
273
|
+
): OpenClawPluginService {
|
|
274
|
+
return {
|
|
275
|
+
id: "sider-auth-setup",
|
|
276
|
+
start: async (ctx) => {
|
|
277
|
+
const accountIds = params.listAccountIds(ctx.config);
|
|
278
|
+
for (const accountId of accountIds) {
|
|
279
|
+
try {
|
|
280
|
+
await ensureSiderAccountSetup({
|
|
281
|
+
cfg: ctx.config,
|
|
282
|
+
accountId,
|
|
283
|
+
getAccountSetupConfig: params.getAccountSetupConfig,
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
+
ctx.logger.warn(`sider setup failed for "${accountId}": ${message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|