aicodeswitch 5.2.13 → 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 +364 -65
- 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 +10 -2
- package/dist/server/conversions/thinking/providers.js +12 -7
- package/dist/server/fs-database.js +10 -1
- package/dist/server/main.js +412 -37
- package/dist/server/original-config-reader.js +71 -1
- package/dist/server/performance-tracker.js +78 -8
- package/dist/server/proxy-server.js +159 -34
- package/dist/server/session-migration.js +1 -0
- package/dist/server/transformers/chunk-collector.js +112 -25
- package/dist/ui/assets/index-BeM4AIzn.js +1187 -0
- package/dist/ui/assets/index-DqRzbMIU.css +1 -0
- package/dist/ui/assets/three-CFpmPosW.js +3802 -0
- package/dist/ui/index.html +3 -2
- package/package.json +3 -1
- package/scripts/dev.js +76 -30
- package/dist/server/tools-service.js +0 -203
- package/dist/server/websocket-service.js +0 -148
- package/dist/ui/assets/index-4liE4bV8.css +0 -1
- package/dist/ui/assets/index-CJMAVkIN.js +0 -813
package/dist/server/main.js
CHANGED
|
@@ -32,8 +32,6 @@ const os_1 = __importDefault(require("os"));
|
|
|
32
32
|
const auth_1 = require("./auth");
|
|
33
33
|
const version_check_1 = require("./version-check");
|
|
34
34
|
const utils_1 = require("./utils");
|
|
35
|
-
const tools_service_1 = require("./tools-service");
|
|
36
|
-
const websocket_service_1 = require("./websocket-service");
|
|
37
35
|
const rules_status_service_1 = require("./rules-status-service");
|
|
38
36
|
const type_migration_1 = require("./type-migration");
|
|
39
37
|
const config_metadata_1 = require("./config-metadata");
|
|
@@ -185,6 +183,30 @@ function applyWriteLocalRecords(proxyServer) {
|
|
|
185
183
|
auth.OPENAI_API_KEY = key.apiKey;
|
|
186
184
|
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
187
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
|
+
}
|
|
188
210
|
}
|
|
189
211
|
catch (error) {
|
|
190
212
|
console.error(`[WriteLocal] Failed to apply key ${record.accessKeyId} to ${target}:`, error);
|
|
@@ -664,6 +686,142 @@ const restoreCodexConfig = () => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
664
686
|
return false;
|
|
665
687
|
}
|
|
666
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
|
+
};
|
|
667
825
|
const checkClaudeBackupExists = () => {
|
|
668
826
|
try {
|
|
669
827
|
// 清理可能的无效元数据
|
|
@@ -705,6 +863,8 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
|
|
|
705
863
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
706
864
|
const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
|
|
707
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'}`);
|
|
708
868
|
});
|
|
709
869
|
const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
|
|
710
870
|
const config = dbManager.getConfig();
|
|
@@ -718,6 +878,8 @@ const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0,
|
|
|
718
878
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
719
879
|
const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
|
|
720
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'}`);
|
|
721
883
|
});
|
|
722
884
|
const getCentralSkillsDir = () => {
|
|
723
885
|
return path_1.default.join(os_1.default.homedir(), '.aicodeswitch', 'skills');
|
|
@@ -736,12 +898,20 @@ function getSkillDirByName(name) {
|
|
|
736
898
|
return path_1.default.join(centralDir, sanitizedName);
|
|
737
899
|
}
|
|
738
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
|
+
}
|
|
739
905
|
const baseDir = targetType === 'claude-code' ? '.claude' : '.codex';
|
|
740
906
|
return path_1.default.join(os_1.default.homedir(), baseDir, 'skills', skillId);
|
|
741
907
|
};
|
|
742
908
|
function isSkillSymlinkExists(skillId, targetType) {
|
|
743
909
|
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
744
910
|
try {
|
|
911
|
+
if (targetType === 'opencode') {
|
|
912
|
+
// OpenCode 是普通文件,不是 symlink
|
|
913
|
+
return fs_1.default.existsSync(symlinkPath);
|
|
914
|
+
}
|
|
745
915
|
const stats = fs_1.default.lstatSync(symlinkPath);
|
|
746
916
|
return stats.isSymbolicLink();
|
|
747
917
|
}
|
|
@@ -749,15 +919,77 @@ function isSkillSymlinkExists(skillId, targetType) {
|
|
|
749
919
|
return false;
|
|
750
920
|
}
|
|
751
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
|
+
}
|
|
752
964
|
function createSkillSymlink(skillId, targetType) {
|
|
753
965
|
return __awaiter(this, void 0, void 0, function* () {
|
|
966
|
+
var _a;
|
|
754
967
|
try {
|
|
755
968
|
const centralDir = getCentralSkillsDir();
|
|
756
969
|
const skillDir = path_1.default.join(centralDir, skillId);
|
|
757
|
-
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
758
970
|
if (!fs_1.default.existsSync(skillDir)) {
|
|
759
971
|
return { success: false, error: 'Skill目录不存在' };
|
|
760
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);
|
|
761
993
|
const targetBaseDir = path_1.default.dirname(symlinkPath);
|
|
762
994
|
if (!fs_1.default.existsSync(targetBaseDir)) {
|
|
763
995
|
fs_1.default.mkdirSync(targetBaseDir, { recursive: true });
|
|
@@ -788,6 +1020,13 @@ function removeSkillSymlink(skillId, targetType) {
|
|
|
788
1020
|
return __awaiter(this, void 0, void 0, function* () {
|
|
789
1021
|
try {
|
|
790
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
|
+
}
|
|
791
1030
|
if (!fs_1.default.existsSync(symlinkPath)) {
|
|
792
1031
|
return { success: true };
|
|
793
1032
|
}
|
|
@@ -1046,7 +1285,7 @@ const listInstalledSkills = () => {
|
|
|
1046
1285
|
try {
|
|
1047
1286
|
const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
|
|
1048
1287
|
const enabledTargets = [];
|
|
1049
|
-
['claude-code', 'codex'].forEach((targetType) => {
|
|
1288
|
+
['claude-code', 'codex', 'opencode'].forEach((targetType) => {
|
|
1050
1289
|
if (isSkillSymlinkExists(skillId, targetType)) {
|
|
1051
1290
|
enabledTargets.push(targetType);
|
|
1052
1291
|
}
|
|
@@ -1198,7 +1437,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1198
1437
|
if (!tool || !routeId) {
|
|
1199
1438
|
return res.status(400).json({ error: 'tool and routeId are required' });
|
|
1200
1439
|
}
|
|
1201
|
-
if (tool !== 'claude-code' && tool !== 'codex') {
|
|
1440
|
+
if (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode') {
|
|
1202
1441
|
return res.status(400).json({ error: 'Invalid tool name' });
|
|
1203
1442
|
}
|
|
1204
1443
|
const route = dbManager.getRoute(routeId);
|
|
@@ -1219,7 +1458,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1219
1458
|
})));
|
|
1220
1459
|
app.post('/api/tool-bindings/deactivate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1221
1460
|
const { tool } = req.body;
|
|
1222
|
-
if (!tool || (tool !== 'claude-code' && tool !== 'codex')) {
|
|
1461
|
+
if (!tool || (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode')) {
|
|
1223
1462
|
return res.status(400).json({ error: 'Invalid tool name' });
|
|
1224
1463
|
}
|
|
1225
1464
|
const result = yield dbManager.deactivateToolRoute(tool);
|
|
@@ -2007,7 +2246,7 @@ ${instruction}
|
|
|
2007
2246
|
app.post('/api/skills/:skillId/enable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2008
2247
|
const { skillId } = req.params;
|
|
2009
2248
|
const { targetType } = req.body;
|
|
2010
|
-
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
2249
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
|
|
2011
2250
|
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
2012
2251
|
return;
|
|
2013
2252
|
}
|
|
@@ -2038,7 +2277,7 @@ ${instruction}
|
|
|
2038
2277
|
app.post('/api/skills/:skillId/disable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2039
2278
|
const { skillId } = req.params;
|
|
2040
2279
|
const { targetType } = req.body;
|
|
2041
|
-
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
2280
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
|
|
2042
2281
|
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
2043
2282
|
return;
|
|
2044
2283
|
}
|
|
@@ -2067,7 +2306,7 @@ ${instruction}
|
|
|
2067
2306
|
res.status(404).json({ success: false, error: 'Skill不存在' });
|
|
2068
2307
|
return;
|
|
2069
2308
|
}
|
|
2070
|
-
['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* () {
|
|
2071
2310
|
yield removeSkillSymlink(skillId, targetType);
|
|
2072
2311
|
}));
|
|
2073
2312
|
fs_1.default.rmSync(skillDir, { recursive: true, force: true });
|
|
@@ -2109,6 +2348,15 @@ ${instruction}
|
|
|
2109
2348
|
applyWriteLocalRecords(proxyServer);
|
|
2110
2349
|
res.json(result);
|
|
2111
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
|
+
})));
|
|
2112
2360
|
// 兼容接口:更新全局 Agent Teams 配置
|
|
2113
2361
|
app.post('/api/update-claude-agent-teams', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2114
2362
|
const { enableAgentTeams } = req.body;
|
|
@@ -2162,12 +2410,19 @@ ${instruction}
|
|
|
2162
2410
|
const result = yield restoreCodexConfig();
|
|
2163
2411
|
res.json(result);
|
|
2164
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
|
+
})));
|
|
2165
2417
|
app.get('/api/check-backup/claude', (_req, res) => {
|
|
2166
2418
|
res.json({ exists: checkClaudeBackupExists() });
|
|
2167
2419
|
});
|
|
2168
2420
|
app.get('/api/check-backup/codex', (_req, res) => {
|
|
2169
2421
|
res.json({ exists: checkCodexBackupExists() });
|
|
2170
2422
|
});
|
|
2423
|
+
app.get('/api/check-backup/opencode', (_req, res) => {
|
|
2424
|
+
res.json({ exists: checkOpencodeBackupExists() });
|
|
2425
|
+
});
|
|
2171
2426
|
// 新的详细配置状态 API 端点
|
|
2172
2427
|
app.get('/api/config-status/claude', (_req, res) => {
|
|
2173
2428
|
const status = (0, config_metadata_1.checkClaudeConfigStatus)();
|
|
@@ -2177,6 +2432,10 @@ ${instruction}
|
|
|
2177
2432
|
const status = (0, config_metadata_1.checkCodexConfigStatus)();
|
|
2178
2433
|
res.json(status);
|
|
2179
2434
|
});
|
|
2435
|
+
app.get('/api/config-status/opencode', (_req, res) => {
|
|
2436
|
+
const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
2437
|
+
res.json(status);
|
|
2438
|
+
});
|
|
2180
2439
|
// API 路径路由映射
|
|
2181
2440
|
app.get('/api/api-path-bindings', (_req, res) => {
|
|
2182
2441
|
res.json({ bindings: dbManager.getApiPathBindings(), models: dbManager.getApiPathModels() });
|
|
@@ -2514,19 +2773,6 @@ ${instruction}
|
|
|
2514
2773
|
res.json({ success: false });
|
|
2515
2774
|
}
|
|
2516
2775
|
})));
|
|
2517
|
-
// 工具安装检测相关路由
|
|
2518
|
-
app.get('/api/tools/status', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2519
|
-
console.log('[API] GET /api/tools/status - 获取工具安装状态');
|
|
2520
|
-
try {
|
|
2521
|
-
const status = yield (0, tools_service_1.getToolsInstallationStatus)();
|
|
2522
|
-
console.log('[API] 工具安装状态:', status);
|
|
2523
|
-
res.json(status);
|
|
2524
|
-
}
|
|
2525
|
-
catch (error) {
|
|
2526
|
-
console.error('[API] 获取工具状态失败:', error);
|
|
2527
|
-
res.status(500).json({ error: '获取工具状态失败' });
|
|
2528
|
-
}
|
|
2529
|
-
})));
|
|
2530
2776
|
// MCP 工具管理相关路由
|
|
2531
2777
|
app.get('/api/mcps', (_req, res) => {
|
|
2532
2778
|
res.json(dbManager.getMCPs());
|
|
@@ -2954,6 +3200,31 @@ ${instruction}
|
|
|
2954
3200
|
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
2955
3201
|
results['codex'] = true;
|
|
2956
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
|
+
}
|
|
2957
3228
|
}
|
|
2958
3229
|
catch (error) {
|
|
2959
3230
|
console.error(`Failed to write local config for ${target}:`, error);
|
|
@@ -3334,6 +3605,76 @@ ${instruction}
|
|
|
3334
3605
|
console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
|
|
3335
3606
|
return true;
|
|
3336
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
|
+
}
|
|
3337
3678
|
return false;
|
|
3338
3679
|
}
|
|
3339
3680
|
catch (error) {
|
|
@@ -3396,6 +3737,41 @@ ${instruction}
|
|
|
3396
3737
|
}
|
|
3397
3738
|
return true;
|
|
3398
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
|
+
}
|
|
3399
3775
|
return false;
|
|
3400
3776
|
}
|
|
3401
3777
|
catch (error) {
|
|
@@ -3563,26 +3939,12 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3563
3939
|
}
|
|
3564
3940
|
setImmediate(() => process.exit(1));
|
|
3565
3941
|
});
|
|
3566
|
-
// 创建 WebSocket 服务器用于工具安装
|
|
3567
|
-
const toolInstallWss = (0, websocket_service_1.createToolInstallationWSServer)();
|
|
3568
3942
|
// 设置黑名单检查函数,用于在规则状态同步时检查黑名单是否已过期
|
|
3569
3943
|
rules_status_service_1.rulesStatusBroadcaster.setBlacklistChecker((serviceId, routeId, contentType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3570
3944
|
// 检查服务��否在黑名单中
|
|
3571
3945
|
const isBlacklisted = yield dbManager.isServiceBlacklisted(serviceId, routeId, contentType);
|
|
3572
3946
|
return isBlacklisted;
|
|
3573
3947
|
}));
|
|
3574
|
-
// 将 WebSocket 服务器附加到 HTTP 服务器(仅用于工具安装)
|
|
3575
|
-
server.on('upgrade', (request, socket, head) => {
|
|
3576
|
-
if (request.url === '/api/tools/install') {
|
|
3577
|
-
toolInstallWss.handleUpgrade(request, socket, head, (ws) => {
|
|
3578
|
-
toolInstallWss.emit('connection', ws, request);
|
|
3579
|
-
});
|
|
3580
|
-
}
|
|
3581
|
-
else {
|
|
3582
|
-
socket.destroy();
|
|
3583
|
-
}
|
|
3584
|
-
});
|
|
3585
|
-
console.log(`WebSocket server for tool installation attached to ws://${clientHost}:${port}/api/tools/install`);
|
|
3586
3948
|
let isShuttingDown = false;
|
|
3587
3949
|
let shutdownPromise = null;
|
|
3588
3950
|
const shutdown = (signal) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -3594,10 +3956,16 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3594
3956
|
console.log(`[Server] Received ${signal}, shutting down...`);
|
|
3595
3957
|
// 立即停止监听以释放端口(同步关闭监听句柄,端口马上可用),
|
|
3596
3958
|
// 避免在漫长的清理流程期间端口仍被占用,导致此时重启产生 EADDRINUSE 冲突。
|
|
3597
|
-
// 现有连接会继续处理直到完成或超时;其回调与下面的清理流程并行等待。
|
|
3598
3959
|
const serverClosedPromise = new Promise((resolve) => {
|
|
3599
3960
|
server.close(() => resolve());
|
|
3600
3961
|
});
|
|
3962
|
+
// 强制断开所有现存连接(含浏览器长连的 SSE:agent-map stream、rules-status
|
|
3963
|
+
// 广播等)。否则这些连接永不自行关闭,server.close 的回调要等满下面 5s 超时
|
|
3964
|
+
// 才触发,表现为 Ctrl+C 后「卡很久才打印 Server stopped.」。closeAllConnections
|
|
3965
|
+
// 立即销毁所有 socket,让 close 回调几乎瞬时完成。
|
|
3966
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
3967
|
+
server.closeAllConnections();
|
|
3968
|
+
}
|
|
3601
3969
|
// 服务终止前恢复配置文件(适用于 aicos stop 与 Ctrl+C)
|
|
3602
3970
|
try {
|
|
3603
3971
|
const claudeRestored = yield restoreClaudeConfig();
|
|
@@ -3613,6 +3981,13 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3613
3981
|
catch (error) {
|
|
3614
3982
|
console.error('[Shutdown ...] Failed to restore Codex config:', error);
|
|
3615
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
|
+
}
|
|
3616
3991
|
// Shutdown AccessKey module
|
|
3617
3992
|
try {
|
|
3618
3993
|
yield accessKeyModule.shutdown();
|