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