openclaw-openagent 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/app/remote-agent-tool.js +108 -10
- package/dist/src/auth/config.js +65 -10
- package/dist/src/channel.js +2 -1
- package/dist/src/plugin-ui/assets/openagent-override.js +257 -5
- package/dist/src/plugin-ui/ui-extension-loader/registry-regex.js +1 -1
- package/dist/src/proxy/auth-proxy.d.ts +1 -0
- package/dist/src/proxy/auth-proxy.js +28 -9
- package/dist/src/transport/oasn/oasn-http.d.ts +12 -0
- package/dist/src/transport/oasn/oasn-http.js +45 -14
- package/dist/src/util/url-resolver.d.ts +6 -0
- package/dist/src/util/url-resolver.js +24 -8
- package/openclaw.plugin.json +15 -0
- package/package.json +1 -1
- package/src/app/remote-agent-tool.ts +126 -11
- package/src/auth/config.ts +79 -10
- package/src/channel.ts +2 -1
- package/src/plugin-ui/assets/openagent-override.js +257 -5
- package/src/plugin-ui/build.cjs +1 -0
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +18 -0
- package/src/plugin-ui/modules/remote-agent/media-links.js +151 -0
- package/src/plugin-ui/modules/remote-agent/output-card.js +21 -4
- package/src/plugin-ui/modules/remote-agent/styles.js +61 -0
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +1 -1
- package/src/proxy/auth-proxy.ts +30 -10
- package/src/transport/oasn/oasn-http.ts +60 -14
- package/src/util/url-resolver.ts +24 -8
|
@@ -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 {
|
|
@@ -265,7 +361,7 @@ export function createCallRemoteAgentTool() {
|
|
|
265
361
|
});
|
|
266
362
|
const artifacts = result.artifacts ?? [];
|
|
267
363
|
let content = result.content;
|
|
268
|
-
let mediaUrls =
|
|
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
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
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
|
package/dist/src/auth/config.js
CHANGED
|
@@ -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 =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
package/dist/src/channel.js
CHANGED
|
@@ -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(
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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');
|
|
@@ -333,7 +333,7 @@ const toolcardRender = {
|
|
|
333
333
|
toolMessageOk = false;
|
|
334
334
|
}
|
|
335
335
|
if (toolMessageOk) {
|
|
336
|
-
const toolMessageHook = `${marker}let __openagentToolMsgCard=null;console.log('[openagent] toolMsg hook: cards='+${cardsVar}.length+' mediaVar='+${mediaVar});if(
|
|
336
|
+
const toolMessageHook = `${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>\`;`;
|
|
337
337
|
const toolMessageInsertAt = toolMessageFn.bodyStart + toolMessageReturnIdx;
|
|
338
338
|
content = content.substring(0, toolMessageInsertAt) + toolMessageHook + content.substring(toolMessageInsertAt);
|
|
339
339
|
patchedToolMessageWrapper = true;
|
|
@@ -17,4 +17,5 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
17
17
|
/** Returns the gateway base URL as seen by the browser (e.g. http://localhost:18794). */
|
|
18
18
|
export declare function getGatewayBaseUrl(): string;
|
|
19
19
|
export declare function createAuthProxyHandler(): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
|
|
20
|
+
export declare function resolveOasnProxyTargetUrl(upstreamBase: string, targetPath: string, search?: string): string;
|
|
20
21
|
export declare function isAllowedOasnProxyRequest(method: string, pathname: string): boolean;
|