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.
@@ -10,6 +10,8 @@
10
10
  */
11
11
 
12
12
  const DEFAULT_API_BASE = 'https://api.openagent.club';
13
+ const DEFAULT_OASN_PUBLIC_ROOT = 'https://oasn-test.haimawan.com';
14
+ const OASN_ENV_ID_RE = /^[A-Za-z0-9_-]+$/;
13
15
  /**
14
16
  * OASN 默认 API base。
15
17
  *
@@ -22,6 +24,55 @@ export function isUnconfiguredOasnApiBase(value: string | undefined): boolean {
22
24
  return !value || value === DEFAULT_OASN_API_BASE;
23
25
  }
24
26
 
27
+ function normalizeOasnApiBase(value: unknown): string {
28
+ const normalized = String(value || '')
29
+ .trim()
30
+ .replace(/\/+$/, '')
31
+ .replace(/\/api$/, '');
32
+ return normalized === DEFAULT_OASN_API_BASE ? '' : normalized;
33
+ }
34
+
35
+ function normalizeOasnEnvId(value: unknown): string {
36
+ const envId = String(value || '').trim();
37
+ if (!envId) return '';
38
+ return OASN_ENV_ID_RE.test(envId) ? envId : '';
39
+ }
40
+
41
+ function oasnPublicBaseFromEnvId(envId: string): string {
42
+ const normalized = normalizeOasnEnvId(envId);
43
+ if (!normalized) return '';
44
+ const root = normalizeOasnApiBase(process.env.OASN_PUBLIC_ROOT || process.env.OPENAGENT_OASN_PUBLIC_ROOT || DEFAULT_OASN_PUBLIC_ROOT);
45
+ return `${root}/e/${normalized}`;
46
+ }
47
+
48
+ function resolveOasnBase({
49
+ accountBase,
50
+ channelBase,
51
+ envBase,
52
+ accountEnvId,
53
+ channelEnvId,
54
+ envEnvId,
55
+ fallbackBase,
56
+ }: {
57
+ accountBase?: unknown;
58
+ channelBase?: unknown;
59
+ envBase?: unknown;
60
+ accountEnvId?: unknown;
61
+ channelEnvId?: unknown;
62
+ envEnvId?: unknown;
63
+ fallbackBase: string;
64
+ }): string {
65
+ const configuredBase = normalizeOasnApiBase(accountBase) || normalizeOasnApiBase(channelBase);
66
+ if (configuredBase) return configuredBase;
67
+ const configuredEnvId = normalizeOasnEnvId(accountEnvId) || normalizeOasnEnvId(channelEnvId);
68
+ if (configuredEnvId) return oasnPublicBaseFromEnvId(configuredEnvId);
69
+ const ambientBase = normalizeOasnApiBase(envBase);
70
+ if (ambientBase) return ambientBase;
71
+ const ambientEnvId = normalizeOasnEnvId(envEnvId);
72
+ if (ambientEnvId) return oasnPublicBaseFromEnvId(ambientEnvId);
73
+ return fallbackBase;
74
+ }
75
+
25
76
  /**
26
77
  * Transport 类型 —— v3.9+ 双轨抽象。
27
78
  *
@@ -132,19 +183,37 @@ export function resolveOpenagentAccount(
132
183
  const envApiBase = transport === 'oasn'
133
184
  ? (process.env.OASN_API_BASE || process.env.OPENAGENT_OASN_API_BASE || '')
134
185
  : '';
135
- const apiBase = (targetAcc['api_base'] || targetAcc['apiBase']
136
- || clCfg['api_base']
137
- || clCfg['apiBase']
138
- || envApiBase
139
- || fallbackApiBase) as string;
186
+ const apiBase = transport === 'oasn'
187
+ ? resolveOasnBase({
188
+ accountBase: targetAcc['api_base'] || targetAcc['apiBase'],
189
+ channelBase: clCfg['api_base'] || clCfg['apiBase'],
190
+ envBase: envApiBase,
191
+ accountEnvId: targetAcc['oasn_env_id'] || targetAcc['oasnEnvId'] || targetAcc['env_id'] || targetAcc['envId'],
192
+ channelEnvId: clCfg['oasn_env_id'] || clCfg['oasnEnvId'] || clCfg['env_id'] || clCfg['envId'],
193
+ envEnvId: process.env.OASN_ENV_ID || process.env.OPENAGENT_OASN_ENV_ID,
194
+ fallbackBase: fallbackApiBase,
195
+ })
196
+ : (targetAcc['api_base'] || targetAcc['apiBase']
197
+ || clCfg['api_base']
198
+ || clCfg['apiBase']
199
+ || fallbackApiBase) as string;
140
200
  const envAccessApiBase = transport === 'oasn'
141
201
  ? (process.env.OASN_ACCESS_API_BASE || process.env.OPENAGENT_OASN_ACCESS_API_BASE || '')
142
202
  : '';
143
- const accessApiBase = (targetAcc['access_api_base'] || targetAcc['accessApiBase']
144
- || clCfg['access_api_base']
145
- || clCfg['accessApiBase']
146
- || envAccessApiBase
147
- || apiBase) as string;
203
+ const accessApiBase = transport === 'oasn'
204
+ ? resolveOasnBase({
205
+ accountBase: targetAcc['access_api_base'] || targetAcc['accessApiBase'],
206
+ channelBase: clCfg['access_api_base'] || clCfg['accessApiBase'],
207
+ envBase: envAccessApiBase,
208
+ accountEnvId: targetAcc['oasn_env_id'] || targetAcc['oasnEnvId'] || targetAcc['env_id'] || targetAcc['envId'],
209
+ channelEnvId: clCfg['oasn_env_id'] || clCfg['oasnEnvId'] || clCfg['env_id'] || clCfg['envId'],
210
+ envEnvId: process.env.OASN_ENV_ID || process.env.OPENAGENT_OASN_ENV_ID,
211
+ fallbackBase: apiBase,
212
+ })
213
+ : (targetAcc['access_api_base'] || targetAcc['accessApiBase']
214
+ || clCfg['access_api_base']
215
+ || clCfg['accessApiBase']
216
+ || apiBase) as string;
148
217
  const claimUrl = (targetAcc['claim_url'] || targetAcc['claimUrl'] || clCfg['claim_url'] || clCfg['claimUrl'] || '') as string;
149
218
  const rawStatus = (targetAcc['status'] || targetAcc['claim_status'] || targetAcc['claimStatus'] || clCfg['status'] || '') as string;
150
219
  const normalizedStatus = normalizeClaimStatus(rawStatus, agentId, apiKey);
package/src/channel.ts CHANGED
@@ -13,6 +13,7 @@ import { registry } from './runtime/registry.js';
13
13
  import { AccountRuntime } from './runtime/account.js';
14
14
 
15
15
  import { logger } from './util/logger.js';
16
+ import { resolveOasnRelativeUrl } from './util/url-resolver.js';
16
17
  import * as timMessages from './tim/messages.js';
17
18
 
18
19
  const API_BASE = 'https://api.clawlink.club';
@@ -544,7 +545,7 @@ async function claimOasnAgent(
544
545
  agentId: string,
545
546
  ownerEmail: string,
546
547
  ): Promise<Record<string, unknown>> {
547
- const res = await fetch(new URL('/api/agents/claim', apiBase), {
548
+ const res = await fetch(resolveOasnRelativeUrl(apiBase, '/api/agents/claim'), {
548
549
  method: 'POST',
549
550
  headers: {
550
551
  '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');
@@ -48,6 +48,7 @@ const MANIFEST = [
48
48
  'remote-agent/tool-card-model.js',
49
49
  'remote-agent/execution-card.js',
50
50
  'remote-agent/markdown-renderer.js',
51
+ 'remote-agent/media-links.js',
51
52
  'remote-agent/output-card.js',
52
53
  'remote-agent/render-hooks.js',
53
54
  // ── loader (bootstrap 必须最后) ──
@@ -136,6 +136,23 @@ function _installSendClickInterceptor() {
136
136
  }, true);
137
137
  }
138
138
 
139
+ function _installSendKeydownInterceptor() {
140
+ if (window.__openagentSendKeydownInterceptorInstalled) return;
141
+ window.__openagentSendKeydownInterceptorInstalled = true;
142
+ document.addEventListener('keydown', function(e) {
143
+ if (!CL.pendingMention || CL.remoteSendInFlight) return;
144
+ if (e.key !== 'Enter' || e.shiftKey || e.isComposing) return;
145
+
146
+ const textarea = _getTextarea();
147
+ if (!textarea || e.target !== textarea) return;
148
+
149
+ e.preventDefault();
150
+ e.stopPropagation();
151
+ if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
152
+ _sendPendingMention(textarea);
153
+ }, true);
154
+ }
155
+
139
156
  /**
140
157
  * 多策略定位 textarea 所在输入区域的工具栏行。
141
158
  * v4.x: 工具栏在 textarea.parentElement.children 中
@@ -184,6 +201,7 @@ function tryInject() {
184
201
 
185
202
  if (!textarea) return false;
186
203
  _installSendClickInterceptor();
204
+ _installSendKeydownInterceptor();
187
205
  if (document.querySelector('.openagent-at-btn')) return true; // 已注入
188
206
 
189
207
  // ── 定位插入点:麦克风图标所在的 toolbar 行(多策略)──
@@ -0,0 +1,151 @@
1
+ // ── OpenAgent Media Links — toolResult MEDIA: protocol renderer ──
2
+ //
3
+ // OpenClaw host parses MEDIA: links in assistant text, but remote-agent
4
+ // toolResult text is rendered through the plugin card path. Keep this parser
5
+ // local to the card so OASN artifacts become visible downloads.
6
+
7
+ function _clExtractOpenagentMediaLinksFromText(rawText) {
8
+ var value = String(rawText || '');
9
+ var links = [];
10
+ var seen = Object.create(null);
11
+
12
+ function addUrl(rawUrl) {
13
+ var url = _clNormalizeOpenagentMediaUrl(rawUrl);
14
+ if (!url || seen[url]) return;
15
+ seen[url] = true;
16
+
17
+ var fileName = _clGetOpenagentMediaFileName(url);
18
+ links.push({
19
+ url: url,
20
+ fileName: fileName,
21
+ extension: _clInferOpenagentMediaExtension(fileName),
22
+ });
23
+ }
24
+
25
+ var re = /^\s*MEDIA:\s*(\S+)\s*$/gm;
26
+ var match;
27
+
28
+ while ((match = re.exec(value)) !== null) {
29
+ addUrl(match[1]);
30
+ }
31
+
32
+ var markdownRe = /\[[^\]]+\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)/g;
33
+ while ((match = markdownRe.exec(value)) !== null) {
34
+ addUrl(match[1]);
35
+ }
36
+
37
+ return links;
38
+ }
39
+
40
+ function _clExtractOpenagentMediaLinksFromModel(model) {
41
+ var allText = [];
42
+ if (model && model.rawText) allText.push(model.rawText);
43
+ var chunks = model && Array.isArray(model.chunks) ? model.chunks : [];
44
+ for (var i = 0; i < chunks.length; i++) {
45
+ if (chunks[i] && chunks[i].text) allText.push(chunks[i].text);
46
+ }
47
+ return _clExtractOpenagentMediaLinksFromText(allText.join('\n'));
48
+ }
49
+
50
+ function _clNormalizeOpenagentMediaUrl(rawUrl) {
51
+ var value = String(rawUrl || '').trim();
52
+ if (!value) return '';
53
+ value = value.replace(/^[`'"]+/, '').replace(/[`'".,;)\]]+$/, '');
54
+
55
+ if (value.charAt(0) === '/') return value;
56
+
57
+ try {
58
+ var parsed = new URL(value);
59
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return value;
60
+ } catch (e) {}
61
+
62
+ return '';
63
+ }
64
+
65
+ function _clGetOpenagentMediaFileName(url) {
66
+ var pathValue = '';
67
+ try {
68
+ var base = (typeof window !== 'undefined' && window.location && window.location.href)
69
+ ? window.location.href
70
+ : 'http://127.0.0.1/';
71
+ pathValue = new URL(url, base).pathname || '';
72
+ } catch (e) {
73
+ pathValue = String(url || '').split('?')[0] || '';
74
+ }
75
+
76
+ var parts = pathValue.split('/');
77
+ var fileName = parts[parts.length - 1] || 'artifact';
78
+ try {
79
+ fileName = decodeURIComponent(fileName);
80
+ } catch (e) {}
81
+ return fileName || 'artifact';
82
+ }
83
+
84
+ function _clInferOpenagentMediaExtension(fileName) {
85
+ var match = String(fileName || '').toLowerCase().match(/\.([a-z0-9]{1,12})$/);
86
+ return match ? match[1] : '';
87
+ }
88
+
89
+ function _clStripOpenagentMediaLines(rawText) {
90
+ return String(rawText || '')
91
+ .split(/\r?\n/)
92
+ .filter(function(line) { return !/^\s*MEDIA:\s*/.test(line); })
93
+ .join('\n')
94
+ .trim();
95
+ }
96
+
97
+ function _clCreateOpenagentMediaLinksSection(links) {
98
+ if (!Array.isArray(links) || links.length === 0) return null;
99
+
100
+ var section = document.createElement('div');
101
+ section.className = 'cl-openagent-media-links';
102
+
103
+ for (var i = 0; i < links.length; i++) {
104
+ var link = links[i] || {};
105
+ if (!link.url) continue;
106
+
107
+ var anchor = document.createElement('a');
108
+ anchor.className = 'cl-openagent-media-link';
109
+ anchor.href = link.url;
110
+ anchor.target = '_blank';
111
+ anchor.rel = 'noopener noreferrer';
112
+ anchor.download = link.fileName || '';
113
+ anchor.title = link.fileName || link.url;
114
+
115
+ var icon = document.createElement('span');
116
+ icon.className = 'cl-openagent-media-link-icon';
117
+ icon.textContent = _clGetOpenagentMediaIconText(link.extension);
118
+ anchor.appendChild(icon);
119
+
120
+ var text = document.createElement('span');
121
+ text.className = 'cl-openagent-media-link-text';
122
+ text.textContent = _clGetOpenagentMediaActionText(link.extension);
123
+ anchor.appendChild(text);
124
+
125
+ var file = document.createElement('span');
126
+ file.className = 'cl-openagent-media-link-file';
127
+ file.textContent = link.fileName || 'artifact';
128
+ anchor.appendChild(file);
129
+
130
+ section.appendChild(anchor);
131
+ }
132
+
133
+ return section.childNodes.length > 0 ? section : null;
134
+ }
135
+
136
+ function _clGetOpenagentMediaActionText(extension) {
137
+ var ext = String(extension || '').toUpperCase();
138
+ if (!ext) return '下载附件';
139
+ return '下载 ' + ext;
140
+ }
141
+
142
+ function _clGetOpenagentMediaIconText(extension) {
143
+ extension = String(extension || '').toLowerCase();
144
+ if (extension === 'pptx' || extension === 'ppt') return 'P';
145
+ if (extension === 'docx' || extension === 'doc') return 'D';
146
+ if (extension === 'xlsx' || extension === 'xls' || extension === 'csv') return 'X';
147
+ if (extension === 'json') return '{}';
148
+ if (extension === 'md' || extension === 'txt') return 'T';
149
+ if (extension === 'zip') return 'Z';
150
+ return 'F';
151
+ }