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/dist/server/main.js
CHANGED
|
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.start = exports.gracefulShutdown = void 0;
|
|
15
16
|
const express_1 = __importDefault(require("express"));
|
|
16
17
|
const cors_1 = __importDefault(require("cors"));
|
|
17
18
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
@@ -183,6 +184,30 @@ function applyWriteLocalRecords(proxyServer) {
|
|
|
183
184
|
auth.OPENAI_API_KEY = key.apiKey;
|
|
184
185
|
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
185
186
|
}
|
|
187
|
+
else if (target === 'opencode') {
|
|
188
|
+
// 将真实 Key 写入 opencode.json 的 provider.aicodeswitch.options.apiKey
|
|
189
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
190
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
191
|
+
if (!fs_1.default.existsSync(opencodeDir)) {
|
|
192
|
+
fs_1.default.mkdirSync(opencodeDir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
let oc = {};
|
|
195
|
+
if (fs_1.default.existsSync(opencodeConfigPath)) {
|
|
196
|
+
try {
|
|
197
|
+
oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
198
|
+
}
|
|
199
|
+
catch ( /* ignore */_c) { /* ignore */ }
|
|
200
|
+
}
|
|
201
|
+
if (!oc.provider)
|
|
202
|
+
oc.provider = {};
|
|
203
|
+
if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
|
|
204
|
+
oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
|
|
205
|
+
}
|
|
206
|
+
if (!oc.provider.aicodeswitch.options)
|
|
207
|
+
oc.provider.aicodeswitch.options = {};
|
|
208
|
+
oc.provider.aicodeswitch.options.apiKey = key.apiKey;
|
|
209
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
|
|
210
|
+
}
|
|
186
211
|
}
|
|
187
212
|
catch (error) {
|
|
188
213
|
console.error(`[WriteLocal] Failed to apply key ${record.accessKeyId} to ${target}:`, error);
|
|
@@ -239,7 +264,7 @@ const isClaudePermissionDefaultMode = (value) => {
|
|
|
239
264
|
const isValidAutocompactPct = (v) => {
|
|
240
265
|
return typeof v === 'number' && Number.isInteger(v) && v >= 1 && v <= 100;
|
|
241
266
|
};
|
|
242
|
-
const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, options = {}) {
|
|
267
|
+
const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, maxRetries_1, ...args_1) => __awaiter(void 0, [_dbManager_1, enableAgentTeams_1, enableBypassPermissionsSupport_1, permissionsDefaultMode_1, effortLevel_1, defaultModel_1, autocompactPctOverride_1, maxRetries_1, ...args_1], void 0, function* (_dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, effortLevel, defaultModel, autocompactPctOverride, maxRetries, options = {}) {
|
|
243
268
|
var _a;
|
|
244
269
|
try {
|
|
245
270
|
const homeDir = os_1.default.homedir();
|
|
@@ -294,7 +319,7 @@ const writeClaudeConfig = (_dbManager_1, enableAgentTeams_1, enableBypassPermiss
|
|
|
294
319
|
ANTHROPIC_BASE_URL: `http://${clientHost}:${port}/claude-code`,
|
|
295
320
|
API_TIMEOUT_MS: "3000000",
|
|
296
321
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
|
297
|
-
CLAUDE_CODE_MAX_RETRIES:
|
|
322
|
+
CLAUDE_CODE_MAX_RETRIES: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5
|
|
298
323
|
};
|
|
299
324
|
// 如果启用Agent Teams功能,添加对应的环境变量
|
|
300
325
|
if (enableAgentTeams) {
|
|
@@ -392,7 +417,7 @@ const DEFAULT_CODEX_REASONING_EFFORT = 'high';
|
|
|
392
417
|
const isCodexReasoningEffort = (value) => {
|
|
393
418
|
return typeof value === 'string' && VALID_CODEX_REASONING_EFFORTS.includes(value);
|
|
394
419
|
};
|
|
395
|
-
const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, enableMemories, options = {}) {
|
|
420
|
+
const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbManager_1, ...args_1], void 0, function* (_dbManager, modelReasoningEffort = DEFAULT_CODEX_REASONING_EFFORT, codexDefaultModel, enableMemories, maxRetries, options = {}) {
|
|
396
421
|
var _a;
|
|
397
422
|
try {
|
|
398
423
|
const homeDir = os_1.default.homedir();
|
|
@@ -454,7 +479,7 @@ const writeCodexConfig = (_dbManager_1, ...args_1) => __awaiter(void 0, [_dbMana
|
|
|
454
479
|
name: "aicodeswitch",
|
|
455
480
|
base_url: `http://${clientHost}:${port}/codex`,
|
|
456
481
|
wire_api: "responses",
|
|
457
|
-
stream_max_retries:
|
|
482
|
+
stream_max_retries: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5,
|
|
458
483
|
stream_retry_backoff: "fixed"
|
|
459
484
|
}
|
|
460
485
|
}
|
|
@@ -662,6 +687,143 @@ const restoreCodexConfig = () => __awaiter(void 0, void 0, void 0, function* ()
|
|
|
662
687
|
return false;
|
|
663
688
|
}
|
|
664
689
|
});
|
|
690
|
+
/**
|
|
691
|
+
* 默认 OpenCode 模型(当用户未配置 opencodeDefaultModel 时使用)
|
|
692
|
+
*/
|
|
693
|
+
const DEFAULT_OPENCODE_MODEL = 'claude-sonnet-4-20250514';
|
|
694
|
+
/**
|
|
695
|
+
* 写入 OpenCode 配置(~/.config/opencode/opencode.json)
|
|
696
|
+
*
|
|
697
|
+
* 注入一个自定义 provider `aicodeswitch`,经 @ai-sdk/openai-compatible 指向本代理
|
|
698
|
+
* 的 /opencode/v1 端点(OpenAI Chat Completions 格式)。仅托管 provider.aicodeswitch
|
|
699
|
+
* 段与 model/small_model/mcp 字段,其余用户配置(其它 provider、agent、command 等)保留。
|
|
700
|
+
*/
|
|
701
|
+
const writeOpencodeConfig = (_dbManager_1, defaultModel_1, maxRetries_1, ...args_1) => __awaiter(void 0, [_dbManager_1, defaultModel_1, maxRetries_1, ...args_1], void 0, function* (_dbManager, defaultModel, maxRetries, options = {}) {
|
|
702
|
+
var _a;
|
|
703
|
+
try {
|
|
704
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
|
|
705
|
+
const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
706
|
+
const configBakPath = `${configPath}.aicodeswitch_backup`;
|
|
707
|
+
const configDir = path_1.default.dirname(configPath);
|
|
708
|
+
const configStatus = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
709
|
+
const isRuntimeRefresh = options.allowOverwriteRefresh === true && configStatus.isOverwritten;
|
|
710
|
+
if (configStatus.isOverwritten && !isRuntimeRefresh) {
|
|
711
|
+
console.error('OpenCode config has already been overwritten. Please restore the original config first.');
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
let originalHash = isRuntimeRefresh
|
|
715
|
+
? (_a = configStatus.metadata) === null || _a === void 0 ? void 0 : _a.originalHash
|
|
716
|
+
: undefined;
|
|
717
|
+
if (!isRuntimeRefresh) {
|
|
718
|
+
if (!fs_1.default.existsSync(configBakPath)) {
|
|
719
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
720
|
+
originalHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
|
|
721
|
+
fs_1.default.renameSync(configPath, configBakPath);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log('OpenCode backup file already exists, skipping backup step');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
729
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
730
|
+
}
|
|
731
|
+
// 读取当前配置(保留用户其它 provider/agent/command 等)
|
|
732
|
+
let currentConfig = {};
|
|
733
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
734
|
+
try {
|
|
735
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
console.warn('Failed to parse current opencode.json, using empty object:', error);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const model = (defaultModel && typeof defaultModel === 'string' && defaultModel.trim())
|
|
742
|
+
? defaultModel.trim()
|
|
743
|
+
: DEFAULT_OPENCODE_MODEL;
|
|
744
|
+
// 构建代理配置
|
|
745
|
+
const proxyConfig = {
|
|
746
|
+
provider: {
|
|
747
|
+
aicodeswitch: {
|
|
748
|
+
npm: '@ai-sdk/openai-compatible',
|
|
749
|
+
name: 'AICodeSwitch',
|
|
750
|
+
options: {
|
|
751
|
+
baseURL: `http://${clientHost}:${port}/opencode/v1`,
|
|
752
|
+
apiKey: 'api_key',
|
|
753
|
+
maxRetries: maxRetries !== null && maxRetries !== void 0 ? maxRetries : 5
|
|
754
|
+
},
|
|
755
|
+
models: {
|
|
756
|
+
[model]: { name: model }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
model: `aicodeswitch/${model}`
|
|
761
|
+
};
|
|
762
|
+
const mergedConfig = (0, config_merge_1.mergeJsonConfig)(proxyConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
|
|
763
|
+
(0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
|
|
764
|
+
// 保存元数据
|
|
765
|
+
const currentHash = (0, crypto_1.createHash)('sha256').update(fs_1.default.readFileSync(configPath, 'utf-8')).digest('hex');
|
|
766
|
+
const metadata = {
|
|
767
|
+
configType: 'opencode',
|
|
768
|
+
timestamp: Date.now(),
|
|
769
|
+
originalHash,
|
|
770
|
+
proxyMarker: `http://${host}:${port}/opencode`,
|
|
771
|
+
files: [
|
|
772
|
+
{
|
|
773
|
+
originalPath: configPath,
|
|
774
|
+
backupPath: configBakPath,
|
|
775
|
+
currentHash
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
(0, config_metadata_1.saveMetadata)(metadata);
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
console.error('Failed to write OpenCode config file:', error);
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
const restoreOpencodeConfig = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
788
|
+
try {
|
|
789
|
+
const configPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
790
|
+
const configBakPath = `${configPath}.aicodeswitch_backup`;
|
|
791
|
+
let restoredAnyFile = false;
|
|
792
|
+
if (fs_1.default.existsSync(configBakPath)) {
|
|
793
|
+
const backupConfig = JSON.parse(fs_1.default.readFileSync(configBakPath, 'utf-8'));
|
|
794
|
+
let currentConfig = {};
|
|
795
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
796
|
+
try {
|
|
797
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
console.warn('Failed to parse current opencode.json during restore, using empty object:', error);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// 备份作为基础,合并当前的非管理字段
|
|
804
|
+
const mergedConfig = (0, config_merge_1.mergeJsonConfig)(backupConfig, currentConfig, config_managed_fields_1.OPENCODE_CONFIG_MANAGED_FIELDS);
|
|
805
|
+
(0, config_merge_1.atomicWriteFile)(configPath, JSON.stringify(mergedConfig, null, 2));
|
|
806
|
+
fs_1.default.unlinkSync(configBakPath);
|
|
807
|
+
restoredAnyFile = true;
|
|
808
|
+
}
|
|
809
|
+
(0, config_metadata_1.deleteMetadata)('opencode');
|
|
810
|
+
return restoredAnyFile;
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
console.error('Failed to restore OpenCode config file:', error);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
const checkOpencodeBackupExists = () => {
|
|
818
|
+
try {
|
|
819
|
+
const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
820
|
+
return status.hasBackup;
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
console.error('Failed to check OpenCode backup:', error);
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
665
827
|
const checkClaudeBackupExists = () => {
|
|
666
828
|
try {
|
|
667
829
|
// 清理可能的无效元数据
|
|
@@ -696,26 +858,30 @@ const syncConfigsOnServerStartup = (dbManager) => __awaiter(void 0, void 0, void
|
|
|
696
858
|
const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
|
|
697
859
|
? config.claudeEffortLevel
|
|
698
860
|
: DEFAULT_CLAUDE_EFFORT_LEVEL;
|
|
699
|
-
const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride);
|
|
861
|
+
const claudeWritten = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, config.claudeMaxRetries);
|
|
700
862
|
console.log(`[Startup Config Sync] Claude Code config ${claudeWritten ? 'written' : 'skipped'}`);
|
|
701
863
|
const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
|
|
702
864
|
? config.codexModelReasoningEffort
|
|
703
865
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
704
|
-
const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories);
|
|
866
|
+
const codexWritten = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, config.codexMaxRetries);
|
|
705
867
|
console.log(`[Startup Config Sync] Codex config ${codexWritten ? 'written' : 'skipped'}`);
|
|
868
|
+
const opencodeWritten = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, config.opencodeMaxRetries);
|
|
869
|
+
console.log(`[Startup Config Sync] OpenCode config ${opencodeWritten ? 'written' : 'skipped'}`);
|
|
706
870
|
});
|
|
707
871
|
const syncConfigsOnGlobalConfigUpdate = (dbManager) => __awaiter(void 0, void 0, void 0, function* () {
|
|
708
872
|
const config = dbManager.getConfig();
|
|
709
873
|
const claudeEffortLevel = isClaudeEffortLevel(config.claudeEffortLevel)
|
|
710
874
|
? config.claudeEffortLevel
|
|
711
875
|
: DEFAULT_CLAUDE_EFFORT_LEVEL;
|
|
712
|
-
const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, { allowOverwriteRefresh: true });
|
|
876
|
+
const claudeUpdated = yield writeClaudeConfig(dbManager, config.enableAgentTeams, config.enableBypassPermissionsSupport, config.claudePermissionsDefaultMode, claudeEffortLevel, config.claudeDefaultModel, config.autocompactPctOverride, config.claudeMaxRetries, { allowOverwriteRefresh: true });
|
|
713
877
|
console.log(`[Config Update Sync] Claude Code config ${claudeUpdated ? 'written' : 'skipped'}`);
|
|
714
878
|
const modelReasoningEffort = isCodexReasoningEffort(config.codexModelReasoningEffort)
|
|
715
879
|
? config.codexModelReasoningEffort
|
|
716
880
|
: DEFAULT_CODEX_REASONING_EFFORT;
|
|
717
|
-
const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, { allowOverwriteRefresh: true });
|
|
881
|
+
const codexUpdated = yield writeCodexConfig(dbManager, modelReasoningEffort, config.codexDefaultModel, config.codexEnableMemories, config.codexMaxRetries, { allowOverwriteRefresh: true });
|
|
718
882
|
console.log(`[Config Update Sync] Codex config ${codexUpdated ? 'written' : 'skipped'}`);
|
|
883
|
+
const opencodeUpdated = yield writeOpencodeConfig(dbManager, config.opencodeDefaultModel, config.opencodeMaxRetries, { allowOverwriteRefresh: true });
|
|
884
|
+
console.log(`[Config Update Sync] OpenCode config ${opencodeUpdated ? 'written' : 'skipped'}`);
|
|
719
885
|
});
|
|
720
886
|
const getCentralSkillsDir = () => {
|
|
721
887
|
return path_1.default.join(os_1.default.homedir(), '.aicodeswitch', 'skills');
|
|
@@ -734,12 +900,20 @@ function getSkillDirByName(name) {
|
|
|
734
900
|
return path_1.default.join(centralDir, sanitizedName);
|
|
735
901
|
}
|
|
736
902
|
const getSkillSymlinkPath = (skillId, targetType) => {
|
|
903
|
+
if (targetType === 'opencode') {
|
|
904
|
+
// OpenCode 没有 skills 目录,映射为全局 command:~/.config/opencode/commands/<skillId>.md
|
|
905
|
+
return path_1.default.join(os_1.default.homedir(), '.config', 'opencode', 'commands', `${skillId}.md`);
|
|
906
|
+
}
|
|
737
907
|
const baseDir = targetType === 'claude-code' ? '.claude' : '.codex';
|
|
738
908
|
return path_1.default.join(os_1.default.homedir(), baseDir, 'skills', skillId);
|
|
739
909
|
};
|
|
740
910
|
function isSkillSymlinkExists(skillId, targetType) {
|
|
741
911
|
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
742
912
|
try {
|
|
913
|
+
if (targetType === 'opencode') {
|
|
914
|
+
// OpenCode 是普通文件,不是 symlink
|
|
915
|
+
return fs_1.default.existsSync(symlinkPath);
|
|
916
|
+
}
|
|
743
917
|
const stats = fs_1.default.lstatSync(symlinkPath);
|
|
744
918
|
return stats.isSymbolicLink();
|
|
745
919
|
}
|
|
@@ -747,15 +921,77 @@ function isSkillSymlinkExists(skillId, targetType) {
|
|
|
747
921
|
return false;
|
|
748
922
|
}
|
|
749
923
|
}
|
|
924
|
+
/**
|
|
925
|
+
* 从 SKILL.md 内容中剥离前导 YAML frontmatter,返回 { description?, body }
|
|
926
|
+
* 没有 frontmatter 时,description 为 undefined,body 为原文。
|
|
927
|
+
*/
|
|
928
|
+
function parseSkillMd(skillMdContent) {
|
|
929
|
+
const trimmed = skillMdContent.replace(/^/, '');
|
|
930
|
+
const fmMatch = trimmed.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
931
|
+
if (!fmMatch) {
|
|
932
|
+
return { body: trimmed };
|
|
933
|
+
}
|
|
934
|
+
const frontmatter = fmMatch[1];
|
|
935
|
+
const body = fmMatch[2];
|
|
936
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
937
|
+
return {
|
|
938
|
+
description: descMatch ? descMatch[1].replace(/^["']|["']$/g, '').trim() : undefined,
|
|
939
|
+
body: body.trim(),
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* 为 OpenCode 生成 command markdown 文件内容。
|
|
944
|
+
* frontmatter 用 description(OpenCode 在 TUI 展示),正文用 SKILL.md 的 body。
|
|
945
|
+
*/
|
|
946
|
+
function buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription) {
|
|
947
|
+
let description = fallbackDescription || '';
|
|
948
|
+
let body = '';
|
|
949
|
+
const skillMdPath = path_1.default.join(skillDir, 'SKILL.md');
|
|
950
|
+
if (fs_1.default.existsSync(skillMdPath)) {
|
|
951
|
+
try {
|
|
952
|
+
const parsed = parseSkillMd(fs_1.default.readFileSync(skillMdPath, 'utf-8'));
|
|
953
|
+
body = parsed.body || '';
|
|
954
|
+
if (!description && parsed.description) {
|
|
955
|
+
description = parsed.description;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
catch ( /* ignore */_a) { /* ignore */ }
|
|
959
|
+
}
|
|
960
|
+
if (!description) {
|
|
961
|
+
description = skillId;
|
|
962
|
+
}
|
|
963
|
+
const bodySection = body ? `\n${body}\n` : '\n';
|
|
964
|
+
return `---\ndescription: ${description.replace(/\n/g, ' ')}\nagent: build\n---\n${bodySection}`;
|
|
965
|
+
}
|
|
750
966
|
function createSkillSymlink(skillId, targetType) {
|
|
751
967
|
return __awaiter(this, void 0, void 0, function* () {
|
|
968
|
+
var _a;
|
|
752
969
|
try {
|
|
753
970
|
const centralDir = getCentralSkillsDir();
|
|
754
971
|
const skillDir = path_1.default.join(centralDir, skillId);
|
|
755
|
-
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
756
972
|
if (!fs_1.default.existsSync(skillDir)) {
|
|
757
973
|
return { success: false, error: 'Skill目录不存在' };
|
|
758
974
|
}
|
|
975
|
+
// OpenCode:生成 command markdown 写入 ~/.config/opencode/commands/<skillId>.md
|
|
976
|
+
if (targetType === 'opencode') {
|
|
977
|
+
const commandPath = getSkillSymlinkPath(skillId, targetType);
|
|
978
|
+
const commandDir = path_1.default.dirname(commandPath);
|
|
979
|
+
if (!fs_1.default.existsSync(commandDir)) {
|
|
980
|
+
fs_1.default.mkdirSync(commandDir, { recursive: true });
|
|
981
|
+
}
|
|
982
|
+
let fallbackDescription;
|
|
983
|
+
const metadataPath = path_1.default.join(skillDir, 'skill.json');
|
|
984
|
+
if (fs_1.default.existsSync(metadataPath)) {
|
|
985
|
+
try {
|
|
986
|
+
fallbackDescription = (_a = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'))) === null || _a === void 0 ? void 0 : _a.description;
|
|
987
|
+
}
|
|
988
|
+
catch ( /* ignore */_b) { /* ignore */ }
|
|
989
|
+
}
|
|
990
|
+
const content = buildOpencodeSkillCommandMarkdown(skillDir, skillId, fallbackDescription);
|
|
991
|
+
(0, config_merge_1.atomicWriteFile)(commandPath, content);
|
|
992
|
+
return { success: true };
|
|
993
|
+
}
|
|
994
|
+
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
759
995
|
const targetBaseDir = path_1.default.dirname(symlinkPath);
|
|
760
996
|
if (!fs_1.default.existsSync(targetBaseDir)) {
|
|
761
997
|
fs_1.default.mkdirSync(targetBaseDir, { recursive: true });
|
|
@@ -786,6 +1022,13 @@ function removeSkillSymlink(skillId, targetType) {
|
|
|
786
1022
|
return __awaiter(this, void 0, void 0, function* () {
|
|
787
1023
|
try {
|
|
788
1024
|
const symlinkPath = getSkillSymlinkPath(skillId, targetType);
|
|
1025
|
+
if (targetType === 'opencode') {
|
|
1026
|
+
// OpenCode:删除 command 文件
|
|
1027
|
+
if (fs_1.default.existsSync(symlinkPath)) {
|
|
1028
|
+
fs_1.default.unlinkSync(symlinkPath);
|
|
1029
|
+
}
|
|
1030
|
+
return { success: true };
|
|
1031
|
+
}
|
|
789
1032
|
if (!fs_1.default.existsSync(symlinkPath)) {
|
|
790
1033
|
return { success: true };
|
|
791
1034
|
}
|
|
@@ -1044,7 +1287,7 @@ const listInstalledSkills = () => {
|
|
|
1044
1287
|
try {
|
|
1045
1288
|
const metadata = JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf-8'));
|
|
1046
1289
|
const enabledTargets = [];
|
|
1047
|
-
['claude-code', 'codex'].forEach((targetType) => {
|
|
1290
|
+
['claude-code', 'codex', 'opencode'].forEach((targetType) => {
|
|
1048
1291
|
if (isSkillSymlinkExists(skillId, targetType)) {
|
|
1049
1292
|
enabledTargets.push(targetType);
|
|
1050
1293
|
}
|
|
@@ -1076,7 +1319,7 @@ const listInstalledSkills = () => {
|
|
|
1076
1319
|
const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1077
1320
|
updateProxyConfig(dbManager.getConfig());
|
|
1078
1321
|
app.get('/health', (_req, res) => res.json({ status: 'ok' }));
|
|
1079
|
-
//
|
|
1322
|
+
// 数据就绪验证端点(供桌面端 Electron 启动阶段确认后端完全可用)
|
|
1080
1323
|
app.get('/api/ready', (_req, res) => {
|
|
1081
1324
|
const vendors = dbManager.getVendors();
|
|
1082
1325
|
const routes = dbManager.getRoutes();
|
|
@@ -1196,7 +1439,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1196
1439
|
if (!tool || !routeId) {
|
|
1197
1440
|
return res.status(400).json({ error: 'tool and routeId are required' });
|
|
1198
1441
|
}
|
|
1199
|
-
if (tool !== 'claude-code' && tool !== 'codex') {
|
|
1442
|
+
if (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode') {
|
|
1200
1443
|
return res.status(400).json({ error: 'Invalid tool name' });
|
|
1201
1444
|
}
|
|
1202
1445
|
const route = dbManager.getRoute(routeId);
|
|
@@ -1217,7 +1460,7 @@ const registerRoutes = (dbManager, proxyServer) => __awaiter(void 0, void 0, voi
|
|
|
1217
1460
|
})));
|
|
1218
1461
|
app.post('/api/tool-bindings/deactivate', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1219
1462
|
const { tool } = req.body;
|
|
1220
|
-
if (!tool || (tool !== 'claude-code' && tool !== 'codex')) {
|
|
1463
|
+
if (!tool || (tool !== 'claude-code' && tool !== 'codex' && tool !== 'opencode')) {
|
|
1221
1464
|
return res.status(400).json({ error: 'Invalid tool name' });
|
|
1222
1465
|
}
|
|
1223
1466
|
const result = yield dbManager.deactivateToolRoute(tool);
|
|
@@ -2005,7 +2248,7 @@ ${instruction}
|
|
|
2005
2248
|
app.post('/api/skills/:skillId/enable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2006
2249
|
const { skillId } = req.params;
|
|
2007
2250
|
const { targetType } = req.body;
|
|
2008
|
-
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
2251
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
|
|
2009
2252
|
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
2010
2253
|
return;
|
|
2011
2254
|
}
|
|
@@ -2036,7 +2279,7 @@ ${instruction}
|
|
|
2036
2279
|
app.post('/api/skills/:skillId/disable', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2037
2280
|
const { skillId } = req.params;
|
|
2038
2281
|
const { targetType } = req.body;
|
|
2039
|
-
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex')) {
|
|
2282
|
+
if (!targetType || (targetType !== 'claude-code' && targetType !== 'codex' && targetType !== 'opencode')) {
|
|
2040
2283
|
res.status(400).json({ success: false, error: '无效的目标类型' });
|
|
2041
2284
|
return;
|
|
2042
2285
|
}
|
|
@@ -2065,7 +2308,7 @@ ${instruction}
|
|
|
2065
2308
|
res.status(404).json({ success: false, error: 'Skill不存在' });
|
|
2066
2309
|
return;
|
|
2067
2310
|
}
|
|
2068
|
-
['claude-code', 'codex'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2311
|
+
['claude-code', 'codex', 'opencode'].forEach((targetType) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2069
2312
|
yield removeSkillSymlink(skillId, targetType);
|
|
2070
2313
|
}));
|
|
2071
2314
|
fs_1.default.rmSync(skillDir, { recursive: true, force: true });
|
|
@@ -2087,7 +2330,7 @@ ${instruction}
|
|
|
2087
2330
|
: isClaudePermissionDefaultMode(appConfig.claudePermissionsDefaultMode)
|
|
2088
2331
|
? appConfig.claudePermissionsDefaultMode
|
|
2089
2332
|
: DEFAULT_CLAUDE_PERMISSION_DEFAULT_MODE;
|
|
2090
|
-
const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride);
|
|
2333
|
+
const result = yield writeClaudeConfig(dbManager, enableAgentTeams, enableBypassPermissionsSupport, permissionsDefaultMode, undefined, appConfig.claudeDefaultModel, appConfig.autocompactPctOverride, appConfig.claudeMaxRetries);
|
|
2091
2334
|
applyWriteLocalRecords(proxyServer);
|
|
2092
2335
|
res.json(result);
|
|
2093
2336
|
})));
|
|
@@ -2103,7 +2346,16 @@ ${instruction}
|
|
|
2103
2346
|
const enableMemories = requestedEnableMemories !== undefined
|
|
2104
2347
|
? !!requestedEnableMemories
|
|
2105
2348
|
: !!appConfig.codexEnableMemories;
|
|
2106
|
-
const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories);
|
|
2349
|
+
const result = yield writeCodexConfig(dbManager, modelReasoningEffort, appConfig.codexDefaultModel, enableMemories, appConfig.codexMaxRetries);
|
|
2350
|
+
applyWriteLocalRecords(proxyServer);
|
|
2351
|
+
res.json(result);
|
|
2352
|
+
})));
|
|
2353
|
+
app.post('/api/write-config/opencode', asyncHandler((req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2354
|
+
var _a;
|
|
2355
|
+
const appConfig = dbManager.getConfig();
|
|
2356
|
+
const requestedModel = typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a.defaultModel) === 'string' ? req.body.defaultModel : undefined;
|
|
2357
|
+
const defaultModel = requestedModel || appConfig.opencodeDefaultModel;
|
|
2358
|
+
const result = yield writeOpencodeConfig(dbManager, defaultModel, appConfig.opencodeMaxRetries);
|
|
2107
2359
|
applyWriteLocalRecords(proxyServer);
|
|
2108
2360
|
res.json(result);
|
|
2109
2361
|
})));
|
|
@@ -2160,12 +2412,19 @@ ${instruction}
|
|
|
2160
2412
|
const result = yield restoreCodexConfig();
|
|
2161
2413
|
res.json(result);
|
|
2162
2414
|
})));
|
|
2415
|
+
app.post('/api/restore-config/opencode', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2416
|
+
const result = yield restoreOpencodeConfig();
|
|
2417
|
+
res.json(result);
|
|
2418
|
+
})));
|
|
2163
2419
|
app.get('/api/check-backup/claude', (_req, res) => {
|
|
2164
2420
|
res.json({ exists: checkClaudeBackupExists() });
|
|
2165
2421
|
});
|
|
2166
2422
|
app.get('/api/check-backup/codex', (_req, res) => {
|
|
2167
2423
|
res.json({ exists: checkCodexBackupExists() });
|
|
2168
2424
|
});
|
|
2425
|
+
app.get('/api/check-backup/opencode', (_req, res) => {
|
|
2426
|
+
res.json({ exists: checkOpencodeBackupExists() });
|
|
2427
|
+
});
|
|
2169
2428
|
// 新的详细配置状态 API 端点
|
|
2170
2429
|
app.get('/api/config-status/claude', (_req, res) => {
|
|
2171
2430
|
const status = (0, config_metadata_1.checkClaudeConfigStatus)();
|
|
@@ -2175,6 +2434,10 @@ ${instruction}
|
|
|
2175
2434
|
const status = (0, config_metadata_1.checkCodexConfigStatus)();
|
|
2176
2435
|
res.json(status);
|
|
2177
2436
|
});
|
|
2437
|
+
app.get('/api/config-status/opencode', (_req, res) => {
|
|
2438
|
+
const status = (0, config_metadata_1.checkOpencodeConfigStatus)();
|
|
2439
|
+
res.json(status);
|
|
2440
|
+
});
|
|
2178
2441
|
// API 路径路由映射
|
|
2179
2442
|
app.get('/api/api-path-bindings', (_req, res) => {
|
|
2180
2443
|
res.json({ bindings: dbManager.getApiPathBindings(), models: dbManager.getApiPathModels() });
|
|
@@ -2939,6 +3202,31 @@ ${instruction}
|
|
|
2939
3202
|
(0, config_merge_1.atomicWriteFile)(authPath, JSON.stringify(auth, null, 2));
|
|
2940
3203
|
results['codex'] = true;
|
|
2941
3204
|
}
|
|
3205
|
+
else if (target === 'opencode') {
|
|
3206
|
+
// 写入 ~/.config/opencode/opencode.json 的 provider.aicodeswitch.options.apiKey
|
|
3207
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
3208
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
3209
|
+
if (!fs_1.default.existsSync(opencodeDir)) {
|
|
3210
|
+
fs_1.default.mkdirSync(opencodeDir, { recursive: true });
|
|
3211
|
+
}
|
|
3212
|
+
let oc = {};
|
|
3213
|
+
if (fs_1.default.existsSync(opencodeConfigPath)) {
|
|
3214
|
+
try {
|
|
3215
|
+
oc = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
3216
|
+
}
|
|
3217
|
+
catch ( /* ignore */_c) { /* ignore */ }
|
|
3218
|
+
}
|
|
3219
|
+
if (!oc.provider)
|
|
3220
|
+
oc.provider = {};
|
|
3221
|
+
if (!oc.provider.aicodeswitch || typeof oc.provider.aicodeswitch !== 'object') {
|
|
3222
|
+
oc.provider.aicodeswitch = { npm: '@ai-sdk/openai-compatible', name: 'AICodeSwitch', options: {} };
|
|
3223
|
+
}
|
|
3224
|
+
if (!oc.provider.aicodeswitch.options)
|
|
3225
|
+
oc.provider.aicodeswitch.options = {};
|
|
3226
|
+
oc.provider.aicodeswitch.options.apiKey = key.apiKey;
|
|
3227
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(oc, null, 2));
|
|
3228
|
+
results['opencode'] = true;
|
|
3229
|
+
}
|
|
2942
3230
|
}
|
|
2943
3231
|
catch (error) {
|
|
2944
3232
|
console.error(`Failed to write local config for ${target}:`, error);
|
|
@@ -3319,6 +3607,76 @@ ${instruction}
|
|
|
3319
3607
|
console.log(`[MCP] Codex MCP config written: ${writtenMcpIds.length} server(s)`);
|
|
3320
3608
|
return true;
|
|
3321
3609
|
}
|
|
3610
|
+
else if (targetType === 'opencode') {
|
|
3611
|
+
// OpenCode 使用 JSON 格式的 opencode.json,MCP 配置位于 mcp 段
|
|
3612
|
+
// 格式:local → { type:"local", command:[...], enabled:true, env? }
|
|
3613
|
+
// remote → { type:"remote", url, enabled:true, headers? }
|
|
3614
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
3615
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
3616
|
+
if (!fs_1.default.existsSync(opencodeDir)) {
|
|
3617
|
+
fs_1.default.mkdirSync(opencodeDir, { recursive: true });
|
|
3618
|
+
}
|
|
3619
|
+
let currentConfig = {};
|
|
3620
|
+
if (fs_1.default.existsSync(opencodeConfigPath)) {
|
|
3621
|
+
try {
|
|
3622
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
3623
|
+
}
|
|
3624
|
+
catch (error) {
|
|
3625
|
+
console.warn('[MCP] Failed to parse opencode.json:', error);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
// 清除代理上次写入的 mcp 条目(通过 metadata 追踪,避免误删用户自配的 mcp)
|
|
3629
|
+
const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
|
|
3630
|
+
let previousMcpIds = [];
|
|
3631
|
+
if (fs_1.default.existsSync(mcpMetaPath)) {
|
|
3632
|
+
try {
|
|
3633
|
+
previousMcpIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
|
|
3634
|
+
for (const id of previousMcpIds) {
|
|
3635
|
+
if (currentConfig.mcp && currentConfig.mcp[id]) {
|
|
3636
|
+
delete currentConfig.mcp[id];
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
catch (_b) {
|
|
3641
|
+
// ignore
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
if (!currentConfig.mcp) {
|
|
3645
|
+
currentConfig.mcp = {};
|
|
3646
|
+
}
|
|
3647
|
+
const writtenMcpIds = [];
|
|
3648
|
+
for (const mcp of mcps) {
|
|
3649
|
+
const mcpConfig = { enabled: true };
|
|
3650
|
+
if (mcp.type === 'stdio') {
|
|
3651
|
+
mcpConfig.type = 'local';
|
|
3652
|
+
const cmdParts = [mcp.command || ''];
|
|
3653
|
+
if (Array.isArray(mcp.args)) {
|
|
3654
|
+
cmdParts.push(...mcp.args);
|
|
3655
|
+
}
|
|
3656
|
+
mcpConfig.command = cmdParts.filter((c, i) => i === 0 ? c !== '' : true);
|
|
3657
|
+
if (mcp.env && Object.keys(mcp.env).length > 0) {
|
|
3658
|
+
mcpConfig.environment = Object.assign({}, mcp.env);
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
else {
|
|
3662
|
+
// http / sse 均使用 remote 类型
|
|
3663
|
+
mcpConfig.type = 'remote';
|
|
3664
|
+
mcpConfig.url = mcp.url || '';
|
|
3665
|
+
if (mcp.headers && Object.keys(mcp.headers).length > 0) {
|
|
3666
|
+
mcpConfig.headers = Object.assign({}, mcp.headers);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
currentConfig.mcp[mcp.id] = mcpConfig;
|
|
3670
|
+
writtenMcpIds.push(mcp.id);
|
|
3671
|
+
}
|
|
3672
|
+
if (Object.keys(currentConfig.mcp).length === 0) {
|
|
3673
|
+
delete currentConfig.mcp;
|
|
3674
|
+
}
|
|
3675
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
|
|
3676
|
+
fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(writtenMcpIds, null, 2));
|
|
3677
|
+
console.log(`[MCP] OpenCode MCP config written: ${writtenMcpIds.length} server(s)`);
|
|
3678
|
+
return true;
|
|
3679
|
+
}
|
|
3322
3680
|
return false;
|
|
3323
3681
|
}
|
|
3324
3682
|
catch (error) {
|
|
@@ -3381,6 +3739,41 @@ ${instruction}
|
|
|
3381
3739
|
}
|
|
3382
3740
|
return true;
|
|
3383
3741
|
}
|
|
3742
|
+
else if (targetType === 'opencode') {
|
|
3743
|
+
const opencodeConfigPath = (0, config_metadata_1.getOpencodeConfigPath)();
|
|
3744
|
+
if (!fs_1.default.existsSync(opencodeConfigPath)) {
|
|
3745
|
+
return true;
|
|
3746
|
+
}
|
|
3747
|
+
let currentConfig = {};
|
|
3748
|
+
try {
|
|
3749
|
+
currentConfig = JSON.parse(fs_1.default.readFileSync(opencodeConfigPath, 'utf-8'));
|
|
3750
|
+
}
|
|
3751
|
+
catch (error) {
|
|
3752
|
+
console.warn('[MCP] Failed to parse opencode.json for removal:', error);
|
|
3753
|
+
return false;
|
|
3754
|
+
}
|
|
3755
|
+
if (currentConfig.mcp && currentConfig.mcp[mcpId]) {
|
|
3756
|
+
delete currentConfig.mcp[mcpId];
|
|
3757
|
+
if (Object.keys(currentConfig.mcp).length === 0) {
|
|
3758
|
+
delete currentConfig.mcp;
|
|
3759
|
+
}
|
|
3760
|
+
(0, config_merge_1.atomicWriteFile)(opencodeConfigPath, JSON.stringify(currentConfig, null, 2));
|
|
3761
|
+
const opencodeDir = path_1.default.dirname(opencodeConfigPath);
|
|
3762
|
+
const mcpMetaPath = path_1.default.join(opencodeDir, '.aicodeswitch_mcp_servers.json');
|
|
3763
|
+
if (fs_1.default.existsSync(mcpMetaPath)) {
|
|
3764
|
+
try {
|
|
3765
|
+
const previousIds = JSON.parse(fs_1.default.readFileSync(mcpMetaPath, 'utf8'));
|
|
3766
|
+
const updatedIds = previousIds.filter(id => id !== mcpId);
|
|
3767
|
+
fs_1.default.writeFileSync(mcpMetaPath, JSON.stringify(updatedIds, null, 2));
|
|
3768
|
+
}
|
|
3769
|
+
catch (_b) {
|
|
3770
|
+
// ignore
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
console.log(`[MCP] Removed MCP ${mcpId} from OpenCode config`);
|
|
3774
|
+
}
|
|
3775
|
+
return true;
|
|
3776
|
+
}
|
|
3384
3777
|
return false;
|
|
3385
3778
|
}
|
|
3386
3779
|
catch (error) {
|
|
@@ -3413,6 +3806,26 @@ ${instruction}
|
|
|
3413
3806
|
});
|
|
3414
3807
|
// listen 就绪标志:区分"启动阶段"与"运行阶段",启动期致命异常应让进程退出
|
|
3415
3808
|
let listenReady = false;
|
|
3809
|
+
/**
|
|
3810
|
+
* 是否以「内嵌进程」模式运行(例如被 Electron 主进程直接 require 并调用 start)。
|
|
3811
|
+
* 该模式下:
|
|
3812
|
+
* - shutdown 流程结束后不调用 process.exit,把退出时机交还给宿主(Electron)
|
|
3813
|
+
* - 模块被 require 时不自动执行 start(),由宿主显式调用导出的 start
|
|
3814
|
+
*/
|
|
3815
|
+
const IN_PROCESS = process.env.AIC_IN_PROCESS === '1';
|
|
3816
|
+
// 保存当前服务实例的优雅关闭函数,供宿主(Electron 主进程)在退出前显式调用。
|
|
3817
|
+
let _gracefulShutdown = null;
|
|
3818
|
+
/**
|
|
3819
|
+
* 供宿主进程调用的优雅关闭入口。
|
|
3820
|
+
* 仅在 start() 成功注册 shutdown 后可用;调用后会恢复工具配置、关闭 DB / 日志、释放端口,
|
|
3821
|
+
* 内嵌模式下不会触发 process.exit。
|
|
3822
|
+
*/
|
|
3823
|
+
const gracefulShutdown = (signal = 'HOST_QUIT') => {
|
|
3824
|
+
if (_gracefulShutdown)
|
|
3825
|
+
return _gracefulShutdown(signal);
|
|
3826
|
+
return Promise.resolve();
|
|
3827
|
+
};
|
|
3828
|
+
exports.gracefulShutdown = gracefulShutdown;
|
|
3416
3829
|
const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
3417
3830
|
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
3418
3831
|
// 自动检测数据库类型并执行迁移(如果需要)
|
|
@@ -3590,6 +4003,13 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3590
4003
|
catch (error) {
|
|
3591
4004
|
console.error('[Shutdown ...] Failed to restore Codex config:', error);
|
|
3592
4005
|
}
|
|
4006
|
+
try {
|
|
4007
|
+
const opencodeRestored = yield restoreOpencodeConfig();
|
|
4008
|
+
console.log(`[Shutdown ...] OpenCode config ${opencodeRestored ? 'restored' : 'was not modified'}`);
|
|
4009
|
+
}
|
|
4010
|
+
catch (error) {
|
|
4011
|
+
console.error('[Shutdown ...] Failed to restore OpenCode config:', error);
|
|
4012
|
+
}
|
|
3593
4013
|
// Shutdown AccessKey module
|
|
3594
4014
|
try {
|
|
3595
4015
|
yield accessKeyModule.shutdown();
|
|
@@ -3623,25 +4043,31 @@ const start = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
3623
4043
|
})
|
|
3624
4044
|
]);
|
|
3625
4045
|
console.log('Server stopped.');
|
|
3626
|
-
|
|
4046
|
+
// 内嵌进程模式下不主动退出,交由宿主(Electron)控制进程生命周期
|
|
4047
|
+
if (!IN_PROCESS) {
|
|
4048
|
+
process.exit(0);
|
|
4049
|
+
}
|
|
3627
4050
|
}))();
|
|
3628
4051
|
return shutdownPromise;
|
|
3629
4052
|
});
|
|
4053
|
+
// 注册到模块级句柄,供宿主在退出前显式触发完整关闭流程
|
|
4054
|
+
_gracefulShutdown = shutdown;
|
|
3630
4055
|
process.on('SIGINT', () => { void shutdown('SIGINT'); });
|
|
3631
4056
|
process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
|
|
3632
|
-
// 优雅关闭端点(供
|
|
4057
|
+
// 优雅关闭端点(供 Electron 等外部调用者触发服务端完整清理流程)
|
|
3633
4058
|
// 放在 shutdown 定义之后注册,确保闭包可引用
|
|
3634
4059
|
app.post('/api/shutdown', asyncHandler((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
3635
4060
|
res.json({ success: true });
|
|
3636
4061
|
setImmediate(() => { void shutdown('HTTP_SHUTDOWN'); });
|
|
3637
4062
|
})));
|
|
3638
4063
|
});
|
|
4064
|
+
exports.start = start;
|
|
3639
4065
|
// 全局未捕获异常处理 - 防止服务崩溃
|
|
3640
4066
|
process.on('uncaughtException', (error) => {
|
|
3641
4067
|
console.error('[Uncaught Exception] 服务遇到未捕获的异常:', error);
|
|
3642
4068
|
console.error('[Uncaught Exception] 堆栈信息:', error.stack);
|
|
3643
4069
|
// 启动阶段(listen 之前)的异常通常是致命的(依赖加载失败、初始化崩溃等),
|
|
3644
|
-
// 静默吞掉会导致"进程在但不 listen"
|
|
4070
|
+
// 静默吞掉会导致"进程在但不 listen",桌面端只能干等超时;此时退出让上层重新探测/诊断。
|
|
3645
4071
|
if (!listenReady) {
|
|
3646
4072
|
console.error('[Uncaught Exception] 发生在服务监听之前,退出进程');
|
|
3647
4073
|
process.exit(1);
|
|
@@ -3654,7 +4080,11 @@ process.on('unhandledRejection', (reason) => {
|
|
|
3654
4080
|
process.exit(1);
|
|
3655
4081
|
}
|
|
3656
4082
|
});
|
|
3657
|
-
start
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
4083
|
+
// 仅在被直接运行时(如 aicos start → node dist/server/main.js)自动启动;
|
|
4084
|
+
// 被 Electron 主进程 require 时(require.main !== module 或 IN_PROCESS)由宿主显式调用 start()。
|
|
4085
|
+
if (!IN_PROCESS && require.main === module) {
|
|
4086
|
+
(0, exports.start)().catch((error) => {
|
|
4087
|
+
console.error('Failed to start server:', error);
|
|
4088
|
+
process.exit(1);
|
|
4089
|
+
});
|
|
4090
|
+
}
|