mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 实现账户分组管理功能和优化响应式设计
主要更新: - 实现账户分组管理功能,支持创建、编辑、删除分组 - 支持将账户添加到分组进行统一调度 - 优化 API Keys 页面响应式设计,解决操作栏被隐藏的问题 - 优化账户管理页面布局,合并平台/类型列,改进操作按钮布局 - 修复代理信息显示溢出问题 - 改进表格列宽分配,充分利用屏幕空间 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
114
scripts/MANAGE_UPDATE.md
Normal file
114
scripts/MANAGE_UPDATE.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# manage.sh 脚本更新说明
|
||||
|
||||
## 新增功能(最新更新)
|
||||
|
||||
### 1. 端口配置
|
||||
- 安装时会询问服务端口,默认为 3000
|
||||
- 端口配置会自动写入 .env 文件
|
||||
- 检查端口是否被占用并提示
|
||||
|
||||
### 2. 自动启动服务
|
||||
- 安装完成后自动启动服务
|
||||
- 不再需要手动执行 `crs start`
|
||||
|
||||
### 3. 公网 IP 显示
|
||||
- 自动获取公网 IP 地址(通过 https://ipinfo.io/json)
|
||||
- 显示本地访问和公网访问地址
|
||||
- IP 地址缓存 1 小时,避免频繁调用 API
|
||||
|
||||
### 4. 动态端口显示
|
||||
- 所有状态显示都使用实际配置的端口
|
||||
- 交互式菜单显示实际端口和公网地址
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 安装时的新体验
|
||||
```bash
|
||||
$ crs install
|
||||
|
||||
# 会依次询问:
|
||||
安装目录 (默认: ~/claude-relay-service):
|
||||
服务端口 (默认: 3000): 8080
|
||||
Redis 地址 (默认: localhost):
|
||||
Redis 端口 (默认: 6379):
|
||||
Redis 密码 (默认: 无密码):
|
||||
|
||||
# 安装完成后自动启动并显示:
|
||||
服务已成功安装并启动!
|
||||
|
||||
访问地址:
|
||||
本地访问: http://localhost:8080/web
|
||||
公网访问: http://1.2.3.4:8080/web
|
||||
|
||||
管理命令:
|
||||
查看状态: crs status
|
||||
停止服务: crs stop
|
||||
重启服务: crs restart
|
||||
```
|
||||
|
||||
### 状态显示增强
|
||||
```bash
|
||||
$ crs status
|
||||
|
||||
=== Claude Relay Service 状态 ===
|
||||
服务状态: 运行中
|
||||
进程 PID: 12345
|
||||
服务端口: 8080
|
||||
|
||||
访问地址:
|
||||
本地访问: http://localhost:8080/web
|
||||
公网访问: http://1.2.3.4:8080/web
|
||||
API 端点: http://localhost:8080/api/v1
|
||||
|
||||
安装目录: /home/user/claude-relay-service
|
||||
|
||||
Redis 状态:
|
||||
连接状态: 正常
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 公网 IP 获取
|
||||
- 主要 API: https://ipinfo.io/json
|
||||
- 备用 API: https://api.ipify.org
|
||||
- 缓存文件: /tmp/.crs_public_ip_cache
|
||||
- 缓存时间: 3600 秒(1 小时)
|
||||
|
||||
### 端口配置存储
|
||||
- 配置文件: .env
|
||||
- 环境变量: PORT
|
||||
- 读取优先级: 命令行参数 > .env 文件 > 默认值 3000
|
||||
|
||||
## Redis 安装说明
|
||||
|
||||
### 系统默认安装位置
|
||||
脚本使用系统包管理器安装 Redis,会自动安装到各系统的默认位置:
|
||||
|
||||
- **Debian/Ubuntu**:
|
||||
- 配置文件: `/etc/redis/redis.conf`
|
||||
- 数据目录: `/var/lib/redis`
|
||||
- 日志文件: `/var/log/redis/redis-server.log`
|
||||
- 通过 systemd 管理: `systemctl status redis-server`
|
||||
|
||||
- **RedHat/CentOS**:
|
||||
- 配置文件: `/etc/redis.conf`
|
||||
- 数据目录: `/var/lib/redis`
|
||||
- 日志文件: `/var/log/redis/redis.log`
|
||||
- 通过 systemd 管理: `systemctl status redis`
|
||||
|
||||
- **Arch Linux**:
|
||||
- 配置文件: `/etc/redis/redis.conf`
|
||||
- 数据目录: `/var/lib/redis`
|
||||
- 通过 systemd 管理: `systemctl status redis`
|
||||
|
||||
- **macOS**:
|
||||
- 通过 Homebrew 安装
|
||||
- 配置文件: `/usr/local/etc/redis.conf`
|
||||
- 数据目录: `/usr/local/var/db/redis/`
|
||||
- 通过 brew services 管理: `brew services list`
|
||||
|
||||
### 优势
|
||||
- Redis 数据独立于应用,卸载应用不会丢失数据
|
||||
- 使用系统标准服务管理
|
||||
- 自动开机启动
|
||||
- 系统级的日志和监控
|
||||
1102
scripts/manage.sh
Normal file
1102
scripts/manage.sh
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 测试 API Key 过期功能
|
||||
* 快速创建和修改 API Key 过期时间以便测试
|
||||
*/
|
||||
|
||||
const apiKeyService = require('../src/services/apiKeyService');
|
||||
const redis = require('../src/models/redis');
|
||||
const logger = require('../src/utils/logger');
|
||||
const chalk = require('chalk');
|
||||
|
||||
async function createTestApiKeys() {
|
||||
console.log(chalk.bold.blue('\n🧪 创建测试 API Keys\n'));
|
||||
|
||||
try {
|
||||
await redis.connect();
|
||||
|
||||
// 创建不同过期时间的测试 Keys
|
||||
const testKeys = [
|
||||
{
|
||||
name: 'Test-Expired',
|
||||
description: '已过期的测试 Key',
|
||||
expiresAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // 1天前过期
|
||||
},
|
||||
{
|
||||
name: 'Test-1Hour',
|
||||
description: '1小时后过期的测试 Key',
|
||||
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString() // 1小时后
|
||||
},
|
||||
{
|
||||
name: 'Test-1Day',
|
||||
description: '1天后过期的测试 Key',
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 1天后
|
||||
},
|
||||
{
|
||||
name: 'Test-7Days',
|
||||
description: '7天后过期的测试 Key',
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7天后
|
||||
},
|
||||
{
|
||||
name: 'Test-Never',
|
||||
description: '永不过期的测试 Key',
|
||||
expiresAt: null // 永不过期
|
||||
}
|
||||
];
|
||||
|
||||
console.log('正在创建测试 API Keys...\n');
|
||||
|
||||
for (const keyData of testKeys) {
|
||||
try {
|
||||
const newKey = await apiKeyService.generateApiKey(keyData);
|
||||
|
||||
const expiryInfo = keyData.expiresAt
|
||||
? new Date(keyData.expiresAt).toLocaleString()
|
||||
: '永不过期';
|
||||
|
||||
console.log(`✅ 创建成功: ${keyData.name}`);
|
||||
console.log(` API Key: ${newKey.apiKey}`);
|
||||
console.log(` 过期时间: ${expiryInfo}`);
|
||||
console.log('');
|
||||
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`❌ 创建失败: ${keyData.name} - ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
// 运行清理任务测试
|
||||
console.log(chalk.bold.yellow('\n🔄 运行清理任务...\n'));
|
||||
const cleanedCount = await apiKeyService.cleanupExpiredKeys();
|
||||
console.log(`清理了 ${cleanedCount} 个过期的 API Keys\n`);
|
||||
|
||||
// 显示所有 API Keys 状态
|
||||
console.log(chalk.bold.cyan('📊 当前所有 API Keys 状态:\n'));
|
||||
const allKeys = await apiKeyService.getAllApiKeys();
|
||||
|
||||
for (const key of allKeys) {
|
||||
const now = new Date();
|
||||
const expiresAt = key.expiresAt ? new Date(key.expiresAt) : null;
|
||||
let status = '✅ 活跃';
|
||||
let expiryInfo = '永不过期';
|
||||
|
||||
if (expiresAt) {
|
||||
if (expiresAt < now) {
|
||||
status = '❌ 已过期';
|
||||
expiryInfo = `过期于 ${expiresAt.toLocaleString()}`;
|
||||
} else {
|
||||
const hoursLeft = Math.ceil((expiresAt - now) / (1000 * 60 * 60));
|
||||
const daysLeft = Math.ceil(hoursLeft / 24);
|
||||
|
||||
if (hoursLeft < 24) {
|
||||
expiryInfo = chalk.yellow(`${hoursLeft}小时后过期`);
|
||||
} else if (daysLeft <= 7) {
|
||||
expiryInfo = chalk.yellow(`${daysLeft}天后过期`);
|
||||
} else {
|
||||
expiryInfo = chalk.green(`${daysLeft}天后过期`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!key.isActive) {
|
||||
status = '🔒 已禁用';
|
||||
}
|
||||
|
||||
console.log(`${status} ${key.name} - ${expiryInfo}`);
|
||||
console.log(` API Key: ${key.apiKey?.substring(0, 30)}...`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red('测试失败:'), error);
|
||||
} finally {
|
||||
await redis.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
console.log(chalk.bold.magenta('\n===================================='));
|
||||
console.log(chalk.bold.magenta(' API Key 过期功能测试工具'));
|
||||
console.log(chalk.bold.magenta('====================================\n'));
|
||||
|
||||
console.log('此工具将:');
|
||||
console.log('1. 创建不同过期时间的测试 API Keys');
|
||||
console.log('2. 运行清理任务禁用过期的 Keys');
|
||||
console.log('3. 显示所有 Keys 的当前状态\n');
|
||||
|
||||
console.log(chalk.yellow('⚠️ 注意:这会在您的系统中创建真实的 API Keys\n'));
|
||||
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
readline.question('是否继续?(y/n): ', async (answer) => {
|
||||
if (answer.toLowerCase() === 'y') {
|
||||
await createTestApiKeys();
|
||||
|
||||
console.log(chalk.bold.green('\n✅ 测试完成!\n'));
|
||||
console.log('您现在可以:');
|
||||
console.log('1. 使用 CLI 工具管理这些测试 Keys:');
|
||||
console.log(' npm run cli keys');
|
||||
console.log('');
|
||||
console.log('2. 在 Web 界面查看和管理这些 Keys');
|
||||
console.log('');
|
||||
console.log('3. 测试 API 调用时的过期验证');
|
||||
} else {
|
||||
console.log('\n已取消');
|
||||
}
|
||||
|
||||
readline.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 运行
|
||||
main().catch(error => {
|
||||
console.error(chalk.red('错误:'), error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// 测试Claude Console账号URL处理
|
||||
|
||||
const testUrls = [
|
||||
'https://api.example.com',
|
||||
'https://api.example.com/',
|
||||
'https://api.example.com/v1/messages',
|
||||
'https://api.example.com/v1/messages/',
|
||||
'https://api.example.com:8080',
|
||||
'https://api.example.com:8080/v1/messages'
|
||||
];
|
||||
|
||||
console.log('🧪 Testing Claude Console URL handling:\n');
|
||||
|
||||
testUrls.forEach(url => {
|
||||
// 模拟账号服务的URL处理逻辑
|
||||
const cleanUrl = url.replace(/\/$/, ''); // 移除末尾斜杠
|
||||
const apiEndpoint = cleanUrl.endsWith('/v1/messages')
|
||||
? cleanUrl
|
||||
: `${cleanUrl}/v1/messages`;
|
||||
|
||||
console.log(`Input: ${url}`);
|
||||
console.log(`Output: ${apiEndpoint}`);
|
||||
console.log('---');
|
||||
});
|
||||
|
||||
console.log('\n✅ URL normalization logic test completed');
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 测试 Gemini 账户解密
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const dotenv = require('dotenv');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.join(__dirname, '..', '.env') });
|
||||
|
||||
const redis = require('../src/models/redis');
|
||||
const config = require('../config/config');
|
||||
|
||||
const ALGORITHM = 'aes-256-cbc';
|
||||
const ENCRYPTION_SALT = 'gemini-account-salt'; // 正确的盐值!
|
||||
const GEMINI_ACCOUNT_KEY_PREFIX = 'gemini_account:';
|
||||
|
||||
// 生成加密密钥(与 geminiAccountService 完全相同)
|
||||
function generateEncryptionKey() {
|
||||
return crypto.scryptSync(config.security.encryptionKey, ENCRYPTION_SALT, 32);
|
||||
}
|
||||
|
||||
// 解密函数(与 geminiAccountService 相同)
|
||||
function decrypt(text) {
|
||||
if (!text) return '';
|
||||
try {
|
||||
const key = generateEncryptionKey();
|
||||
// IV 是固定长度的 32 个十六进制字符(16 字节)
|
||||
const ivHex = text.substring(0, 32);
|
||||
const encryptedHex = text.substring(33); // 跳过冒号
|
||||
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const encryptedText = Buffer.from(encryptedHex, 'hex');
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
let decrypted = decipher.update(encryptedText);
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
||||
return decrypted.toString();
|
||||
} catch (error) {
|
||||
console.error('解密错误:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDecrypt() {
|
||||
try {
|
||||
console.log('🚀 测试 Gemini 账户解密...\n');
|
||||
|
||||
console.log('📋 加密配置:');
|
||||
console.log(` config.security.encryptionKey: ${config.security.encryptionKey}`);
|
||||
console.log(` ENCRYPTION_SALT: ${ENCRYPTION_SALT}`);
|
||||
console.log();
|
||||
|
||||
// 连接 Redis
|
||||
console.log('📡 连接 Redis...');
|
||||
await redis.connect();
|
||||
console.log('✅ Redis 连接成功\n');
|
||||
|
||||
const client = redis.getClient();
|
||||
const keys = await client.keys(`${GEMINI_ACCOUNT_KEY_PREFIX}*`);
|
||||
|
||||
if (keys.length === 0) {
|
||||
console.log('❌ 没有找到 Gemini 账户');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`🔍 找到 ${keys.length} 个 Gemini 账户\n`);
|
||||
|
||||
for (const key of keys) {
|
||||
const accountData = await client.hgetall(key);
|
||||
const accountId = key.replace(GEMINI_ACCOUNT_KEY_PREFIX, '');
|
||||
|
||||
console.log(`📋 账户: ${accountData.name} (${accountId})`);
|
||||
|
||||
if (accountData.refreshToken) {
|
||||
console.log('🔐 尝试解密 refreshToken...');
|
||||
const decrypted = decrypt(accountData.refreshToken);
|
||||
|
||||
if (decrypted) {
|
||||
console.log('✅ 解密成功!');
|
||||
console.log(` Token 前缀: ${decrypted.substring(0, 20)}...`);
|
||||
} else {
|
||||
console.log('❌ 解密失败');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 无 refreshToken');
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error);
|
||||
} finally {
|
||||
await redis.disconnect();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
testDecrypt();
|
||||
545
scripts/test-group-scheduling.js
Normal file
545
scripts/test-group-scheduling.js
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* 分组调度功能测试脚本
|
||||
* 用于测试账户分组管理和调度逻辑的正确性
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const redis = require('../src/models/redis');
|
||||
const accountGroupService = require('../src/services/accountGroupService');
|
||||
const claudeAccountService = require('../src/services/claudeAccountService');
|
||||
const claudeConsoleAccountService = require('../src/services/claudeConsoleAccountService');
|
||||
const apiKeyService = require('../src/services/apiKeyService');
|
||||
const unifiedClaudeScheduler = require('../src/services/unifiedClaudeScheduler');
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
// 测试配置
|
||||
const TEST_PREFIX = 'test_group_';
|
||||
const CLEANUP_ON_FINISH = true; // 测试完成后是否清理数据
|
||||
|
||||
// 测试数据存储
|
||||
const testData = {
|
||||
groups: [],
|
||||
accounts: [],
|
||||
apiKeys: []
|
||||
};
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
green: '\x1b[32m',
|
||||
red: '\x1b[31m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
reset: '\x1b[0m'
|
||||
};
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const color = {
|
||||
success: colors.green,
|
||||
error: colors.red,
|
||||
warning: colors.yellow,
|
||||
info: colors.blue
|
||||
}[type] || colors.reset;
|
||||
|
||||
console.log(`${color}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 清理测试数据
|
||||
async function cleanup() {
|
||||
log('\n🧹 清理测试数据...', 'info');
|
||||
|
||||
// 删除测试API Keys
|
||||
for (const apiKey of testData.apiKeys) {
|
||||
try {
|
||||
await apiKeyService.deleteApiKey(apiKey.id);
|
||||
log(`✅ 删除测试API Key: ${apiKey.name}`, 'success');
|
||||
} catch (error) {
|
||||
log(`❌ 删除API Key失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除测试账户
|
||||
for (const account of testData.accounts) {
|
||||
try {
|
||||
if (account.type === 'claude') {
|
||||
await claudeAccountService.deleteAccount(account.id);
|
||||
} else if (account.type === 'claude-console') {
|
||||
await claudeConsoleAccountService.deleteAccount(account.id);
|
||||
}
|
||||
log(`✅ 删除测试账户: ${account.name}`, 'success');
|
||||
} catch (error) {
|
||||
log(`❌ 删除账户失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除测试分组
|
||||
for (const group of testData.groups) {
|
||||
try {
|
||||
await accountGroupService.deleteGroup(group.id);
|
||||
log(`✅ 删除测试分组: ${group.name}`, 'success');
|
||||
} catch (error) {
|
||||
// 可能因为还有成员而删除失败,先移除所有成员
|
||||
if (error.message.includes('分组内还有账户')) {
|
||||
const members = await accountGroupService.getGroupMembers(group.id);
|
||||
for (const memberId of members) {
|
||||
await accountGroupService.removeAccountFromGroup(memberId, group.id);
|
||||
}
|
||||
// 重试删除
|
||||
await accountGroupService.deleteGroup(group.id);
|
||||
log(`✅ 删除测试分组: ${group.name} (清空成员后)`, 'success');
|
||||
} else {
|
||||
log(`❌ 删除分组失败: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试1: 创建分组
|
||||
async function test1_createGroups() {
|
||||
log('\n📝 测试1: 创建账户分组', 'info');
|
||||
|
||||
try {
|
||||
// 创建Claude分组
|
||||
const claudeGroup = await accountGroupService.createGroup({
|
||||
name: TEST_PREFIX + 'Claude组',
|
||||
platform: 'claude',
|
||||
description: '测试用Claude账户分组'
|
||||
});
|
||||
testData.groups.push(claudeGroup);
|
||||
log(`✅ 创建Claude分组成功: ${claudeGroup.name} (ID: ${claudeGroup.id})`, 'success');
|
||||
|
||||
// 创建Gemini分组
|
||||
const geminiGroup = await accountGroupService.createGroup({
|
||||
name: TEST_PREFIX + 'Gemini组',
|
||||
platform: 'gemini',
|
||||
description: '测试用Gemini账户分组'
|
||||
});
|
||||
testData.groups.push(geminiGroup);
|
||||
log(`✅ 创建Gemini分组成功: ${geminiGroup.name} (ID: ${geminiGroup.id})`, 'success');
|
||||
|
||||
// 验证分组信息
|
||||
const allGroups = await accountGroupService.getAllGroups();
|
||||
const testGroups = allGroups.filter(g => g.name.startsWith(TEST_PREFIX));
|
||||
|
||||
if (testGroups.length === 2) {
|
||||
log(`✅ 分组创建验证通过,共创建 ${testGroups.length} 个测试分组`, 'success');
|
||||
} else {
|
||||
throw new Error(`分组数量不正确,期望2个,实际${testGroups.length}个`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试1失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试2: 创建账户并添加到分组
|
||||
async function test2_createAccountsAndAddToGroup() {
|
||||
log('\n📝 测试2: 创建账户并添加到分组', 'info');
|
||||
|
||||
try {
|
||||
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
|
||||
|
||||
// 创建Claude OAuth账户
|
||||
const claudeAccount1 = await claudeAccountService.createAccount({
|
||||
name: TEST_PREFIX + 'Claude账户1',
|
||||
email: 'test1@example.com',
|
||||
refreshToken: 'test_refresh_token_1',
|
||||
accountType: 'group'
|
||||
});
|
||||
testData.accounts.push({ ...claudeAccount1, type: 'claude' });
|
||||
log(`✅ 创建Claude OAuth账户1成功: ${claudeAccount1.name}`, 'success');
|
||||
|
||||
const claudeAccount2 = await claudeAccountService.createAccount({
|
||||
name: TEST_PREFIX + 'Claude账户2',
|
||||
email: 'test2@example.com',
|
||||
refreshToken: 'test_refresh_token_2',
|
||||
accountType: 'group'
|
||||
});
|
||||
testData.accounts.push({ ...claudeAccount2, type: 'claude' });
|
||||
log(`✅ 创建Claude OAuth账户2成功: ${claudeAccount2.name}`, 'success');
|
||||
|
||||
// 创建Claude Console账户
|
||||
const consoleAccount = await claudeConsoleAccountService.createAccount({
|
||||
name: TEST_PREFIX + 'Console账户',
|
||||
apiUrl: 'https://api.example.com',
|
||||
apiKey: 'test_api_key',
|
||||
accountType: 'group'
|
||||
});
|
||||
testData.accounts.push({ ...consoleAccount, type: 'claude-console' });
|
||||
log(`✅ 创建Claude Console账户成功: ${consoleAccount.name}`, 'success');
|
||||
|
||||
// 添加账户到分组
|
||||
await accountGroupService.addAccountToGroup(claudeAccount1.id, claudeGroup.id, 'claude');
|
||||
log(`✅ 添加账户1到分组成功`, 'success');
|
||||
|
||||
await accountGroupService.addAccountToGroup(claudeAccount2.id, claudeGroup.id, 'claude');
|
||||
log(`✅ 添加账户2到分组成功`, 'success');
|
||||
|
||||
await accountGroupService.addAccountToGroup(consoleAccount.id, claudeGroup.id, 'claude');
|
||||
log(`✅ 添加Console账户到分组成功`, 'success');
|
||||
|
||||
// 验证分组成员
|
||||
const members = await accountGroupService.getGroupMembers(claudeGroup.id);
|
||||
if (members.length === 3) {
|
||||
log(`✅ 分组成员验证通过,共有 ${members.length} 个成员`, 'success');
|
||||
} else {
|
||||
throw new Error(`分组成员数量不正确,期望3个,实际${members.length}个`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试2失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试3: 平台一致性验证
|
||||
async function test3_platformConsistency() {
|
||||
log('\n📝 测试3: 平台一致性验证', 'info');
|
||||
|
||||
try {
|
||||
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
|
||||
const geminiGroup = testData.groups.find(g => g.platform === 'gemini');
|
||||
|
||||
// 尝试将Claude账户添加到Gemini分组(应该失败)
|
||||
const claudeAccount = testData.accounts.find(a => a.type === 'claude');
|
||||
|
||||
try {
|
||||
await accountGroupService.addAccountToGroup(claudeAccount.id, geminiGroup.id, 'claude');
|
||||
throw new Error('平台验证失败:Claude账户不应该能添加到Gemini分组');
|
||||
} catch (error) {
|
||||
if (error.message.includes('平台与分组平台不匹配')) {
|
||||
log(`✅ 平台一致性验证通过:${error.message}`, 'success');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试3失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试4: API Key绑定分组
|
||||
async function test4_apiKeyBindGroup() {
|
||||
log('\n📝 测试4: API Key绑定分组', 'info');
|
||||
|
||||
try {
|
||||
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
|
||||
|
||||
// 创建绑定到分组的API Key
|
||||
const apiKey = await apiKeyService.generateApiKey({
|
||||
name: TEST_PREFIX + 'API Key',
|
||||
description: '测试分组调度的API Key',
|
||||
claudeAccountId: `group:${claudeGroup.id}`,
|
||||
permissions: 'claude'
|
||||
});
|
||||
testData.apiKeys.push(apiKey);
|
||||
log(`✅ 创建API Key成功: ${apiKey.name} (绑定到分组: ${claudeGroup.name})`, 'success');
|
||||
|
||||
// 验证API Key信息
|
||||
const keyInfo = await redis.getApiKey(apiKey.id);
|
||||
if (keyInfo && keyInfo.claudeAccountId === `group:${claudeGroup.id}`) {
|
||||
log(`✅ API Key分组绑定验证通过`, 'success');
|
||||
} else {
|
||||
throw new Error('API Key分组绑定信息不正确');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试4失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试5: 分组调度负载均衡
|
||||
async function test5_groupSchedulingLoadBalance() {
|
||||
log('\n📝 测试5: 分组调度负载均衡', 'info');
|
||||
|
||||
try {
|
||||
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
|
||||
const apiKey = testData.apiKeys[0];
|
||||
|
||||
// 记录每个账户被选中的次数
|
||||
const selectionCount = {};
|
||||
const totalSelections = 30;
|
||||
|
||||
for (let i = 0; i < totalSelections; i++) {
|
||||
// 模拟不同的会话
|
||||
const sessionHash = uuidv4();
|
||||
|
||||
const result = await unifiedClaudeScheduler.selectAccountForApiKey({
|
||||
id: apiKey.id,
|
||||
claudeAccountId: apiKey.claudeAccountId,
|
||||
name: apiKey.name
|
||||
}, sessionHash);
|
||||
|
||||
if (!selectionCount[result.accountId]) {
|
||||
selectionCount[result.accountId] = 0;
|
||||
}
|
||||
selectionCount[result.accountId]++;
|
||||
|
||||
// 短暂延迟,模拟真实请求间隔
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
// 分析选择分布
|
||||
log(`\n📊 负载均衡分布统计 (共${totalSelections}次选择):`, 'info');
|
||||
const accounts = Object.keys(selectionCount);
|
||||
|
||||
for (const accountId of accounts) {
|
||||
const count = selectionCount[accountId];
|
||||
const percentage = ((count / totalSelections) * 100).toFixed(1);
|
||||
const accountInfo = testData.accounts.find(a => a.id === accountId);
|
||||
log(` ${accountInfo.name}: ${count}次 (${percentage}%)`, 'info');
|
||||
}
|
||||
|
||||
// 验证是否实现了负载均衡
|
||||
const counts = Object.values(selectionCount);
|
||||
const avgCount = totalSelections / accounts.length;
|
||||
const variance = counts.reduce((sum, count) => sum + Math.pow(count - avgCount, 2), 0) / counts.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
log(`\n 平均选择次数: ${avgCount.toFixed(1)}`, 'info');
|
||||
log(` 标准差: ${stdDev.toFixed(1)}`, 'info');
|
||||
|
||||
// 如果标准差小于平均值的50%,认为负载均衡效果良好
|
||||
if (stdDev < avgCount * 0.5) {
|
||||
log(`✅ 负载均衡验证通过,分布相对均匀`, 'success');
|
||||
} else {
|
||||
log(`⚠️ 负载分布不够均匀,但这可能是正常的随机波动`, 'warning');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试5失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试6: 会话粘性测试
|
||||
async function test6_stickySession() {
|
||||
log('\n📝 测试6: 会话粘性(Sticky Session)测试', 'info');
|
||||
|
||||
try {
|
||||
const apiKey = testData.apiKeys[0];
|
||||
const sessionHash = 'test_session_' + uuidv4();
|
||||
|
||||
// 第一次选择
|
||||
const firstSelection = await unifiedClaudeScheduler.selectAccountForApiKey({
|
||||
id: apiKey.id,
|
||||
claudeAccountId: apiKey.claudeAccountId,
|
||||
name: apiKey.name
|
||||
}, sessionHash);
|
||||
|
||||
log(` 首次选择账户: ${firstSelection.accountId}`, 'info');
|
||||
|
||||
// 使用相同的sessionHash多次请求
|
||||
let consistentCount = 0;
|
||||
const testCount = 10;
|
||||
|
||||
for (let i = 0; i < testCount; i++) {
|
||||
const selection = await unifiedClaudeScheduler.selectAccountForApiKey({
|
||||
id: apiKey.id,
|
||||
claudeAccountId: apiKey.claudeAccountId,
|
||||
name: apiKey.name
|
||||
}, sessionHash);
|
||||
|
||||
if (selection.accountId === firstSelection.accountId) {
|
||||
consistentCount++;
|
||||
}
|
||||
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
log(` 会话一致性: ${consistentCount}/${testCount} 次选择了相同账户`, 'info');
|
||||
|
||||
if (consistentCount === testCount) {
|
||||
log(`✅ 会话粘性验证通过,同一会话始终选择相同账户`, 'success');
|
||||
} else {
|
||||
throw new Error(`会话粘性失败,只有${consistentCount}/${testCount}次选择了相同账户`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试6失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试7: 账户可用性检查
|
||||
async function test7_accountAvailability() {
|
||||
log('\n📝 测试7: 账户可用性检查', 'info');
|
||||
|
||||
try {
|
||||
const apiKey = testData.apiKeys[0];
|
||||
const accounts = testData.accounts.filter(a => a.type === 'claude' || a.type === 'claude-console');
|
||||
|
||||
// 禁用第一个账户
|
||||
const firstAccount = accounts[0];
|
||||
if (firstAccount.type === 'claude') {
|
||||
await claudeAccountService.updateAccount(firstAccount.id, { isActive: false });
|
||||
} else {
|
||||
await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: false });
|
||||
}
|
||||
log(` 已禁用账户: ${firstAccount.name}`, 'info');
|
||||
|
||||
// 多次选择,验证不会选择到禁用的账户
|
||||
const selectionResults = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const sessionHash = uuidv4(); // 每次使用新会话
|
||||
const result = await unifiedClaudeScheduler.selectAccountForApiKey({
|
||||
id: apiKey.id,
|
||||
claudeAccountId: apiKey.claudeAccountId,
|
||||
name: apiKey.name
|
||||
}, sessionHash);
|
||||
|
||||
selectionResults.push(result.accountId);
|
||||
}
|
||||
|
||||
// 检查是否选择了禁用的账户
|
||||
const selectedDisabled = selectionResults.includes(firstAccount.id);
|
||||
|
||||
if (!selectedDisabled) {
|
||||
log(`✅ 账户可用性验证通过,未选择禁用的账户`, 'success');
|
||||
} else {
|
||||
throw new Error('错误:选择了已禁用的账户');
|
||||
}
|
||||
|
||||
// 重新启用账户
|
||||
if (firstAccount.type === 'claude') {
|
||||
await claudeAccountService.updateAccount(firstAccount.id, { isActive: true });
|
||||
} else {
|
||||
await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: true });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试7失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试8: 分组成员管理
|
||||
async function test8_groupMemberManagement() {
|
||||
log('\n📝 测试8: 分组成员管理', 'info');
|
||||
|
||||
try {
|
||||
const claudeGroup = testData.groups.find(g => g.platform === 'claude');
|
||||
const account = testData.accounts.find(a => a.type === 'claude');
|
||||
|
||||
// 获取账户所属分组
|
||||
const accountGroup = await accountGroupService.getAccountGroup(account.id);
|
||||
if (accountGroup && accountGroup.id === claudeGroup.id) {
|
||||
log(`✅ 账户分组查询验证通过`, 'success');
|
||||
} else {
|
||||
throw new Error('账户分组查询结果不正确');
|
||||
}
|
||||
|
||||
// 从分组移除账户
|
||||
await accountGroupService.removeAccountFromGroup(account.id, claudeGroup.id);
|
||||
log(` 从分组移除账户: ${account.name}`, 'info');
|
||||
|
||||
// 验证账户已不在分组中
|
||||
const membersAfterRemove = await accountGroupService.getGroupMembers(claudeGroup.id);
|
||||
if (!membersAfterRemove.includes(account.id)) {
|
||||
log(`✅ 账户移除验证通过`, 'success');
|
||||
} else {
|
||||
throw new Error('账户移除失败');
|
||||
}
|
||||
|
||||
// 重新添加账户
|
||||
await accountGroupService.addAccountToGroup(account.id, claudeGroup.id, 'claude');
|
||||
log(` 重新添加账户到分组`, 'info');
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试8失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试9: 空分组处理
|
||||
async function test9_emptyGroupHandling() {
|
||||
log('\n📝 测试9: 空分组处理', 'info');
|
||||
|
||||
try {
|
||||
// 创建一个空分组
|
||||
const emptyGroup = await accountGroupService.createGroup({
|
||||
name: TEST_PREFIX + '空分组',
|
||||
platform: 'claude',
|
||||
description: '测试空分组'
|
||||
});
|
||||
testData.groups.push(emptyGroup);
|
||||
|
||||
// 创建绑定到空分组的API Key
|
||||
const apiKey = await apiKeyService.generateApiKey({
|
||||
name: TEST_PREFIX + '空分组API Key',
|
||||
claudeAccountId: `group:${emptyGroup.id}`,
|
||||
permissions: 'claude'
|
||||
});
|
||||
testData.apiKeys.push(apiKey);
|
||||
|
||||
// 尝试从空分组选择账户(应该失败)
|
||||
try {
|
||||
await unifiedClaudeScheduler.selectAccountForApiKey({
|
||||
id: apiKey.id,
|
||||
claudeAccountId: apiKey.claudeAccountId,
|
||||
name: apiKey.name
|
||||
});
|
||||
throw new Error('空分组选择账户应该失败');
|
||||
} catch (error) {
|
||||
if (error.message.includes('has no members')) {
|
||||
log(`✅ 空分组处理验证通过:${error.message}`, 'success');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ 测试9失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 主测试函数
|
||||
async function runTests() {
|
||||
log('\n🚀 开始分组调度功能测试\n', 'info');
|
||||
|
||||
try {
|
||||
// 连接Redis
|
||||
await redis.connect();
|
||||
log('✅ Redis连接成功', 'success');
|
||||
|
||||
// 执行测试
|
||||
await test1_createGroups();
|
||||
await test2_createAccountsAndAddToGroup();
|
||||
await test3_platformConsistency();
|
||||
await test4_apiKeyBindGroup();
|
||||
await test5_groupSchedulingLoadBalance();
|
||||
await test6_stickySession();
|
||||
await test7_accountAvailability();
|
||||
await test8_groupMemberManagement();
|
||||
await test9_emptyGroupHandling();
|
||||
|
||||
log('\n🎉 所有测试通过!分组调度功能工作正常', 'success');
|
||||
|
||||
} catch (error) {
|
||||
log(`\n❌ 测试失败: ${error.message}`, 'error');
|
||||
console.error(error);
|
||||
} finally {
|
||||
// 清理测试数据
|
||||
if (CLEANUP_ON_FINISH) {
|
||||
await cleanup();
|
||||
} else {
|
||||
log('\n⚠️ 测试数据未清理,请手动清理', 'warning');
|
||||
}
|
||||
|
||||
// 关闭Redis连接
|
||||
await redis.disconnect();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
runTests();
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 测试导入加密处理
|
||||
* 验证增强版数据传输工具是否正确处理加密和未加密的导出数据
|
||||
*/
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const config = require('../config/config');
|
||||
const logger = require('../src/utils/logger');
|
||||
|
||||
// 模拟加密函数
|
||||
function encryptData(data, salt = 'salt') {
|
||||
if (!data || !config.security.encryptionKey) return data;
|
||||
|
||||
const key = crypto.scryptSync(config.security.encryptionKey, salt, 32);
|
||||
const iv = crypto.randomBytes(16);
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
return iv.toString('hex') + ':' + encrypted;
|
||||
}
|
||||
|
||||
// 模拟解密函数
|
||||
function decryptData(encryptedData, salt = 'salt') {
|
||||
if (!encryptedData || !config.security.encryptionKey) return encryptedData;
|
||||
|
||||
try {
|
||||
if (encryptedData.includes(':')) {
|
||||
const parts = encryptedData.split(':');
|
||||
const key = crypto.scryptSync(config.security.encryptionKey, salt, 32);
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const encrypted = parts[1];
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
}
|
||||
return encryptedData;
|
||||
} catch (error) {
|
||||
logger.warn(`⚠️ Failed to decrypt data: ${error.message}`);
|
||||
return encryptedData;
|
||||
}
|
||||
}
|
||||
|
||||
async function testImportHandling() {
|
||||
console.log('🧪 测试导入加密处理\n');
|
||||
|
||||
// 测试数据
|
||||
const testClaudeAccount = {
|
||||
id: 'test-claude-123',
|
||||
name: 'Test Claude Account',
|
||||
email: 'test@example.com',
|
||||
password: 'testPassword123',
|
||||
accessToken: 'test-access-token',
|
||||
refreshToken: 'test-refresh-token',
|
||||
claudeAiOauth: {
|
||||
access_token: 'oauth-access-token',
|
||||
refresh_token: 'oauth-refresh-token',
|
||||
scopes: ['read', 'write']
|
||||
}
|
||||
};
|
||||
|
||||
const testGeminiAccount = {
|
||||
id: 'test-gemini-456',
|
||||
name: 'Test Gemini Account',
|
||||
geminiOauth: {
|
||||
access_token: 'gemini-access-token',
|
||||
refresh_token: 'gemini-refresh-token'
|
||||
},
|
||||
accessToken: 'gemini-access-token',
|
||||
refreshToken: 'gemini-refresh-token'
|
||||
};
|
||||
|
||||
// 1. 创建解密的导出文件(模拟 --decrypt=true)
|
||||
const decryptedExport = {
|
||||
metadata: {
|
||||
version: '2.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
sanitized: false,
|
||||
decrypted: true, // 标记为已解密
|
||||
types: ['all']
|
||||
},
|
||||
data: {
|
||||
claudeAccounts: [testClaudeAccount],
|
||||
geminiAccounts: [testGeminiAccount]
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 创建加密的导出文件(模拟 --decrypt=false)
|
||||
const encryptedClaudeAccount = { ...testClaudeAccount };
|
||||
encryptedClaudeAccount.email = encryptData(encryptedClaudeAccount.email);
|
||||
encryptedClaudeAccount.password = encryptData(encryptedClaudeAccount.password);
|
||||
encryptedClaudeAccount.accessToken = encryptData(encryptedClaudeAccount.accessToken);
|
||||
encryptedClaudeAccount.refreshToken = encryptData(encryptedClaudeAccount.refreshToken);
|
||||
encryptedClaudeAccount.claudeAiOauth = encryptData(JSON.stringify(encryptedClaudeAccount.claudeAiOauth));
|
||||
|
||||
const encryptedGeminiAccount = { ...testGeminiAccount };
|
||||
encryptedGeminiAccount.geminiOauth = encryptData(JSON.stringify(encryptedGeminiAccount.geminiOauth), 'gemini-account-salt');
|
||||
encryptedGeminiAccount.accessToken = encryptData(encryptedGeminiAccount.accessToken, 'gemini-account-salt');
|
||||
encryptedGeminiAccount.refreshToken = encryptData(encryptedGeminiAccount.refreshToken, 'gemini-account-salt');
|
||||
|
||||
const encryptedExport = {
|
||||
metadata: {
|
||||
version: '2.0',
|
||||
exportDate: new Date().toISOString(),
|
||||
sanitized: false,
|
||||
decrypted: false, // 标记为未解密(加密状态)
|
||||
types: ['all']
|
||||
},
|
||||
data: {
|
||||
claudeAccounts: [encryptedClaudeAccount],
|
||||
geminiAccounts: [encryptedGeminiAccount]
|
||||
}
|
||||
};
|
||||
|
||||
// 写入测试文件
|
||||
const testDir = path.join(__dirname, '../data/test-imports');
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testDir, 'decrypted-export.json'),
|
||||
JSON.stringify(decryptedExport, null, 2)
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(testDir, 'encrypted-export.json'),
|
||||
JSON.stringify(encryptedExport, null, 2)
|
||||
);
|
||||
|
||||
console.log('✅ 测试文件已创建:');
|
||||
console.log(' - data/test-imports/decrypted-export.json (解密的数据)');
|
||||
console.log(' - data/test-imports/encrypted-export.json (加密的数据)\n');
|
||||
|
||||
console.log('📋 测试场景:\n');
|
||||
|
||||
console.log('1. 导入解密的数据(decrypted=true):');
|
||||
console.log(' - 导入时应该重新加密敏感字段');
|
||||
console.log(' - 命令: npm run data:import:enhanced -- --input=data/test-imports/decrypted-export.json\n');
|
||||
|
||||
console.log('2. 导入加密的数据(decrypted=false):');
|
||||
console.log(' - 导入时应该保持原样(已经是加密的)');
|
||||
console.log(' - 命令: npm run data:import:enhanced -- --input=data/test-imports/encrypted-export.json\n');
|
||||
|
||||
console.log('3. 验证导入后的数据:');
|
||||
console.log(' - 使用 CLI 查看账户状态');
|
||||
console.log(' - 命令: npm run cli accounts list\n');
|
||||
|
||||
// 显示示例数据对比
|
||||
console.log('📊 数据对比示例:\n');
|
||||
console.log('原始数据(解密状态):');
|
||||
console.log(` email: "${testClaudeAccount.email}"`);
|
||||
console.log(` password: "${testClaudeAccount.password}"`);
|
||||
console.log(` accessToken: "${testClaudeAccount.accessToken}"\n`);
|
||||
|
||||
console.log('加密后的数据:');
|
||||
console.log(` email: "${encryptedClaudeAccount.email.substring(0, 50)}..."`);
|
||||
console.log(` password: "${encryptedClaudeAccount.password.substring(0, 50)}..."`);
|
||||
console.log(` accessToken: "${encryptedClaudeAccount.accessToken.substring(0, 50)}..."\n`);
|
||||
|
||||
// 验证加密/解密
|
||||
console.log('🔐 验证加密/解密功能:');
|
||||
const testString = 'test-data-123';
|
||||
const encrypted = encryptData(testString);
|
||||
const decrypted = decryptData(encrypted);
|
||||
console.log(` 原始: "${testString}"`);
|
||||
console.log(` 加密: "${encrypted.substring(0, 50)}..."`);
|
||||
console.log(` 解密: "${decrypted}"`);
|
||||
console.log(` 验证: ${testString === decrypted ? '✅ 成功' : '❌ 失败'}\n`);
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testImportHandling().catch(error => {
|
||||
console.error('❌ 测试失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user