openclaw-openagent 1.0.7 → 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.
@@ -11,20 +11,30 @@
11
11
  // UI 结构复用 shared thought-chain styles;数据来源是 RenderHook model。
12
12
 
13
13
  function _clRenderOutputCard(model, onOpenSidebar) {
14
+ var mediaLinks = typeof _clExtractOpenagentMediaLinksFromModel === 'function'
15
+ ? _clExtractOpenagentMediaLinksFromModel(model)
16
+ : [];
17
+
14
18
  // ── 从 chunks 提取最终结果。SSE 思考链只由 call/execution card 展示,避免重复。──
15
19
  var resultTexts = [];
16
20
  for (var i = 0; i < model.chunks.length; i++) {
17
21
  var chunk = model.chunks[i];
18
22
  if (chunk.type === 'result') {
19
- if (_clShouldRenderOutputText(chunk.text)) resultTexts.push(chunk.text);
23
+ var resultText = typeof _clStripOpenagentMediaLines === 'function'
24
+ ? _clStripOpenagentMediaLines(chunk.text)
25
+ : chunk.text;
26
+ if (_clShouldRenderOutputText(resultText)) resultTexts.push(resultText);
20
27
  } else if (chunk.type === 'text') {
21
28
  // 远端 Agent 最终回复是普通文本(toolResult(reply.content)),
22
29
  // chunk-parser 归为 type='text',也应作为最终结果显示
23
- if (_clShouldRenderOutputText(chunk.text)) resultTexts.push(chunk.text);
30
+ var textChunk = typeof _clStripOpenagentMediaLines === 'function'
31
+ ? _clStripOpenagentMediaLines(chunk.text)
32
+ : chunk.text;
33
+ if (_clShouldRenderOutputText(textChunk)) resultTexts.push(textChunk);
24
34
  }
25
35
  }
26
36
 
27
- if (resultTexts.length === 0) {
37
+ if (resultTexts.length === 0 && mediaLinks.length === 0) {
28
38
  var placeholder = document.createElement('span');
29
39
  placeholder.className = 'cl-remote-agent-card cl-remote-agent-output-placeholder';
30
40
  placeholder.style.display = 'none';
@@ -37,7 +47,8 @@ function _clRenderOutputCard(model, onOpenSidebar) {
37
47
  }
38
48
 
39
49
  var container = document.createElement('div');
40
- container.className = 'cl-remote-agent-card cl-thought-chain cl-output-card cl-output-card--pending';
50
+ container.className = 'cl-remote-agent-card cl-thought-chain cl-output-card'
51
+ + (resultTexts.length > 0 ? ' cl-output-card--pending' : '');
41
52
  container.dataset.clCardRole = 'output';
42
53
  if (CL && typeof CL.diagMarkCard === 'function') {
43
54
  CL.diagMarkCard(container, 'output', { tcid: model.toolCallId || '' });
@@ -65,6 +76,12 @@ function _clRenderOutputCard(model, onOpenSidebar) {
65
76
  divider.className = 'cl-thought-chain-divider';
66
77
  content.appendChild(divider);
67
78
 
79
+ // ── 附件下载链接 ──
80
+ if (mediaLinks.length > 0 && typeof _clCreateOpenagentMediaLinksSection === 'function') {
81
+ var mediaSection = _clCreateOpenagentMediaLinksSection(mediaLinks);
82
+ if (mediaSection) content.appendChild(mediaSection);
83
+ }
84
+
68
85
  // ── 最终结果展示 ──
69
86
  if (resultTexts.length > 0) {
70
87
  var resultSection = document.createElement('div');
@@ -587,6 +587,67 @@
587
587
  ' font-size: 12px;',
588
588
  '}',
589
589
 
590
+ // ── 附件下载区 ──
591
+ '.cl-openagent-media-links {',
592
+ ' display: flex;',
593
+ ' flex-wrap: wrap;',
594
+ ' gap: 8px;',
595
+ ' width: 100%;',
596
+ ' box-sizing: border-box;',
597
+ ' padding: 6px 18px 12px;',
598
+ '}',
599
+ '.cl-openagent-media-link {',
600
+ ' display: inline-grid;',
601
+ ' grid-template-columns: auto auto;',
602
+ ' grid-template-areas: "icon action" "icon file";',
603
+ ' align-items: center;',
604
+ ' column-gap: 8px;',
605
+ ' row-gap: 1px;',
606
+ ' max-width: min(100%, 360px);',
607
+ ' box-sizing: border-box;',
608
+ ' padding: 8px 10px;',
609
+ ' border: 1px solid rgba(47,47,51,0.12);',
610
+ ' border-radius: 8px;',
611
+ ' background: rgba(47,47,51,0.03);',
612
+ ' color: rgba(47,47,51,0.90) !important;',
613
+ ' text-decoration: none !important;',
614
+ ' font-family: "PingFang SC", sans-serif;',
615
+ '}',
616
+ '.cl-openagent-media-link:hover {',
617
+ ' background: rgba(16,189,235,0.08);',
618
+ ' border-color: rgba(16,189,235,0.35);',
619
+ '}',
620
+ '.cl-openagent-media-link-icon {',
621
+ ' grid-area: icon;',
622
+ ' width: 28px;',
623
+ ' height: 28px;',
624
+ ' border-radius: 6px;',
625
+ ' background: rgba(16,189,235,0.12);',
626
+ ' color: #088BAE;',
627
+ ' display: inline-flex;',
628
+ ' align-items: center;',
629
+ ' justify-content: center;',
630
+ ' font-size: 11px;',
631
+ ' font-weight: 700;',
632
+ '}',
633
+ '.cl-openagent-media-link-text {',
634
+ ' grid-area: action;',
635
+ ' min-width: 0;',
636
+ ' font-size: 13px;',
637
+ ' font-weight: 600;',
638
+ ' line-height: 1.2;',
639
+ '}',
640
+ '.cl-openagent-media-link-file {',
641
+ ' grid-area: file;',
642
+ ' min-width: 0;',
643
+ ' overflow: hidden;',
644
+ ' text-overflow: ellipsis;',
645
+ ' white-space: nowrap;',
646
+ ' font-size: 11px;',
647
+ ' line-height: 1.2;',
648
+ ' color: rgba(47,47,51,0.50);',
649
+ '}',
650
+
590
651
  // ── 错误状态 ──
591
652
  '.cl-remote-agent-card--error {',
592
653
  ' border-color: rgba(220, 38, 38, 0.3);',
@@ -311,14 +311,33 @@ const toolcardRender: UIExtension = {
311
311
  }
312
312
 
313
313
  if (toolMessageOk) {
314
+ // 双模式匹配单卡 guard:
315
+ // Shape A (2026.4.x): ,Z=X.length===1?X[0]:null,Y=Z&&!A&&!B?
316
+ // Shape B (2026.5.x): ,N=u.length===1?u[0]:null,...ae=P?`Tool error`:ne&&!C&&!h?ne:...
314
317
  const singleCardMatch = toolMessageFn!.body.match(/(?:let\s+|,)([a-zA-Z_$]\w*)=([a-zA-Z_$]\w*)\.length===1\?\2\[0\]:null,\w+=\1&&!([a-zA-Z_$]\w*)&&!([a-zA-Z_$]\w*)\?/);
315
- if (!singleCardMatch) {
316
- logger.warn('[ui-ext:regex] toolcard-render: tool message single-card guard not found');
317
- toolMessageOk = false;
318
+ let cardsVar = '';
319
+ let mediaVar = '';
320
+ if (singleCardMatch) {
321
+ // Shape A: old bundle
322
+ cardsVar = singleCardMatch[2];
323
+ mediaVar = singleCardMatch[4];
324
+ } else {
325
+ // Shape B: 2026.5.x bundle
326
+ const shapeB = toolMessageFn!.body.match(/,(?:let\s+)?([a-zA-Z_$]\w*)=([a-zA-Z_$]\w*)\.length===1\?\2\[0\]:null/);
327
+ if (!shapeB) {
328
+ logger.warn('[ui-ext:regex] toolcard-render: tool message single-card guard not found');
329
+ toolMessageOk = false;
330
+ } else {
331
+ cardsVar = shapeB[2];
332
+ // 从 ae=P?`Tool error`:ne&&!C&&!h 中提取排除变量 C, h
333
+ const aeMatch = toolMessageFn!.body.match(/,\w+=\w+\?`Tool error`:(\w+)&&!(\w+)&&!(\w+)\?/);
334
+ if (aeMatch) {
335
+ mediaVar = `(${aeMatch[2]}||${aeMatch[3]})`;
336
+ } else {
337
+ mediaVar = 'false';
338
+ }
339
+ }
318
340
  }
319
- if (toolMessageOk) {
320
- const cardsVar = singleCardMatch![2];
321
- const mediaVar = singleCardMatch![4];
322
341
  const chatBubbleClassMatch = toolMessageFn!.body.match(/[,;]([a-zA-Z_$]\w*)=\[`chat-bubble`,/);
323
342
  if (!chatBubbleClassMatch) {
324
343
  logger.warn('[ui-ext:regex] toolcard-render: tool message chat-bubble class var not found');
@@ -335,7 +354,7 @@ const toolcardRender: UIExtension = {
335
354
  }
336
355
  if (toolMessageOk) {
337
356
  const toolMessageHook =
338
- `${marker}let __openagentToolMsgCard=null;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>\`;`;
357
+ `${marker}/* openagent-media-aware-tool-message */let __openagentToolMsgCard=null;console.log('[openagent] toolMsg hook: cards='+${cardsVar}.length+' mediaVar='+${mediaVar});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>\`;`;
339
358
  const toolMessageInsertAt = toolMessageFn!.bodyStart + toolMessageReturnIdx;
340
359
  content = content.substring(0, toolMessageInsertAt) + toolMessageHook + content.substring(toolMessageInsertAt);
341
360
  patchedToolMessageWrapper = true;
@@ -344,9 +363,8 @@ const toolcardRender: UIExtension = {
344
363
  }
345
364
  }
346
365
  }
347
- }
348
366
 
349
- writeFileSync(bundlePath, content, 'utf-8');
367
+ writeFileSync(bundlePath, content, 'utf-8');
350
368
  logger.info(`[ui-ext:regex] toolcard-render: patched ${funcName}(${params.substring(0, 20)}) html=${litHtmlVar}${patchedWrapper ? ' + wrapper' : ''}${patchedToolMessageWrapper ? ' + tool-message' : ''}`);
351
369
  return true;
352
370
  },
@@ -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
  }