aicodeswitch 6.0.0 → 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
 
package/bin/restore.js CHANGED
@@ -5,7 +5,7 @@ const chalk = require('chalk');
5
5
  const boxen = require('boxen');
6
6
  const ora = require('ora');
7
7
  const { parseToml, stringifyToml, mergeJsonSettings, mergeTomlSettings, atomicWriteFile } = require('./utils/config-helpers');
8
- const { CLAUDE_SETTINGS_MANAGED_FIELDS, CLAUDE_JSON_MANAGED_FIELDS, CODEX_CONFIG_MANAGED_FIELDS, CODEX_AUTH_MANAGED_FIELDS } = require('./utils/managed-fields');
8
+ const { CLAUDE_SETTINGS_MANAGED_FIELDS, CLAUDE_JSON_MANAGED_FIELDS, CODEX_CONFIG_MANAGED_FIELDS, CODEX_AUTH_MANAGED_FIELDS, OPENCODE_CONFIG_MANAGED_FIELDS } = require('./utils/managed-fields');
9
9
  const { isServerRunning, getServerInfo } = require('./utils/get-server');
10
10
  const { findPidByPort } = require('./utils/port-utils');
11
11
 
@@ -240,10 +240,57 @@ const restoreCodexConfig = () => {
240
240
  return results;
241
241
  };
242
242
 
243
+ // 恢复 OpenCode 配置(使用智能合并)
244
+ const restoreOpencodeConfig = () => {
245
+ const results = {
246
+ restored: [],
247
+ notFound: [],
248
+ errors: []
249
+ };
250
+
251
+ try {
252
+ const homeDir = os.homedir();
253
+ const opencodeConfigPath = path.join(homeDir, '.config', 'opencode', 'opencode.json');
254
+ const opencodeConfigBakPath = `${opencodeConfigPath}.aicodeswitch_backup`;
255
+
256
+ if (fs.existsSync(opencodeConfigBakPath)) {
257
+ try {
258
+ const backupConfig = JSON.parse(fs.readFileSync(opencodeConfigBakPath, 'utf-8'));
259
+ let currentConfig = {};
260
+ if (fs.existsSync(opencodeConfigPath)) {
261
+ try {
262
+ currentConfig = JSON.parse(fs.readFileSync(opencodeConfigPath, 'utf-8'));
263
+ } catch (e) {
264
+ // 忽略解析错误
265
+ }
266
+ }
267
+
268
+ const mergedConfig = mergeJsonSettings(
269
+ backupConfig,
270
+ currentConfig,
271
+ OPENCODE_CONFIG_MANAGED_FIELDS
272
+ );
273
+
274
+ atomicWriteFile(opencodeConfigPath, JSON.stringify(mergedConfig, null, 2));
275
+ fs.unlinkSync(opencodeConfigBakPath);
276
+ results.restored.push('opencode.json');
277
+ } catch (error) {
278
+ results.errors.push({ file: 'opencode.json', error: error.message });
279
+ }
280
+ } else {
281
+ results.notFound.push('opencode.json.aicodeswitch_backup');
282
+ }
283
+ } catch (err) {
284
+ results.errors.push({ file: 'opencode.json', error: err.message });
285
+ }
286
+
287
+ return results;
288
+ };
289
+
243
290
  // 显示恢复结果
244
291
  const showRestoreResult = (target, results) => {
245
- const targetName = target === 'claude-code' ? 'Claude Code' : 'Codex';
246
- const targetColor = target === 'claude-code' ? chalk.cyan : chalk.magenta;
292
+ const targetName = target === 'claude-code' ? 'Claude Code' : target === 'opencode' ? 'OpenCode' : 'Codex';
293
+ const targetColor = target === 'claude-code' ? chalk.cyan : target === 'opencode' ? chalk.blue : chalk.magenta;
247
294
 
248
295
  let message = targetColor.bold(`${targetName} Configuration Restore\n\n`);
249
296
 
@@ -276,6 +323,8 @@ const showRestoreResult = (target, results) => {
276
323
  message += chalk.yellow.bold('⚠️ Important:\n\n');
277
324
  if (target === 'claude-code') {
278
325
  message += chalk.white('Please restart ') + chalk.cyan.bold('Claude Code') + chalk.white(' to apply the restored configuration.\n');
326
+ } else if (target === 'opencode') {
327
+ message += chalk.white('Please restart ') + chalk.blue.bold('OpenCode') + chalk.white(' to apply the restored configuration.\n');
279
328
  } else {
280
329
  message += chalk.white('Please restart ') + chalk.magenta.bold('Codex') + chalk.white(' to apply the restored configuration.\n');
281
330
  }
@@ -307,7 +356,7 @@ const restore = async () => {
307
356
 
308
357
  console.log('\n');
309
358
 
310
- const validTargets = ['claude-code', 'codex', undefined];
359
+ const validTargets = ['claude-code', 'codex', 'opencode', undefined];
311
360
 
312
361
  if (target && !validTargets.includes(target)) {
313
362
  console.log(boxen(
@@ -316,6 +365,7 @@ const restore = async () => {
316
365
  chalk.white('Targets:\n') +
317
366
  chalk.white(' claude-code Restore Claude Code configuration\n') +
318
367
  chalk.white(' codex Restore Codex configuration\n') +
368
+ chalk.white(' opencode Restore OpenCode configuration\n') +
319
369
  chalk.white(' (no arg) Restore all configurations\n'),
320
370
  {
321
371
  padding: 1,
@@ -379,6 +429,19 @@ const restore = async () => {
379
429
  showRestoreResult('codex', codexResults);
380
430
  }
381
431
 
432
+ if (target === 'opencode' || !target) {
433
+ if (!target) console.log('');
434
+
435
+ const spinner = ora({
436
+ text: chalk.blue('Restoring OpenCode configuration...'),
437
+ color: 'blue'
438
+ }).start();
439
+
440
+ const opencodeResults = restoreOpencodeConfig();
441
+ spinner.succeed(chalk.green('OpenCode restore complete'));
442
+ showRestoreResult('opencode', opencodeResults);
443
+ }
444
+
382
445
  // 停用所有激活的路由
383
446
  console.log('');
384
447
  const routesSpinner = ora({
@@ -56,9 +56,21 @@ const CODEX_AUTH_MANAGED_FIELDS = [
56
56
  'OPENAI_API_KEY',
57
57
  ];
58
58
 
59
+ /**
60
+ * OpenCode opencode.json 管理字段列表
61
+ * 托管整个 provider.aicodeswitch 段与 model/small_model/mcp
62
+ */
63
+ const OPENCODE_CONFIG_MANAGED_FIELDS = [
64
+ 'provider.aicodeswitch',
65
+ 'model',
66
+ 'small_model',
67
+ 'mcp',
68
+ ];
69
+
59
70
  module.exports = {
60
71
  CLAUDE_SETTINGS_MANAGED_FIELDS,
61
72
  CLAUDE_JSON_MANAGED_FIELDS,
62
73
  CODEX_CONFIG_MANAGED_FIELDS,
63
74
  CODEX_AUTH_MANAGED_FIELDS,
75
+ OPENCODE_CONFIG_MANAGED_FIELDS,
64
76
  };
@@ -314,6 +314,14 @@ function detectTurnEnd(agent, downstream, responseBody) {
314
314
  return true;
315
315
  return null;
316
316
  }
317
+ if (agent === 'opencode') {
318
+ // OpenAI Chat Completions:tool_calls 仍在 → 继续;finish_reason=stop / [DONE] → 本轮结束
319
+ if (/"finish_reason"\s*:\s*"tool_calls"/.test(raw))
320
+ return false;
321
+ if (/"finish_reason"\s*:\s*"(stop|length|content_filter)"/.test(raw) || /\[DONE\]/.test(raw))
322
+ return true;
323
+ return null;
324
+ }
317
325
  // Claude:看 stop_reason(流式 message_delta 或非流式 JSON 都带该字段)
318
326
  const m = raw.match(/"stop_reason"\s*:\s*"([a-z_]+)"/);
319
327
  const stopReason = m ? m[1] : null;
@@ -599,7 +599,8 @@ class AgentMapService extends events_1.EventEmitter {
599
599
  // 异步解析本地会话元信息(项目路径 + 原始标题);仅 global 来源、且未解析过
600
600
  this.enrichSession(st);
601
601
  }
602
- /** 从本机 Claude/Codex 会话存储读取项目路径与原始标题并回填(access-key 不解析) */
602
+ /** 从本机 Claude/Codex 会话存储读取项目路径并回填(access-key 不解析)。
603
+ * 注意:不在此处覆盖标题——统一沿用 proxy 抽取的标题(与会话列表一致),避免两边标题分叉。 */
603
604
  enrichSession(st) {
604
605
  return __awaiter(this, void 0, void 0, function* () {
605
606
  if (st.source === 'access-key' || st.metaResolved)
@@ -607,18 +608,10 @@ class AgentMapService extends events_1.EventEmitter {
607
608
  st.metaResolved = true;
608
609
  try {
609
610
  const meta = yield (0, session_meta_1.resolveSessionMeta)(st.sessionId, st.agent);
610
- let changed = false;
611
611
  if (meta.projectPath && !st.projectPath) {
612
612
  st.projectPath = meta.projectPath;
613
- changed = true;
614
- }
615
- // 原始标题更标准,命中即覆盖日志截取的标题
616
- if (meta.title) {
617
- st.title = meta.title;
618
- changed = true;
619
- }
620
- if (changed)
621
613
  this.emitSession(st);
614
+ }
622
615
  }
623
616
  catch ( /* ignore */_a) { /* ignore */ }
624
617
  });
@@ -631,18 +624,16 @@ class AgentMapService extends events_1.EventEmitter {
631
624
  if (source === 'access-key' || source === 'unknown') {
632
625
  return { source, projectPath: st === null || st === void 0 ? void 0 : st.projectPath, title: st === null || st === void 0 ? void 0 : st.title };
633
626
  }
634
- // global:命中缓存或现解析
627
+ // global:命中缓存或现解析(仅项目路径;标题沿用 proxy 抽取值,与会话列表保持一致)
635
628
  const meta = yield (0, session_meta_1.resolveSessionMeta)(sessionId, (st === null || st === void 0 ? void 0 : st.agent) || 'claude-code');
636
629
  if (st) {
637
630
  if (meta.projectPath && !st.projectPath)
638
631
  st.projectPath = meta.projectPath;
639
- if (meta.title)
640
- st.title = meta.title;
641
632
  st.metaResolved = true;
642
633
  // legacy 会话(修复前累积、拆分从未持久化):点开详情时按日志回填输入/输出拆分
643
634
  this.backfillTokenSplit(st).catch(err => console.error('[AgentMap] backfillTokenSplit error:', err));
644
635
  }
645
- return { source, projectPath: meta.projectPath || (st === null || st === void 0 ? void 0 : st.projectPath), title: meta.title || (st === null || st === void 0 ? void 0 : st.title) };
636
+ return { source, projectPath: meta.projectPath || (st === null || st === void 0 ? void 0 : st.projectPath), title: st === null || st === void 0 ? void 0 : st.title };
646
637
  });
647
638
  }
648
639
  /**
@@ -736,6 +727,8 @@ class AgentMapService extends events_1.EventEmitter {
736
727
  return; // 本轮已调度
737
728
  if (st.lastStatusCode === 499)
738
729
  return; // 用户主动取消,不弹
730
+ if (st.thinkingInFlight > 0)
731
+ return; // 仍有思考请求在途,暂不弹结束通知
739
732
  st.notifiedForTurn = true;
740
733
  this.scheduleNotify(st, isError);
741
734
  }
@@ -808,7 +801,9 @@ class AgentMapService extends events_1.EventEmitter {
808
801
  }
809
802
  // 已开始产出:思考请求用更宽的 THINKING_SILENCE_MS,否则 SSE_SILENCE_MS(30s)
810
803
  const limit = args.thinkingInFlight > 0 ? this.thinkingSilenceMs : this.sseSilenceMs;
811
- const since = args.streamFirstChunkAt;
804
+ // 静默基准用「最近一次活动」(每个下游 chunk 经 heartbeat 刷新),而非首个 chunk 时刻:
805
+ // 否则任何总时长超过 30s 的流式响应都会被误判为停滞,在响应仍正常输出时弹出「任务已结束」通知。
806
+ const since = args.lastActivityAt;
812
807
  if (args.now - since <= limit) {
813
808
  return { status: 'active', reason: args.thinkingInFlight > 0 ? 'thinking' : 'streaming', notifyEligible: true };
814
809
  }
@@ -37,6 +37,9 @@ function isCodingToolRequest(body, format, headers) {
37
37
  // Codex: originator header
38
38
  if ((headers['originator'] || '').toLowerCase().includes('codex'))
39
39
  return { isCoding: true, reason: '' };
40
+ // OpenCode: user-agent 包含 "opencode"
41
+ if (ua.includes('opencode'))
42
+ return { isCoding: true, reason: '' };
40
43
  }
41
44
  // ── Layer 2: Claude Messages API ───────────────────────────────────
42
45
  if (format === 'claude' && Array.isArray(body === null || body === void 0 ? void 0 : body.messages)) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getManagedFields = exports.CODEX_AUTH_MANAGED_FIELDS = exports.CODEX_CONFIG_MANAGED_FIELDS = exports.CLAUDE_JSON_MANAGED_FIELDS = exports.CLAUDE_SETTINGS_MANAGED_FIELDS = void 0;
3
+ exports.getManagedFields = exports.OPENCODE_CONFIG_MANAGED_FIELDS = exports.CODEX_AUTH_MANAGED_FIELDS = exports.CODEX_CONFIG_MANAGED_FIELDS = exports.CLAUDE_JSON_MANAGED_FIELDS = exports.CLAUDE_SETTINGS_MANAGED_FIELDS = void 0;
4
4
  /**
5
5
  * Claude Code settings.json 管理字段定义
6
6
  */
@@ -50,6 +50,20 @@ exports.CODEX_CONFIG_MANAGED_FIELDS = [
50
50
  exports.CODEX_AUTH_MANAGED_FIELDS = [
51
51
  { path: ['OPENAI_API_KEY'] },
52
52
  ];
53
+ /**
54
+ * OpenCode opencode.json 管理字段定义
55
+ *
56
+ * OpenCode 配置为纯 JSON,使用自定义 provider 指向本代理。
57
+ * 托管整个 `provider.aicodeswitch` section(npm/name/options/models)
58
+ * 以及 `model` / `small_model` 两个模型选择字段。其余字段(其它 provider、
59
+ * agent、command、mcp、theme 等)一律保留给用户。
60
+ */
61
+ exports.OPENCODE_CONFIG_MANAGED_FIELDS = [
62
+ { path: ['provider', 'aicodeswitch'], isSection: true },
63
+ { path: ['model'] },
64
+ { path: ['small_model'], optional: true },
65
+ { path: ['mcp'], isSection: true, optional: true },
66
+ ];
53
67
  /**
54
68
  * 根据配置类型和文件路径获取管理字段列表
55
69
  */
@@ -70,6 +84,11 @@ const getManagedFields = (configType, filePath) => {
70
84
  return exports.CODEX_AUTH_MANAGED_FIELDS;
71
85
  }
72
86
  }
87
+ else if (configType === 'opencode') {
88
+ if (filePath.endsWith('opencode.json')) {
89
+ return exports.OPENCODE_CONFIG_MANAGED_FIELDS;
90
+ }
91
+ }
73
92
  return [];
74
93
  };
75
94
  exports.getManagedFields = getManagedFields;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.cleanupInvalidMetadata = exports.checkCodexConfigStatus = exports.checkClaudeConfigStatus = exports.deleteMetadata = exports.loadMetadata = exports.saveMetadata = void 0;
6
+ exports.cleanupInvalidMetadata = exports.checkOpencodeConfigStatus = exports.getOpencodeConfigPath = exports.checkCodexConfigStatus = exports.checkClaudeConfigStatus = exports.deleteMetadata = exports.loadMetadata = exports.saveMetadata = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
@@ -270,6 +270,83 @@ const checkCodexConfigStatus = () => {
270
270
  };
271
271
  };
272
272
  exports.checkCodexConfigStatus = checkCodexConfigStatus;
273
+ /**
274
+ * 检查 OpenCode 配置文件是否包含我们的代理特征
275
+ */
276
+ const isOpencodeProxyConfig = (filePath) => {
277
+ var _a, _b;
278
+ try {
279
+ if (!fs_1.default.existsSync(filePath)) {
280
+ return false;
281
+ }
282
+ const content = fs_1.default.readFileSync(filePath, 'utf-8');
283
+ const config = JSON.parse(content);
284
+ // 检查是否包含我们的自定义 provider(provider.aicodeswitch)
285
+ const provider = (_a = config === null || config === void 0 ? void 0 : config.provider) === null || _a === void 0 ? void 0 : _a.aicodeswitch;
286
+ if (provider && typeof provider === 'object') {
287
+ const baseUrl = (_b = provider.options) === null || _b === void 0 ? void 0 : _b.baseURL;
288
+ // baseURL 指向本代理的 /opencode 路径
289
+ if (baseUrl && typeof baseUrl === 'string' && /\/opencode(\/|$)/.test(baseUrl)) {
290
+ return true;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+ catch (error) {
296
+ console.error(`Failed to check OpenCode config:`, error);
297
+ return false;
298
+ }
299
+ };
300
+ /**
301
+ * OpenCode 配置文件路径(~/.config/opencode/opencode.json)
302
+ */
303
+ const getOpencodeConfigPath = () => {
304
+ const homeDir = os_1.default.homedir();
305
+ return path_1.default.join(homeDir, '.config', 'opencode', 'opencode.json');
306
+ };
307
+ exports.getOpencodeConfigPath = getOpencodeConfigPath;
308
+ /**
309
+ * 检查 OpenCode 配置状态
310
+ */
311
+ const checkOpencodeConfigStatus = () => {
312
+ var _a;
313
+ const configPath = (0, exports.getOpencodeConfigPath)();
314
+ const configBakPath = `${configPath}.aicodeswitch_backup`;
315
+ // 检查备份文件是否存在
316
+ const hasBackup = fs_1.default.existsSync(configBakPath);
317
+ // 尝试加载元数据
318
+ const metadata = (0, exports.loadMetadata)('opencode');
319
+ if (metadata) {
320
+ const currentHash = calculateFileHash(configPath);
321
+ const isProxyConfig = isOpencodeProxyConfig(configPath);
322
+ let isModified = false;
323
+ if (currentHash && ((_a = metadata.files[0]) === null || _a === void 0 ? void 0 : _a.currentHash)) {
324
+ isModified = currentHash !== metadata.files[0].currentHash;
325
+ }
326
+ const isOverwritten = isProxyConfig;
327
+ return {
328
+ isOverwritten,
329
+ isModified,
330
+ hasBackup,
331
+ metadata
332
+ };
333
+ }
334
+ if (hasBackup) {
335
+ const isProxyConfig = isOpencodeProxyConfig(configPath);
336
+ return {
337
+ isOverwritten: isProxyConfig,
338
+ isModified: false,
339
+ hasBackup
340
+ };
341
+ }
342
+ const isProxyConfig = isOpencodeProxyConfig(configPath);
343
+ return {
344
+ isOverwritten: isProxyConfig,
345
+ isModified: false,
346
+ hasBackup: false
347
+ };
348
+ };
349
+ exports.checkOpencodeConfigStatus = checkOpencodeConfigStatus;
273
350
  /**
274
351
  * 清理无效的元数据
275
352
  * 当备份文件不存在但元数据存在时,说明状态不一致,需要清理
@@ -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
  }