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.
@@ -77,6 +77,102 @@ function isTextArtifact(candidate) {
77
77
  function artifactContentToText(content) {
78
78
  return Buffer.from(content).toString('utf8').trim();
79
79
  }
80
+ const OPENAGENT_MEDIA_DIR = '/tmp/openclaw/openagent-media';
81
+ const EXT_BY_MIME = {
82
+ 'application/json': '.json',
83
+ 'text/markdown': '.md',
84
+ 'text/plain': '.txt',
85
+ 'application/pdf': '.pdf',
86
+ 'image/png': '.png',
87
+ 'image/jpeg': '.jpg',
88
+ 'image/gif': '.gif',
89
+ 'image/webp': '.webp',
90
+ 'application/zip': '.zip',
91
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
92
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
93
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
94
+ };
95
+ function sanitizeMediaFileName(name) {
96
+ const base = nodePath.basename(name || 'artifact');
97
+ return base
98
+ .replace(/[\0\r\n]/g, '')
99
+ .replace(/[/:\\]/g, '_')
100
+ .replace(/^\.+$/, 'artifact')
101
+ .slice(0, 180) || 'artifact';
102
+ }
103
+ function inferExtension(buffer, mimeType) {
104
+ const mime = mimeType.split(';')[0]?.trim().toLowerCase() || '';
105
+ const byMime = EXT_BY_MIME[mime];
106
+ if (byMime)
107
+ return byMime;
108
+ if (buffer.subarray(0, 2).toString('latin1') === 'PK') {
109
+ const zipNames = buffer.toString('latin1');
110
+ if (zipNames.includes('[Content_Types].xml') && zipNames.includes('ppt/'))
111
+ return '.pptx';
112
+ if (zipNames.includes('[Content_Types].xml') && zipNames.includes('word/'))
113
+ return '.docx';
114
+ if (zipNames.includes('[Content_Types].xml') && zipNames.includes('xl/'))
115
+ return '.xlsx';
116
+ return '.zip';
117
+ }
118
+ if (buffer.subarray(0, 4).toString('latin1') === '%PDF')
119
+ return '.pdf';
120
+ if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])))
121
+ return '.png';
122
+ if (buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff])))
123
+ return '.jpg';
124
+ return '.bin';
125
+ }
126
+ function buildOasnMediaFileName(artifact, buffer, downloadedName, downloadedMime) {
127
+ const preferredName = downloadedName && downloadedName !== 'artifact'
128
+ ? downloadedName
129
+ : artifact.displayName && artifact.displayName !== 'artifact'
130
+ ? artifact.displayName
131
+ : artifact.id || 'artifact';
132
+ let safeName = sanitizeMediaFileName(preferredName);
133
+ if (!nodePath.extname(safeName)) {
134
+ safeName += inferExtension(buffer, downloadedMime || artifact.mimeType);
135
+ }
136
+ return safeName;
137
+ }
138
+ async function cacheOasnArtifactsForMedia(oasnTransport, artifacts) {
139
+ const localPaths = [];
140
+ for (const artifact of artifacts) {
141
+ const ref = artifact.contentUrl || artifact.id;
142
+ if (!ref)
143
+ continue;
144
+ try {
145
+ const downloaded = await oasnTransport.downloadArtifact(ref);
146
+ const buffer = Buffer.from(downloaded.content);
147
+ const fileName = buildOasnMediaFileName(artifact, buffer, downloaded.displayName, downloaded.mimeType);
148
+ const filePath = nodePath.join(OPENAGENT_MEDIA_DIR, fileName);
149
+ fs.mkdirSync(nodePath.dirname(filePath), { recursive: true });
150
+ fs.writeFileSync(filePath, buffer);
151
+ localPaths.push(filePath);
152
+ logger.info(`[remote-tool] Cached OASN artifact for media: id=${artifact.id} file=${filePath} size=${buffer.byteLength}`);
153
+ }
154
+ catch (err) {
155
+ logger.warn(`[remote-tool] Failed to cache OASN artifact ${artifact.id || ref}: ${err.message}`);
156
+ }
157
+ }
158
+ return localPaths;
159
+ }
160
+ function mediaDownloadLabel(url) {
161
+ let fileName = 'artifact';
162
+ try {
163
+ fileName = decodeURIComponent(new URL(url).pathname.split('/').pop() || fileName);
164
+ }
165
+ catch {
166
+ fileName = decodeURIComponent(url.split('?')[0]?.split('/').pop() || fileName);
167
+ }
168
+ return sanitizeMediaFileName(fileName).replace(/[[\]\r\n]/g, ' ') || 'artifact';
169
+ }
170
+ function formatMediaDownloadLines(urls) {
171
+ const lines = urls
172
+ .filter((url) => typeof url === 'string' && url.trim())
173
+ .map((url) => `- [${mediaDownloadLabel(url)}](${url})`);
174
+ return lines.length ? `下载链接:\n${lines.join('\n')}` : '';
175
+ }
80
176
  function normalizeRemoteToolExecuteArgs(signalOrUpdate, onUpdateOrContext, legacySignal) {
81
177
  if (typeof signalOrUpdate === 'function') {
82
178
  return {
@@ -197,7 +293,7 @@ export function createCallRemoteAgentTool() {
197
293
  // listUnsettledInvocations() 续轮询,避免重复扣费
198
294
  const requestId = generateRequestId();
199
295
  const TIMEOUT_MS = 15 * 60 * 1000; // 15 分钟:远程 agent 可能跑长工具
200
- const OASN_INVOCATION_TIMEOUT_MS = 60_000; // compute02 REL 当前允许的 ServiceAgent 执行预算
296
+ const OASN_INVOCATION_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 小时
201
297
  logger.info(`[remote-tool] Calling remote agent: agentId=${agentId} requestId=${requestId} ` +
202
298
  `mode=${mode} transport=${rt.transportType}`);
203
299
  // 进度回调适配器:两条路径共用一套桥接逻辑,统一把字符串内容包装成 ToolResult
@@ -265,7 +361,7 @@ export function createCallRemoteAgentTool() {
265
361
  });
266
362
  const artifacts = result.artifacts ?? [];
267
363
  let content = result.content;
268
- let mediaUrls = artifacts.map((a) => a.contentUrl).filter(Boolean);
364
+ let mediaUrls = [];
269
365
  if (!content.trim()) {
270
366
  for (const textArtifact of artifacts) {
271
367
  if (!textArtifact.contentUrl && !textArtifact.contentAvailable)
@@ -274,10 +370,6 @@ export function createCallRemoteAgentTool() {
274
370
  const downloaded = await oasnTransport.downloadArtifact(textArtifact.contentUrl || textArtifact.id);
275
371
  if (isTextArtifact(downloaded)) {
276
372
  content = artifactContentToText(downloaded.content);
277
- mediaUrls = artifacts
278
- .filter((a) => a.id !== textArtifact.id)
279
- .map((a) => a.contentUrl)
280
- .filter(Boolean);
281
373
  break;
282
374
  }
283
375
  }
@@ -316,12 +408,15 @@ export function createCallRemoteAgentTool() {
316
408
  ? `${content}\n\nFiles from remote agent (use openagent_download_file to save):\n${artifactLines.join('\n')}`
317
409
  : `Files from remote agent:\n${artifactLines.join('\n')}`;
318
410
  }
411
+ if (artifacts.length > 0) {
412
+ mediaUrls = await cacheOasnArtifactsForMedia(oasnTransport, artifacts);
413
+ }
319
414
  // Resolve OASN-relative URLs in content (remote agent may have embedded
320
415
  // /webui/continuations/... or /api/artifacts/... in its output text).
321
416
  if (content && accessBase) {
322
417
  content = content.replace(/(?<![a-zA-Z0-9])(\/webui\/[^\s"')\]]+|\/api\/[^\s"')\]]+)/g, (match) => resolveOasnAccessUrl(accessBase, match));
323
418
  }
324
- // OASN 的非文本 artifacts 暂继续复用 ArtifactRef.contentUrl 作为 mediaUrls。
419
+ // OASN artifacts are cached locally before being handed to Gateway media rendering.
325
420
  reply = {
326
421
  status: 'complete',
327
422
  content,
@@ -405,13 +500,16 @@ export function createCallRemoteAgentTool() {
405
500
  return `${baseUrl}/plugins/openagent/media/${encodeURIComponent(fileName)}`;
406
501
  });
407
502
  }
408
- // Path 1: MEDIA: lines in text for Gateway MEDIA: parser + LLM echo
503
+ // Path 1: explicit markdown links + MEDIA: lines for Gateway parser.
504
+ // The markdown block makes the link visible even when the host does not
505
+ // render MEDIA: attachments in assistant/tool-result bubbles.
506
+ const downloadLines = formatMediaDownloadLines(httpMediaUrls);
409
507
  const mediaLines = httpMediaUrls
410
508
  .map((url) => `MEDIA:${url}`)
411
509
  .join('\n');
412
- const textWithMedia = reply.content
413
- ? `${reply.content}\n${mediaLines}`
414
- : mediaLines;
510
+ const textWithMedia = [reply.content.trim(), downloadLines, mediaLines]
511
+ .filter(Boolean)
512
+ .join('\n\n');
415
513
  return {
416
514
  content: [{ type: 'text', text: textWithMedia }],
417
515
  // Path 2: details.media for v2026.4.11+ native attachment rendering
@@ -9,6 +9,8 @@
9
9
  * See §0.1 of refactoring-plan.md.
10
10
  */
11
11
  const DEFAULT_API_BASE = 'https://api.openagent.club';
12
+ const DEFAULT_OASN_PUBLIC_ROOT = 'https://oasn-test.haimawan.com';
13
+ const OASN_ENV_ID_RE = /^[A-Za-z0-9_-]+$/;
12
14
  /**
13
15
  * OASN 默认 API base。
14
16
  *
@@ -19,6 +21,41 @@ export const DEFAULT_OASN_API_BASE = 'https://oasn.invalid/CONFIGURE_ME';
19
21
  export function isUnconfiguredOasnApiBase(value) {
20
22
  return !value || value === DEFAULT_OASN_API_BASE;
21
23
  }
24
+ function normalizeOasnApiBase(value) {
25
+ const normalized = String(value || '')
26
+ .trim()
27
+ .replace(/\/+$/, '')
28
+ .replace(/\/api$/, '');
29
+ return normalized === DEFAULT_OASN_API_BASE ? '' : normalized;
30
+ }
31
+ function normalizeOasnEnvId(value) {
32
+ const envId = String(value || '').trim();
33
+ if (!envId)
34
+ return '';
35
+ return OASN_ENV_ID_RE.test(envId) ? envId : '';
36
+ }
37
+ function oasnPublicBaseFromEnvId(envId) {
38
+ const normalized = normalizeOasnEnvId(envId);
39
+ if (!normalized)
40
+ return '';
41
+ const root = normalizeOasnApiBase(process.env.OASN_PUBLIC_ROOT || process.env.OPENAGENT_OASN_PUBLIC_ROOT || DEFAULT_OASN_PUBLIC_ROOT);
42
+ return `${root}/e/${normalized}`;
43
+ }
44
+ function resolveOasnBase({ accountBase, channelBase, envBase, accountEnvId, channelEnvId, envEnvId, fallbackBase, }) {
45
+ const configuredBase = normalizeOasnApiBase(accountBase) || normalizeOasnApiBase(channelBase);
46
+ if (configuredBase)
47
+ return configuredBase;
48
+ const configuredEnvId = normalizeOasnEnvId(accountEnvId) || normalizeOasnEnvId(channelEnvId);
49
+ if (configuredEnvId)
50
+ return oasnPublicBaseFromEnvId(configuredEnvId);
51
+ const ambientBase = normalizeOasnApiBase(envBase);
52
+ if (ambientBase)
53
+ return ambientBase;
54
+ const ambientEnvId = normalizeOasnEnvId(envEnvId);
55
+ if (ambientEnvId)
56
+ return oasnPublicBaseFromEnvId(ambientEnvId);
57
+ return fallbackBase;
58
+ }
22
59
  /**
23
60
  * Extract the OpenAgent-specific config block from the full OpenClaw config.
24
61
  * Handles multiple config key locations that have drifted across versions.
@@ -75,19 +112,37 @@ export function resolveOpenagentAccount(cfg, accountId) {
75
112
  const envApiBase = transport === 'oasn'
76
113
  ? (process.env.OASN_API_BASE || process.env.OPENAGENT_OASN_API_BASE || '')
77
114
  : '';
78
- const apiBase = (targetAcc['api_base'] || targetAcc['apiBase']
79
- || clCfg['api_base']
80
- || clCfg['apiBase']
81
- || envApiBase
82
- || fallbackApiBase);
115
+ const apiBase = transport === 'oasn'
116
+ ? resolveOasnBase({
117
+ accountBase: targetAcc['api_base'] || targetAcc['apiBase'],
118
+ channelBase: clCfg['api_base'] || clCfg['apiBase'],
119
+ envBase: envApiBase,
120
+ accountEnvId: targetAcc['oasn_env_id'] || targetAcc['oasnEnvId'] || targetAcc['env_id'] || targetAcc['envId'],
121
+ channelEnvId: clCfg['oasn_env_id'] || clCfg['oasnEnvId'] || clCfg['env_id'] || clCfg['envId'],
122
+ envEnvId: process.env.OASN_ENV_ID || process.env.OPENAGENT_OASN_ENV_ID,
123
+ fallbackBase: fallbackApiBase,
124
+ })
125
+ : (targetAcc['api_base'] || targetAcc['apiBase']
126
+ || clCfg['api_base']
127
+ || clCfg['apiBase']
128
+ || fallbackApiBase);
83
129
  const envAccessApiBase = transport === 'oasn'
84
130
  ? (process.env.OASN_ACCESS_API_BASE || process.env.OPENAGENT_OASN_ACCESS_API_BASE || '')
85
131
  : '';
86
- const accessApiBase = (targetAcc['access_api_base'] || targetAcc['accessApiBase']
87
- || clCfg['access_api_base']
88
- || clCfg['accessApiBase']
89
- || envAccessApiBase
90
- || apiBase);
132
+ const accessApiBase = transport === 'oasn'
133
+ ? resolveOasnBase({
134
+ accountBase: targetAcc['access_api_base'] || targetAcc['accessApiBase'],
135
+ channelBase: clCfg['access_api_base'] || clCfg['accessApiBase'],
136
+ envBase: envAccessApiBase,
137
+ accountEnvId: targetAcc['oasn_env_id'] || targetAcc['oasnEnvId'] || targetAcc['env_id'] || targetAcc['envId'],
138
+ channelEnvId: clCfg['oasn_env_id'] || clCfg['oasnEnvId'] || clCfg['env_id'] || clCfg['envId'],
139
+ envEnvId: process.env.OASN_ENV_ID || process.env.OPENAGENT_OASN_ENV_ID,
140
+ fallbackBase: apiBase,
141
+ })
142
+ : (targetAcc['access_api_base'] || targetAcc['accessApiBase']
143
+ || clCfg['access_api_base']
144
+ || clCfg['accessApiBase']
145
+ || apiBase);
91
146
  const claimUrl = (targetAcc['claim_url'] || targetAcc['claimUrl'] || clCfg['claim_url'] || clCfg['claimUrl'] || '');
92
147
  const rawStatus = (targetAcc['status'] || targetAcc['claim_status'] || targetAcc['claimStatus'] || clCfg['status'] || '');
93
148
  const normalizedStatus = normalizeClaimStatus(rawStatus, agentId, apiKey);
@@ -11,6 +11,7 @@ import { isUnconfiguredOasnApiBase, resolveOpenagentAccount, getOpenagentConfig
11
11
  import { registry } from './runtime/registry.js';
12
12
  import { AccountRuntime } from './runtime/account.js';
13
13
  import { logger } from './util/logger.js';
14
+ import { resolveOasnRelativeUrl } from './util/url-resolver.js';
14
15
  const API_BASE = 'https://api.clawlink.club';
15
16
  const OASN_TRANSPORT = 'oasn';
16
17
  // ── Channel adapter meta ──
@@ -467,7 +468,7 @@ function compactConfig(input) {
467
468
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ''));
468
469
  }
469
470
  async function claimOasnAgent(apiBase, apiKey, agentId, ownerEmail) {
470
- const res = await fetch(new URL('/api/agents/claim', apiBase), {
471
+ const res = await fetch(resolveOasnRelativeUrl(apiBase, '/api/agents/claim'), {
471
472
  method: 'POST',
472
473
  headers: {
473
474
  'Content-Type': 'application/json',
@@ -2,7 +2,7 @@
2
2
  * OpenAgent UI Override — Auto-generated, do not edit directly!
3
3
  *
4
4
  * Source: src/plugin-ui/modules/
5
- * Built: 2026-06-25T09:33:52.490Z
5
+ * Built: 2026-06-29T06:32:31.586Z
6
6
  *
7
7
  * To modify, edit the source modules and run:
8
8
  * npm run build:override
@@ -3129,6 +3129,23 @@ function _installSendClickInterceptor() {
3129
3129
  }, true);
3130
3130
  }
3131
3131
 
3132
+ function _installSendKeydownInterceptor() {
3133
+ if (window.__openagentSendKeydownInterceptorInstalled) return;
3134
+ window.__openagentSendKeydownInterceptorInstalled = true;
3135
+ document.addEventListener('keydown', function(e) {
3136
+ if (!CL.pendingMention || CL.remoteSendInFlight) return;
3137
+ if (e.key !== 'Enter' || e.shiftKey || e.isComposing) return;
3138
+
3139
+ const textarea = _getTextarea();
3140
+ if (!textarea || e.target !== textarea) return;
3141
+
3142
+ e.preventDefault();
3143
+ e.stopPropagation();
3144
+ if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
3145
+ _sendPendingMention(textarea);
3146
+ }, true);
3147
+ }
3148
+
3132
3149
  /**
3133
3150
  * 多策略定位 textarea 所在输入区域的工具栏行。
3134
3151
  * v4.x: 工具栏在 textarea.parentElement.children 中
@@ -3177,6 +3194,7 @@ function tryInject() {
3177
3194
 
3178
3195
  if (!textarea) return false;
3179
3196
  _installSendClickInterceptor();
3197
+ _installSendKeydownInterceptor();
3180
3198
  if (document.querySelector('.openagent-at-btn')) return true; // 已注入
3181
3199
 
3182
3200
  // ── 定位插入点:麦克风图标所在的 toolbar 行(多策略)──
@@ -6294,6 +6312,67 @@ function _clScan() {
6294
6312
  ' font-size: 12px;',
6295
6313
  '}',
6296
6314
 
6315
+ // ── 附件下载区 ──
6316
+ '.cl-openagent-media-links {',
6317
+ ' display: flex;',
6318
+ ' flex-wrap: wrap;',
6319
+ ' gap: 8px;',
6320
+ ' width: 100%;',
6321
+ ' box-sizing: border-box;',
6322
+ ' padding: 6px 18px 12px;',
6323
+ '}',
6324
+ '.cl-openagent-media-link {',
6325
+ ' display: inline-grid;',
6326
+ ' grid-template-columns: auto auto;',
6327
+ ' grid-template-areas: "icon action" "icon file";',
6328
+ ' align-items: center;',
6329
+ ' column-gap: 8px;',
6330
+ ' row-gap: 1px;',
6331
+ ' max-width: min(100%, 360px);',
6332
+ ' box-sizing: border-box;',
6333
+ ' padding: 8px 10px;',
6334
+ ' border: 1px solid rgba(47,47,51,0.12);',
6335
+ ' border-radius: 8px;',
6336
+ ' background: rgba(47,47,51,0.03);',
6337
+ ' color: rgba(47,47,51,0.90) !important;',
6338
+ ' text-decoration: none !important;',
6339
+ ' font-family: "PingFang SC", sans-serif;',
6340
+ '}',
6341
+ '.cl-openagent-media-link:hover {',
6342
+ ' background: rgba(16,189,235,0.08);',
6343
+ ' border-color: rgba(16,189,235,0.35);',
6344
+ '}',
6345
+ '.cl-openagent-media-link-icon {',
6346
+ ' grid-area: icon;',
6347
+ ' width: 28px;',
6348
+ ' height: 28px;',
6349
+ ' border-radius: 6px;',
6350
+ ' background: rgba(16,189,235,0.12);',
6351
+ ' color: #088BAE;',
6352
+ ' display: inline-flex;',
6353
+ ' align-items: center;',
6354
+ ' justify-content: center;',
6355
+ ' font-size: 11px;',
6356
+ ' font-weight: 700;',
6357
+ '}',
6358
+ '.cl-openagent-media-link-text {',
6359
+ ' grid-area: action;',
6360
+ ' min-width: 0;',
6361
+ ' font-size: 13px;',
6362
+ ' font-weight: 600;',
6363
+ ' line-height: 1.2;',
6364
+ '}',
6365
+ '.cl-openagent-media-link-file {',
6366
+ ' grid-area: file;',
6367
+ ' min-width: 0;',
6368
+ ' overflow: hidden;',
6369
+ ' text-overflow: ellipsis;',
6370
+ ' white-space: nowrap;',
6371
+ ' font-size: 11px;',
6372
+ ' line-height: 1.2;',
6373
+ ' color: rgba(47,47,51,0.50);',
6374
+ '}',
6375
+
6297
6376
  // ── 错误状态 ──
6298
6377
  '.cl-remote-agent-card--error {',
6299
6378
  ' border-color: rgba(220, 38, 38, 0.3);',
@@ -7504,6 +7583,162 @@ function _clRenderOutputMarkdown(text) {
7504
7583
  return result;
7505
7584
  }
7506
7585
 
7586
+ // ════════════════════════════════════════════════════════════════
7587
+ // MODULE: remote-agent/media-links.js
7588
+ // ════════════════════════════════════════════════════════════════
7589
+
7590
+ // ── OpenAgent Media Links — toolResult MEDIA: protocol renderer ──
7591
+ //
7592
+ // OpenClaw host parses MEDIA: links in assistant text, but remote-agent
7593
+ // toolResult text is rendered through the plugin card path. Keep this parser
7594
+ // local to the card so OASN artifacts become visible downloads.
7595
+
7596
+ function _clExtractOpenagentMediaLinksFromText(rawText) {
7597
+ var value = String(rawText || '');
7598
+ var links = [];
7599
+ var seen = Object.create(null);
7600
+
7601
+ function addUrl(rawUrl) {
7602
+ var url = _clNormalizeOpenagentMediaUrl(rawUrl);
7603
+ if (!url || seen[url]) return;
7604
+ seen[url] = true;
7605
+
7606
+ var fileName = _clGetOpenagentMediaFileName(url);
7607
+ links.push({
7608
+ url: url,
7609
+ fileName: fileName,
7610
+ extension: _clInferOpenagentMediaExtension(fileName),
7611
+ });
7612
+ }
7613
+
7614
+ var re = /^\s*MEDIA:\s*(\S+)\s*$/gm;
7615
+ var match;
7616
+
7617
+ while ((match = re.exec(value)) !== null) {
7618
+ addUrl(match[1]);
7619
+ }
7620
+
7621
+ var markdownRe = /\[[^\]]+\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)/g;
7622
+ while ((match = markdownRe.exec(value)) !== null) {
7623
+ addUrl(match[1]);
7624
+ }
7625
+
7626
+ return links;
7627
+ }
7628
+
7629
+ function _clExtractOpenagentMediaLinksFromModel(model) {
7630
+ var allText = [];
7631
+ if (model && model.rawText) allText.push(model.rawText);
7632
+ var chunks = model && Array.isArray(model.chunks) ? model.chunks : [];
7633
+ for (var i = 0; i < chunks.length; i++) {
7634
+ if (chunks[i] && chunks[i].text) allText.push(chunks[i].text);
7635
+ }
7636
+ return _clExtractOpenagentMediaLinksFromText(allText.join('\n'));
7637
+ }
7638
+
7639
+ function _clNormalizeOpenagentMediaUrl(rawUrl) {
7640
+ var value = String(rawUrl || '').trim();
7641
+ if (!value) return '';
7642
+ value = value.replace(/^[`'"]+/, '').replace(/[`'".,;)\]]+$/, '');
7643
+
7644
+ if (value.charAt(0) === '/') return value;
7645
+
7646
+ try {
7647
+ var parsed = new URL(value);
7648
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return value;
7649
+ } catch (e) {}
7650
+
7651
+ return '';
7652
+ }
7653
+
7654
+ function _clGetOpenagentMediaFileName(url) {
7655
+ var pathValue = '';
7656
+ try {
7657
+ var base = (typeof window !== 'undefined' && window.location && window.location.href)
7658
+ ? window.location.href
7659
+ : 'http://127.0.0.1/';
7660
+ pathValue = new URL(url, base).pathname || '';
7661
+ } catch (e) {
7662
+ pathValue = String(url || '').split('?')[0] || '';
7663
+ }
7664
+
7665
+ var parts = pathValue.split('/');
7666
+ var fileName = parts[parts.length - 1] || 'artifact';
7667
+ try {
7668
+ fileName = decodeURIComponent(fileName);
7669
+ } catch (e) {}
7670
+ return fileName || 'artifact';
7671
+ }
7672
+
7673
+ function _clInferOpenagentMediaExtension(fileName) {
7674
+ var match = String(fileName || '').toLowerCase().match(/\.([a-z0-9]{1,12})$/);
7675
+ return match ? match[1] : '';
7676
+ }
7677
+
7678
+ function _clStripOpenagentMediaLines(rawText) {
7679
+ return String(rawText || '')
7680
+ .split(/\r?\n/)
7681
+ .filter(function(line) { return !/^\s*MEDIA:\s*/.test(line); })
7682
+ .join('\n')
7683
+ .trim();
7684
+ }
7685
+
7686
+ function _clCreateOpenagentMediaLinksSection(links) {
7687
+ if (!Array.isArray(links) || links.length === 0) return null;
7688
+
7689
+ var section = document.createElement('div');
7690
+ section.className = 'cl-openagent-media-links';
7691
+
7692
+ for (var i = 0; i < links.length; i++) {
7693
+ var link = links[i] || {};
7694
+ if (!link.url) continue;
7695
+
7696
+ var anchor = document.createElement('a');
7697
+ anchor.className = 'cl-openagent-media-link';
7698
+ anchor.href = link.url;
7699
+ anchor.target = '_blank';
7700
+ anchor.rel = 'noopener noreferrer';
7701
+ anchor.download = link.fileName || '';
7702
+ anchor.title = link.fileName || link.url;
7703
+
7704
+ var icon = document.createElement('span');
7705
+ icon.className = 'cl-openagent-media-link-icon';
7706
+ icon.textContent = _clGetOpenagentMediaIconText(link.extension);
7707
+ anchor.appendChild(icon);
7708
+
7709
+ var text = document.createElement('span');
7710
+ text.className = 'cl-openagent-media-link-text';
7711
+ text.textContent = _clGetOpenagentMediaActionText(link.extension);
7712
+ anchor.appendChild(text);
7713
+
7714
+ var file = document.createElement('span');
7715
+ file.className = 'cl-openagent-media-link-file';
7716
+ file.textContent = link.fileName || 'artifact';
7717
+ anchor.appendChild(file);
7718
+
7719
+ section.appendChild(anchor);
7720
+ }
7721
+
7722
+ return section.childNodes.length > 0 ? section : null;
7723
+ }
7724
+
7725
+ function _clGetOpenagentMediaActionText(extension) {
7726
+ var ext = String(extension || '').toUpperCase();
7727
+ if (!ext) return '下载附件';
7728
+ return '下载 ' + ext;
7729
+ }
7730
+
7731
+ function _clGetOpenagentMediaIconText(extension) {
7732
+ extension = String(extension || '').toLowerCase();
7733
+ if (extension === 'pptx' || extension === 'ppt') return 'P';
7734
+ if (extension === 'docx' || extension === 'doc') return 'D';
7735
+ if (extension === 'xlsx' || extension === 'xls' || extension === 'csv') return 'X';
7736
+ if (extension === 'json') return '{}';
7737
+ if (extension === 'md' || extension === 'txt') return 'T';
7738
+ if (extension === 'zip') return 'Z';
7739
+ return 'F';
7740
+ }
7741
+
7507
7742
  // ════════════════════════════════════════════════════════════════
7508
7743
  // MODULE: remote-agent/output-card.js
7509
7744
  // ════════════════════════════════════════════════════════════════
@@ -7521,20 +7756,30 @@ function _clRenderOutputMarkdown(text) {
7521
7756
  // UI 结构复用 shared thought-chain styles;数据来源是 RenderHook model。
7522
7757
 
7523
7758
  function _clRenderOutputCard(model, onOpenSidebar) {
7759
+ var mediaLinks = typeof _clExtractOpenagentMediaLinksFromModel === 'function'
7760
+ ? _clExtractOpenagentMediaLinksFromModel(model)
7761
+ : [];
7762
+
7524
7763
  // ── 从 chunks 提取最终结果。SSE 思考链只由 call/execution card 展示,避免重复。──
7525
7764
  var resultTexts = [];
7526
7765
  for (var i = 0; i < model.chunks.length; i++) {
7527
7766
  var chunk = model.chunks[i];
7528
7767
  if (chunk.type === 'result') {
7529
- if (_clShouldRenderOutputText(chunk.text)) resultTexts.push(chunk.text);
7768
+ var resultText = typeof _clStripOpenagentMediaLines === 'function'
7769
+ ? _clStripOpenagentMediaLines(chunk.text)
7770
+ : chunk.text;
7771
+ if (_clShouldRenderOutputText(resultText)) resultTexts.push(resultText);
7530
7772
  } else if (chunk.type === 'text') {
7531
7773
  // 远端 Agent 最终回复是普通文本(toolResult(reply.content)),
7532
7774
  // chunk-parser 归为 type='text',也应作为最终结果显示
7533
- if (_clShouldRenderOutputText(chunk.text)) resultTexts.push(chunk.text);
7775
+ var textChunk = typeof _clStripOpenagentMediaLines === 'function'
7776
+ ? _clStripOpenagentMediaLines(chunk.text)
7777
+ : chunk.text;
7778
+ if (_clShouldRenderOutputText(textChunk)) resultTexts.push(textChunk);
7534
7779
  }
7535
7780
  }
7536
7781
 
7537
- if (resultTexts.length === 0) {
7782
+ if (resultTexts.length === 0 && mediaLinks.length === 0) {
7538
7783
  var placeholder = document.createElement('span');
7539
7784
  placeholder.className = 'cl-remote-agent-card cl-remote-agent-output-placeholder';
7540
7785
  placeholder.style.display = 'none';
@@ -7547,7 +7792,8 @@ function _clRenderOutputCard(model, onOpenSidebar) {
7547
7792
  }
7548
7793
 
7549
7794
  var container = document.createElement('div');
7550
- container.className = 'cl-remote-agent-card cl-thought-chain cl-output-card cl-output-card--pending';
7795
+ container.className = 'cl-remote-agent-card cl-thought-chain cl-output-card'
7796
+ + (resultTexts.length > 0 ? ' cl-output-card--pending' : '');
7551
7797
  container.dataset.clCardRole = 'output';
7552
7798
  if (CL && typeof CL.diagMarkCard === 'function') {
7553
7799
  CL.diagMarkCard(container, 'output', { tcid: model.toolCallId || '' });
@@ -7575,6 +7821,12 @@ function _clRenderOutputCard(model, onOpenSidebar) {
7575
7821
  divider.className = 'cl-thought-chain-divider';
7576
7822
  content.appendChild(divider);
7577
7823
 
7824
+ // ── 附件下载链接 ──
7825
+ if (mediaLinks.length > 0 && typeof _clCreateOpenagentMediaLinksSection === 'function') {
7826
+ var mediaSection = _clCreateOpenagentMediaLinksSection(mediaLinks);
7827
+ if (mediaSection) content.appendChild(mediaSection);
7828
+ }
7829
+
7578
7830
  // ── 最终结果展示 ──
7579
7831
  if (resultTexts.length > 0) {
7580
7832
  var resultSection = document.createElement('div');