codexmate 0.0.25 → 0.0.26

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 (135) hide show
  1. package/README.md +416 -413
  2. package/README.zh.md +349 -346
  3. package/cli/agents-files.js +224 -224
  4. package/cli/archive-helpers.js +446 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1079 -1079
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +384 -384
  9. package/cli/config-health.js +338 -338
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/openai-bridge.js +997 -997
  13. package/cli/openclaw-config.js +629 -629
  14. package/cli/session-convert-args.js +65 -0
  15. package/cli/session-convert-io.js +82 -0
  16. package/cli/session-convert.js +43 -0
  17. package/cli/session-usage.concurrent.js +28 -28
  18. package/cli/session-usage.js +118 -118
  19. package/cli/session-usage.models.js +176 -176
  20. package/cli/skills.js +1141 -1141
  21. package/cli/zip-commands.js +510 -510
  22. package/cli.js +15218 -14736
  23. package/lib/automation.js +404 -404
  24. package/lib/cli-file-utils.js +151 -151
  25. package/lib/cli-models-utils.js +379 -379
  26. package/lib/cli-network-utils.js +190 -190
  27. package/lib/cli-path-utils.js +85 -85
  28. package/lib/cli-session-utils.js +121 -121
  29. package/lib/cli-sessions.js +417 -417
  30. package/lib/cli-utils.js +155 -155
  31. package/lib/download-artifacts.js +92 -92
  32. package/lib/mcp-stdio.js +453 -453
  33. package/lib/task-orchestrator.js +869 -869
  34. package/lib/text-diff.js +303 -303
  35. package/lib/workflow-engine.js +340 -340
  36. package/package.json +74 -74
  37. package/plugins/README.md +20 -20
  38. package/plugins/README.zh-CN.md +20 -20
  39. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  40. package/plugins/prompt-templates/computed.mjs +253 -253
  41. package/plugins/prompt-templates/index.mjs +8 -8
  42. package/plugins/prompt-templates/manifest.mjs +15 -15
  43. package/plugins/prompt-templates/methods.mjs +619 -619
  44. package/plugins/prompt-templates/overview.mjs +90 -90
  45. package/plugins/prompt-templates/ownership.mjs +19 -19
  46. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  47. package/plugins/prompt-templates/storage.mjs +64 -64
  48. package/plugins/registry.mjs +16 -16
  49. package/web-ui/app.js +625 -612
  50. package/web-ui/index.html +35 -35
  51. package/web-ui/logic.agents-diff.mjs +386 -386
  52. package/web-ui/logic.claude.mjs +168 -168
  53. package/web-ui/logic.mjs +5 -5
  54. package/web-ui/logic.runtime.mjs +128 -128
  55. package/web-ui/logic.session-convert.mjs +70 -0
  56. package/web-ui/logic.sessions.mjs +709 -614
  57. package/web-ui/modules/api.mjs +90 -90
  58. package/web-ui/modules/app.computed.dashboard.mjs +171 -128
  59. package/web-ui/modules/app.computed.index.mjs +17 -17
  60. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  61. package/web-ui/modules/app.computed.session.mjs +946 -670
  62. package/web-ui/modules/app.constants.mjs +15 -15
  63. package/web-ui/modules/app.methods.agents.mjs +632 -632
  64. package/web-ui/modules/app.methods.claude-config.mjs +179 -174
  65. package/web-ui/modules/app.methods.codex-config.mjs +860 -784
  66. package/web-ui/modules/app.methods.index.mjs +92 -92
  67. package/web-ui/modules/app.methods.install.mjs +205 -205
  68. package/web-ui/modules/app.methods.navigation.mjs +743 -695
  69. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  70. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  71. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  72. package/web-ui/modules/app.methods.providers.mjs +404 -404
  73. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  74. package/web-ui/modules/app.methods.session-actions.mjs +596 -544
  75. package/web-ui/modules/app.methods.session-browser.mjs +985 -722
  76. package/web-ui/modules/app.methods.session-timeline.mjs +479 -448
  77. package/web-ui/modules/app.methods.session-trash.mjs +424 -424
  78. package/web-ui/modules/app.methods.startup-claude.mjs +522 -417
  79. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  80. package/web-ui/modules/config-mode.computed.mjs +124 -124
  81. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  82. package/web-ui/modules/i18n.dict.mjs +2113 -2055
  83. package/web-ui/modules/i18n.mjs +56 -56
  84. package/web-ui/modules/plugins.computed.mjs +3 -3
  85. package/web-ui/modules/plugins.methods.mjs +3 -3
  86. package/web-ui/modules/plugins.storage.mjs +11 -11
  87. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  88. package/web-ui/modules/skills.computed.mjs +107 -107
  89. package/web-ui/modules/skills.methods.mjs +481 -481
  90. package/web-ui/partials/index/layout-footer.html +13 -13
  91. package/web-ui/partials/index/layout-header.html +475 -475
  92. package/web-ui/partials/index/modal-config-template-agents.html +174 -174
  93. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  94. package/web-ui/partials/index/modal-health-check.html +45 -45
  95. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  96. package/web-ui/partials/index/modal-skills.html +200 -200
  97. package/web-ui/partials/index/modals-basic.html +165 -165
  98. package/web-ui/partials/index/panel-config-claude.html +184 -179
  99. package/web-ui/partials/index/panel-config-codex.html +283 -283
  100. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  101. package/web-ui/partials/index/panel-dashboard.html +186 -186
  102. package/web-ui/partials/index/panel-docs.html +147 -147
  103. package/web-ui/partials/index/panel-market.html +177 -177
  104. package/web-ui/partials/index/panel-orchestration.html +391 -391
  105. package/web-ui/partials/index/panel-plugins.html +279 -279
  106. package/web-ui/partials/index/panel-sessions.html +326 -303
  107. package/web-ui/partials/index/panel-settings.html +258 -258
  108. package/web-ui/partials/index/panel-usage.html +342 -361
  109. package/web-ui/res/json5.min.js +1 -1
  110. package/web-ui/res/vue.global.prod.js +13 -13
  111. package/web-ui/session-helpers.mjs +576 -573
  112. package/web-ui/source-bundle.cjs +233 -233
  113. package/web-ui/styles/base-theme.css +268 -264
  114. package/web-ui/styles/controls-forms.css +423 -423
  115. package/web-ui/styles/dashboard.css +274 -274
  116. package/web-ui/styles/docs-panel.css +247 -247
  117. package/web-ui/styles/feedback.css +108 -108
  118. package/web-ui/styles/health-check-dialog.css +144 -144
  119. package/web-ui/styles/layout-shell.css +603 -603
  120. package/web-ui/styles/modals-core.css +464 -464
  121. package/web-ui/styles/navigation-panels.css +390 -390
  122. package/web-ui/styles/openclaw-structured.css +266 -266
  123. package/web-ui/styles/plugins-panel.css +523 -523
  124. package/web-ui/styles/responsive.css +454 -454
  125. package/web-ui/styles/sessions-list.css +415 -398
  126. package/web-ui/styles/sessions-preview.css +411 -411
  127. package/web-ui/styles/sessions-toolbar-trash.css +330 -268
  128. package/web-ui/styles/sessions-usage.css +945 -912
  129. package/web-ui/styles/settings-panel.css +166 -166
  130. package/web-ui/styles/skills-list.css +303 -303
  131. package/web-ui/styles/skills-market.css +406 -406
  132. package/web-ui/styles/task-orchestration.css +822 -822
  133. package/web-ui/styles/titles-cards.css +408 -408
  134. package/web-ui/styles.css +21 -21
  135. package/web-ui.html +17 -17
@@ -1,1079 +1,1079 @@
1
- const http = require('http');
2
- const net = require('net');
3
- const crypto = require('crypto');
4
- const toml = require('@iarna/toml');
5
- const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
6
- const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
7
- const { toIsoTime } = require('../lib/cli-session-utils');
8
-
9
- function createBuiltinProxyRuntimeController(deps = {}) {
10
- const {
11
- fs,
12
- https,
13
- CONFIG_FILE,
14
- BUILTIN_PROXY_SETTINGS_FILE,
15
- DEFAULT_BUILTIN_PROXY_SETTINGS,
16
- BUILTIN_PROXY_PROVIDER_NAME,
17
- CODEXMATE_MANAGED_MARKER,
18
- HTTP_KEEP_ALIVE_AGENT,
19
- HTTPS_KEEP_ALIVE_AGENT,
20
- readConfig,
21
- writeConfig,
22
- readConfigOrVirtualDefault,
23
- resolveAuthTokenFromCurrentProfile,
24
- isPlainObject,
25
- isBuiltinManagedProvider,
26
- findProviderSectionRanges,
27
- findProviderDescendantSectionRanges,
28
- normalizeLegacySegments,
29
- buildLegacySegmentsKey,
30
- formatHostForUrl
31
- } = deps;
32
-
33
- if (!fs) throw new Error('createBuiltinProxyRuntimeController 缺少 fs');
34
- if (!https) throw new Error('createBuiltinProxyRuntimeController 缺少 https');
35
- if (!CONFIG_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 CONFIG_FILE');
36
- if (!BUILTIN_PROXY_SETTINGS_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_SETTINGS_FILE');
37
- if (!DEFAULT_BUILTIN_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_PROXY_SETTINGS !== 'object') {
38
- throw new Error('createBuiltinProxyRuntimeController 缺少 DEFAULT_BUILTIN_PROXY_SETTINGS');
39
- }
40
- if (!BUILTIN_PROXY_PROVIDER_NAME) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_PROVIDER_NAME');
41
- if (typeof readConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 readConfig');
42
- if (typeof writeConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 writeConfig');
43
- if (typeof readConfigOrVirtualDefault !== 'function') {
44
- throw new Error('createBuiltinProxyRuntimeController 缺少 readConfigOrVirtualDefault');
45
- }
46
- if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
47
- throw new Error('createBuiltinProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
48
- }
49
- if (typeof isPlainObject !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 isPlainObject');
50
- if (typeof isBuiltinManagedProvider !== 'function') {
51
- throw new Error('createBuiltinProxyRuntimeController 缺少 isBuiltinManagedProvider');
52
- }
53
- if (typeof findProviderSectionRanges !== 'function') {
54
- throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderSectionRanges');
55
- }
56
- if (typeof findProviderDescendantSectionRanges !== 'function') {
57
- throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderDescendantSectionRanges');
58
- }
59
- if (typeof normalizeLegacySegments !== 'function') {
60
- throw new Error('createBuiltinProxyRuntimeController 缺少 normalizeLegacySegments');
61
- }
62
- if (typeof buildLegacySegmentsKey !== 'function') {
63
- throw new Error('createBuiltinProxyRuntimeController 缺少 buildLegacySegmentsKey');
64
- }
65
- if (typeof formatHostForUrl !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 formatHostForUrl');
66
-
67
- let runtime = null;
68
-
69
- function readRequestBody(req, maxBytes) {
70
- return new Promise((resolve) => {
71
- let body = '';
72
- let size = 0;
73
- let aborted = false;
74
- req.on('data', (chunk) => {
75
- if (aborted) return;
76
- size += chunk.length;
77
- if (Number.isFinite(maxBytes) && maxBytes > 0 && size > maxBytes) {
78
- aborted = true;
79
- try { req.destroy(); } catch (_) {}
80
- resolve({ error: '请求体过大' });
81
- return;
82
- }
83
- body += chunk;
84
- });
85
- req.on('end', () => {
86
- if (aborted) return;
87
- resolve({ body });
88
- });
89
- req.on('error', (err) => resolve({ error: err && err.message ? err.message : 'request failed' }));
90
- });
91
- }
92
-
93
- function parseJsonOrError(text) {
94
- if (typeof text !== 'string' || !text.trim()) {
95
- return { value: null, error: 'empty body' };
96
- }
97
- try {
98
- return { value: JSON.parse(text), error: '' };
99
- } catch (e) {
100
- return { value: null, error: e && e.message ? e.message : 'invalid json' };
101
- }
102
- }
103
-
104
- function shouldFallbackFromUpstreamResponses(status, bodyText) {
105
- if (!Number.isFinite(status)) return false;
106
- if (status === 404 || status === 405 || status === 501) return true;
107
- const text = String(bodyText || '');
108
- if (!text) return false;
109
- if (/not implemented/i.test(text)) return true;
110
- if (/convert_request_failed/i.test(text)) return true;
111
- try {
112
- const parsed = JSON.parse(text);
113
- const code = parsed && parsed.error && typeof parsed.error.code === 'string' ? parsed.error.code : '';
114
- const msg = parsed && parsed.error && typeof parsed.error.message === 'string' ? parsed.error.message : '';
115
- if (code === 'convert_request_failed') return true;
116
- if (/not implemented/i.test(msg)) return true;
117
- } catch (_) {}
118
- return false;
119
- }
120
-
121
- function proxyRequestJson(targetUrl, options = {}) {
122
- const parsed = new URL(targetUrl);
123
- const transport = parsed.protocol === 'https:' ? https : http;
124
- const bodyText = options.body ? JSON.stringify(options.body) : '';
125
- const headers = {
126
- 'Accept': 'application/json',
127
- ...(options.body ? { 'Content-Type': 'application/json' } : {}),
128
- ...(options.headers || {})
129
- };
130
- if (options.body) {
131
- headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
132
- }
133
- const timeoutMs = Number.isFinite(options.timeoutMs)
134
- ? Math.max(1000, Number(options.timeoutMs))
135
- : 30000;
136
-
137
- return new Promise((resolve) => {
138
- let settled = false;
139
- const finish = (value) => {
140
- if (settled) return;
141
- settled = true;
142
- resolve(value);
143
- };
144
- const req = transport.request({
145
- protocol: parsed.protocol,
146
- hostname: parsed.hostname,
147
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
148
- method: options.method || 'GET',
149
- path: `${parsed.pathname}${parsed.search}`,
150
- headers,
151
- agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
152
- }, (upstreamRes) => {
153
- const chunks = [];
154
- upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
155
- upstreamRes.on('end', () => {
156
- const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
157
- finish({
158
- ok: true,
159
- status: upstreamRes.statusCode || 0,
160
- headers: upstreamRes.headers || {},
161
- bodyText: text
162
- });
163
- });
164
- });
165
- req.setTimeout(timeoutMs, () => {
166
- try { req.destroy(new Error('timeout')); } catch (_) {}
167
- finish({ ok: false, error: 'timeout' });
168
- });
169
- req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
170
- if (bodyText) {
171
- req.write(bodyText);
172
- }
173
- req.end();
174
- });
175
- }
176
-
177
- function extractChatCompletionResult(payload) {
178
- if (!payload || typeof payload !== 'object') return { text: '' };
179
- const choice = Array.isArray(payload.choices) ? payload.choices[0] : null;
180
- const message = choice && typeof choice === 'object' ? choice.message : null;
181
- const content = message && typeof message === 'object' ? message.content : '';
182
- let text = '';
183
- if (typeof content === 'string') {
184
- text = content;
185
- } else if (Array.isArray(content)) {
186
- text = content
187
- .map((item) => {
188
- if (!item) return '';
189
- if (typeof item === 'string') return item;
190
- if (typeof item === 'object') {
191
- if (typeof item.text === 'string') return item.text;
192
- if (typeof item.content === 'string') return item.content;
193
- }
194
- return '';
195
- })
196
- .filter(Boolean)
197
- .join('');
198
- }
199
- return { text };
200
- }
201
-
202
- function normalizeResponsesInputToChatMessages(input) {
203
- // 支持:
204
- // - string
205
- // - { role, content }(单条 message)
206
- // - { type:"input_text"|"input_image", ... }(单个 block)
207
- // - [{ role, content: [{type:"input_text"|"input_image", ...}] }]
208
- // - [{ type:"input_text"|"input_image", ... }](视为单条 user 消息)
209
- if (typeof input === 'string') {
210
- return [{ role: 'user', content: input }];
211
- }
212
- if (input && typeof input === 'object' && !Array.isArray(input)) {
213
- if (typeof input.role === 'string' && input.content != null) {
214
- const role = input.role.trim() || 'user';
215
- const content = Array.isArray(input.content)
216
- ? toChatContent(input.content)
217
- : input.content;
218
- return content ? [{ role, content }] : [];
219
- }
220
- // 单个 block:{type:"input_text"|"input_image", ...}
221
- if (typeof input.type === 'string') {
222
- const content = toChatContent([input]);
223
- return content ? [{ role: 'user', content }] : [];
224
- }
225
- return [];
226
- }
227
- if (!Array.isArray(input)) {
228
- return [];
229
- }
230
-
231
- const toChatContent = (blocks) => {
232
- if (!Array.isArray(blocks)) return '';
233
- const out = [];
234
- for (const block of blocks) {
235
- if (!block || typeof block !== 'object') continue;
236
- const type = typeof block.type === 'string' ? block.type : '';
237
- if (type === 'input_text' && typeof block.text === 'string') {
238
- out.push({ type: 'text', text: block.text });
239
- continue;
240
- }
241
- if (type === 'input_image') {
242
- const raw = block.image_url != null ? block.image_url : block.imageUrl;
243
- const url = typeof raw === 'string'
244
- ? raw
245
- : (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
246
- if (url) {
247
- out.push({ type: 'image_url', image_url: { url } });
248
- }
249
- continue;
250
- }
251
- // 容错:兼容已是 chat content 的 {type:"text"} / {type:"image_url"}
252
- if (type === 'text' && typeof block.text === 'string') {
253
- out.push({ type: 'text', text: block.text });
254
- continue;
255
- }
256
- if (type === 'image_url' && block.image_url) {
257
- out.push({ type: 'image_url', image_url: block.image_url });
258
- }
259
- }
260
- if (out.length === 0) return '';
261
- return out;
262
- };
263
-
264
- const messages = [];
265
- for (const item of input) {
266
- if (!item || typeof item !== 'object') continue;
267
- if (typeof item.role === 'string' && item.content != null) {
268
- const role = item.role.trim() || 'user';
269
- const content = Array.isArray(item.content)
270
- ? toChatContent(item.content)
271
- : item.content;
272
- if (content) {
273
- messages.push({ role, content });
274
- }
275
- continue;
276
- }
277
- }
278
-
279
- if (messages.length > 0) {
280
- return messages;
281
- }
282
-
283
- // 退化:把 input array 当作单条 user content blocks
284
- const fallbackContent = toChatContent(input);
285
- if (fallbackContent) {
286
- return [{ role: 'user', content: fallbackContent }];
287
- }
288
- return [];
289
- }
290
-
291
- function ensureResponseMetadata(payload) {
292
- const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
293
- const id = typeof base.id === 'string' && base.id.trim()
294
- ? base.id.trim()
295
- : `resp_${crypto.randomBytes(10).toString('hex')}`;
296
- const model = typeof base.model === 'string' ? base.model : '';
297
- return {
298
- object: 'response',
299
- id,
300
- model,
301
- ...base
302
- };
303
- }
304
-
305
- function writeSse(res, eventName, dataObj) {
306
- if (eventName) {
307
- res.write(`event: ${eventName}\n`);
308
- }
309
- if (dataObj === '[DONE]') {
310
- res.write('data: [DONE]\n\n');
311
- return;
312
- }
313
- res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
314
- }
315
-
316
- function sendResponsesSse(res, responsePayload) {
317
- const response = ensureResponseMetadata(responsePayload);
318
- const responseId = response.id;
319
- const model = response.model;
320
- let sequence = 0;
321
- const nextSeq = () => {
322
- sequence += 1;
323
- return sequence;
324
- };
325
-
326
- writeSse(res, 'response.created', {
327
- type: 'response.created',
328
- response: {
329
- id: responseId,
330
- model,
331
- created_at: response.created_at
332
- }
333
- });
334
-
335
- const output = Array.isArray(response.output) ? response.output : [];
336
- for (let outputIndex = 0; outputIndex < output.length; outputIndex += 1) {
337
- const item = output[outputIndex];
338
- if (!item || typeof item !== 'object') continue;
339
- const itemType = typeof item.type === 'string' ? item.type : '';
340
- const itemId = typeof item.id === 'string' && item.id.trim()
341
- ? item.id.trim()
342
- : `item_${crypto.randomBytes(8).toString('hex')}`;
343
-
344
- writeSse(res, 'response.output_item.added', {
345
- type: 'response.output_item.added',
346
- output_index: outputIndex,
347
- item: { ...item, id: itemId }
348
- });
349
-
350
- if (itemType === 'message') {
351
- const content = Array.isArray(item.content) ? item.content : [];
352
- for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) {
353
- const block = content[contentIndex];
354
- if (!block || typeof block !== 'object') continue;
355
- if (block.type !== 'output_text') continue;
356
- const text = typeof block.text === 'string' ? block.text : '';
357
- if (text) {
358
- writeSse(res, 'response.output_text.delta', {
359
- type: 'response.output_text.delta',
360
- item_id: itemId,
361
- output_index: outputIndex,
362
- content_index: contentIndex,
363
- delta: text,
364
- sequence_number: nextSeq()
365
- });
366
- }
367
- writeSse(res, 'response.output_text.done', {
368
- type: 'response.output_text.done',
369
- item_id: itemId,
370
- output_index: outputIndex,
371
- content_index: contentIndex,
372
- text,
373
- sequence_number: nextSeq()
374
- });
375
- }
376
- }
377
-
378
- writeSse(res, 'response.output_item.done', {
379
- type: 'response.output_item.done',
380
- output_index: outputIndex,
381
- item: { ...item, id: itemId },
382
- sequence_number: nextSeq()
383
- });
384
- }
385
-
386
- writeSse(res, 'response.completed', { type: 'response.completed', response });
387
- writeSse(res, 'done', '[DONE]');
388
- }
389
-
390
- function canListenPort(host, port) {
391
- return new Promise((resolve) => {
392
- const tester = net.createServer();
393
- tester.unref();
394
- tester.once('error', () => resolve(false));
395
- tester.once('listening', () => {
396
- tester.close(() => resolve(true));
397
- });
398
- tester.listen(port, host);
399
- });
400
- }
401
-
402
- async function findAvailablePort(host, startPort, maxAttempts = 20) {
403
- const start = parseInt(String(startPort), 10);
404
- if (!Number.isFinite(start) || start <= 0) {
405
- return 0;
406
- }
407
- const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
408
- for (let offset = 0; offset < attempts; offset += 1) {
409
- const candidate = start + offset;
410
- if (candidate > 65535) {
411
- break;
412
- }
413
- // eslint-disable-next-line no-await-in-loop
414
- const ok = await canListenPort(host, candidate);
415
- if (ok) {
416
- return candidate;
417
- }
418
- }
419
- return 0;
420
- }
421
-
422
- function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
423
- const providerMap = providers && isPlainObject(providers) ? providers : {};
424
- const providerNames = Object.keys(providerMap)
425
- .filter((name) => name && !isBuiltinManagedProvider(name));
426
- const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
427
- if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
428
- return requested;
429
- }
430
- const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
431
- if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
432
- return preferred;
433
- }
434
- return providerNames[0] || '';
435
- }
436
-
437
- function normalizeBuiltinProxySettings(raw) {
438
- const merged = {
439
- ...DEFAULT_BUILTIN_PROXY_SETTINGS,
440
- ...(isPlainObject(raw) ? raw : {})
441
- };
442
- const host = typeof merged.host === 'string' ? merged.host.trim() : '';
443
- const port = parseInt(String(merged.port), 10);
444
- const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
445
- const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
446
- const timeoutMs = parseInt(String(merged.timeoutMs), 10);
447
- const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
448
-
449
- return {
450
- enabled: merged.enabled !== false,
451
- host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
452
- port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
453
- provider,
454
- authSource,
455
- timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
456
- ? timeoutMs
457
- : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
458
- };
459
- }
460
-
461
- function readBuiltinProxySettings() {
462
- const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
463
- return normalizeBuiltinProxySettings(parsed);
464
- }
465
-
466
- function saveBuiltinProxySettings(payload = {}, options = {}) {
467
- const current = readBuiltinProxySettings();
468
- const merged = normalizeBuiltinProxySettings({
469
- ...current,
470
- ...(isPlainObject(payload) ? payload : {})
471
- });
472
-
473
- if (!merged.host) {
474
- return { error: '代理 host 不能为空' };
475
- }
476
- if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
477
- return { error: '代理端口无效(1-65535)' };
478
- }
479
-
480
- const { config } = readConfigOrVirtualDefault();
481
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
482
- const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
483
- const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
484
-
485
- const normalized = {
486
- ...merged,
487
- provider: finalProvider
488
- };
489
-
490
- if (!options.skipWrite) {
491
- writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
492
- }
493
-
494
- return {
495
- success: true,
496
- settings: normalized
497
- };
498
- }
499
-
500
- function buildProxyListenUrl(settings) {
501
- const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
502
- return `http://${host}:${settings.port}`;
503
- }
504
-
505
- function buildBuiltinProxyProviderBaseUrl(settings) {
506
- return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
507
- }
508
-
509
- function removePersistedBuiltinProxyProviderFromConfig() {
510
- if (!fs.existsSync(CONFIG_FILE)) {
511
- return { success: true, removed: false };
512
- }
513
-
514
- let config;
515
- try {
516
- config = readConfig();
517
- } catch (e) {
518
- return { error: e.message || '读取 config.toml 失败' };
519
- }
520
-
521
- if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
522
- return { success: true, removed: false };
523
- }
524
-
525
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
526
- const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
527
- const hasBom = content.charCodeAt(0) === 0xFEFF;
528
- const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
529
- const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
530
- ? providerConfig.__codexmate_legacy_segments
531
- : null;
532
- const providerSegmentVariants = (() => {
533
- const variants = [];
534
- const seen = new Set();
535
- const pushVariant = (segments) => {
536
- const normalized = normalizeLegacySegments(segments);
537
- const key = buildLegacySegmentsKey(normalized);
538
- if (!key || seen.has(key)) return;
539
- seen.add(key);
540
- variants.push(normalized);
541
- };
542
- if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
543
- pushVariant(providerConfig.__codexmate_legacy_segments);
544
- }
545
- if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
546
- for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
547
- pushVariant(segments);
548
- }
549
- }
550
- if (providerSegments) {
551
- pushVariant(providerSegments);
552
- }
553
- if (variants.length === 0) {
554
- pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
555
- }
556
- return variants;
557
- })();
558
-
559
- let updatedContent = null;
560
- const combinedRanges = [];
561
- for (const segments of providerSegmentVariants) {
562
- combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
563
- combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
564
- }
565
- if (combinedRanges.length === 0) {
566
- combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
567
- }
568
-
569
- if (combinedRanges.length > 0) {
570
- const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
571
- const seen = new Set();
572
- let removedContent = content;
573
- for (const range of sorted) {
574
- const rangeKey = `${range.start}:${range.end}`;
575
- if (seen.has(rangeKey)) continue;
576
- seen.add(rangeKey);
577
- removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
578
- }
579
- updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
580
- }
581
-
582
- if (!updatedContent) {
583
- const rebuilt = JSON.parse(JSON.stringify(config));
584
- delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
585
- const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
586
- let rebuiltToml = toml.stringify(rebuilt).trimEnd();
587
- rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
588
- if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
589
- rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
590
- }
591
- updatedContent = rebuiltToml + lineEnding;
592
- if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
593
- updatedContent = '\uFEFF' + updatedContent;
594
- }
595
- }
596
-
597
- try {
598
- writeConfig(updatedContent.trimEnd() + lineEnding);
599
- } catch (e) {
600
- return { error: e.message || '写入 config.toml 失败' };
601
- }
602
-
603
- return { success: true, removed: true };
604
- }
605
-
606
- function hasCodexConfigReadyForProxy() {
607
- const result = readConfigOrVirtualDefault();
608
- if (!result || result.isVirtual) {
609
- return false;
610
- }
611
- const config = result.config || {};
612
- if (!isPlainObject(config.model_providers)) {
613
- return false;
614
- }
615
- const providerNames = Object.keys(config.model_providers)
616
- .filter((name) => name && !isBuiltinManagedProvider(name));
617
- return providerNames.length > 0;
618
- }
619
-
620
- function resolveBuiltinProxyUpstream(settings) {
621
- const { config } = readConfigOrVirtualDefault();
622
- const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
623
- const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
624
- const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
625
- if (!providerName) {
626
- return { error: '未找到可用的上游 provider,请先添加 provider' };
627
- }
628
- if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
629
- return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
630
- }
631
- const provider = providers[providerName];
632
- if (!provider || !isPlainObject(provider)) {
633
- return { error: `上游 provider 不存在: ${providerName}` };
634
- }
635
-
636
- const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
637
- if (!baseUrl || !isValidHttpUrl(baseUrl)) {
638
- return { error: `上游 provider base_url 无效: ${providerName}` };
639
- }
640
-
641
- let token = '';
642
- if (settings.authSource === 'profile') {
643
- token = resolveAuthTokenFromCurrentProfile();
644
- } else if (settings.authSource === 'provider') {
645
- token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
646
- if (!token) {
647
- token = resolveAuthTokenFromCurrentProfile();
648
- }
649
- }
650
-
651
- let authHeader = '';
652
- if (token) {
653
- authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
654
- }
655
-
656
- return {
657
- providerName,
658
- baseUrl: normalizeBaseUrl(baseUrl),
659
- authHeader
660
- };
661
- }
662
-
663
- function createBuiltinProxyServer(settings, upstream) {
664
- const connections = new Set();
665
- const timeoutMs = settings.timeoutMs;
666
-
667
- const server = http.createServer((req, res) => {
668
- let parsedIncoming;
669
- try {
670
- parsedIncoming = new URL(req.url || '/', 'http://localhost');
671
- } catch (e) {
672
- res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
673
- res.end(JSON.stringify({ error: 'invalid request path' }));
674
- return;
675
- }
676
-
677
- const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
678
- const isLoopback = !remoteAddr
679
- || remoteAddr === '127.0.0.1'
680
- || remoteAddr === '::1'
681
- || remoteAddr === '::ffff:127.0.0.1';
682
- if (!isLoopback) {
683
- const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
684
- ? process.env.CODEXMATE_HTTP_TOKEN.trim()
685
- : '';
686
- if (!expected) {
687
- const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
688
- res.writeHead(403, {
689
- 'Content-Type': 'application/json; charset=utf-8',
690
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
691
- });
692
- res.end(body, 'utf-8');
693
- return;
694
- }
695
- const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
696
- const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
697
- const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
698
- const actual = match && match[1]
699
- ? match[1].trim()
700
- : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
701
- if (!actual || actual !== expected) {
702
- const body = JSON.stringify({ error: 'Unauthorized' });
703
- res.writeHead(401, {
704
- 'Content-Type': 'application/json; charset=utf-8',
705
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
706
- });
707
- res.end(body, 'utf-8');
708
- return;
709
- }
710
- }
711
-
712
- const incomingPath = parsedIncoming.pathname || '/';
713
- if (incomingPath === '/health' || incomingPath === '/status') {
714
- const body = JSON.stringify({
715
- ok: true,
716
- upstreamProvider: upstream.providerName,
717
- upstreamBaseUrl: upstream.baseUrl
718
- });
719
- res.writeHead(200, {
720
- 'Content-Type': 'application/json; charset=utf-8',
721
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
722
- });
723
- res.end(body, 'utf-8');
724
- return;
725
- }
726
-
727
- if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
728
- const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
729
- res.writeHead(404, {
730
- 'Content-Type': 'application/json; charset=utf-8',
731
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
732
- });
733
- res.end(body, 'utf-8');
734
- return;
735
- }
736
-
737
- // Responses shim:
738
- // - Codex CLI 默认走 /v1/responses(含 SSE)
739
- // - 某些上游只支持 /v1/chat/completions
740
- // 因此这里优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
741
- if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
742
- void (async () => {
743
- const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
744
- if (error) {
745
- res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
746
- res.end(JSON.stringify({ error }));
747
- return;
748
- }
749
- const parsed = parseJsonOrError(body);
750
- if (parsed.error) {
751
- res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
752
- res.end(JSON.stringify({ error: `invalid json: ${parsed.error}` }));
753
- return;
754
- }
755
-
756
- const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
757
- const wantsStream = payload.stream === true;
758
-
759
- const commonHeaders = {
760
- ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
761
- 'X-Codexmate-Proxy': '1'
762
- };
763
-
764
- const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
765
- const upstreamResponses = upstreamResponsesUrl
766
- ? await proxyRequestJson(upstreamResponsesUrl, {
767
- method: 'POST',
768
- headers: commonHeaders,
769
- timeoutMs,
770
- body: { ...payload, stream: false }
771
- })
772
- : { ok: false, error: 'failed to build upstream URL' };
773
-
774
- // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
775
- if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
776
- const json = parseJsonOrError(upstreamResponses.bodyText);
777
- if (json.error) {
778
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
779
- res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${json.error}` }));
780
- return;
781
- }
782
- const responsesPayload = ensureResponseMetadata(json.value);
783
- if (wantsStream) {
784
- res.writeHead(200, {
785
- 'Content-Type': 'text/event-stream; charset=utf-8',
786
- 'Cache-Control': 'no-cache',
787
- 'Connection': 'keep-alive',
788
- 'X-Accel-Buffering': 'no'
789
- });
790
- sendResponsesSse(res, responsesPayload);
791
- res.end();
792
- return;
793
- }
794
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
795
- res.end(JSON.stringify(responsesPayload));
796
- return;
797
- }
798
-
799
- if (upstreamResponses.ok && upstreamResponses.status >= 400) {
800
- if (!shouldFallbackFromUpstreamResponses(upstreamResponses.status, upstreamResponses.bodyText)) {
801
- res.writeHead(upstreamResponses.status, { 'Content-Type': 'application/json; charset=utf-8' });
802
- res.end(upstreamResponses.bodyText || JSON.stringify({ error: 'Upstream error' }));
803
- return;
804
- }
805
- // fallthrough to chat/completions conversion
806
- }
807
-
808
- if (!upstreamResponses.ok) {
809
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
810
- res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
811
- return;
812
- }
813
-
814
- const model = typeof payload.model === 'string' ? payload.model : '';
815
- const messages = normalizeResponsesInputToChatMessages(payload.input);
816
- const chatBody = {
817
- model,
818
- messages,
819
- stream: false
820
- };
821
- if (payload.max_output_tokens != null && chatBody.max_tokens == null) {
822
- chatBody.max_tokens = payload.max_output_tokens;
823
- }
824
-
825
- const upstreamChatUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
826
- if (!upstreamChatUrl) {
827
- res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
828
- res.end(JSON.stringify({ error: 'failed to build upstream URL' }));
829
- return;
830
- }
831
-
832
- const upstreamChat = await proxyRequestJson(upstreamChatUrl, {
833
- method: 'POST',
834
- headers: commonHeaders,
835
- timeoutMs,
836
- body: chatBody
837
- });
838
- if (!upstreamChat.ok) {
839
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
840
- res.end(JSON.stringify({ error: upstreamChat.error || 'proxy request failed' }));
841
- return;
842
- }
843
-
844
- const chatJson = parseJsonOrError(upstreamChat.bodyText);
845
- if (chatJson.error) {
846
- res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
847
- res.end(JSON.stringify({ error: `invalid upstream response: ${chatJson.error}` }));
848
- return;
849
- }
850
-
851
- const { text } = extractChatCompletionResult(chatJson.value);
852
- const responsesPayload = ensureResponseMetadata({
853
- model,
854
- output: [{
855
- type: 'message',
856
- role: 'assistant',
857
- content: [{ type: 'output_text', text }]
858
- }],
859
- usage: chatJson.value && chatJson.value.usage ? chatJson.value.usage : undefined
860
- });
861
-
862
- if (wantsStream) {
863
- res.writeHead(200, {
864
- 'Content-Type': 'text/event-stream; charset=utf-8',
865
- 'Cache-Control': 'no-cache',
866
- 'Connection': 'keep-alive',
867
- 'X-Accel-Buffering': 'no'
868
- });
869
- sendResponsesSse(res, responsesPayload);
870
- res.end();
871
- return;
872
- }
873
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
874
- res.end(JSON.stringify(responsesPayload));
875
- })();
876
- return;
877
- }
878
-
879
- const suffix = incomingPath === '/v1'
880
- ? ''
881
- : incomingPath.replace(/^\/v1\/?/, '');
882
- const targetBase = joinApiUrl(upstream.baseUrl, suffix);
883
- if (!targetBase) {
884
- const body = JSON.stringify({ error: 'failed to build upstream URL' });
885
- res.writeHead(500, {
886
- 'Content-Type': 'application/json; charset=utf-8',
887
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
888
- });
889
- res.end(body, 'utf-8');
890
- return;
891
- }
892
-
893
- let targetUrl;
894
- try {
895
- targetUrl = new URL(targetBase);
896
- targetUrl.search = parsedIncoming.search || '';
897
- } catch (e) {
898
- const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
899
- res.writeHead(500, {
900
- 'Content-Type': 'application/json; charset=utf-8',
901
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
902
- });
903
- res.end(body, 'utf-8');
904
- return;
905
- }
906
-
907
- const requestHeaders = { ...req.headers };
908
- delete requestHeaders.host;
909
- delete requestHeaders.connection;
910
- delete requestHeaders['content-length'];
911
- if (upstream.authHeader) {
912
- requestHeaders.authorization = upstream.authHeader;
913
- }
914
- requestHeaders['x-codexmate-proxy'] = '1';
915
- if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
916
- requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
917
- }
918
-
919
- const transport = targetUrl.protocol === 'https:' ? https : http;
920
- const upstreamReq = transport.request({
921
- protocol: targetUrl.protocol,
922
- hostname: targetUrl.hostname,
923
- port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
924
- method: req.method || 'GET',
925
- path: `${targetUrl.pathname}${targetUrl.search}`,
926
- headers: requestHeaders,
927
- agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
928
- }, (upstreamRes) => {
929
- const responseHeaders = { ...upstreamRes.headers };
930
- delete responseHeaders.connection;
931
- res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
932
- upstreamRes.pipe(res);
933
- });
934
-
935
- upstreamReq.setTimeout(timeoutMs, () => {
936
- upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
937
- });
938
-
939
- upstreamReq.on('error', (err) => {
940
- if (res.headersSent) {
941
- try { res.destroy(err); } catch (_) {}
942
- return;
943
- }
944
- const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
945
- res.writeHead(502, {
946
- 'Content-Type': 'application/json; charset=utf-8',
947
- 'Content-Length': Buffer.byteLength(body, 'utf-8')
948
- });
949
- res.end(body, 'utf-8');
950
- });
951
-
952
- req.pipe(upstreamReq);
953
- });
954
-
955
- server.on('connection', (socket) => {
956
- connections.add(socket);
957
- socket.on('close', () => connections.delete(socket));
958
- });
959
-
960
- return new Promise((resolve, reject) => {
961
- server.once('error', reject);
962
- server.listen(settings.port, settings.host, () => {
963
- server.removeListener('error', reject);
964
- resolve({
965
- server,
966
- connections,
967
- settings,
968
- upstream,
969
- startedAt: toIsoTime(Date.now()),
970
- listenUrl: buildProxyListenUrl(settings)
971
- });
972
- });
973
- });
974
- }
975
-
976
- async function startBuiltinProxyRuntime(payload = {}) {
977
- if (runtime) {
978
- return {
979
- error: '内建代理已在运行',
980
- runtime: {
981
- listenUrl: runtime.listenUrl,
982
- upstreamProvider: runtime.upstream.providerName
983
- }
984
- };
985
- }
986
-
987
- const saveResult = saveBuiltinProxySettings(payload);
988
- if (saveResult.error) {
989
- return { error: saveResult.error };
990
- }
991
- const settings = saveResult.settings;
992
- const upstream = resolveBuiltinProxyUpstream(settings);
993
- if (upstream.error) {
994
- return { error: upstream.error };
995
- }
996
-
997
- try {
998
- runtime = await createBuiltinProxyServer(settings, upstream);
999
- return {
1000
- success: true,
1001
- running: true,
1002
- listenUrl: runtime.listenUrl,
1003
- upstreamProvider: upstream.providerName,
1004
- settings
1005
- };
1006
- } catch (e) {
1007
- return { error: `启动内建代理失败: ${e.message}` };
1008
- }
1009
- }
1010
-
1011
- async function stopBuiltinProxyRuntime() {
1012
- if (!runtime) {
1013
- return { success: true, running: false };
1014
- }
1015
- const currentRuntime = runtime;
1016
- runtime = null;
1017
-
1018
- await new Promise((resolve) => {
1019
- let settled = false;
1020
- const finish = () => {
1021
- if (settled) return;
1022
- settled = true;
1023
- resolve();
1024
- };
1025
-
1026
- currentRuntime.server.close(() => finish());
1027
- setTimeout(() => finish(), 1000);
1028
- });
1029
-
1030
- for (const socket of currentRuntime.connections) {
1031
- try { socket.destroy(); } catch (_) {}
1032
- }
1033
- currentRuntime.connections.clear();
1034
-
1035
- return {
1036
- success: true,
1037
- running: false
1038
- };
1039
- }
1040
-
1041
- function getBuiltinProxyStatus() {
1042
- const settings = readBuiltinProxySettings();
1043
- return {
1044
- running: !!runtime,
1045
- settings,
1046
- runtime: runtime
1047
- ? {
1048
- provider: BUILTIN_PROXY_PROVIDER_NAME,
1049
- startedAt: runtime.startedAt,
1050
- listenUrl: runtime.listenUrl,
1051
- upstreamProvider: runtime.upstream.providerName,
1052
- upstreamBaseUrl: runtime.upstream.baseUrl
1053
- }
1054
- : null
1055
- };
1056
- }
1057
-
1058
- return {
1059
- canListenPort,
1060
- findAvailablePort,
1061
- normalizeBuiltinProxySettings,
1062
- readBuiltinProxySettings,
1063
- resolveBuiltinProxyProviderName,
1064
- saveBuiltinProxySettings,
1065
- buildProxyListenUrl,
1066
- buildBuiltinProxyProviderBaseUrl,
1067
- removePersistedBuiltinProxyProviderFromConfig,
1068
- hasCodexConfigReadyForProxy,
1069
- resolveBuiltinProxyUpstream,
1070
- createBuiltinProxyServer,
1071
- startBuiltinProxyRuntime,
1072
- stopBuiltinProxyRuntime,
1073
- getBuiltinProxyStatus
1074
- };
1075
- }
1076
-
1077
- module.exports = {
1078
- createBuiltinProxyRuntimeController
1079
- };
1
+ const http = require('http');
2
+ const net = require('net');
3
+ const crypto = require('crypto');
4
+ const toml = require('@iarna/toml');
5
+ const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
6
+ const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
7
+ const { toIsoTime } = require('../lib/cli-session-utils');
8
+
9
+ function createBuiltinProxyRuntimeController(deps = {}) {
10
+ const {
11
+ fs,
12
+ https,
13
+ CONFIG_FILE,
14
+ BUILTIN_PROXY_SETTINGS_FILE,
15
+ DEFAULT_BUILTIN_PROXY_SETTINGS,
16
+ BUILTIN_PROXY_PROVIDER_NAME,
17
+ CODEXMATE_MANAGED_MARKER,
18
+ HTTP_KEEP_ALIVE_AGENT,
19
+ HTTPS_KEEP_ALIVE_AGENT,
20
+ readConfig,
21
+ writeConfig,
22
+ readConfigOrVirtualDefault,
23
+ resolveAuthTokenFromCurrentProfile,
24
+ isPlainObject,
25
+ isBuiltinManagedProvider,
26
+ findProviderSectionRanges,
27
+ findProviderDescendantSectionRanges,
28
+ normalizeLegacySegments,
29
+ buildLegacySegmentsKey,
30
+ formatHostForUrl
31
+ } = deps;
32
+
33
+ if (!fs) throw new Error('createBuiltinProxyRuntimeController 缺少 fs');
34
+ if (!https) throw new Error('createBuiltinProxyRuntimeController 缺少 https');
35
+ if (!CONFIG_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 CONFIG_FILE');
36
+ if (!BUILTIN_PROXY_SETTINGS_FILE) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_SETTINGS_FILE');
37
+ if (!DEFAULT_BUILTIN_PROXY_SETTINGS || typeof DEFAULT_BUILTIN_PROXY_SETTINGS !== 'object') {
38
+ throw new Error('createBuiltinProxyRuntimeController 缺少 DEFAULT_BUILTIN_PROXY_SETTINGS');
39
+ }
40
+ if (!BUILTIN_PROXY_PROVIDER_NAME) throw new Error('createBuiltinProxyRuntimeController 缺少 BUILTIN_PROXY_PROVIDER_NAME');
41
+ if (typeof readConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 readConfig');
42
+ if (typeof writeConfig !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 writeConfig');
43
+ if (typeof readConfigOrVirtualDefault !== 'function') {
44
+ throw new Error('createBuiltinProxyRuntimeController 缺少 readConfigOrVirtualDefault');
45
+ }
46
+ if (typeof resolveAuthTokenFromCurrentProfile !== 'function') {
47
+ throw new Error('createBuiltinProxyRuntimeController 缺少 resolveAuthTokenFromCurrentProfile');
48
+ }
49
+ if (typeof isPlainObject !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 isPlainObject');
50
+ if (typeof isBuiltinManagedProvider !== 'function') {
51
+ throw new Error('createBuiltinProxyRuntimeController 缺少 isBuiltinManagedProvider');
52
+ }
53
+ if (typeof findProviderSectionRanges !== 'function') {
54
+ throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderSectionRanges');
55
+ }
56
+ if (typeof findProviderDescendantSectionRanges !== 'function') {
57
+ throw new Error('createBuiltinProxyRuntimeController 缺少 findProviderDescendantSectionRanges');
58
+ }
59
+ if (typeof normalizeLegacySegments !== 'function') {
60
+ throw new Error('createBuiltinProxyRuntimeController 缺少 normalizeLegacySegments');
61
+ }
62
+ if (typeof buildLegacySegmentsKey !== 'function') {
63
+ throw new Error('createBuiltinProxyRuntimeController 缺少 buildLegacySegmentsKey');
64
+ }
65
+ if (typeof formatHostForUrl !== 'function') throw new Error('createBuiltinProxyRuntimeController 缺少 formatHostForUrl');
66
+
67
+ let runtime = null;
68
+
69
+ function readRequestBody(req, maxBytes) {
70
+ return new Promise((resolve) => {
71
+ let body = '';
72
+ let size = 0;
73
+ let aborted = false;
74
+ req.on('data', (chunk) => {
75
+ if (aborted) return;
76
+ size += chunk.length;
77
+ if (Number.isFinite(maxBytes) && maxBytes > 0 && size > maxBytes) {
78
+ aborted = true;
79
+ try { req.destroy(); } catch (_) {}
80
+ resolve({ error: '请求体过大' });
81
+ return;
82
+ }
83
+ body += chunk;
84
+ });
85
+ req.on('end', () => {
86
+ if (aborted) return;
87
+ resolve({ body });
88
+ });
89
+ req.on('error', (err) => resolve({ error: err && err.message ? err.message : 'request failed' }));
90
+ });
91
+ }
92
+
93
+ function parseJsonOrError(text) {
94
+ if (typeof text !== 'string' || !text.trim()) {
95
+ return { value: null, error: 'empty body' };
96
+ }
97
+ try {
98
+ return { value: JSON.parse(text), error: '' };
99
+ } catch (e) {
100
+ return { value: null, error: e && e.message ? e.message : 'invalid json' };
101
+ }
102
+ }
103
+
104
+ function shouldFallbackFromUpstreamResponses(status, bodyText) {
105
+ if (!Number.isFinite(status)) return false;
106
+ if (status === 404 || status === 405 || status === 501) return true;
107
+ const text = String(bodyText || '');
108
+ if (!text) return false;
109
+ if (/not implemented/i.test(text)) return true;
110
+ if (/convert_request_failed/i.test(text)) return true;
111
+ try {
112
+ const parsed = JSON.parse(text);
113
+ const code = parsed && parsed.error && typeof parsed.error.code === 'string' ? parsed.error.code : '';
114
+ const msg = parsed && parsed.error && typeof parsed.error.message === 'string' ? parsed.error.message : '';
115
+ if (code === 'convert_request_failed') return true;
116
+ if (/not implemented/i.test(msg)) return true;
117
+ } catch (_) {}
118
+ return false;
119
+ }
120
+
121
+ function proxyRequestJson(targetUrl, options = {}) {
122
+ const parsed = new URL(targetUrl);
123
+ const transport = parsed.protocol === 'https:' ? https : http;
124
+ const bodyText = options.body ? JSON.stringify(options.body) : '';
125
+ const headers = {
126
+ 'Accept': 'application/json',
127
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
128
+ ...(options.headers || {})
129
+ };
130
+ if (options.body) {
131
+ headers['Content-Length'] = Buffer.byteLength(bodyText, 'utf-8');
132
+ }
133
+ const timeoutMs = Number.isFinite(options.timeoutMs)
134
+ ? Math.max(1000, Number(options.timeoutMs))
135
+ : 30000;
136
+
137
+ return new Promise((resolve) => {
138
+ let settled = false;
139
+ const finish = (value) => {
140
+ if (settled) return;
141
+ settled = true;
142
+ resolve(value);
143
+ };
144
+ const req = transport.request({
145
+ protocol: parsed.protocol,
146
+ hostname: parsed.hostname,
147
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
148
+ method: options.method || 'GET',
149
+ path: `${parsed.pathname}${parsed.search}`,
150
+ headers,
151
+ agent: parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
152
+ }, (upstreamRes) => {
153
+ const chunks = [];
154
+ upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
155
+ upstreamRes.on('end', () => {
156
+ const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
157
+ finish({
158
+ ok: true,
159
+ status: upstreamRes.statusCode || 0,
160
+ headers: upstreamRes.headers || {},
161
+ bodyText: text
162
+ });
163
+ });
164
+ });
165
+ req.setTimeout(timeoutMs, () => {
166
+ try { req.destroy(new Error('timeout')); } catch (_) {}
167
+ finish({ ok: false, error: 'timeout' });
168
+ });
169
+ req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
170
+ if (bodyText) {
171
+ req.write(bodyText);
172
+ }
173
+ req.end();
174
+ });
175
+ }
176
+
177
+ function extractChatCompletionResult(payload) {
178
+ if (!payload || typeof payload !== 'object') return { text: '' };
179
+ const choice = Array.isArray(payload.choices) ? payload.choices[0] : null;
180
+ const message = choice && typeof choice === 'object' ? choice.message : null;
181
+ const content = message && typeof message === 'object' ? message.content : '';
182
+ let text = '';
183
+ if (typeof content === 'string') {
184
+ text = content;
185
+ } else if (Array.isArray(content)) {
186
+ text = content
187
+ .map((item) => {
188
+ if (!item) return '';
189
+ if (typeof item === 'string') return item;
190
+ if (typeof item === 'object') {
191
+ if (typeof item.text === 'string') return item.text;
192
+ if (typeof item.content === 'string') return item.content;
193
+ }
194
+ return '';
195
+ })
196
+ .filter(Boolean)
197
+ .join('');
198
+ }
199
+ return { text };
200
+ }
201
+
202
+ function normalizeResponsesInputToChatMessages(input) {
203
+ // 支持:
204
+ // - string
205
+ // - { role, content }(单条 message)
206
+ // - { type:"input_text"|"input_image", ... }(单个 block)
207
+ // - [{ role, content: [{type:"input_text"|"input_image", ...}] }]
208
+ // - [{ type:"input_text"|"input_image", ... }](视为单条 user 消息)
209
+ if (typeof input === 'string') {
210
+ return [{ role: 'user', content: input }];
211
+ }
212
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
213
+ if (typeof input.role === 'string' && input.content != null) {
214
+ const role = input.role.trim() || 'user';
215
+ const content = Array.isArray(input.content)
216
+ ? toChatContent(input.content)
217
+ : input.content;
218
+ return content ? [{ role, content }] : [];
219
+ }
220
+ // 单个 block:{type:"input_text"|"input_image", ...}
221
+ if (typeof input.type === 'string') {
222
+ const content = toChatContent([input]);
223
+ return content ? [{ role: 'user', content }] : [];
224
+ }
225
+ return [];
226
+ }
227
+ if (!Array.isArray(input)) {
228
+ return [];
229
+ }
230
+
231
+ const toChatContent = (blocks) => {
232
+ if (!Array.isArray(blocks)) return '';
233
+ const out = [];
234
+ for (const block of blocks) {
235
+ if (!block || typeof block !== 'object') continue;
236
+ const type = typeof block.type === 'string' ? block.type : '';
237
+ if (type === 'input_text' && typeof block.text === 'string') {
238
+ out.push({ type: 'text', text: block.text });
239
+ continue;
240
+ }
241
+ if (type === 'input_image') {
242
+ const raw = block.image_url != null ? block.image_url : block.imageUrl;
243
+ const url = typeof raw === 'string'
244
+ ? raw
245
+ : (raw && typeof raw === 'object' && typeof raw.url === 'string' ? raw.url : '');
246
+ if (url) {
247
+ out.push({ type: 'image_url', image_url: { url } });
248
+ }
249
+ continue;
250
+ }
251
+ // 容错:兼容已是 chat content 的 {type:"text"} / {type:"image_url"}
252
+ if (type === 'text' && typeof block.text === 'string') {
253
+ out.push({ type: 'text', text: block.text });
254
+ continue;
255
+ }
256
+ if (type === 'image_url' && block.image_url) {
257
+ out.push({ type: 'image_url', image_url: block.image_url });
258
+ }
259
+ }
260
+ if (out.length === 0) return '';
261
+ return out;
262
+ };
263
+
264
+ const messages = [];
265
+ for (const item of input) {
266
+ if (!item || typeof item !== 'object') continue;
267
+ if (typeof item.role === 'string' && item.content != null) {
268
+ const role = item.role.trim() || 'user';
269
+ const content = Array.isArray(item.content)
270
+ ? toChatContent(item.content)
271
+ : item.content;
272
+ if (content) {
273
+ messages.push({ role, content });
274
+ }
275
+ continue;
276
+ }
277
+ }
278
+
279
+ if (messages.length > 0) {
280
+ return messages;
281
+ }
282
+
283
+ // 退化:把 input array 当作单条 user content blocks
284
+ const fallbackContent = toChatContent(input);
285
+ if (fallbackContent) {
286
+ return [{ role: 'user', content: fallbackContent }];
287
+ }
288
+ return [];
289
+ }
290
+
291
+ function ensureResponseMetadata(payload) {
292
+ const base = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
293
+ const id = typeof base.id === 'string' && base.id.trim()
294
+ ? base.id.trim()
295
+ : `resp_${crypto.randomBytes(10).toString('hex')}`;
296
+ const model = typeof base.model === 'string' ? base.model : '';
297
+ return {
298
+ object: 'response',
299
+ id,
300
+ model,
301
+ ...base
302
+ };
303
+ }
304
+
305
+ function writeSse(res, eventName, dataObj) {
306
+ if (eventName) {
307
+ res.write(`event: ${eventName}\n`);
308
+ }
309
+ if (dataObj === '[DONE]') {
310
+ res.write('data: [DONE]\n\n');
311
+ return;
312
+ }
313
+ res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
314
+ }
315
+
316
+ function sendResponsesSse(res, responsePayload) {
317
+ const response = ensureResponseMetadata(responsePayload);
318
+ const responseId = response.id;
319
+ const model = response.model;
320
+ let sequence = 0;
321
+ const nextSeq = () => {
322
+ sequence += 1;
323
+ return sequence;
324
+ };
325
+
326
+ writeSse(res, 'response.created', {
327
+ type: 'response.created',
328
+ response: {
329
+ id: responseId,
330
+ model,
331
+ created_at: response.created_at
332
+ }
333
+ });
334
+
335
+ const output = Array.isArray(response.output) ? response.output : [];
336
+ for (let outputIndex = 0; outputIndex < output.length; outputIndex += 1) {
337
+ const item = output[outputIndex];
338
+ if (!item || typeof item !== 'object') continue;
339
+ const itemType = typeof item.type === 'string' ? item.type : '';
340
+ const itemId = typeof item.id === 'string' && item.id.trim()
341
+ ? item.id.trim()
342
+ : `item_${crypto.randomBytes(8).toString('hex')}`;
343
+
344
+ writeSse(res, 'response.output_item.added', {
345
+ type: 'response.output_item.added',
346
+ output_index: outputIndex,
347
+ item: { ...item, id: itemId }
348
+ });
349
+
350
+ if (itemType === 'message') {
351
+ const content = Array.isArray(item.content) ? item.content : [];
352
+ for (let contentIndex = 0; contentIndex < content.length; contentIndex += 1) {
353
+ const block = content[contentIndex];
354
+ if (!block || typeof block !== 'object') continue;
355
+ if (block.type !== 'output_text') continue;
356
+ const text = typeof block.text === 'string' ? block.text : '';
357
+ if (text) {
358
+ writeSse(res, 'response.output_text.delta', {
359
+ type: 'response.output_text.delta',
360
+ item_id: itemId,
361
+ output_index: outputIndex,
362
+ content_index: contentIndex,
363
+ delta: text,
364
+ sequence_number: nextSeq()
365
+ });
366
+ }
367
+ writeSse(res, 'response.output_text.done', {
368
+ type: 'response.output_text.done',
369
+ item_id: itemId,
370
+ output_index: outputIndex,
371
+ content_index: contentIndex,
372
+ text,
373
+ sequence_number: nextSeq()
374
+ });
375
+ }
376
+ }
377
+
378
+ writeSse(res, 'response.output_item.done', {
379
+ type: 'response.output_item.done',
380
+ output_index: outputIndex,
381
+ item: { ...item, id: itemId },
382
+ sequence_number: nextSeq()
383
+ });
384
+ }
385
+
386
+ writeSse(res, 'response.completed', { type: 'response.completed', response });
387
+ writeSse(res, 'done', '[DONE]');
388
+ }
389
+
390
+ function canListenPort(host, port) {
391
+ return new Promise((resolve) => {
392
+ const tester = net.createServer();
393
+ tester.unref();
394
+ tester.once('error', () => resolve(false));
395
+ tester.once('listening', () => {
396
+ tester.close(() => resolve(true));
397
+ });
398
+ tester.listen(port, host);
399
+ });
400
+ }
401
+
402
+ async function findAvailablePort(host, startPort, maxAttempts = 20) {
403
+ const start = parseInt(String(startPort), 10);
404
+ if (!Number.isFinite(start) || start <= 0) {
405
+ return 0;
406
+ }
407
+ const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
408
+ for (let offset = 0; offset < attempts; offset += 1) {
409
+ const candidate = start + offset;
410
+ if (candidate > 65535) {
411
+ break;
412
+ }
413
+ // eslint-disable-next-line no-await-in-loop
414
+ const ok = await canListenPort(host, candidate);
415
+ if (ok) {
416
+ return candidate;
417
+ }
418
+ }
419
+ return 0;
420
+ }
421
+
422
+ function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
423
+ const providerMap = providers && isPlainObject(providers) ? providers : {};
424
+ const providerNames = Object.keys(providerMap)
425
+ .filter((name) => name && !isBuiltinManagedProvider(name));
426
+ const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
427
+ if (requested && !isBuiltinManagedProvider(requested) && providerMap[requested]) {
428
+ return requested;
429
+ }
430
+ const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
431
+ if (preferred && !isBuiltinManagedProvider(preferred) && providerMap[preferred]) {
432
+ return preferred;
433
+ }
434
+ return providerNames[0] || '';
435
+ }
436
+
437
+ function normalizeBuiltinProxySettings(raw) {
438
+ const merged = {
439
+ ...DEFAULT_BUILTIN_PROXY_SETTINGS,
440
+ ...(isPlainObject(raw) ? raw : {})
441
+ };
442
+ const host = typeof merged.host === 'string' ? merged.host.trim() : '';
443
+ const port = parseInt(String(merged.port), 10);
444
+ const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
445
+ const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
446
+ const timeoutMs = parseInt(String(merged.timeoutMs), 10);
447
+ const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
448
+
449
+ return {
450
+ enabled: merged.enabled !== false,
451
+ host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
452
+ port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
453
+ provider,
454
+ authSource,
455
+ timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000
456
+ ? timeoutMs
457
+ : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
458
+ };
459
+ }
460
+
461
+ function readBuiltinProxySettings() {
462
+ const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
463
+ return normalizeBuiltinProxySettings(parsed);
464
+ }
465
+
466
+ function saveBuiltinProxySettings(payload = {}, options = {}) {
467
+ const current = readBuiltinProxySettings();
468
+ const merged = normalizeBuiltinProxySettings({
469
+ ...current,
470
+ ...(isPlainObject(payload) ? payload : {})
471
+ });
472
+
473
+ if (!merged.host) {
474
+ return { error: '代理 host 不能为空' };
475
+ }
476
+ if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
477
+ return { error: '代理端口无效(1-65535)' };
478
+ }
479
+
480
+ const { config } = readConfigOrVirtualDefault();
481
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
482
+ const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
483
+ const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
484
+
485
+ const normalized = {
486
+ ...merged,
487
+ provider: finalProvider
488
+ };
489
+
490
+ if (!options.skipWrite) {
491
+ writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
492
+ }
493
+
494
+ return {
495
+ success: true,
496
+ settings: normalized
497
+ };
498
+ }
499
+
500
+ function buildProxyListenUrl(settings) {
501
+ const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
502
+ return `http://${host}:${settings.port}`;
503
+ }
504
+
505
+ function buildBuiltinProxyProviderBaseUrl(settings) {
506
+ return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`;
507
+ }
508
+
509
+ function removePersistedBuiltinProxyProviderFromConfig() {
510
+ if (!fs.existsSync(CONFIG_FILE)) {
511
+ return { success: true, removed: false };
512
+ }
513
+
514
+ let config;
515
+ try {
516
+ config = readConfig();
517
+ } catch (e) {
518
+ return { error: e.message || '读取 config.toml 失败' };
519
+ }
520
+
521
+ if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) {
522
+ return { success: true, removed: false };
523
+ }
524
+
525
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
526
+ const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
527
+ const hasBom = content.charCodeAt(0) === 0xFEFF;
528
+ const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
529
+ const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)
530
+ ? providerConfig.__codexmate_legacy_segments
531
+ : null;
532
+ const providerSegmentVariants = (() => {
533
+ const variants = [];
534
+ const seen = new Set();
535
+ const pushVariant = (segments) => {
536
+ const normalized = normalizeLegacySegments(segments);
537
+ const key = buildLegacySegmentsKey(normalized);
538
+ if (!key || seen.has(key)) return;
539
+ seen.add(key);
540
+ variants.push(normalized);
541
+ };
542
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) {
543
+ pushVariant(providerConfig.__codexmate_legacy_segments);
544
+ }
545
+ if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) {
546
+ for (const segments of providerConfig.__codexmate_legacy_segment_variants) {
547
+ pushVariant(segments);
548
+ }
549
+ }
550
+ if (providerSegments) {
551
+ pushVariant(providerSegments);
552
+ }
553
+ if (variants.length === 0) {
554
+ pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item));
555
+ }
556
+ return variants;
557
+ })();
558
+
559
+ let updatedContent = null;
560
+ const combinedRanges = [];
561
+ for (const segments of providerSegmentVariants) {
562
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments));
563
+ combinedRanges.push(...findProviderDescendantSectionRanges(content, segments));
564
+ }
565
+ if (combinedRanges.length === 0) {
566
+ combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments));
567
+ }
568
+
569
+ if (combinedRanges.length > 0) {
570
+ const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end);
571
+ const seen = new Set();
572
+ let removedContent = content;
573
+ for (const range of sorted) {
574
+ const rangeKey = `${range.start}:${range.end}`;
575
+ if (seen.has(rangeKey)) continue;
576
+ seen.add(rangeKey);
577
+ removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end);
578
+ }
579
+ updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding);
580
+ }
581
+
582
+ if (!updatedContent) {
583
+ const rebuilt = JSON.parse(JSON.stringify(config));
584
+ delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME];
585
+ const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
586
+ let rebuiltToml = toml.stringify(rebuilt).trimEnd();
587
+ rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
588
+ if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
589
+ rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
590
+ }
591
+ updatedContent = rebuiltToml + lineEnding;
592
+ if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
593
+ updatedContent = '\uFEFF' + updatedContent;
594
+ }
595
+ }
596
+
597
+ try {
598
+ writeConfig(updatedContent.trimEnd() + lineEnding);
599
+ } catch (e) {
600
+ return { error: e.message || '写入 config.toml 失败' };
601
+ }
602
+
603
+ return { success: true, removed: true };
604
+ }
605
+
606
+ function hasCodexConfigReadyForProxy() {
607
+ const result = readConfigOrVirtualDefault();
608
+ if (!result || result.isVirtual) {
609
+ return false;
610
+ }
611
+ const config = result.config || {};
612
+ if (!isPlainObject(config.model_providers)) {
613
+ return false;
614
+ }
615
+ const providerNames = Object.keys(config.model_providers)
616
+ .filter((name) => name && !isBuiltinManagedProvider(name));
617
+ return providerNames.length > 0;
618
+ }
619
+
620
+ function resolveBuiltinProxyUpstream(settings) {
621
+ const { config } = readConfigOrVirtualDefault();
622
+ const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
623
+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
624
+ const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
625
+ if (!providerName) {
626
+ return { error: '未找到可用的上游 provider,请先添加 provider' };
627
+ }
628
+ if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
629
+ return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
630
+ }
631
+ const provider = providers[providerName];
632
+ if (!provider || !isPlainObject(provider)) {
633
+ return { error: `上游 provider 不存在: ${providerName}` };
634
+ }
635
+
636
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
637
+ if (!baseUrl || !isValidHttpUrl(baseUrl)) {
638
+ return { error: `上游 provider base_url 无效: ${providerName}` };
639
+ }
640
+
641
+ let token = '';
642
+ if (settings.authSource === 'profile') {
643
+ token = resolveAuthTokenFromCurrentProfile();
644
+ } else if (settings.authSource === 'provider') {
645
+ token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
646
+ if (!token) {
647
+ token = resolveAuthTokenFromCurrentProfile();
648
+ }
649
+ }
650
+
651
+ let authHeader = '';
652
+ if (token) {
653
+ authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
654
+ }
655
+
656
+ return {
657
+ providerName,
658
+ baseUrl: normalizeBaseUrl(baseUrl),
659
+ authHeader
660
+ };
661
+ }
662
+
663
+ function createBuiltinProxyServer(settings, upstream) {
664
+ const connections = new Set();
665
+ const timeoutMs = settings.timeoutMs;
666
+
667
+ const server = http.createServer((req, res) => {
668
+ let parsedIncoming;
669
+ try {
670
+ parsedIncoming = new URL(req.url || '/', 'http://localhost');
671
+ } catch (e) {
672
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
673
+ res.end(JSON.stringify({ error: 'invalid request path' }));
674
+ return;
675
+ }
676
+
677
+ const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
678
+ const isLoopback = !remoteAddr
679
+ || remoteAddr === '127.0.0.1'
680
+ || remoteAddr === '::1'
681
+ || remoteAddr === '::ffff:127.0.0.1';
682
+ if (!isLoopback) {
683
+ const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
684
+ ? process.env.CODEXMATE_HTTP_TOKEN.trim()
685
+ : '';
686
+ if (!expected) {
687
+ const body = JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' });
688
+ res.writeHead(403, {
689
+ 'Content-Type': 'application/json; charset=utf-8',
690
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
691
+ });
692
+ res.end(body, 'utf-8');
693
+ return;
694
+ }
695
+ const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
696
+ const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
697
+ const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
698
+ const actual = match && match[1]
699
+ ? match[1].trim()
700
+ : (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
701
+ if (!actual || actual !== expected) {
702
+ const body = JSON.stringify({ error: 'Unauthorized' });
703
+ res.writeHead(401, {
704
+ 'Content-Type': 'application/json; charset=utf-8',
705
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
706
+ });
707
+ res.end(body, 'utf-8');
708
+ return;
709
+ }
710
+ }
711
+
712
+ const incomingPath = parsedIncoming.pathname || '/';
713
+ if (incomingPath === '/health' || incomingPath === '/status') {
714
+ const body = JSON.stringify({
715
+ ok: true,
716
+ upstreamProvider: upstream.providerName,
717
+ upstreamBaseUrl: upstream.baseUrl
718
+ });
719
+ res.writeHead(200, {
720
+ 'Content-Type': 'application/json; charset=utf-8',
721
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
722
+ });
723
+ res.end(body, 'utf-8');
724
+ return;
725
+ }
726
+
727
+ if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
728
+ const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
729
+ res.writeHead(404, {
730
+ 'Content-Type': 'application/json; charset=utf-8',
731
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
732
+ });
733
+ res.end(body, 'utf-8');
734
+ return;
735
+ }
736
+
737
+ // Responses shim:
738
+ // - Codex CLI 默认走 /v1/responses(含 SSE)
739
+ // - 某些上游只支持 /v1/chat/completions
740
+ // 因此这里优先尝试 /v1/responses(stream=false),失败再转换到 chat/completions 并回包为 responses。
741
+ if ((incomingPath === '/v1/responses' || incomingPath === '/v1/responses/') && (req.method || 'GET').toUpperCase() === 'POST') {
742
+ void (async () => {
743
+ const { body, error } = await readRequestBody(req, 10 * 1024 * 1024);
744
+ if (error) {
745
+ res.writeHead(413, { 'Content-Type': 'application/json; charset=utf-8' });
746
+ res.end(JSON.stringify({ error }));
747
+ return;
748
+ }
749
+ const parsed = parseJsonOrError(body);
750
+ if (parsed.error) {
751
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
752
+ res.end(JSON.stringify({ error: `invalid json: ${parsed.error}` }));
753
+ return;
754
+ }
755
+
756
+ const payload = parsed.value && typeof parsed.value === 'object' ? parsed.value : {};
757
+ const wantsStream = payload.stream === true;
758
+
759
+ const commonHeaders = {
760
+ ...(upstream.authHeader ? { 'Authorization': upstream.authHeader } : {}),
761
+ 'X-Codexmate-Proxy': '1'
762
+ };
763
+
764
+ const upstreamResponsesUrl = joinApiUrl(upstream.baseUrl, 'responses');
765
+ const upstreamResponses = upstreamResponsesUrl
766
+ ? await proxyRequestJson(upstreamResponsesUrl, {
767
+ method: 'POST',
768
+ headers: commonHeaders,
769
+ timeoutMs,
770
+ body: { ...payload, stream: false }
771
+ })
772
+ : { ok: false, error: 'failed to build upstream URL' };
773
+
774
+ // 优先走上游 /responses(如果支持)。若上游报错且不是“端点不支持”,则直接透传错误。
775
+ if (upstreamResponses.ok && upstreamResponses.status >= 200 && upstreamResponses.status < 300) {
776
+ const json = parseJsonOrError(upstreamResponses.bodyText);
777
+ if (json.error) {
778
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
779
+ res.end(JSON.stringify({ error: `Upstream JSON parse failed: ${json.error}` }));
780
+ return;
781
+ }
782
+ const responsesPayload = ensureResponseMetadata(json.value);
783
+ if (wantsStream) {
784
+ res.writeHead(200, {
785
+ 'Content-Type': 'text/event-stream; charset=utf-8',
786
+ 'Cache-Control': 'no-cache',
787
+ 'Connection': 'keep-alive',
788
+ 'X-Accel-Buffering': 'no'
789
+ });
790
+ sendResponsesSse(res, responsesPayload);
791
+ res.end();
792
+ return;
793
+ }
794
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
795
+ res.end(JSON.stringify(responsesPayload));
796
+ return;
797
+ }
798
+
799
+ if (upstreamResponses.ok && upstreamResponses.status >= 400) {
800
+ if (!shouldFallbackFromUpstreamResponses(upstreamResponses.status, upstreamResponses.bodyText)) {
801
+ res.writeHead(upstreamResponses.status, { 'Content-Type': 'application/json; charset=utf-8' });
802
+ res.end(upstreamResponses.bodyText || JSON.stringify({ error: 'Upstream error' }));
803
+ return;
804
+ }
805
+ // fallthrough to chat/completions conversion
806
+ }
807
+
808
+ if (!upstreamResponses.ok) {
809
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
810
+ res.end(JSON.stringify({ error: upstreamResponses.error || 'Upstream request failed' }));
811
+ return;
812
+ }
813
+
814
+ const model = typeof payload.model === 'string' ? payload.model : '';
815
+ const messages = normalizeResponsesInputToChatMessages(payload.input);
816
+ const chatBody = {
817
+ model,
818
+ messages,
819
+ stream: false
820
+ };
821
+ if (payload.max_output_tokens != null && chatBody.max_tokens == null) {
822
+ chatBody.max_tokens = payload.max_output_tokens;
823
+ }
824
+
825
+ const upstreamChatUrl = joinApiUrl(upstream.baseUrl, 'chat/completions');
826
+ if (!upstreamChatUrl) {
827
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
828
+ res.end(JSON.stringify({ error: 'failed to build upstream URL' }));
829
+ return;
830
+ }
831
+
832
+ const upstreamChat = await proxyRequestJson(upstreamChatUrl, {
833
+ method: 'POST',
834
+ headers: commonHeaders,
835
+ timeoutMs,
836
+ body: chatBody
837
+ });
838
+ if (!upstreamChat.ok) {
839
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
840
+ res.end(JSON.stringify({ error: upstreamChat.error || 'proxy request failed' }));
841
+ return;
842
+ }
843
+
844
+ const chatJson = parseJsonOrError(upstreamChat.bodyText);
845
+ if (chatJson.error) {
846
+ res.writeHead(502, { 'Content-Type': 'application/json; charset=utf-8' });
847
+ res.end(JSON.stringify({ error: `invalid upstream response: ${chatJson.error}` }));
848
+ return;
849
+ }
850
+
851
+ const { text } = extractChatCompletionResult(chatJson.value);
852
+ const responsesPayload = ensureResponseMetadata({
853
+ model,
854
+ output: [{
855
+ type: 'message',
856
+ role: 'assistant',
857
+ content: [{ type: 'output_text', text }]
858
+ }],
859
+ usage: chatJson.value && chatJson.value.usage ? chatJson.value.usage : undefined
860
+ });
861
+
862
+ if (wantsStream) {
863
+ res.writeHead(200, {
864
+ 'Content-Type': 'text/event-stream; charset=utf-8',
865
+ 'Cache-Control': 'no-cache',
866
+ 'Connection': 'keep-alive',
867
+ 'X-Accel-Buffering': 'no'
868
+ });
869
+ sendResponsesSse(res, responsesPayload);
870
+ res.end();
871
+ return;
872
+ }
873
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
874
+ res.end(JSON.stringify(responsesPayload));
875
+ })();
876
+ return;
877
+ }
878
+
879
+ const suffix = incomingPath === '/v1'
880
+ ? ''
881
+ : incomingPath.replace(/^\/v1\/?/, '');
882
+ const targetBase = joinApiUrl(upstream.baseUrl, suffix);
883
+ if (!targetBase) {
884
+ const body = JSON.stringify({ error: 'failed to build upstream URL' });
885
+ res.writeHead(500, {
886
+ 'Content-Type': 'application/json; charset=utf-8',
887
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
888
+ });
889
+ res.end(body, 'utf-8');
890
+ return;
891
+ }
892
+
893
+ let targetUrl;
894
+ try {
895
+ targetUrl = new URL(targetBase);
896
+ targetUrl.search = parsedIncoming.search || '';
897
+ } catch (e) {
898
+ const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
899
+ res.writeHead(500, {
900
+ 'Content-Type': 'application/json; charset=utf-8',
901
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
902
+ });
903
+ res.end(body, 'utf-8');
904
+ return;
905
+ }
906
+
907
+ const requestHeaders = { ...req.headers };
908
+ delete requestHeaders.host;
909
+ delete requestHeaders.connection;
910
+ delete requestHeaders['content-length'];
911
+ if (upstream.authHeader) {
912
+ requestHeaders.authorization = upstream.authHeader;
913
+ }
914
+ requestHeaders['x-codexmate-proxy'] = '1';
915
+ if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
916
+ requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
917
+ }
918
+
919
+ const transport = targetUrl.protocol === 'https:' ? https : http;
920
+ const upstreamReq = transport.request({
921
+ protocol: targetUrl.protocol,
922
+ hostname: targetUrl.hostname,
923
+ port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
924
+ method: req.method || 'GET',
925
+ path: `${targetUrl.pathname}${targetUrl.search}`,
926
+ headers: requestHeaders,
927
+ agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
928
+ }, (upstreamRes) => {
929
+ const responseHeaders = { ...upstreamRes.headers };
930
+ delete responseHeaders.connection;
931
+ res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
932
+ upstreamRes.pipe(res);
933
+ });
934
+
935
+ upstreamReq.setTimeout(timeoutMs, () => {
936
+ upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
937
+ });
938
+
939
+ upstreamReq.on('error', (err) => {
940
+ if (res.headersSent) {
941
+ try { res.destroy(err); } catch (_) {}
942
+ return;
943
+ }
944
+ const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
945
+ res.writeHead(502, {
946
+ 'Content-Type': 'application/json; charset=utf-8',
947
+ 'Content-Length': Buffer.byteLength(body, 'utf-8')
948
+ });
949
+ res.end(body, 'utf-8');
950
+ });
951
+
952
+ req.pipe(upstreamReq);
953
+ });
954
+
955
+ server.on('connection', (socket) => {
956
+ connections.add(socket);
957
+ socket.on('close', () => connections.delete(socket));
958
+ });
959
+
960
+ return new Promise((resolve, reject) => {
961
+ server.once('error', reject);
962
+ server.listen(settings.port, settings.host, () => {
963
+ server.removeListener('error', reject);
964
+ resolve({
965
+ server,
966
+ connections,
967
+ settings,
968
+ upstream,
969
+ startedAt: toIsoTime(Date.now()),
970
+ listenUrl: buildProxyListenUrl(settings)
971
+ });
972
+ });
973
+ });
974
+ }
975
+
976
+ async function startBuiltinProxyRuntime(payload = {}) {
977
+ if (runtime) {
978
+ return {
979
+ error: '内建代理已在运行',
980
+ runtime: {
981
+ listenUrl: runtime.listenUrl,
982
+ upstreamProvider: runtime.upstream.providerName
983
+ }
984
+ };
985
+ }
986
+
987
+ const saveResult = saveBuiltinProxySettings(payload);
988
+ if (saveResult.error) {
989
+ return { error: saveResult.error };
990
+ }
991
+ const settings = saveResult.settings;
992
+ const upstream = resolveBuiltinProxyUpstream(settings);
993
+ if (upstream.error) {
994
+ return { error: upstream.error };
995
+ }
996
+
997
+ try {
998
+ runtime = await createBuiltinProxyServer(settings, upstream);
999
+ return {
1000
+ success: true,
1001
+ running: true,
1002
+ listenUrl: runtime.listenUrl,
1003
+ upstreamProvider: upstream.providerName,
1004
+ settings
1005
+ };
1006
+ } catch (e) {
1007
+ return { error: `启动内建代理失败: ${e.message}` };
1008
+ }
1009
+ }
1010
+
1011
+ async function stopBuiltinProxyRuntime() {
1012
+ if (!runtime) {
1013
+ return { success: true, running: false };
1014
+ }
1015
+ const currentRuntime = runtime;
1016
+ runtime = null;
1017
+
1018
+ await new Promise((resolve) => {
1019
+ let settled = false;
1020
+ const finish = () => {
1021
+ if (settled) return;
1022
+ settled = true;
1023
+ resolve();
1024
+ };
1025
+
1026
+ currentRuntime.server.close(() => finish());
1027
+ setTimeout(() => finish(), 1000);
1028
+ });
1029
+
1030
+ for (const socket of currentRuntime.connections) {
1031
+ try { socket.destroy(); } catch (_) {}
1032
+ }
1033
+ currentRuntime.connections.clear();
1034
+
1035
+ return {
1036
+ success: true,
1037
+ running: false
1038
+ };
1039
+ }
1040
+
1041
+ function getBuiltinProxyStatus() {
1042
+ const settings = readBuiltinProxySettings();
1043
+ return {
1044
+ running: !!runtime,
1045
+ settings,
1046
+ runtime: runtime
1047
+ ? {
1048
+ provider: BUILTIN_PROXY_PROVIDER_NAME,
1049
+ startedAt: runtime.startedAt,
1050
+ listenUrl: runtime.listenUrl,
1051
+ upstreamProvider: runtime.upstream.providerName,
1052
+ upstreamBaseUrl: runtime.upstream.baseUrl
1053
+ }
1054
+ : null
1055
+ };
1056
+ }
1057
+
1058
+ return {
1059
+ canListenPort,
1060
+ findAvailablePort,
1061
+ normalizeBuiltinProxySettings,
1062
+ readBuiltinProxySettings,
1063
+ resolveBuiltinProxyProviderName,
1064
+ saveBuiltinProxySettings,
1065
+ buildProxyListenUrl,
1066
+ buildBuiltinProxyProviderBaseUrl,
1067
+ removePersistedBuiltinProxyProviderFromConfig,
1068
+ hasCodexConfigReadyForProxy,
1069
+ resolveBuiltinProxyUpstream,
1070
+ createBuiltinProxyServer,
1071
+ startBuiltinProxyRuntime,
1072
+ stopBuiltinProxyRuntime,
1073
+ getBuiltinProxyStatus
1074
+ };
1075
+ }
1076
+
1077
+ module.exports = {
1078
+ createBuiltinProxyRuntimeController
1079
+ };