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 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
@@ -21,6 +21,12 @@ npm install -g aicodeswitch
21
21
  aicos start
22
22
  ```
23
23
 
24
+ **停止服务**
25
+
26
+ ```
27
+ aicos stop
28
+ ```
29
+
24
30
  **进入管理界面**
25
31
 
26
32
  ```
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('./start'),
8
- stop: () => require('./stop'),
9
- restart: () => require('./restart'),
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();
@@ -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);