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.
- package/README.en.md +349 -255
- package/README.md +284 -248
- package/cli/agents-files.js +162 -0
- package/cli/archive-helpers.js +446 -0
- package/cli/auth-profiles.js +359 -0
- package/cli/builtin-proxy.js +580 -0
- package/cli/claude-proxy.js +998 -0
- package/cli/config-bootstrap.js +384 -0
- package/cli/config-health.js +338 -0
- package/cli/openclaw-config.js +629 -0
- package/cli/skills.js +1141 -0
- package/cli/zip-commands.js +510 -0
- package/cli.js +13129 -12973
- package/lib/cli-file-utils.js +151 -151
- package/lib/cli-models-utils.js +419 -152
- package/lib/cli-network-utils.js +164 -148
- package/lib/cli-path-utils.js +69 -0
- package/lib/cli-session-utils.js +121 -121
- package/lib/cli-sessions.js +386 -0
- package/lib/cli-utils.js +155 -155
- package/lib/download-artifacts.js +77 -0
- package/lib/mcp-stdio.js +440 -440
- package/lib/task-orchestrator.js +869 -0
- package/lib/text-diff.js +303 -303
- package/lib/workflow-engine.js +340 -340
- package/package.json +74 -63
- package/res/json5.min.js +1 -1
- package/res/vue.global.prod.js +13 -0
- package/web-ui/app.js +530 -5548
- package/web-ui/index.html +33 -2246
- package/web-ui/logic.agents-diff.mjs +386 -0
- package/web-ui/logic.claude.mjs +168 -0
- package/web-ui/logic.mjs +5 -793
- package/web-ui/logic.runtime.mjs +124 -0
- package/web-ui/logic.sessions.mjs +581 -0
- package/web-ui/modules/api.mjs +90 -0
- package/web-ui/modules/app.computed.dashboard.mjs +113 -0
- package/web-ui/modules/app.computed.index.mjs +15 -0
- package/web-ui/modules/app.computed.main-tabs.mjs +195 -0
- package/web-ui/modules/app.computed.session.mjs +507 -0
- package/web-ui/modules/app.constants.mjs +15 -0
- package/web-ui/modules/app.methods.agents.mjs +493 -0
- package/web-ui/modules/app.methods.claude-config.mjs +174 -0
- package/web-ui/modules/app.methods.codex-config.mjs +640 -0
- package/web-ui/modules/app.methods.index.mjs +88 -0
- package/web-ui/modules/app.methods.install.mjs +149 -0
- package/web-ui/modules/app.methods.navigation.mjs +619 -0
- package/web-ui/modules/app.methods.openclaw-core.mjs +814 -0
- package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -0
- package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -0
- package/web-ui/modules/app.methods.providers.mjs +363 -0
- package/web-ui/modules/app.methods.runtime.mjs +323 -0
- package/web-ui/modules/app.methods.session-actions.mjs +520 -0
- package/web-ui/modules/app.methods.session-browser.mjs +626 -0
- package/web-ui/modules/app.methods.session-timeline.mjs +448 -0
- package/web-ui/modules/app.methods.session-trash.mjs +422 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +412 -0
- package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
- package/web-ui/modules/config-mode.computed.mjs +126 -124
- package/web-ui/modules/skills.computed.mjs +107 -107
- package/web-ui/modules/skills.methods.mjs +481 -481
- package/web-ui/partials/index/layout-footer.html +13 -0
- package/web-ui/partials/index/layout-header.html +402 -0
- package/web-ui/partials/index/modal-config-template-agents.html +125 -0
- package/web-ui/partials/index/modal-confirm-toast.html +32 -0
- package/web-ui/partials/index/modal-health-check.html +72 -0
- package/web-ui/partials/index/modal-openclaw-config.html +280 -0
- package/web-ui/partials/index/modal-skills.html +184 -0
- package/web-ui/partials/index/modals-basic.html +156 -0
- package/web-ui/partials/index/panel-config-claude.html +126 -0
- package/web-ui/partials/index/panel-config-codex.html +237 -0
- package/web-ui/partials/index/panel-config-openclaw.html +78 -0
- package/web-ui/partials/index/panel-docs.html +130 -0
- package/web-ui/partials/index/panel-market.html +174 -0
- package/web-ui/partials/index/panel-orchestration.html +397 -0
- package/web-ui/partials/index/panel-sessions.html +292 -0
- package/web-ui/partials/index/panel-settings.html +190 -0
- package/web-ui/partials/index/panel-usage.html +213 -0
- package/web-ui/session-helpers.mjs +559 -362
- package/web-ui/source-bundle.cjs +233 -0
- package/web-ui/styles/base-theme.css +271 -0
- package/web-ui/styles/controls-forms.css +360 -0
- package/web-ui/styles/docs-panel.css +182 -0
- package/web-ui/styles/feedback.css +108 -0
- package/web-ui/styles/health-check-dialog.css +144 -0
- package/web-ui/styles/layout-shell.css +376 -0
- package/web-ui/styles/modals-core.css +464 -0
- package/web-ui/styles/navigation-panels.css +348 -0
- package/web-ui/styles/openclaw-structured.css +266 -0
- package/web-ui/styles/responsive.css +450 -0
- package/web-ui/styles/sessions-list.css +400 -0
- package/web-ui/styles/sessions-preview.css +411 -0
- package/web-ui/styles/sessions-toolbar-trash.css +243 -0
- package/web-ui/styles/sessions-usage.css +628 -0
- package/web-ui/styles/skills-list.css +296 -0
- package/web-ui/styles/skills-market.css +335 -0
- package/web-ui/styles/task-orchestration.css +776 -0
- package/web-ui/styles/titles-cards.css +408 -0
- package/web-ui/styles.css +18 -4668
- package/web-ui.html +17 -17
- package/res/screenshot.png +0 -0
- 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
|
+
};
|