codexmate 0.0.19 → 0.0.21

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 (102) hide show
  1. package/README.en.md +349 -255
  2. package/README.md +284 -248
  3. package/cli/agents-files.js +162 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +580 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/config-health.js +338 -0
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/skills.js +1141 -0
  12. package/cli/zip-commands.js +510 -0
  13. package/cli.js +13129 -12973
  14. package/lib/cli-file-utils.js +151 -151
  15. package/lib/cli-models-utils.js +419 -152
  16. package/lib/cli-network-utils.js +164 -148
  17. package/lib/cli-path-utils.js +69 -0
  18. package/lib/cli-session-utils.js +121 -121
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/cli-utils.js +155 -155
  21. package/lib/download-artifacts.js +77 -0
  22. package/lib/mcp-stdio.js +440 -440
  23. package/lib/task-orchestrator.js +869 -0
  24. package/lib/text-diff.js +303 -303
  25. package/lib/workflow-engine.js +340 -340
  26. package/package.json +74 -63
  27. package/res/json5.min.js +1 -1
  28. package/res/vue.global.prod.js +13 -0
  29. package/web-ui/app.js +530 -5548
  30. package/web-ui/index.html +33 -2246
  31. package/web-ui/logic.agents-diff.mjs +386 -0
  32. package/web-ui/logic.claude.mjs +168 -0
  33. package/web-ui/logic.mjs +5 -793
  34. package/web-ui/logic.runtime.mjs +124 -0
  35. package/web-ui/logic.sessions.mjs +581 -0
  36. package/web-ui/modules/api.mjs +90 -0
  37. package/web-ui/modules/app.computed.dashboard.mjs +113 -0
  38. package/web-ui/modules/app.computed.index.mjs +15 -0
  39. package/web-ui/modules/app.computed.main-tabs.mjs +195 -0
  40. package/web-ui/modules/app.computed.session.mjs +507 -0
  41. package/web-ui/modules/app.constants.mjs +15 -0
  42. package/web-ui/modules/app.methods.agents.mjs +493 -0
  43. package/web-ui/modules/app.methods.claude-config.mjs +174 -0
  44. package/web-ui/modules/app.methods.codex-config.mjs +640 -0
  45. package/web-ui/modules/app.methods.index.mjs +88 -0
  46. package/web-ui/modules/app.methods.install.mjs +149 -0
  47. package/web-ui/modules/app.methods.navigation.mjs +619 -0
  48. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -0
  49. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -0
  50. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -0
  51. package/web-ui/modules/app.methods.providers.mjs +363 -0
  52. package/web-ui/modules/app.methods.runtime.mjs +323 -0
  53. package/web-ui/modules/app.methods.session-actions.mjs +520 -0
  54. package/web-ui/modules/app.methods.session-browser.mjs +626 -0
  55. package/web-ui/modules/app.methods.session-timeline.mjs +448 -0
  56. package/web-ui/modules/app.methods.session-trash.mjs +422 -0
  57. package/web-ui/modules/app.methods.startup-claude.mjs +412 -0
  58. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  59. package/web-ui/modules/config-mode.computed.mjs +126 -124
  60. package/web-ui/modules/skills.computed.mjs +107 -107
  61. package/web-ui/modules/skills.methods.mjs +481 -481
  62. package/web-ui/partials/index/layout-footer.html +13 -0
  63. package/web-ui/partials/index/layout-header.html +402 -0
  64. package/web-ui/partials/index/modal-config-template-agents.html +125 -0
  65. package/web-ui/partials/index/modal-confirm-toast.html +32 -0
  66. package/web-ui/partials/index/modal-health-check.html +72 -0
  67. package/web-ui/partials/index/modal-openclaw-config.html +280 -0
  68. package/web-ui/partials/index/modal-skills.html +184 -0
  69. package/web-ui/partials/index/modals-basic.html +156 -0
  70. package/web-ui/partials/index/panel-config-claude.html +126 -0
  71. package/web-ui/partials/index/panel-config-codex.html +237 -0
  72. package/web-ui/partials/index/panel-config-openclaw.html +78 -0
  73. package/web-ui/partials/index/panel-docs.html +130 -0
  74. package/web-ui/partials/index/panel-market.html +174 -0
  75. package/web-ui/partials/index/panel-orchestration.html +397 -0
  76. package/web-ui/partials/index/panel-sessions.html +292 -0
  77. package/web-ui/partials/index/panel-settings.html +190 -0
  78. package/web-ui/partials/index/panel-usage.html +213 -0
  79. package/web-ui/session-helpers.mjs +559 -362
  80. package/web-ui/source-bundle.cjs +233 -0
  81. package/web-ui/styles/base-theme.css +271 -0
  82. package/web-ui/styles/controls-forms.css +360 -0
  83. package/web-ui/styles/docs-panel.css +182 -0
  84. package/web-ui/styles/feedback.css +108 -0
  85. package/web-ui/styles/health-check-dialog.css +144 -0
  86. package/web-ui/styles/layout-shell.css +376 -0
  87. package/web-ui/styles/modals-core.css +464 -0
  88. package/web-ui/styles/navigation-panels.css +348 -0
  89. package/web-ui/styles/openclaw-structured.css +266 -0
  90. package/web-ui/styles/responsive.css +450 -0
  91. package/web-ui/styles/sessions-list.css +400 -0
  92. package/web-ui/styles/sessions-preview.css +411 -0
  93. package/web-ui/styles/sessions-toolbar-trash.css +243 -0
  94. package/web-ui/styles/sessions-usage.css +628 -0
  95. package/web-ui/styles/skills-list.css +296 -0
  96. package/web-ui/styles/skills-market.css +335 -0
  97. package/web-ui/styles/task-orchestration.css +776 -0
  98. package/web-ui/styles/titles-cards.css +408 -0
  99. package/web-ui/styles.css +18 -4668
  100. package/web-ui.html +17 -17
  101. package/res/screenshot.png +0 -0
  102. package/res/vue.global.js +0 -18552
@@ -0,0 +1,580 @@
1
+ const http = require('http');
2
+ const net = require('net');
3
+ const toml = require('@iarna/toml');
4
+ const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
5
+ const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
6
+ const { toIsoTime } = require('../lib/cli-session-utils');
7
+
8
+ function createBuiltinProxyRuntimeController(deps = {}) {
9
+ const {
10
+ fs,
11
+ https,
12
+ CONFIG_FILE,
13
+ BUILTIN_PROXY_SETTINGS_FILE,
14
+ DEFAULT_BUILTIN_PROXY_SETTINGS,
15
+ BUILTIN_PROXY_PROVIDER_NAME,
16
+ CODEXMATE_MANAGED_MARKER,
17
+ HTTP_KEEP_ALIVE_AGENT,
18
+ HTTPS_KEEP_ALIVE_AGENT,
19
+ readConfig,
20
+ writeConfig,
21
+ readConfigOrVirtualDefault,
22
+ resolveAuthTokenFromCurrentProfile,
23
+ isPlainObject,
24
+ isBuiltinManagedProvider,
25
+ findProviderSectionRanges,
26
+ findProviderDescendantSectionRanges,
27
+ normalizeLegacySegments,
28
+ buildLegacySegmentsKey,
29
+ formatHostForUrl
30
+ } = deps;
31
+
32
+ if (!fs) throw new Error('createBuiltinProxyRuntimeController 缺少 fs');
33
+ if (!https) throw new Error('createBuiltinProxyRuntimeController 缺少 https');
34
+ if (!CONFIG_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 CONFIG_FILE');
35
+ if (!BUILTIN_PROXY_SETTINGS_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_SETTINGS_FILE');
36
+ if (!DEFAULT_BUILTIN_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_PROXY_SETTINGS !== 'object') {
37
+ throw new Error('createBuiltinProxyRuntimeController 缺少 DEFAULT_BUILTIN_PROXY_SETTINGS');
38
+ }
39
+ if (!BUILTIN_PROXY_PROVIDER_NAME) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_PROVIDER_NAME');
40
+ if (typeof readConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 readConfig');
41
+ if (typeof writeConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 writeConfig');
42
+ if (typeof readConfigOrVirtualDefault !== 'function') {
43
+ throw new Error('createBuiltinProxyRuntimeController 缺少 readConfigOrVirtualDefault');
44
+ }
45
+ if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
46
+ throw new Error('createBuiltinProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
47
+ }
48
+ if (typeof isPlainObject !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 isPlainObject');
49
+ if (typeof isBuiltinManagedProvider !== 'function') {
50
+ throw new Error('createBuiltinProxyRuntimeController 缺少 isBuiltinManagedProvider');
51
+ }
52
+ if (typeof findProviderSectionRanges !== 'function') {
53
+ throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderSectionRanges');
54
+ }
55
+ if (typeof findProviderDescendantSectionRanges !== 'function') {
56
+ throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderDescendantSectionRanges');
57
+ }
58
+ if (typeof normalizeLegacySegments !== 'function') {
59
+ throw new Error('createBuiltinProxyRuntimeController 缺少 normalizeLegacySegments');
60
+ }
61
+ if (typeof buildLegacySegmentsKey !== 'function') {
62
+ throw new Error('createBuiltinProxyRuntimeController 缺少 buildLegacySegmentsKey');
63
+ }
64
+ if (typeof formatHostForUrl !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 formatHostForUrl');
65
+
66
+ let runtime = null;
67
+
68
+ function canListenPort(host, port) {
69
+ return new Promise((resolve) => {
70
+ const tester = net.createServer();
71
+ tester.unref();
72
+ tester.once('error', () => resolve(false));
73
+ tester.once('listening', () => {
74
+ tester.close(() => resolve(true));
75
+ });
76
+ tester.listen(port, host);
77
+ });
78
+ }
79
+
80
+ async function findAvailablePort(host, startPort, maxAttempts = 20) {
81
+ const start = parseInt(String(startPort), 10);
82
+ if (!Number.isFinite(start) || start <= 0) {
83
+ return 0;
84
+ }
85
+ const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
86
+ for (let offset = 0; offset < attempts; offset += 1) {
87
+ const candidate = start + offset;
88
+ if (candidate > 65535) {
89
+ break;
90
+ }
91
+ // eslint-disable-next-line no-await-in-loop
92
+ const ok = await canListenPort(host, candidate);
93
+ if (ok) {
94
+ return candidate;
95
+ }
96
+ }
97
+ return 0;
98
+ }
99
+
100
+ function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
101
+ const providerMap = providers && isPlainObject(providers) ? providers : {};
102
+ const providerNames = Object.keys(providerMap)
103
+ .filter((name) => name && !isBuiltinManagedProvider(name));
104
+ const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
105
+ if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
106
+ return requested;
107
+ }
108
+ const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
109
+ if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
110
+ return preferred;
111
+ }
112
+ return providerNames[0] || '';
113
+ }
114
+
115
+ function normalizeBuiltinProxySettings(raw) {
116
+ const merged = {
117
+ ...DEFAULT_BUILTIN_PROXY_SETTINGS,
118
+ ...(isPlainObject(raw) ? raw : {})
119
+ };
120
+ const host = typeof merged.host === 'string' ? merged.host.trim() : '';
121
+ const port = parseInt(String(merged.port), 10);
122
+ const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
123
+ const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
124
+ const timeoutMs = parseInt(String(merged.timeoutMs), 10);
125
+ const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
126
+
127
+ return {
128
+ enabled: merged.enabled !== false,
129
+ host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
130
+ port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
131
+ provider,
132
+ authSource,
133
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
134
+ ? timeoutMs
135
+ : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
136
+ };
137
+ }
138
+
139
+ function readBuiltinProxySettings() {
140
+ const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
141
+ return normalizeBuiltinProxySettings(parsed);
142
+ }
143
+
144
+ function saveBuiltinProxySettings(payload = {}, options = {}) {
145
+ const current = readBuiltinProxySettings();
146
+ const merged = normalizeBuiltinProxySettings({
147
+ ...current,
148
+ ...(isPlainObject(payload) ? payload : {})
149
+ });
150
+
151
+ if (!merged.host) {
152
+ return { error: '代理 host 不能为空' };
153
+ }
154
+ if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
155
+ return { error: '代理端口无效(1-65535)' };
156
+ }
157
+
158
+ const { config } = readConfigOrVirtualDefault();
159
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
160
+ const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
161
+ const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
162
+
163
+ const normalized = {
164
+ ...merged,
165
+ provider: finalProvider
166
+ };
167
+
168
+ if (!options.skipWrite) {
169
+ writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
170
+ }
171
+
172
+ return {
173
+ success: true,
174
+ settings: normalized
175
+ };
176
+ }
177
+
178
+ function buildProxyListenUrl(settings) {
179
+ const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
180
+ return `http://${host}:${settings.port}`;
181
+ }
182
+
183
+ function buildBuiltinProxyProviderBaseUrl(settings) {
184
+ return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
185
+ }
186
+
187
+ function removePersistedBuiltinProxyProviderFromConfig() {
188
+ if (!fs.existsSync(CONFIG_FILE)) {
189
+ return { success: true, removed: false };
190
+ }
191
+
192
+ let config;
193
+ try {
194
+ config = readConfig();
195
+ } catch (e) {
196
+ return { error: e.message || '读取 config.toml 失败' };
197
+ }
198
+
199
+ if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
200
+ return { success: true, removed: false };
201
+ }
202
+
203
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
204
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
205
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
206
+ const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
207
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
208
+ ? providerConfig.__codexmate_legacy_segments
209
+ : null;
210
+ const providerSegmentVariants = (() => {
211
+ const variants = [];
212
+ const seen = new Set();
213
+ const pushVariant = (segments) => {
214
+ const normalized = normalizeLegacySegments(segments);
215
+ const key = buildLegacySegmentsKey(normalized);
216
+ if (!key || seen.has(key)) return;
217
+ seen.add(key);
218
+ variants.push(normalized);
219
+ };
220
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
221
+ pushVariant(providerConfig.__codexmate_legacy_segments);
222
+ }
223
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
224
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
225
+ pushVariant(segments);
226
+ }
227
+ }
228
+ if (providerSegments) {
229
+ pushVariant(providerSegments);
230
+ }
231
+ if (variants.length === 0) {
232
+ pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
233
+ }
234
+ return variants;
235
+ })();
236
+
237
+ let updatedContent = null;
238
+ const combinedRanges = [];
239
+ for (const segments of providerSegmentVariants) {
240
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
241
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
242
+ }
243
+ if (combinedRanges.length === 0) {
244
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
245
+ }
246
+
247
+ if (combinedRanges.length > 0) {
248
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
249
+ const seen = new Set();
250
+ let removedContent = content;
251
+ for (const range of sorted) {
252
+ const rangeKey = `${range.start}:${range.end}`;
253
+ if (seen.has(rangeKey)) continue;
254
+ seen.add(rangeKey);
255
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
256
+ }
257
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
258
+ }
259
+
260
+ if (!updatedContent) {
261
+ const rebuilt = JSON.parse(JSON.stringify(config));
262
+ delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
263
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
264
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
265
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
266
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
267
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
268
+ }
269
+ updatedContent = rebuiltToml + lineEnding;
270
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
271
+ updatedContent = '\uFEFF' + updatedContent;
272
+ }
273
+ }
274
+
275
+ try {
276
+ writeConfig(updatedContent.trimEnd() + lineEnding);
277
+ } catch (e) {
278
+ return { error: e.message || '写入 config.toml 失败' };
279
+ }
280
+
281
+ return { success: true, removed: true };
282
+ }
283
+
284
+ function hasCodexConfigReadyForProxy() {
285
+ const result = readConfigOrVirtualDefault();
286
+ if (!result || result.isVirtual) {
287
+ return false;
288
+ }
289
+ const config = result.config || {};
290
+ if (!isPlainObject(config.model_providers)) {
291
+ return false;
292
+ }
293
+ const providerNames = Object.keys(config.model_providers)
294
+ .filter((name) => name && !isBuiltinManagedProvider(name));
295
+ return providerNames.length > 0;
296
+ }
297
+
298
+ function resolveBuiltinProxyUpstream(settings) {
299
+ const { config } = readConfigOrVirtualDefault();
300
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
301
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
302
+ const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
303
+ if (!providerName) {
304
+ return { error: '未找到可用的上游 provider,请先添加 provider' };
305
+ }
306
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
307
+ return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
308
+ }
309
+ const provider = providers[providerName];
310
+ if (!provider || !isPlainObject(provider)) {
311
+ return { error: `上游 provider 不存在: ${providerName}` };
312
+ }
313
+
314
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
315
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
316
+ return { error: `上游 provider base_url 无效: ${providerName}` };
317
+ }
318
+
319
+ let token = '';
320
+ if (settings.authSource === 'profile') {
321
+ token = resolveAuthTokenFromCurrentProfile();
322
+ } else if (settings.authSource === 'provider') {
323
+ token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
324
+ if (!token) {
325
+ token = resolveAuthTokenFromCurrentProfile();
326
+ }
327
+ }
328
+
329
+ let authHeader = '';
330
+ if (token) {
331
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
332
+ }
333
+
334
+ return {
335
+ providerName,
336
+ baseUrl: normalizeBaseUrl(baseUrl),
337
+ authHeader
338
+ };
339
+ }
340
+
341
+ function createBuiltinProxyServer(settings, upstream) {
342
+ const connections = new Set();
343
+ const timeoutMs = settings.timeoutMs;
344
+
345
+ const server = http.createServer((req, res) => {
346
+ let parsedIncoming;
347
+ try {
348
+ parsedIncoming = new URL(req.url || '/', 'http://localhost');
349
+ } catch (e) {
350
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
351
+ res.end(JSON.stringify({ error: 'invalid request path' }));
352
+ return;
353
+ }
354
+
355
+ const incomingPath = parsedIncoming.pathname || '/';
356
+ if (incomingPath === '/health' || incomingPath === '/status') {
357
+ const body = JSON.stringify({
358
+ ok: true,
359
+ upstreamProvider: upstream.providerName,
360
+ upstreamBaseUrl: upstream.baseUrl
361
+ });
362
+ res.writeHead(200, {
363
+ 'Content-Type': 'application/json; charset=utf-8',
364
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
365
+ });
366
+ res.end(body, 'utf-8');
367
+ return;
368
+ }
369
+
370
+ if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
371
+ const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
372
+ res.writeHead(404, {
373
+ 'Content-Type': 'application/json; charset=utf-8',
374
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
375
+ });
376
+ res.end(body, 'utf-8');
377
+ return;
378
+ }
379
+
380
+ const suffix = incomingPath === '/v1'
381
+ ? ''
382
+ : incomingPath.replace(/^\/v1\/?/, '');
383
+ const targetBase = joinApiUrl(upstream.baseUrl, suffix);
384
+ if (!targetBase) {
385
+ const body = JSON.stringify({ error: 'failed to build upstream URL' });
386
+ res.writeHead(500, {
387
+ 'Content-Type': 'application/json; charset=utf-8',
388
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
389
+ });
390
+ res.end(body, 'utf-8');
391
+ return;
392
+ }
393
+
394
+ let targetUrl;
395
+ try {
396
+ targetUrl = new URL(targetBase);
397
+ targetUrl.search = parsedIncoming.search || '';
398
+ } catch (e) {
399
+ const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
400
+ res.writeHead(500, {
401
+ 'Content-Type': 'application/json; charset=utf-8',
402
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
403
+ });
404
+ res.end(body, 'utf-8');
405
+ return;
406
+ }
407
+
408
+ const requestHeaders = { ...req.headers };
409
+ delete requestHeaders.host;
410
+ delete requestHeaders.connection;
411
+ delete requestHeaders['content-length'];
412
+ if (upstream.authHeader) {
413
+ requestHeaders.authorization = upstream.authHeader;
414
+ }
415
+ requestHeaders['x-codexmate-proxy'] = '1';
416
+ if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
417
+ requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
418
+ }
419
+
420
+ const transport = targetUrl.protocol === 'https:' ? https : http;
421
+ const upstreamReq = transport.request({
422
+ protocol: targetUrl.protocol,
423
+ hostname: targetUrl.hostname,
424
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
425
+ method: req.method || 'GET',
426
+ path: `${targetUrl.pathname}${targetUrl.search}`,
427
+ headers: requestHeaders,
428
+ agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
429
+ }, (upstreamRes) => {
430
+ const responseHeaders = { ...upstreamRes.headers };
431
+ delete responseHeaders.connection;
432
+ res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
433
+ upstreamRes.pipe(res);
434
+ });
435
+
436
+ upstreamReq.setTimeout(timeoutMs, () => {
437
+ upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
438
+ });
439
+
440
+ upstreamReq.on('error', (err) => {
441
+ if (res.headersSent) {
442
+ try { res.destroy(err); } catch (_) {}
443
+ return;
444
+ }
445
+ const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
446
+ res.writeHead(502, {
447
+ 'Content-Type': 'application/json; charset=utf-8',
448
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
449
+ });
450
+ res.end(body, 'utf-8');
451
+ });
452
+
453
+ req.pipe(upstreamReq);
454
+ });
455
+
456
+ server.on('connection', (socket) => {
457
+ connections.add(socket);
458
+ socket.on('close', () => connections.delete(socket));
459
+ });
460
+
461
+ return new Promise((resolve, reject) => {
462
+ server.once('error', reject);
463
+ server.listen(settings.port, settings.host, () => {
464
+ server.removeListener('error', reject);
465
+ resolve({
466
+ server,
467
+ connections,
468
+ settings,
469
+ upstream,
470
+ startedAt: toIsoTime(Date.now()),
471
+ listenUrl: buildProxyListenUrl(settings)
472
+ });
473
+ });
474
+ });
475
+ }
476
+
477
+ async function startBuiltinProxyRuntime(payload = {}) {
478
+ if (runtime) {
479
+ return {
480
+ error: '内建代理已在运行',
481
+ runtime: {
482
+ listenUrl: runtime.listenUrl,
483
+ upstreamProvider: runtime.upstream.providerName
484
+ }
485
+ };
486
+ }
487
+
488
+ const saveResult = saveBuiltinProxySettings(payload);
489
+ if (saveResult.error) {
490
+ return { error: saveResult.error };
491
+ }
492
+ const settings = saveResult.settings;
493
+ const upstream = resolveBuiltinProxyUpstream(settings);
494
+ if (upstream.error) {
495
+ return { error: upstream.error };
496
+ }
497
+
498
+ try {
499
+ runtime = await createBuiltinProxyServer(settings, upstream);
500
+ return {
501
+ success: true,
502
+ running: true,
503
+ listenUrl: runtime.listenUrl,
504
+ upstreamProvider: upstream.providerName,
505
+ settings
506
+ };
507
+ } catch (e) {
508
+ return { error: `启动内建代理失败: ${e.message}` };
509
+ }
510
+ }
511
+
512
+ async function stopBuiltinProxyRuntime() {
513
+ if (!runtime) {
514
+ return { success: true, running: false };
515
+ }
516
+ const currentRuntime = runtime;
517
+ runtime = null;
518
+
519
+ await new Promise((resolve) => {
520
+ let settled = false;
521
+ const finish = () => {
522
+ if (settled) return;
523
+ settled = true;
524
+ resolve();
525
+ };
526
+
527
+ currentRuntime.server.close(() => finish());
528
+ setTimeout(() => finish(), 1000);
529
+ });
530
+
531
+ for (const socket of currentRuntime.connections) {
532
+ try { socket.destroy(); } catch (_) {}
533
+ }
534
+ currentRuntime.connections.clear();
535
+
536
+ return {
537
+ success: true,
538
+ running: false
539
+ };
540
+ }
541
+
542
+ function getBuiltinProxyStatus() {
543
+ const settings = readBuiltinProxySettings();
544
+ return {
545
+ running: !!runtime,
546
+ settings,
547
+ runtime: runtime
548
+ ? {
549
+ provider: BUILTIN_PROXY_PROVIDER_NAME,
550
+ startedAt: runtime.startedAt,
551
+ listenUrl: runtime.listenUrl,
552
+ upstreamProvider: runtime.upstream.providerName,
553
+ upstreamBaseUrl: runtime.upstream.baseUrl
554
+ }
555
+ : null
556
+ };
557
+ }
558
+
559
+ return {
560
+ canListenPort,
561
+ findAvailablePort,
562
+ normalizeBuiltinProxySettings,
563
+ readBuiltinProxySettings,
564
+ resolveBuiltinProxyProviderName,
565
+ saveBuiltinProxySettings,
566
+ buildProxyListenUrl,
567
+ buildBuiltinProxyProviderBaseUrl,
568
+ removePersistedBuiltinProxyProviderFromConfig,
569
+ hasCodexConfigReadyForProxy,
570
+ resolveBuiltinProxyUpstream,
571
+ createBuiltinProxyServer,
572
+ startBuiltinProxyRuntime,
573
+ stopBuiltinProxyRuntime,
574
+ getBuiltinProxyStatus
575
+ };
576
+ }
577
+
578
+ module.exports = {
579
+ createBuiltinProxyRuntimeController
580
+ };