openclaw-openagent 1.0.8 → 1.0.11

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.
Files changed (39) hide show
  1. package/dist/src/app/remote-agent-tool.js +108 -10
  2. package/dist/src/auth/config.js +65 -10
  3. package/dist/src/channel.js +2 -1
  4. package/dist/src/plugin-ui/assets/bg.png +0 -0
  5. package/dist/src/plugin-ui/assets/icon.png +0 -0
  6. package/dist/src/plugin-ui/assets/openagent-override.js +1753 -774
  7. package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +53 -5
  8. package/dist/src/proxy/auth-proxy.d.ts +1 -0
  9. package/dist/src/proxy/auth-proxy.js +28 -9
  10. package/dist/src/transport/oasn/oasn-http.d.ts +12 -0
  11. package/dist/src/transport/oasn/oasn-http.js +45 -14
  12. package/dist/src/util/url-resolver.d.ts +6 -0
  13. package/dist/src/util/url-resolver.js +24 -8
  14. package/openclaw.plugin.json +16 -1
  15. package/package.json +3 -3
  16. package/src/app/remote-agent-tool.ts +126 -11
  17. package/src/auth/config.ts +79 -10
  18. package/src/channel.ts +2 -1
  19. package/src/plugin-ui/assets/bg.png +0 -0
  20. package/src/plugin-ui/assets/icon.png +0 -0
  21. package/src/plugin-ui/assets/openagent-override.js +1753 -774
  22. package/src/plugin-ui/build.cjs +80 -4
  23. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +92 -9
  24. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +26 -10
  25. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +1 -1
  26. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +64 -0
  27. package/src/plugin-ui/modules/agent-book/panel/styles.js +313 -178
  28. package/src/plugin-ui/modules/loader/bootstrap.js +1 -1
  29. package/src/plugin-ui/modules/loader/shared-state.js +54 -0
  30. package/src/plugin-ui/modules/remote-agent/execution-card.js +347 -124
  31. package/src/plugin-ui/modules/remote-agent/media-links.js +151 -0
  32. package/src/plugin-ui/modules/remote-agent/output-card.js +110 -33
  33. package/src/plugin-ui/modules/remote-agent/render-hooks.js +97 -18
  34. package/src/plugin-ui/modules/remote-agent/styles.js +488 -402
  35. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +6 -0
  36. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +50 -5
  37. package/src/proxy/auth-proxy.ts +30 -10
  38. package/src/transport/oasn/oasn-http.ts +60 -14
  39. package/src/util/url-resolver.ts +24 -8
@@ -23,6 +23,8 @@ function _clParseToolCardModel(card) {
23
23
  var agentId = args.agent_id || args.agentId || null;
24
24
  var agentName = args.agent_name || args.agentName || agentId || 'Remote Agent';
25
25
  var agentAvatar = args.agent_avatar || args.agentAvatar || args.avatar || args.avatar_url || args.avatarUrl || null;
26
+ var agentBio = args.agent_bio || args.agentBio || args.description || args.agent_description || args.MOM || '';
27
+ var agentStars = args.agent_stars || args.agentStars || args.stars || args.claws || '3.2万';
26
28
  var taskText = args.task || args.message || args.instruction || '';
27
29
 
28
30
  // 从 text 解析思考步骤 / 最终文本
@@ -38,10 +40,14 @@ function _clParseToolCardModel(card) {
38
40
  agentId: agentId,
39
41
  agentName: agentName,
40
42
  agentAvatar: agentAvatar,
43
+ agentBio: agentBio,
44
+ agentStars: agentStars,
41
45
  taskText: taskText,
42
46
  toolCallId: card.toolCallId || '',
43
47
  status: hasOutput ? 'completed' : 'executing',
44
48
  rawText: rawText,
49
+ text: rawText,
50
+ outputText: rawText,
45
51
  chunks: chunks,
46
52
  isError: isError,
47
53
  inferredKind: inferredKind,
@@ -18,6 +18,8 @@ import type { UIExtension } from './types.js';
18
18
  // ── 常量 ──────────────────────────────────────────────────────────────────
19
19
 
20
20
  const OVERRIDE_ASSET_URL = new URL('../assets/openagent-override.js', import.meta.url);
21
+ const ICON_ASSET_URL = new URL('../assets/icon.png', import.meta.url);
22
+ const BG_ASSET_URL = new URL('../assets/bg.png', import.meta.url);
21
23
 
22
24
  function getOverrideAssetHash(): string {
23
25
  return createHash('sha256')
@@ -123,6 +125,27 @@ const overrideScript: UIExtension = {
123
125
  }
124
126
  copyFileSync(OVERRIDE_ASSET_URL, dest);
125
127
  logger.info('[ui-ext:regex] override-script: deployed openagent-override.js');
128
+
129
+ // Deploy icon.png alongside
130
+ if (existsSync(ICON_ASSET_URL)) {
131
+ const iconDest = join(controlUiDir, 'icon.png');
132
+ try {
133
+ copyFileSync(ICON_ASSET_URL, iconDest);
134
+ logger.info('[ui-ext:regex] override-script: deployed icon.png');
135
+ } catch (err) {
136
+ logger.warn('[ui-ext:regex] override-script: failed to deploy icon.png');
137
+ }
138
+ }
139
+ if (existsSync(BG_ASSET_URL)) {
140
+ const bgDest = join(controlUiDir, 'bg.png');
141
+ try {
142
+ copyFileSync(BG_ASSET_URL, bgDest);
143
+ logger.info('[ui-ext:regex] override-script: deployed bg.png');
144
+ } catch (err) {
145
+ logger.warn('[ui-ext:regex] override-script: failed to deploy bg.png');
146
+ }
147
+ }
148
+
126
149
  return true;
127
150
  },
128
151
 
@@ -132,6 +155,20 @@ const overrideScript: UIExtension = {
132
155
  unlinkSync(dest);
133
156
  logger.info('[ui-ext:regex] override-script: removed');
134
157
  }
158
+ const iconDest = join(controlUiDir, 'icon.png');
159
+ if (existsSync(iconDest)) {
160
+ try {
161
+ unlinkSync(iconDest);
162
+ logger.info('[ui-ext:regex] override-script: removed icon.png');
163
+ } catch {}
164
+ }
165
+ const bgDest = join(controlUiDir, 'bg.png');
166
+ if (existsSync(bgDest)) {
167
+ try {
168
+ unlinkSync(bgDest);
169
+ logger.info('[ui-ext:regex] override-script: removed bg.png');
170
+ } catch {}
171
+ }
135
172
  },
136
173
  };
137
174
 
@@ -139,7 +176,7 @@ const overrideScript: UIExtension = {
139
176
 
140
177
  const htmlScriptTag: UIExtension = {
141
178
  id: 'html-script-tag',
142
- version: 4,
179
+ version: 5,
143
180
  alwaysApply: true,
144
181
 
145
182
  apply(controlUiDir: string): boolean {
@@ -150,12 +187,21 @@ const htmlScriptTag: UIExtension = {
150
187
  }
151
188
  let html = readFileSync(indexPath, 'utf-8');
152
189
  const htmlMarker = `<!--openagent:${this.id}-->`;
190
+ const markerPattern = /(?:\/\*openagent:html-script-tag\*\/|<!--openagent:html-script-tag-->)/;
153
191
  const overrideHash = getOverrideAssetHash();
154
192
  const scriptTag = `${htmlMarker}<script src="./openagent-override.js?v=${overrideHash}"></script>`;
155
193
  const markedScriptPattern = /(?:\/\*openagent:html-script-tag\*\/|<!--openagent:html-script-tag-->)<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
194
+ const oldScriptPattern = /<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
156
195
 
157
- if (html.includes(htmlMarker) || hasMarker(html, this.id)) {
158
- const nextHtml = html.replace(markedScriptPattern, `${scriptTag}\n `);
196
+ if (markerPattern.test(html)) {
197
+ let nextHtml = html.replace(markedScriptPattern, `${scriptTag}\n `);
198
+ if (nextHtml === html && !nextHtml.includes('openagent-override.js')) {
199
+ nextHtml = nextHtml.replace(markerPattern, `${scriptTag}\n `);
200
+ } else if (nextHtml === html && nextHtml.includes('openagent-override.js')) {
201
+ nextHtml = nextHtml
202
+ .replace(oldScriptPattern, '')
203
+ .replace(markerPattern, `${scriptTag}\n `);
204
+ }
159
205
  if (nextHtml === html) {
160
206
  logger.debug('[ui-ext:regex] html-script-tag: already applied');
161
207
  } else {
@@ -164,7 +210,6 @@ const htmlScriptTag: UIExtension = {
164
210
  }
165
211
  return true;
166
212
  }
167
- const oldScriptPattern = /<script[^>]*openagent-override[^>]*><\/script>\n?\s*/g;
168
213
  if (oldScriptPattern.test(html)) {
169
214
  html = html.replace(oldScriptPattern, '');
170
215
  }
@@ -354,7 +399,7 @@ const toolcardRender: UIExtension = {
354
399
  }
355
400
  if (toolMessageOk) {
356
401
  const toolMessageHook =
357
- `${marker}let __openagentToolMsgCard=null;console.log('[openagent] toolMsg hook: cards='+${cardsVar}.length+' mediaVar='+${mediaVar});if(!${mediaVar}&&window.__openagentRenderToolCard){for(let __openagentI=${cardsVar}.length-1;__openagentI>=0&&!__openagentToolMsgCard;__openagentI--){let __openagentCandidate=${cardsVar}[__openagentI];if(__openagentCandidate&&(__openagentCandidate.outputText||__openagentCandidate.text))__openagentToolMsgCard=window.__openagentRenderToolCard(__openagentCandidate,${toolMessageSidebarArg})}for(let __openagentI=0;__openagentI<${cardsVar}.length&&!__openagentToolMsgCard;__openagentI++){__openagentToolMsgCard=window.__openagentRenderToolCard(${cardsVar}[__openagentI],${toolMessageSidebarArg})}}if(__openagentToolMsgCard)return ${toolMessageHtmlVar}\`<div class="\${${chatBubbleClassVar}}">\${__openagentToolMsgCard}</div>\`;`;
402
+ `${marker}/* openagent-media-aware-tool-message */let __openagentToolMsgCard=null;if(window.__openagentRenderToolCard){for(let __openagentI=${cardsVar}.length-1;__openagentI>=0&&!__openagentToolMsgCard;__openagentI--){let __openagentCandidate=${cardsVar}[__openagentI];if(__openagentCandidate&&(__openagentCandidate.outputText||__openagentCandidate.text))__openagentToolMsgCard=window.__openagentRenderToolCard(__openagentCandidate,${toolMessageSidebarArg})}for(let __openagentI=0;__openagentI<${cardsVar}.length&&!__openagentToolMsgCard;__openagentI++){__openagentToolMsgCard=window.__openagentRenderToolCard(${cardsVar}[__openagentI],${toolMessageSidebarArg})}}if(__openagentToolMsgCard)return ${toolMessageHtmlVar}\`<div class="\${${chatBubbleClassVar}}">\${__openagentToolMsgCard}</div>\`;`;
358
403
  const toolMessageInsertAt = toolMessageFn!.bodyStart + toolMessageReturnIdx;
359
404
  content = content.substring(0, toolMessageInsertAt) + toolMessageHook + content.substring(toolMessageInsertAt);
360
405
  patchedToolMessageWrapper = true;
@@ -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 ?? `http://localhost:18794`;
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 = new URL(targetPath + url.search, upstreamBase).href;
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: RequestOptions = {
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: RequestOptions = {
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;
@@ -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 new URL(url, accessApiBase).toString();
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 claimOrigin = originOf(claimUrl);
34
- if (!claimOrigin || !isWebuiUrl(url, accessApiBase || claimOrigin)) {
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(url, accessApiBase || claimOrigin);
40
- return new URL(`${absolute.pathname}${absolute.search}${absolute.hash}`, claimOrigin).toString();
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 originOf(value: string | undefined): string | undefined {
59
+ function publicBaseOfClaimUrl(value: string | undefined): string | undefined {
47
60
  if (!value) return undefined;
48
61
  try {
49
- return new URL(value).origin;
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(url, base).pathname.startsWith('/webui/');
73
+ return /(?:^|\/)webui\//.test(new URL(resolveOasnAccessUrl(base, url)).pathname);
58
74
  } catch {
59
75
  return false;
60
76
  }