codefree-proxy 0.1.3 → 0.1.4

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/dist/cli.js CHANGED
@@ -5,10 +5,14 @@ import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { promisify } from 'node:util';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { connect } from 'node:net';
8
9
  import { buildServer } from './server.js';
9
- import { AuthManager } from './auth/auth-manager.js';
10
+ import { AuthManager, nativeCredentialPaths } from './auth/auth-manager.js';
11
+ import { inspectCodeFreeOStore } from './auth/codefree-o.js';
10
12
  import { SUPPORTED_MODELS } from './proxy/model-map.js';
13
+
11
14
  const execFileAsync = promisify(execFile);
15
+
12
16
  export function daemonPaths(homeDir = homedir()) {
13
17
  const dir = join(homeDir, '.codefree-proxy');
14
18
  const logDir = join(dir, 'logs');
@@ -16,9 +20,11 @@ export function daemonPaths(homeDir = homedir()) {
16
20
  dir,
17
21
  logDir,
18
22
  logFile: join(logDir, 'proxy.log'),
19
- pidFile: join(dir, 'proxy.pid')
23
+ pidFile: join(dir, 'proxy.pid'),
24
+ configFile: join(dir, 'config.json')
20
25
  };
21
26
  }
27
+
22
28
  export async function startDaemon(options, deps = {}) {
23
29
  const paths = daemonPaths(deps.homeDir);
24
30
  const entryPath = deps.entryPath ?? process.argv[1];
@@ -44,6 +50,7 @@ export async function startDaemon(options, deps = {}) {
44
50
  closeLog(logFd);
45
51
  }
46
52
  }
53
+
47
54
  export function formatStartupInfo(options) {
48
55
  const origin = `http://${options.host}:${options.port}`;
49
56
  return [
@@ -52,14 +59,13 @@ export function formatStartupInfo(options) {
52
59
  `Chat endpoint: POST ${origin}/v1/chat/completions`,
53
60
  `Models endpoint: GET ${origin}/v1/models`,
54
61
  `Health check: GET ${origin}/health`,
55
- `Auth status: GET ${origin}/auth/status`,
56
- `Auth refresh: POST ${origin}/auth/refresh`,
57
62
  `Stop command: codefree-proxy stop --port ${options.port}`,
58
63
  'API Key: any value or omit',
59
64
  `Authorized user: ${options.userId}`,
60
65
  `Models: ${options.models.join(', ')}`
61
66
  ].join('\n');
62
67
  }
68
+
63
69
  export function formatDaemonStartedInfo(options, result) {
64
70
  return [
65
71
  `Started codefree-proxy in background: ${result.pid}`,
@@ -68,12 +74,34 @@ export function formatDaemonStartedInfo(options, result) {
68
74
  `PID file: ${result.pidFile}`
69
75
  ].join('\n');
70
76
  }
77
+
78
+ const USAGE = `codefree-proxy — 本地 CodeFree 反向代理(OpenAI 兼容)
79
+
80
+ 用法:
81
+ codefree-proxy doctor 自检环境与登录态(推荐先跑这个)
82
+ codefree-proxy serve 前台启动
83
+ codefree-proxy start 后台启动(守护进程)
84
+ codefree-proxy login 从 CodeFree-O 导入登录态
85
+ codefree-proxy status 查看登录态(只读,不触发登录)
86
+ codefree-proxy logout 删除本代理保存的登录态
87
+ codefree-proxy stop 停止指定端口的代理
88
+
89
+ 选项:
90
+ --host <地址> 默认 127.0.0.1
91
+ --port <端口> 默认 8788
92
+ --timeout <秒> 上游超时,默认 120
93
+ --no-mirror 不回写登录态到 CodeFree-O(避免与原生 CLI 互相顶号)
94
+ -h, --help 显示本帮助
95
+
96
+ 前置条件: Node >= 18,且本机已运行过 codefree-o login。
97
+ `;
98
+
71
99
  export function parseArgs(argv) {
72
- const options = { command: 'serve', host: '127.0.0.1', port: 8788, timeoutMs: 120_000 };
100
+ const commands = ['serve', 'start', 'login', 'logout', 'status', 'stop', 'doctor', 'help'];
101
+ const options = { command: 'serve', host: '127.0.0.1', port: 8788, timeoutMs: 120_000, mirror: true };
73
102
  const args = [...argv];
74
- if (args[0] === 'serve' || args[0] === 'start' || args[0] === 'login' || args[0] === 'logout' || args[0] === 'status' || args[0] === 'stop') {
103
+ if (args[0] && commands.includes(args[0]))
75
104
  options.command = args.shift();
76
- }
77
105
  while (args.length > 0) {
78
106
  const arg = args.shift();
79
107
  if (arg === '--host')
@@ -82,18 +110,29 @@ export function parseArgs(argv) {
82
110
  options.port = Number(args.shift() ?? options.port);
83
111
  else if (arg === '--timeout')
84
112
  options.timeoutMs = Number(args.shift() ?? options.timeoutMs / 1000) * 1000;
113
+ else if (arg === '--no-mirror')
114
+ options.mirror = false;
115
+ else if (arg === '-h' || arg === '--help')
116
+ options.command = 'help';
85
117
  else
86
- throw new Error(`unknown argument: ${arg}`);
118
+ throw new Error(`unknown argument: ${arg}(用 --help 查看用法)`);
87
119
  }
120
+ if (!Number.isFinite(options.port) || options.port <= 0 || options.port > 65535)
121
+ throw new Error('端口无效,请用 --port 指定 1-65535 之间的数字');
122
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 1000)
123
+ throw new Error('超时无效,请用 --timeout 指定秒数(>= 1)');
88
124
  return options;
89
125
  }
126
+
90
127
  async function runCommand(command, args) {
91
128
  const { stdout } = await execFileAsync(command, args);
92
129
  return stdout;
93
130
  }
131
+
94
132
  function uniquePids(values) {
95
133
  return [...new Set(values.filter((pid) => Number.isInteger(pid) && pid > 0))];
96
134
  }
135
+
97
136
  export async function findListeningPids(port, platform = process.platform, run = runCommand) {
98
137
  if (platform === 'win32') {
99
138
  const output = await run('netstat', ['-ano', '-p', 'tcp']);
@@ -104,13 +143,44 @@ export async function findListeningPids(port, platform = process.platform, run =
104
143
  .map((parts) => Number(parts[4]));
105
144
  return uniquePids(pids);
106
145
  }
107
- const output = await run('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN']);
108
- return uniquePids(output.split('\n').map((line) => Number(line.trim())));
146
+ try {
147
+ const output = await run('lsof', [`-tiTCP:${port}`, '-sTCP:LISTEN']);
148
+ return uniquePids(output.split('\n').map((line) => Number(line.trim())));
149
+ }
150
+ catch (error) {
151
+ // lsof exits 1 when nothing matches; ENOENT means lsof is not installed.
152
+ if (String(error?.code) === '1')
153
+ return [];
154
+ throw error;
155
+ }
109
156
  }
157
+
158
+ async function commandLineOf(pid) {
159
+ try {
160
+ const { stdout } = await execFileAsync('ps', ['-o', 'command=', '-p', String(pid)]);
161
+ return stdout.trim();
162
+ }
163
+ catch {
164
+ return '';
165
+ }
166
+ }
167
+
168
+ function looksLikeCodefreeProxy(commandLine) {
169
+ return /codefree[-\w]*proxy/i.test(commandLine) || (/cli\.js/.test(commandLine) && / serve( |$)/.test(commandLine));
170
+ }
171
+
110
172
  export async function stopByPort(port, platform = process.platform, run = runCommand) {
111
173
  const pids = await findListeningPids(port, platform, run);
112
174
  if (pids.length === 0)
113
- return { port, pids, stopped: false };
175
+ return { port, pids, stopped: false, reason: 'no-listener' };
176
+ const foreign = [];
177
+ for (const pid of pids) {
178
+ const cmd = await commandLineOf(pid);
179
+ if (!looksLikeCodefreeProxy(cmd))
180
+ foreign.push({ pid, cmd });
181
+ }
182
+ if (foreign.length > 0)
183
+ return { port, pids, stopped: false, reason: 'not-codefree-proxy', foreign };
114
184
  if (platform === 'win32') {
115
185
  for (const pid of pids)
116
186
  await run('taskkill', ['/PID', String(pid), '/F']);
@@ -120,43 +190,230 @@ export async function stopByPort(port, platform = process.platform, run = runCom
120
190
  }
121
191
  return { port, pids, stopped: true };
122
192
  }
123
- async function main() {
124
- const options = parseArgs(process.argv.slice(2));
125
- if (options.command === 'stop') {
126
- const result = await stopByPort(options.port);
127
- if (result.stopped)
128
- console.log(`Stopped codefree-proxy on port ${result.port}: ${result.pids.join(', ')}`);
129
- else
130
- console.log(`No codefree-proxy process listening on port ${result.port}`);
131
- return;
193
+
194
+ async function portInUse(port, host = '127.0.0.1') {
195
+ return new Promise((resolve) => {
196
+ const socket = connect({ port, host });
197
+ const done = (free) => { socket.destroy(); resolve(free); };
198
+ socket.setTimeout(1200);
199
+ socket.once('connect', () => done(true));
200
+ socket.once('timeout', () => done(false));
201
+ socket.once('error', () => done(false));
202
+ });
203
+ }
204
+
205
+ async function waitForHealth(port, host = '127.0.0.1', timeoutMs = 6000) {
206
+ const deadline = Date.now() + timeoutMs;
207
+ while (Date.now() < deadline) {
208
+ try {
209
+ const response = await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(1000) });
210
+ if (response.ok)
211
+ return true;
212
+ }
213
+ catch {
214
+ // keep polling until the deadline
215
+ }
216
+ await new Promise((r) => setTimeout(r, 250));
132
217
  }
133
- if (options.command === 'start') {
134
- const auth = new AuthManager();
135
- const credentials = await auth.getCredentials();
136
- const result = await startDaemon(options);
137
- console.log(formatDaemonStartedInfo({ host: options.host, port: options.port, userId: credentials.userId, models: SUPPORTED_MODELS }, result));
138
- return;
218
+ return false;
219
+ }
220
+
221
+ async function tailLog(file, lines = 8) {
222
+ try {
223
+ const { stdout } = await execFileAsync('tail', ['-n', String(lines), file]);
224
+ return stdout.trim();
225
+ }
226
+ catch {
227
+ return '';
228
+ }
229
+ }
230
+
231
+ async function findNativeCli() {
232
+ try {
233
+ const { stdout } = await execFileAsync('which', ['codefree-o']);
234
+ const found = stdout.trim();
235
+ return found.length > 0 ? found : null;
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+
242
+ async function runNativeLogin() {
243
+ const bin = await findNativeCli();
244
+ if (!bin) {
245
+ return { ok: false, reason: '未找到 codefree-o 命令。请先安装 CodeFree-O 客户端,并在终端运行 codefree-o login。' };
246
+ }
247
+ console.log(`检测到 CodeFree-O 尚未登录,正在调用 ${bin} login …(浏览器会打开研发云登录页)\n`);
248
+ const code = await new Promise((resolve) => {
249
+ const child = spawn(bin, ['login'], { stdio: 'inherit' });
250
+ child.once('exit', (value) => resolve(value ?? 1));
251
+ child.once('error', () => resolve(1));
252
+ });
253
+ if (code !== 0)
254
+ return { ok: false, reason: `codefree-o login 未完成(退出码 ${code}),请在终端重试。` };
255
+ return { ok: true };
256
+ }
257
+
258
+ async function doctor() {
259
+ const rows = [];
260
+ const nodeMajor = Number(process.versions.node.split('.')[0]);
261
+ const nodeOk = nodeMajor >= 18;
262
+ rows.push({ ok: nodeOk, label: 'Node.js', detail: `v${process.versions.node}${nodeOk ? '' : '(需要 >= 18)'}` });
263
+
264
+ const paths = nativeCredentialPaths();
265
+ let nativeUsable = false;
266
+ let sawNativeFile = false;
267
+ for (const path of paths) {
268
+ const info = await inspectCodeFreeOStore(path);
269
+ if (info.ok) {
270
+ nativeUsable = true;
271
+ rows.push({ ok: true, label: 'CodeFree-O 登录态', detail: `${path}(用户 ${info.userId ?? '未知'}${info.expiresAtIso ? `,到期 ${info.expiresAtIso.slice(0, 10)}` : ''})` });
272
+ }
273
+ else if (info.exists && info.credentialFile) {
274
+ sawNativeFile = true;
275
+ rows.push({ ok: false, label: 'CodeFree-O 登录态', detail: `${path}:${info.reason}` });
276
+ }
139
277
  }
278
+ if (!nativeUsable && !sawNativeFile)
279
+ rows.push({ ok: false, label: 'CodeFree-O 登录态', detail: `未找到登录态文件(已检查 ${paths.join('、')})` });
280
+
140
281
  const auth = new AuthManager();
141
- if (options.command === 'login') {
142
- const credentials = await auth.login();
143
- console.log(`Authorized user: ${credentials.userId}`);
144
- return;
282
+ const status = await auth.status();
283
+ rows.push({
284
+ ok: Boolean(status.proxyConfig?.ok),
285
+ label: '代理缓存',
286
+ detail: status.proxyConfig?.ok
287
+ ? `~/.codefree-proxy/config.json(用户 ${status.proxyConfig.userId ?? '未知'})`
288
+ : '尚无缓存,首次启动会自动从 CodeFree-O 导入'
289
+ });
290
+
291
+ if (await portInUse(8788)) {
292
+ const pids = await findListeningPids(8788).catch(() => []);
293
+ const owners = [];
294
+ for (const pid of pids)
295
+ owners.push(`${pid}${looksLikeCodefreeProxy(await commandLineOf(pid)) ? '(codefree-proxy)' : '(其他程序)'}`);
296
+ rows.push({ ok: true, label: '端口 8788', detail: `已被占用 ${owners.join(' ')}` });
297
+ }
298
+ else {
299
+ rows.push({ ok: true, label: '端口 8788', detail: '空闲' });
145
300
  }
301
+
302
+ const started = Date.now();
303
+ const reachable = await new Promise((resolve) => {
304
+ const socket = connect({ port: 443, host: 'www.srdcloud.cn' });
305
+ socket.setTimeout(4000);
306
+ socket.once('connect', () => { socket.destroy(); resolve(true); });
307
+ socket.once('timeout', () => { socket.destroy(); resolve(false); });
308
+ socket.once('error', () => { socket.destroy(); resolve(false); });
309
+ });
310
+ rows.push({ ok: reachable, label: '上游连通性', detail: reachable ? `www.srdcloud.cn:443 可达(${Date.now() - started}ms)` : '无法连接 www.srdcloud.cn:443,请检查网络或代理设置' });
311
+
312
+ console.log('codefree-proxy 自检\n');
313
+ for (const row of rows)
314
+ console.log(`${row.ok ? '✓' : '✗'} ${row.label}:${row.detail}`);
315
+
316
+ const blockers = [];
317
+ if (!nodeOk)
318
+ blockers.push('Node.js 版本过低');
319
+ if (!nativeUsable && !status.proxyConfig?.ok)
320
+ blockers.push('没有可用的 CodeFree-O 登录态,请运行 codefree-proxy login');
321
+ if (status.proxyConfig?.ok && !nativeUsable)
322
+ console.log('\n提示: 原生 CodeFree-O 登录态文件缺失,代理会使用自己的缓存;如需同步请重新运行 codefree-o login。');
323
+ if (!reachable)
324
+ blockers.push('网络无法访问研发云');
325
+ const noCredential = !nativeUsable && !status.proxyConfig?.ok;
326
+ if (noCredential) {
327
+ const nativeCli = await findNativeCli();
328
+ console.log(nativeCli
329
+ ? '\n下一步:运行 codefree-proxy login,会自动打开研发云登录页,登录成功后导入登录态。'
330
+ : '\n下一步:本机没有 codefree-o 命令,请先安装 CodeFree-O 客户端并登录。');
331
+ }
332
+ console.log(`\n结论:${blockers.length === 0 ? '可以启动,运行 codefree-proxy start' : '暂不能启动 — ' + blockers.join(';')}`);
333
+ return blockers.length === 0 ? 0 : 1;
334
+ }
335
+
336
+ async function main() {
337
+ const options = parseArgs(process.argv.slice(2));
338
+ const auth = new AuthManager();
339
+ if (options.command === 'help') {
340
+ console.log(USAGE);
341
+ return 0;
342
+ }
343
+ if (options.command === 'doctor')
344
+ return doctor();
146
345
  if (options.command === 'logout') {
147
346
  await auth.logout();
148
- console.log('Credentials removed');
149
- return;
347
+ console.log('已删除 ~/.codefree-proxy/config.json 中的登录态');
348
+ return 0;
349
+ }
350
+ if (options.command === 'login') {
351
+ try {
352
+ const credentials = await auth.login();
353
+ console.log(`已导入 CodeFree-O 登录态,用户 ${credentials.userId}`);
354
+ return 0;
355
+ }
356
+ catch {
357
+ // 没有可导入的登录态:直接引导并调用原生 CLI 完成登录,再导入
358
+ }
359
+ const native = await runNativeLogin();
360
+ if (!native.ok) {
361
+ console.error(native.reason);
362
+ return 1;
363
+ }
364
+ const credentials = await auth.login();
365
+ console.log(`\n登录完成,已导入 CodeFree-O 登录态,用户 ${credentials.userId}`);
366
+ return 0;
150
367
  }
151
368
  if (options.command === 'status') {
152
- console.log(JSON.stringify(await auth.status(), null, 2));
153
- return;
369
+ const status = await auth.status();
370
+ console.log(JSON.stringify(status, null, 2));
371
+ return status.status === 'ok' ? 0 : 1;
154
372
  }
373
+ if (options.command === 'stop') {
374
+ const result = await stopByPort(options.port);
375
+ if (result.stopped) {
376
+ console.log(`已停止端口 ${result.port} 上的 codefree-proxy:${result.pids.join(', ')}`);
377
+ return 0;
378
+ }
379
+ if (result.reason === 'not-codefree-proxy') {
380
+ const owners = result.foreign.map((f) => `${f.pid}(${f.cmd || '未知进程'})`).join('、');
381
+ console.error(`端口 ${result.port} 被其他程序占用,未执行停止:${owners}`);
382
+ return 1;
383
+ }
384
+ console.log(`端口 ${result.port} 上没有正在运行的 codefree-proxy`);
385
+ return 0;
386
+ }
387
+ // serve / start 需要可用凭证:不再静默弹浏览器等待。
155
388
  const credentials = await auth.getCredentials();
389
+ if (!options.mirror)
390
+ process.env.CODEFREE_PROXY_NO_MIRROR = '1';
391
+ if (options.command === 'start') {
392
+ if (await portInUse(options.port, options.host)) {
393
+ const pids = await findListeningPids(options.port).catch(() => []);
394
+ const owners = [];
395
+ for (const pid of pids)
396
+ owners.push(`${pid}${looksLikeCodefreeProxy(await commandLineOf(pid)) ? '(codefree-proxy)' : '(其他程序)'}`);
397
+ console.error(`端口 ${options.port} 已被占用(${owners.join(' ')}),未启动。换端口用 --port,或先停掉占用进程。`);
398
+ return 1;
399
+ }
400
+ const result = await startDaemon(options);
401
+ if (!await waitForHealth(options.port, options.host)) {
402
+ const log = await tailLog(result.logFile);
403
+ console.error(`启动失败:进程 ${result.pid} 未能在 6 秒内监听 http://${options.host}:${options.port}/health`);
404
+ if (log)
405
+ console.error(`日志末尾:\n${log}`);
406
+ return 1;
407
+ }
408
+ console.log(formatDaemonStartedInfo({ host: options.host, port: options.port, userId: credentials.userId, models: SUPPORTED_MODELS }, result));
409
+ return 0;
410
+ }
156
411
  const app = buildServer({ auth, timeoutMs: options.timeoutMs });
157
412
  await app.listen({ host: options.host, port: options.port });
158
413
  console.log(formatStartupInfo({ host: options.host, port: options.port, userId: credentials.userId, models: SUPPORTED_MODELS }));
414
+ return 0;
159
415
  }
416
+
160
417
  function isSelf() {
161
418
  const argPath = process.argv[1];
162
419
  if (!argPath)
@@ -168,10 +425,16 @@ function isSelf() {
168
425
  return import.meta.url.endsWith(argPath.replace(/\\/g, '/'));
169
426
  }
170
427
  }
428
+
171
429
  if (isSelf()) {
172
- main().catch((error) => {
173
- console.error(error instanceof Error ? error.message : String(error));
174
- process.exit(1);
430
+ main().then((code) => {
431
+ if (typeof code === 'number' && code !== 0)
432
+ process.exit(code);
433
+ }).catch((error) => {
434
+ const message = error instanceof Error ? error.message : String(error);
435
+ console.error(message);
436
+ if (error?.code === 'CODEFREE_NO_CREDENTIALS')
437
+ console.error('\n下一步: 运行 codefree-proxy login 完成登录后重试;运行 codefree-proxy doctor 可查看环境详情。');
438
+ process.exit(error?.code === 'CODEFREE_NO_CREDENTIALS' ? 3 : 1);
175
439
  });
176
440
  }
177
- //# sourceMappingURL=cli.js.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Strip multimodal image parts before SRD Cloud acbackend.
3
+ * Upstream often returns bare "400 Bad Request" for OpenAI image_url /
4
+ * Anthropic image blocks, and does not reliably process vision inputs.
5
+ *
6
+ * Applied by patches/apply-image-strip.mjs into codefree-proxy openai-routes.
7
+ */
8
+
9
+ export const IMAGE_OMIT_NOTE =
10
+ '[Image omitted: CodeFree upstream rejects multimodal image payloads (400 Bad Request). Prefer OCR or a text description.]';
11
+
12
+ export function isImagePart(part) {
13
+ if (!part || typeof part !== 'object' || Array.isArray(part))
14
+ return false;
15
+ const type = typeof part.type === 'string' ? part.type : '';
16
+ if (type === 'image_url' || type === 'image' || type === 'input_image' || type === 'media')
17
+ return true;
18
+ if (part.image_url != null)
19
+ return true;
20
+ const source = part.source;
21
+ if (source && typeof source === 'object' && (source.type === 'base64' || source.type === 'url'))
22
+ return true;
23
+ // raw data-url style leftovers
24
+ if (typeof part.url === 'string' && part.url.startsWith('data:image/'))
25
+ return true;
26
+ return false;
27
+ }
28
+
29
+ /**
30
+ * Collapse an all-text array content into one string. Upstream rejects an array
31
+ * content on role="tool" outright (bare 400 Bad Request), and some clients keep
32
+ * a 2-part [text, note] shape after replacing a screenshot with placeholder text.
33
+ */
34
+ function collapseTextOnly(content) {
35
+ if (!Array.isArray(content) || content.length === 0)
36
+ return content;
37
+ const allText = content.every((part) => part && part.type === 'text' && typeof part.text === 'string');
38
+ if (!allText)
39
+ return content;
40
+ return content.map((part) => part.text).join('');
41
+ }
42
+
43
+ function contentHasText(parts) {
44
+ return parts.some((part) => {
45
+ if (!part || typeof part !== 'object')
46
+ return false;
47
+ if (part.type === 'text' && typeof part.text === 'string' && part.text.length > 0)
48
+ return true;
49
+ if (typeof part.text === 'string' && part.text.length > 0 && !part.type)
50
+ return true;
51
+ return false;
52
+ });
53
+ }
54
+
55
+ /**
56
+ * @returns {{ content: any, stripped: number }}
57
+ */
58
+ export function sanitizeContent(content) {
59
+ if (typeof content === 'string')
60
+ return { content, stripped: 0 };
61
+ if (!Array.isArray(content))
62
+ return { content, stripped: 0 };
63
+
64
+ const kept = [];
65
+ let stripped = 0;
66
+ for (const part of content) {
67
+ if (isImagePart(part)) {
68
+ stripped += 1;
69
+ continue;
70
+ }
71
+ kept.push(part);
72
+ }
73
+ if (stripped === 0)
74
+ return { content: collapseTextOnly(content), stripped: 0 };
75
+
76
+ if (!contentHasText(kept)) {
77
+ kept.unshift({ type: 'text', text: IMAGE_OMIT_NOTE });
78
+ }
79
+ else {
80
+ kept.push({ type: 'text', text: `\n${IMAGE_OMIT_NOTE}` });
81
+ }
82
+
83
+ // Tool results are happiest as plain strings. Upstream also rejects an array
84
+ // content on role="tool" outright (bare 400 Bad Request), so collapse every
85
+ // text part into one string — a 2-part [text, note] result must not stay a list.
86
+ if (kept.every((part) => part && part.type === 'text' && typeof part.text === 'string'))
87
+ return { content: kept.map((part) => part.text).join(''), stripped };
88
+
89
+ return { content: kept, stripped };
90
+ }
91
+
92
+ /**
93
+ * @returns {{ messages: any[], stripped: number }}
94
+ */
95
+ export function sanitizeMessages(messages) {
96
+ if (!Array.isArray(messages))
97
+ return { messages, stripped: 0 };
98
+ let stripped = 0;
99
+ const out = messages.map((message) => {
100
+ if (!message || typeof message !== 'object' || Array.isArray(message))
101
+ return message;
102
+ if (!('content' in message))
103
+ return message;
104
+ const result = sanitizeContent(message.content);
105
+ stripped += result.stripped;
106
+ if (result.content === message.content)
107
+ return message;
108
+ return { ...message, content: result.content };
109
+ });
110
+ return { messages: out, stripped };
111
+ }
112
+
113
+ /**
114
+ * @returns {{ body: any, stripped: number }}
115
+ */
116
+ export function sanitizeUpstreamBody(body) {
117
+ if (!body || typeof body !== 'object' || Array.isArray(body))
118
+ return { body, stripped: 0 };
119
+ if (!Array.isArray(body.messages))
120
+ return { body, stripped: 0 };
121
+ const { messages, stripped } = sanitizeMessages(body.messages);
122
+ const changed = messages !== body.messages && messages.some((m, i) => m !== body.messages[i]);
123
+ if (stripped === 0 && !changed)
124
+ return { body, stripped: 0 };
125
+ return { body: { ...body, messages }, stripped };
126
+ }
@@ -1,6 +1,60 @@
1
- export const DEFAULT_MODEL = 'GLM-5.1';
2
- export const SUPPORTED_MODELS = ['GLM-5.1', 'GLM-4.7', 'Qwen3.5-122B-A10B'];
1
+ export const DEFAULT_MODEL = 'DeepSeek-V4-Flash-0731';
2
+ // Keep the CodeFree models selected for local clients.
3
+ export const SUPPORTED_MODELS = [
4
+ 'DeepSeek-V4-Flash-0731',
5
+ 'DeepSeek-V4.1-Flash-1017',
6
+ 'GLM-5.2',
7
+ 'GLM-5.3',
8
+ 'Qwen3.8-27B',
9
+ ];
10
+ const MODEL_ALIASES = {
11
+ 'deepseek-v4-flash-0731': 'DeepSeek-V4-Flash-0731',
12
+ 'deepseek-v4-flash': 'DeepSeek-V4-Flash-0731',
13
+ 'deepseekv4flash0731': 'DeepSeek-V4-Flash-0731',
14
+ 'deepseekv4flash': 'DeepSeek-V4-Flash-0731',
15
+ 'deepseek-v4.1-flash-1017': 'DeepSeek-V4.1-Flash-1017',
16
+ 'deepseek-v4.1-flash': 'DeepSeek-V4.1-Flash-1017',
17
+ 'deepseekv41flash1017': 'DeepSeek-V4.1-Flash-1017',
18
+ 'deepseekv41flash': 'DeepSeek-V4.1-Flash-1017',
19
+ 'glm-5.2': 'GLM-5.2',
20
+ 'glm5.2': 'GLM-5.2',
21
+ 'glm-5.3': 'GLM-5.3',
22
+ 'glm5.3': 'GLM-5.3',
23
+ 'qwen3.8-27b': 'Qwen3.8-27B',
24
+ 'qwen3.8': 'Qwen3.8-27B',
25
+ };
26
+ const SUFFIX = '-TokenPlan';
27
+ /**
28
+ * Resolve a client-supplied model name (case-insensitive, optional
29
+ * "-TokenPlan" suffix) to a canonical CodeFree model. Returns null for names
30
+ * that are not supported, so callers can answer 400 instead of silently
31
+ * switching the user's model.
32
+ */
33
+ export function resolveModel(model) {
34
+ if (typeof model !== 'string')
35
+ return null;
36
+ const trimmed = model.trim();
37
+ if (trimmed.length === 0)
38
+ return null;
39
+ const base = trimmed.endsWith(SUFFIX) ? trimmed.slice(0, -SUFFIX.length) : trimmed;
40
+ const direct = SUPPORTED_MODELS.find((candidate) => candidate.toLowerCase() === base.toLowerCase());
41
+ if (direct)
42
+ return direct;
43
+ return MODEL_ALIASES[base.toLowerCase()] ?? null;
44
+ }
45
+ /** Upstream expects the -TokenPlan variant of every model. */
46
+ export function toUpstreamModel(canonical) {
47
+ return canonical.endsWith(SUFFIX) ? canonical : `${canonical}${SUFFIX}`;
48
+ }
49
+ function unsupportedModelMessage(model) {
50
+ const shown = typeof model === 'string' && model.trim().length > 0 ? model : '(未提供)';
51
+ return `不支持的模型:${shown}。可用模型:${SUPPORTED_MODELS.join(', ')}`;
52
+ }
3
53
  export function mapModel(model) {
4
- return SUPPORTED_MODELS.includes(model) ? model : DEFAULT_MODEL;
54
+ const canonical = resolveModel(model) ?? DEFAULT_MODEL;
55
+ return toUpstreamModel(canonical);
56
+ }
57
+ export function isSupportedModel(model) {
58
+ return resolveModel(model) !== null;
5
59
  }
6
- //# sourceMappingURL=model-map.js.map
60
+ export { unsupportedModelMessage };