openclaw-openagent 1.0.8 → 1.0.9
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/app/remote-agent-tool.js +108 -10
- package/dist/src/auth/config.js +65 -10
- package/dist/src/channel.js +2 -1
- package/dist/src/plugin-ui/assets/openagent-override.js +257 -5
- package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +1 -1
- package/dist/src/proxy/auth-proxy.d.ts +1 -0
- package/dist/src/proxy/auth-proxy.js +28 -9
- package/dist/src/transport/oasn/oasn-http.d.ts +12 -0
- package/dist/src/transport/oasn/oasn-http.js +45 -14
- package/dist/src/util/url-resolver.d.ts +6 -0
- package/dist/src/util/url-resolver.js +24 -8
- package/openclaw.plugin.json +15 -0
- package/package.json +1 -1
- package/src/app/remote-agent-tool.ts +126 -11
- package/src/auth/config.ts +79 -10
- package/src/channel.ts +2 -1
- package/src/plugin-ui/assets/openagent-override.js +257 -5
- package/src/plugin-ui/build.cjs +1 -0
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +18 -0
- package/src/plugin-ui/modules/remote-agent/media-links.js +151 -0
- package/src/plugin-ui/modules/remote-agent/output-card.js +21 -4
- package/src/plugin-ui/modules/remote-agent/styles.js +61 -0
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +1 -1
- package/src/proxy/auth-proxy.ts +30 -10
- package/src/transport/oasn/oasn-http.ts +60 -14
- package/src/util/url-resolver.ts +24 -8
package/src/proxy/auth-proxy.ts
CHANGED
|
@@ -20,6 +20,7 @@ import nodePath from 'node:path';
|
|
|
20
20
|
import { logger } from '../util/logger.js';
|
|
21
21
|
import { registry } from '../runtime/registry.js';
|
|
22
22
|
import { isUnconfiguredOasnApiBase } from '../auth/config.js';
|
|
23
|
+
import { resolveOasnRelativeUrl } from '../util/url-resolver.js';
|
|
23
24
|
|
|
24
25
|
/** TIM 路径默认上游(兼容旧版) */
|
|
25
26
|
const AUTH_UPSTREAM_TIM = 'https://auth.ai-talk.live';
|
|
@@ -62,7 +63,7 @@ let _gatewayBaseUrl: string | undefined;
|
|
|
62
63
|
|
|
63
64
|
/** Returns the gateway base URL as seen by the browser (e.g. http://localhost:18794). */
|
|
64
65
|
export function getGatewayBaseUrl(): string {
|
|
65
|
-
return _gatewayBaseUrl ??
|
|
66
|
+
return _gatewayBaseUrl ?? 'http://localhost:18794';
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
// ── Handler factory ─────────────────────────────────────────────────────
|
|
@@ -86,20 +87,19 @@ export function createAuthProxyHandler() {
|
|
|
86
87
|
logger.info(`[proxy] Gateway base URL detected: ${_gatewayBaseUrl}`);
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
// ── T4: Serve local media files from /tmp/openclaw/openagent-media/ ──
|
|
90
|
-
// Files downloaded from remote agents via C2C are stored here.
|
|
91
|
-
// Tool result references them as MEDIA:http://localhost:PORT/plugins/openagent/media/{filename}
|
|
92
|
-
// to pass through the framework's filterToolResultMediaUrls (http URLs bypass whitelist check).
|
|
93
|
-
if (url.pathname.startsWith('/media/')) {
|
|
94
|
-
return serveLocalMedia(url.pathname, res);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
90
|
// req.url may or may not have the /plugins/openagent prefix stripped by Gateway
|
|
98
91
|
// (behavior varies across Gateway versions), so strip it defensively.
|
|
99
92
|
let targetPath = url.pathname;
|
|
100
93
|
if (targetPath.startsWith('/plugins/openagent')) {
|
|
101
94
|
targetPath = targetPath.slice('/plugins/openagent'.length);
|
|
102
95
|
}
|
|
96
|
+
// ── T4: Serve local media files from /tmp/openclaw/openagent-media/ ──
|
|
97
|
+
// Files downloaded from remote agents are stored here. The media route must
|
|
98
|
+
// be detected after prefix normalization because Gateway versions disagree
|
|
99
|
+
// on whether /plugins/openagent has already been stripped.
|
|
100
|
+
if (targetPath.startsWith('/media/')) {
|
|
101
|
+
return serveLocalMedia(targetPath, res);
|
|
102
|
+
}
|
|
103
103
|
// v3.9+ 双轨抽象:上游按当前 transport 切换(OASN → api.oasn.ai;TIM → auth.ai-talk.live)
|
|
104
104
|
const method = req.method ?? 'GET';
|
|
105
105
|
const rt = registry.getDefault();
|
|
@@ -128,7 +128,7 @@ export function createAuthProxyHandler() {
|
|
|
128
128
|
writeJson(res, 503, { error: 'OASN api_base is not configured' });
|
|
129
129
|
return true;
|
|
130
130
|
}
|
|
131
|
-
const targetUrl =
|
|
131
|
+
const targetUrl = resolveOasnProxyTargetUrl(upstreamBase, targetPath, url.search);
|
|
132
132
|
|
|
133
133
|
logger.info(`[proxy] ${method} ${url.pathname} → ${targetUrl}`);
|
|
134
134
|
|
|
@@ -193,6 +193,10 @@ export function createAuthProxyHandler() {
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
export function resolveOasnProxyTargetUrl(upstreamBase: string, targetPath: string, search = ''): string {
|
|
197
|
+
return resolveOasnRelativeUrl(upstreamBase, `${targetPath}${search}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
196
200
|
// ── T4: Local media serve ────────────────────────────────────────────────
|
|
197
201
|
|
|
198
202
|
const MEDIA_DIR = '/tmp/openclaw/openagent-media';
|
|
@@ -212,6 +216,12 @@ const MIME_MAP: Record<string, string> = {
|
|
|
212
216
|
'.webp': 'image/webp',
|
|
213
217
|
'.pdf': 'application/pdf',
|
|
214
218
|
'.txt': 'text/plain',
|
|
219
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
220
|
+
'.json': 'application/json; charset=utf-8',
|
|
221
|
+
'.zip': 'application/zip',
|
|
222
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
223
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
224
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
215
225
|
};
|
|
216
226
|
|
|
217
227
|
/**
|
|
@@ -259,6 +269,9 @@ function serveLocalMedia(pathname: string, res: ServerResponse): true {
|
|
|
259
269
|
|
|
260
270
|
res.setHeader('Content-Type', contentType);
|
|
261
271
|
res.setHeader('Content-Length', stat.size);
|
|
272
|
+
if (shouldDownloadAsAttachment(contentType)) {
|
|
273
|
+
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
|
274
|
+
}
|
|
262
275
|
// Allow browser to cache media for 1 hour
|
|
263
276
|
res.setHeader('Cache-Control', 'private, max-age=3600');
|
|
264
277
|
|
|
@@ -284,6 +297,13 @@ function serveLocalMedia(pathname: string, res: ServerResponse): true {
|
|
|
284
297
|
return true;
|
|
285
298
|
}
|
|
286
299
|
|
|
300
|
+
function shouldDownloadAsAttachment(contentType: string): boolean {
|
|
301
|
+
return !contentType.startsWith('image/')
|
|
302
|
+
&& !contentType.startsWith('audio/')
|
|
303
|
+
&& !contentType.startsWith('video/')
|
|
304
|
+
&& !contentType.startsWith('text/plain');
|
|
305
|
+
}
|
|
306
|
+
|
|
287
307
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
288
308
|
|
|
289
309
|
/**
|
|
@@ -59,6 +59,62 @@ export interface OasnRequestOptions {
|
|
|
59
59
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
60
60
|
const DEFAULT_MAX_RETRIES = 3;
|
|
61
61
|
|
|
62
|
+
type GlobalAgentState = {
|
|
63
|
+
NO_PROXY?: string | null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type GlobalWithAgent = typeof globalThis & {
|
|
67
|
+
GLOBAL_AGENT?: GlobalAgentState;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* OpenClaw gateway may install global-agent for model/provider traffic. OASN
|
|
72
|
+
* REL endpoints should keep their original TLS hostname; otherwise a local
|
|
73
|
+
* proxy can turn `https://oasn-test...` into a certificate check against
|
|
74
|
+
* `localhost`.
|
|
75
|
+
*/
|
|
76
|
+
export function ensureOasnHostBypassesGlobalAgentProxy(hostname: string): void {
|
|
77
|
+
const normalized = hostname.trim().toLowerCase();
|
|
78
|
+
if (!normalized || normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1') return;
|
|
79
|
+
|
|
80
|
+
const globalAgent = (globalThis as GlobalWithAgent).GLOBAL_AGENT;
|
|
81
|
+
if (!globalAgent || typeof globalAgent !== 'object') return;
|
|
82
|
+
|
|
83
|
+
const existing = String(globalAgent.NO_PROXY || '');
|
|
84
|
+
const entries = existing.split(/[\s,]+/).filter(Boolean);
|
|
85
|
+
if (entries.some((entry) => entry.toLowerCase() === normalized)) return;
|
|
86
|
+
|
|
87
|
+
globalAgent.NO_PROXY = entries.length > 0 ? `${entries.join(',')},${normalized}` : normalized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
91
|
+
const lower = name.toLowerCase();
|
|
92
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === lower);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function buildNodeRequestOptions(
|
|
96
|
+
url: URL,
|
|
97
|
+
method: string,
|
|
98
|
+
headers: Record<string, string>,
|
|
99
|
+
): RequestOptions & { servername?: string } {
|
|
100
|
+
const isHttps = url.protocol === 'https:';
|
|
101
|
+
const requestHeaders = { ...headers };
|
|
102
|
+
if (!hasHeader(requestHeaders, 'Host')) {
|
|
103
|
+
requestHeaders.Host = url.host;
|
|
104
|
+
}
|
|
105
|
+
const options: RequestOptions & { servername?: string } = {
|
|
106
|
+
method,
|
|
107
|
+
headers: requestHeaders,
|
|
108
|
+
hostname: url.hostname,
|
|
109
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
110
|
+
path: `${url.pathname}${url.search}`,
|
|
111
|
+
};
|
|
112
|
+
if (isHttps) {
|
|
113
|
+
options.servername = url.hostname;
|
|
114
|
+
}
|
|
115
|
+
return options;
|
|
116
|
+
}
|
|
117
|
+
|
|
62
118
|
// ───────────────────────────────────────────────────────────────
|
|
63
119
|
// 错误转换
|
|
64
120
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -354,15 +410,10 @@ export class OasnHttpClient {
|
|
|
354
410
|
): Promise<{ statusCode: number; headers: Record<string, string>; body: string }> {
|
|
355
411
|
const isHttps = url.protocol === 'https:';
|
|
356
412
|
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
413
|
+
ensureOasnHostBypassesGlobalAgentProxy(url.hostname);
|
|
357
414
|
|
|
358
415
|
return new Promise((resolve, reject) => {
|
|
359
|
-
const reqOptions
|
|
360
|
-
method,
|
|
361
|
-
headers,
|
|
362
|
-
hostname: url.hostname,
|
|
363
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
364
|
-
path: `${url.pathname}${url.search}`,
|
|
365
|
-
};
|
|
416
|
+
const reqOptions = buildNodeRequestOptions(url, method, headers);
|
|
366
417
|
|
|
367
418
|
let settled = false;
|
|
368
419
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -421,15 +472,10 @@ export class OasnHttpClient {
|
|
|
421
472
|
): Promise<{ content: ArrayBuffer; mimeType: string; displayName: string }> {
|
|
422
473
|
const isHttps = url.protocol === 'https:';
|
|
423
474
|
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
475
|
+
ensureOasnHostBypassesGlobalAgentProxy(url.hostname);
|
|
424
476
|
|
|
425
477
|
return new Promise((resolve, reject) => {
|
|
426
|
-
const reqOptions
|
|
427
|
-
method: 'GET',
|
|
428
|
-
headers,
|
|
429
|
-
hostname: url.hostname,
|
|
430
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
431
|
-
path: `${url.pathname}${url.search}`,
|
|
432
|
-
};
|
|
478
|
+
const reqOptions = buildNodeRequestOptions(url, 'GET', headers);
|
|
433
479
|
|
|
434
480
|
let settled = false;
|
|
435
481
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
package/src/util/url-resolver.ts
CHANGED
|
@@ -10,12 +10,23 @@ export function resolveOasnAccessUrl(accessApiBase: string | undefined, url: str
|
|
|
10
10
|
if (/^https?:\/\//i.test(url)) return url;
|
|
11
11
|
if (!accessApiBase) return url;
|
|
12
12
|
try {
|
|
13
|
-
return
|
|
13
|
+
return resolveOasnRelativeUrl(accessApiBase, url);
|
|
14
14
|
} catch {
|
|
15
15
|
return url;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Resolve an OASN API/WebUI-relative path without losing env prefixes such as
|
|
21
|
+
* `/e/limj1`. `new URL('/api/..', 'https://host/e/limj1')` resets to the
|
|
22
|
+
* origin root, so use explicit path joining for env-scoped public bases.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveOasnRelativeUrl(accessApiBase: string, path: string): string {
|
|
25
|
+
const base = accessApiBase.replace(/\/+$/, '');
|
|
26
|
+
const suffix = path.startsWith('/') ? path : `/${path}`;
|
|
27
|
+
return new URL(`${base}${suffix}`).toString();
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* Resolve a WebUI continuation URL for user-facing browser clicks.
|
|
21
32
|
*
|
|
@@ -30,23 +41,28 @@ export function resolveOasnWebuiUrl(
|
|
|
30
41
|
): string {
|
|
31
42
|
if (!url) return url;
|
|
32
43
|
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
44
|
+
const claimPublicBase = publicBaseOfClaimUrl(claimUrl);
|
|
45
|
+
if (!claimPublicBase || !isWebuiUrl(url, accessApiBase || claimPublicBase)) {
|
|
35
46
|
return resolveOasnAccessUrl(accessApiBase, url);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
try {
|
|
39
|
-
const absolute = new URL(
|
|
40
|
-
|
|
50
|
+
const absolute = new URL(resolveOasnAccessUrl(accessApiBase || claimPublicBase, url));
|
|
51
|
+
const webuiIndex = absolute.pathname.indexOf('/webui/');
|
|
52
|
+
const path = webuiIndex >= 0 ? absolute.pathname.slice(webuiIndex) : absolute.pathname;
|
|
53
|
+
return resolveOasnRelativeUrl(claimPublicBase, `${path}${absolute.search}${absolute.hash}`);
|
|
41
54
|
} catch {
|
|
42
55
|
return resolveOasnAccessUrl(accessApiBase, url);
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
function
|
|
59
|
+
function publicBaseOfClaimUrl(value: string | undefined): string | undefined {
|
|
47
60
|
if (!value) return undefined;
|
|
48
61
|
try {
|
|
49
|
-
|
|
62
|
+
const parsed = new URL(value);
|
|
63
|
+
const claimPathIndex = parsed.pathname.indexOf('/agents/claim');
|
|
64
|
+
if (claimPathIndex <= 0) return parsed.origin;
|
|
65
|
+
return `${parsed.origin}${parsed.pathname.slice(0, claimPathIndex)}`;
|
|
50
66
|
} catch {
|
|
51
67
|
return undefined;
|
|
52
68
|
}
|
|
@@ -54,7 +70,7 @@ function originOf(value: string | undefined): string | undefined {
|
|
|
54
70
|
|
|
55
71
|
function isWebuiUrl(url: string, base: string): boolean {
|
|
56
72
|
try {
|
|
57
|
-
return new URL(
|
|
73
|
+
return /(?:^|\/)webui\//.test(new URL(resolveOasnAccessUrl(base, url)).pathname);
|
|
58
74
|
} catch {
|
|
59
75
|
return false;
|
|
60
76
|
}
|