aicodeswitch 1.0.0 → 1.1.0
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/CLAUDE.md +182 -0
- package/README.md +6 -0
- package/bin/cli.js +35 -3
- package/bin/update.js +251 -0
- package/bin/version.js +97 -0
- package/dist/server/database.js +22 -11
- package/dist/server/proxy-server.js +134 -29
- package/dist/ui/assets/index-CL2KLI0M.js +278 -0
- package/dist/ui/assets/index-CRLNbjRB.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-BN77E7-U.js +0 -259
- package/dist/ui/assets/index-CaNSVfpD.css +0 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
```
|
|
2
|
+
# CLAUDE.md
|
|
3
|
+
|
|
4
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
## AI Code Switch - Project Overview
|
|
8
|
+
|
|
9
|
+
AI Code Switch is a local proxy server that manages AI programming tool connections to large language models, allowing tools like Claude Code and Codex to use custom model APIs instead of official ones.
|
|
10
|
+
|
|
11
|
+
## Development Commands
|
|
12
|
+
|
|
13
|
+
### Installation
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Development
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev # Run both UI and server in watch mode
|
|
21
|
+
npm run dev:ui # Run only React UI (Vite dev server)
|
|
22
|
+
npm run dev:server # Run only Node.js server (TSX watch)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Build
|
|
26
|
+
```bash
|
|
27
|
+
npm run build # Build both UI and server
|
|
28
|
+
npm run build:ui # Build React UI to dist/ui
|
|
29
|
+
npm run build:server # Build TypeScript server to dist/server
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Linting
|
|
33
|
+
```bash
|
|
34
|
+
npm run lint # Run ESLint on all .ts/.tsx files
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### CLI Commands
|
|
38
|
+
```bash
|
|
39
|
+
npm link # Link local package for CLI testing
|
|
40
|
+
aicos start # Start the proxy server
|
|
41
|
+
aicos stop # Stop the proxy server
|
|
42
|
+
aicos restart # Restart the proxy server
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
### High-Level Structure
|
|
48
|
+
```
|
|
49
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ AI Code Switch │
|
|
51
|
+
├─────────────────────────────────────────────────────────────┤
|
|
52
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
53
|
+
│ │ React UI │ │ Express API │ │ Proxy Core │ │
|
|
54
|
+
│ │ (Vite dev) │ │ (Node.js) │ │ │ │
|
|
55
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
56
|
+
│ │ │ │ │
|
|
57
|
+
│ └──────────────┼──────────────┘ │
|
|
58
|
+
│ ▼ │
|
|
59
|
+
│ ┌──────────────┐ │
|
|
60
|
+
│ │ Database │ │
|
|
61
|
+
│ │ (SQLite3) │ │
|
|
62
|
+
│ └──────────────┘ │
|
|
63
|
+
│ │ │
|
|
64
|
+
│ ▼ │
|
|
65
|
+
│ ┌──────────────┐ │
|
|
66
|
+
│ │ Transformers │ │
|
|
67
|
+
│ │ (Stream/SSE) │ │
|
|
68
|
+
│ └──────────────┘ │
|
|
69
|
+
│ │ │
|
|
70
|
+
│ ▼ │
|
|
71
|
+
│ ┌──────────────┐ │
|
|
72
|
+
│ │ Upstream │ │
|
|
73
|
+
│ │ APIs (LLMs) │ │
|
|
74
|
+
│ └──────────────┘ │
|
|
75
|
+
└─────────────────────────────────────────────────────────────┘
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Core Components
|
|
79
|
+
|
|
80
|
+
#### 1. Server (Node.js/Express) - `server/main.ts`
|
|
81
|
+
- Main entry point
|
|
82
|
+
- Configures Express with CORS, body parsing
|
|
83
|
+
- Reads configuration from `~/.aicodeswitch/aicodeswitch.conf`
|
|
84
|
+
- Sets up authentication middleware
|
|
85
|
+
- Registers all API routes
|
|
86
|
+
- Initializes database and proxy server
|
|
87
|
+
|
|
88
|
+
#### 2. Proxy Server - `server/proxy-server.ts`
|
|
89
|
+
- **Route Matching**: Finds active route based on target type (claude-code/codex)
|
|
90
|
+
- **Rule Matching**: Determines content type from request (image-understanding/thinking/long-context/background/default)
|
|
91
|
+
- **Request Transformation**: Converts between different API formats (Claude ↔ OpenAI ↔ OpenAI Responses)
|
|
92
|
+
- **Streaming**: Handles SSE (Server-Sent Events) streaming responses with real-time transformation
|
|
93
|
+
- **Logging**: Tracks requests, responses, and errors
|
|
94
|
+
|
|
95
|
+
#### 3. Transformers - `server/transformers/`
|
|
96
|
+
- **streaming.ts**: SSE parsing/serialization and event transformation
|
|
97
|
+
- **claude-openai.ts**: Claude ↔ OpenAI Chat format conversion
|
|
98
|
+
- **openai-responses.ts**: OpenAI Responses format conversion
|
|
99
|
+
- **chunk-collector.ts**: Collects streaming chunks for logging
|
|
100
|
+
|
|
101
|
+
#### 4. Database - `server/database.ts`
|
|
102
|
+
- SQLite3 database wrapper
|
|
103
|
+
- Manages: Vendors, API Services, Routes, Rules, Logs
|
|
104
|
+
- Configuration storage (API key, logging settings, etc.)
|
|
105
|
+
|
|
106
|
+
#### 5. UI (React) - `ui/`
|
|
107
|
+
- Main app: `App.tsx` - Navigation and layout
|
|
108
|
+
- Pages:
|
|
109
|
+
- `VendorsPage.tsx` - Manage AI service vendors
|
|
110
|
+
- `RoutesPage.tsx` - Configure routing rules
|
|
111
|
+
- `LogsPage.tsx` - View request/access/error logs
|
|
112
|
+
- `SettingsPage.tsx` - Application settings
|
|
113
|
+
- `WriteConfigPage.tsx` - Overwrite Claude Code/Codex config files
|
|
114
|
+
- `UsagePage.tsx` - Usage statistics
|
|
115
|
+
|
|
116
|
+
#### 6. Types - `types/`
|
|
117
|
+
- TypeScript type definitions for:
|
|
118
|
+
- Database models (Vendors, Services, Routes, Rules)
|
|
119
|
+
- API requests/responses
|
|
120
|
+
- Configuration
|
|
121
|
+
- Token usage tracking
|
|
122
|
+
|
|
123
|
+
#### 7. CLI - `bin/`
|
|
124
|
+
- `cli.js` - Main CLI entry point
|
|
125
|
+
- `start.js` - Server startup with PID management
|
|
126
|
+
- `stop.js` - Server shutdown
|
|
127
|
+
- `restart.js` - Restart server
|
|
128
|
+
|
|
129
|
+
## Key Features
|
|
130
|
+
|
|
131
|
+
### Routing System
|
|
132
|
+
- **Routes**: Define target type (Claude Code or Codex) and activation status
|
|
133
|
+
- **Rules**: Match requests by content type and route to specific API services
|
|
134
|
+
- **Content Type Detection**:
|
|
135
|
+
- `image-understanding`: Requests with image content
|
|
136
|
+
- `thinking`: Requests with reasoning/thinking signals
|
|
137
|
+
- `long-context`: Requests with large context (≥12000 chars or ≥8000 max tokens)
|
|
138
|
+
- `background`: Background/priority requests
|
|
139
|
+
- `default`: All other requests
|
|
140
|
+
|
|
141
|
+
### Request Transformation
|
|
142
|
+
- Supports multiple source types:
|
|
143
|
+
- OpenAI Chat
|
|
144
|
+
- OpenAI Code
|
|
145
|
+
- OpenAI Responses
|
|
146
|
+
- Claude Chat
|
|
147
|
+
- Claude Code
|
|
148
|
+
- DeepSeek Chat
|
|
149
|
+
|
|
150
|
+
### Configuration Management
|
|
151
|
+
- Writes/ restores Claude Code config files (`~/.claude/settings.json`, `~/.claude.json`)
|
|
152
|
+
- Writes/ restores Codex config files (`~/.codex/config.toml`, `~/.codex/auth.json`)
|
|
153
|
+
- Exports/ imports encrypted configuration data
|
|
154
|
+
|
|
155
|
+
### Logging
|
|
156
|
+
- Request logs: Detailed API call records with token usage
|
|
157
|
+
- Access logs: System access records
|
|
158
|
+
- Error logs: Error and exception records
|
|
159
|
+
|
|
160
|
+
## Development Tips
|
|
161
|
+
|
|
162
|
+
1. **Environment Variables**: Copy `.env.example` to `.env` and modify as needed
|
|
163
|
+
2. **Data Directory**: Default: `~/.aicodeswitch/data/` (SQLite3 database)
|
|
164
|
+
3. **Config File**: `~/.aicodeswitch/aicodeswitch.conf` (HOST, PORT, AUTH)
|
|
165
|
+
4. **Dev Ports**: UI (4568), Server (4567) - configured in `vite.config.ts` and `server/main.ts`
|
|
166
|
+
5. **API Endpoints**: All routes are prefixed with `/api/` except proxy routes (`/claude-code/`, `/codex/`)
|
|
167
|
+
|
|
168
|
+
## Build and Deployment
|
|
169
|
+
|
|
170
|
+
1. Run `npm run build` to create production builds
|
|
171
|
+
2. UI build outputs to `dist/ui/` (static files)
|
|
172
|
+
3. Server build outputs to `dist/server/` (JavaScript)
|
|
173
|
+
4. Configuration files are created in user's home directory on first run
|
|
174
|
+
|
|
175
|
+
## Technology Stack
|
|
176
|
+
|
|
177
|
+
- **Backend**: Node.js, Express, TypeScript, SQLite3
|
|
178
|
+
- **Frontend**: React 18, TypeScript, Vite, React Router
|
|
179
|
+
- **Streaming**: SSE (Server-Sent Events)
|
|
180
|
+
- **HTTP Client**: Axios
|
|
181
|
+
- **Encryption**: CryptoJS (AES)
|
|
182
|
+
- **CLI**: Yargs-like custom implementation
|
package/README.md
CHANGED
package/bin/cli.js
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
3
7
|
const args = process.argv.slice(2);
|
|
4
8
|
const command = args[0];
|
|
5
9
|
|
|
10
|
+
// 检查是否有更新版本的 current 文件
|
|
11
|
+
const CURRENT_FILE = path.join(os.homedir(), '.aicodeswitch', 'current');
|
|
12
|
+
|
|
13
|
+
let binDir = __dirname;
|
|
14
|
+
let useLocalVersion = true;
|
|
15
|
+
|
|
16
|
+
// 如果存在 current 文件,使用更新版本的脚本
|
|
17
|
+
if (fs.existsSync(CURRENT_FILE)) {
|
|
18
|
+
try {
|
|
19
|
+
const currentPath = fs.readFileSync(CURRENT_FILE, 'utf-8').trim();
|
|
20
|
+
const currentBinDir = path.join(currentPath, 'bin');
|
|
21
|
+
|
|
22
|
+
// 检查新版本的 bin 目录是否存在
|
|
23
|
+
if (fs.existsSync(currentBinDir) && fs.existsSync(path.join(currentBinDir, 'cli.js'))) {
|
|
24
|
+
binDir = currentBinDir;
|
|
25
|
+
useLocalVersion = false;
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// 读取失败,使用本地版本
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
const commands = {
|
|
7
|
-
start: () => require('
|
|
8
|
-
stop: () => require('
|
|
9
|
-
restart: () => require('
|
|
33
|
+
start: () => require(path.join(binDir, 'start')),
|
|
34
|
+
stop: () => require(path.join(binDir, 'stop')),
|
|
35
|
+
restart: () => require(path.join(binDir, 'restart')),
|
|
36
|
+
update: () => require(path.join(binDir, 'update')),
|
|
37
|
+
version: () => require(path.join(binDir, 'version')),
|
|
10
38
|
};
|
|
11
39
|
|
|
12
40
|
if (!command || !commands[command]) {
|
|
@@ -17,11 +45,15 @@ Commands:
|
|
|
17
45
|
start Start the AI Code Switch server
|
|
18
46
|
stop Stop the AI Code Switch server
|
|
19
47
|
restart Restart the AI Code Switch server
|
|
48
|
+
update Update to the latest version and restart
|
|
49
|
+
version Show current version information
|
|
20
50
|
|
|
21
51
|
Example:
|
|
22
52
|
aicos start
|
|
23
53
|
aicos stop
|
|
24
54
|
aicos restart
|
|
55
|
+
aicos update
|
|
56
|
+
aicos version
|
|
25
57
|
`);
|
|
26
58
|
process.exit(1);
|
|
27
59
|
}
|
package/bin/update.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const ora = require('ora');
|
|
8
|
+
const boxen = require('boxen');
|
|
9
|
+
|
|
10
|
+
const AICOSWITCH_DIR = path.join(os.homedir(), '.aicodeswitch');
|
|
11
|
+
const RELEASES_DIR = path.join(AICOSWITCH_DIR, 'releases');
|
|
12
|
+
const CURRENT_FILE = path.join(AICOSWITCH_DIR, 'current');
|
|
13
|
+
const PACKAGE_NAME = 'aicodeswitch';
|
|
14
|
+
const NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
15
|
+
|
|
16
|
+
// 确保目录存在
|
|
17
|
+
const ensureDir = (dirPath) => {
|
|
18
|
+
if (!fs.existsSync(dirPath)) {
|
|
19
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 获取当前使用的版本(从 current 文件或本地 package.json)
|
|
24
|
+
const getCurrentVersion = () => {
|
|
25
|
+
// 先检查是否有 current 文件(更新的版本)
|
|
26
|
+
if (fs.existsSync(CURRENT_FILE)) {
|
|
27
|
+
try {
|
|
28
|
+
const currentPath = fs.readFileSync(CURRENT_FILE, 'utf-8').trim();
|
|
29
|
+
const currentPackageJson = path.join(currentPath, 'package.json');
|
|
30
|
+
if (fs.existsSync(currentPackageJson)) {
|
|
31
|
+
const pkg = JSON.parse(fs.readFileSync(currentPackageJson, 'utf-8'));
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// 读取失败,fallback 到本地版本
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 使用本地 package.json
|
|
40
|
+
try {
|
|
41
|
+
const packageJson = path.join(__dirname, '..', 'package.json');
|
|
42
|
+
const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf-8'));
|
|
43
|
+
return pkg.version;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return '0.0.0';
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// 比较版本号
|
|
50
|
+
const compareVersions = (v1, v2) => {
|
|
51
|
+
const parts1 = v1.split('.').map(Number);
|
|
52
|
+
const parts2 = v2.split('.').map(Number);
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < 3; i++) {
|
|
55
|
+
if (parts1[i] > parts2[i]) return 1;
|
|
56
|
+
if (parts1[i] < parts2[i]) return -1;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 从 npm registry 获取最新版本
|
|
62
|
+
const getLatestVersion = () => {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const options = {
|
|
65
|
+
hostname: 'registry.npmjs.org',
|
|
66
|
+
path: `/${PACKAGE_NAME}`,
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent': 'aicodeswitch'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const req = https.request(options, (res) => {
|
|
74
|
+
let data = '';
|
|
75
|
+
|
|
76
|
+
res.on('data', (chunk) => {
|
|
77
|
+
data += chunk;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
res.on('end', () => {
|
|
81
|
+
try {
|
|
82
|
+
const packageInfo = JSON.parse(data);
|
|
83
|
+
const latestVersion = packageInfo['dist-tags'].latest;
|
|
84
|
+
resolve({
|
|
85
|
+
version: latestVersion,
|
|
86
|
+
tarball: packageInfo.versions[latestVersion].dist.tarball
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
reject(new Error('Failed to parse package info from npm'));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// 使用 npm 安装指定版本到指定目录
|
|
100
|
+
const installPackage = (version, targetDir) => {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const npmProcess = spawn('npm', [
|
|
103
|
+
'install',
|
|
104
|
+
`${PACKAGE_NAME}@${version}`,
|
|
105
|
+
'--prefix',
|
|
106
|
+
targetDir,
|
|
107
|
+
'--no-save',
|
|
108
|
+
'--no-package-lock',
|
|
109
|
+
'--no-bin-links'
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
let stderr = '';
|
|
113
|
+
|
|
114
|
+
npmProcess.stderr.on('data', (data) => {
|
|
115
|
+
stderr += data.toString();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
npmProcess.on('close', (code) => {
|
|
119
|
+
if (code === 0) {
|
|
120
|
+
// npm install 会把包安装到 targetDir/node_modules/ 目录下
|
|
121
|
+
const packageDir = path.join(targetDir, 'node_modules', PACKAGE_NAME);
|
|
122
|
+
if (fs.existsSync(packageDir)) {
|
|
123
|
+
resolve(packageDir);
|
|
124
|
+
} else {
|
|
125
|
+
reject(new Error('Package installation directory not found'));
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
reject(new Error(`npm install failed: ${stderr}`));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
npmProcess.on('error', reject);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 更新 current 文件
|
|
137
|
+
const updateCurrentFile = (versionPath) => {
|
|
138
|
+
fs.writeFileSync(CURRENT_FILE, versionPath);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// 执行 restart
|
|
142
|
+
const restart = () => {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const restartScript = path.join(__dirname, 'restart.js');
|
|
145
|
+
|
|
146
|
+
const restartProcess = spawn('node', [restartScript], {
|
|
147
|
+
stdio: 'inherit'
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
restartProcess.on('close', (code) => {
|
|
151
|
+
if (code === 0) {
|
|
152
|
+
resolve();
|
|
153
|
+
} else {
|
|
154
|
+
reject(new Error(`Restart failed with exit code ${code}`));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
restartProcess.on('error', reject);
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// 主更新逻辑
|
|
163
|
+
const update = async () => {
|
|
164
|
+
console.log('\n');
|
|
165
|
+
|
|
166
|
+
const currentVersion = getCurrentVersion();
|
|
167
|
+
const spinner = ora({
|
|
168
|
+
text: chalk.cyan('Checking for updates...'),
|
|
169
|
+
color: 'cyan'
|
|
170
|
+
}).start();
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// 获取最新版本信息
|
|
174
|
+
const latestInfo = await getLatestVersion();
|
|
175
|
+
const latestVersion = latestInfo.version;
|
|
176
|
+
|
|
177
|
+
spinner.succeed(chalk.green(`Latest version: ${chalk.bold(latestVersion)}`));
|
|
178
|
+
|
|
179
|
+
// 检查是否需要更新
|
|
180
|
+
const comparison = compareVersions(latestVersion, currentVersion);
|
|
181
|
+
|
|
182
|
+
if (comparison <= 0) {
|
|
183
|
+
console.log(chalk.yellow(`\n✓ You are already on the latest version (${chalk.bold(currentVersion)})\n`));
|
|
184
|
+
process.exit(0);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(chalk.cyan(`\n📦 Update available: ${chalk.bold(currentVersion)} → ${chalk.bold(latestVersion)}\n`));
|
|
189
|
+
|
|
190
|
+
// 确保目录存在
|
|
191
|
+
ensureDir(RELEASES_DIR);
|
|
192
|
+
|
|
193
|
+
// 安装新版本
|
|
194
|
+
const installSpinner = ora({
|
|
195
|
+
text: chalk.cyan('Downloading and installing from npm...'),
|
|
196
|
+
color: 'cyan'
|
|
197
|
+
}).start();
|
|
198
|
+
|
|
199
|
+
const versionDir = path.join(RELEASES_DIR, latestVersion);
|
|
200
|
+
ensureDir(versionDir);
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const packageDir = await installPackage(latestVersion, versionDir);
|
|
204
|
+
installSpinner.succeed(chalk.green('Package installed'));
|
|
205
|
+
} catch (err) {
|
|
206
|
+
installSpinner.fail(chalk.red('Installation failed'));
|
|
207
|
+
console.log(chalk.red(`Error: ${err.message}\n`));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 实际的包在 node_modules/aicodeswitch 目录下
|
|
213
|
+
const actualPackageDir = path.join(versionDir, 'node_modules', PACKAGE_NAME);
|
|
214
|
+
updateCurrentFile(actualPackageDir);
|
|
215
|
+
|
|
216
|
+
// 显示更新成功信息
|
|
217
|
+
console.log(boxen(
|
|
218
|
+
chalk.green.bold('✨ Update Successful!\n\n') +
|
|
219
|
+
chalk.white('Version: ') + chalk.cyan.bold(latestVersion) + '\n' +
|
|
220
|
+
chalk.white('Location: ') + chalk.gray(actualPackageDir) + '\n\n' +
|
|
221
|
+
chalk.gray('Restarting server with the new version...'),
|
|
222
|
+
{
|
|
223
|
+
padding: 1,
|
|
224
|
+
margin: 1,
|
|
225
|
+
borderStyle: 'double',
|
|
226
|
+
borderColor: 'green'
|
|
227
|
+
}
|
|
228
|
+
));
|
|
229
|
+
|
|
230
|
+
// 重启服务器
|
|
231
|
+
try {
|
|
232
|
+
await restart();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.log(chalk.yellow(`\n⚠️ Update completed, but restart failed: ${err.message}`));
|
|
235
|
+
console.log(chalk.cyan('Please manually run: ') + chalk.yellow('aicos restart\n'));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.exit(0);
|
|
241
|
+
|
|
242
|
+
} catch (err) {
|
|
243
|
+
spinner.fail(chalk.red('Update check failed'));
|
|
244
|
+
console.log(chalk.red(`Error: ${err.message}\n`));
|
|
245
|
+
console.log(chalk.gray('You can check for updates manually at:\n'));
|
|
246
|
+
console.log(chalk.cyan(' https://www.npmjs.com/package/aicodeswitch\n'));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
module.exports = update();
|
package/bin/version.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const boxen = require('boxen');
|
|
6
|
+
|
|
7
|
+
const AICOSWITCH_DIR = path.join(os.homedir(), '.aicodeswitch');
|
|
8
|
+
const CURRENT_FILE = path.join(AICOSWITCH_DIR, 'current');
|
|
9
|
+
|
|
10
|
+
const getVersionInfo = () => {
|
|
11
|
+
// 先检查是否有 current 文件(更新的版本)
|
|
12
|
+
if (fs.existsSync(CURRENT_FILE)) {
|
|
13
|
+
try {
|
|
14
|
+
const currentPath = fs.readFileSync(CURRENT_FILE, 'utf-8').trim();
|
|
15
|
+
const currentPackageJson = path.join(currentPath, 'package.json');
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(currentPackageJson)) {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(currentPackageJson, 'utf-8'));
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
version: pkg.version,
|
|
22
|
+
source: 'npm',
|
|
23
|
+
path: currentPath,
|
|
24
|
+
isUpdated: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// 读取失败,fallback 到本地版本
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 使用本地 package.json
|
|
33
|
+
try {
|
|
34
|
+
const packageJson = path.join(__dirname, '..', 'package.json');
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(packageJson, 'utf-8'));
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
version: pkg.version,
|
|
39
|
+
source: 'local',
|
|
40
|
+
path: path.dirname(packageJson),
|
|
41
|
+
isUpdated: false
|
|
42
|
+
};
|
|
43
|
+
} catch (err) {
|
|
44
|
+
return {
|
|
45
|
+
version: 'unknown',
|
|
46
|
+
source: 'unknown',
|
|
47
|
+
path: 'unknown',
|
|
48
|
+
isUpdated: false
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const version = () => {
|
|
54
|
+
const info = getVersionInfo();
|
|
55
|
+
|
|
56
|
+
console.log('\n');
|
|
57
|
+
|
|
58
|
+
if (info.isUpdated) {
|
|
59
|
+
console.log(boxen(
|
|
60
|
+
chalk.green.bold('AI Code Switch\n\n') +
|
|
61
|
+
chalk.white('Version: ') + chalk.cyan.bold(info.version) + '\n' +
|
|
62
|
+
chalk.white('Source: ') + chalk.yellow.bold('npm (updated)') + '\n' +
|
|
63
|
+
chalk.white('Location: ') + chalk.gray(info.path),
|
|
64
|
+
{
|
|
65
|
+
padding: 1,
|
|
66
|
+
margin: 1,
|
|
67
|
+
borderStyle: 'double',
|
|
68
|
+
borderColor: 'green'
|
|
69
|
+
}
|
|
70
|
+
));
|
|
71
|
+
|
|
72
|
+
console.log(chalk.cyan('💡 Tips:\n'));
|
|
73
|
+
console.log(chalk.white(' • Check for updates: ') + chalk.yellow('aicos update'));
|
|
74
|
+
console.log(chalk.white(' • Revert to local: ') + chalk.gray('rm ~/.aicodeswitch/current\n'));
|
|
75
|
+
} else {
|
|
76
|
+
console.log(boxen(
|
|
77
|
+
chalk.cyan.bold('AI Code Switch\n\n') +
|
|
78
|
+
chalk.white('Version: ') + chalk.cyan.bold(info.version) + '\n' +
|
|
79
|
+
chalk.white('Source: ') + chalk.yellow.bold('local development') + '\n' +
|
|
80
|
+
chalk.white('Location: ') + chalk.gray(info.path),
|
|
81
|
+
{
|
|
82
|
+
padding: 1,
|
|
83
|
+
margin: 1,
|
|
84
|
+
borderStyle: 'double',
|
|
85
|
+
borderColor: 'cyan'
|
|
86
|
+
}
|
|
87
|
+
));
|
|
88
|
+
|
|
89
|
+
console.log(chalk.cyan('💡 Tips:\n'));
|
|
90
|
+
console.log(chalk.white(' • Check for updates: ') + chalk.yellow('aicos update'));
|
|
91
|
+
console.log(chalk.white(' • Update to latest: ') + chalk.yellow('aicos update\n'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.exit(0);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
module.exports = version();
|
package/dist/server/database.js
CHANGED
|
@@ -99,14 +99,15 @@ class DatabaseManager {
|
|
|
99
99
|
CREATE TABLE IF NOT EXISTS rules (
|
|
100
100
|
id TEXT PRIMARY KEY,
|
|
101
101
|
route_id TEXT NOT NULL,
|
|
102
|
-
content_type TEXT NOT NULL CHECK(content_type IN ('default', 'background', 'thinking', 'long-context', 'image-understanding')),
|
|
102
|
+
content_type TEXT NOT NULL CHECK(content_type IN ('default', 'background', 'thinking', 'long-context', 'image-understanding', 'model-mapping')),
|
|
103
103
|
target_service_id TEXT NOT NULL,
|
|
104
104
|
target_model TEXT,
|
|
105
|
+
replaced_model TEXT,
|
|
106
|
+
sort_order INTEGER DEFAULT 0,
|
|
105
107
|
created_at INTEGER NOT NULL,
|
|
106
108
|
updated_at INTEGER NOT NULL,
|
|
107
109
|
FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE,
|
|
108
|
-
FOREIGN KEY (target_service_id) REFERENCES api_services(id) ON DELETE CASCADE
|
|
109
|
-
UNIQUE(route_id, content_type)
|
|
110
|
+
FOREIGN KEY (target_service_id) REFERENCES api_services(id) ON DELETE CASCADE
|
|
110
111
|
);
|
|
111
112
|
|
|
112
113
|
CREATE TABLE IF NOT EXISTS config (
|
|
@@ -246,8 +247,8 @@ class DatabaseManager {
|
|
|
246
247
|
// Rule operations
|
|
247
248
|
getRules(routeId) {
|
|
248
249
|
const query = routeId
|
|
249
|
-
? 'SELECT * FROM rules WHERE route_id = ? ORDER BY created_at DESC'
|
|
250
|
-
: 'SELECT * FROM rules ORDER BY created_at DESC';
|
|
250
|
+
? 'SELECT * FROM rules WHERE route_id = ? ORDER BY sort_order DESC, created_at DESC'
|
|
251
|
+
: 'SELECT * FROM rules ORDER BY sort_order DESC, created_at DESC';
|
|
251
252
|
const stmt = routeId ? this.db.prepare(query).bind(routeId) : this.db.prepare(query);
|
|
252
253
|
const rows = stmt.all();
|
|
253
254
|
return rows.map((row) => ({
|
|
@@ -256,6 +257,8 @@ class DatabaseManager {
|
|
|
256
257
|
contentType: row.content_type,
|
|
257
258
|
targetServiceId: row.target_service_id,
|
|
258
259
|
targetModel: row.target_model,
|
|
260
|
+
replacedModel: row.replaced_model,
|
|
261
|
+
sortOrder: row.sort_order,
|
|
259
262
|
createdAt: row.created_at,
|
|
260
263
|
updatedAt: row.updated_at,
|
|
261
264
|
}));
|
|
@@ -264,15 +267,15 @@ class DatabaseManager {
|
|
|
264
267
|
const id = crypto_1.default.randomUUID();
|
|
265
268
|
const now = Date.now();
|
|
266
269
|
this.db
|
|
267
|
-
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
268
|
-
.run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, now, now);
|
|
270
|
+
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
271
|
+
.run(id, route.routeId, route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, now, now);
|
|
269
272
|
return Object.assign(Object.assign({}, route), { id, createdAt: now, updatedAt: now });
|
|
270
273
|
}
|
|
271
274
|
updateRule(id, route) {
|
|
272
275
|
const now = Date.now();
|
|
273
276
|
const result = this.db
|
|
274
|
-
.prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, updated_at = ? WHERE id = ?')
|
|
275
|
-
.run(route.contentType, route.targetServiceId, route.targetModel || null, now, id);
|
|
277
|
+
.prepare('UPDATE rules SET content_type = ?, target_service_id = ?, target_model = ?, replaced_model = ?, sort_order = ?, updated_at = ? WHERE id = ?')
|
|
278
|
+
.run(route.contentType, route.targetServiceId, route.targetModel || null, route.replacedModel || null, route.sortOrder || 0, now, id);
|
|
276
279
|
return result.changes > 0;
|
|
277
280
|
}
|
|
278
281
|
deleteRule(id) {
|
|
@@ -324,6 +327,14 @@ class DatabaseManager {
|
|
|
324
327
|
return __awaiter(this, void 0, void 0, function* () {
|
|
325
328
|
const id = crypto_1.default.randomUUID();
|
|
326
329
|
yield this.accessLogDb.put(id, JSON.stringify(Object.assign(Object.assign({}, log), { id })));
|
|
330
|
+
return id;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
updateAccessLog(id, data) {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
const log = yield this.accessLogDb.get(id);
|
|
336
|
+
const updatedLog = Object.assign(Object.assign({}, JSON.parse(log)), data);
|
|
337
|
+
yield this.accessLogDb.put(id, JSON.stringify(updatedLog));
|
|
327
338
|
});
|
|
328
339
|
}
|
|
329
340
|
getAccessLogs() {
|
|
@@ -459,8 +470,8 @@ class DatabaseManager {
|
|
|
459
470
|
// Import rules
|
|
460
471
|
for (const rule of importData.rules) {
|
|
461
472
|
this.db
|
|
462
|
-
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
463
|
-
.run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.createdAt, rule.updatedAt);
|
|
473
|
+
.prepare('INSERT INTO rules (id, route_id, content_type, target_service_id, target_model, replaced_model, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
474
|
+
.run(rule.id, rule.routeId, rule.contentType || 'default', rule.targetServiceId, rule.targetModel || null, rule.replacedModel || null, rule.sortOrder || 0, rule.createdAt, rule.updatedAt);
|
|
464
475
|
}
|
|
465
476
|
// Update config
|
|
466
477
|
this.updateConfig(importData.config);
|