aicodeswitch 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  ```
@@ -171,6 +177,13 @@ PORT=4567
171
177
 
172
178
  在系统设置页面修改**日志保留天数**配置。
173
179
 
180
+ ## 许可
181
+
182
+ 此项目采用双许可证模式:
183
+
184
+ - **开源使用**:项目默认采用 GPL 3.0 许可证,允许个人免费使用、修改和分发,但所有衍生品必须开源。
185
+ - **商业使用**:如果您希望商业化使用而不遵守 GPL 条款(例如闭源销售),请联系我们购买单独的商业许可证。
186
+
174
187
  ## 技术支持
175
188
 
176
189
  如有问题或建议,请访问项目 [GitHub 仓库](https://github.com/tangshuang/aicodeswitch/issues)提交 Issue。
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);