aicodeswitch 6.0.0 → 6.0.1
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/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/fs-database.js +4 -1
- package/dist/server/main.js +405 -7
- 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-BeM4AIzn.js +1187 -0
- package/dist/ui/assets/index-DqRzbMIU.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/ui/assets/index-CQVhBlBB.css +0 -1
- package/dist/ui/assets/index-QZBX5HHX.js +0 -1186
package/dist/server/main.js
CHANGED
|
@@ -183,6 +183,30 @@ function applyWriteLocalRecords(proxyServer) {
|
|
|
183
183
|
auth.OPENAI_API_KEY = key.apiKey;
|
|
184
184
|
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
185
185
|
}
|
|
186
|
+
else if (target === 'opencode') {
|
|
187
|
+
// 将真实 Key 写入 opencode.json 的 provider.aicodeswitch.options.apiKey
|
|
188
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
189
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
190
|
+
if (!fs_1.default.existsSync(opencodeDir)) {
|
|
191
|
+
fs_1.default.mkdirSync(opencodeDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
let oc = {};
|
|
194
|
+
if (fs_1.default.existsSync(opencodeConfigPath)) {
|
|
195
|
+
try {
|
|
196
|
+
oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
197
|
+
}
|
|
198
|
+
catch ( /* ignore */_c) { /* ignore */ }
|
|
199
|
+
}
|
|
200
|
+
if (!oc.provider)
|
|
201
|
+
oc.provider = {};
|
|
202
|
+
if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
|
|
203
|
+
oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
|
|
204
|
+
}
|
|
205
|
+
if (!oc.provider.aicodeswitch.options)
|
|
206
|
+
oc.provider.aicodeswitch.options = {};
|
|
207
|
+
oc.provider.aicodeswitch.options.apiKey = key.apiKey;
|
|
208
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
|
|
209
|
+
}
|
|
186
210
|
}
|
|
187
211
|
catch (error) {
|
|
188
212
|
console.error(`[WriteLocal] Failed to apply key ${record.accessKeyId} to ${target}:`, error);
|
|
@@ -662,6 +686,142 @@ const restoreCodexConfig = () => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
662
686
|
return false;
|
|
663
687
|
}
|
|
664
688
|
});
|
|
689
|
+
/**
|
|
690
|
+
* 默认 OpenCode 模型(当用户未配置 opencodeDefaultModel 时使用)
|
|
691
|
+
*/
|
|
692
|
+
const DEFAULT_OPENCODE_MODEL = 'claude-sonnet-4-20250514';
|
|
693
|
+
/**
|
|
694
|
+
* 写入 OpenCode 配置(~/.config/opencode/opencode.json)
|
|
695
|
+
*
|
|
696
|
+
* 注入一个自定义 provider `aicodeswitch`,经 @ai-sdk/openai-compatible 指向本代理
|
|
697
|
+
* 的 /opencode/v1 端点(OpenAI Chat Completions 格式)。仅托管 provider.aicodeswitch
|
|
698
|
+
* 段与 model/small_model/mcp 字段,其余用户配置(其它 provider、agent、command 等)保留。
|
|
699
|
+
*/
|
|
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
|
+
var _a;
|
|
702
|
+
try {
|
|
703
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
704
|
+
const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
705
|
+
const configBakPath = `${configPath}.aicodeswitch_backup`;
|
|
706
|
+
const configDir = path_1.default.dirname(configPath);
|
|
707
|
+
const configStatus = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
708
|
+
const isRuntimeRefresh = options.allowOverwriteRefresh === true && configStatus.isOverwritten;
|
|
709
|
+
if (configStatus.isOverwritten && !isRuntimeRefresh) {
|
|
710
|
+
console.error('OpenCode config has already been overwritten. Please restore the original config first.');
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
let originalHash = isRuntimeRefresh
|
|
714
|
+
? (_a = configStatus.metadata) === null || _a === void 0 ? void 0 : _a.originalHash
|
|
715
|
+
: undefined;
|
|
716
|
+
if (!isRuntimeRefresh) {
|
|
717
|
+
if (!fs_1.default.existsSync(configBakPath)) {
|
|
718
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
719
|
+
originalHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
|
|
720
|
+
fs_1.default.renameSync(configPath, configBakPath);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
console.log('OpenCode backup file already exists, skipping backup step');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
728
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
729
|
+
}
|
|
730
|
+
// 读取当前配置(保留用户其它 provider/agent/command 等)
|
|
731
|
+
let currentConfig = {};
|
|
732
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
733
|
+
try {
|
|
734
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
console.warn('Failed to parse current opencode.json, using empty object:', error);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const model = (defaultModel && typeof defaultModel === 'string' && defaultModel.trim())
|
|
741
|
+
? defaultModel.trim()
|
|
742
|
+
: DEFAULT_OPENCODE_MODEL;
|
|
743
|
+
// 构建代理配置
|
|
744
|
+
const proxyConfig = {
|
|
745
|
+
provider: {
|
|
746
|
+
aicodeswitch: {
|
|
747
|
+
npm: '@ai-sdk/openai-compatible',
|
|
748
|
+
name: 'AICodeSwitch',
|
|
749
|
+
options: {
|
|
750
|
+
baseURL: `http://${clientHost}:${port}/opencode/v1`,
|
|
751
|
+
apiKey: 'api_key'
|
|
752
|
+
},
|
|
753
|
+
models: {
|
|
754
|
+
[model]: { name: model }
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
model: `aicodeswitch/${model}`
|
|
759
|
+
};
|
|
760
|
+
const mergedConfig = (0, config_merge_1.mergeJsonConfig)(proxyConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
|
|
761
|
+
(0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
|
|
762
|
+
// 保存元数据
|
|
763
|
+
const currentHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
|
|
764
|
+
const metadata = {
|
|
765
|
+
configType: 'opencode',
|
|
766
|
+
timestamp: Date.now(),
|
|
767
|
+
originalHash,
|
|
768
|
+
proxyMarker: `http://${host}:${port}/opencode`,
|
|
769
|
+
files: [
|
|
770
|
+
{
|
|
771
|
+
originalPath: configPath,
|
|
772
|
+
backupPath: configBakPath,
|
|
773
|
+
currentHash
|
|
774
|
+
}
|
|
775
|
+
]
|
|
776
|
+
};
|
|
777
|
+
(0, config_metadata_1.saveMetadata)(metadata);
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
console.error('Failed to write OpenCode config file:', error);
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
const restoreOpencodeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
786
|
+
try {
|
|
787
|
+
const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
788
|
+
const configBakPath = `${configPath}.aicodeswitch_backup`;
|
|
789
|
+
let restoredAnyFile = false;
|
|
790
|
+
if (fs_1.default.existsSync(configBakPath)) {
|
|
791
|
+
const backupConfig = JSON.parse(fs_1.default.readFileSync(configBakPath, 'utf-8'));
|
|
792
|
+
let currentConfig = {};
|
|
793
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
794
|
+
try {
|
|
795
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
console.warn('Failed to parse current opencode.json during restore, using empty object:', error);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
// 备份作为基础,合并当前的非管理字段
|
|
802
|
+
const mergedConfig = (0, config_merge_1.mergeJsonConfig)(backupConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
|
|
803
|
+
(0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
|
|
804
|
+
fs_1.default.unlinkSync(configBakPath);
|
|
805
|
+
restoredAnyFile = true;
|
|
806
|
+
}
|
|
807
|
+
(0, config_metadata_1.deleteMetadata)('opencode');
|
|
808
|
+
return restoredAnyFile;
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
console.error('Failed to restore OpenCode config file:', error);
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
const checkOpencodeBackupExists = () => {
|
|
816
|
+
try {
|
|
817
|
+
const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
818
|
+
return status.hasBackup;
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
console.error('Failed to check OpenCode backup:', error);
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
};
|
|
665
825
|
const checkClaudeBackupExists = () => {
|
|
666
826
|
try {
|
|
667
827
|
// 清理可能的无效元数据
|
|
@@ -703,6 +863,8 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
|
|
|
703
863
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
704
864
|
const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
|
|
705
865
|
console.log(`[Startup Config Sync] Codex config ${codexWritten ? 'written' : 'skipped'}`);
|
|
866
|
+
const opencodeWritten = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel);
|
|
867
|
+
console.log(`[Startup Config Sync] OpenCode config ${opencodeWritten ? 'written' : 'skipped'}`);
|
|
706
868
|
});
|
|
707
869
|
const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
|
|
708
870
|
const config = dbManager.getConfig();
|
|
@@ -716,6 +878,8 @@ const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0,
|
|
|
716
878
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
717
879
|
const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
|
|
718
880
|
console.log(`[Config Update Sync] Codex config ${codexUpdated ? 'written' : 'skipped'}`);
|
|
881
|
+
const opencodeUpdated = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, { allowOverwriteRefresh: true });
|
|
882
|
+
console.log(`[Config Update Sync] OpenCode config ${opencodeUpdated ? 'written' : 'skipped'}`);
|
|
719
883
|
});
|
|
720
884
|
const getCentralSkillsDir = () => {
|
|
721
885
|
return path_1.default.join(os_1.default.homedir(), '.aicodeswitch', 'skills');
|
|
@@ -734,12 +898,20 @@ function getSkillDirByName(name) {
|
|
|
734
898
|
return path_1.default.join(centralDir, sanitizedName);
|
|
735
899
|
}
|
|
736
900
|
const getSkillSymlinkPath = (skillId, targetType) => {
|
|
901
|
+
if (targetType === 'opencode') {
|
|
902
|
+
// OpenCode 没有 skills 目录,映射为全局 command:~/.config/opencode/commands/<skillId>.md
|
|
903
|
+
return path_1.default.join(os_1.default.homedir(), '.config', 'opencode', 'commands', `${skillId}.md`);
|
|
904
|
+
}
|
|
737
905
|
const baseDir = targetType === 'claude-code' ? '.claude' : '.codex';
|
|
738
906
|
return path_1.default.join(os_1.default.homedir(), baseDir, 'skills', skillId);
|
|
739
907
|
};
|
|
740
908
|
function isSkillSymlinkExists(skillId, targetType) {
|
|
741
909
|
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
742
910
|
try {
|
|
911
|
+
if (targetType === 'opencode') {
|
|
912
|
+
// OpenCode 是普通文件,不是 symlink
|
|
913
|
+
return fs_1.default.existsSync(symlinkPath);
|
|
914
|
+
}
|
|
743
915
|
const stats = fs_1.default.lstatSync(symlinkPath);
|
|
744
916
|
return stats.isSymbolicLink();
|
|
745
917
|
}
|
|
@@ -747,15 +919,77 @@ function isSkillSymlinkExists(skillId, targetType) {
|
|
|
747
919
|
return false;
|
|
748
920
|
}
|
|
749
921
|
}
|
|
922
|
+
/**
|
|
923
|
+
* 从 SKILL.md 内容中剥离前导 YAML frontmatter,返回 { description?, body }
|
|
924
|
+
* 没有 frontmatter 时,description 为 undefined,body 为原文。
|
|
925
|
+
*/
|
|
926
|
+
function parseSkillMd(skillMdContent) {
|
|
927
|
+
const trimmed = skillMdContent.replace(/^/, '');
|
|
928
|
+
const fmMatch = trimmed.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
929
|
+
if (!fmMatch) {
|
|
930
|
+
return { body: trimmed };
|
|
931
|
+
}
|
|
932
|
+
const frontmatter = fmMatch[1];
|
|
933
|
+
const body = fmMatch[2];
|
|
934
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
935
|
+
return {
|
|
936
|
+
description: descMatch ? descMatch[1].replace(/^["']|["']$/g, '').trim() : undefined,
|
|
937
|
+
body: body.trim(),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* 为 OpenCode 生成 command markdown 文件内容。
|
|
942
|
+
* frontmatter 用 description(OpenCode 在 TUI 展示),正文用 SKILL.md 的 body。
|
|
943
|
+
*/
|
|
944
|
+
function buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription) {
|
|
945
|
+
let description = fallbackDescription || '';
|
|
946
|
+
let body = '';
|
|
947
|
+
const skillMdPath = path_1.default.join(skillDir, 'SKILL.md');
|
|
948
|
+
if (fs_1.default.existsSync(skillMdPath)) {
|
|
949
|
+
try {
|
|
950
|
+
const parsed = parseSkillMd(fs_1.default.readFileSync(skillMdPath, 'utf-8'));
|
|
951
|
+
body = parsed.body || '';
|
|
952
|
+
if (!description && parsed.description) {
|
|
953
|
+
description = parsed.description;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
957
|
+
}
|
|
958
|
+
if (!description) {
|
|
959
|
+
description = skillId;
|
|
960
|
+
}
|
|
961
|
+
const bodySection = body ? `\n${body}\n` : '\n';
|
|
962
|
+
return `---\ndescription: ${description.replace(/\n/g, ' ')}\nagent: build\n---\n${bodySection}`;
|
|
963
|
+
}
|
|
750
964
|
function createSkillSymlink(skillId, targetType) {
|
|
751
965
|
return __awaiter(this, void 0, void 0, function* () {
|
|
966
|
+
var _a;
|
|
752
967
|
try {
|
|
753
968
|
const centralDir = getCentralSkillsDir();
|
|
754
969
|
const skillDir = path_1.default.join(centralDir, skillId);
|
|
755
|
-
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
756
970
|
if (!fs_1.default.existsSync(skillDir)) {
|
|
757
971
|
return { success: false, error: 'Skill目录不存在' };
|
|
758
972
|
}
|
|
973
|
+
// OpenCode:生成 command markdown 写入 ~/.config/opencode/commands/<skillId>.md
|
|
974
|
+
if (targetType === 'opencode') {
|
|
975
|
+
const commandPath = getSkillSymlinkPath(skillId, targetType);
|
|
976
|
+
const commandDir = path_1.default.dirname(commandPath);
|
|
977
|
+
if (!fs_1.default.existsSync(commandDir)) {
|
|
978
|
+
fs_1.default.mkdirSync(commandDir, { recursive: true });
|
|
979
|
+
}
|
|
980
|
+
let fallbackDescription;
|
|
981
|
+
const metadataPath = path_1.default.join(skillDir, 'skill.json');
|
|
982
|
+
if (fs_1.default.existsSync(metadataPath)) {
|
|
983
|
+
try {
|
|
984
|
+
fallbackDescription = (_a = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'))) === null || _a === void 0 ? void 0 : _a.description;
|
|
985
|
+
}
|
|
986
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
987
|
+
}
|
|
988
|
+
const content = buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription);
|
|
989
|
+
(0, config_merge_1.atomicWriteFile)(commandPath, content);
|
|
990
|
+
return { success: true };
|
|
991
|
+
}
|
|
992
|
+
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
759
993
|
const targetBaseDir = path_1.default.dirname(symlinkPath);
|
|
760
994
|
if (!fs_1.default.existsSync(targetBaseDir)) {
|
|
761
995
|
fs_1.default.mkdirSync(targetBaseDir, { recursive: true });
|
|
@@ -786,6 +1020,13 @@ function removeSkillSymlink(skillId, targetType) {
|
|
|
786
1020
|
return __awaiter(this, void 0, void 0, function* () {
|
|
787
1021
|
try {
|
|
788
1022
|
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
1023
|
+
if (targetType === 'opencode') {
|
|
1024
|
+
// OpenCode:删除 command 文件
|
|
1025
|
+
if (fs_1.default.existsSync(symlinkPath)) {
|
|
1026
|
+
fs_1.default.unlinkSync(symlinkPath);
|
|
1027
|
+
}
|
|
1028
|
+
return { success: true };
|
|
1029
|
+
}
|
|
789
1030
|
if (!fs_1.default.existsSync(symlinkPath)) {
|
|
790
1031
|
return { success: true };
|
|
791
1032
|
}
|
|
@@ -1044,7 +1285,7 @@ const listInstalledSkills = () => {
|
|
|
1044
1285
|
try {
|
|
1045
1286
|
const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
|
|
1046
1287
|
const enabledTargets = [];
|
|
1047
|
-
['claude-code', 'codex'].forEach((targetType) => {
|
|
1288
|
+
['claude-code', 'codex', 'opencode'].forEach((targetType) => {
|
|
1048
1289
|
if (isSkillSymlinkExists(skillId, targetType)) {
|
|
1049
1290
|
enabledTargets.push(targetType);
|
|
1050
1291
|
}
|
|
@@ -1196,7 +1437,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1196
1437
|
if (!tool || !routeId) {
|
|
1197
1438
|
return res.status(400).json({ error: 'tool and routeId are required' });
|
|
1198
1439
|
}
|
|
1199
|
-
if (tool !== 'claude-code' && tool !== 'codex') {
|
|
1440
|
+
if (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode') {
|
|
1200
1441
|
return res.status(400).json({ error: 'Invalid tool name' });
|
|
1201
1442
|
}
|
|
1202
1443
|
const route = dbManager.getRoute(routeId);
|
|
@@ -1217,7 +1458,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1217
1458
|
})));
|
|
1218
1459
|
app.post('/api/tool-bindings/deactivate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1219
1460
|
const { tool } = req.body;
|
|
1220
|
-
if (!tool || (tool !== 'claude-code' && tool !== 'codex')) {
|
|
1461
|
+
if (!tool || (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode')) {
|
|
1221
1462
|
return res.status(400).json({ error: 'Invalid tool name' });
|
|
1222
1463
|
}
|
|
1223
1464
|
const result = yield dbManager.deactivateToolRoute(tool);
|
|
@@ -2005,7 +2246,7 @@ ${instruction}
|
|
|
2005
2246
|
app.post('/api/skills/:skillId/enable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2006
2247
|
const { skillId } = req.params;
|
|
2007
2248
|
const { targetType } = req.body;
|
|
2008
|
-
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
2249
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
|
|
2009
2250
|
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
2010
2251
|
return;
|
|
2011
2252
|
}
|
|
@@ -2036,7 +2277,7 @@ ${instruction}
|
|
|
2036
2277
|
app.post('/api/skills/:skillId/disable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2037
2278
|
const { skillId } = req.params;
|
|
2038
2279
|
const { targetType } = req.body;
|
|
2039
|
-
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
2280
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
|
|
2040
2281
|
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
2041
2282
|
return;
|
|
2042
2283
|
}
|
|
@@ -2065,7 +2306,7 @@ ${instruction}
|
|
|
2065
2306
|
res.status(404).json({ success: false, error: 'Skill不存在' });
|
|
2066
2307
|
return;
|
|
2067
2308
|
}
|
|
2068
|
-
['claude-code', 'codex'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2309
|
+
['claude-code', 'codex', 'opencode'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2069
2310
|
yield removeSkillSymlink(skillId, targetType);
|
|
2070
2311
|
}));
|
|
2071
2312
|
fs_1.default.rmSync(skillDir, { recursive: true, force: true });
|
|
@@ -2107,6 +2348,15 @@ ${instruction}
|
|
|
2107
2348
|
applyWriteLocalRecords(proxyServer);
|
|
2108
2349
|
res.json(result);
|
|
2109
2350
|
})));
|
|
2351
|
+
app.post('/api/write-config/opencode', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2352
|
+
var _a;
|
|
2353
|
+
const appConfig = dbManager.getConfig();
|
|
2354
|
+
const requestedModel = typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a.defaultModel) === 'string' ? req.body.defaultModel : undefined;
|
|
2355
|
+
const defaultModel = requestedModel || appConfig.opencodeDefaultModel;
|
|
2356
|
+
const result = yield writeOpencodeConfig(dbManager, defaultModel);
|
|
2357
|
+
applyWriteLocalRecords(proxyServer);
|
|
2358
|
+
res.json(result);
|
|
2359
|
+
})));
|
|
2110
2360
|
// 兼容接口:更新全局 Agent Teams 配置
|
|
2111
2361
|
app.post('/api/update-claude-agent-teams', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2112
2362
|
const { enableAgentTeams } = req.body;
|
|
@@ -2160,12 +2410,19 @@ ${instruction}
|
|
|
2160
2410
|
const result = yield restoreCodexConfig();
|
|
2161
2411
|
res.json(result);
|
|
2162
2412
|
})));
|
|
2413
|
+
app.post('/api/restore-config/opencode', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2414
|
+
const result = yield restoreOpencodeConfig();
|
|
2415
|
+
res.json(result);
|
|
2416
|
+
})));
|
|
2163
2417
|
app.get('/api/check-backup/claude', (_req, res) => {
|
|
2164
2418
|
res.json({ exists: checkClaudeBackupExists() });
|
|
2165
2419
|
});
|
|
2166
2420
|
app.get('/api/check-backup/codex', (_req, res) => {
|
|
2167
2421
|
res.json({ exists: checkCodexBackupExists() });
|
|
2168
2422
|
});
|
|
2423
|
+
app.get('/api/check-backup/opencode', (_req, res) => {
|
|
2424
|
+
res.json({ exists: checkOpencodeBackupExists() });
|
|
2425
|
+
});
|
|
2169
2426
|
// 新的详细配置状态 API 端点
|
|
2170
2427
|
app.get('/api/config-status/claude', (_req, res) => {
|
|
2171
2428
|
const status = (0, config_metadata_1.checkClaudeConfigStatus)();
|
|
@@ -2175,6 +2432,10 @@ ${instruction}
|
|
|
2175
2432
|
const status = (0, config_metadata_1.checkCodexConfigStatus)();
|
|
2176
2433
|
res.json(status);
|
|
2177
2434
|
});
|
|
2435
|
+
app.get('/api/config-status/opencode', (_req, res) => {
|
|
2436
|
+
const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
2437
|
+
res.json(status);
|
|
2438
|
+
});
|
|
2178
2439
|
// API 路径路由映射
|
|
2179
2440
|
app.get('/api/api-path-bindings', (_req, res) => {
|
|
2180
2441
|
res.json({ bindings: dbManager.getApiPathBindings(), models: dbManager.getApiPathModels() });
|
|
@@ -2939,6 +3200,31 @@ ${instruction}
|
|
|
2939
3200
|
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
2940
3201
|
results['codex'] = true;
|
|
2941
3202
|
}
|
|
3203
|
+
else if (target === 'opencode') {
|
|
3204
|
+
// 写入 ~/.config/opencode/opencode.json 的 provider.aicodeswitch.options.apiKey
|
|
3205
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
3206
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
3207
|
+
if (!fs_1.default.existsSync(opencodeDir)) {
|
|
3208
|
+
fs_1.default.mkdirSync(opencodeDir, { recursive: true });
|
|
3209
|
+
}
|
|
3210
|
+
let oc = {};
|
|
3211
|
+
if (fs_1.default.existsSync(opencodeConfigPath)) {
|
|
3212
|
+
try {
|
|
3213
|
+
oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
3214
|
+
}
|
|
3215
|
+
catch ( /* ignore */_c) { /* ignore */ }
|
|
3216
|
+
}
|
|
3217
|
+
if (!oc.provider)
|
|
3218
|
+
oc.provider = {};
|
|
3219
|
+
if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
|
|
3220
|
+
oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
|
|
3221
|
+
}
|
|
3222
|
+
if (!oc.provider.aicodeswitch.options)
|
|
3223
|
+
oc.provider.aicodeswitch.options = {};
|
|
3224
|
+
oc.provider.aicodeswitch.options.apiKey = key.apiKey;
|
|
3225
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
|
|
3226
|
+
results['opencode'] = true;
|
|
3227
|
+
}
|
|
2942
3228
|
}
|
|
2943
3229
|
catch (error) {
|
|
2944
3230
|
console.error(`Failed to write local config for ${target}:`, error);
|
|
@@ -3319,6 +3605,76 @@ ${instruction}
|
|
|
3319
3605
|
console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
|
|
3320
3606
|
return true;
|
|
3321
3607
|
}
|
|
3608
|
+
else if (targetType === 'opencode') {
|
|
3609
|
+
// OpenCode 使用 JSON 格式的 opencode.json,MCP 配置位于 mcp 段
|
|
3610
|
+
// 格式:local → { type:"local", command:[...], enabled:true, env? }
|
|
3611
|
+
// remote → { type:"remote", url, enabled:true, headers? }
|
|
3612
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
3613
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
3614
|
+
if (!fs_1.default.existsSync(opencodeDir)) {
|
|
3615
|
+
fs_1.default.mkdirSync(opencodeDir, { recursive: true });
|
|
3616
|
+
}
|
|
3617
|
+
let currentConfig = {};
|
|
3618
|
+
if (fs_1.default.existsSync(opencodeConfigPath)) {
|
|
3619
|
+
try {
|
|
3620
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
3621
|
+
}
|
|
3622
|
+
catch (error) {
|
|
3623
|
+
console.warn('[MCP] Failed to parse opencode.json:', error);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
// 清除代理上次写入的 mcp 条目(通过 metadata 追踪,避免误删用户自配的 mcp)
|
|
3627
|
+
const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
|
|
3628
|
+
let previousMcpIds = [];
|
|
3629
|
+
if (fs_1.default.existsSync(mcpMetaPath)) {
|
|
3630
|
+
try {
|
|
3631
|
+
previousMcpIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
|
|
3632
|
+
for (const id of previousMcpIds) {
|
|
3633
|
+
if (currentConfig.mcp && currentConfig.mcp[id]) {
|
|
3634
|
+
delete currentConfig.mcp[id];
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
catch (_b) {
|
|
3639
|
+
// ignore
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
if (!currentConfig.mcp) {
|
|
3643
|
+
currentConfig.mcp = {};
|
|
3644
|
+
}
|
|
3645
|
+
const writtenMcpIds = [];
|
|
3646
|
+
for (const mcp of mcps) {
|
|
3647
|
+
const mcpConfig = { enabled: true };
|
|
3648
|
+
if (mcp.type === 'stdio') {
|
|
3649
|
+
mcpConfig.type = 'local';
|
|
3650
|
+
const cmdParts = [mcp.command || ''];
|
|
3651
|
+
if (Array.isArray(mcp.args)) {
|
|
3652
|
+
cmdParts.push(...mcp.args);
|
|
3653
|
+
}
|
|
3654
|
+
mcpConfig.command = cmdParts.filter((c, i) => i === 0 ? c !== '' : true);
|
|
3655
|
+
if (mcp.env && Object.keys(mcp.env).length > 0) {
|
|
3656
|
+
mcpConfig.environment = Object.assign({}, mcp.env);
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
else {
|
|
3660
|
+
// http / sse 均使用 remote 类型
|
|
3661
|
+
mcpConfig.type = 'remote';
|
|
3662
|
+
mcpConfig.url = mcp.url || '';
|
|
3663
|
+
if (mcp.headers && Object.keys(mcp.headers).length > 0) {
|
|
3664
|
+
mcpConfig.headers = Object.assign({}, mcp.headers);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
currentConfig.mcp[mcp.id] = mcpConfig;
|
|
3668
|
+
writtenMcpIds.push(mcp.id);
|
|
3669
|
+
}
|
|
3670
|
+
if (Object.keys(currentConfig.mcp).length === 0) {
|
|
3671
|
+
delete currentConfig.mcp;
|
|
3672
|
+
}
|
|
3673
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
|
|
3674
|
+
fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(writtenMcpIds, null, 2));
|
|
3675
|
+
console.log(`[MCP] OpenCode MCP config written: ${writtenMcpIds.length} server(s)`);
|
|
3676
|
+
return true;
|
|
3677
|
+
}
|
|
3322
3678
|
return false;
|
|
3323
3679
|
}
|
|
3324
3680
|
catch (error) {
|
|
@@ -3381,6 +3737,41 @@ ${instruction}
|
|
|
3381
3737
|
}
|
|
3382
3738
|
return true;
|
|
3383
3739
|
}
|
|
3740
|
+
else if (targetType === 'opencode') {
|
|
3741
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
3742
|
+
if (!fs_1.default.existsSync(opencodeConfigPath)) {
|
|
3743
|
+
return true;
|
|
3744
|
+
}
|
|
3745
|
+
let currentConfig = {};
|
|
3746
|
+
try {
|
|
3747
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
3748
|
+
}
|
|
3749
|
+
catch (error) {
|
|
3750
|
+
console.warn('[MCP] Failed to parse opencode.json for removal:', error);
|
|
3751
|
+
return false;
|
|
3752
|
+
}
|
|
3753
|
+
if (currentConfig.mcp && currentConfig.mcp[mcpId]) {
|
|
3754
|
+
delete currentConfig.mcp[mcpId];
|
|
3755
|
+
if (Object.keys(currentConfig.mcp).length === 0) {
|
|
3756
|
+
delete currentConfig.mcp;
|
|
3757
|
+
}
|
|
3758
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
|
|
3759
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
3760
|
+
const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
|
|
3761
|
+
if (fs_1.default.existsSync(mcpMetaPath)) {
|
|
3762
|
+
try {
|
|
3763
|
+
const previousIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
|
|
3764
|
+
const updatedIds = previousIds.filter(id => id !== mcpId);
|
|
3765
|
+
fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(updatedIds, null, 2));
|
|
3766
|
+
}
|
|
3767
|
+
catch (_b) {
|
|
3768
|
+
// ignore
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
console.log(`[MCP] Removed MCP ${mcpId} from OpenCode config`);
|
|
3772
|
+
}
|
|
3773
|
+
return true;
|
|
3774
|
+
}
|
|
3384
3775
|
return false;
|
|
3385
3776
|
}
|
|
3386
3777
|
catch (error) {
|
|
@@ -3590,6 +3981,13 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3590
3981
|
catch (error) {
|
|
3591
3982
|
console.error('[Shutdown ...] Failed to restore Codex config:', error);
|
|
3592
3983
|
}
|
|
3984
|
+
try {
|
|
3985
|
+
const opencodeRestored = yield restoreOpencodeConfig();
|
|
3986
|
+
console.log(`[Shutdown ...] OpenCode config ${opencodeRestored ? 'restored' : 'was not modified'}`);
|
|
3987
|
+
}
|
|
3988
|
+
catch (error) {
|
|
3989
|
+
console.error('[Shutdown ...] Failed to restore OpenCode config:', error);
|
|
3990
|
+
}
|
|
3593
3991
|
// Shutdown AccessKey module
|
|
3594
3992
|
try {
|
|
3595
3993
|
yield accessKeyModule.shutdown();
|
|
@@ -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.readOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = void 0;
|
|
6
|
+
exports.readOriginalConfig = exports.readOpencodeOriginalConfig = exports.readCodexOriginalConfig = exports.readClaudeOriginalConfig = 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"));
|
|
@@ -232,6 +232,73 @@ const readCodexOriginalConfig = () => {
|
|
|
232
232
|
}
|
|
233
233
|
};
|
|
234
234
|
exports.readCodexOriginalConfig = readCodexOriginalConfig;
|
|
235
|
+
/**
|
|
236
|
+
* 读取 OpenCode 原始配置
|
|
237
|
+
* fallback 场景下仅从备份文件(~/.config/opencode/opencode.json.aicodeswitch_backup)读取
|
|
238
|
+
*
|
|
239
|
+
* OpenCode 的模型形如 "<providerId>/<modelId>",真实上游由用户原始 opencode.json 中的
|
|
240
|
+
* provider 段(options.baseURL + options.apiKey)决定。此处解析备份中的真实 provider。
|
|
241
|
+
*/
|
|
242
|
+
const readOpencodeOriginalConfig = () => {
|
|
243
|
+
var _a, _b;
|
|
244
|
+
try {
|
|
245
|
+
const homeDir = os_1.default.homedir();
|
|
246
|
+
const configBakPath = path_1.default.join(homeDir, '.config', 'opencode', 'opencode.json.aicodeswitch_backup');
|
|
247
|
+
if (!fs_1.default.existsSync(configBakPath)) {
|
|
248
|
+
console.log('No OpenCode backup config file found');
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const content = fs_1.default.readFileSync(configBakPath, 'utf-8');
|
|
252
|
+
const config = JSON.parse(content);
|
|
253
|
+
// 解析 model:"providerId/modelId"
|
|
254
|
+
const modelField = typeof config.model === 'string' ? config.model : '';
|
|
255
|
+
const [providerId, modelId] = modelField.split('/');
|
|
256
|
+
const providers = (config.provider && typeof config.provider === 'object') ? config.provider : {};
|
|
257
|
+
// 选定真实 provider:优先 model 中声明的 providerId,否则取第一个非 aicodeswitch 的 provider
|
|
258
|
+
let providerConfig;
|
|
259
|
+
let resolvedProviderId = '';
|
|
260
|
+
if (providerId && providers[providerId]) {
|
|
261
|
+
providerConfig = providers[providerId];
|
|
262
|
+
resolvedProviderId = providerId;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
for (const [pid, pc] of Object.entries(providers)) {
|
|
266
|
+
if (pid !== 'aicodeswitch' && pc && typeof pc === 'object') {
|
|
267
|
+
providerConfig = pc;
|
|
268
|
+
resolvedProviderId = pid;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const baseUrlRaw = ((_a = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.options) === null || _a === void 0 ? void 0 : _a.baseURL) || '';
|
|
274
|
+
const baseUrl = normalizeApiUrl(baseUrlRaw);
|
|
275
|
+
// apiKey 可能内联在 options.apiKey,也可能经 {env:XXX}/{file:xxx} 引用(fallback 无法解析,置空)
|
|
276
|
+
let apiKey = '';
|
|
277
|
+
if (typeof ((_b = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.options) === null || _b === void 0 ? void 0 : _b.apiKey) === 'string') {
|
|
278
|
+
const raw = providerConfig.options.apiKey.trim();
|
|
279
|
+
if (raw && !raw.startsWith('{env:') && !raw.startsWith('{file:')) {
|
|
280
|
+
apiKey = raw;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (!baseUrl) {
|
|
284
|
+
console.log('No baseURL found in OpenCode backup config provider:', resolvedProviderId);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const sourceType = inferSourceTypeFromBaseUrlAndWireApi(baseUrl, undefined);
|
|
288
|
+
return {
|
|
289
|
+
apiUrl: baseUrl,
|
|
290
|
+
apiKey,
|
|
291
|
+
authType: inferAuthTypeFromSource(sourceType),
|
|
292
|
+
sourceType,
|
|
293
|
+
model: modelId || modelField || undefined,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.error('Failed to read OpenCode original config:', error);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
exports.readOpencodeOriginalConfig = readOpencodeOriginalConfig;
|
|
235
302
|
/**
|
|
236
303
|
* 根据目标类型读取原始配置
|
|
237
304
|
*/
|
|
@@ -242,6 +309,9 @@ const readOriginalConfig = (targetType) => {
|
|
|
242
309
|
else if (targetType === 'codex') {
|
|
243
310
|
return (0, exports.readCodexOriginalConfig)();
|
|
244
311
|
}
|
|
312
|
+
else if (targetType === 'opencode') {
|
|
313
|
+
return (0, exports.readOpencodeOriginalConfig)();
|
|
314
|
+
}
|
|
245
315
|
return null;
|
|
246
316
|
};
|
|
247
317
|
exports.readOriginalConfig = readOriginalConfig;
|