codexmate 0.0.23 → 0.0.25
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.md +32 -9
- package/README.zh.md +33 -9
- package/cli/auth-profiles.js +23 -7
- package/cli/builtin-proxy.js +35 -0
- package/cli/claude-proxy.js +24 -0
- package/cli/doctor-core.js +903 -0
- package/cli/import-skills-url.js +356 -0
- package/cli/openai-bridge.js +51 -4
- package/cli/session-usage.js +8 -2
- package/cli.js +1921 -399
- package/lib/automation.js +404 -0
- package/lib/cli-models-utils.js +0 -40
- package/lib/cli-network-utils.js +28 -2
- package/lib/cli-path-utils.js +21 -5
- package/lib/cli-sessions.js +32 -1
- package/lib/download-artifacts.js +17 -2
- package/lib/mcp-stdio.js +13 -0
- package/package.json +3 -3
- package/plugins/README.md +20 -0
- package/plugins/README.zh-CN.md +20 -0
- package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
- package/plugins/prompt-templates/computed.mjs +253 -0
- package/plugins/prompt-templates/index.mjs +8 -0
- package/plugins/prompt-templates/manifest.mjs +15 -0
- package/plugins/prompt-templates/methods.mjs +619 -0
- package/plugins/prompt-templates/overview.mjs +90 -0
- package/plugins/prompt-templates/ownership.mjs +19 -0
- package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
- package/plugins/prompt-templates/storage.mjs +64 -0
- package/plugins/registry.mjs +16 -0
- package/web-ui/app.js +21 -35
- package/web-ui/index.html +4 -3
- package/web-ui/logic.sessions.mjs +2 -2
- package/web-ui/modules/app.computed.dashboard.mjs +24 -22
- package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
- package/web-ui/modules/app.computed.session.mjs +17 -0
- package/web-ui/modules/app.methods.agents.mjs +91 -3
- package/web-ui/modules/app.methods.codex-config.mjs +153 -164
- package/web-ui/modules/app.methods.install.mjs +28 -0
- package/web-ui/modules/app.methods.navigation.mjs +34 -1
- package/web-ui/modules/app.methods.runtime.mjs +24 -2
- package/web-ui/modules/app.methods.session-actions.mjs +8 -1
- package/web-ui/modules/app.methods.session-browser.mjs +37 -6
- package/web-ui/modules/app.methods.session-trash.mjs +4 -2
- package/web-ui/modules/config-mode.computed.mjs +1 -3
- package/web-ui/modules/i18n.dict.mjs +2055 -0
- package/web-ui/modules/i18n.mjs +2 -1769
- package/web-ui/partials/index/layout-header.html +48 -34
- package/web-ui/partials/index/modal-config-template-agents.html +3 -4
- package/web-ui/partials/index/modal-health-check.html +33 -60
- package/web-ui/partials/index/panel-config-claude.html +35 -15
- package/web-ui/partials/index/panel-config-codex.html +47 -19
- package/web-ui/partials/index/panel-config-openclaw.html +8 -3
- package/web-ui/partials/index/panel-dashboard.html +186 -0
- package/web-ui/partials/index/panel-docs.html +1 -1
- package/web-ui/partials/index/panel-market.html +3 -0
- package/web-ui/partials/index/panel-orchestration.html +3 -0
- package/web-ui/partials/index/panel-plugins.html +16 -10
- package/web-ui/partials/index/panel-sessions.html +8 -3
- package/web-ui/partials/index/panel-settings.html +1 -1
- package/web-ui/partials/index/panel-usage.html +9 -1
- package/web-ui/res/logo-pack.webp +0 -0
- package/web-ui/styles/controls-forms.css +58 -4
- package/web-ui/styles/dashboard.css +274 -0
- package/web-ui/styles/layout-shell.css +3 -2
- package/web-ui/styles/responsive.css +0 -2
- package/web-ui/styles/sessions-list.css +5 -7
- package/web-ui/styles/sessions-toolbar-trash.css +4 -4
- package/web-ui/styles/sessions-usage.css +33 -0
- package/web-ui/styles.css +1 -0
- package/res/logo.png +0 -0
- /package/{res → web-ui/res}/json5.min.js +0 -0
- /package/{res → web-ui/res}/vue.global.prod.js +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { isValidHttpUrl } = require('../lib/cli-utils');
|
|
7
|
+
const { MAX_SKILLS_ZIP_UPLOAD_SIZE, importSkillsFromZipFile } = require('./skills');
|
|
8
|
+
|
|
9
|
+
function decodeUrlPathPart(part) {
|
|
10
|
+
try {
|
|
11
|
+
return decodeURIComponent(part);
|
|
12
|
+
} catch (_) {
|
|
13
|
+
return part;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseGithubRepoFromUrl(inputUrl) {
|
|
18
|
+
const raw = typeof inputUrl === 'string' ? inputUrl.trim() : '';
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = new URL(raw);
|
|
23
|
+
} catch (_) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
if (parsed.hostname !== 'github.com') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const parts = parsed.pathname.split('/').filter(Boolean).map(decodeUrlPathPart);
|
|
33
|
+
if (parts.length < 2) return null;
|
|
34
|
+
const owner = parts[0];
|
|
35
|
+
const repoPart = parts[1] || '';
|
|
36
|
+
const repo = repoPart.endsWith('.git') ? repoPart.slice(0, -4) : repoPart;
|
|
37
|
+
if (!owner || !repo) return null;
|
|
38
|
+
const ref = parts[2] === 'tree' && parts[3]
|
|
39
|
+
? parts.slice(3).join('/')
|
|
40
|
+
: '';
|
|
41
|
+
return { owner, repo, ref };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildGithubArchiveZipBase(repoInfo) {
|
|
45
|
+
if (!repoInfo || !repoInfo.owner || !repoInfo.repo) return '';
|
|
46
|
+
return `https://github.com/${encodeURIComponent(repoInfo.owner)}/${encodeURIComponent(repoInfo.repo)}/archive/refs`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function encodeGithubRefPath(ref) {
|
|
50
|
+
return String(ref || '')
|
|
51
|
+
.split('/')
|
|
52
|
+
.map(part => encodeURIComponent(part))
|
|
53
|
+
.join('/');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveGithubArchiveZipUrl(inputUrl) {
|
|
57
|
+
const repoInfo = parseGithubRepoFromUrl(inputUrl);
|
|
58
|
+
if (!repoInfo) return '';
|
|
59
|
+
const base = buildGithubArchiveZipBase(repoInfo);
|
|
60
|
+
const ref = repoInfo.ref || 'main';
|
|
61
|
+
return `${base}/heads/${encodeGithubRefPath(ref)}.zip`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildGithubArchiveZipCandidates(inputUrl) {
|
|
65
|
+
const repoInfo = parseGithubRepoFromUrl(inputUrl);
|
|
66
|
+
if (!repoInfo) return [];
|
|
67
|
+
const base = buildGithubArchiveZipBase(repoInfo);
|
|
68
|
+
if (repoInfo.ref) {
|
|
69
|
+
const ref = encodeGithubRefPath(repoInfo.ref);
|
|
70
|
+
return [
|
|
71
|
+
`${base}/heads/${ref}.zip`,
|
|
72
|
+
`${base}/tags/${ref}.zip`
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
return [
|
|
76
|
+
`${base}/heads/main.zip`,
|
|
77
|
+
`${base}/heads/master.zip`
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function redactUrlForLog(inputUrl) {
|
|
82
|
+
const raw = typeof inputUrl === 'string' ? inputUrl.trim() : '';
|
|
83
|
+
if (!raw) return '';
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(raw);
|
|
86
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
87
|
+
} catch (_) {
|
|
88
|
+
return raw;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractHttpStatusFromError(err) {
|
|
93
|
+
const message = err && err.message ? String(err.message) : '';
|
|
94
|
+
const matched = message.match(/\bHTTP\s+(\d{3})\b/);
|
|
95
|
+
if (!matched) return 0;
|
|
96
|
+
const value = Number(matched[1]);
|
|
97
|
+
return Number.isFinite(value) ? value : 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isAllowedSkillsRedirectHost(originHost, nextHost) {
|
|
101
|
+
const origin = typeof originHost === 'string' ? originHost.trim().toLowerCase() : '';
|
|
102
|
+
const next = typeof nextHost === 'string' ? nextHost.trim().toLowerCase() : '';
|
|
103
|
+
if (!origin || !next) return false;
|
|
104
|
+
if (origin === next) return true;
|
|
105
|
+
if (process.env.CODEXMATE_ALLOW_SKILLS_REDIRECT === '1') return true;
|
|
106
|
+
if (origin === 'github.com' && next === 'codeload.github.com') return true;
|
|
107
|
+
if (origin === 'github.com' && next.endsWith('.githubusercontent.com')) return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function downloadUrlToFile(targetUrl, filePath, options = {}) {
|
|
112
|
+
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
|
|
113
|
+
? Math.floor(options.maxBytes)
|
|
114
|
+
: MAX_SKILLS_ZIP_UPLOAD_SIZE;
|
|
115
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
|
|
116
|
+
? Math.floor(options.timeoutMs)
|
|
117
|
+
: 30000;
|
|
118
|
+
const maxRedirects = Number.isFinite(options.maxRedirects) && options.maxRedirects >= 0
|
|
119
|
+
? Math.floor(options.maxRedirects)
|
|
120
|
+
: 5;
|
|
121
|
+
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = new URL(targetUrl);
|
|
126
|
+
} catch (_) {
|
|
127
|
+
reject(new Error('Invalid URL'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
131
|
+
reject(new Error(`ERR_INVALID_PROTOCOL: Protocol "${parsed.protocol}" not supported. Expected "http:" or "https:"`));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
136
|
+
const requestOptions = {
|
|
137
|
+
method: 'GET',
|
|
138
|
+
headers: {
|
|
139
|
+
'User-Agent': 'codexmate-import-skills',
|
|
140
|
+
'Accept': 'application/octet-stream,application/zip,*/*'
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const req = transport.request(parsed, requestOptions, (res) => {
|
|
145
|
+
const status = Number(res.statusCode) || 0;
|
|
146
|
+
const redirectLocation = res.headers && typeof res.headers.location === 'string' ? res.headers.location : '';
|
|
147
|
+
if (status >= 300 && status < 400 && redirectLocation) {
|
|
148
|
+
if (maxRedirects <= 0) {
|
|
149
|
+
reject(new Error('Too many redirects'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const nextUrl = redirectLocation.startsWith('http')
|
|
153
|
+
? redirectLocation
|
|
154
|
+
: `${parsed.origin}${redirectLocation}`;
|
|
155
|
+
let originHost = typeof options.originHost === 'string' && options.originHost.trim()
|
|
156
|
+
? options.originHost.trim()
|
|
157
|
+
: parsed.host;
|
|
158
|
+
try {
|
|
159
|
+
const nextParsed = new URL(nextUrl);
|
|
160
|
+
if (!isAllowedSkillsRedirectHost(originHost, nextParsed.host)) {
|
|
161
|
+
res.resume();
|
|
162
|
+
reject(new Error('Cross-origin redirect is not allowed'));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} catch (_) {}
|
|
166
|
+
res.resume();
|
|
167
|
+
downloadUrlToFile(nextUrl, filePath, { maxBytes, timeoutMs, maxRedirects: maxRedirects - 1, originHost })
|
|
168
|
+
.then(resolve)
|
|
169
|
+
.catch(reject);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (status < 200 || status >= 300) {
|
|
173
|
+
res.resume();
|
|
174
|
+
reject(new Error(`HTTP ${status}`));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const out = fs.createWriteStream(filePath, { flags: 'w' });
|
|
179
|
+
let bytes = 0;
|
|
180
|
+
let finished = false;
|
|
181
|
+
|
|
182
|
+
const fail = (err) => {
|
|
183
|
+
if (finished) return;
|
|
184
|
+
finished = true;
|
|
185
|
+
try {
|
|
186
|
+
out.close();
|
|
187
|
+
} catch (_) {}
|
|
188
|
+
try {
|
|
189
|
+
fs.unlinkSync(filePath);
|
|
190
|
+
} catch (_) {}
|
|
191
|
+
reject(err);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
res.on('data', (chunk) => {
|
|
195
|
+
if (!chunk || finished) return;
|
|
196
|
+
bytes += chunk.length;
|
|
197
|
+
if (bytes > maxBytes) {
|
|
198
|
+
req.destroy(new Error('download too large'));
|
|
199
|
+
res.destroy(new Error('download too large'));
|
|
200
|
+
fail(new Error('Download too large'));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
res.on('error', fail);
|
|
205
|
+
out.on('error', fail);
|
|
206
|
+
out.on('finish', () => {
|
|
207
|
+
if (finished) return;
|
|
208
|
+
finished = true;
|
|
209
|
+
resolve({ bytes });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
res.pipe(out);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
req.on('error', (err) => {
|
|
216
|
+
reject(new Error(err && err.message ? err.message : 'request failed'));
|
|
217
|
+
});
|
|
218
|
+
req.setTimeout(timeoutMs, () => {
|
|
219
|
+
req.destroy(new Error('timeout'));
|
|
220
|
+
});
|
|
221
|
+
req.end();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function printImportSkillsUsage() {
|
|
226
|
+
process.stdout.write('\n用法:\n');
|
|
227
|
+
process.stdout.write(' codexmate import-skills <URL> [--target-app codex|claude] [--name <NAME>] [--timeout-ms <MS>]\n');
|
|
228
|
+
process.stdout.write('\n示例:\n');
|
|
229
|
+
process.stdout.write(' codexmate import-skills https://github.com/<owner>/<repo>\n');
|
|
230
|
+
process.stdout.write(' codexmate import-skills https://github.com/<owner>/<repo>/tree/dev\n');
|
|
231
|
+
process.stdout.write(' codexmate import-skills https://github.com/<owner>/<repo>/archive/refs/heads/main.zip\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseImportSkillsCommandArgs(argv = []) {
|
|
235
|
+
const options = {
|
|
236
|
+
url: '',
|
|
237
|
+
targetApp: 'codex',
|
|
238
|
+
name: '',
|
|
239
|
+
timeoutMs: 30000,
|
|
240
|
+
help: false
|
|
241
|
+
};
|
|
242
|
+
let cursor = 0;
|
|
243
|
+
while (cursor < argv.length) {
|
|
244
|
+
const token = String(argv[cursor] || '');
|
|
245
|
+
if (token === '--help' || token === '-h') {
|
|
246
|
+
options.help = true;
|
|
247
|
+
cursor += 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (token && !token.startsWith('-') && !options.url) {
|
|
251
|
+
options.url = token.trim();
|
|
252
|
+
cursor += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (token === '--target-app') {
|
|
256
|
+
const value = String(argv[cursor + 1] || '').trim().toLowerCase();
|
|
257
|
+
if (!value || value.startsWith('-')) {
|
|
258
|
+
throw new Error('错误: --target-app 需要一个值(codex/claude)');
|
|
259
|
+
}
|
|
260
|
+
options.targetApp = value === 'claude' ? 'claude' : 'codex';
|
|
261
|
+
cursor += 2;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (token === '--name') {
|
|
265
|
+
const value = String(argv[cursor + 1] || '').trim();
|
|
266
|
+
if (!value || value.startsWith('-')) {
|
|
267
|
+
throw new Error('错误: --name 需要一个值');
|
|
268
|
+
}
|
|
269
|
+
options.name = value;
|
|
270
|
+
cursor += 2;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (token === '--timeout-ms') {
|
|
274
|
+
const value = Number(argv[cursor + 1]);
|
|
275
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
276
|
+
throw new Error('错误: --timeout-ms 需要一个正整数');
|
|
277
|
+
}
|
|
278
|
+
options.timeoutMs = Math.floor(value);
|
|
279
|
+
cursor += 2;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (token.startsWith('-')) {
|
|
283
|
+
throw new Error(`错误: 未知参数: ${token}`);
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`错误: 多余参数: ${token}`);
|
|
286
|
+
}
|
|
287
|
+
return options;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function cmdImportSkills(argv = []) {
|
|
291
|
+
const options = parseImportSkillsCommandArgs(argv);
|
|
292
|
+
if (options.help) {
|
|
293
|
+
printImportSkillsUsage();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!options.url) {
|
|
297
|
+
printImportSkillsUsage();
|
|
298
|
+
throw new Error('错误: 缺少 URL(例如: https://github.com/<owner>/<repo>/archive/refs/heads/main.zip)');
|
|
299
|
+
}
|
|
300
|
+
const candidates = buildGithubArchiveZipCandidates(options.url);
|
|
301
|
+
if (!candidates.length) {
|
|
302
|
+
const resolvedGithubUrl = resolveGithubArchiveZipUrl(options.url);
|
|
303
|
+
candidates.push(resolvedGithubUrl || options.url);
|
|
304
|
+
}
|
|
305
|
+
const uniqueCandidates = Array.from(new Set(candidates.filter(Boolean)));
|
|
306
|
+
if (!uniqueCandidates.length || !uniqueCandidates.every(isValidHttpUrl)) {
|
|
307
|
+
throw new Error('错误: URL 非法(仅支持 http/https)');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-skills-url-'));
|
|
311
|
+
const zipPath = path.join(tempDir, 'skills.zip');
|
|
312
|
+
let finalUrl = uniqueCandidates[0];
|
|
313
|
+
try {
|
|
314
|
+
let lastError = null;
|
|
315
|
+
for (const candidateUrl of uniqueCandidates) {
|
|
316
|
+
finalUrl = candidateUrl;
|
|
317
|
+
console.log(`\n[Skills] Download: ${redactUrlForLog(candidateUrl)}`);
|
|
318
|
+
try {
|
|
319
|
+
await downloadUrlToFile(candidateUrl, zipPath, {
|
|
320
|
+
maxBytes: MAX_SKILLS_ZIP_UPLOAD_SIZE,
|
|
321
|
+
timeoutMs: options.timeoutMs,
|
|
322
|
+
maxRedirects: 5
|
|
323
|
+
});
|
|
324
|
+
lastError = null;
|
|
325
|
+
break;
|
|
326
|
+
} catch (e) {
|
|
327
|
+
lastError = e;
|
|
328
|
+
if (extractHttpStatusFromError(e) === 404) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw e;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (lastError) {
|
|
335
|
+
throw lastError;
|
|
336
|
+
}
|
|
337
|
+
const fallbackName = options.name || path.basename(new URL(finalUrl).pathname) || 'skills.zip';
|
|
338
|
+
const result = await importSkillsFromZipFile(zipPath, {
|
|
339
|
+
tempDir,
|
|
340
|
+
targetApp: options.targetApp,
|
|
341
|
+
fallbackName
|
|
342
|
+
});
|
|
343
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
344
|
+
} finally {
|
|
345
|
+
try {
|
|
346
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
347
|
+
} catch (_) {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
parseGithubRepoFromUrl,
|
|
353
|
+
resolveGithubArchiveZipUrl,
|
|
354
|
+
buildGithubArchiveZipCandidates,
|
|
355
|
+
cmdImportSkills
|
|
356
|
+
};
|
package/cli/openai-bridge.js
CHANGED
|
@@ -238,6 +238,10 @@ function normalizeResponsesInputToChatMessages(input) {
|
|
|
238
238
|
out.push({ type: 'text', text: block.text });
|
|
239
239
|
continue;
|
|
240
240
|
}
|
|
241
|
+
if ((type === 'reasoning' || type === 'reasoning_text' || type === 'reasoning_content') && typeof block.text === 'string') {
|
|
242
|
+
out.push({ type: 'text', text: block.text });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
241
245
|
if (type === 'input_image') {
|
|
242
246
|
const raw = block.image_url != null ? block.image_url : block.imageUrl;
|
|
243
247
|
const url = typeof raw === 'string'
|
|
@@ -255,7 +259,21 @@ function normalizeResponsesInputToChatMessages(input) {
|
|
|
255
259
|
}
|
|
256
260
|
if (type === 'image_url' && block.image_url) {
|
|
257
261
|
out.push({ type: 'image_url', image_url: block.image_url });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const text = typeof block.text === 'string'
|
|
265
|
+
? block.text
|
|
266
|
+
: (typeof block.content === 'string' ? block.content : '');
|
|
267
|
+
if (text) {
|
|
268
|
+
out.push({ type: 'text', text });
|
|
269
|
+
continue;
|
|
258
270
|
}
|
|
271
|
+
try {
|
|
272
|
+
const raw = JSON.stringify(block);
|
|
273
|
+
if (raw) {
|
|
274
|
+
out.push({ type: 'text', text: raw.slice(0, 4000) });
|
|
275
|
+
}
|
|
276
|
+
} catch (_) {}
|
|
259
277
|
}
|
|
260
278
|
if (out.length === 0) return '';
|
|
261
279
|
return out;
|
|
@@ -635,6 +653,9 @@ async function proxyRequestJson(targetUrl, options = {}) {
|
|
|
635
653
|
const parsed = new URL(targetUrl);
|
|
636
654
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
637
655
|
const bodyText = options.body ? JSON.stringify(options.body) : '';
|
|
656
|
+
const maxBytes = Number.isFinite(options.maxBytes) && options.maxBytes > 0
|
|
657
|
+
? Math.floor(options.maxBytes)
|
|
658
|
+
: 0;
|
|
638
659
|
const headers = {
|
|
639
660
|
'Accept': 'application/json',
|
|
640
661
|
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
@@ -664,7 +685,21 @@ async function proxyRequestJson(targetUrl, options = {}) {
|
|
|
664
685
|
agent: parsed.protocol === 'https:' ? options.httpsAgent : options.httpAgent
|
|
665
686
|
}, (upstreamRes) => {
|
|
666
687
|
const chunks = [];
|
|
667
|
-
|
|
688
|
+
let size = 0;
|
|
689
|
+
upstreamRes.on('data', (chunk) => {
|
|
690
|
+
if (!chunk) return;
|
|
691
|
+
if (maxBytes > 0) {
|
|
692
|
+
size += chunk.length;
|
|
693
|
+
if (size > maxBytes) {
|
|
694
|
+
chunks.length = 0;
|
|
695
|
+
try { upstreamRes.destroy(new Error('response too large')); } catch (_) {}
|
|
696
|
+
try { req.destroy(new Error('response too large')); } catch (_) {}
|
|
697
|
+
finish({ ok: false, error: 'response too large' });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
chunks.push(chunk);
|
|
702
|
+
});
|
|
668
703
|
upstreamRes.on('end', () => {
|
|
669
704
|
const text = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
670
705
|
finish({
|
|
@@ -689,12 +724,16 @@ async function proxyRequestJson(targetUrl, options = {}) {
|
|
|
689
724
|
|
|
690
725
|
function createOpenaiBridgeHttpHandler(options = {}) {
|
|
691
726
|
const settingsFile = options.settingsFile;
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
727
|
+
const expectedTokenRaw = typeof options.expectedToken === 'string' ? options.expectedToken.trim() : '';
|
|
728
|
+
const expectedToken = Object.prototype.hasOwnProperty.call(options, 'expectedToken')
|
|
729
|
+
? expectedTokenRaw
|
|
730
|
+
: (expectedTokenRaw || DEFAULT_BRIDGE_TOKEN);
|
|
695
731
|
const maxBodySize = Number.isFinite(options.maxBodySize) ? options.maxBodySize : 0;
|
|
696
732
|
const httpAgent = options.httpAgent;
|
|
697
733
|
const httpsAgent = options.httpsAgent;
|
|
734
|
+
const maxUpstreamBytes = Number.isFinite(options.maxUpstreamBytes) && options.maxUpstreamBytes > 0
|
|
735
|
+
? Math.floor(options.maxUpstreamBytes)
|
|
736
|
+
: Math.max(16 * 1024 * 1024, maxBodySize > 0 ? maxBodySize * 4 : 0);
|
|
698
737
|
|
|
699
738
|
if (!settingsFile) {
|
|
700
739
|
throw new Error('createOpenaiBridgeHttpHandler 缺少 settingsFile');
|
|
@@ -730,6 +769,11 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
730
769
|
// 为避免在 LAN 暴露无鉴权的代理,这里仅允许 loopback 连接缺省 token。
|
|
731
770
|
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
732
771
|
const isLoopback = isLoopbackAddress(remoteAddr);
|
|
772
|
+
if (!isLoopback && !expectedToken) {
|
|
773
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
774
|
+
res.end(JSON.stringify({ error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN)' }));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
733
777
|
if (!token && !isLoopback) {
|
|
734
778
|
res.writeHead(401, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
735
779
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
@@ -774,6 +818,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
774
818
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
775
819
|
...upstreamHeaders
|
|
776
820
|
},
|
|
821
|
+
maxBytes: maxUpstreamBytes,
|
|
777
822
|
httpAgent,
|
|
778
823
|
httpsAgent
|
|
779
824
|
});
|
|
@@ -827,6 +872,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
827
872
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
828
873
|
...upstreamHeaders
|
|
829
874
|
},
|
|
875
|
+
maxBytes: maxUpstreamBytes,
|
|
830
876
|
httpAgent,
|
|
831
877
|
httpsAgent
|
|
832
878
|
});
|
|
@@ -887,6 +933,7 @@ function createOpenaiBridgeHttpHandler(options = {}) {
|
|
|
887
933
|
...(authHeader ? { Authorization: authHeader } : {}),
|
|
888
934
|
...upstreamHeaders
|
|
889
935
|
},
|
|
936
|
+
maxBytes: maxUpstreamBytes,
|
|
890
937
|
httpAgent,
|
|
891
938
|
httpsAgent
|
|
892
939
|
});
|
package/cli/session-usage.js
CHANGED
|
@@ -7,11 +7,13 @@ async function listSessionUsageCore(params = {}, deps = {}) {
|
|
|
7
7
|
listSessionBrowse,
|
|
8
8
|
parseCodexSessionSummary,
|
|
9
9
|
parseClaudeSessionSummary,
|
|
10
|
+
parseCodeBuddySessionSummary,
|
|
11
|
+
parseGeminiSessionSummary,
|
|
10
12
|
MAX_SESSION_USAGE_LIST_SIZE,
|
|
11
13
|
SESSION_BROWSE_SUMMARY_READ_BYTES
|
|
12
14
|
} = deps;
|
|
13
15
|
|
|
14
|
-
const source = params.source === 'codex' || params.source === 'claude'
|
|
16
|
+
const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
|
|
15
17
|
? params.source
|
|
16
18
|
: 'all';
|
|
17
19
|
const rawLimit = Number(params.limit);
|
|
@@ -81,7 +83,11 @@ async function listSessionUsageCore(params = {}, deps = {}) {
|
|
|
81
83
|
try {
|
|
82
84
|
summary = normalized.source === 'claude'
|
|
83
85
|
? parseClaudeSessionSummary(filePath, summaryOptions)
|
|
84
|
-
:
|
|
86
|
+
: (normalized.source === 'gemini'
|
|
87
|
+
? parseGeminiSessionSummary(filePath, summaryOptions)
|
|
88
|
+
: (normalized.source === 'codebuddy'
|
|
89
|
+
? parseCodeBuddySessionSummary(filePath, summaryOptions)
|
|
90
|
+
: parseCodexSessionSummary(filePath, summaryOptions)));
|
|
85
91
|
} catch (_) {
|
|
86
92
|
summary = null;
|
|
87
93
|
}
|