aicodeswitch 5.2.9 → 5.2.11

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 CHANGED
@@ -35,50 +35,26 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
35
35
  * 数据完全本地,自主可控
36
36
  * 特殊语法:在发送的提示词最前面添加`[!]`来直接切换为高智商模型服务(`[x]`关闭),简单快捷
37
37
 
38
+ ![](public/aicodeswitch-flow.svg)
39
+
38
40
  ## 桌面客户端
39
41
 
40
42
  [进入下载](https://github.com/tangshuang/aicodeswitch/releases)
41
43
 
42
44
  ## 命令行工具
43
45
 
44
- ### 安装
45
-
46
46
  ```
47
+ # 安装
47
48
  npm install -g aicodeswitch
48
- ```
49
-
50
- ### 使用方法
51
-
52
- **启动服务**
53
-
54
- ```
49
+ # 启动服务
55
50
  aicos start
56
- ```
57
-
58
- 或者直接运行
59
-
60
- ```
51
+ # http://127.0.0.1:4567
52
+ # 启动服务并打开管理界面
61
53
  aicos ui
62
- ```
63
-
64
- **停止服务**
65
-
66
- ```
54
+ # 停止服务
67
55
  aicos stop
68
56
  ```
69
57
 
70
- **进入管理界面**
71
-
72
- ```
73
- # 自动启动服务和打开界面
74
- aicos ui
75
- ```
76
-
77
- ```
78
- # 手动在浏览器打开管理界面
79
- http://127.0.0.1:4567
80
- ```
81
-
82
58
  ## 管理界面
83
59
 
84
60
  **配置供应商**
@@ -257,6 +233,7 @@ PORT=4567
257
233
  ## 关联资源
258
234
 
259
235
  * [Claude Code 深度教程](https://claudecode.tangshuang.net): 100%免费的Claude Code入门到精通教程
236
+ * [AICodingBus](https://aicodingbus.24x7.to): AI Tokens 交换平台
260
237
 
261
238
  ## 支持我
262
239
 
package/bin/cli.js CHANGED
@@ -7,6 +7,7 @@ const commands = {
7
7
  start: require('./start'),
8
8
  stop: require('./stop'),
9
9
  restart: require('./restart'),
10
+ status: require('./status'),
10
11
  upgrade: require('./upgrade'),
11
12
  restore: require('./restore'),
12
13
  version: require('./version'),
@@ -21,6 +22,7 @@ Commands:
21
22
  start Start the AI Code Switch server
22
23
  stop Stop the AI Code Switch server
23
24
  restart Restart the AI Code Switch server
25
+ status Show server status, running address and port
24
26
  ui Open the web UI in browser (starts server if needed)
25
27
  upgrade Upgrade to the latest version and restart
26
28
  restore Restore original configuration files
@@ -30,6 +32,7 @@ Example:
30
32
  aicos start
31
33
  aicos stop
32
34
  aicos restart
35
+ aicos status
33
36
  aicos ui
34
37
  aicos upgrade
35
38
  aicos restore
package/bin/status.js ADDED
@@ -0,0 +1,84 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+ const boxen = require('boxen');
6
+ const { isServerRunning, getServerInfo } = require('./utils/get-server');
7
+ const { findPidByPort, getProcessInfo } = require('./utils/port-utils');
8
+
9
+ const PID_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.pid');
10
+ const LOG_FILE = path.join(os.homedir(), '.aicodeswitch', 'server.log');
11
+
12
+ const status = async () => {
13
+ console.log('\n');
14
+
15
+ // 读取配置的 host/port
16
+ const { host, port } = getServerInfo();
17
+ const url = `http://${host}:${port}`;
18
+
19
+ // 优先通过 PID 文件判断
20
+ let pidFromFile = null;
21
+ if (fs.existsSync(PID_FILE)) {
22
+ try {
23
+ pidFromFile = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
24
+ } catch (err) {
25
+ pidFromFile = null;
26
+ }
27
+ }
28
+
29
+ const runningByPidFile = isServerRunning();
30
+ // 通过端口检测(兜底:PID 文件丢失但服务仍在监听的情况)
31
+ const pidByPort = await findPidByPort(port);
32
+ const isRunning = runningByPidFile || !!pidByPort;
33
+
34
+ if (isRunning) {
35
+ // PID 优先使用 PID 文件记录的,其次使用端口检测到的
36
+ const pid = runningByPidFile ? pidFromFile : pidByPort;
37
+ const processInfo = await getProcessInfo(pid);
38
+
39
+ console.log(boxen(
40
+ chalk.green.bold('🟢 AI Code Switch Server\n\n') +
41
+ chalk.white('Status: ') + chalk.green.bold('● Running\n') +
42
+ chalk.white('Host: ') + chalk.cyan(host) + '\n' +
43
+ chalk.white('Port: ') + chalk.cyan.bold(port) + '\n' +
44
+ chalk.white('URL: ') + chalk.cyan.bold(url) + '\n' +
45
+ chalk.white('PID: ') + chalk.yellow(pid) + '\n' +
46
+ chalk.white('Process: ') + chalk.gray(processInfo) + '\n' +
47
+ chalk.white('Logs: ') + chalk.gray(LOG_FILE) + '\n\n' +
48
+ chalk.gray('Open the URL in your browser to access the dashboard'),
49
+ {
50
+ padding: 1,
51
+ margin: 1,
52
+ borderStyle: 'double',
53
+ borderColor: 'green'
54
+ }
55
+ ));
56
+
57
+ console.log(chalk.cyan('💡 Tips:\n'));
58
+ console.log(chalk.white(' • Open browser: ') + chalk.cyan(url));
59
+ console.log(chalk.white(' • View logs: ') + chalk.gray(`tail -f ${LOG_FILE}`));
60
+ console.log(chalk.white(' • Stop server: ') + chalk.yellow('aicos stop'));
61
+ console.log(chalk.white(' • Restart: ') + chalk.yellow('aicos restart'));
62
+ console.log('\n');
63
+ } else {
64
+ console.log(boxen(
65
+ chalk.gray('AI Code Switch Server\n\n') +
66
+ chalk.white('Status: ') + chalk.red('● Stopped\n\n') +
67
+ chalk.white('Host: ') + chalk.gray(host) + '\n' +
68
+ chalk.white('Port: ') + chalk.gray(port) + '\n' +
69
+ chalk.white('URL: ') + chalk.gray(url) + ' ' + chalk.gray('(not listening)'),
70
+ {
71
+ padding: 1,
72
+ margin: 1,
73
+ borderStyle: 'round',
74
+ borderColor: 'gray'
75
+ }
76
+ ));
77
+
78
+ console.log(chalk.white('Use ') + chalk.cyan('aicos start') + chalk.white(' to start the server.\n'));
79
+ }
80
+
81
+ process.exit(0);
82
+ };
83
+
84
+ module.exports = status;
@@ -45,6 +45,8 @@ const CODEX_CONFIG_MANAGED_FIELDS = [
45
45
  'enableRouteSelection',
46
46
  'model_providers.aicodeswitch',
47
47
  'mcp_servers',
48
+ 'features',
49
+ 'memories',
48
50
  ];
49
51
 
50
52
  /**
@@ -41,6 +41,8 @@ exports.CODEX_CONFIG_MANAGED_FIELDS = [
41
41
  { path: ['enableRouteSelection'] },
42
42
  { path: ['model_providers', 'aicodeswitch'], isSection: true },
43
43
  { path: ['mcp_servers'], isSection: true, optional: true },
44
+ { path: ['features'], isSection: true, optional: true },
45
+ { path: ['memories'], isSection: true, optional: true },
44
46
  ];
45
47
  /**
46
48
  * Codex auth.json 管理字段定义
@@ -9,6 +9,7 @@ exports.claudeToResponsesResponse = claudeToResponsesResponse;
9
9
  const id_js_1 = require("../../utils/id.js");
10
10
  const stop_reasons_js_1 = require("../../utils/stop-reasons.js");
11
11
  const mapper_js_1 = require("../../thinking/mapper.js");
12
+ const usage_js_1 = require("../../utils/usage.js");
12
13
  /**
13
14
  * Convert a Claude Messages response to an OpenAI Responses API response.
14
15
  */
@@ -45,26 +46,9 @@ function claudeToResponsesResponse(response) {
45
46
  }
46
47
  }
47
48
  const { status, incomplete_details } = (0, stop_reasons_js_1.claudeToResponsesStatus)(response.stop_reason);
48
- const usage = claudeToResponsesUsage(response.usage);
49
+ // 上游无 usage 时省略 usage 字段(不伪造 0)
50
+ const usage = (0, usage_js_1.toResponsesUsage)(response.usage);
49
51
  const responseId = response.id || (0, id_js_1.generateResponseId)();
50
- return Object.assign(Object.assign({ id: responseId, object: 'response', status,
51
- output, model: response.model || '', created_at: Math.floor(Date.now() / 1000), usage }, (incomplete_details ? { incomplete_details } : {})), { metadata: {} });
52
- }
53
- // ---------------------------------------------------------------------------
54
- // Helpers
55
- // ---------------------------------------------------------------------------
56
- /**
57
- * Map Claude usage to Responses API usage.
58
- */
59
- function claudeToResponsesUsage(usage) {
60
- var _a, _b;
61
- if (!usage)
62
- return { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
63
- const input = (_a = usage.input_tokens) !== null && _a !== void 0 ? _a : 0;
64
- const output = (_b = usage.output_tokens) !== null && _b !== void 0 ? _b : 0;
65
- return {
66
- input_tokens: input,
67
- output_tokens: output,
68
- total_tokens: input + output,
69
- };
52
+ return Object.assign(Object.assign(Object.assign({ id: responseId, object: 'response', status,
53
+ output, model: response.model || '', created_at: Math.floor(Date.now() / 1000) }, (usage ? { usage } : {})), (incomplete_details ? { incomplete_details } : {})), { metadata: {} });
70
54
  }
@@ -10,6 +10,7 @@ exports.ClaudeToResponsesConverter = void 0;
10
10
  const id_js_1 = require("../../utils/id.js");
11
11
  const stop_reasons_js_1 = require("../../utils/stop-reasons.js");
12
12
  const streaming_helpers_js_1 = require("../../utils/streaming-helpers.js");
13
+ const usage_js_1 = require("../../utils/usage.js");
13
14
  /**
14
15
  * ClaudeToResponsesConverter: Claude Messages SSE → Responses API SSE
15
16
  */
@@ -49,7 +50,7 @@ class ClaudeToResponsesConverter {
49
50
  enumerable: true,
50
51
  configurable: true,
51
52
  writable: true,
52
- value: { input_tokens: 0, output_tokens: 0 }
53
+ value: null
53
54
  });
54
55
  Object.defineProperty(this, "output", {
55
56
  enumerable: true,
@@ -89,7 +90,7 @@ class ClaudeToResponsesConverter {
89
90
  });
90
91
  }
91
92
  convertEvent(event) {
92
- var _a, _b, _c, _d, _e;
93
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
93
94
  if (!event.data)
94
95
  return [];
95
96
  const events = [];
@@ -99,6 +100,14 @@ class ClaudeToResponsesConverter {
99
100
  case 'message_start': {
100
101
  this.model = ((_a = data.message) === null || _a === void 0 ? void 0 : _a.model) || '';
101
102
  this.responseId = ((_b = data.message) === null || _b === void 0 ? void 0 : _b.id) || this.responseId;
103
+ // Claude 的 prompt token 数在 message_start 的 message.usage.input_tokens
104
+ const startUsage = (_c = data.message) === null || _c === void 0 ? void 0 : _c.usage;
105
+ if (startUsage) {
106
+ this.usage = {
107
+ input_tokens: (_d = startUsage.input_tokens) !== null && _d !== void 0 ? _d : 0,
108
+ output_tokens: (_e = startUsage.output_tokens) !== null && _e !== void 0 ? _e : 0,
109
+ };
110
+ }
102
111
  events.push(this.makeSSE('response.created', {
103
112
  id: this.responseId,
104
113
  object: 'response',
@@ -236,11 +245,11 @@ class ClaudeToResponsesConverter {
236
245
  break;
237
246
  }
238
247
  case 'message_delta': {
239
- this.pendingStopReason = ((_c = data.delta) === null || _c === void 0 ? void 0 : _c.stop_reason) || null;
248
+ this.pendingStopReason = ((_f = data.delta) === null || _f === void 0 ? void 0 : _f.stop_reason) || null;
240
249
  if (data.usage) {
241
250
  this.usage = {
242
- input_tokens: this.usage.input_tokens,
243
- output_tokens: (_e = (_d = data.usage.output_tokens) !== null && _d !== void 0 ? _d : data.usage.tokens) !== null && _e !== void 0 ? _e : 0,
251
+ input_tokens: (_j = (_g = data.usage.input_tokens) !== null && _g !== void 0 ? _g : (_h = this.usage) === null || _h === void 0 ? void 0 : _h.input_tokens) !== null && _j !== void 0 ? _j : 0,
252
+ output_tokens: (_o = (_l = (_k = data.usage.output_tokens) !== null && _k !== void 0 ? _k : data.usage.tokens) !== null && _l !== void 0 ? _l : (_m = this.usage) === null || _m === void 0 ? void 0 : _m.output_tokens) !== null && _o !== void 0 ? _o : 0,
244
253
  };
245
254
  }
246
255
  break;
@@ -253,7 +262,7 @@ class ClaudeToResponsesConverter {
253
262
  break;
254
263
  }
255
264
  }
256
- catch (_f) {
265
+ catch (_p) {
257
266
  // Ignore parse errors
258
267
  }
259
268
  return events;
@@ -315,20 +324,9 @@ class ClaudeToResponsesConverter {
315
324
  this.closeText(events);
316
325
  this.closeThinking(events);
317
326
  const { status, incomplete_details } = (0, stop_reasons_js_1.claudeToResponsesStatus)(this.pendingStopReason);
318
- const responseObj = {
319
- id: this.responseId,
320
- object: 'response',
321
- status,
322
- output: this.output,
323
- model: this.model,
324
- created_at: Math.floor(Date.now() / 1000),
325
- usage: {
326
- input_tokens: this.usage.input_tokens,
327
- output_tokens: this.usage.output_tokens,
328
- total_tokens: this.usage.input_tokens + this.usage.output_tokens,
329
- },
330
- metadata: {},
331
- };
327
+ // 上游无 usage 时省略 usage 字段(不伪造 0,避免 Codex missing field input_tokens)
328
+ const responsesUsage = (0, usage_js_1.toResponsesUsage)(this.usage);
329
+ const responseObj = Object.assign({ id: this.responseId, object: 'response', status, output: this.output, model: this.model, created_at: Math.floor(Date.now() / 1000), metadata: {} }, (responsesUsage ? { usage: responsesUsage } : {}));
332
330
  if (incomplete_details) {
333
331
  responseObj.incomplete_details = incomplete_details;
334
332
  }
@@ -80,15 +80,8 @@ function completionsToResponsesResponse(response) {
80
80
  const finishReason = (0, stop_reasons_js_1.completionsToResponsesFinishReason)(choice.finish_reason);
81
81
  const status = finishReason === 'incomplete' ? 'incomplete' : 'completed';
82
82
  const usage = (0, usage_js_1.completionsToResponsesUsage)(response.usage);
83
- const result = {
84
- id: ((_m = response.id) === null || _m === void 0 ? void 0 : _m.startsWith('resp_')) ? response.id : (0, id_js_1.generateResponseId)(),
85
- object: 'response',
86
- status,
87
- output,
88
- model: response.model,
89
- usage,
90
- created_at: response.created || Math.floor(Date.now() / 1000),
91
- };
83
+ const result = Object.assign({ id: ((_m = response.id) === null || _m === void 0 ? void 0 : _m.startsWith('resp_')) ? response.id : (0, id_js_1.generateResponseId)(), object: 'response', status,
84
+ output, model: response.model, created_at: response.created || Math.floor(Date.now() / 1000) }, (usage ? { usage } : {}));
92
85
  if (status === 'incomplete') {
93
86
  result.incomplete_details = { reason: 'max_output_tokens' };
94
87
  }
@@ -320,16 +320,9 @@ class CompletionsToResponsesConverter {
320
320
  }
321
321
  // Build full response object
322
322
  const status = this.finishReason === 'length' ? 'incomplete' : 'completed';
323
- const responseObj = {
324
- id: this.responseId,
325
- object: 'response',
326
- status,
327
- output: this.output,
328
- model: this.model,
329
- usage: this.usage ? (0, usage_js_1.completionsToResponsesUsage)(this.usage) : {},
330
- created_at: Math.floor(Date.now() / 1000),
331
- metadata: {},
332
- };
323
+ // 上游无 usage 时省略 usage 字段(不伪造 0,避免 Codex missing field input_tokens)
324
+ const responsesUsage = (0, usage_js_1.completionsToResponsesUsage)(this.usage);
325
+ const responseObj = Object.assign({ id: this.responseId, object: 'response', status, output: this.output, model: this.model, created_at: Math.floor(Date.now() / 1000), metadata: {} }, (responsesUsage ? { usage: responsesUsage } : {}));
333
326
  if (status === 'incomplete') {
334
327
  responseObj.incomplete_details = { reason: 'max_output_tokens' };
335
328
  }
@@ -8,6 +8,7 @@ exports.claudeToCompletionsUsage = claudeToCompletionsUsage;
8
8
  exports.geminiToClaudeUsage = geminiToClaudeUsage;
9
9
  exports.claudeToGeminiUsage = claudeToGeminiUsage;
10
10
  exports.responsesToClaudeUsage = responsesToClaudeUsage;
11
+ exports.toResponsesUsage = toResponsesUsage;
11
12
  exports.completionsToResponsesUsage = completionsToResponsesUsage;
12
13
  /** Map OpenAI Chat usage to Claude usage */
13
14
  function completionsToClaudeUsage(usage) {
@@ -69,14 +70,33 @@ function responsesToClaudeUsage(usage) {
69
70
  cache_creation_input_tokens: usage.cache_creation_input_tokens,
70
71
  };
71
72
  }
72
- /** Map OpenAI Chat usage to Responses API usage */
73
- function completionsToResponsesUsage(usage) {
74
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
75
- if (!usage)
76
- return {};
73
+ /**
74
+ * 构造标准 Responses API usage 对象(转换层兼容入口)。
75
+ *
76
+ * 真实值优先:input_tokens ?? prompt_tokens、output_tokens ?? completion_tokens、total_tokens。
77
+ * 覆盖上游 chat completions / claude / gemini 三种格式的字段命名归一。
78
+ *
79
+ * 仅当上游确实提供了任意 token 字段时返回归一化对象;否则返回 null(调用方应省略 usage 字段,
80
+ * 不伪造 0)。这是避免 Codex `ResponseCompleted: missing field input_tokens` 的关键——
81
+ * 既不吐空 `{}`,也不吐伪造的 `{0,0,0}`。
82
+ */
83
+ function toResponsesUsage(usage) {
84
+ var _a, _b;
85
+ if (!usage || typeof usage !== 'object')
86
+ return null;
87
+ const input_tokens = (_a = usage.input_tokens) !== null && _a !== void 0 ? _a : usage.prompt_tokens;
88
+ const output_tokens = (_b = usage.output_tokens) !== null && _b !== void 0 ? _b : usage.completion_tokens;
89
+ const total_tokens = usage.total_tokens;
90
+ // 上游没返回任何 token 字段 → 不伪造,返回 null
91
+ if (input_tokens == null && output_tokens == null && total_tokens == null)
92
+ return null;
77
93
  return {
78
- input_tokens: (_b = (_a = usage.input_tokens) !== null && _a !== void 0 ? _a : usage.prompt_tokens) !== null && _b !== void 0 ? _b : 0,
79
- output_tokens: (_d = (_c = usage.output_tokens) !== null && _c !== void 0 ? _c : usage.completion_tokens) !== null && _d !== void 0 ? _d : 0,
80
- total_tokens: (_e = usage.total_tokens) !== null && _e !== void 0 ? _e : (((_g = (_f = usage.input_tokens) !== null && _f !== void 0 ? _f : usage.prompt_tokens) !== null && _g !== void 0 ? _g : 0) + ((_j = (_h = usage.output_tokens) !== null && _h !== void 0 ? _h : usage.completion_tokens) !== null && _j !== void 0 ? _j : 0)),
94
+ input_tokens: input_tokens !== null && input_tokens !== void 0 ? input_tokens : 0,
95
+ output_tokens: output_tokens !== null && output_tokens !== void 0 ? output_tokens : 0,
96
+ total_tokens: total_tokens !== null && total_tokens !== void 0 ? total_tokens : ((input_tokens !== null && input_tokens !== void 0 ? input_tokens : 0) + (output_tokens !== null && output_tokens !== void 0 ? output_tokens : 0)),
81
97
  };
82
98
  }
99
+ /** Map OpenAI Chat usage to Responses API usage(薄封装,保持现有调用方不变) */
100
+ function completionsToResponsesUsage(usage) {
101
+ return toResponsesUsage(usage);
102
+ }
@@ -383,7 +383,7 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
383
383
  const isCodexReasoningEffort = (value) => {
384
384
  return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
385
385
  };
386
- const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, options = {}) {
386
+ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, enableMemories, options = {}) {
387
387
  var _a;
388
388
  try {
389
389
  const homeDir = os_1.default.homedir();
@@ -450,6 +450,17 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
450
450
  }
451
451
  }
452
452
  };
453
+ // 记忆功能配置
454
+ if (enableMemories) {
455
+ proxyConfig.features = {
456
+ memories: true,
457
+ };
458
+ proxyConfig.memories = {
459
+ generate_memories: true,
460
+ use_memories: true,
461
+ disable_on_external_context: true,
462
+ };
463
+ }
453
464
  // 使用智能合并
454
465
  const mergedConfig = (0, config_merge_1.mergeTomlConfig)(proxyConfig, currentConfig, config_managed_fields_1.CODEX_CONFIG_MANAGED_FIELDS);
455
466
  // 原子性写入合并后的配置
@@ -681,7 +692,7 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
681
692
  const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
682
693
  ? config.codexModelReasoningEffort
683
694
  : DEFAULT_CODEX_REASONING_EFFORT;
684
- const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel);
695
+ const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
685
696
  console.log(`[Startup Config Sync] Codex config ${codexWritten ? 'written' : 'skipped'}`);
686
697
  });
687
698
  const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
@@ -694,7 +705,7 @@ const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0,
694
705
  const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
695
706
  ? config.codexModelReasoningEffort
696
707
  : DEFAULT_CODEX_REASONING_EFFORT;
697
- const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, { allowOverwriteRefresh: true });
708
+ const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
698
709
  console.log(`[Config Update Sync] Codex config ${codexUpdated ? 'written' : 'skipped'}`);
699
710
  });
700
711
  const getCentralSkillsDir = () => {
@@ -2038,7 +2049,11 @@ ${instruction}
2038
2049
  : isCodexReasoningEffort(appConfig.codexModelReasoningEffort)
2039
2050
  ? appConfig.codexModelReasoningEffort
2040
2051
  : DEFAULT_CODEX_REASONING_EFFORT;
2041
- const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel);
2052
+ const requestedEnableMemories = req.body.enableMemories;
2053
+ const enableMemories = requestedEnableMemories !== undefined
2054
+ ? !!requestedEnableMemories
2055
+ : !!appConfig.codexEnableMemories;
2056
+ const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories);
2042
2057
  applyWriteLocalRecords(proxyServer);
2043
2058
  res.json(result);
2044
2059
  })));
@@ -3288,21 +3303,23 @@ ${instruction}
3288
3303
  }
3289
3304
  }
3290
3305
  });
3306
+ // listen 就绪标志:区分"启动阶段"与"运行阶段",启动期致命异常应让进程退出
3307
+ let listenReady = false;
3291
3308
  const start = () => __awaiter(void 0, void 0, void 0, function* () {
3292
3309
  fs_1.default.mkdirSync(dataDir, { recursive: true });
3293
3310
  // 自动检测数据库类型并执行迁移(如果需要)
3294
- console.log('[Server] Initializing database...');
3311
+ console.time('[Server] step "database-init"');
3295
3312
  const dbManager = yield database_factory_1.DatabaseFactory.createAuto(dataDir, legacyDataDir);
3296
- console.log('[Server] Database initialized successfully');
3313
+ console.timeEnd('[Server] step "database-init"');
3297
3314
  // 服务启动时自动同步配置文件(适用于 CLI 和 dev:server)
3298
- console.log('[Server] Syncing tool configs with global settings...');
3315
+ console.time('[Server] step "sync-configs"');
3299
3316
  try {
3300
3317
  yield syncConfigsOnServerStartup(dbManager);
3301
- console.log('[Server] Tool config sync completed');
3302
3318
  }
3303
3319
  catch (error) {
3304
3320
  console.error('[Server] Tool config sync failed:', error);
3305
3321
  }
3322
+ console.timeEnd('[Server] step "sync-configs"');
3306
3323
  // 清理旧的迁移临时文件
3307
3324
  try {
3308
3325
  (0, session_launcher_1.cleanupOldTempFiles)();
@@ -3328,8 +3345,10 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3328
3345
  // Initialize proxy server and register proxy routes last
3329
3346
  proxyServer.initialize();
3330
3347
  // Register admin routes first
3348
+ console.time('[Server] step "register-routes"');
3331
3349
  yield registerRoutes(dbManager, proxyServer);
3332
3350
  yield proxyServer.registerProxyRoutes();
3351
+ console.timeEnd('[Server] step "register-routes"');
3333
3352
  app.use(express_1.default.static(path_1.default.resolve(__dirname, '../ui')));
3334
3353
  // 404 处理程序 - 确保返回 JSON 而不是 HTML(放在所有路由和静态文件之后)
3335
3354
  app.use((_req, res) => {
@@ -3347,14 +3366,28 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3347
3366
  console.error(`端口 ${port} 已被占用,无法启动服务。请执行 aicos stop 后重启。`);
3348
3367
  process.exit(1);
3349
3368
  }
3369
+ console.time('[Server] step "listen"');
3350
3370
  const server = app.listen(port, host, () => {
3371
+ listenReady = true;
3351
3372
  console.log(`Admin server running on http://${host}:${port}`);
3373
+ console.timeEnd('[Server] step "listen"');
3352
3374
  // 启动后异步执行延迟维护任务(分片校验/修复、日志清理、会话索引构建)
3353
3375
  // 不阻塞服务启动,后台静默执行
3354
3376
  dbManager.deferredMaintenance().catch(err => {
3355
3377
  console.error('[Server] Deferred maintenance error:', err);
3356
3378
  });
3357
3379
  });
3380
+ // 显式处理 listen 错误(EADDRINUSE/权限不足等),打印明确日志并退出,
3381
+ // 避免被全局 uncaughtException 静默吞掉导致"进程在但不 listen"
3382
+ server.on('error', (err) => {
3383
+ if (err.code === 'EADDRINUSE') {
3384
+ console.error(`[Server] 端口 ${port} 已被占用(EADDRINUSE)。请执行 aicos stop 后重启,或更换端口(PORT 环境变量)。`);
3385
+ }
3386
+ else {
3387
+ console.error('[Server] 监听失败:', err);
3388
+ }
3389
+ setImmediate(() => process.exit(1));
3390
+ });
3358
3391
  // 创建 WebSocket 服务器用于工具安装
3359
3392
  const toolInstallWss = (0, websocket_service_1.createToolInstallationWSServer)();
3360
3393
  // 设置黑名单检查函数,用于在规则状态同步时检查黑名单是否已过期
@@ -3435,11 +3468,19 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
3435
3468
  process.on('uncaughtException', (error) => {
3436
3469
  console.error('[Uncaught Exception] 服务遇到未捕获的异常:', error);
3437
3470
  console.error('[Uncaught Exception] 堆栈信息:', error.stack);
3438
- // 不退出进程,继续运行
3471
+ // 启动阶段(listen 之前)的异常通常是致命的(依赖加载失败、初始化崩溃等),
3472
+ // 静默吞掉会导致"进程在但不 listen",Tauri 只能干等超时;此时退出让上层重新探测/诊断。
3473
+ if (!listenReady) {
3474
+ console.error('[Uncaught Exception] 发生在服务监听之前,退出进程');
3475
+ process.exit(1);
3476
+ }
3439
3477
  });
3440
3478
  process.on('unhandledRejection', (reason) => {
3441
3479
  console.error('[Unhandled Rejection] 服务遇到未处理的 Promise 拒绝:', reason);
3442
- // 不退出进程,继续运行
3480
+ if (!listenReady) {
3481
+ console.error('[Unhandled Rejection] 发生在服务监听之前,退出进程');
3482
+ process.exit(1);
3483
+ }
3443
3484
  });
3444
3485
  start().catch((error) => {
3445
3486
  console.error('Failed to start server:', error);
@@ -22,7 +22,7 @@ const os_1 = require("os");
22
22
  function which(cmd) {
23
23
  try {
24
24
  const command = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
25
- (0, child_process_1.execSync)(command, { stdio: 'ignore' });
25
+ (0, child_process_1.execSync)(command, { stdio: 'ignore', windowsHide: true }); // 隐藏 Windows 命令行窗口,避免检测时闪窗
26
26
  return true;
27
27
  }
28
28
  catch (_a) {
@@ -29,6 +29,7 @@ function checkToolInstalled(toolName) {
29
29
  const child = (0, child_process_1.spawn)(command, ['--version'], {
30
30
  shell: true,
31
31
  stdio: ['ignore', 'pipe', 'pipe'],
32
+ windowsHide: true, // 隐藏 Windows 命令行窗口,避免检测时闪窗
32
33
  });
33
34
  let stdout = '';
34
35
  let stderr = '';
@@ -9,13 +9,27 @@ Object.defineProperty(exports, "isLastMessageCompact", { enumerable: true, get:
9
9
  Object.defineProperty(exports, "isCodexCompactRequest", { enumerable: true, get: function () { return compact_1.isCodexCompactRequest; } });
10
10
  function checkPortUsable(port) {
11
11
  return new Promise((resolve) => {
12
+ let settled = false;
12
13
  const server = net.createConnection({ port });
13
- server.on('connect', () => {
14
- server.end();
15
- resolve(false);
16
- });
17
- server.on('error', () => {
18
- resolve(true);
14
+ const finish = (val) => {
15
+ if (settled)
16
+ return;
17
+ settled = true;
18
+ try {
19
+ server.destroy();
20
+ }
21
+ catch ( /* ignore */_a) { /* ignore */ }
22
+ resolve(val);
23
+ };
24
+ // 正常:连得上 = 端口被占;连不上(ECONNREFUSED) = 端口可用
25
+ server.on('connect', () => finish(false));
26
+ server.on('error', () => finish(true));
27
+ // 兜底:网络栈异常(防火墙/杀软 hook)时 connect/error 可能都不触发,
28
+ // 1.5s 后强制按可用处理,避免 start() 永久卡死。误判由 app.listen 的 EADDRINUSE 兜底。
29
+ server.setTimeout(1500);
30
+ server.once('timeout', () => {
31
+ console.warn(`[checkPortUsable] 探测端口 ${port} 超时(1.5s),按可用处理`);
32
+ finish(true);
19
33
  });
20
34
  });
21
35
  }