openclaw-openagent 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/src/messaging/executor.d.ts +2 -3
  2. package/dist/src/messaging/executor.js +4 -31
  3. package/dist/src/plugin-ui/assets/openagent-override.js +4 -5
  4. package/dist/src/runtime/update-checker.d.ts +1 -1
  5. package/dist/src/runtime/update-checker.js +9 -46
  6. package/package.json +1 -3
  7. package/src/app/channel-tools.ts +0 -249
  8. package/src/app/discovery-tools.ts +0 -273
  9. package/src/app/download-file-tool.ts +0 -133
  10. package/src/app/hooks.ts +0 -144
  11. package/src/app/index.ts +0 -84
  12. package/src/app/messaging-tools.ts +0 -79
  13. package/src/app/ops-tools.ts +0 -155
  14. package/src/app/remote-agent-tool.ts +0 -762
  15. package/src/app/types.ts +0 -68
  16. package/src/app/upload-file-tool.ts +0 -411
  17. package/src/app/verbose-preflight.ts +0 -190
  18. package/src/auth/config.ts +0 -266
  19. package/src/auth/credential-manager.ts +0 -146
  20. package/src/auth/index.ts +0 -24
  21. package/src/auth/verify.ts +0 -99
  22. package/src/channel.ts +0 -566
  23. package/src/compat.ts +0 -94
  24. package/src/config/config-schema.ts +0 -45
  25. package/src/messaging/aggregator.ts +0 -120
  26. package/src/messaging/collector.ts +0 -89
  27. package/src/messaging/executor.ts +0 -72
  28. package/src/messaging/inbound.ts +0 -150
  29. package/src/messaging/index.ts +0 -11
  30. package/src/messaging/mention-protocol.ts +0 -94
  31. package/src/messaging/process-c2c-request.ts +0 -564
  32. package/src/messaging/process-message.ts +0 -373
  33. package/src/messaging/scheduler.ts +0 -55
  34. package/src/messaging/types.ts +0 -38
  35. package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
  36. package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
  37. package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
  38. package/src/plugin-ui/adapters/oc-unknown.js +0 -48
  39. package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
  40. package/src/plugin-ui/assets/bg.png +0 -0
  41. package/src/plugin-ui/assets/icon.png +0 -0
  42. package/src/plugin-ui/assets/magic.svg +0 -5
  43. package/src/plugin-ui/assets/openagent-override.js +0 -11263
  44. package/src/plugin-ui/build.cjs +0 -463
  45. package/src/plugin-ui/index.ts +0 -18
  46. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
  47. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
  48. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
  49. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
  50. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
  51. package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -832
  52. package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
  53. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
  54. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
  55. package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
  56. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
  57. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
  58. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -262
  59. package/src/plugin-ui/modules/loader/bootstrap.js +0 -133
  60. package/src/plugin-ui/modules/loader/shared-state.js +0 -838
  61. package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
  62. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
  63. package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
  64. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
  65. package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
  66. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
  67. package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
  68. package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
  69. package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
  70. package/src/plugin-ui/modules/remote-agent/styles.js +0 -903
  71. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
  72. package/src/plugin-ui/postinstall-deploy.cjs +0 -52
  73. package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
  74. package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
  75. package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
  76. package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
  77. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
  78. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
  79. package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
  80. package/src/proxy/auth-proxy.ts +0 -414
  81. package/src/runtime/account.ts +0 -549
  82. package/src/runtime/index.ts +0 -7
  83. package/src/runtime/plugin-runtime.ts +0 -94
  84. package/src/runtime/registry.ts +0 -71
  85. package/src/runtime/update-checker.ts +0 -286
  86. package/src/sdk/CLASS_MAP.md +0 -143
  87. package/src/sdk/index.d.ts +0 -126
  88. package/src/sdk/index.js +0 -23990
  89. package/src/sdk/modules/cloud-search-module.js +0 -1117
  90. package/src/sdk/modules/follow-module.js +0 -1069
  91. package/src/sdk/modules/group-module.js +0 -7397
  92. package/src/sdk/modules/relationship-module.js +0 -2269
  93. package/src/sdk/modules/signaling-module.js +0 -1468
  94. package/src/sdk/modules/tim-upload-plugin.js +0 -730
  95. package/src/sdk/node-env/http-request.js +0 -90
  96. package/src/sdk/node-env/index.js +0 -57
  97. package/src/sdk/node-env/storage.js +0 -114
  98. package/src/sdk/package.json +0 -10
  99. package/src/sdk/tsconfig.json +0 -16
  100. package/src/state/pending-invocation-store.ts +0 -43
  101. package/src/state/store.ts +0 -756
  102. package/src/tim/c2c.ts +0 -451
  103. package/src/tim/channels.ts +0 -364
  104. package/src/tim/client.ts +0 -330
  105. package/src/tim/index.ts +0 -18
  106. package/src/tim/messages.ts +0 -166
  107. package/src/tim/sdk-logger-init.ts +0 -50
  108. package/src/tools.ts +0 -10
  109. package/src/transport/factory.ts +0 -95
  110. package/src/transport/oasn/index.ts +0 -17
  111. package/src/transport/oasn/oasn-agent-card.ts +0 -111
  112. package/src/transport/oasn/oasn-discovery.ts +0 -108
  113. package/src/transport/oasn/oasn-files.ts +0 -210
  114. package/src/transport/oasn/oasn-http.ts +0 -529
  115. package/src/transport/oasn/oasn-invocation.ts +0 -604
  116. package/src/transport/oasn/oasn-normalize.ts +0 -133
  117. package/src/transport/oasn/oasn-register.ts +0 -33
  118. package/src/transport/oasn/oasn-transport.ts +0 -318
  119. package/src/transport/oasn/oasn-types.ts +0 -404
  120. package/src/transport/tim/index.ts +0 -8
  121. package/src/transport/tim/tim-transport.ts +0 -515
  122. package/src/transport/types.ts +0 -538
  123. package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
  124. package/src/types/openclaw.d.ts +0 -97
  125. package/src/types/tencentcloud-chat.d.ts +0 -15
  126. package/src/util/http.ts +0 -113
  127. package/src/util/logger.ts +0 -131
  128. package/src/util/url-resolver.ts +0 -77
@@ -1,70 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { logger } from '../../util/logger.js';
4
- import { findFileByPrefix, resolveOpenClawDistDir } from './locator.js';
5
- import { restoreFile } from './backup.js';
6
-
7
- export type RemovedRevertResult = 'none' | 'restored' | 'failed';
8
-
9
- export type RemovedUIExtension = {
10
- id: string;
11
- /** Which shared file group this removed extension belongs to (if any). */
12
- sharedFile?: string;
13
- revert(controlUiDir: string): RemovedRevertResult;
14
- };
15
-
16
- function fileNeedsRemoval(filepath: string, id: string, fingerprints: string[]): boolean {
17
- const content = readFileSync(filepath, 'utf-8');
18
- return content.includes(`/*openagent:${id}`) || fingerprints.some(fp => content.includes(fp));
19
- }
20
-
21
- function restoreRemovedPatch(filepath: string | null, id: string, fingerprints: string[]): RemovedRevertResult {
22
- if (!filepath) return 'none';
23
- if (!fileNeedsRemoval(filepath, id, fingerprints)) return 'none';
24
-
25
- const restored = restoreFile(filepath);
26
- if (!restored) {
27
- logger.warn(`[ui-ext] ${id}: removed extension is present but no backup exists; refusing to continue`);
28
- return 'failed';
29
- }
30
-
31
- logger.info(`[ui-ext] ${id}: removed extension reverted from backup`);
32
- return 'restored';
33
- }
34
-
35
- export const REMOVED_UI_EXTENSIONS: RemovedUIExtension[] = [
36
- {
37
- id: 'gateway-openagent-tool-payload',
38
- revert(controlUiDir: string): RemovedRevertResult {
39
- const distDir = resolveOpenClawDistDir(controlUiDir);
40
- const gatewayPath = distDir ? findFileByPrefix(distDir, 'gateway-cli-') : null;
41
- return restoreRemovedPatch(gatewayPath, this.id, [
42
- '__openagentToolCallIds',
43
- '__openagentKeepToolPayload',
44
- '__openagentEffectiveToolVerbose',
45
- ]);
46
- },
47
- },
48
- {
49
- id: 'agent-event-openagent-tool-result',
50
- revert(controlUiDir: string): RemovedRevertResult {
51
- const distDir = resolveOpenClawDistDir(controlUiDir);
52
- const agentBundlePath = distDir ? findFileByPrefix(distDir, 'auth-profiles-') : null;
53
- return restoreRemovedPatch(agentBundlePath, this.id, [
54
- '__openagentIsRemoteToolName',
55
- ]);
56
- },
57
- },
58
- {
59
- id: 'marked-expose',
60
- sharedFile: 'index-bundle',
61
- revert(controlUiDir: string): RemovedRevertResult {
62
- const assetsDir = join(controlUiDir, 'assets');
63
- const bundlePath = findFileByPrefix(assetsDir, 'index-');
64
- return restoreRemovedPatch(bundlePath, this.id, [
65
- 'window.__openagentMarked=',
66
- '__openagentMarked',
67
- ]);
68
- },
69
- },
70
- ];
@@ -1,72 +0,0 @@
1
- /**
2
- * UIExtension 类型定义与注册表
3
- *
4
- * 每个 UIExtension 是一个独立的、可增删的扩展点。
5
- * 框架只关心 id 和 version,具体操作由 apply/revert 内部处理。
6
- */
7
-
8
- export interface UIExtension {
9
- /** 唯一标识(如 'override-script'、'html-script-tag') */
10
- id: string;
11
-
12
- /** 内容版本,改了 apply 逻辑就递增 */
13
- version: number;
14
-
15
- /**
16
- * 共享文件标识(可选)。
17
- *
18
- * 多个扩展修改同一文件时(如 toolcard-render 和 toolcallid-propagate 都改 index-*.js),
19
- * 设置相同的 sharedFile 值。reconciler 会把它们作为一组处理:
20
- * 任何一个需要变更 → 恢复 backup → 重新 apply 全组中应保留的扩展。
21
- *
22
- * 不设置此字段的扩展独立 apply/revert,不受其他扩展影响。
23
- */
24
- sharedFile?: string;
25
-
26
- /**
27
- * 每次启动都调用 apply(可选)。
28
- *
29
- * 默认行为:manifest version 匹配时跳过 apply。
30
- * 设为 true 后:即使 version 不变也调用 apply,由 apply 内部判断是否需要操作。
31
- * 适用于内容可能独立于 version 变化的扩展(如 override-script 的产物文件)。
32
- */
33
- alwaysApply?: boolean;
34
-
35
- /**
36
- * 加载扩展。
37
- * 接收 control-ui 目录路径,内部自行决定改什么文件、怎么改。
38
- * 返回 true 表示成功,false 表示失败(不写入 manifest,下次重试)。
39
- */
40
- apply: (controlUiDir: string, context?: UIExtensionContext) => boolean;
41
-
42
- /**
43
- * 卸载扩展。
44
- * 还原 apply 所做的全部修改。
45
- * 注意:共享文件的扩展不应单独调用 revert,由 reconciler 统一处理。
46
- */
47
- revert: (controlUiDir: string) => void;
48
- }
49
-
50
- export interface UIExtensionContext {
51
- hostVersion?: string;
52
- }
53
-
54
- /**
55
- * OpenAgent 标记前缀,注入的代码旁加此注释以标识修改来源。
56
- * 格式:/*openagent:<extension-id>* /
57
- */
58
- export const OPENAGENT_MARKER_PREFIX = '/*openagent:';
59
-
60
- /**
61
- * 生成标记注释。
62
- */
63
- export function makeMarker(extensionId: string): string {
64
- return `/*openagent:${extensionId}*/`;
65
- }
66
-
67
- /**
68
- * 检查内容中是否包含指定扩展点的标记。
69
- */
70
- export function hasMarker(content: string, extensionId: string): boolean {
71
- return content.includes(makeMarker(extensionId));
72
- }
@@ -1,414 +0,0 @@
1
- /**
2
- * Auth API Proxy — Gateway-side reverse proxy for auth.ai-talk.live
3
- *
4
- * Eliminates CSP connect-src patching by proxying browser requests
5
- * through a same-origin plugin HTTP route:
6
- *
7
- * Browser: fetch('/plugins/openagent/api/agents')
8
- * → Gateway plugin-http stage
9
- * → this handler
10
- * → fetch('https://auth.ai-talk.live/api/agents')
11
- * → response back to browser
12
- *
13
- * Registered via api.registerHttpRoute() in index.ts.
14
- * See: docs/audit/022-A-loader-audit-v1.md §4.7–4.10
15
- */
16
-
17
- import type { IncomingMessage, ServerResponse } from 'node:http';
18
- import fs from 'node:fs';
19
- import nodePath from 'node:path';
20
- import { logger } from '../util/logger.js';
21
- import { registry } from '../runtime/registry.js';
22
- import { getOpenagentUpdateStatus } from '../runtime/update-checker.js';
23
- import { isUnconfiguredOasnApiBase } from '../auth/config.js';
24
- import { resolveOasnRelativeUrl } from '../util/url-resolver.js';
25
-
26
- /** TIM 路径默认上游(兼容旧版) */
27
- const AUTH_UPSTREAM_TIM = 'https://auth.ai-talk.live';
28
- const PROXY_TIMEOUT_MS = 15_000;
29
- const MAX_BODY_BYTES = 64 * 1024; // 64KB — tagline API body is ~100 bytes
30
-
31
- /**
32
- * 获取当前 OASN access API base URL,用于解析响应中的相对路径。
33
- */
34
- function getOasnAccessBase(): string {
35
- const rt = registry.getDefault();
36
- if (rt?.transportType !== 'oasn') return '';
37
- return (rt.config?.accessApiBase || rt.config?.apiBase || '').replace(/\/+$/, '');
38
- }
39
-
40
- /**
41
- * 根据当前 active runtime 的 transport 类型决定上游。
42
- *
43
- * - transport='oasn':Discovery/onboarding 用 account.apiBase;
44
- * Access-layer endpoint 用 account.accessApiBase(未配置时回落到 apiBase)
45
- * - transport='tim' 或无 active runtime:fallback 到 AUTH_UPSTREAM_TIM
46
- *
47
- * 浏览器侧 Agent Book 通过 /plugins/openagent/api/* 统一访问,由此处分流。
48
- */
49
- function pickUpstream(method: string, pathname: string): string | null {
50
- const rt = registry.getDefault();
51
- if (rt?.transportType === 'oasn') {
52
- const apiBase = rt.config?.apiBase;
53
- const accessApiBase = rt.config?.accessApiBase || apiBase;
54
- const upstream = isAccessLayerProxyPath(method, pathname) ? accessApiBase : apiBase;
55
- return isUnconfiguredOasnApiBase(upstream) ? null : upstream ?? null;
56
- }
57
- return AUTH_UPSTREAM_TIM;
58
- }
59
-
60
- // ── Gateway base URL detection ──────────────────────────────────────────
61
- // Captured from the first incoming HTTP request's Host header.
62
- // Handles Docker port mapping: container port ≠ host port.
63
- let _gatewayBaseUrl: string | undefined;
64
-
65
- /** Returns the gateway base URL as seen by the browser (e.g. http://localhost:18794). */
66
- export function getGatewayBaseUrl(): string {
67
- return _gatewayBaseUrl ?? 'http://localhost:18794';
68
- }
69
-
70
- // ── Handler factory ─────────────────────────────────────────────────────
71
-
72
- export function createAuthProxyHandler() {
73
- return async (req: IncomingMessage, res: ServerResponse): Promise<boolean> => {
74
- // Gateway strips the matched prefix; req.url is the path after prefix removal.
75
- // E.g. /plugins/openagent/api/agents → req.url = /api/agents
76
- const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
77
- logger.info(`[proxy] RECEIVED: ${req.method} ${req.url}`);
78
-
79
- // Capture the gateway's external base URL from the browser's request.
80
- // Reads standard reverse-proxy headers for correct protocol/host detection:
81
- // - Local: http://localhost:18794
82
- // - Docker: http://127.0.0.1:18794 (port mapped)
83
- // - Nginx: https://oc6.openagent.club (X-Forwarded-Proto/Host)
84
- if (!_gatewayBaseUrl && req.headers.host) {
85
- const proto = (req.headers['x-forwarded-proto'] as string)?.split(',')[0]?.trim() || 'http';
86
- const host = (req.headers['x-forwarded-host'] as string)?.split(',')[0]?.trim() || req.headers.host;
87
- _gatewayBaseUrl = `${proto}://${host}`;
88
- logger.info(`[proxy] Gateway base URL detected: ${_gatewayBaseUrl}`);
89
- }
90
-
91
- // req.url may or may not have the /plugins/openagent prefix stripped by Gateway
92
- // (behavior varies across Gateway versions), so strip it defensively.
93
- let targetPath = url.pathname;
94
- if (targetPath.startsWith('/plugins/openagent')) {
95
- targetPath = targetPath.slice('/plugins/openagent'.length);
96
- }
97
- // ── T4: Serve local media files from /tmp/openclaw/openagent-media/ ──
98
- // Files downloaded from remote agents are stored here. The media route must
99
- // be detected after prefix normalization because Gateway versions disagree
100
- // on whether /plugins/openagent has already been stripped.
101
- if (targetPath.startsWith('/media/')) {
102
- return serveLocalMedia(targetPath, res);
103
- }
104
- if (targetPath === '/update-status') {
105
- writeJson(res, 200, getOpenagentUpdateStatus() as unknown as Record<string, unknown>);
106
- return true;
107
- }
108
- // v3.9+ 双轨抽象:上游按当前 transport 切换(OASN → api.oasn.ai;TIM → auth.ai-talk.live)
109
- const method = req.method ?? 'GET';
110
- const rt = registry.getDefault();
111
- if (!rt && isOasnProxyPath(method, targetPath)) {
112
- writeJson(res, 503, {
113
- error: 'OASN account is not configured',
114
- detail: 'Run openclaw-plugin-openagent-cli install to register this device before opening Agent Book.',
115
- });
116
- return true;
117
- }
118
- if (rt && rt.transportType !== 'oasn' && isOasnProxyPath(method, targetPath)) {
119
- writeJson(res, 503, {
120
- error: 'OASN transport is not active',
121
- detail: 'Agent Book discovery requires an installed OASN account.',
122
- });
123
- return true;
124
- }
125
- if (rt?.transportType === 'oasn' && !isAllowedOasnProxyRequest(method, targetPath)) {
126
- logger.warn(`[proxy] blocked OASN proxy path: ${method} ${targetPath}`);
127
- writeJson(res, 403, { error: 'proxy path not allowed' });
128
- return true;
129
- }
130
-
131
- const upstreamBase = pickUpstream(method, targetPath);
132
- if (!upstreamBase) {
133
- writeJson(res, 503, { error: 'OASN api_base is not configured' });
134
- return true;
135
- }
136
- const targetUrl = resolveOasnProxyTargetUrl(upstreamBase, targetPath, url.search);
137
-
138
- logger.info(`[proxy] ${method} ${url.pathname} → ${targetUrl}`);
139
-
140
- try {
141
- // Read request body for non-GET methods
142
- let body: string | undefined;
143
- if (method !== 'GET' && method !== 'HEAD') {
144
- body = await readBody(req, MAX_BODY_BYTES);
145
- }
146
-
147
- // Forward to upstream with timeout
148
- const upstream = await fetch(targetUrl, {
149
- method,
150
- headers: buildUpstreamHeaders(req),
151
- body,
152
- signal: AbortSignal.timeout(PROXY_TIMEOUT_MS),
153
- });
154
-
155
- // Relay response (binary-safe for images)
156
- res.statusCode = upstream.status;
157
- const contentType = upstream.headers.get('content-type');
158
- if (contentType) res.setHeader('Content-Type', contentType);
159
-
160
- let buf = Buffer.from(await upstream.arrayBuffer());
161
-
162
- // Resolve OASN-relative URLs in GET invocation responses so the
163
- // frontend receives absolute URLs (launch_url, content_url, etc.).
164
- if (method === 'GET' && /^\/api\/invocations\/[^/]+$/.test(targetPath) && buf.length > 0) {
165
- try {
166
- const isJson = contentType?.includes('json');
167
- if (isJson) {
168
- const oasnBase = getOasnAccessBase();
169
- if (oasnBase) {
170
- const text = buf.toString('utf-8');
171
- const resolved = text.replace(
172
- /"(launch_url|content_url)":"(\/[^"]+)"/g,
173
- (_m: string, key: string, path: string) => {
174
- try {
175
- return `"${key}":"${new URL(path, oasnBase).toString()}"`;
176
- } catch { return _m; }
177
- },
178
- );
179
- buf = Buffer.from(resolved, 'utf-8');
180
- }
181
- }
182
- } catch { /* best-effort; don't break proxy on parse errors */ }
183
- }
184
-
185
- res.end(buf);
186
-
187
- logger.info(`[proxy] ${method} ${url.pathname} → ${upstream.status} (${buf.length} bytes)`);
188
- } catch (err) {
189
- const msg = (err as Error).message;
190
- logger.warn(`[proxy] ${method} ${targetUrl} failed: ${msg}`);
191
-
192
- if (!res.headersSent) {
193
- writeJson(res, 502, { error: 'proxy upstream failed', detail: msg });
194
- }
195
- }
196
-
197
- return true;
198
- };
199
- }
200
-
201
- export function resolveOasnProxyTargetUrl(upstreamBase: string, targetPath: string, search = ''): string {
202
- return resolveOasnRelativeUrl(upstreamBase, `${targetPath}${search}`);
203
- }
204
-
205
- // ── T4: Local media serve ────────────────────────────────────────────────
206
-
207
- const MEDIA_DIR = '/tmp/openclaw/openagent-media';
208
-
209
- /** MIME type lookup for common media extensions. */
210
- const MIME_MAP: Record<string, string> = {
211
- '.mp3': 'audio/mpeg',
212
- '.wav': 'audio/wav',
213
- '.ogg': 'audio/ogg',
214
- '.m4a': 'audio/mp4',
215
- '.mp4': 'video/mp4',
216
- '.webm': 'video/webm',
217
- '.png': 'image/png',
218
- '.jpg': 'image/jpeg',
219
- '.jpeg': 'image/jpeg',
220
- '.gif': 'image/gif',
221
- '.webp': 'image/webp',
222
- '.pdf': 'application/pdf',
223
- '.txt': 'text/plain',
224
- '.md': 'text/markdown; charset=utf-8',
225
- '.json': 'application/json; charset=utf-8',
226
- '.zip': 'application/zip',
227
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
228
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
229
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
230
- };
231
-
232
- /**
233
- * Serve a file from the local openagent-media directory.
234
- * Used by T4 remote file transfer — tool result references files as
235
- * MEDIA:http://localhost:PORT/plugins/openagent/media/{filename}
236
- *
237
- * @returns true (request handled)
238
- */
239
- function serveLocalMedia(pathname: string, res: ServerResponse): true {
240
- // /media/{filename} → {filename} (prefix already stripped by Gateway)
241
- const raw = pathname.replace('/media/', '');
242
- const fileName = decodeURIComponent(raw);
243
-
244
- // Security: block path traversal, null bytes, and directory components
245
- if (!fileName || fileName.includes('..') || fileName.includes('/') || fileName.includes('\\') || fileName.includes('\0')) {
246
- logger.warn(`[proxy/media] Blocked unsafe path: ${raw}`);
247
- res.statusCode = 400;
248
- res.end('Bad request');
249
- return true;
250
- }
251
-
252
- const filePath = nodePath.join(MEDIA_DIR, fileName);
253
-
254
- // Double-check: resolved path must stay within MEDIA_DIR
255
- if (!nodePath.resolve(filePath).startsWith(nodePath.resolve(MEDIA_DIR) + nodePath.sep) &&
256
- nodePath.resolve(filePath) !== nodePath.resolve(MEDIA_DIR)) {
257
- logger.warn(`[proxy/media] Path escape attempt: ${fileName} → ${filePath}`);
258
- res.statusCode = 400;
259
- res.end('Bad request');
260
- return true;
261
- }
262
-
263
- if (!fs.existsSync(filePath)) {
264
- logger.debug(`[proxy/media] File not found: ${filePath}`);
265
- res.statusCode = 404;
266
- res.end('Not found');
267
- return true;
268
- }
269
-
270
- try {
271
- const ext = nodePath.extname(fileName).toLowerCase();
272
- const contentType = MIME_MAP[ext] ?? 'application/octet-stream';
273
- const stat = fs.statSync(filePath);
274
-
275
- res.setHeader('Content-Type', contentType);
276
- res.setHeader('Content-Length', stat.size);
277
- if (shouldDownloadAsAttachment(contentType)) {
278
- res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
279
- }
280
- // Allow browser to cache media for 1 hour
281
- res.setHeader('Cache-Control', 'private, max-age=3600');
282
-
283
- const stream = fs.createReadStream(filePath);
284
- stream.pipe(res);
285
- stream.on('error', (err) => {
286
- logger.error(`[proxy/media] Stream error: ${err.message} file=${fileName}`);
287
- if (!res.headersSent) {
288
- res.statusCode = 500;
289
- res.end('Internal error');
290
- }
291
- });
292
-
293
- logger.info(`[proxy/media] Serving ${fileName} (${contentType}, ${stat.size} bytes)`);
294
- } catch (err) {
295
- logger.error(`[proxy/media] Error serving ${fileName}: ${(err as Error).message}`);
296
- if (!res.headersSent) {
297
- res.statusCode = 500;
298
- res.end('Internal error');
299
- }
300
- }
301
-
302
- return true;
303
- }
304
-
305
- function shouldDownloadAsAttachment(contentType: string): boolean {
306
- return !contentType.startsWith('image/')
307
- && !contentType.startsWith('audio/')
308
- && !contentType.startsWith('video/')
309
- && !contentType.startsWith('text/plain');
310
- }
311
-
312
- // ── Helpers ──────────────────────────────────────────────────────────────
313
-
314
- /**
315
- * Build headers for the upstream request.
316
- * Forwards Content-Type and Authorization (needed by tagline API).
317
- *
318
- * OASN 模式:浏览器端没有 api_key,代理自动注入 Bearer token。
319
- * TIM 模式:转发浏览器自带的 Authorization(如有)。
320
- */
321
- function buildUpstreamHeaders(req: IncomingMessage): Record<string, string> {
322
- const headers: Record<string, string> = {};
323
-
324
- const ct = req.headers['content-type'];
325
- if (ct) headers['Content-Type'] = ct;
326
-
327
- const rt = registry.getDefault();
328
- if (rt?.transportType === 'oasn') {
329
- if (rt.config?.apiKey) headers['Authorization'] = `Bearer ${rt.config.apiKey}`;
330
- return headers;
331
- }
332
-
333
- // TIM mode: keep forwarding browser Authorization for the legacy auth API.
334
- const auth = req.headers['authorization'];
335
- if (auth) {
336
- headers['Authorization'] = auth;
337
- }
338
-
339
- return headers;
340
- }
341
-
342
- export function isAllowedOasnProxyRequest(method: string, pathname: string): boolean {
343
- const m = method.toUpperCase();
344
- const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
345
- const isRead = m === 'GET' || m === 'HEAD';
346
-
347
- if (isRead && p === '/api/agent-card-categories') return true;
348
- if (isRead && /^\/api\/agent-cards\/[^/]+$/.test(p)) return true;
349
- if (m === 'POST' && /^\/api\/agent-cards\/search\/(by-query|by-category|by-tags)$/.test(p)) return true;
350
- if (isRead && /^\/api\/agents\/[^/]+$/.test(p)) return true;
351
- if (m === 'POST' && p === '/api/agents/claim') return true;
352
- if (m === 'POST' && p === '/api/invocations') return true;
353
- if (isRead && /^\/api\/invocations\/[^/]+$/.test(p)) return true;
354
- if (m === 'POST' && p === '/api/files') return true;
355
- if (isRead && /^\/api\/artifacts\/[^/]+\/content$/.test(p)) return true;
356
- if (isRead && p.startsWith('/avatars/')) return true;
357
- return false;
358
- }
359
-
360
- function isAccessLayerProxyPath(method: string, pathname: string): boolean {
361
- const m = method.toUpperCase();
362
- const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
363
- const isRead = m === 'GET' || m === 'HEAD';
364
- if (m === 'POST' && p === '/api/invocations') return true;
365
- if (isRead && /^\/api\/invocations\/[^/]+$/.test(p)) return true;
366
- if (m === 'POST' && p === '/api/files') return true;
367
- if (isRead && /^\/api\/artifacts\/[^/]+\/content$/.test(p)) return true;
368
- return false;
369
- }
370
-
371
- function isOasnProxyPath(method: string, pathname: string): boolean {
372
- const m = method.toUpperCase();
373
- const p = pathname.startsWith('/') ? pathname : `/${pathname}`;
374
- const isRead = m === 'GET' || m === 'HEAD';
375
- if (isRead && p === '/api/agent-card-categories') return true;
376
- if (isRead && /^\/api\/agent-cards\/[^/]+$/.test(p)) return true;
377
- if (m === 'POST' && /^\/api\/agent-cards\/search\/(by-query|by-category|by-tags)$/.test(p)) return true;
378
- if (isRead && /^\/api\/agents\/[^/]+$/.test(p)) return true;
379
- if (m === 'POST' && p === '/api/agents/claim') return true;
380
- if (m === 'POST' && p === '/api/invocations') return true;
381
- if (isRead && /^\/api\/invocations\/[^/]+$/.test(p)) return true;
382
- if (m === 'POST' && p === '/api/files') return true;
383
- if (isRead && /^\/api\/artifacts\/[^/]+\/content$/.test(p)) return true;
384
- return false;
385
- }
386
-
387
- function writeJson(res: ServerResponse, statusCode: number, body: Record<string, unknown>): void {
388
- res.statusCode = statusCode;
389
- res.setHeader('Content-Type', 'application/json');
390
- res.end(JSON.stringify(body));
391
- }
392
-
393
- /**
394
- * Read the request body with a size limit.
395
- */
396
- function readBody(req: IncomingMessage, maxBytes: number): Promise<string> {
397
- return new Promise((resolve, reject) => {
398
- const chunks: Buffer[] = [];
399
- let total = 0;
400
-
401
- req.on('data', (chunk: Buffer) => {
402
- total += chunk.length;
403
- if (total > maxBytes) {
404
- req.destroy();
405
- reject(new Error(`Request body exceeds ${maxBytes} bytes`));
406
- return;
407
- }
408
- chunks.push(chunk);
409
- });
410
-
411
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
412
- req.on('error', reject);
413
- });
414
- }