aicodeswitch 6.0.1 → 6.0.2

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
@@ -39,7 +39,10 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
39
39
 
40
40
  ## 桌面客户端
41
41
 
42
- [进入下载](https://github.com/tangshuang/aicodeswitch/releases)
42
+ 桌面版基于 Electron,内置了 Node 运行时,**无需另外安装 Node.js**。
43
+
44
+ - 下载安装包:[进入下载](https://github.com/tangshuang/aicodeswitch/releases)(提供 Windows `.exe`/`.msi`、macOS `.dmg`(arm64 与 x64)、Linux `.AppImage`/`.deb`)
45
+ - 本地构建:`npm install` 后执行 `npm run electron:build`,产物输出到 `release/` 目录
43
46
 
44
47
  ## 命令行工具
45
48
 
@@ -100,8 +100,8 @@ const effort_js_1 = require("./thinking/effort.js");
100
100
  * Transform a request body from one format to another.
101
101
  */
102
102
  function transformRequest(options) {
103
- const { fromFormat, toFormat, body, sanitizeBody, providerConfig } = options;
104
- const targetBody = buildTargetBody({ fromFormat, toFormat, body, sanitizeBody, providerConfig });
103
+ const { fromFormat, toFormat, body, sanitizeBody, providerConfig, serverToolConfig } = options;
104
+ const targetBody = buildTargetBody({ fromFormat, toFormat, body, sanitizeBody, providerConfig, serverToolConfig });
105
105
  return { body: targetBody, headers: {} };
106
106
  }
107
107
  // ============================================================
@@ -205,11 +205,13 @@ function createStreamConverter(options) {
205
205
  */
206
206
  function buildTargetBody(options) {
207
207
  const { fromFormat, toFormat, sanitizeBody, providerConfig, serverToolConfig } = options;
208
- // Pre-processing: convert server_tool_use tool_use when upstream doesn't support it.
208
+ // Pre-processing: strip Anthropic server-tool artifacts (server_tool_use,
209
+ // web_search_tool_result, server_tool_result, advisor_tool_result, and server-tool
210
+ // definitions in `tools`) when the upstream doesn't support them.
209
211
  // Must happen before format conversion so all pair transformers handle the blocks correctly.
210
212
  let processedBody = options.body;
211
213
  if (fromFormat === 'claude' && !(serverToolConfig === null || serverToolConfig === void 0 ? void 0 : serverToolConfig.supportsServerToolUse)) {
212
- processedBody = (0, mapper_js_1.convertServerToolUseToToolUse)(processedBody);
214
+ processedBody = (0, mapper_js_1.sanitizeServerToolArtifacts)(processedBody);
213
215
  }
214
216
  // Dispatch to the correct conversion pair
215
217
  const key = `${fromFormat}->${toFormat}`;
@@ -2,48 +2,161 @@
2
2
  /**
3
3
  * Server tool use content block transformation.
4
4
  *
5
- * Converts server_tool_use blocks to regular tool_use blocks so that upstream
6
- * providers which do not recognise the server_tool_use type can still process
7
- * the conversation history correctly.
5
+ * Anthropic 提供一类"服务端工具"(Web Search / Web Fetch / Computer Use / Code Execution 等),
6
+ * 其交互在内容块层面会用到 Anthropic 专有的类型:
7
+ * - `server_tool_use` (assistant 消息:模型发起的服务端工具调用)
8
+ * - `web_search_tool_result` (user 消息:Web Search 结果)
9
+ * - `server_tool_result` (user 消息:通用服务端工具结果)
10
+ * - `advisor_tool_result` (user 消息:advisor 工具结果)
11
+ * 此外 `tools` 数组里会带上服务端工具定义(如 `{ type: 'web_search_20250305' }`)。
8
12
  *
9
- * Conversion is simple: only the `type` field changes from 'server_tool_use'
10
- * to 'tool_use'. The `id`, `name`, and `input` fields are preserved, and
11
- * matching `tool_result` blocks (which reference by `tool_use_id`) remain valid.
13
+ * 多数第三方 Claude 兼容端点(GLM、MiniMax 等)只实现了客户端 `tool_use`/`tool_result`,
14
+ * 遇到上述任意一种都会以 `Unsupported content type: server_tool_use` 之类报错拒绝。
15
+ *
16
+ * 本模块在转发到"不支持服务端工具"的上游前,彻底清理这些痕迹:
17
+ * 1. assistant 的 `server_tool_use` → 改名 `tool_use`(保留 id/name/input);
18
+ * 2. user 的 `web_search_tool_result` / `server_tool_result` / `advisor_tool_result`
19
+ * → 降级为标准 `tool_result`(引用同一 tool_use_id,内容拍平为文本),
20
+ * 以便与上一步改名的 `tool_use` 维持合法配对;
21
+ * 3. 顶层 `tools` 数组中删除所有服务端工具定义,仅保留客户端自定义工具。
22
+ *
23
+ * 返回浅拷贝,不修改传入 body。
12
24
  */
13
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.sanitizeServerToolArtifacts = sanitizeServerToolArtifacts;
14
27
  exports.convertServerToolUseToToolUse = convertServerToolUseToToolUse;
28
+ /** user 消息里需要降级为 tool_result 的服务端结果块类型 */
29
+ const SERVER_RESULT_TYPES = new Set([
30
+ 'web_search_tool_result',
31
+ 'server_tool_result',
32
+ 'advisor_tool_result',
33
+ ]);
15
34
  /**
16
- * Convert all server_tool_use content blocks in the request body to tool_use.
35
+ * 判断 `tools` 数组中的某项是否为 Anthropic 服务端工具定义。
36
+ * 客户端自定义工具的 `type` 为 'custom' 或缺省,且通常带 `input_schema`;
37
+ * 服务端工具 `type` 形如 'web_search_20250305' / 'computer_20250124' / 'bash_20250124'
38
+ * / 'text_editor_20250124' / 'code_execution_20250522' 等,且无 `input_schema`。
39
+ */
40
+ function isServerToolDefinition(tool) {
41
+ if (!tool || typeof tool !== 'object')
42
+ return false;
43
+ const type = tool.type;
44
+ // 自定义工具:type 缺省或 'custom'
45
+ if (type === undefined || type === null || type === 'custom')
46
+ return false;
47
+ if (typeof type !== 'string')
48
+ return true;
49
+ // 已知服务端工具类型前缀
50
+ const SERVER_TOOL_PREFIXES = [
51
+ 'web_search',
52
+ 'computer',
53
+ 'bash',
54
+ 'text_editor',
55
+ 'code_execution',
56
+ ];
57
+ if (SERVER_TOOL_PREFIXES.some((p) => type.startsWith(p)))
58
+ return true;
59
+ // 兜底:带非 custom 的 type 且没有 input_schema,视为服务端/私有工具
60
+ if (!tool.input_schema)
61
+ return true;
62
+ return false;
63
+ }
64
+ /** 把任意值压平为纯文本字符串(用于降级服务端结果块内容) */
65
+ function flattenToText(value) {
66
+ if (value == null)
67
+ return '';
68
+ if (typeof value === 'string')
69
+ return value;
70
+ if (Array.isArray(value)) {
71
+ return value
72
+ .map((item) => {
73
+ if (!item || typeof item !== 'object')
74
+ return String(item);
75
+ // 常见形态:{ type: 'text', text } / { type: 'web_search_tool_result', content: [...] }
76
+ if (typeof item.text === 'string')
77
+ return item.text;
78
+ if (typeof item.title === 'string')
79
+ return item.title;
80
+ if (Array.isArray(item.content))
81
+ return flattenToText(item.content);
82
+ if (item.url)
83
+ return `${item.url}`;
84
+ return JSON.stringify(item);
85
+ })
86
+ .filter(Boolean)
87
+ .join('\n');
88
+ }
89
+ if (typeof value === 'object') {
90
+ if (typeof value.text === 'string')
91
+ return value.text;
92
+ if (Array.isArray(value.content))
93
+ return flattenToText(value.content);
94
+ return JSON.stringify(value);
95
+ }
96
+ return String(value);
97
+ }
98
+ /**
99
+ * 清理请求体中所有"服务端工具"痕迹(内容块 + tools 定义)。
17
100
  *
18
- * Scans assistant messages in body.messages and replaces the block type.
19
- * Returns a shallow-cloned body with modified messages; original body is not mutated.
101
+ * 扫描 body.messages 中所有 role 的内容块,以及顶层 body.tools。
102
+ * 返回浅拷贝;无改动时原样返回原 body
20
103
  */
21
- function convertServerToolUseToToolUse(body) {
22
- if (!(body === null || body === void 0 ? void 0 : body.messages) || !Array.isArray(body.messages)) {
104
+ function sanitizeServerToolArtifacts(body) {
105
+ if (!body || typeof body !== 'object')
23
106
  return body;
24
- }
25
107
  let modified = false;
26
- const newMessages = body.messages.map((msg) => {
27
- // server_tool_use only appears in assistant messages
28
- if (msg.role !== 'assistant' || !Array.isArray(msg.content)) {
29
- return msg;
30
- }
31
- let msgModified = false;
32
- const newContent = msg.content.map((block) => {
33
- if ((block === null || block === void 0 ? void 0 : block.type) === 'server_tool_use') {
34
- msgModified = true;
35
- return Object.assign(Object.assign({}, block), { type: 'tool_use' });
108
+ let newBody = body;
109
+ // --- 1 & 2:处理 messages 内容块 ---
110
+ if (Array.isArray(body.messages)) {
111
+ const newMessages = body.messages.map((msg) => {
112
+ if (!msg || typeof msg !== 'object' || !Array.isArray(msg.content)) {
113
+ return msg;
114
+ }
115
+ let msgModified = false;
116
+ const newContent = msg.content.map((block) => {
117
+ if (!block || typeof block !== 'object')
118
+ return block;
119
+ // assistant: server_tool_use → tool_use
120
+ if (block.type === 'server_tool_use') {
121
+ msgModified = true;
122
+ return Object.assign(Object.assign({}, block), { type: 'tool_use' });
123
+ }
124
+ // user: 服务端结果块 → 标准 tool_result(保持与上面改名的 tool_use 配对)
125
+ if (SERVER_RESULT_TYPES.has(block.type)) {
126
+ msgModified = true;
127
+ const text = flattenToText(block.content);
128
+ return {
129
+ type: 'tool_result',
130
+ tool_use_id: block.tool_use_id,
131
+ content: text || '[server tool result omitted]',
132
+ };
133
+ }
134
+ return block;
135
+ });
136
+ if (msgModified) {
137
+ modified = true;
138
+ return Object.assign(Object.assign({}, msg), { content: newContent });
36
139
  }
37
- return block;
140
+ return msg;
38
141
  });
39
- if (msgModified) {
142
+ if (modified) {
143
+ newBody = Object.assign(Object.assign({}, newBody), { messages: newMessages });
144
+ }
145
+ }
146
+ // --- 3:清理 tools 数组中的服务端工具定义 ---
147
+ if (Array.isArray(newBody.tools) && newBody.tools.length > 0) {
148
+ const filteredTools = newBody.tools.filter((tool) => !isServerToolDefinition(tool));
149
+ if (filteredTools.length !== newBody.tools.length) {
40
150
  modified = true;
41
- return Object.assign(Object.assign({}, msg), { content: newContent });
151
+ newBody = Object.assign(Object.assign({}, newBody), { tools: filteredTools });
42
152
  }
43
- return msg;
44
- });
45
- if (!modified) {
46
- return body;
47
153
  }
48
- return Object.assign(Object.assign({}, body), { messages: newMessages });
154
+ return modified ? newBody : body;
155
+ }
156
+ /**
157
+ * @deprecated 别名,等价于 {@link sanitizeServerToolArtifacts}。
158
+ * 保留旧名以兼容既有导入;新代码请使用 sanitizeServerToolArtifacts。
159
+ */
160
+ function convertServerToolUseToToolUse(body) {
161
+ return sanitizeServerToolArtifacts(body);
49
162
  }
@@ -55,6 +55,10 @@ const isClaudePermissionDefaultMode = (value) => {
55
55
  const isValidAutocompactPct = (v) => {
56
56
  return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
57
57
  };
58
+ // 最大重试次数校验:正整数(1-20),非法值返回 undefined(由 write 函数兜底默认 5)
59
+ const isValidMaxRetries = (v) => {
60
+ return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 20;
61
+ };
58
62
  const normalizeFailoverRecoverySeconds = (value) => {
59
63
  const parsed = typeof value === 'number' ? value : Number(value);
60
64
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -836,6 +840,15 @@ class FileSystemDatabaseManager {
836
840
  if (typeof this.config.autocompactPctOverride !== 'undefined' && !isValidAutocompactPct(this.config.autocompactPctOverride)) {
837
841
  this.config.autocompactPctOverride = undefined;
838
842
  }
843
+ if (typeof this.config.claudeMaxRetries !== 'undefined' && !isValidMaxRetries(this.config.claudeMaxRetries)) {
844
+ this.config.claudeMaxRetries = undefined;
845
+ }
846
+ if (typeof this.config.codexMaxRetries !== 'undefined' && !isValidMaxRetries(this.config.codexMaxRetries)) {
847
+ this.config.codexMaxRetries = undefined;
848
+ }
849
+ if (typeof this.config.opencodeMaxRetries !== 'undefined' && !isValidMaxRetries(this.config.opencodeMaxRetries)) {
850
+ this.config.opencodeMaxRetries = undefined;
851
+ }
839
852
  this.config.failoverRecoverySeconds = normalizeFailoverRecoverySeconds(this.config.failoverRecoverySeconds);
840
853
  if (typeof this.config.ruleGlobalTimeout !== 'number' || this.config.ruleGlobalTimeout <= 0) {
841
854
  this.config.ruleGlobalTimeout = undefined;
@@ -1752,6 +1765,15 @@ class FileSystemDatabaseManager {
1752
1765
  if (typeof merged.autocompactPctOverride !== 'undefined' && !isValidAutocompactPct(merged.autocompactPctOverride)) {
1753
1766
  merged.autocompactPctOverride = undefined;
1754
1767
  }
1768
+ if (typeof merged.claudeMaxRetries !== 'undefined' && !isValidMaxRetries(merged.claudeMaxRetries)) {
1769
+ merged.claudeMaxRetries = undefined;
1770
+ }
1771
+ if (typeof merged.codexMaxRetries !== 'undefined' && !isValidMaxRetries(merged.codexMaxRetries)) {
1772
+ merged.codexMaxRetries = undefined;
1773
+ }
1774
+ if (typeof merged.opencodeMaxRetries !== 'undefined' && !isValidMaxRetries(merged.opencodeMaxRetries)) {
1775
+ merged.opencodeMaxRetries = undefined;
1776
+ }
1755
1777
  merged.failoverRecoverySeconds = normalizeFailoverRecoverySeconds(merged.failoverRecoverySeconds);
1756
1778
  if (typeof merged.ruleGlobalTimeout !== 'number' || merged.ruleGlobalTimeout <= 0) {
1757
1779
  merged.ruleGlobalTimeout = undefined;
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.start = exports.gracefulShutdown = void 0;
15
16
  const express_1 = __importDefault(require("express"));
16
17
  const cors_1 = __importDefault(require("cors"));
17
18
  const dotenv_1 = __importDefault(require("dotenv"));
@@ -263,7 +264,7 @@ const isClaudePermissionDefaultMode = (value) => {
263
264
  const isValidAutocompactPct = (v) => {
264
265
  return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
265
266
  };
266
- const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, options = {}) {
267
+ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, maxRetries_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, maxRetries_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, maxRetries, options = {}) {
267
268
  var _a;
268
269
  try {
269
270
  const homeDir = os_1.default.homedir();
@@ -318,7 +319,7 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
318
319
  ANTHROPIC_BASE_URL: `http://${clientHost}:${port}/claude-code`,
319
320
  API_TIMEOUT_MS: "3000000",
320
321
  CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
321
- CLAUDE_CODE_MAX_RETRIES: 3
322
+ CLAUDE_CODE_MAX_RETRIES: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5
322
323
  };
323
324
  // 如果启用Agent Teams功能,添加对应的环境变量
324
325
  if (enableAgentTeams) {
@@ -416,7 +417,7 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
416
417
  const isCodexReasoningEffort = (value) => {
417
418
  return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
418
419
  };
419
- 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 = {}) {
420
+ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, enableMemories, maxRetries, options = {}) {
420
421
  var _a;
421
422
  try {
422
423
  const homeDir = os_1.default.homedir();
@@ -478,7 +479,7 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
478
479
  name: "aicodeswitch",
479
480
  base_url: `http://${clientHost}:${port}/codex`,
480
481
  wire_api: "responses",
481
- stream_max_retries: 3,
482
+ stream_max_retries: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5,
482
483
  stream_retry_backoff: "fixed"
483
484
  }
484
485
  }
@@ -697,7 +698,7 @@ const DEFAULT_OPENCODE_MODEL = 'claude-sonnet-4-20250514';
697
698
  * 的 /opencode/v1 端点(OpenAI Chat Completions 格式)。仅托管 provider.aicodeswitch
698
699
  * 段与 model/small_model/mcp 字段,其余用户配置(其它 provider、agent、command 等)保留。
699
700
  */
700
- const writeOpencodeConfig = (_dbManager_1, defaultModel_1, ...args_1) => __awaiter(void 0, [_dbManager_1, defaultModel_1, ...args_1], void 0, function* (_dbManager, defaultModel, options = {}) {
701
+ const writeOpencodeConfig = (_dbManager_1, defaultModel_1, maxRetries_1, ...args_1) => __awaiter(void 0, [_dbManager_1, defaultModel_1, maxRetries_1, ...args_1], void 0, function* (_dbManager, defaultModel, maxRetries, options = {}) {
701
702
  var _a;
702
703
  try {
703
704
  const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
@@ -748,7 +749,8 @@ const writeOpencodeConfig = (_dbManager_1, defaultModel_1, ...args_1) => __await
748
749
  name: 'AICodeSwitch',
749
750
  options: {
750
751
  baseURL: `http://${clientHost}:${port}/opencode/v1`,
751
- apiKey: 'api_key'
752
+ apiKey: 'api_key',
753
+ maxRetries: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5
752
754
  },
753
755
  models: {
754
756
  [model]: { name: model }
@@ -856,14 +858,14 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
856
858
  const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
857
859
  ? config.claudeEffortLevel
858
860
  : DEFAULT_CLAUDE_EFFORT_LEVEL;
859
- const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride);
861
+ const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, config.claudeMaxRetries);
860
862
  console.log(`[Startup Config Sync] Claude Code config ${claudeWritten ? 'written' : 'skipped'}`);
861
863
  const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
862
864
  ? config.codexModelReasoningEffort
863
865
  : DEFAULT_CODEX_REASONING_EFFORT;
864
- const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
866
+ const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, config.codexMaxRetries);
865
867
  console.log(`[Startup Config Sync] Codex config ${codexWritten ? 'written' : 'skipped'}`);
866
- const opencodeWritten = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel);
868
+ const opencodeWritten = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, config.opencodeMaxRetries);
867
869
  console.log(`[Startup Config Sync] OpenCode config ${opencodeWritten ? 'written' : 'skipped'}`);
868
870
  });
869
871
  const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
@@ -871,14 +873,14 @@ const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0,
871
873
  const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
872
874
  ? config.claudeEffortLevel
873
875
  : DEFAULT_CLAUDE_EFFORT_LEVEL;
874
- const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, { allowOverwriteRefresh: true });
876
+ const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, config.claudeMaxRetries, { allowOverwriteRefresh: true });
875
877
  console.log(`[Config Update Sync] Claude Code config ${claudeUpdated ? 'written' : 'skipped'}`);
876
878
  const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
877
879
  ? config.codexModelReasoningEffort
878
880
  : DEFAULT_CODEX_REASONING_EFFORT;
879
- const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
881
+ const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, config.codexMaxRetries, { allowOverwriteRefresh: true });
880
882
  console.log(`[Config Update Sync] Codex config ${codexUpdated ? 'written' : 'skipped'}`);
881
- const opencodeUpdated = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, { allowOverwriteRefresh: true });
883
+ const opencodeUpdated = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, config.opencodeMaxRetries, { allowOverwriteRefresh: true });
882
884
  console.log(`[Config Update Sync] OpenCode config ${opencodeUpdated ? 'written' : 'skipped'}`);
883
885
  });
884
886
  const getCentralSkillsDir = () => {
@@ -1317,7 +1319,7 @@ const listInstalledSkills = () => {
1317
1319
  const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
1318
1320
  updateProxyConfig(dbManager.getConfig());
1319
1321
  app.get('/health', (_req, res) => res.json({ status: 'ok' }));
1320
- // 数据就绪验证端点(供 Tauri 启动阶段确认后端完全可用)
1322
+ // 数据就绪验证端点(供桌面端 Electron 启动阶段确认后端完全可用)
1321
1323
  app.get('/api/ready', (_req, res) => {
1322
1324
  const vendors = dbManager.getVendors();
1323
1325
  const routes = dbManager.getRoutes();
@@ -2328,7 +2330,7 @@ ${instruction}
2328
2330
  : isClaudePermissionDefaultMode(appConfig.claudePermissionsDefaultMode)
2329
2331
  ? appConfig.claudePermissionsDefaultMode
2330
2332
  : DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
2331
- const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
2333
+ const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride, appConfig.claudeMaxRetries);
2332
2334
  applyWriteLocalRecords(proxyServer);
2333
2335
  res.json(result);
2334
2336
  })));
@@ -2344,7 +2346,7 @@ ${instruction}
2344
2346
  const enableMemories = requestedEnableMemories !== undefined
2345
2347
  ? !!requestedEnableMemories
2346
2348
  : !!appConfig.codexEnableMemories;
2347
- const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories);
2349
+ const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories, appConfig.codexMaxRetries);
2348
2350
  applyWriteLocalRecords(proxyServer);
2349
2351
  res.json(result);
2350
2352
  })));
@@ -2353,7 +2355,7 @@ ${instruction}
2353
2355
  const appConfig = dbManager.getConfig();
2354
2356
  const requestedModel = typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a.defaultModel) === 'string' ? req.body.defaultModel : undefined;
2355
2357
  const defaultModel = requestedModel || appConfig.opencodeDefaultModel;
2356
- const result = yield writeOpencodeConfig(dbManager, defaultModel);
2358
+ const result = yield writeOpencodeConfig(dbManager, defaultModel, appConfig.opencodeMaxRetries);
2357
2359
  applyWriteLocalRecords(proxyServer);
2358
2360
  res.json(result);
2359
2361
  })));
@@ -3804,6 +3806,26 @@ ${instruction}
3804
3806
  });
3805
3807
  // listen 就绪标志:区分"启动阶段"与"运行阶段",启动期致命异常应让进程退出
3806
3808
  let listenReady = false;
3809
+ /**
3810
+ * 是否以「内嵌进程」模式运行(例如被 Electron 主进程直接 require 并调用 start)。
3811
+ * 该模式下:
3812
+ * - shutdown 流程结束后不调用 process.exit,把退出时机交还给宿主(Electron)
3813
+ * - 模块被 require 时不自动执行 start(),由宿主显式调用导出的 start
3814
+ */
3815
+ const IN_PROCESS = process.env.AIC_IN_PROCESS === '1';
3816
+ // 保存当前服务实例的优雅关闭函数,供宿主(Electron 主进程)在退出前显式调用。
3817
+ let _gracefulShutdown = null;
3818
+ /**
3819
+ * 供宿主进程调用的优雅关闭入口。
3820
+ * 仅在 start() 成功注册 shutdown 后可用;调用后会恢复工具配置、关闭 DB / 日志、释放端口,
3821
+ * 内嵌模式下不会触发 process.exit。
3822
+ */
3823
+ const gracefulShutdown = (signal = 'HOST_QUIT') => {
3824
+ if (_gracefulShutdown)
3825
+ return _gracefulShutdown(signal);
3826
+ return Promise.resolve();
3827
+ };
3828
+ exports.gracefulShutdown = gracefulShutdown;
3807
3829
  const start = () => __awaiter(void 0, void 0, void 0, function* () {
3808
3830
  fs_1.default.mkdirSync(dataDir, { recursive: true });
3809
3831
  // 自动检测数据库类型并执行迁移(如果需要)
@@ -4021,25 +4043,31 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
4021
4043
  })
4022
4044
  ]);
4023
4045
  console.log('Server stopped.');
4024
- process.exit(0);
4046
+ // 内嵌进程模式下不主动退出,交由宿主(Electron)控制进程生命周期
4047
+ if (!IN_PROCESS) {
4048
+ process.exit(0);
4049
+ }
4025
4050
  }))();
4026
4051
  return shutdownPromise;
4027
4052
  });
4053
+ // 注册到模块级句柄,供宿主在退出前显式触发完整关闭流程
4054
+ _gracefulShutdown = shutdown;
4028
4055
  process.on('SIGINT', () => { void shutdown('SIGINT'); });
4029
4056
  process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
4030
- // 优雅关闭端点(供 Tauri 等外部调用者触发服务端完整清理流程)
4057
+ // 优雅关闭端点(供 Electron 等外部调用者触发服务端完整清理流程)
4031
4058
  // 放在 shutdown 定义之后注册,确保闭包可引用
4032
4059
  app.post('/api/shutdown', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
4033
4060
  res.json({ success: true });
4034
4061
  setImmediate(() => { void shutdown('HTTP_SHUTDOWN'); });
4035
4062
  })));
4036
4063
  });
4064
+ exports.start = start;
4037
4065
  // 全局未捕获异常处理 - 防止服务崩溃
4038
4066
  process.on('uncaughtException', (error) => {
4039
4067
  console.error('[Uncaught Exception] 服务遇到未捕获的异常:', error);
4040
4068
  console.error('[Uncaught Exception] 堆栈信息:', error.stack);
4041
4069
  // 启动阶段(listen 之前)的异常通常是致命的(依赖加载失败、初始化崩溃等),
4042
- // 静默吞掉会导致"进程在但不 listen",Tauri 只能干等超时;此时退出让上层重新探测/诊断。
4070
+ // 静默吞掉会导致"进程在但不 listen",桌面端只能干等超时;此时退出让上层重新探测/诊断。
4043
4071
  if (!listenReady) {
4044
4072
  console.error('[Uncaught Exception] 发生在服务监听之前,退出进程');
4045
4073
  process.exit(1);
@@ -4052,7 +4080,11 @@ process.on('unhandledRejection', (reason) => {
4052
4080
  process.exit(1);
4053
4081
  }
4054
4082
  });
4055
- start().catch((error) => {
4056
- console.error('Failed to start server:', error);
4057
- process.exit(1);
4058
- });
4083
+ // 仅在被直接运行时(如 aicos start node dist/server/main.js)自动启动;
4084
+ // 被 Electron 主进程 require 时(require.main !== module IN_PROCESS)由宿主显式调用 start()
4085
+ if (!IN_PROCESS && require.main === module) {
4086
+ (0, exports.start)().catch((error) => {
4087
+ console.error('Failed to start server:', error);
4088
+ process.exit(1);
4089
+ });
4090
+ }