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 +4 -1
- package/bin/restore.js +67 -4
- package/bin/utils/managed-fields.js +12 -0
- package/dist/server/agent-map/activity-extractor.js +8 -0
- package/dist/server/agent-map/agent-map-service.js +10 -15
- package/dist/server/coding-plan.js +3 -0
- package/dist/server/config-managed-fields.js +20 -1
- package/dist/server/config-metadata.js +78 -1
- package/dist/server/conversions/index.js +6 -4
- package/dist/server/conversions/server-tool/mapper.js +143 -30
- package/dist/server/fs-database.js +26 -1
- package/dist/server/main.js +455 -25
- package/dist/server/original-config-reader.js +71 -1
- package/dist/server/proxy-server.js +55 -8
- package/dist/server/session-migration.js +1 -0
- package/dist/ui/assets/index-BOeOgctP.js +1190 -0
- package/dist/ui/assets/index-DqRzbMIU.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +69 -8
- package/dist/ui/assets/index-CQVhBlBB.css +0 -1
- package/dist/ui/assets/index-QZBX5HHX.js +0 -1186
- package/scripts/dev.js +0 -283
package/README.md
CHANGED
|
@@ -39,7 +39,10 @@ AI Code Switch 是帮助你在本地管理 AI 编程工具接入大模型的工
|
|
|
39
39
|
|
|
40
40
|
## 桌面客户端
|
|
41
41
|
|
|
42
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
101
|
+
* 扫描 body.messages 中所有 role 的内容块,以及顶层 body.tools。
|
|
102
|
+
* 返回浅拷贝;无改动时原样返回原 body。
|
|
20
103
|
*/
|
|
21
|
-
function
|
|
22
|
-
if (!
|
|
104
|
+
function sanitizeServerToolArtifacts(body) {
|
|
105
|
+
if (!body || typeof body !== 'object')
|
|
23
106
|
return body;
|
|
24
|
-
}
|
|
25
107
|
let modified = false;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
140
|
+
return msg;
|
|
38
141
|
});
|
|
39
|
-
if (
|
|
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
|
-
|
|
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
|
|
154
|
+
return modified ? newBody : body;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @deprecated 别名,等价于 {@link sanitizeServerToolArtifacts}。
|
|
158
|
+
* 保留旧名以兼容既有导入;新代码请使用 sanitizeServerToolArtifacts。
|
|
159
|
+
*/
|
|
160
|
+
function convertServerToolUseToToolUse(body) {
|
|
161
|
+
return sanitizeServerToolArtifacts(body);
|
|
49
162
|
}
|