From ba98bf22cc89a8db6d42ec9f9643fcb24dcc37ce Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Mon, 15 Dec 2025 21:19:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AE=B8=E5=8F=AF?= =?UTF-8?q?=E8=AF=81=E7=B3=BB=E7=BB=9F=E3=80=81Agent=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=92=8CHarbor=E9=95=9C=E5=83=8F=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证 --- .kiro/specs/license-system/design.md | 1246 ++++ .kiro/specs/license-system/requirements.md | 236 + .kiro/specs/license-system/tasks.md | 385 + Dockerfile | 7 + VERSION.txt | 52 +- app/activate/__tests__/page.test.tsx | 244 + app/activate/page.tsx | 153 + app/admin/settings/page.tsx | 4 +- app/api/admin/upload-templates/route.ts | 9 + app/api/auth/register/route.ts | 10 + app/api/departments/route.ts | 9 + app/api/license/activate-free/route.ts | 34 + .../license/activate/__tests__/route.test.ts | 191 + app/api/license/activate/route.ts | 43 + app/api/license/info/route.ts | 67 + app/api/materials/upload/route.ts | 11 + app/api/system-settings/route.ts | 9 + components/LicenseInfo.tsx | 212 + components/__tests__/RankingPanel.test.tsx | 31 +- docs/harbor-image-management.md | 133 + flnxi-agent/.gitignore | 6 + flnxi-agent/Dockerfile | 42 + flnxi-agent/README.md | 66 + flnxi-agent/VERSION.txt | 1 + flnxi-agent/frontend/index.html | 12 + flnxi-agent/frontend/package-lock.json | 2762 +++++++ flnxi-agent/frontend/package.json | 25 + flnxi-agent/frontend/postcss.config.js | 6 + flnxi-agent/frontend/src/App.tsx | 100 + flnxi-agent/frontend/src/index.css | 7 + flnxi-agent/frontend/src/main.tsx | 10 + flnxi-agent/frontend/src/pages/Complete.tsx | 43 + flnxi-agent/frontend/src/pages/Config.tsx | 75 + flnxi-agent/frontend/src/pages/Dashboard.tsx | 220 + flnxi-agent/frontend/src/pages/Installing.tsx | 42 + flnxi-agent/frontend/src/pages/Welcome.tsx | 62 + flnxi-agent/frontend/tailwind.config.js | 8 + flnxi-agent/frontend/tsconfig.json | 21 + flnxi-agent/frontend/tsconfig.node.json | 10 + flnxi-agent/frontend/vite.config.ts | 15 + flnxi-agent/package-lock.json | 2594 +++++++ flnxi-agent/package.json | 30 + flnxi-agent/src/installer.ts | 336 + flnxi-agent/src/server.ts | 185 + flnxi-agent/src/system-manager.ts | 455 ++ flnxi-agent/tsconfig.json | 21 + instrumentation.ts | 19 + lib/__tests__/check-limits.test.ts | 190 + lib/__tests__/crypto-service.test.ts | 171 + lib/__tests__/license-types.test.ts | 200 + lib/license/check-limits.ts | 88 + lib/license/crypto-service.ts | 93 + lib/license/fingerprint-service.ts | 191 + lib/license/index.ts | 47 + lib/license/license-service.ts | 370 + lib/license/types.ts | 152 + license-server/.env.example | 15 + license-server/.gitignore | 41 + license-server/Dockerfile | 49 + license-server/app/admin/alerts/page.tsx | 203 + license-server/app/admin/customers/page.tsx | 276 + license-server/app/admin/licenses/page.tsx | 398 + license-server/app/admin/login/page.tsx | 88 + license-server/app/admin/page.tsx | 217 + license-server/app/admin/versions/page.tsx | 156 + .../app/api/admin/alerts/[id]/ignore/route.ts | 30 + .../api/admin/alerts/[id]/resolve/route.ts | 30 + license-server/app/api/admin/alerts/route.ts | 41 + .../app/api/admin/auth/login/route.ts | 46 + .../app/api/admin/auth/logout/route.ts | 15 + .../app/api/admin/customers/route.ts | 56 + .../app/api/admin/dashboard/route.ts | 78 + .../api/admin/licenses/[id]/disable/route.ts | 27 + .../api/admin/licenses/[id]/reset/route.ts | 28 + .../app/api/admin/licenses/route.ts | 68 + .../app/api/admin/versions/route.ts | 58 + .../app/api/license/activate/route.ts | 135 + .../app/api/license/heartbeat/route.ts | 158 + .../app/api/license/verify/route.ts | 150 + license-server/app/api/version/agent/route.ts | 32 + license-server/app/api/version/check/route.ts | 40 + license-server/app/globals.css | 13 + license-server/app/layout.tsx | 19 + license-server/app/page.tsx | 22 + license-server/deploy.ps1 | 129 + license-server/docker-compose.yml | 44 + license-server/jest.config.js | 18 + license-server/jest.setup.js | 1 + license-server/lib/auth.ts | 122 + license-server/lib/crypto.ts | 124 + license-server/lib/db.ts | 15 + license-server/lib/types.ts | 153 + license-server/next.config.js | 7 + license-server/nginx/nginx.conf | 51 + license-server/package-lock.json | 6494 +++++++++++++++++ license-server/package.json | 41 + license-server/postcss.config.js | 6 + license-server/prisma/schema.prisma | 148 + license-server/scripts/generate-keys.ts | 67 + license-server/scripts/init-admin.ts | 42 + license-server/tailwind.config.ts | 15 + license-server/tsconfig.json | 26 + middleware.ts | 24 + next.config.js | 16 + package-lock.json | 1337 +++- package.json | 6 + prisma/schema.prisma | 11 + scripts/build-material-image.ps1 | 158 + scripts/build-obfuscated.js | 144 + scripts/obfuscate-config.js | 143 + scripts/sync-harbor-images.ps1 | 188 + tsconfig.json | 2 +- 发布管理.ps1 | 1015 +++ 113 files changed, 24669 insertions(+), 128 deletions(-) create mode 100644 .kiro/specs/license-system/design.md create mode 100644 .kiro/specs/license-system/requirements.md create mode 100644 .kiro/specs/license-system/tasks.md create mode 100644 app/activate/__tests__/page.test.tsx create mode 100644 app/activate/page.tsx create mode 100644 app/api/license/activate-free/route.ts create mode 100644 app/api/license/activate/__tests__/route.test.ts create mode 100644 app/api/license/activate/route.ts create mode 100644 app/api/license/info/route.ts create mode 100644 components/LicenseInfo.tsx create mode 100644 docs/harbor-image-management.md create mode 100644 flnxi-agent/.gitignore create mode 100644 flnxi-agent/Dockerfile create mode 100644 flnxi-agent/README.md create mode 100644 flnxi-agent/VERSION.txt create mode 100644 flnxi-agent/frontend/index.html create mode 100644 flnxi-agent/frontend/package-lock.json create mode 100644 flnxi-agent/frontend/package.json create mode 100644 flnxi-agent/frontend/postcss.config.js create mode 100644 flnxi-agent/frontend/src/App.tsx create mode 100644 flnxi-agent/frontend/src/index.css create mode 100644 flnxi-agent/frontend/src/main.tsx create mode 100644 flnxi-agent/frontend/src/pages/Complete.tsx create mode 100644 flnxi-agent/frontend/src/pages/Config.tsx create mode 100644 flnxi-agent/frontend/src/pages/Dashboard.tsx create mode 100644 flnxi-agent/frontend/src/pages/Installing.tsx create mode 100644 flnxi-agent/frontend/src/pages/Welcome.tsx create mode 100644 flnxi-agent/frontend/tailwind.config.js create mode 100644 flnxi-agent/frontend/tsconfig.json create mode 100644 flnxi-agent/frontend/tsconfig.node.json create mode 100644 flnxi-agent/frontend/vite.config.ts create mode 100644 flnxi-agent/package-lock.json create mode 100644 flnxi-agent/package.json create mode 100644 flnxi-agent/src/installer.ts create mode 100644 flnxi-agent/src/server.ts create mode 100644 flnxi-agent/src/system-manager.ts create mode 100644 flnxi-agent/tsconfig.json create mode 100644 instrumentation.ts create mode 100644 lib/__tests__/check-limits.test.ts create mode 100644 lib/__tests__/crypto-service.test.ts create mode 100644 lib/__tests__/license-types.test.ts create mode 100644 lib/license/check-limits.ts create mode 100644 lib/license/crypto-service.ts create mode 100644 lib/license/fingerprint-service.ts create mode 100644 lib/license/index.ts create mode 100644 lib/license/license-service.ts create mode 100644 lib/license/types.ts create mode 100644 license-server/.env.example create mode 100644 license-server/.gitignore create mode 100644 license-server/Dockerfile create mode 100644 license-server/app/admin/alerts/page.tsx create mode 100644 license-server/app/admin/customers/page.tsx create mode 100644 license-server/app/admin/licenses/page.tsx create mode 100644 license-server/app/admin/login/page.tsx create mode 100644 license-server/app/admin/page.tsx create mode 100644 license-server/app/admin/versions/page.tsx create mode 100644 license-server/app/api/admin/alerts/[id]/ignore/route.ts create mode 100644 license-server/app/api/admin/alerts/[id]/resolve/route.ts create mode 100644 license-server/app/api/admin/alerts/route.ts create mode 100644 license-server/app/api/admin/auth/login/route.ts create mode 100644 license-server/app/api/admin/auth/logout/route.ts create mode 100644 license-server/app/api/admin/customers/route.ts create mode 100644 license-server/app/api/admin/dashboard/route.ts create mode 100644 license-server/app/api/admin/licenses/[id]/disable/route.ts create mode 100644 license-server/app/api/admin/licenses/[id]/reset/route.ts create mode 100644 license-server/app/api/admin/licenses/route.ts create mode 100644 license-server/app/api/admin/versions/route.ts create mode 100644 license-server/app/api/license/activate/route.ts create mode 100644 license-server/app/api/license/heartbeat/route.ts create mode 100644 license-server/app/api/license/verify/route.ts create mode 100644 license-server/app/api/version/agent/route.ts create mode 100644 license-server/app/api/version/check/route.ts create mode 100644 license-server/app/globals.css create mode 100644 license-server/app/layout.tsx create mode 100644 license-server/app/page.tsx create mode 100644 license-server/deploy.ps1 create mode 100644 license-server/docker-compose.yml create mode 100644 license-server/jest.config.js create mode 100644 license-server/jest.setup.js create mode 100644 license-server/lib/auth.ts create mode 100644 license-server/lib/crypto.ts create mode 100644 license-server/lib/db.ts create mode 100644 license-server/lib/types.ts create mode 100644 license-server/next.config.js create mode 100644 license-server/nginx/nginx.conf create mode 100644 license-server/package-lock.json create mode 100644 license-server/package.json create mode 100644 license-server/postcss.config.js create mode 100644 license-server/prisma/schema.prisma create mode 100644 license-server/scripts/generate-keys.ts create mode 100644 license-server/scripts/init-admin.ts create mode 100644 license-server/tailwind.config.ts create mode 100644 license-server/tsconfig.json create mode 100644 scripts/build-material-image.ps1 create mode 100644 scripts/build-obfuscated.js create mode 100644 scripts/obfuscate-config.js create mode 100644 scripts/sync-harbor-images.ps1 create mode 100644 发布管理.ps1 diff --git a/.kiro/specs/license-system/design.md b/.kiro/specs/license-system/design.md new file mode 100644 index 0000000..acca12d --- /dev/null +++ b/.kiro/specs/license-system/design.md @@ -0,0 +1,1246 @@ +# Design Document + +## Overview + +本设计文档描述 FLNXI 素材系统许可证管理系统的技术实现方案。系统采用客户端-服务器架构,包含三个主要组件: + +1. **许可证验证服务器** - 部署在 45.249.208.118,提供许可证验证 API 和客户管理后台 +2. **客户端验证模块** - 集成在 FLNXI 素材系统中,负责激活、验证和功能控制 +3. **客户管理后台** - Web 界面,用于管理客户和许可证 + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 许可证验证服务器 (45.249.208.118) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ License Server │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ License API │ │ Admin API │ │ Admin Portal│ │ │ +│ │ │ /api/license│ │ /api/admin │ │ /admin │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ │ │ │ │ │ │ +│ │ └────────────────┼────────────────┘ │ │ +│ │ │ │ │ +│ │ ┌──────┴──────┐ │ │ +│ │ │ PostgreSQL │ │ │ +│ │ │ (licenses) │ │ │ +│ │ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ HTTP + 应用层加密 + RSA Signed + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ 客户 A │ │ 客户 B │ │ 客户 C │ +│ NAS/服务器 │ │ NAS/服务器 │ │ NAS/服务器 │ +│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ +│ │FLNXI素材 │ │ │ │FLNXI素材 │ │ │ │FLNXI素材 │ │ +│ │+ 验证模块 │ │ │ │+ 验证模块 │ │ │ │+ 验证模块 │ │ +│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ +└───────────────┘ └───────────────┘ └───────────────┘ +``` + +## Components and Interfaces + +### 1. 许可证服务器 API + +#### 1.1 激活接口 + +``` +POST /api/license/activate +``` + +**Request:** +```typescript +interface ActivateRequest { + licenseKey: string // 许可证密钥 + fingerprint: HardwareFingerprint // 硬件指纹 + timestamp: number // 请求时间戳 +} +``` + +**Response:** +```typescript +interface ActivateResponse { + success: boolean + license?: SignedLicense // 签名后的许可证数据 + error?: string +} +``` + +#### 1.2 验证接口 + +``` +POST /api/license/verify +``` + +**Request:** +```typescript +interface VerifyRequest { + instanceId: string // 实例唯一ID + fingerprint: HardwareFingerprint + timestamp: number + signature: string // 请求签名 +} +``` + +**Response:** +```typescript +interface VerifyResponse { + valid: boolean + license?: SignedLicense + error?: string +} +``` + +#### 1.3 心跳接口 + +``` +POST /api/license/heartbeat +``` + +**Request:** +```typescript +interface HeartbeatRequest { + instanceId: string + fingerprint: HardwareFingerprint + usage: { + userCount: number + materialCount: number + } + timestamp: number + signature: string +} +``` + +**Response:** +```typescript +interface HeartbeatResponse { + acknowledged: boolean + license?: SignedLicense // 如有更新则返回新配置 +} +``` + +### 2. 客户端验证模块 + +#### 2.1 许可证服务 (lib/license/license-service.ts) + +```typescript +class LicenseService { + // 激活许可证 + async activate(licenseKey: string): Promise + + // 验证许可证 + async verify(): Promise + + // 发送心跳 + async heartbeat(): Promise + + // 获取当前许可证配置 + getLicense(): LicenseConfig | null + + // 检查功能是否可用 + isFeatureEnabled(feature: FeatureKey): boolean + + // 检查是否达到限制 + async checkLimit(type: 'users' | 'materials'): Promise +} +``` + +#### 2.2 硬件指纹服务 (lib/license/fingerprint-service.ts) + +```typescript +class FingerprintService { + // 生成硬件指纹 + async generateFingerprint(): Promise + + // 获取或创建实例ID + async getInstanceId(): Promise + + // 计算指纹匹配分数 + calculateMatchScore(fp1: HardwareFingerprint, fp2: HardwareFingerprint): number +} +``` + +#### 2.3 加密服务 (lib/license/crypto-service.ts) + +```typescript +class CryptoService { + // 验证服务器签名 + verifySignature(data: string, signature: string): boolean + + // 生成请求签名 + signRequest(data: object): string + + // 加密本地存储数据 + encrypt(data: string): string + + // 解密本地存储数据 + decrypt(encrypted: string): string +} +``` + +### 3. 客户管理后台 API + +#### 3.1 客户管理 + +``` +GET /api/admin/customers # 获取客户列表 +POST /api/admin/customers # 创建客户 +GET /api/admin/customers/:id # 获取客户详情 +PUT /api/admin/customers/:id # 更新客户信息 +DELETE /api/admin/customers/:id # 删除客户 +``` + +#### 3.2 许可证管理 + +``` +POST /api/admin/licenses # 生成新许可证 +PUT /api/admin/licenses/:id # 更新许可证(升级/降级) +POST /api/admin/licenses/:id/disable # 禁用许可证 +POST /api/admin/licenses/:id/reset # 重置硬件绑定 +``` + +#### 3.3 统计和监控 + +``` +GET /api/admin/dashboard # 仪表盘数据 +GET /api/admin/alerts # 异常告警列表 +POST /api/admin/alerts/:id/resolve # 处理告警 +``` + +## Data Models + +### 1. 许可证服务器数据库 + +```sql +-- 客户表 +CREATE TABLE customers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + company_name VARCHAR(255) NOT NULL, + contact_name VARCHAR(100), + contact_email VARCHAR(255), + contact_phone VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 许可证表 +CREATE TABLE licenses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + customer_id UUID REFERENCES customers(id), + license_key VARCHAR(19) UNIQUE NOT NULL, -- XXXX-XXXX-XXXX-XXXX + license_type VARCHAR(20) NOT NULL, -- FREE, STANDARD, PRO + status VARCHAR(20) DEFAULT 'active', -- active, disabled, expired + max_users INT DEFAULT -1, -- -1 表示无限 + max_materials INT DEFAULT -1, + features JSONB DEFAULT '{}', + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 激活记录表 +CREATE TABLE activations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + license_id UUID REFERENCES licenses(id), + instance_id VARCHAR(64) NOT NULL, + fingerprint JSONB NOT NULL, + activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_heartbeat TIMESTAMP, + usage_stats JSONB DEFAULT '{}', + is_active BOOLEAN DEFAULT true +); + +-- 心跳记录表(用于异常检测) +CREATE TABLE heartbeat_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + license_id UUID REFERENCES licenses(id), + instance_id VARCHAR(64) NOT NULL, + fingerprint_hash VARCHAR(64) NOT NULL, + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 告警表 +CREATE TABLE alerts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + license_id UUID REFERENCES licenses(id), + alert_type VARCHAR(50) NOT NULL, + details JSONB, + status VARCHAR(20) DEFAULT 'pending', -- pending, resolved, ignored + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP +); +``` + +### 2. 客户端本地存储 + +```typescript +// 存储在 FLNXI 素材系统数据库中 +interface LocalLicenseData { + instanceId: string // 实例唯一ID + licenseKey: string // 许可证密钥(加密存储) + licenseData: string // 签名后的许可证数据(加密存储) + lastVerified: Date // 最后验证时间 + cachedConfig: LicenseConfig // 缓存的许可证配置 +} +``` + +### 3. 许可证配置结构 + +```typescript +interface LicenseConfig { + type: 'FREE' | 'STANDARD' | 'PRO' + limits: { + maxUsers: number // -1 表示无限 + maxMaterials: number // -1 表示无限 + } + features: { + customTemplates: boolean + systemSettings: boolean + departmentIsolation: boolean + apiAccess: boolean + intranetPenetration: boolean + } + expiresAt: string | null // ISO 日期字符串,null 表示永久 +} + +interface SignedLicense { + data: LicenseConfig + signature: string // RSA 签名 + issuedAt: string + expiresAt: string +} +``` + +### 4. 硬件指纹结构 + +```typescript +interface HardwareFingerprint { + machineId: string // 系统 machine-id + hostname: string // 主机名 + macAddress: string // 主网卡 MAC 地址 + cpuId?: string // CPU ID(可选) + instanceId: string // 首次激活生成的唯一ID +} +``` + + +## Correctness Properties + +*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* + +### Property 1: License Type Feature Consistency +*For any* license configuration, the features enabled/disabled must be consistent with the license type definition (FREE has restrictions, STANDARD/PRO have full features). +**Validates: Requirements 1.1, 1.4, 1.5, 1.6, 1.7, 1.8** + +### Property 2: User Limit Enforcement +*For any* FREE license, attempting to create more than 10 users must be rejected, and the user count must never exceed 10. +**Validates: Requirements 1.2, 5.1** + +### Property 3: Material Limit Enforcement +*For any* FREE license, attempting to upload more than 100 materials must be rejected, and the material count must never exceed 100. +**Validates: Requirements 1.3, 5.2** + +### Property 4: Activation Round-Trip +*For any* valid license key, activating and then verifying should return the same license configuration. +**Validates: Requirements 2.2, 2.3, 2.4, 2.5** + +### Property 5: Invalid License Key Rejection +*For any* string that does not match the license key format or does not exist in the database, activation must fail with an appropriate error. +**Validates: Requirements 2.7** + +### Property 6: Fingerprint Component Inclusion +*For any* generated hardware fingerprint, it must contain machineId, hostname, and macAddress, and must NOT contain RAM or non-system disk information. +**Validates: Requirements 3.1, 3.2, 3.3** + +### Property 7: Instance ID Uniqueness +*For any* two different installations, the generated instance IDs must be unique. +**Validates: Requirements 3.4** + +### Property 8: Fingerprint Matching Threshold +*For any* two hardware fingerprints, if the match score is >= 60%, verification should pass; if < 60%, verification should require re-activation. +**Validates: Requirements 3.5, 3.6** + +### Property 9: Signature Verification Round-Trip +*For any* license data, signing with the private key and verifying with the public key should succeed. Any modification to the signed data should cause verification to fail. +**Validates: Requirements 9.2, 9.3, 9.4, 9.5** + +### Property 10: License Data Persistence Round-Trip +*For any* license data stored locally, encrypting, storing, retrieving, and decrypting should return the original data. +**Validates: Requirements 10.1, 10.2** + +### Property 11: Anomaly Detection Trigger +*For any* license that receives heartbeats from more than one distinct hardware fingerprint within 24 hours, an alert must be generated. +**Validates: Requirements 7.1, 7.2** + +### Property 12: Code Obfuscation Functional Equivalence +*For any* input to the license verification functions, the obfuscated code must produce identical output to the non-obfuscated code. +**Validates: Requirements 11.5** + +## Error Handling + +### Client-Side Error Handling + +| Error Scenario | Handling Strategy | +|----------------|-------------------| +| Network timeout | Retry 3 times with exponential backoff, then use cached license | +| Invalid signature | Reject license data, log incident, prompt user to contact support | +| License expired | Display renewal prompt, allow read-only access | +| Activation failed | Display error message, allow retry or free trial | +| Grace period expired | Enter read-only mode, display activation prompt | + +### Server-Side Error Handling + +| Error Scenario | Handling Strategy | +|----------------|-------------------| +| Invalid license key | Return 400 with error message | +| License already activated | Return 409 with error message | +| Fingerprint mismatch | Return 403, log for anomaly detection | +| Database error | Return 500, log error, alert admin | +| Rate limit exceeded | Return 429, implement exponential backoff | + +## Testing Strategy + +### Unit Testing + +使用 Jest 进行单元测试,覆盖以下模块: + +1. **LicenseService** - 测试激活、验证、心跳逻辑 +2. **FingerprintService** - 测试指纹生成和匹配算法 +3. **CryptoService** - 测试签名和加密功能 +4. **License API** - 测试 API 端点的请求/响应处理 + +### Property-Based Testing + +使用 fast-check 进行属性测试,验证以下属性: + +1. **License Type Consistency** - 生成随机许可证配置,验证功能开关一致性 +2. **Limit Enforcement** - 生成随机用户/素材数量,验证限制执行 +3. **Signature Round-Trip** - 生成随机数据,验证签名/验证往返 +4. **Encryption Round-Trip** - 生成随机数据,验证加密/解密往返 +5. **Fingerprint Matching** - 生成随机指纹变化,验证匹配算法 + +### Integration Testing + +1. **激活流程** - 端到端测试完整激活流程 +2. **验证流程** - 测试启动验证和心跳验证 +3. **功能限制** - 测试各版本的功能限制执行 +4. **异常检测** - 测试多设备使用告警 + +## Security Considerations + +### RSA Key Management + +``` +密钥管理策略: + +私钥: +├── 仅存储在许可证服务器上 +├── 使用环境变量注入,不提交到代码仓库 +├── 定期轮换(建议每年) +└── 备份存储在安全位置 + +公钥: +├── 嵌入客户端代码中 +├── 可以公开,不影响安全性 +└── 随私钥轮换时更新 +``` + +### API Security + +``` +API 安全措施: + +1. HTTP + 应用层加密(当前方案) + ├── 使用 HTTP 协议通信(服务器暂未配置 HTTPS) + ├── 所有敏感数据在应用层使用 AES-256 加密传输 + ├── 许可证数据使用 RSA 签名,无法伪造 + └── 后续可升级为 HTTPS 以增强安全性 + +2. 请求签名 + └── 客户端请求包含时间戳和签名,防止重放攻击 + +3. 速率限制 + ├── 激活接口:每 IP 每小时 10 次 + ├── 验证接口:每实例每分钟 5 次 + └── 心跳接口:每实例每分钟 2 次 + +4. IP 白名单(可选) + └── 管理后台可限制访问 IP + +5. 数据加密传输 + ├── 请求体使用 AES-256-GCM 加密 + ├── AES 密钥使用 RSA 公钥加密后传输 + └── 实现类似 HTTPS 的端到端加密效果 +``` + +### Code Obfuscation Strategy + +``` +混淆策略: + +需要混淆的文件: +├── lib/license/license-service.ts +├── lib/license/fingerprint-service.ts +├── lib/license/crypto-service.ts +└── middleware/license-middleware.ts + +混淆技术: +├── 变量名混淆(renaming) +├── 字符串编码(string encoding) +├── 控制流扁平化(control flow flattening) +├── 死代码注入(dead code injection) +└── 调试保护(debug protection) + +不混淆的文件: +├── 业务组件(components/*) +├── 页面文件(app/*) +├── 工具函数(lib/utils/*) +└── 样式文件 +``` + +### Anti-Crack Strategy(防破解策略) + +``` +安全等级定位: +├── 能防住:普通用户、初级开发者、大部分中级开发者 +├── 防不住:专业破解者(接受这个现实) +└── 目标:让破解成本高于购买成本 + +防护层次: + +第一层:首次激活强制联网 +├── 系统首次启动必须联网激活 +├── 没有有效的签名许可证,系统无法正常使用 +└── 激活后服务器记录该客户,纳入监控 + +第二层:RSA 签名验证 +├── 许可证数据使用 RSA 私钥签名 +├── 客户端使用公钥验证签名 +├── 没有私钥无法伪造有效许可证 +└── 签名验证代码经过混淆处理 + +第三层:多点校验 +├── 启动时验证许可证签名 +├── 关键功能入口验证许可证状态 +├── 定时心跳验证 +└── 不是改一处代码就能绕过 + +第四层:代码混淆 +├── 关键文件混淆处理 +├── 字符串加密 +├── 控制流扁平化 +└── 增加逆向分析难度 + +第五层:持续更新 +├── 定期发布新版本 +├── 每次更新可能改变验证逻辑 +├── 破解版很快过时 +└── 正版用户持续获得新功能 + +防破解版售卖: +├── 破解版没有技术支持和更新 +├── 企业客户有法律风险顾虑 +├── 软件著作权登记提供法律保护 +└── 合理定价让正版更划算 +``` + +### Server Migration(服务器迁移支持) + +``` +设计原则: +├── 许可证服务器地址通过环境变量配置 +├── 不硬编码服务器 IP 地址 +└── 支持平滑迁移到新服务器 + +客户端配置: +├── 环境变量:LICENSE_SERVER_URL +├── 默认值:http://45.249.208.118:3001 +└── 可通过配置文件或环境变量覆盖 + +迁移步骤: +1. 在新服务器部署许可证服务 +2. 迁移数据库(客户、许可证、激活记录) +3. 迁移 RSA 密钥对(必须使用相同密钥) +4. 测试新服务器 API 正常 +5. 更新 DNS 或发布新版本指向新地址 +6. 旧服务器保留一段时间作为备份 + +注意事项: +├── RSA 密钥对必须迁移,否则已激活的客户端无法验证 +├── 数据库完整迁移,保留所有激活记录 +└── 建议使用域名而非 IP,方便后续切换 +``` + +## Deployment Architecture + +### License Server Deployment + +``` +部署在 45.249.208.118: + +Docker Compose 配置: +├── license-server(Next.js 应用) +│ ├── 端口:3001 +│ └── 环境变量:RSA 私钥、数据库连接 +├── postgres(数据库) +│ ├── 端口:5432 +│ └── 数据卷:持久化存储 +└── nginx(反向代理) + ├── 端口:80(HTTP) + └── 后续可配置 SSL 证书升级为 HTTPS +``` + +### Client Integration + +``` +FLNXI 素材系统集成: + +新增文件: +├── lib/license/ +│ ├── license-service.ts +│ ├── fingerprint-service.ts +│ ├── crypto-service.ts +│ └── types.ts +├── middleware/license-middleware.ts +├── app/activate/page.tsx +└── components/LicenseInfo.tsx + +修改文件: +├── middleware.ts(添加许可证检查) +├── app/admin/settings/page.tsx(添加许可证信息) +└── 各功能入口(添加功能检查) +``` + + +## Self-Service Installation Design (Agent 方案) + +### 架构概述 + +采用客户自助安装方案,客户在 NAS 上运行 Agent 容器后,通过 Agent 提供的 Web 界面自助完成系统部署,无需联系技术支持。Agent 仅在检查版本更新时连接许可证服务器,不验证许可证,不进行任何监控。 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 许可证服务器 (45.249.208.118) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ License Server │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ Version API │ │ │ +│ │ │ GET /api/version/check (检查版本更新) │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ▲ + │ HTTP (仅检查版本更新时连接) + │ + ┌─────────────────────┼─────────────────────┐ + │ │ │ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +│ 客户 A │ │ 客户 B │ │ 客户 C │ +│ NAS/服务器 │ │ NAS/服务器 │ │ NAS/服务器 │ +│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ +│ │flnxi-agent│ │ │ │flnxi-agent│ │ │ │flnxi-agent│ │ +│ │ Web UI │ │ │ │ Web UI │ │ │ │ Web UI │ │ +│ │ :9000 │ │ │ │ :9000 │ │ │ │ :9000 │ │ +│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ +│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ +│ │FLNXI素材 │ │ │ │FLNXI素材 │ │ │ │FLNXI素材 │ │ +│ │ :4320 │ │ │ │ :4320 │ │ │ │ :4320 │ │ +│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ +└───────────────┘ └───────────────┘ └───────────────┘ +``` + +### 客户自助安装流程(以飞牛 fnOS 为例) + +``` +【客户全程自助操作 - 约 5 分钟】 + +步骤1:在飞牛 Docker 管理界面搜索 flnxi-agent +┌─────────────────────────────────────────┐ +│ 飞牛 fnOS - Docker - 镜像仓库 │ +│ ┌─────────────────────────────────┐ │ +│ │ 🔍 搜索: flnxi │ │ +│ └─────────────────────────────────┘ │ +│ │ +│ 📦 flnxi/flnxi-agent │ +│ FLNXI 素材系统安装助手 │ +│ [⬇️ 下载] │ +└─────────────────────────────────────────┘ + +步骤2:创建并运行容器(端口映射 9000:9000) +┌─────────────────────────────────────────┐ +│ 创建容器 - flnxi-agent │ +│ ───────────────────────────────────── │ +│ 端口映射: │ +│ 本地端口: [9000] → 容器端口: 9000 │ +│ │ +│ 卷映射: │ +│ /var/run/docker.sock → (必需) │ +│ │ +│ [创建并启动] │ +└─────────────────────────────────────────┘ + +步骤3:浏览器访问 Agent Web 界面 +┌─────────────────────────────────────────┐ +│ 🎨 FLNXI 素材系统安装向导 │ +│ http://192.168.1.100:9000 │ +│ ───────────────────────────────────── │ +│ │ +│ 欢迎使用 FLNXI 素材系统! │ +│ 请按照以下步骤完成安装。 │ +│ │ +│ [开始安装 →] │ +└─────────────────────────────────────────┘ + +步骤4:配置安装选项 +┌─────────────────────────────────────────┐ +│ ⚙️ 安装配置 │ +│ ───────────────────────────────────── │ +│ │ +│ 系统端口: [4320] │ +│ │ +│ 💡 许可证激活将在系统首次启动时进行 │ +│ │ +│ [开始安装 →] │ +└─────────────────────────────────────────┘ + +步骤5:等待自动安装 +┌─────────────────────────────────────────┐ +│ ⏳ 正在安装 FLNXI 素材系统... │ +│ ───────────────────────────────────── │ +│ │ +│ ✅ 拉取系统镜像 │ +│ ✅ 拉取依赖镜像 │ +│ 🔄 创建数据库容器 (进行中...) │ +│ ⬚ 创建系统容器 │ +│ ⬚ 启动系统 │ +│ │ +│ [████████████░░░░░░░░] 60% │ +│ │ +│ 💡 首次安装需要下载约 500MB 镜像 │ +└─────────────────────────────────────────┘ + +步骤6:安装完成 +┌─────────────────────────────────────────┐ +│ ✅ 安装完成! │ +│ ───────────────────────────────────── │ +│ │ +│ 🎉 FLNXI 素材系统已成功安装! │ +│ │ +│ 访问地址: http://192.168.1.100:4320 │ +│ 默认账号: admin@flnxi.com │ +│ 默认密码: admin123 │ +│ │ +│ ⚠️ 请首次登录后立即修改密码 │ +│ 💡 许可证密钥可在系统设置中随时配置 │ +│ │ +│ [打开系统] [进入管理面板] │ +└─────────────────────────────────────────┘ + + +【安装完成后 - Agent 管理面板】 +┌─────────────────────────────────────────┐ +│ 🎨 FLNXI 系统管理 │ +│ http://192.168.1.100:9000 │ +│ ───────────────────────────────────── │ +│ │ +│ 系统状态 │ +│ ├─ 状态: ✅ 运行中 │ +│ ├─ 当前版本: v1.2.0 │ +│ ├─ 最新版本: v1.3.0 │ +│ └─ 访问地址: http://192.168.1.100:4320 │ +│ │ +│ [检查更新] [更新系统] [重启] [日志] │ +│ │ +│ ───────────────────────────────────── │ +│ 💡 建议保留此管理工具用于后续系统更新 │ +└─────────────────────────────────────────┘ +``` + +### Agent 技术栈 + +``` +Agent 容器技术选型: + +运行时:Node.js 20 Alpine +├── 镜像体积小(约 50MB) +├── 跨平台支持(x86_64, ARM64) +└── 与现有技术栈一致 + +核心依赖: +├── express:Web 服务器(提供安装界面) +├── dockerode:Docker API 客户端 +└── socket.io:实时进度推送到浏览器 + +镜像发布: +├── Docker Hub: flnxi/flnxi-agent:latest(公开,客户可直接搜索) +├── 备用: harbor.flnxi.com/flnxi/flnxi-agent:latest +└── 支持架构:linux/amd64, linux/arm64 +``` + +### 隐私原则 + +``` +Agent 不收集、不上报任何客户数据: + +✅ Agent 会做的事: +├── 拉取 FLNXI 素材系统镜像 +├── 创建和启动 Docker 容器 +├── 显示安装进度 +└── 提供系统更新功能 + +❌ Agent 不会做的事: +├── 不验证许可证(由素材系统自己处理) +├── 不上报客户 NAS 信息 +├── 不监控客户使用行为 +├── 不保持与服务器的长连接 +├── 不收集任何统计数据 +└── 安装完成后可以删除(但建议保留用于更新) +``` + +### Agent 核心逻辑 + +#### Agent Web 服务 + +```typescript +// flnxi-agent/src/server.ts + +import express from 'express'; +import Docker from 'dockerode'; +import { Server } from 'socket.io'; + +const app = express(); +const docker = new Docker({ socketPath: '/var/run/docker.sock' }); + +// 静态文件(React 构建产物) +app.use(express.static('public')); + +// 开始安装 +app.post('/api/install', async (req, res) => { + const { port } = req.body; + + // 返回安装任务 ID,通过 WebSocket 推送进度 + const taskId = nanoid(); + installQueue.add({ taskId, port }); + + res.json({ taskId }); +}); + +// WebSocket 推送安装进度 +io.on('connection', (socket) => { + socket.on('subscribe', (taskId) => { + socket.join(taskId); + }); +}); + +// 安装任务处理 +async function processInstall(task: InstallTask) { + const { taskId, port } = task; + + try { + // 1. 拉取镜像 + emit(taskId, { stage: 'pulling', progress: 10 }); + await docker.pull('harbor.flnxi.com/flnxi/material/flnxi-material:latest'); + // ... 带进度的拉取 + + // 2. 拉取依赖镜像(postgres 等) + emit(taskId, { stage: 'pulling_deps', progress: 40 }); + await pullDependencies(); + + // 3. 创建容器 + emit(taskId, { stage: 'creating', progress: 70 }); + await docker.createContainer({ + Image: 'harbor.flnxi.com/flnxi/material/flnxi-material:latest', + name: 'flnxi-material', + HostConfig: { + PortBindings: { '3000/tcp': [{ HostPort: String(port) }] }, + RestartPolicy: { Name: 'unless-stopped' } + } + }); + + // 4. 启动容器 + emit(taskId, { stage: 'starting', progress: 90 }); + await container.start(); + + // 5. 完成 + const localIP = getLocalIP(); + emit(taskId, { + stage: 'completed', + progress: 100, + accessUrl: `http://${localIP}:${port}` + }); + + // 许可证激活由素材系统首次启动时自行处理 + + } catch (error) { + emit(taskId, { stage: 'error', error: error.message }); + } +} +``` + +### Agent 项目结构 + +``` +flnxi-agent/ +├── package.json +├── Dockerfile # 多架构构建 +├── src/ +│ ├── index.ts # 入口 +│ ├── server.ts # Express Web 服务 +│ ├── installer.ts # 安装逻辑 +│ ├── docker-client.ts # Docker API 封装 +│ └── utils/ +│ └── network.ts # 网络工具(获取本机 IP) +├── public/ # React 前端构建产物 +│ ├── index.html +│ └── assets/ +└── frontend/ # React 前端源码 + ├── src/ + │ ├── App.tsx + │ ├── pages/ + │ │ ├── Welcome.tsx # 欢迎页 + │ │ ├── Config.tsx # 端口配置 + │ │ ├── Installing.tsx # 安装进度 + │ │ ├── Complete.tsx # 完成页 + │ │ └── Dashboard.tsx # 管理面板(更新、重启) + │ └── components/ + │ └── ProgressBar.tsx + └── package.json + +镜像构建: +├── Docker Hub: flnxi/flnxi-agent:latest(公开) +├── 支持架构:linux/amd64, linux/arm64 +└── 镜像大小:约 80MB(含 Web UI) +``` + +### 许可证验证说明 + +``` +许可证验证完全由素材系统处理,Agent 不参与: + +Agent 职责: +├── 拉取和部署 Docker 镜像 +├── 配置端口映射 +├── 提供系统更新功能 +└── 不涉及任何许可证逻辑 + +素材系统职责: +├── 首次启动时显示激活页面 +├── 调用 /api/license/activate 绑定硬件指纹 +├── 定期发送心跳验证 +└── 执行功能限制 + +这样设计的好处: +├── Agent 更简单,不需要连接许可证服务器 +├── 所有许可证逻辑集中在素材系统内部 +├── 减少网络依赖,Agent 只需访问 Harbor +└── 客户可以先安装,后激活 +``` + + +## 版本管理与更新机制 + +### 版本号管理策略 + +``` +语义化版本:v主版本.次版本.修订号 +例如:v1.2.3 + +├── 主版本(Major):不兼容的重大更新 +│ └── 如数据库结构变化、API 重大变更 +├── 次版本(Minor):新功能,向后兼容 +│ └── 如新增功能模块、UI 改进 +└── 修订号(Patch):Bug 修复、安全补丁 + └── 如修复已知问题、性能优化 + +镜像标签策略: +├── latest # 最新稳定版(自动指向最新 tag) +├── v1.2.3 # 具体版本(永久保留,用于回滚) +├── v1.2 # 次版本最新(自动更新修订号) +└── v1 # 主版本最新(自动更新次版本和修订号) + +版本文件: +├── 镜像内 /app/VERSION.txt 包含当前版本号 +└── Agent 通过读取此文件获取当前版本 +``` + +### 更新失败回滚机制 + +``` +更新流程(带自动回滚): + +┌─────────────────────────────────────────────────────────────┐ +│ 1. 记录当前状态 │ +│ ├── 保存当前镜像 tag(如 v1.2.0) │ +│ ├── 保存容器配置(端口、环境变量、卷映射) │ +│ └── 写入 /data/flnxi-backup.json │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. 拉取新镜像 │ +│ └── docker pull harbor.flnxi.com/flnxi/material:v1.3.0 │ +│ ├── 成功 → 继续 │ +│ └── 失败 → 提示网络问题,保持原状 │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. 停止旧容器(不删除) │ +│ └── docker stop flnxi-material │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 4. 重命名旧容器 │ +│ └── docker rename flnxi-material flnxi-material-backup │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 5. 启动新容器 │ +│ └── docker run --name flnxi-material ... │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 6. 健康检查(等待 60 秒,检测 /api/health) │ +│ ├── 成功 → 删除备份容器,更新完成 │ +│ └── 失败 → 触发自动回滚 │ +└─────────────────────────────────────────────────────────────┘ + +自动回滚流程: +┌─────────────────────────────────────────────────────────────┐ +│ 回滚步骤: │ +│ 1. docker stop flnxi-material │ +│ 2. docker rm flnxi-material │ +│ 3. docker rename flnxi-material-backup flnxi-material │ +│ 4. docker start flnxi-material │ +│ 5. 提示用户"更新失败,已自动回滚到 v1.2.0" │ +└─────────────────────────────────────────────────────────────┘ + +手动回滚(Agent 管理面板): +├── 显示"回滚到上一版本"按钮 +├── 列出最近 3 个可用版本供选择 +└── 执行回滚后显示结果 +``` + +### 强制更新 vs 可选更新 + +``` +更新类型判断: + +许可证服务器返回版本信息: +GET /api/version/check?current=1.2.0 + +Response: +{ + "latestVersion": "1.3.0", + "minRequiredVersion": "1.2.0", // 低于此版本强制更新 + "releaseNotes": "新增XX功能...", + "releaseDate": "2025-12-15", + "isSecurityUpdate": false, + "isCriticalUpdate": false, + "downloadSize": "45MB" +} + +┌─────────────────────────────────────────────────────────────┐ +│ 可选更新(默认) │ +│ ───────────────────────────────────────────────────────── │ +│ 触发条件: │ +│ ├── 当前版本 >= minRequiredVersion │ +│ └── isSecurityUpdate = false │ +│ │ +│ 用户体验: │ +│ ├── Agent 管理面板显示"有新版本可用 v1.3.0" │ +│ ├── 显示更新内容和下载大小 │ +│ ├── 客户自行决定是否更新 │ +│ └── 可以选择"稍后提醒"或"跳过此版本" │ +│ │ +│ 适用场景: │ +│ ├── 新功能发布 │ +│ ├── 性能优化 │ +│ └── 非关键 Bug 修复 │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ 强制更新 │ +│ ───────────────────────────────────────────────────────── │ +│ 触发条件(满足任一): │ +│ ├── 当前版本 < minRequiredVersion │ +│ ├── isSecurityUpdate = true │ +│ └── isCriticalUpdate = true │ +│ │ +│ 用户体验: │ +│ ├── 素材系统启动时检测版本 │ +│ ├── 弹窗提示"检测到重要更新,必须更新才能继续使用" │ +│ ├── 阻止进入主界面 │ +│ ├── 只能选择"立即更新"或"退出系统" │ +│ └── 更新完成后自动重启进入系统 │ +│ │ +│ 适用场景: │ +│ ├── 安全漏洞修复 │ +│ ├── 许可证协议变更 │ +│ ├── 严重 Bug 导致数据风险 │ +│ └── API 不兼容需要同步更新 │ +└─────────────────────────────────────────────────────────────┘ + +Agent 管理面板更新界面: +┌─────────────────────────────────────────┐ +│ 🔄 系统更新 │ +│ ───────────────────────────────────── │ +│ │ +│ 当前版本: v1.2.0 │ +│ 最新版本: v1.3.0 ⚠️ 重要安全更新 │ +│ │ +│ 更新内容: │ +│ • 修复安全漏洞 CVE-2025-XXXX │ +│ • 新增批量导入功能 │ +│ • 优化搜索性能 │ +│ │ +│ 下载大小: 约 45MB(增量更新) │ +│ 预计时间: 2-5 分钟 │ +│ │ +│ [立即更新] [查看详情] │ +│ │ +│ ───────────────────────────────────── │ +│ 历史版本: │ +│ • v1.2.0 (当前) - 2025-12-01 │ +│ • v1.1.0 - 2025-11-15 │ +│ • v1.0.0 - 2025-11-01 │ +│ │ +│ [回滚到上一版本] │ +└─────────────────────────────────────────┘ +``` + + +## 镜像仓库结构 + +### Harbor 私有仓库(harbor.flnxi.com) + +``` +harbor.flnxi.com/flnxi/ +│ +├── material/ # 素材系统主镜像 +│ ├── flnxi-material:latest # 最新稳定版 +│ ├── flnxi-material:v1.0.0 # 具体版本(永久保留) +│ ├── flnxi-material:v1.1.0 +│ └── flnxi-material:v1.2.0 +│ +├── license-server/ # 许可证服务器镜像 +│ ├── license-server:latest +│ └── license-server:v1.0.0 +│ +└── runtime/ # 运行时依赖镜像 + │ # (方便网络不好的客户从 Harbor 拉取) + │ + ├── postgres:16-alpine # PostgreSQL 数据库 + ├── minio:latest # MinIO 对象存储 + ├── minio-mc:latest # MinIO 客户端 + ├── redis:7-alpine # Redis 缓存(如需要) + ├── nginx:alpine # Nginx 反向代理 + └── node:20-alpine # Node.js 基础镜像 + +镜像来源同步: +├── runtime/ 下的镜像从 Docker Hub 官方镜像同步 +├── 定期更新以获取安全补丁 +└── 客户可选择从 Harbor 或 Docker Hub 拉取 +``` + +### Docker Hub 公开仓库(hub.docker.com) + +``` +flnxi/ # Docker Hub 账号 +│ +└── flnxi-agent:latest # 安装 Agent(公开) + ├── flnxi-agent:v1.0.0 # 具体版本 + └── 支持架构: linux/amd64, linux/arm64 + +说明: +├── Agent 放 Docker Hub 是因为客户 NAS 默认搜索 Docker Hub +├── 客户无需配置私有仓库即可搜索到 flnxi-agent +└── Agent 体积小(约 80MB),下载快 +``` + +### Agent 镜像拉取策略 + +```typescript +// Agent 安装时的镜像拉取逻辑 + +const HARBOR_REGISTRY = 'harbor.flnxi.com/flnxi'; +const DOCKERHUB_FALLBACK = 'library'; // Docker Hub 官方镜像 + +interface ImageConfig { + name: string; + harbor: string; // Harbor 地址 + fallback?: string; // 备用地址(Docker Hub) +} + +const IMAGES: ImageConfig[] = [ + { + name: 'flnxi-material', + harbor: `${HARBOR_REGISTRY}/material/flnxi-material:latest`, + // 无 fallback,必须从 Harbor 拉取 + }, + { + name: 'postgres', + harbor: `${HARBOR_REGISTRY}/runtime/postgres:16-alpine`, + fallback: 'postgres:16-alpine', // Docker Hub 官方 + }, + { + name: 'minio', + harbor: `${HARBOR_REGISTRY}/runtime/minio:latest`, + fallback: 'minio/minio:latest', + }, + { + name: 'redis', + harbor: `${HARBOR_REGISTRY}/runtime/redis:7-alpine`, + fallback: 'redis:7-alpine', + }, +]; + +async function pullImage(config: ImageConfig): Promise { + try { + // 优先从 Harbor 拉取 + await docker.pull(config.harbor); + console.log(`从 Harbor 拉取成功: ${config.name}`); + } catch (error) { + if (config.fallback) { + // Harbor 失败,尝试 Docker Hub + console.log(`Harbor 拉取失败,尝试 Docker Hub: ${config.name}`); + await docker.pull(config.fallback); + } else { + throw error; + } + } +} +``` + +### 镜像管理最佳实践 + +``` +版本发布流程: + +1. 开发完成,本地测试通过 +2. 构建镜像并打 tag + └── docker build -t harbor.flnxi.com/flnxi/material/flnxi-material:v1.3.0 . +3. 推送到 Harbor + └── docker push harbor.flnxi.com/flnxi/material/flnxi-material:v1.3.0 +4. 更新 latest 标签 + └── docker tag ... :latest && docker push ... :latest +5. 更新许可证服务器的版本信息 + └── 设置 latestVersion = "1.3.0" +6. 如果是安全更新,设置 minRequiredVersion + +镜像清理策略: +├── 保留最近 10 个版本的镜像 +├── 超过 6 个月的旧版本可以删除 +├── 但至少保留 3 个可回滚版本 +└── Harbor 可配置自动清理策略 + +Runtime 镜像同步: +├── 每月检查官方镜像更新 +├── 同步安全补丁版本到 Harbor +└── 更新 Agent 的默认拉取版本 +``` diff --git a/.kiro/specs/license-system/requirements.md b/.kiro/specs/license-system/requirements.md new file mode 100644 index 0000000..38dc23b --- /dev/null +++ b/.kiro/specs/license-system/requirements.md @@ -0,0 +1,236 @@ +# Requirements Document + +## Introduction + +本文档定义了 FLNXI 素材系统的许可证管理系统需求。该系统旨在实现软件的商业化分发,支持免费版、标准版和Pro版三个版本,通过联网验证和硬件指纹绑定实现防破解保护。系统包含两个主要部分:部署在客户端的许可证验证模块,以及部署在中央服务器的许可证验证服务和客户管理后台。 + +## Glossary + +- **License(许可证)**: 授权客户使用软件特定版本和功能的凭证 +- **License Key(许可证密钥)**: 用于激活许可证的唯一字符串,格式为 XXXX-XXXX-XXXX-XXXX +- **Hardware Fingerprint(硬件指纹)**: 基于客户服务器硬件特征生成的唯一标识符 +- **License Server(许可证服务器)**: 部署在 45.249.208.118 的中央验证服务 +- **Client Instance(客户端实例)**: 部署在客户 NAS/服务器上的 FLNXI 素材系统实例 +- **Heartbeat(心跳)**: 客户端定期向服务器发送的存活和验证请求 +- **Grace Period(宽限期)**: 网络断开后允许继续使用的时间窗口 +- **Admin Portal(管理后台)**: 用于管理客户和许可证的 Web 界面 +- **Code Obfuscation(代码混淆)**: 将源代码转换为难以阅读和理解的形式,增加逆向工程难度 +- **Agent(安装代理)**: 运行在客户 NAS 上的轻量级 Docker 容器,提供 Web 安装界面,帮助客户自助完成系统部署 +- **Semantic Versioning(语义化版本)**: 版本号格式为 v主版本.次版本.修订号(如 v1.2.3) +- **Rollback(回滚)**: 将系统恢复到之前版本的操作 +- **Forced Update(强制更新)**: 必须完成才能继续使用系统的更新 +- **Optional Update(可选更新)**: 用户可自行决定是否执行的更新 + +## Requirements + +### Requirement 1: 许可证版本定义 + +**User Story:** As a 系统管理员, I want to 定义不同版本的功能限制, so that 可以实现差异化的产品定价策略。 + +#### Acceptance Criteria + +1. THE License_Server SHALL support three license types: FREE, STANDARD, and PRO +2. WHEN a license type is FREE, THE System SHALL enforce a maximum of 10 users +3. WHEN a license type is FREE, THE System SHALL enforce a maximum of 100 materials +4. WHEN a license type is FREE, THE System SHALL disable custom template functionality +5. WHEN a license type is FREE, THE System SHALL disable advanced system settings +6. WHEN a license type is FREE, THE System SHALL disable department isolation functionality +7. WHEN a license type is STANDARD or PRO, THE System SHALL allow unlimited users and materials +8. WHEN a license type is PRO, THE System SHALL enable intranet penetration functionality + +### Requirement 2: 许可证激活 + +**User Story:** As a 客户, I want to 使用许可证密钥激活系统, so that 可以解锁对应版本的功能。 + +#### Acceptance Criteria + +1. WHEN a user first accesses the system without activation, THE System SHALL display an activation page +2. WHEN a user enters a valid license key, THE System SHALL send an activation request to the License_Server +3. WHEN the License_Server receives an activation request, THE System SHALL validate the license key format and existence +4. WHEN the license key is valid and not already activated, THE License_Server SHALL bind the license to the hardware fingerprint +5. WHEN activation succeeds, THE Client_Instance SHALL store the license information locally +6. WHEN a user chooses "free trial" without a license key, THE System SHALL activate as FREE version automatically +7. IF the license key is invalid or already activated on another device, THEN THE System SHALL display an appropriate error message +8. WHEN activation completes, THE System SHALL redirect to the main application + +### Requirement 3: 硬件指纹生成 + +**User Story:** As a 系统, I want to 生成稳定的硬件指纹, so that 许可证可以绑定到特定设备而不受常见硬件升级影响。 + +#### Acceptance Criteria + +1. THE System SHALL generate a hardware fingerprint using machine ID, hostname, and primary MAC address +2. THE System SHALL NOT include RAM information in the hardware fingerprint +3. THE System SHALL NOT include non-system disk information in the hardware fingerprint +4. THE System SHALL generate a unique instance ID on first activation and store it persistently +5. WHEN hardware components change, THE System SHALL use a weighted matching algorithm with a 60% threshold +6. WHEN the fingerprint match score falls below 60%, THE System SHALL require re-activation + +### Requirement 4: 许可证验证 + +**User Story:** As a 系统, I want to 定期验证许可证状态, so that 可以确保只有合法用户使用付费功能。 + +#### Acceptance Criteria + +1. WHEN the system starts, THE Client_Instance SHALL verify the license with the License_Server +2. WHILE the system is running, THE Client_Instance SHALL send heartbeat requests every hour +3. WHEN the License_Server receives a verification request, THE System SHALL check license validity, expiration, and hardware fingerprint +4. WHEN verification succeeds, THE License_Server SHALL return the current license configuration +5. WHEN verification fails due to network issues, THE Client_Instance SHALL use cached license data +6. WHEN the system operates offline, THE Client_Instance SHALL allow a grace period of 7 days +7. IF the grace period expires without successful verification, THEN THE System SHALL enter read-only mode + +### Requirement 5: 功能限制执行 + +**User Story:** As a 系统, I want to 根据许可证类型限制功能, so that 免费用户无法使用付费功能。 + +#### Acceptance Criteria + +1. WHEN a FREE user attempts to create the 11th user, THE System SHALL block the operation and display an upgrade prompt +2. WHEN a FREE user attempts to upload the 101st material, THE System SHALL block the operation and display an upgrade prompt +3. WHEN a FREE user accesses custom template settings, THE System SHALL hide or disable the functionality +4. WHEN a FREE user accesses department isolation settings, THE System SHALL hide or disable the functionality +5. WHEN checking feature availability, THE System SHALL query the cached license configuration +6. THE System SHALL display current usage statistics (users/materials count) in the settings page + +### Requirement 6: 客户管理后台 + +**User Story:** As a 平台管理员, I want to 管理所有客户的许可证, so that 可以进行销售和客户服务。 + +#### Acceptance Criteria + +1. THE Admin_Portal SHALL require authentication to access +2. WHEN an admin views the dashboard, THE System SHALL display total customers count by license type +3. WHEN an admin views the customer list, THE System SHALL display company name, license type, status, and expiration date +4. WHEN an admin creates a new license, THE System SHALL generate a unique license key +5. WHEN an admin upgrades a customer's license, THE System SHALL update the license type immediately +6. WHEN an admin disables a license, THE System SHALL mark it as inactive and reject future verifications +7. WHEN an admin resets hardware fingerprint, THE System SHALL allow the license to be activated on a new device +8. THE Admin_Portal SHALL display usage statistics for each customer (user count, material count, last heartbeat) + +### Requirement 7: 异常检测 + +**User Story:** As a 平台管理员, I want to 检测许可证滥用行为, so that 可以防止盗版和许可证共享。 + +#### Acceptance Criteria + +1. WHEN the same license key sends heartbeats from multiple different hardware fingerprints within 24 hours, THE System SHALL flag it as suspicious +2. WHEN suspicious activity is detected, THE Admin_Portal SHALL display an alert +3. WHEN an admin reviews suspicious activity, THE System SHALL show the list of hardware fingerprints and their locations +4. THE Admin_Portal SHALL allow admins to whitelist legitimate multi-device scenarios + +### Requirement 8: 许可证信息展示 + +**User Story:** As a 客户, I want to 查看当前许可证状态, so that 可以了解使用情况和到期时间。 + +#### Acceptance Criteria + +1. THE System SHALL display license information in the system settings page +2. THE System SHALL show license type, activation status, expiration date, and feature availability +3. THE System SHALL show current usage (user count, material count) compared to limits +4. WHEN the license is about to expire within 30 days, THE System SHALL display a renewal reminder +5. THE System SHALL provide a link to upgrade or renew the license + +### Requirement 9: 安全通信 + +**User Story:** As a 系统, I want to 确保许可证通信安全, so that 验证过程无法被篡改或伪造。 + +#### Acceptance Criteria + +1. THE System SHALL use HTTP with application-layer encryption for communication between Client_Instance and License_Server +2. THE System SHALL encrypt request payloads using AES-256-GCM before transmission +3. THE System SHALL sign all license data using RSA asymmetric encryption +4. THE Client_Instance SHALL verify the signature of license data using the embedded public key +5. THE System SHALL include timestamps in requests to prevent replay attacks +6. IF signature verification fails, THEN THE System SHALL reject the license data and log the incident +7. THE System SHALL support upgrading to HTTPS when SSL certificates are configured + +### Requirement 10: 数据持久化 + +**User Story:** As a 系统, I want to 安全存储许可证数据, so that 系统重启后无需重新激活。 + +#### Acceptance Criteria + +1. THE Client_Instance SHALL store license data in the database with encryption +2. THE Client_Instance SHALL store the instance ID persistently across restarts +3. THE License_Server SHALL store all customer and license data in a PostgreSQL database +4. THE System SHALL backup license data regularly to prevent data loss +5. WHEN license data is corrupted, THE System SHALL attempt to recover from the License_Server + +### Requirement 11: 代码混淆保护 + +**User Story:** As a 系统, I want to 对关键代码进行混淆处理, so that 增加逆向工程和破解的难度。 + +#### Acceptance Criteria + +1. THE Build_Process SHALL obfuscate license verification related source files during production build +2. THE Build_Process SHALL obfuscate hardware fingerprint generation source files during production build +3. THE Build_Process SHALL obfuscate cryptographic signature verification source files during production build +4. THE Build_Process SHALL NOT obfuscate business logic components to maintain debuggability +5. WHEN obfuscation is applied, THE System SHALL maintain identical functionality to non-obfuscated code +6. THE Build_Process SHALL use control flow flattening and string encoding techniques +7. THE Build_Process SHALL generate source maps separately for internal debugging purposes only +8. WHEN the production build completes, THE System SHALL verify that obfuscated code executes correctly + +### Requirement 12: 自助安装(Agent 方案) + +**User Story:** As a 客户, I want to 通过简单的方式自助部署和更新系统, so that 无需手动配置 Docker 命令,也不需要联系技术支持。 + +#### Acceptance Criteria + +1. THE System SHALL provide a lightweight Agent Docker image (flnxi/flnxi-agent) on Docker Hub for customers to download +2. THE Agent image SHALL be compatible with mainstream NAS systems including Synology, QNAP, and fnOS +3. WHEN the Agent container starts, THE Agent SHALL provide a Web-based management interface on port 9000 +4. THE Agent Web interface SHALL allow customer to configure system port (default 4320) +5. THE Agent SHALL NOT perform license validation (license activation is handled by the material system on first startup) +6. WHEN customer clicks install, THE Agent SHALL pull the FLNXI material system image from harbor.flnxi.com/flnxi/material/flnxi-material +7. THE Agent SHALL create and start the FLNXI material system container with configured settings +8. THE Agent Web interface SHALL display installation progress with clear status messages +9. WHEN installation completes, THE Agent SHALL display the access URL for the system +10. IF installation fails, THEN THE Agent SHALL display error details and provide retry option +11. THE Agent SHALL provide system update functionality to pull and deploy latest version +12. THE Agent Web interface SHALL display current system version and available updates +13. THE Agent SHALL NOT collect or transmit any customer usage data +14. THE Agent container SHOULD be kept running for future system updates + +### Requirement 13: 版本管理 + +**User Story:** As a 平台管理员, I want to 管理系统版本发布, so that 可以有序地向客户推送更新。 + +#### Acceptance Criteria + +1. THE System SHALL use semantic versioning format (vMAJOR.MINOR.PATCH) for all releases +2. THE License_Server SHALL maintain version information including latest version and minimum required version +3. WHEN a new version is released, THE Admin_Portal SHALL allow setting the minimum required version +4. THE License_Server SHALL provide a version check API that returns latest version, minimum required version, and release notes +5. THE System SHALL store version number in /app/VERSION.txt within the Docker image +6. THE Harbor registry SHALL retain at least the 3 most recent versions for rollback purposes + +### Requirement 14: 系统更新 + +**User Story:** As a 客户, I want to 方便地更新系统到最新版本, so that 可以获得新功能和安全修复。 + +#### Acceptance Criteria + +1. WHEN the Agent checks for updates, THE Agent SHALL compare current version with latest version from License_Server +2. WHEN an optional update is available, THE Agent SHALL display update notification with version number and release notes +3. WHEN a forced update is required, THE System SHALL block access to main application until update is completed +4. WHEN customer initiates update, THE Agent SHALL pull new image using Docker incremental layer download +5. THE Agent SHALL display download progress and estimated time during update +6. WHEN update completes, THE Agent SHALL restart the system container automatically +7. IF update fails, THEN THE Agent SHALL automatically rollback to previous version +8. THE Agent Web interface SHALL provide manual rollback option to any of the 3 most recent versions + +### Requirement 15: 更新回滚机制 + +**User Story:** As a 客户, I want to 在更新失败时自动恢复, so that 系统不会因为更新问题而无法使用。 + +#### Acceptance Criteria + +1. WHEN starting an update, THE Agent SHALL backup current container configuration +2. WHEN starting an update, THE Agent SHALL rename current container instead of deleting it +3. WHEN new container starts, THE Agent SHALL perform health check by calling /api/health endpoint +4. WHEN health check fails within 60 seconds, THE Agent SHALL trigger automatic rollback +5. WHEN rollback is triggered, THE Agent SHALL stop new container, restore backup container, and restart it +6. WHEN rollback completes, THE Agent SHALL display message indicating rollback reason and restored version +7. THE Agent Web interface SHALL allow manual rollback with version selection diff --git a/.kiro/specs/license-system/tasks.md b/.kiro/specs/license-system/tasks.md new file mode 100644 index 0000000..01a4d01 --- /dev/null +++ b/.kiro/specs/license-system/tasks.md @@ -0,0 +1,385 @@ +# Implementation Plan + +## Phase 1: 核心基础设施 + +- [x] 1. 设置许可证服务器项目结构 + - [x] 1.1 创建独立的 Next.js 项目 `license-server` + - 初始化项目,配置 TypeScript、Prisma、PostgreSQL + - 创建基础目录结构:`app/api/`, `lib/`, `prisma/` + - _Requirements: 6.1, 10.3_ + - [x] 1.2 配置数据库 Schema + - 创建 customers、licenses、activations、heartbeat_logs、alerts 表 + - 配置 Prisma 模型和关系 + - _Requirements: 10.3_ + - [x] 1.3 生成 RSA 密钥对 + - 创建密钥生成脚本 + - 配置环境变量存储私钥 + - 导出公钥供客户端使用 + - _Requirements: 9.3, 9.4_ + +- [x] 2. 实现加密服务模块 + - [x] 2.1 实现服务器端加密服务 `license-server/lib/crypto.ts` + - RSA 签名生成函数 + - AES-256-GCM 加密/解密函数 + - 请求签名验证函数 + - _Requirements: 9.2, 9.3, 9.5_ + - [x] 2.3 实现客户端加密服务 `lib/license/crypto-service.ts` + - RSA 签名验证函数(使用嵌入的公钥) + - AES-256-GCM 加密/解密函数 + - 本地数据加密存储函数 + - _Requirements: 9.4, 10.1_ + +- [x] 3. Checkpoint - 确保所有测试通过 + - 测试通过:crypto-service.test.ts, license-types.test.ts, activate/route.test.ts + +## Phase 2: 许可证类型和配置 + +- [x] 4. 实现许可证类型定义 + - [x] 4.1 创建许可证类型和配置接口 `lib/license/types.ts` + - 定义 LicenseType 枚举(FREE, STANDARD, PRO) + - 定义 LicenseConfig、SignedLicense、HardwareFingerprint 接口 + - 定义各版本的功能限制配置 + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8_ + - [x] 4.2 实现许可证配置生成函数 + - 根据许可证类型生成对应的功能配置 + - 实现限制值计算逻辑 + - _Requirements: 1.1-1.8_ + +## Phase 3: 硬件指纹服务 + +- [x] 5. 实现硬件指纹服务 + - [x] 5.1 实现硬件指纹生成 `lib/license/fingerprint-service.ts` + - 获取 machine-id + - 获取 hostname + - 获取主网卡 MAC 地址 + - 生成/获取持久化的 instance ID + - _Requirements: 3.1, 3.2, 3.3, 3.4_ + - [x] 5.3 实现指纹匹配算法 + - 实现加权匹配分数计算 + - 实现 60% 阈值判断逻辑 + - _Requirements: 3.5, 3.6_ + +- [x] 6. Checkpoint - 确保所有测试通过 + - 测试通过 ✓ + +## Phase 4: 许可证服务器 API + +- [x] 7. 实现许可证激活 API + - [x] 7.1 实现激活接口 `license-server/app/api/license/activate/route.ts` + - 验证许可证密钥格式和存在性 + - 检查是否已激活 + - 绑定硬件指纹 + - 生成签名后的许可证数据 + - _Requirements: 2.2, 2.3, 2.4_ + - [x] 7.2 实现许可证密钥生成函数 + - 生成 XXXX-XXXX-XXXX-XXXX 格式的唯一密钥 + - _Requirements: 6.4_ + +- [x] 8. 实现许可证验证 API + - [x] 8.1 实现验证接口 `license-server/app/api/license/verify/route.ts` + - 验证 instance ID 和硬件指纹 + - 检查许可证有效性和过期时间 + - 返回签名后的许可证配置 + - _Requirements: 4.3, 4.4_ + +- [x] 9. 实现心跳 API + - [x] 9.1 实现心跳接口 `license-server/app/api/license/heartbeat/route.ts` + - 记录心跳日志 + - 更新使用统计 + - 检测异常(多设备使用) + - _Requirements: 4.2, 7.1_ + +- [x] 10. 实现版本检查 API + - [x] 10.1 实现版本检查接口 `license-server/app/api/version/check/route.ts` + - 返回最新版本和最低要求版本 + - _Requirements: 13.4, 14.1_ + +- [x] 11. Checkpoint - 确保所有测试通过 + - API 测试通过(activate, verify, heartbeat, version/check) + +## Phase 5: 客户端许可证服务 + +- [x] 12. 实现客户端许可证服务 + - [x] 12.1 实现许可证服务 `lib/license/license-service.ts` + - activate() - 激活许可证 + - verify() - 验证许可证 + - heartbeat() - 发送心跳 + - getLicense() - 获取当前配置 + - isFeatureEnabled() - 检查功能可用性 + - checkLimit() - 检查限制 + - _Requirements: 2.2, 2.5, 4.1, 4.2, 4.5, 4.6, 5.5_ + - [x] 12.2 实现本地许可证存储 + - 添加 Prisma 模型存储许可证数据 + - 实现加密存储和读取 + - _Requirements: 10.1, 10.2_ + - [x] 12.3 实现宽限期逻辑 + - 离线时使用缓存数据 + - 7 天宽限期计算 + - 宽限期过期进入只读模式 + - _Requirements: 4.5, 4.6, 4.7_ + +- [x] 13. 实现功能限制执行 + - [x] 13.1 实现用户数量限制检查 + - 在用户创建前检查限制 `app/api/auth/register/route.ts` + - 显示升级提示 + - _Requirements: 5.1_ + - [x] 13.2 实现素材数量限制检查 + - 在素材上传前检查限制 `app/api/materials/upload/route.ts` + - 显示升级提示 + - _Requirements: 5.2_ + - [x] 13.3 实现功能开关检查 + - 自定义模板功能检查 `app/api/admin/upload-templates/route.ts` + - 部门隔离功能检查 `app/api/departments/route.ts` + - 系统设置功能检查 `app/api/system-settings/route.ts` + - _Requirements: 5.3, 5.4_ + +- [x] 14. Checkpoint - 确保所有测试通过 + - 测试通过 ✓ + +## Phase 6: 客户端 UI 集成 + +- [x] 15. 实现激活页面 + - [x] 15.1 创建激活页面 `app/activate/page.tsx` + - 许可证密钥输入表单 + - 免费试用选项 + - 激活状态显示 + - 错误提示 + - _Requirements: 2.1, 2.6, 2.7, 2.8_ + - [x] 15.2 实现许可证中间件 `middleware.ts` + - 检查激活状态(通过 cookie) + - 未激活时重定向到激活页面 + - 激活成功后设置 cookie + - _Requirements: 2.1_ + +- [x] 16. 实现许可证信息展示 + - [x] 16.1 创建许可证信息组件 `components/LicenseInfo.tsx` + - 显示许可证类型和状态 + - 显示到期时间 + - 显示使用统计 + - 显示续期/升级链接 + - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_ + - [x] 16.2 集成到系统设置页面 + - 在设置页面添加许可证信息区域 + - 显示功能可用性状态 + - 支持暗色模式 + - _Requirements: 5.6, 8.1_ + +## Phase 7: 客户管理后台 + +- [x] 17. 实现管理后台认证 + - [x] 17.1 创建管理员登录页面 `license-server/app/admin/login/page.tsx` + - 登录表单 + - JWT Session 管理 + - _Requirements: 6.1_ + - [x] 17.2 实现认证 API + - POST /api/admin/auth/login - 登录 + - POST /api/admin/auth/logout - 登出 + - lib/auth.ts - 认证服务 + - _Requirements: 6.1_ + - [x] 17.3 创建初始化管理员脚本 + - scripts/init-admin.ts + - _Requirements: 6.1_ + +- [x] 18. 实现客户管理功能 + - [x] 18.1 创建客户列表页面 `license-server/app/admin/customers/page.tsx` + - 客户列表展示 + - 搜索和筛选 + - 添加客户弹窗 + - _Requirements: 6.3_ + - [x] 18.2 实现客户 CRUD API + - GET/POST /api/admin/customers + - _Requirements: 6.3_ + +- [x] 19. 实现许可证管理功能 + - [x] 19.1 实现许可证管理 API + - GET/POST /api/admin/licenses - 列表和生成 + - POST /api/admin/licenses/:id/disable - 禁用许可证 + - POST /api/admin/licenses/:id/reset - 重置硬件绑定 + - _Requirements: 6.4, 6.5, 6.6, 6.7_ + - [x] 19.2 创建许可证管理界面 `license-server/app/admin/licenses/page.tsx` + - 许可证列表 + - 生成新许可证表单 + - 许可证操作按钮 + - _Requirements: 6.4, 6.5, 6.6, 6.7_ + +- [x] 20. 实现仪表盘和监控 + - [x] 20.1 创建仪表盘页面 `license-server/app/admin/page.tsx` + - 客户统计(按许可证类型) + - 最近激活列表 + - 系统状态 + - _Requirements: 6.2, 6.8_ + - [x] 20.2 实现异常告警功能 `license-server/app/admin/alerts/page.tsx` + - 告警列表页面 + - 告警详情 + - 处理/忽略操作 + - _Requirements: 7.2, 7.3, 7.4_ + +- [x] 21. Checkpoint - 确保所有测试通过 + - 管理后台功能测试通过 ✓ + +## Phase 8: 代码混淆 + +- [x] 22. 配置代码混淆 + - [x] 22.1 创建混淆配置 `scripts/obfuscate-config.js` + - 配置混淆选项(控制流扁平化、字符串编码) + - 指定需要混淆的文件列表 + - 配置保留名称和字符串 + - _Requirements: 11.1, 11.2, 11.3, 11.6_ + - [x] 22.2 创建混淆构建脚本 `scripts/build-obfuscated.js` + - 构建后自动混淆敏感代码 + - 添加 `npm run build:prod` 命令 + - _Requirements: 11.4, 11.7_ + - [x] 22.3 安装依赖并验证 + - 安装 javascript-obfuscator ✓ + - 运行 `npm run build:prod` 混淆 74 个文件 ✓ + - 测试验证功能正确性 ✓ + - _Requirements: 11.5, 11.8_ + +## Phase 9: 自助安装 Agent + +- [x] 23. 设置 Agent 项目 + - [x] 23.1 初始化 Agent 项目 `flnxi-agent/` + - 创建 Node.js + Express + TypeScript 项目 + - 配置 dockerode、socket.io 依赖 + - 创建 Dockerfile(多架构支持) + - _Requirements: 12.1, 12.2_ + - [x] 23.2 创建前端安装向导和管理面板 + - Welcome.tsx - 欢迎页面 + - Config.tsx - 端口配置 + - Installing.tsx - 安装进度 + - Complete.tsx - 完成页面 + - Dashboard.tsx - 管理面板(系统状态、更新、重启) + - _Requirements: 12.3, 12.4, 12.8, 12.11, 12.12_ + +- [x] 24. 实现 Agent 后端功能 + - [x] 24.1 实现 Web 服务 `flnxi-agent/src/server.ts` + - Express 静态文件服务 + - Socket.io 实时进度推送 + - _Requirements: 12.3, 12.8_ + - [x] 24.2 实现 Docker 操作 `flnxi-agent/src/installer.ts` + - 拉取 FLNXI 素材系统镜像(带进度) + - 拉取依赖镜像(postgres、minio 等) + - 创建并启动容器 + - 许可证验证由素材系统首次启动时处理 + - _Requirements: 12.5, 12.6, 12.7_ + +- [x] 25. 实现安装和更新流程 + - [x] 25.1 实现安装 API `flnxi-agent/src/server.ts` + - POST /api/install - 开始安装 + - WebSocket 推送安装进度 + - 支持从 Harbor 拉取镜像 + - _Requirements: 12.4, 12.8_ + - [x] 25.2 实现系统更新 API `flnxi-agent/src/system-manager.ts` + - GET /api/status - 获取系统状态和版本 + - GET /api/check-update - 检查最新版本 + - POST /api/update - 更新系统(带回滚) + - _Requirements: 12.12, 12.13, 14.1, 14.2, 14.4_ + - [x] 25.3 实现强制更新检测 + - 比较当前版本与 minRequiredVersion + - Dashboard 显示更新提示 + - _Requirements: 14.3_ + - [x] 25.4 实现更新回滚机制 + - 更新前备份容器配置 + - 重命名旧容器而非删除 + - 健康检查失败时自动回滚 + - _Requirements: 15.1, 15.2, 15.3, 15.4_ + - [x] 25.5 实现错误处理 + - 安装/更新失败时显示错误详情 + - 提供重试选项 + - _Requirements: 12.11_ + +- [x] 26. 构建和发布 + - [x] 26.1 Agent 自更新功能 + - `flnxi-agent/src/system-manager.ts` - 添加 checkAgentUpdate, updateAgent 方法 + - `flnxi-agent/src/server.ts` - 添加 Agent 更新 API + - `flnxi-agent/frontend/src/pages/Dashboard.tsx` - 添加 Agent 更新 UI + - `license-server/app/api/version/agent/route.ts` - Agent 版本检查 API + - `license-server/prisma/schema.prisma` - 添加 AgentVersion 模型 + - _Requirements: 12.12, 12.13_ + - [x] 26.2 发布管理工具 + - `scripts/发布管理.ps1` - 统一发布管理脚本 + - 支持测试版部署、正式版发布、Agent 发布 + - 正式版操作需要二次确认 + - _Requirements: 12.1, 12.2_ + - [x] 26.3 测试安装流程 + - 本地测试 Agent 构建和启动 ✓ + - API 测试:/api/status 正常 ✓ + - 前端页面加载正常 ✓ + - 待实际 NAS 设备测试(飞牛 fnOS、群晖) + - _Requirements: 12.1-12.13_ + +## Phase 10: 部署和集成 + +- [x] 27. 实现版本管理 API + - [x] 27.1 版本信息数据模型(已在 Phase 1 完成) + - versions 表已在 prisma/schema.prisma 中定义 + - _Requirements: 13.1, 13.2_ + - [x] 27.2 版本检查 API(已在 Phase 4 完成) + - GET /api/version/check + - _Requirements: 13.4, 14.1_ + - [x] 27.3 实现版本管理后台 `license-server/app/admin/versions/page.tsx` + - 版本列表页面 + - 发布新版本表单 + - _Requirements: 13.3_ + +- [x] 28. 部署许可证服务器配置 + - [x] 28.1 创建 Docker Compose 配置 `license-server/docker-compose.yml` + - license-server 服务 + - PostgreSQL 数据库 + - _Requirements: 10.3_ + - [x] 28.2 创建 Dockerfile `license-server/Dockerfile` + - 多阶段构建 + - _Requirements: 10.3_ + - [x] 28.3 部署到 45.249.208.118 + - 配置环境变量(RSA 私钥、数据库连接) + - 启动服务(端口 9527,域名 lic.flnxi.com) + - 验证 API 可访问 ✓ + - 创建管理员账号(admin/admin123) + - _Requirements: 10.3_ + +- [x] 29. 集成到素材系统 + - [x] 29.1 更新素材系统 middleware.ts(已在 Phase 6 完成) + - 添加许可证检查逻辑 + - 配置激活页面路由 + - _Requirements: 2.1_ + - [x] 29.2 集成功能限制检查 + - 用户注册接口添加限制检查 `app/api/auth/register/route.ts` + - 素材上传接口添加限制检查 `app/api/materials/upload/route.ts` + - 创建限制检查工具 `lib/license/check-limits.ts` + - _Requirements: 5.1, 5.2, 5.3, 5.4_ + - [x] 29.3 配置心跳定时任务 `instrumentation.ts` + - 启动时验证许可证 + - 每小时发送心跳 + - _Requirements: 4.1, 4.2_ + - [x] 29.4 添加版本文件 + - 在 Dockerfile 中添加 VERSION.txt + - 构建时通过 ARG 写入版本号 + - _Requirements: 13.5_ + +- [x] 30. 同步 Runtime 镜像到 Harbor + - [x] 30.1 创建镜像同步脚本 `scripts/sync-harbor-images.ps1` + - postgres:16-alpine + - minio/minio:latest + - minio/mc:latest + - redis:7-alpine + - nginx:alpine + - node:20-alpine + - _Requirements: 12.7_ + - [x] 30.2 创建镜像构建脚本 `scripts/build-material-image.ps1` + - 支持 dev/test/release 三种环境 + - 测试版推送到 material-dev/ 目录 + - 正式版推送到 material/ 目录 + - _Requirements: 13.6_ + - [x] 30.3 创建镜像管理文档 `docs/harbor-image-management.md` + - 镜像目录结构说明 + - 工作流程指南 + - 版本管理规范 + - _Requirements: 13.6_ + - [x] 30.4 执行镜像同步(已完成) + - Runtime 镜像已同步到 Harbor + - _Requirements: 12.7_ + +- [x] 30. Final Checkpoint - 确保所有测试通过 + - 单元测试:56 passed + - API 集成测试:全部通过 + - 许可证服务器部署验证:✓ diff --git a/Dockerfile b/Dockerfile index 313ce17..52c9b33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -181,6 +181,10 @@ RUN if [ ! -f ".next/standalone/server.js" ]; then \ exit 1; \ fi +# 写入版本文件(用于许可证系统版本检查) +ARG APP_VERSION="1.0.0" +RUN echo "${APP_VERSION}" > VERSION.txt + # 为可选模块创建占位符(确保 COPY 不会失败) # 如果 canvas 未安装,创建一个空占位符目录 RUN if [ ! -d "node_modules/canvas" ]; then \ @@ -244,6 +248,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/logo ./public/logo # 3. Prisma schema 和迁移文件(运行时可能需要) COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma +# 4. 版本文件(许可证系统版本检查) +COPY --from=builder --chown=nextjs:nodejs /app/VERSION.txt ./VERSION.txt + # 4. lib 目录(包含 configLoader 等运行时模块) COPY --from=builder --chown=nextjs:nodejs /app/lib ./lib diff --git a/VERSION.txt b/VERSION.txt index 70b55a8..3eefcb9 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1,51 +1 @@ -FLNXI素材库管理系统 -版本历史 - -============================================ -Version 1.0.0 (2024-10-26) -============================================ - -初始版本发布 - -主要功能: -✅ 用户注册/登录系统 -✅ 3D素材上传(支持多种格式) -✅ 素材浏览和搜索 -✅ 多维度筛选(店铺、场景、月份) -✅ 素材下载(带进度显示) -✅ 点赞和分享功能 -✅ 管理员后台 -✅ 用户权限管理 -✅ 系统设置 -✅ 数据统计和排行榜 -✅ MinIO 对象存储集成 -✅ PostgreSQL 数据库 -✅ Docker 一键部署 - -技术栈: -- Next.js 14 -- React 18 -- TypeScript -- TailwindCSS -- PostgreSQL 15 -- MinIO -- Prisma ORM -- NextAuth.js - -部署方式: -- Docker Compose -- 完全私有化部署 -- 包含数据库和对象存储 -- 一键安装脚本 - -============================================ - -未来计划: -- [ ] 批量上传功能 -- [ ] 在线3D预览 -- [ ] 移动端适配 -- [ ] API接口开放 -- [ ] 更多素材类型支持 -- [ ] 协作功能增强 - - +1.0.0 diff --git a/app/activate/__tests__/page.test.tsx b/app/activate/__tests__/page.test.tsx new file mode 100644 index 0000000..7783c6e --- /dev/null +++ b/app/activate/__tests__/page.test.tsx @@ -0,0 +1,244 @@ +/** + * @jest-environment jsdom + */ +/** + * 激活页面测试 + * 测试许可证激活和免费试用功能 + */ +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; +import ActivatePage from '../page'; + +// Mock next/navigation +const mockPush = jest.fn(); +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockPush, + }), +})); + +// Mock fetch +global.fetch = jest.fn(); + +describe('ActivatePage', () => { + beforeEach(() => { + jest.clearAllMocks(); + (global.fetch as jest.Mock).mockReset(); + }); + + describe('渲染测试', () => { + it('应该正确渲染页面标题和表单', () => { + render(); + + expect(screen.getByText('FLNXI 素材系统')).toBeInTheDocument(); + expect(screen.getByText('请激活您的许可证')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX')).toBeInTheDocument(); + expect(screen.getByText('激活许可证')).toBeInTheDocument(); + expect(screen.getByText('免费试用')).toBeInTheDocument(); + }); + + it('应该显示免费版限制说明', () => { + render(); + expect(screen.getByText('免费版限制:10 个用户、100 个素材')).toBeInTheDocument(); + }); + + it('应该显示联系信息', () => { + render(); + expect(screen.getByText('需要购买许可证?请联系我们')).toBeInTheDocument(); + }); + }); + + describe('许可证密钥格式化', () => { + it('应该自动将输入转换为大写', () => { + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX') as HTMLInputElement; + + fireEvent.change(input, { target: { value: 'abcd' } }); + expect(input.value).toBe('ABCD'); + }); + + it('应该自动添加横杠分隔符', () => { + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX') as HTMLInputElement; + + fireEvent.change(input, { target: { value: 'abcd1234' } }); + expect(input.value).toBe('ABCD-1234'); + }); + + it('应该过滤非字母数字字符', () => { + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX') as HTMLInputElement; + + fireEvent.change(input, { target: { value: 'ab!@#cd12' } }); + expect(input.value).toBe('ABCD-12'); + }); + + it('应该限制最大长度为19个字符(含横杠)', () => { + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX') as HTMLInputElement; + + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678EXTRA' } }); + expect(input.value).toBe('ABCD-1234-EFGH-5678'); + }); + }); + + describe('激活按钮状态', () => { + it('密钥不完整时激活按钮应该禁用', () => { + render(); + const button = screen.getByText('激活许可证'); + expect(button).toBeDisabled(); + }); + + it('密钥完整时激活按钮应该启用', () => { + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX'); + + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678' } }); + + const button = screen.getByText('激活许可证'); + expect(button).not.toBeDisabled(); + }); + }); + + describe('许可证激活', () => { + it('激活成功后应该跳转到首页', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + json: () => Promise.resolve({ success: true }), + }); + + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX'); + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678' } }); + + const button = screen.getByText('激活许可证'); + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/'); + }); + }); + + it('激活失败应该显示错误信息', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + json: () => Promise.resolve({ success: false, error: '许可证无效' }), + }); + + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX'); + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678' } }); + + const button = screen.getByText('激活许可证'); + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(screen.getByText('许可证无效')).toBeInTheDocument(); + }); + }); + + it('网络错误应该显示网络错误提示', async () => { + (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); + + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX'); + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678' } }); + + const button = screen.getByText('激活许可证'); + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(screen.getByText('网络错误,请检查网络连接')).toBeInTheDocument(); + }); + }); + + it('激活时应该显示加载状态', async () => { + let resolvePromise: (value: unknown) => void; + (global.fetch as jest.Mock).mockImplementation(() => + new Promise(resolve => { resolvePromise = resolve; }) + ); + + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX'); + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678' } }); + + const button = screen.getByText('激活许可证'); + await act(async () => { + fireEvent.click(button); + }); + + expect(screen.getByText('激活中...')).toBeInTheDocument(); + + // 清理 + await act(async () => { + resolvePromise!({ json: () => Promise.resolve({ success: true }) }); + }); + }); + }); + + describe('免费试用', () => { + it('免费试用成功后应该跳转到首页', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + json: () => Promise.resolve({ success: true }), + }); + + render(); + const button = screen.getByText('免费试用'); + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith('/'); + }); + + expect(global.fetch).toHaveBeenCalledWith('/api/license/activate-free', { + method: 'POST', + }); + }); + + it('免费试用失败应该显示错误信息', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + json: () => Promise.resolve({ success: false, error: '试用已过期' }), + }); + + render(); + const button = screen.getByText('免费试用'); + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(screen.getByText('试用已过期')).toBeInTheDocument(); + }); + }); + }); + + describe('API 调用', () => { + it('激活时应该发送正确的请求', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + json: () => Promise.resolve({ success: true }), + }); + + render(); + const input = screen.getByPlaceholderText('XXXX-XXXX-XXXX-XXXX'); + fireEvent.change(input, { target: { value: 'ABCD1234EFGH5678' } }); + + const button = screen.getByText('激活许可证'); + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('/api/license/activate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ licenseKey: 'ABCD-1234-EFGH-5678' }), + }); + }); + }); + }); +}); diff --git a/app/activate/page.tsx b/app/activate/page.tsx new file mode 100644 index 0000000..1573f89 --- /dev/null +++ b/app/activate/page.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function ActivatePage() { + const router = useRouter(); + const [licenseKey, setLicenseKey] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + // 格式化许可证密钥输入(自动添加横杠) + const formatLicenseKey = (value: string) => { + const cleaned = value.toUpperCase().replace(/[^A-Z0-9]/g, ''); + const parts = []; + for (let i = 0; i < cleaned.length && i < 16; i += 4) { + parts.push(cleaned.slice(i, i + 4)); + } + return parts.join('-'); + }; + + const handleKeyChange = (e: React.ChangeEvent) => { + setLicenseKey(formatLicenseKey(e.target.value)); + setError(''); + }; + + // 激活许可证 + const handleActivate = async () => { + if (!licenseKey || licenseKey.length !== 19) { + setError('请输入完整的许可证密钥'); + return; + } + + setLoading(true); + setError(''); + + try { + const res = await fetch('/api/license/activate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ licenseKey }), + }); + + const data = await res.json(); + + if (data.success) { + router.push('/'); + } else { + setError(data.error || '激活失败,请检查许可证密钥'); + } + } catch { + setError('网络错误,请检查网络连接'); + } finally { + setLoading(false); + } + }; + + // 免费试用 + const handleFreeTrial = async () => { + setLoading(true); + setError(''); + + try { + const res = await fetch('/api/license/activate-free', { + method: 'POST', + }); + + const data = await res.json(); + + if (data.success) { + router.push('/'); + } else { + setError(data.error || '激活失败'); + } + } catch { + setError('网络错误,请检查网络连接'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ {/* Logo */} +
+

FLNXI 素材系统

+

请激活您的许可证

+
+ + {/* 许可证输入 */} +
+ + +
+ + {/* 错误提示 */} + {error && ( +
+ {error} +
+ )} + + {/* 激活按钮 */} + + + {/* 分隔线 */} +
+
+ 或 +
+
+ + {/* 免费试用 */} + + + {/* 免费版说明 */} +

+ 免费版限制:10 个用户、100 个素材 +

+
+ + {/* 联系信息 */} +

+ 需要购买许可证?请联系我们 +

+
+
+ ); +} diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx index c546400..bf5f045 100644 --- a/app/admin/settings/page.tsx +++ b/app/admin/settings/page.tsx @@ -5,6 +5,7 @@ import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import Header from '@/components/Header' import AppearanceSettingsPanel from '../_components/AppearanceSettingsPanel' +import LicenseInfo from '@/components/LicenseInfo' export default function SystemSettingsPage() { const { data: session, status } = useSession() @@ -22,7 +23,7 @@ export default function SystemSettingsPage() { if (status === 'loading') { return (
-
正在加载外观设置...
+
正在加载设置...
) } @@ -35,6 +36,7 @@ export default function SystemSettingsPage() {
+
diff --git a/app/api/admin/upload-templates/route.ts b/app/api/admin/upload-templates/route.ts index 33f2a2c..8dde280 100644 --- a/app/api/admin/upload-templates/route.ts +++ b/app/api/admin/upload-templates/route.ts @@ -4,6 +4,7 @@ import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' import { Prisma } from '@prisma/client' import { syncFieldPoolFromTemplates } from '@/lib/field-pool' +import { canUseCustomTemplates } from '@/lib/license' const baseSelect = { id: true, @@ -96,6 +97,14 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: '无权限' }, { status: 403 }) } + // 检查自定义模板功能是否可用 + if (!canUseCustomTemplates()) { + return NextResponse.json({ + error: '自定义模板功能需要标准版或专业版许可证,请升级后使用', + errorCode: 'LICENSE_REQUIRED', + }, { status: 403 }) + } + body = await req.json() const { name, diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 8e63ca3..9842aa3 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,9 +1,19 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' import bcrypt from 'bcryptjs' +import { canCreateUser } from '@/lib/license' export async function POST(req: NextRequest) { try { + // 检查许可证用户数量限制 + const userLimit = await canCreateUser(); + if (!userLimit.allowed) { + return NextResponse.json( + { error: userLimit.message || '已达到用户数量上限' }, + { status: 403 } + ) + } + // 检查注册功能是否开启 const settings = await prisma.systemSettings.findFirst() if (settings && !settings.enableRegistration) { diff --git a/app/api/departments/route.ts b/app/api/departments/route.ts index 28adc57..78895d3 100644 --- a/app/api/departments/route.ts +++ b/app/api/departments/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { prisma } from '@/lib/prisma' +import { canUseDepartmentIsolation } from '@/lib/license' // Get all departments export async function GET() { @@ -34,6 +35,14 @@ export async function POST(req: NextRequest) { ) } + // 检查部门隔离功能是否可用 + if (!canUseDepartmentIsolation()) { + return NextResponse.json( + { error: '部门隔离功能需要专业版许可证,请升级后使用' }, + { status: 403 } + ) + } + const { name } = await req.json() if (!name || name.trim() === '') { diff --git a/app/api/license/activate-free/route.ts b/app/api/license/activate-free/route.ts new file mode 100644 index 0000000..57a99b5 --- /dev/null +++ b/app/api/license/activate-free/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from 'next/server'; +import { activateFreeTrial } from '@/lib/license'; + +export async function POST() { + try { + const result = await activateFreeTrial(); + + if (result.success) { + const response = NextResponse.json({ success: true }); + + // 设置许可证激活 cookie(用于 middleware 快速检查) + response.cookies.set('license_activated', 'true', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 365 * 24 * 60 * 60, // 1年 + path: '/', + }); + + return response; + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ); + } + } catch (error) { + console.error('免费试用激活失败:', error); + return NextResponse.json( + { success: false, error: '服务器错误' }, + { status: 500 } + ); + } +} diff --git a/app/api/license/activate/__tests__/route.test.ts b/app/api/license/activate/__tests__/route.test.ts new file mode 100644 index 0000000..1766147 --- /dev/null +++ b/app/api/license/activate/__tests__/route.test.ts @@ -0,0 +1,191 @@ +/** + * 许可证激活 API 路由测试 + * 测试 app/api/license/activate/route.ts + */ +import { NextRequest } from 'next/server'; +import { POST } from '../route'; + +// Mock lib/license 模块 +jest.mock('@/lib/license', () => ({ + activate: jest.fn(), +})); + +import { activate } from '@/lib/license'; + +const mockActivate = activate as jest.MockedFunction; + +describe('POST /api/license/activate', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('请求验证', () => { + it('缺少 licenseKey 时应返回 400 错误', async () => { + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({}), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe('请输入许可证密钥'); + }); + + it('licenseKey 为空字符串时应返回 400 错误', async () => { + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: '' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + }); + }); + + describe('激活成功', () => { + it('激活成功时应返回 success: true', async () => { + mockActivate.mockResolvedValueOnce({ success: true }); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'ABCD-1234-EFGH-5678' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(200); + expect(data.success).toBe(true); + expect(mockActivate).toHaveBeenCalledWith('ABCD-1234-EFGH-5678'); + }); + + it('激活成功时应设置 license_activated cookie', async () => { + mockActivate.mockResolvedValueOnce({ success: true }); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'ABCD-1234-EFGH-5678' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const setCookieHeader = response.headers.get('set-cookie'); + + expect(setCookieHeader).toBeTruthy(); + expect(setCookieHeader).toContain('license_activated=true'); + expect(setCookieHeader).toContain('HttpOnly'); + expect(setCookieHeader).toContain('Path=/'); + // SameSite 值可能是 Lax 或 lax(大小写不敏感) + expect(setCookieHeader?.toLowerCase()).toContain('samesite=lax'); + }); + + it('cookie 应设置 1 年有效期', async () => { + mockActivate.mockResolvedValueOnce({ success: true }); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'ABCD-1234-EFGH-5678' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const setCookieHeader = response.headers.get('set-cookie'); + + // 1年 = 365 * 24 * 60 * 60 = 31536000 秒 + expect(setCookieHeader).toContain('Max-Age=31536000'); + }); + }); + + describe('激活失败', () => { + it('许可证无效时应返回 400 错误', async () => { + mockActivate.mockResolvedValueOnce({ success: false, error: '许可证无效' }); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'INVALID-KEY-1234' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe('许可证无效'); + }); + + it('许可证已被使用时应返回相应错误', async () => { + mockActivate.mockResolvedValueOnce({ success: false, error: '许可证已被其他设备激活' }); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'USED-KEY-1234-5678' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(400); + expect(data.success).toBe(false); + expect(data.error).toBe('许可证已被其他设备激活'); + }); + + it('激活失败时不应设置 cookie', async () => { + mockActivate.mockResolvedValueOnce({ success: false, error: '许可证无效' }); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'INVALID-KEY-1234' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const setCookieHeader = response.headers.get('set-cookie'); + + expect(setCookieHeader).toBeNull(); + }); + }); + + describe('服务器错误处理', () => { + it('activate 函数抛出异常时应返回 500 错误', async () => { + mockActivate.mockRejectedValueOnce(new Error('数据库连接失败')); + + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: JSON.stringify({ licenseKey: 'ABCD-1234-EFGH-5678' }), + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.success).toBe(false); + expect(data.error).toBe('服务器错误'); + }); + + it('JSON 解析错误时应返回 500 错误', async () => { + const request = new NextRequest('http://localhost/api/license/activate', { + method: 'POST', + body: 'invalid json', + headers: { 'Content-Type': 'application/json' }, + }); + + const response = await POST(request); + const data = await response.json(); + + expect(response.status).toBe(500); + expect(data.success).toBe(false); + }); + }); +}); diff --git a/app/api/license/activate/route.ts b/app/api/license/activate/route.ts new file mode 100644 index 0000000..b6121a4 --- /dev/null +++ b/app/api/license/activate/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { activate } from '@/lib/license'; + +export async function POST(request: NextRequest) { + try { + const { licenseKey } = await request.json(); + + if (!licenseKey) { + return NextResponse.json( + { success: false, error: '请输入许可证密钥' }, + { status: 400 } + ); + } + + const result = await activate(licenseKey); + + if (result.success) { + const response = NextResponse.json({ success: true }); + + // 设置许可证激活 cookie(用于 middleware 快速检查) + response.cookies.set('license_activated', 'true', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 365 * 24 * 60 * 60, // 1年 + path: '/', + }); + + return response; + } else { + return NextResponse.json( + { success: false, error: result.error }, + { status: 400 } + ); + } + } catch (error) { + console.error('激活失败:', error); + return NextResponse.json( + { success: false, error: '服务器错误' }, + { status: 500 } + ); + } +} diff --git a/app/api/license/info/route.ts b/app/api/license/info/route.ts new file mode 100644 index 0000000..031c5e4 --- /dev/null +++ b/app/api/license/info/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getLicense, getLicenseType } from '@/lib/license'; + +export async function GET() { + try { + const license = getLicense(); + + if (!license) { + return NextResponse.json( + { error: '未激活' }, + { status: 401 } + ); + } + + // 获取使用统计 + const [userCount, materialCount] = await Promise.all([ + prisma.user.count(), + prisma.material.count(), + ]); + + // 获取本地许可证数据 + const localLicense = await prisma.systemConfig.findUnique({ + where: { key: 'license' }, + }); + + let lastVerified = new Date().toISOString(); + if (localLicense) { + try { + const data = JSON.parse(localLicense.value); + lastVerified = data.lastVerified || lastVerified; + } catch { + // ignore + } + } + + // 判断状态 + let status: 'active' | 'expired' | 'grace' = 'active'; + if (license.expiresAt && new Date(license.expiresAt) < new Date()) { + status = 'expired'; + } + + return NextResponse.json({ + type: license.type, + status, + expiresAt: license.expiresAt, + usage: { + users: { + current: userCount, + max: license.limits.maxUsers, + }, + materials: { + current: materialCount, + max: license.limits.maxMaterials, + }, + }, + features: license.features, + lastVerified, + }); + } catch (error) { + console.error('获取许可证信息失败:', error); + return NextResponse.json( + { error: '服务器错误' }, + { status: 500 } + ); + } +} diff --git a/app/api/materials/upload/route.ts b/app/api/materials/upload/route.ts index 195130b..a3bdb01 100644 --- a/app/api/materials/upload/route.ts +++ b/app/api/materials/upload/route.ts @@ -7,6 +7,7 @@ import { uploadToS3 } from '@/lib/s3' import { processImage, getProcessedFileName, sliceImage } from '@/lib/image-processor' import { parseAudioMetadata, isAudioFile } from '@/lib/audio-processor' import { validateTemplateId } from '@/lib/template-validator' +import { canUploadMaterial } from '@/lib/license' import path from 'path' import { randomBytes } from 'crypto' @@ -78,6 +79,16 @@ export async function POST(req: NextRequest) { ) } + // 检查许可证素材数量限制 + const materialLimit = await canUploadMaterial(); + if (!materialLimit.allowed) { + console.log('[Upload] Material limit reached:', materialLimit) + return NextResponse.json( + { error: materialLimit.message || '已达到素材数量上限' }, + { status: 403 } + ) + } + console.log('[Upload] Parsing form data...') const formData = await req.formData() const normalizeInput = (value: FormDataEntryValue | null) => { diff --git a/app/api/system-settings/route.ts b/app/api/system-settings/route.ts index 5f9bace..fbb2905 100644 --- a/app/api/system-settings/route.ts +++ b/app/api/system-settings/route.ts @@ -5,6 +5,7 @@ import { prisma } from '@/lib/prisma' import { defaultLogos, featureFlags } from '@/lib/config' import { cache } from '@/lib/cache' import { ensureSystemSettingsOptionalColumns } from '@/lib/system-settings-columns' +import { canUseSystemSettings } from '@/lib/license' const CACHE_KEY = 'system-settings' const CACHE_TTL = 60000 // 1分钟缓存 @@ -188,6 +189,14 @@ export async function PUT(request: NextRequest) { ) } + // 检查系统设置功能是否可用(免费版只能查看,不能修改) + if (!canUseSystemSettings()) { + return NextResponse.json( + { error: '系统设置功能需要标准版或专业版许可证,请升级后使用' }, + { status: 403 } + ) + } + const body = await request.json() const { headerBgColor, diff --git a/components/LicenseInfo.tsx b/components/LicenseInfo.tsx new file mode 100644 index 0000000..6bd53f5 --- /dev/null +++ b/components/LicenseInfo.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +interface LicenseData { + type: 'FREE' | 'STANDARD' | 'PRO'; + status: 'active' | 'expired' | 'grace'; + expiresAt: string | null; + usage: { + users: { current: number; max: number }; + materials: { current: number; max: number }; + }; + features: { + customTemplates: boolean; + departmentIsolation: boolean; + intranetPenetration: boolean; + }; + lastVerified: string; +} + +const LICENSE_TYPE_NAMES = { + FREE: '免费版', + STANDARD: '标准版', + PRO: '专业版', +}; + +const STATUS_NAMES = { + active: '正常', + expired: '已过期', + grace: '宽限期', +}; + +export default function LicenseInfo() { + const [license, setLicense] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchLicenseInfo(); + }, []); + + const fetchLicenseInfo = async () => { + try { + const res = await fetch('/api/license/info'); + if (res.ok) { + const data = await res.json(); + setLicense(data); + } + } catch (error) { + console.error('获取许可证信息失败:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + if (!license) { + return ( +
+

无法获取许可证信息

+
+ ); + } + + const isExpiringSoon = + license.expiresAt && + new Date(license.expiresAt).getTime() - Date.now() < 30 * 24 * 60 * 60 * 1000; + + return ( +
+ {/* 头部 */} +
+
+
+

许可证信息

+

+ 最后验证: {new Date(license.lastVerified).toLocaleString()} +

+
+
+ + {LICENSE_TYPE_NAMES[license.type]} + + + {STATUS_NAMES[license.status]} + +
+
+
+ + {/* 到期提醒 */} + {isExpiringSoon && license.expiresAt && ( +
+

+ ⚠️ 许可证将于 {new Date(license.expiresAt).toLocaleDateString()} 到期,请及时续期 +

+
+ )} + + {/* 使用统计 */} +
+

使用情况

+
+ {/* 用户数 */} +
+
+ 用户数 + + {license.usage.users.current} / {license.usage.users.max === -1 ? '无限' : license.usage.users.max} + +
+ {license.usage.users.max !== -1 && ( +
+
0.9 + ? 'bg-red-500' + : 'bg-blue-500' + }`} + style={{ + width: `${Math.min(100, (license.usage.users.current / license.usage.users.max) * 100)}%`, + }} + /> +
+ )} +
+ + {/* 素材数 */} +
+
+ 素材数 + + {license.usage.materials.current} / {license.usage.materials.max === -1 ? '无限' : license.usage.materials.max} + +
+ {license.usage.materials.max !== -1 && ( +
+
0.9 + ? 'bg-red-500' + : 'bg-blue-500' + }`} + style={{ + width: `${Math.min(100, (license.usage.materials.current / license.usage.materials.max) * 100)}%`, + }} + /> +
+ )} +
+
+
+ + {/* 功能列表 */} +
+

功能权限

+
+ + + +
+
+ + {/* 升级按钮 */} + {license.type !== 'PRO' && ( +
+ +
+ )} +
+ ); +} + +function FeatureItem({ label, enabled }: { label: string; enabled: boolean }) { + return ( +
+ {label} + {enabled ? ( + ✓ + ) : ( + ✗ + )} +
+ ); +} diff --git a/components/__tests__/RankingPanel.test.tsx b/components/__tests__/RankingPanel.test.tsx index 9639e4a..a7d0887 100644 --- a/components/__tests__/RankingPanel.test.tsx +++ b/components/__tests__/RankingPanel.test.tsx @@ -267,15 +267,19 @@ describe('RankingPanel 组件', () => { localStorageMock.setItem('rankingPanelClosed', 'true') // 不应该抛出错误 - expect(() => render()).not.toThrow() + await act(async () => { + expect(() => render()).not.toThrow() + }) }) it('应该处理空的 localStorage', async () => { - render() - - await waitFor(() => { - expect(screen.getByText('贡献榜')).toBeInTheDocument() + await act(async () => { + render() }) + + // 使用 getAllByText 因为有多个元素包含相同文本 + const elements = screen.getAllByText('贡献榜') + expect(elements.length).toBeGreaterThan(0) }) it('应该将位置限制在视口内', async () => { @@ -287,7 +291,9 @@ describe('RankingPanel 组件', () => { })) localStorageMock.setItem('rankingPanelClosed', 'true') - render() + await act(async () => { + render() + }) const button = document.querySelector('button[title="显示排行榜(可拖动)"]') expect(button).toBeInTheDocument() @@ -303,6 +309,7 @@ describe('RankingPanel 位置恢复逻辑', () => { beforeEach(() => { jest.clearAllMocks() localStorageMock.clear() + jest.useFakeTimers() mockFetch.mockResolvedValue({ ok: true, @@ -314,6 +321,10 @@ describe('RankingPanel 位置恢复逻辑', () => { }) }) + afterEach(() => { + jest.useRealTimers() + }) + it('centered: true 时按钮应该垂直居中', async () => { localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({ x: 32, @@ -322,7 +333,9 @@ describe('RankingPanel 位置恢复逻辑', () => { })) localStorageMock.setItem('rankingPanelClosed', 'true') - render() + await act(async () => { + render() + }) const button = document.querySelector('button[title="显示排行榜(可拖动)"]') expect(button).toHaveStyle({ @@ -340,7 +353,9 @@ describe('RankingPanel 位置恢复逻辑', () => { })) localStorageMock.setItem('rankingPanelClosed', 'true') - render() + await act(async () => { + render() + }) const button = document.querySelector('button[title="显示排行榜(可拖动)"]') expect(button).toHaveStyle({ diff --git a/docs/harbor-image-management.md b/docs/harbor-image-management.md new file mode 100644 index 0000000..8eb93db --- /dev/null +++ b/docs/harbor-image-management.md @@ -0,0 +1,133 @@ +# Harbor 镜像管理指南 + +## 镜像目录结构 + +``` +harbor.flnxi.com/flnxi/ +├── material/ # 正式版镜像(客户拉取) +│ ├── flnxi-material:latest # 最新正式版 +│ ├── flnxi-material:1.0.0 # 版本标签 +│ └── flnxi-material:1.0.1 +│ +├── material-dev/ # 测试版镜像(开发测试用) +│ ├── flnxi-material:latest # 最新测试版 +│ ├── flnxi-material:dev-xxx # 开发分支 +│ └── flnxi-material:test-xxx # 测试版本 +│ +└── runtime/ # 依赖镜像 + ├── postgres:16-alpine + ├── minio:latest + ├── mc:latest + ├── redis:7-alpine + ├── nginx:alpine + └── node:20-alpine +``` + +## 工作流程 + +### 1. 开发阶段 + +构建开发版镜像,推送到 `material-dev/` 目录: + +```powershell +# 构建并推送开发版 +.\scripts\build-material-image.ps1 -Environment dev -Push + +# 镜像会被推送到: +# harbor.flnxi.com/flnxi/material-dev/flnxi-material:dev-20241215-1430 +# harbor.flnxi.com/flnxi/material-dev/flnxi-material:latest +``` + +### 2. 测试阶段 + +构建测试版镜像,推送到 `material-dev/` 目录: + +```powershell +# 构建并推送测试版 +.\scripts\build-material-image.ps1 -Environment test -Push + +# 镜像会被推送到: +# harbor.flnxi.com/flnxi/material-dev/flnxi-material:test-20241215 +# harbor.flnxi.com/flnxi/material-dev/flnxi-material:latest +``` + +### 3. 发布正式版 + +测试通过后,构建正式版镜像,推送到 `material/` 目录: + +```powershell +# 更新版本号 +echo "1.0.1" > VERSION.txt + +# 构建并推送正式版 +.\scripts\build-material-image.ps1 -Environment release -Push + +# 镜像会被推送到: +# harbor.flnxi.com/flnxi/material/flnxi-material:1.0.1 +# harbor.flnxi.com/flnxi/material/flnxi-material:latest +``` + +### 4. 同步 Runtime 镜像 + +首次部署或更新依赖版本时,同步 Runtime 镜像: + +```powershell +# 预览模式(不实际执行) +.\scripts\sync-harbor-images.ps1 -DryRun + +# 执行同步 +.\scripts\sync-harbor-images.ps1 +``` + +## 客户拉取 + +客户通过 Agent 安装时,会自动从以下地址拉取镜像: + +- 素材系统:`harbor.flnxi.com/flnxi/material/flnxi-material:latest` +- PostgreSQL:`harbor.flnxi.com/flnxi/runtime/postgres:16-alpine` +- MinIO:`harbor.flnxi.com/flnxi/runtime/minio:latest` +- Redis:`harbor.flnxi.com/flnxi/runtime/redis:7-alpine` + +如果 Harbor 不可用,会自动回退到 Docker Hub 官方镜像。 + +## 版本管理 + +### 版本号规范 + +- 正式版:`x.y.z`(如 `1.0.0`、`1.0.1`、`1.1.0`) +- 测试版:`test-yyyyMMdd`(如 `test-20241215`) +- 开发版:`dev-yyyyMMdd-HHmm`(如 `dev-20241215-1430`) + +### 版本文件 + +项目根目录的 `VERSION.txt` 文件记录当前正式版本号: + +``` +1.0.1 +``` + +构建正式版时会自动读取此文件。 + +## 镜像保留策略 + +建议在 Harbor 中配置以下保留策略: + +- `material/`:保留最近 10 个版本 +- `material-dev/`:保留最近 5 个版本 +- `runtime/`:保留所有版本(依赖镜像较少) + +## 常用命令 + +```powershell +# 登录 Harbor +docker login harbor.flnxi.com + +# 查看本地镜像 +docker images | grep flnxi + +# 删除本地测试镜像 +docker rmi $(docker images -q harbor.flnxi.com/flnxi/material-dev/*) + +# 拉取最新正式版 +docker pull harbor.flnxi.com/flnxi/material/flnxi-material:latest +``` diff --git a/flnxi-agent/.gitignore b/flnxi-agent/.gitignore new file mode 100644 index 0000000..3abe625 --- /dev/null +++ b/flnxi-agent/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.env +*.log +frontend/node_modules/ +frontend/dist/ diff --git a/flnxi-agent/Dockerfile b/flnxi-agent/Dockerfile new file mode 100644 index 0000000..adffb63 --- /dev/null +++ b/flnxi-agent/Dockerfile @@ -0,0 +1,42 @@ +# 多阶段构建 - 前端 +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# 多阶段构建 - 后端 +FROM node:20-alpine AS backend-builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY tsconfig.json ./ +COPY src/ ./src/ +# 只编译 TypeScript,不运行 build:frontend(前端已单独构建) +RUN npx tsc + +# 最终镜像 +FROM node:20-alpine +WORKDIR /app + +# 安装 docker cli(用于与宿主机 docker 通信) +RUN apk add --no-cache docker-cli + +# 复制构建产物 +COPY --from=backend-builder /app/dist ./dist +COPY --from=backend-builder /app/node_modules ./node_modules +COPY --from=frontend-builder /app/frontend/dist ./dist/public +COPY package.json ./ + +# 复制版本文件 +ARG AGENT_VERSION=1.0.0 +RUN echo "${AGENT_VERSION}" > /app/VERSION.txt + +# 环境变量 +ENV NODE_ENV=production +ENV PORT=3080 + +EXPOSE 3080 + +CMD ["node", "dist/server.js"] diff --git a/flnxi-agent/README.md b/flnxi-agent/README.md new file mode 100644 index 0000000..fa75863 --- /dev/null +++ b/flnxi-agent/README.md @@ -0,0 +1,66 @@ +# FLNXI Agent + +FLNXI 素材系统安装助手,用于在 NAS(飞牛 fnOS、群晖等)上一键安装素材系统。 + +## 功能 + +- 🚀 一键安装素材系统及所有依赖 +- 📊 实时安装进度显示 +- 🔄 系统更新和回滚 +- 📈 系统状态监控 +- 📝 日志查看 + +## 使用方法 + +### Docker 安装(推荐) + +```bash +docker run -d \ + --name flnxi-agent \ + -p 3080:3080 \ + -v /var/run/docker.sock:/var/run/docker.sock \ + flnxi/flnxi-agent:latest +``` + +然后访问 `http://your-nas-ip:3080` 开始安装。 + +### 本地开发 + +```bash +# 安装依赖 +npm install +cd frontend && npm install && cd .. + +# 开发模式 +npm run dev + +# 构建 +npm run build +``` + +## 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| PORT | Agent 服务端口 | 3080 | +| LICENSE_SERVER_URL | 许可证服务器地址 | http://45.249.208.118:3001 | + +## 架构 + +``` +flnxi-agent/ +├── src/ # 后端代码 +│ ├── server.ts # Express 服务 +│ ├── installer.ts # 安装逻辑 +│ └── system-manager.ts # 系统管理 +├── frontend/ # 前端代码 (React + Vite) +│ └── src/ +│ ├── App.tsx +│ └── pages/ +│ ├── Welcome.tsx +│ ├── Config.tsx +│ ├── Installing.tsx +│ ├── Complete.tsx +│ └── Dashboard.tsx +└── Dockerfile # 多架构构建 +``` diff --git a/flnxi-agent/VERSION.txt b/flnxi-agent/VERSION.txt new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/flnxi-agent/VERSION.txt @@ -0,0 +1 @@ +1.0.0 diff --git a/flnxi-agent/frontend/index.html b/flnxi-agent/frontend/index.html new file mode 100644 index 0000000..2d5454f --- /dev/null +++ b/flnxi-agent/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + FLNXI 素材系统安装助手 + + +
+ + + diff --git a/flnxi-agent/frontend/package-lock.json b/flnxi-agent/frontend/package-lock.json new file mode 100644 index 0000000..6a28e72 --- /dev/null +++ b/flnxi-agent/frontend/package-lock.json @@ -0,0 +1,2762 @@ +{ + "name": "flnxi-agent-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flnxi-agent-frontend", + "version": "1.0.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "socket.io-client": "^4.7.2" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.2", + "vite": "^5.0.8" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.7", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", + "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/flnxi-agent/frontend/package.json b/flnxi-agent/frontend/package.json new file mode 100644 index 0000000..c5347ed --- /dev/null +++ b/flnxi-agent/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "flnxi-agent-frontend", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "socket.io-client": "^4.7.2" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.3.6", + "typescript": "^5.3.2", + "vite": "^5.0.8" + } +} diff --git a/flnxi-agent/frontend/postcss.config.js b/flnxi-agent/frontend/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/flnxi-agent/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/flnxi-agent/frontend/src/App.tsx b/flnxi-agent/frontend/src/App.tsx new file mode 100644 index 0000000..1ef2849 --- /dev/null +++ b/flnxi-agent/frontend/src/App.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from 'react'; +import { io, Socket } from 'socket.io-client'; +import Welcome from './pages/Welcome'; +import Config from './pages/Config'; +import Installing from './pages/Installing'; +import Complete from './pages/Complete'; +import Dashboard from './pages/Dashboard'; + +type Step = 'welcome' | 'config' | 'installing' | 'complete' | 'dashboard'; + +interface InstallProgress { + step: string; + progress: number; + message: string; + detail?: string; +} + +export default function App() { + const [step, setStep] = useState('welcome'); + const [socket, setSocket] = useState(null); + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); + const [config, setConfig] = useState({ + webPort: 3000, + dataPath: '/volume1/docker/flnxi', + }); + + useEffect(() => { + // 检查是否已安装 + fetch('/api/installed') + .then((res) => res.json()) + .then((data) => { + if (data.installed) { + setStep('dashboard'); + } + }) + .catch(() => {}); + + // 建立 WebSocket 连接 + const s = io(); + setSocket(s); + + s.on('install:progress', (data: InstallProgress) => { + setProgress(data); + }); + + s.on('install:complete', () => { + setStep('complete'); + }); + + s.on('install:error', (data: { error: string }) => { + setError(data.error); + }); + + return () => { + s.disconnect(); + }; + }, []); + + const handleStartInstall = async () => { + setStep('installing'); + setError(null); + + try { + const res = await fetch('/api/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ config }), + }); + + if (!res.ok) { + const data = await res.json(); + setError(data.error || '安装失败'); + } + } catch { + setError('网络错误'); + } + }; + + return ( +
+ {step === 'welcome' && setStep('config')} />} + {step === 'config' && ( + setStep('welcome')} + onNext={handleStartInstall} + /> + )} + {step === 'installing' && ( + + )} + {step === 'complete' && ( + setStep('dashboard')} /> + )} + {step === 'dashboard' && } +
+ ); +} diff --git a/flnxi-agent/frontend/src/index.css b/flnxi-agent/frontend/src/index.css new file mode 100644 index 0000000..d7caaf2 --- /dev/null +++ b/flnxi-agent/frontend/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} diff --git a/flnxi-agent/frontend/src/main.tsx b/flnxi-agent/frontend/src/main.tsx new file mode 100644 index 0000000..2339d59 --- /dev/null +++ b/flnxi-agent/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/flnxi-agent/frontend/src/pages/Complete.tsx b/flnxi-agent/frontend/src/pages/Complete.tsx new file mode 100644 index 0000000..e69c1c8 --- /dev/null +++ b/flnxi-agent/frontend/src/pages/Complete.tsx @@ -0,0 +1,43 @@ +interface Props { + port: number; + onDashboard: () => void; +} + +export default function Complete({ port, onDashboard }: Props) { + const url = `http://${window.location.hostname}:${port}`; + + return ( +
+
+
+ + + +
+ +

安装完成!

+

FLNXI 素材系统已成功安装。

+ +
+

访问地址:

+ + {url} + +
+ +

+ 首次访问时需要激活许可证或开始免费试用。 +

+ +
+ + + 打开素材系统 + +
+
+
+ ); +} diff --git a/flnxi-agent/frontend/src/pages/Config.tsx b/flnxi-agent/frontend/src/pages/Config.tsx new file mode 100644 index 0000000..4b3b188 --- /dev/null +++ b/flnxi-agent/frontend/src/pages/Config.tsx @@ -0,0 +1,75 @@ +interface Props { + config: { + webPort: number; + dataPath: string; + }; + onChange: (config: { webPort: number; dataPath: string }) => void; + onBack: () => void; + onNext: () => void; +} + +export default function Config({ config, onChange, onBack, onNext }: Props) { + return ( +
+
+

配置安装选项

+ +
+
+ + onChange({ ...config, webPort: parseInt(e.target.value) || 3000 })} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + min={1} + max={65535} + /> +

+ 安装完成后通过此端口访问素材系统 +

+
+ +
+ + onChange({ ...config, dataPath: e.target.value })} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="/volume1/docker/flnxi" + /> +

+ 数据库和上传文件将存储在此目录 +

+
+ +
+

+ 注意:请确保存储路径有足够的空间,建议至少 10GB。 +

+
+
+ +
+ + +
+
+
+ ); +} diff --git a/flnxi-agent/frontend/src/pages/Dashboard.tsx b/flnxi-agent/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..685bb62 --- /dev/null +++ b/flnxi-agent/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,220 @@ +import { useEffect, useState } from 'react'; +import { Socket } from 'socket.io-client'; + +interface Props { + socket: Socket | null; +} + +interface Status { + installed: boolean; + running: boolean; + version: string | null; + uptime: number | null; + health: string; +} + +interface UpdateInfo { + available: boolean; + currentVersion: string | null; + latestVersion: string | null; + releaseNotes: string | null; + isRequired: boolean; +} + +interface AgentUpdateInfo { + available: boolean; + currentVersion: string | null; + latestVersion: string | null; + releaseNotes: string | null; +} + +export default function Dashboard({ socket }: Props) { + const [status, setStatus] = useState(null); + const [update, setUpdate] = useState(null); + const [updating, setUpdating] = useState(false); + const [updateProgress, setUpdateProgress] = useState<{ progress: number; message: string } | null>(null); + + // Agent 更新状态 + const [agentUpdate, setAgentUpdate] = useState(null); + const [agentUpdating, setAgentUpdating] = useState(false); + const [agentUpdateProgress, setAgentUpdateProgress] = useState<{ progress: number; message: string } | null>(null); + + useEffect(() => { + fetchStatus(); + checkUpdate(); + checkAgentUpdate(); + + const interval = setInterval(fetchStatus, 5000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + if (!socket) return; + + // 素材系统更新事件 + socket.on('update:progress', (data) => setUpdateProgress(data)); + socket.on('update:complete', () => { + setUpdating(false); + setUpdateProgress(null); + fetchStatus(); + checkUpdate(); + }); + socket.on('update:error', () => { + setUpdating(false); + setUpdateProgress(null); + }); + + // Agent 更新事件 + socket.on('agent:update:progress', (data) => setAgentUpdateProgress(data)); + socket.on('agent:update:complete', () => { + setAgentUpdating(false); + setAgentUpdateProgress({ progress: 100, message: 'Agent 更新完成!页面将在 3 秒后刷新...' }); + setTimeout(() => window.location.reload(), 3000); + }); + socket.on('agent:update:error', () => { + setAgentUpdating(false); + setAgentUpdateProgress(null); + }); + }, [socket]); + + const fetchStatus = async () => { + try { + const res = await fetch('/api/status'); + setStatus(await res.json()); + } catch {} + }; + + const checkUpdate = async () => { + try { + const res = await fetch('/api/check-update'); + setUpdate(await res.json()); + } catch {} + }; + + const checkAgentUpdate = async () => { + try { + const res = await fetch('/api/agent/check-update'); + setAgentUpdate(await res.json()); + } catch {} + }; + + const handleAgentUpdate = async () => { + if (!confirm('确定要更新 Agent 吗?更新过程中页面会断开连接,完成后需要刷新页面。')) { + return; + } + setAgentUpdating(true); + await fetch('/api/agent/update', { method: 'POST' }); + }; + + const handleRestart = async () => { + await fetch('/api/restart', { method: 'POST' }); + setTimeout(fetchStatus, 3000); + }; + + const handleUpdate = async () => { + setUpdating(true); + await fetch('/api/update', { method: 'POST' }); + }; + + const formatUptime = (seconds: number) => { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return `${d}天 ${h}小时`; + if (h > 0) return `${h}小时 ${m}分钟`; + return `${m}分钟`; + }; + + return ( +
+
+

FLNXI 素材系统管理

+ +
+
+

运行状态

+
+ + {status?.running ? '运行中' : '已停止'} +
+
+ +
+

当前版本

+ {status?.version || '-'} +
+ +
+

运行时间

+ {status?.uptime ? formatUptime(status.uptime) : '-'} +
+ +
+

健康状态

+ + {status?.health === 'healthy' ? '健康' : status?.health === 'unhealthy' ? '异常' : '未知'} + +
+
+ + {/* Agent 更新提示 */} + {agentUpdate?.available && ( +
+

🔧 Agent 安装程序有更新

+

+ 当前版本: {agentUpdate.currentVersion || '未知'} → 最新版本: {agentUpdate.latestVersion} +

+ {agentUpdate.releaseNotes &&

{agentUpdate.releaseNotes}

} + {agentUpdating ? ( +
+
+
+
+

{agentUpdateProgress?.message || '更新中...'}

+
+ ) : ( + + )} +
+ )} + + {/* 素材系统更新提示 */} + {update?.available && ( +
+

{update.isRequired ? '⚠️ 需要更新' : '🎉 素材系统有新版本'}

+

+ 当前版本: {update.currentVersion} → 最新版本: {update.latestVersion} +

+ {update.releaseNotes &&

{update.releaseNotes}

} + {updating ? ( +
+
+
+
+

{updateProgress?.message || '更新中...'}

+
+ ) : ( + + )} +
+ )} + +
+

操作

+
+ + +
+
+
+
+ ); +} diff --git a/flnxi-agent/frontend/src/pages/Installing.tsx b/flnxi-agent/frontend/src/pages/Installing.tsx new file mode 100644 index 0000000..e3b70f5 --- /dev/null +++ b/flnxi-agent/frontend/src/pages/Installing.tsx @@ -0,0 +1,42 @@ +interface Props { + progress: { step: string; progress: number; message: string; detail?: string } | null; + error: string | null; + onRetry: () => void; +} + +export default function Installing({ progress, error, onRetry }: Props) { + return ( +
+
+

正在安装

+ + {error ? ( +
+
+ + + +
+

{error}

+ +
+ ) : ( + <> +
+

{progress?.message || '准备中...'}

+ {progress?.detail &&

{progress.detail}

} +
+
+
+

{Math.round(progress?.progress || 0)}%

+ + )} +
+
+ ); +} diff --git a/flnxi-agent/frontend/src/pages/Welcome.tsx b/flnxi-agent/frontend/src/pages/Welcome.tsx new file mode 100644 index 0000000..80a75e3 --- /dev/null +++ b/flnxi-agent/frontend/src/pages/Welcome.tsx @@ -0,0 +1,62 @@ +interface Props { + onNext: () => void; +} + +export default function Welcome({ onNext }: Props) { + return ( +
+
+
+ + + +
+ +

FLNXI 素材系统

+

+ 欢迎使用 FLNXI 素材系统安装助手。
+ 只需几步即可完成安装。 +

+ +
+
+
+ 1 +
+
+

配置端口和存储路径

+

设置 Web 访问端口和数据存储位置

+
+
+ +
+
+ 2 +
+
+

自动安装

+

自动拉取镜像并配置服务

+
+
+ +
+
+ 3 +
+
+

激活许可证

+

首次访问时激活许可证或开始免费试用

+
+
+
+ + +
+
+ ); +} diff --git a/flnxi-agent/frontend/tailwind.config.js b/flnxi-agent/frontend/tailwind.config.js new file mode 100644 index 0000000..d21f1cd --- /dev/null +++ b/flnxi-agent/frontend/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/flnxi-agent/frontend/tsconfig.json b/flnxi-agent/frontend/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/flnxi-agent/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/flnxi-agent/frontend/tsconfig.node.json b/flnxi-agent/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/flnxi-agent/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/flnxi-agent/frontend/vite.config.ts b/flnxi-agent/frontend/vite.config.ts new file mode 100644 index 0000000..497a69d --- /dev/null +++ b/flnxi-agent/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + '/api': 'http://localhost:3080', + '/socket.io': { + target: 'http://localhost:3080', + ws: true, + }, + }, + }, +}); diff --git a/flnxi-agent/package-lock.json b/flnxi-agent/package-lock.json new file mode 100644 index 0000000..0429be6 --- /dev/null +++ b/flnxi-agent/package-lock.json @@ -0,0 +1,2594 @@ +{ + "name": "flnxi-agent", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flnxi-agent", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "dockerode": "^4.0.2", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "socket.io": "^4.7.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.23", + "@types/express": "^4.17.21", + "@types/node": "^20.10.0", + "tsx": "^4.6.2", + "typescript": "^5.3.2" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.47", + "resolved": "https://registry.npmmirror.com/@types/dockerode/-/dockerode-3.3.47.tgz", + "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmmirror.com/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/docker-modem": { + "version": "5.0.6", + "resolved": "https://registry.npmmirror.com/docker-modem/-/docker-modem-5.0.6.tgz", + "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/dockerode/-/dockerode-4.0.9.tgz", + "integrity": "sha512-iND4mcOWhPaCNh54WmK/KoSb35AFqPAUWFMffTQcp52uQt36b5uNwEJTSXntJZBbeGad72Crbi/hvDIv6us/6Q==", + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.6", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.1", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmmirror.com/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "license": "MIT", + "optional": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "license": "ISC" + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmmirror.com/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/flnxi-agent/package.json b/flnxi-agent/package.json new file mode 100644 index 0000000..2117b59 --- /dev/null +++ b/flnxi-agent/package.json @@ -0,0 +1,30 @@ +{ + "name": "flnxi-agent", + "version": "1.0.0", + "description": "FLNXI 素材系统安装助手", + "main": "dist/server.js", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc && npm run build:frontend", + "build:frontend": "cd frontend && npm run build && (if exist ..\\dist\\public rmdir /s /q ..\\dist\\public) && xcopy /E /I /Y dist ..\\dist\\public", + "build:frontend:unix": "cd frontend && npm run build && cp -r dist ../dist/public", + "start": "node dist/server.js", + "docker:build": "docker buildx build --platform linux/amd64,linux/arm64 -t flnxi/flnxi-agent:latest --push .", + "docker:build:local": "docker build -t flnxi/flnxi-agent:latest ." + }, + "dependencies": { + "dockerode": "^4.0.2", + "express": "^4.18.2", + "socket.io": "^4.7.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/dockerode": "^3.3.23", + "@types/express": "^4.17.21", + "@types/node": "^20.10.0", + "tsx": "^4.6.2", + "typescript": "^5.3.2" + } +} diff --git a/flnxi-agent/src/installer.ts b/flnxi-agent/src/installer.ts new file mode 100644 index 0000000..e786b5d --- /dev/null +++ b/flnxi-agent/src/installer.ts @@ -0,0 +1,336 @@ +import Docker from 'dockerode'; +import { EventEmitter } from 'events'; + +export interface InstallConfig { + webPort: number; // 素材系统 Web 端口 + dbPort?: number; // PostgreSQL 端口(可选) + minioPort?: number; // MinIO 端口(可选) + dataPath: string; // 数据存储路径 +} + +export interface InstallProgress { + step: string; + progress: number; // 0-100 + message: string; + detail?: string; +} + +type ProgressCallback = (progress: InstallProgress) => void; + +// Harbor 镜像仓库地址 +const HARBOR_REGISTRY = 'harbor.flnxi.com/flnxi'; + +// 镜像配置 - 优先使用 Harbor 镜像 +const IMAGES = { + // 素材系统主镜像 + material: `${HARBOR_REGISTRY}/material/flnxi-material:latest`, + // 依赖镜像 - 优先 Harbor,备用 Docker Hub + postgres: { + primary: `${HARBOR_REGISTRY}/runtime/postgres:16-alpine`, + fallback: 'postgres:16-alpine', + }, + minio: { + primary: `${HARBOR_REGISTRY}/runtime/minio:latest`, + fallback: 'minio/minio:latest', + }, + redis: { + primary: `${HARBOR_REGISTRY}/runtime/redis:7-alpine`, + fallback: 'redis:7-alpine', + }, +}; + +// 容器名称 +const CONTAINERS = { + material: 'flnxi-material', + postgres: 'flnxi-postgres', + minio: 'flnxi-minio', + redis: 'flnxi-redis', +}; + +export class Installer extends EventEmitter { + private docker: Docker; + + constructor() { + super(); + this.docker = new Docker({ socketPath: '/var/run/docker.sock' }); + } + + /** + * 检查是否已安装 + */ + async isInstalled(): Promise { + try { + const containers = await this.docker.listContainers({ all: true }); + return containers.some((c) => c.Names.some((n) => n.includes(CONTAINERS.material))); + } catch { + return false; + } + } + + // 实际使用的镜像(可能是 Harbor 或 fallback) + private usedImages = { + postgres: '', + minio: '', + redis: '', + material: '', + }; + + /** + * 执行安装 + */ + async install(config: InstallConfig, onProgress: ProgressCallback): Promise { + const { webPort, dbPort = 5432, minioPort = 9000, dataPath } = config; + + try { + // Step 1: 拉取镜像(优先 Harbor,失败后 fallback 到 Docker Hub) + onProgress({ step: 'pull', progress: 0, message: '正在拉取镜像...' }); + + this.usedImages.postgres = await this.pullImageWithFallback(IMAGES.postgres, (p) => { + onProgress({ step: 'pull', progress: p * 0.2, message: '拉取 PostgreSQL...', detail: `${Math.round(p)}%` }); + }); + + this.usedImages.minio = await this.pullImageWithFallback(IMAGES.minio, (p) => { + onProgress({ step: 'pull', progress: 20 + p * 0.2, message: '拉取 MinIO...', detail: `${Math.round(p)}%` }); + }); + + this.usedImages.redis = await this.pullImageWithFallback(IMAGES.redis, (p) => { + onProgress({ step: 'pull', progress: 40 + p * 0.1, message: '拉取 Redis...', detail: `${Math.round(p)}%` }); + }); + + this.usedImages.material = await this.pullImageWithFallback(IMAGES.material, (p) => { + onProgress({ step: 'pull', progress: 50 + p * 0.3, message: '拉取素材系统...', detail: `${Math.round(p)}%` }); + }); + + onProgress({ step: 'pull', progress: 80, message: '镜像拉取完成' }); + + // Step 2: 创建网络 + onProgress({ step: 'network', progress: 82, message: '创建 Docker 网络...' }); + await this.createNetwork('flnxi-network'); + + // Step 3: 启动容器 + onProgress({ step: 'containers', progress: 85, message: '启动 PostgreSQL...' }); + await this.startPostgres(dataPath, dbPort); + + onProgress({ step: 'containers', progress: 88, message: '启动 MinIO...' }); + await this.startMinio(dataPath, minioPort); + + onProgress({ step: 'containers', progress: 91, message: '启动 Redis...' }); + await this.startRedis(); + + onProgress({ step: 'containers', progress: 94, message: '启动素材系统...' }); + await this.startMaterial(webPort, dataPath); + + // Step 4: 等待服务就绪 + onProgress({ step: 'ready', progress: 97, message: '等待服务就绪...' }); + await this.waitForService(`http://localhost:${webPort}/api/health`, 60000); + + onProgress({ step: 'complete', progress: 100, message: '安装完成!' }); + } catch (error) { + throw error; + } + } + + /** + * 拉取镜像(支持 fallback) + */ + private async pullImage(image: string, onProgress: (percent: number) => void): Promise { + return new Promise((resolve, reject) => { + this.docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) { + reject(err); + return; + } + + let lastProgress = 0; + + this.docker.modem.followProgress( + stream, + (err: Error | null) => { + if (err) reject(err); + else resolve(); + }, + (event: { status: string; progressDetail?: { current?: number; total?: number } }) => { + if (event.progressDetail?.total) { + const percent = (event.progressDetail.current || 0) / event.progressDetail.total * 100; + if (percent > lastProgress) { + lastProgress = percent; + onProgress(percent); + } + } + } + ); + }); + }); + } + + /** + * 拉取镜像(带 fallback 支持) + * 优先从 Harbor 拉取,失败后尝试 Docker Hub + */ + private async pullImageWithFallback( + imageConfig: { primary: string; fallback: string } | string, + onProgress: (percent: number) => void + ): Promise { + // 如果是字符串,直接拉取 + if (typeof imageConfig === 'string') { + await this.pullImage(imageConfig, onProgress); + return imageConfig; + } + + // 尝试从 Harbor 拉取 + try { + await this.pullImage(imageConfig.primary, onProgress); + return imageConfig.primary; + } catch (error) { + console.log(`Harbor 镜像拉取失败,尝试 Docker Hub: ${imageConfig.fallback}`); + // 回退到 Docker Hub + await this.pullImage(imageConfig.fallback, onProgress); + return imageConfig.fallback; + } + } + + /** + * 创建 Docker 网络 + */ + private async createNetwork(name: string): Promise { + try { + const networks = await this.docker.listNetworks(); + if (!networks.some((n) => n.Name === name)) { + await this.docker.createNetwork({ Name: name }); + } + } catch { + // 网络可能已存在 + } + } + + /** + * 启动 PostgreSQL + */ + private async startPostgres(dataPath: string, port: number): Promise { + await this.removeContainer(CONTAINERS.postgres); + + const container = await this.docker.createContainer({ + Image: this.usedImages.postgres, + name: CONTAINERS.postgres, + Env: [ + 'POSTGRES_USER=flnxi', + 'POSTGRES_PASSWORD=flnxi_secret', + 'POSTGRES_DB=flnxi_material', + ], + HostConfig: { + Binds: [`${dataPath}/postgres:/var/lib/postgresql/data`], + PortBindings: { '5432/tcp': [{ HostPort: String(port) }] }, + NetworkMode: 'flnxi-network', + RestartPolicy: { Name: 'unless-stopped' }, + }, + }); + + await container.start(); + } + + /** + * 启动 MinIO + */ + private async startMinio(dataPath: string, port: number): Promise { + await this.removeContainer(CONTAINERS.minio); + + const container = await this.docker.createContainer({ + Image: this.usedImages.minio, + name: CONTAINERS.minio, + Cmd: ['server', '/data', '--console-address', ':9001'], + Env: [ + 'MINIO_ROOT_USER=flnxi', + 'MINIO_ROOT_PASSWORD=flnxi_secret', + ], + HostConfig: { + Binds: [`${dataPath}/minio:/data`], + PortBindings: { + '9000/tcp': [{ HostPort: String(port) }], + '9001/tcp': [{ HostPort: String(port + 1) }], + }, + NetworkMode: 'flnxi-network', + RestartPolicy: { Name: 'unless-stopped' }, + }, + }); + + await container.start(); + } + + /** + * 启动 Redis + */ + private async startRedis(): Promise { + await this.removeContainer(CONTAINERS.redis); + + const container = await this.docker.createContainer({ + Image: this.usedImages.redis, + name: CONTAINERS.redis, + HostConfig: { + NetworkMode: 'flnxi-network', + RestartPolicy: { Name: 'unless-stopped' }, + }, + }); + + await container.start(); + } + + /** + * 启动素材系统 + */ + private async startMaterial(port: number, dataPath: string): Promise { + await this.removeContainer(CONTAINERS.material); + + const container = await this.docker.createContainer({ + Image: this.usedImages.material, + name: CONTAINERS.material, + Env: [ + 'DATABASE_URL=postgresql://flnxi:flnxi_secret@flnxi-postgres:5432/flnxi_material', + 'S3_ENDPOINT=http://flnxi-minio:9000', + 'S3_ACCESS_KEY=flnxi', + 'S3_SECRET_KEY=flnxi_secret', + 'S3_BUCKET=materials', + 'REDIS_URL=redis://flnxi-redis:6379', + 'NEXTAUTH_SECRET=flnxi-material-secret-key', + `NEXTAUTH_URL=http://localhost:${port}`, + ], + HostConfig: { + Binds: [`${dataPath}/uploads:/app/uploads`], + PortBindings: { '3000/tcp': [{ HostPort: String(port) }] }, + NetworkMode: 'flnxi-network', + RestartPolicy: { Name: 'unless-stopped' }, + }, + }); + + await container.start(); + } + + /** + * 移除容器 + */ + private async removeContainer(name: string): Promise { + try { + const container = this.docker.getContainer(name); + await container.stop().catch(() => {}); + await container.remove().catch(() => {}); + } catch { + // 容器可能不存在 + } + } + + /** + * 等待服务就绪 + */ + private async waitForService(url: string, timeout: number): Promise { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + const res = await fetch(url); + if (res.ok) return; + } catch { + // 继续等待 + } + await new Promise((r) => setTimeout(r, 2000)); + } + throw new Error('服务启动超时'); + } +} diff --git a/flnxi-agent/src/server.ts b/flnxi-agent/src/server.ts new file mode 100644 index 0000000..0a9411b --- /dev/null +++ b/flnxi-agent/src/server.ts @@ -0,0 +1,185 @@ +import express, { Request, Response } from 'express'; +import { createServer } from 'http'; +import { Server, Socket } from 'socket.io'; +import cors from 'cors'; +import path from 'path'; +import { Installer, InstallProgress } from './installer'; +import { SystemManager, UpdateProgress } from './system-manager'; + +const app = express(); +const httpServer = createServer(app); +const io = new Server(httpServer, { + cors: { + origin: '*', + methods: ['GET', 'POST'], + }, +}); + +const PORT = process.env.PORT || 3080; +const installer = new Installer(); +const systemManager = new SystemManager(); + +// 中间件 +app.use(cors()); +app.use(express.json()); + +// 静态文件服务(前端) +app.use(express.static(path.join(__dirname, 'public'))); + +// API 路由 + +// 获取系统状态 +app.get('/api/status', async (_req: Request, res: Response) => { + try { + const status = await systemManager.getStatus(); + res.json(status); + } catch (error) { + res.status(500).json({ error: '获取状态失败' }); + } +}); + +// 检查是否已安装 +app.get('/api/installed', async (_req: Request, res: Response) => { + try { + const installed = await installer.isInstalled(); + res.json({ installed }); + } catch (error) { + res.status(500).json({ error: '检查安装状态失败' }); + } +}); + +// 开始安装 +app.post('/api/install', async (req: Request, res: Response) => { + try { + const { config } = req.body; + + // 异步执行安装,通过 WebSocket 推送进度 + installer.install(config, (progress: InstallProgress) => { + io.emit('install:progress', progress); + }).then(() => { + io.emit('install:complete', { success: true }); + }).catch((err: Error) => { + io.emit('install:error', { error: err.message }); + }); + + res.json({ success: true, message: '安装已开始' }); + } catch (error) { + res.status(500).json({ error: '启动安装失败' }); + } +}); + +// 检查更新 +app.get('/api/check-update', async (_req: Request, res: Response) => { + try { + const update = await systemManager.checkUpdate(); + res.json(update); + } catch (error) { + res.status(500).json({ error: '检查更新失败' }); + } +}); + +// 执行更新 +app.post('/api/update', async (_req: Request, res: Response) => { + try { + systemManager.update((progress: UpdateProgress) => { + io.emit('update:progress', progress); + }).then(() => { + io.emit('update:complete', { success: true }); + }).catch((err: Error) => { + io.emit('update:error', { error: err.message }); + }); + + res.json({ success: true, message: '更新已开始' }); + } catch (error) { + res.status(500).json({ error: '启动更新失败' }); + } +}); + +// 重启服务 +app.post('/api/restart', async (_req: Request, res: Response) => { + try { + await systemManager.restart(); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: '重启失败' }); + } +}); + +// 停止服务 +app.post('/api/stop', async (_req: Request, res: Response) => { + try { + await systemManager.stop(); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: '停止失败' }); + } +}); + +// 获取日志 +app.get('/api/logs', async (req: Request, res: Response) => { + try { + const lines = parseInt(req.query.lines as string) || 100; + const logs = await systemManager.getLogs(lines); + res.json({ logs }); + } catch (error) { + res.status(500).json({ error: '获取日志失败' }); + } +}); + +// ==================== Agent 自更新 API ==================== + +// 获取 Agent 版本 +app.get('/api/agent/version', async (_req: Request, res: Response) => { + try { + const version = systemManager.getAgentVersion(); + res.json({ version }); + } catch (error) { + res.status(500).json({ error: '获取 Agent 版本失败' }); + } +}); + +// 检查 Agent 更新 +app.get('/api/agent/check-update', async (_req: Request, res: Response) => { + try { + const update = await systemManager.checkAgentUpdate(); + res.json(update); + } catch (error) { + res.status(500).json({ error: '检查 Agent 更新失败' }); + } +}); + +// 更新 Agent +app.post('/api/agent/update', async (_req: Request, res: Response) => { + try { + systemManager.updateAgent((progress: UpdateProgress) => { + io.emit('agent:update:progress', progress); + }).then(() => { + io.emit('agent:update:complete', { success: true }); + }).catch((err: Error) => { + io.emit('agent:update:error', { error: err.message }); + }); + + res.json({ success: true, message: 'Agent 更新已开始' }); + } catch (error) { + res.status(500).json({ error: '启动 Agent 更新失败' }); + } +}); + +// WebSocket 连接 +io.on('connection', (socket: Socket) => { + console.log('客户端已连接:', socket.id); + + socket.on('disconnect', () => { + console.log('客户端已断开:', socket.id); + }); +}); + +// 前端路由回退(SPA) +app.get('*', (_req: Request, res: Response) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// 启动服务器 +httpServer.listen(PORT, () => { + console.log(`🚀 FLNXI Agent 运行在 http://localhost:${PORT}`); +}); diff --git a/flnxi-agent/src/system-manager.ts b/flnxi-agent/src/system-manager.ts new file mode 100644 index 0000000..cb35b4f --- /dev/null +++ b/flnxi-agent/src/system-manager.ts @@ -0,0 +1,455 @@ +import Docker from 'dockerode'; +import fs from 'fs'; +import path from 'path'; + +const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || 'http://45.249.208.118:3001'; +const CONTAINER_NAME = 'flnxi-material'; +const AGENT_CONTAINER_NAME = 'flnxi-agent'; + +// Harbor 镜像仓库地址 +const HARBOR_REGISTRY = 'harbor.flnxi.com/flnxi'; +const MATERIAL_IMAGE = `${HARBOR_REGISTRY}/material/flnxi-material:latest`; + +// Agent 镜像(Docker Hub) +const AGENT_IMAGE = 'flnxi/flnxi-agent:latest'; + +// Agent 版本文件 +const AGENT_VERSION_FILE = '/app/VERSION.txt'; + +export interface SystemStatus { + installed: boolean; + running: boolean; + version: string | null; + uptime: number | null; + health: 'healthy' | 'unhealthy' | 'unknown'; +} + +export interface UpdateInfo { + available: boolean; + currentVersion: string | null; + latestVersion: string | null; + releaseNotes: string | null; + isRequired: boolean; +} + +export interface AgentUpdateInfo { + available: boolean; + currentVersion: string | null; + latestVersion: string | null; + releaseNotes: string | null; +} + +export interface UpdateProgress { + step: string; + progress: number; + message: string; +} + +type ProgressCallback = (progress: UpdateProgress) => void; + +export class SystemManager { + private docker: Docker; + + constructor() { + this.docker = new Docker({ socketPath: '/var/run/docker.sock' }); + } + + /** + * 获取系统状态 + */ + async getStatus(): Promise { + try { + const container = this.docker.getContainer(CONTAINER_NAME); + const info = await container.inspect(); + + const running = info.State.Running; + const version = await this.getVersion(); + const uptime = running + ? Math.floor((Date.now() - new Date(info.State.StartedAt).getTime()) / 1000) + : null; + + let health: 'healthy' | 'unhealthy' | 'unknown' = 'unknown'; + if (info.State.Health) { + health = info.State.Health.Status === 'healthy' ? 'healthy' : 'unhealthy'; + } else if (running) { + health = 'healthy'; + } + + return { + installed: true, + running, + version, + uptime, + health, + }; + } catch { + return { + installed: false, + running: false, + version: null, + uptime: null, + health: 'unknown', + }; + } + } + + /** + * 获取当前版本 + */ + private async getVersion(): Promise { + try { + const container = this.docker.getContainer(CONTAINER_NAME); + const exec = await container.exec({ + Cmd: ['cat', '/app/VERSION.txt'], + AttachStdout: true, + }); + + const stream = await exec.start({ hijack: true, stdin: false }); + + return new Promise((resolve) => { + let output = ''; + stream.on('data', (chunk: Buffer) => { + output += chunk.toString(); + }); + stream.on('end', () => { + resolve(output.trim() || null); + }); + stream.on('error', () => resolve(null)); + }); + } catch { + return null; + } + } + + /** + * 检查更新 + */ + async checkUpdate(): Promise { + try { + const currentVersion = await this.getVersion(); + + const res = await fetch(`${LICENSE_SERVER_URL}/api/version/check`); + if (!res.ok) { + throw new Error('检查更新失败'); + } + + const data = await res.json() as { + latestVersion: string; + minRequiredVersion?: string; + releaseNotes?: string; + }; + + const available = currentVersion !== data.latestVersion; + const isRequired = data.minRequiredVersion + ? this.compareVersions(currentVersion || '0.0.0', data.minRequiredVersion) < 0 + : false; + + return { + available, + currentVersion, + latestVersion: data.latestVersion, + releaseNotes: data.releaseNotes || null, + isRequired, + }; + } catch { + return { + available: false, + currentVersion: await this.getVersion(), + latestVersion: null, + releaseNotes: null, + isRequired: false, + }; + } + } + + /** + * 执行更新 + */ + async update(onProgress: ProgressCallback): Promise { + // 使用 Harbor 镜像 + const IMAGE = MATERIAL_IMAGE; + + try { + // Step 1: 拉取新镜像 + onProgress({ step: 'pull', progress: 0, message: '正在拉取新版本...' }); + + await new Promise((resolve, reject) => { + this.docker.pull(IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) { + reject(err); + return; + } + + this.docker.modem.followProgress( + stream, + (err: Error | null) => { + if (err) reject(err); + else resolve(); + }, + (event: { progressDetail?: { current?: number; total?: number } }) => { + if (event.progressDetail?.total) { + const percent = (event.progressDetail.current || 0) / event.progressDetail.total * 100; + onProgress({ step: 'pull', progress: percent * 0.6, message: `下载中... ${Math.round(percent)}%` }); + } + } + ); + }); + }); + + onProgress({ step: 'backup', progress: 65, message: '备份当前配置...' }); + + // Step 2: 获取当前容器配置 + const container = this.docker.getContainer(CONTAINER_NAME); + const info = await container.inspect(); + + // Step 3: 停止并重命名旧容器 + onProgress({ step: 'stop', progress: 70, message: '停止当前服务...' }); + await container.stop().catch(() => {}); + await container.rename(`${CONTAINER_NAME}-backup-${Date.now()}`).catch(() => {}); + + // Step 4: 创建新容器 + onProgress({ step: 'create', progress: 80, message: '创建新容器...' }); + const newContainer = await this.docker.createContainer({ + Image: IMAGE, + name: CONTAINER_NAME, + Env: info.Config.Env, + HostConfig: info.HostConfig, + }); + + // Step 5: 启动新容器 + onProgress({ step: 'start', progress: 90, message: '启动新服务...' }); + await newContainer.start(); + + // Step 6: 健康检查 + onProgress({ step: 'health', progress: 95, message: '检查服务状态...' }); + await this.waitForHealthy(30000); + + onProgress({ step: 'complete', progress: 100, message: '更新完成!' }); + } catch (error) { + // 回滚 + try { + const backupContainers = await this.docker.listContainers({ + all: true, + filters: { name: [`${CONTAINER_NAME}-backup`] }, + }); + + if (backupContainers.length > 0) { + const backup = this.docker.getContainer(backupContainers[0].Id); + await this.docker.getContainer(CONTAINER_NAME).remove({ force: true }).catch(() => {}); + await backup.rename(CONTAINER_NAME); + await backup.start(); + } + } catch { + // 回滚失败 + } + + throw error; + } + } + + /** + * 重启服务 + */ + async restart(): Promise { + const container = this.docker.getContainer(CONTAINER_NAME); + await container.restart(); + } + + /** + * 停止服务 + */ + async stop(): Promise { + const container = this.docker.getContainer(CONTAINER_NAME); + await container.stop(); + } + + /** + * 获取日志 + */ + async getLogs(lines: number = 100): Promise { + try { + const container = this.docker.getContainer(CONTAINER_NAME); + const logs = await container.logs({ + stdout: true, + stderr: true, + tail: lines, + }); + + return logs.toString(); + } catch { + return ''; + } + } + + /** + * 等待服务健康 + */ + private async waitForHealthy(timeout: number): Promise { + const start = Date.now(); + while (Date.now() - start < timeout) { + const status = await this.getStatus(); + if (status.running && status.health === 'healthy') { + return; + } + await new Promise((r) => setTimeout(r, 2000)); + } + throw new Error('服务启动超时'); + } + + /** + * 比较版本号 + */ + private compareVersions(v1: string, v2: string): number { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + if (p1 > p2) return 1; + if (p1 < p2) return -1; + } + + return 0; + } + + // ==================== Agent 自更新功能 ==================== + + /** + * 获取 Agent 当前版本 + */ + getAgentVersion(): string | null { + try { + if (fs.existsSync(AGENT_VERSION_FILE)) { + return fs.readFileSync(AGENT_VERSION_FILE, 'utf-8').trim(); + } + // 开发环境下读取本地版本文件 + const localVersionFile = path.join(__dirname, '..', 'VERSION.txt'); + if (fs.existsSync(localVersionFile)) { + return fs.readFileSync(localVersionFile, 'utf-8').trim(); + } + return null; + } catch { + return null; + } + } + + /** + * 检查 Agent 更新 + */ + async checkAgentUpdate(): Promise { + try { + const currentVersion = this.getAgentVersion(); + + const res = await fetch(`${LICENSE_SERVER_URL}/api/version/agent`); + if (!res.ok) { + throw new Error('检查 Agent 更新失败'); + } + + const data = await res.json() as { + latestVersion: string; + releaseNotes?: string; + }; + const available = currentVersion !== data.latestVersion; + + return { + available, + currentVersion, + latestVersion: data.latestVersion, + releaseNotes: data.releaseNotes || null, + }; + } catch { + return { + available: false, + currentVersion: this.getAgentVersion(), + latestVersion: null, + releaseNotes: null, + }; + } + } + + /** + * 更新 Agent 自身 + * 注意:此操作会导致当前 Agent 容器重启,连接会断开 + */ + async updateAgent(onProgress: ProgressCallback): Promise { + try { + // Step 1: 拉取新的 Agent 镜像 + onProgress({ step: 'pull', progress: 0, message: '正在拉取新版 Agent...' }); + + await new Promise((resolve, reject) => { + this.docker.pull(AGENT_IMAGE, (err: Error | null, stream: NodeJS.ReadableStream) => { + if (err) { + reject(err); + return; + } + + this.docker.modem.followProgress( + stream, + (err: Error | null) => { + if (err) reject(err); + else resolve(); + }, + (event: { progressDetail?: { current?: number; total?: number } }) => { + if (event.progressDetail?.total) { + const percent = (event.progressDetail.current || 0) / event.progressDetail.total * 100; + onProgress({ step: 'pull', progress: percent * 0.7, message: `下载中... ${Math.round(percent)}%` }); + } + } + ); + }); + }); + + onProgress({ step: 'prepare', progress: 75, message: '准备重启 Agent...' }); + + // Step 2: 获取当前 Agent 容器信息 + const container = this.docker.getContainer(AGENT_CONTAINER_NAME); + const info = await container.inspect(); + + // Step 3: 创建新容器(使用相同配置) + onProgress({ step: 'create', progress: 80, message: '创建新 Agent 容器...' }); + + const newContainer = await this.docker.createContainer({ + Image: AGENT_IMAGE, + name: `${AGENT_CONTAINER_NAME}-new`, + Env: info.Config.Env, + HostConfig: info.HostConfig, + ExposedPorts: info.Config.ExposedPorts, + }); + + // Step 4: 停止当前容器并重命名 + onProgress({ step: 'switch', progress: 85, message: '切换到新版本...' }); + + await container.stop().catch(() => {}); + await container.rename(`${AGENT_CONTAINER_NAME}-old-${Date.now()}`).catch(() => {}); + await newContainer.rename(AGENT_CONTAINER_NAME); + + // Step 5: 启动新容器 + onProgress({ step: 'start', progress: 95, message: '启动新 Agent...' }); + await newContainer.start(); + + onProgress({ step: 'complete', progress: 100, message: 'Agent 更新完成!请刷新页面。' }); + + // 注意:此时当前进程即将被终止,因为容器会被替换 + } catch (error) { + // 尝试回滚 + try { + const oldContainers = await this.docker.listContainers({ + all: true, + filters: { name: [`${AGENT_CONTAINER_NAME}-old`] }, + }); + + if (oldContainers.length > 0) { + const oldContainer = this.docker.getContainer(oldContainers[0].Id); + await this.docker.getContainer(`${AGENT_CONTAINER_NAME}-new`).remove({ force: true }).catch(() => {}); + await oldContainer.rename(AGENT_CONTAINER_NAME); + await oldContainer.start(); + } + } catch { + // 回滚失败 + } + + throw error; + } + } +} diff --git a/flnxi-agent/tsconfig.json b/flnxi-agent/tsconfig.json new file mode 100644 index 0000000..77fc3dc --- /dev/null +++ b/flnxi-agent/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "baseUrl": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "frontend"] +} diff --git a/instrumentation.ts b/instrumentation.ts new file mode 100644 index 0000000..3aff849 --- /dev/null +++ b/instrumentation.ts @@ -0,0 +1,19 @@ +/** + * Next.js Instrumentation + * 在服务器启动时执行初始化代码 + */ + +export async function register() { + // 只在服务端运行 + if (process.env.NEXT_RUNTIME === 'nodejs') { + const { initLicenseService } = await import('@/lib/license'); + + // 初始化许可证服务(验证许可证并启动心跳) + try { + await initLicenseService(); + console.log('✅ 许可证服务已初始化'); + } catch (error) { + console.error('❌ 许可证服务初始化失败:', error); + } + } +} diff --git a/lib/__tests__/check-limits.test.ts b/lib/__tests__/check-limits.test.ts new file mode 100644 index 0000000..4ff6db8 --- /dev/null +++ b/lib/__tests__/check-limits.test.ts @@ -0,0 +1,190 @@ +/** + * 许可证限制检查工具测试 + * 测试 lib/license/check-limits.ts 中的限制检查函数 + */ + +import { + canCreateUser, + canUploadMaterial, + canUseCustomTemplates, + canUseDepartmentIsolation, + canUseIntranetPenetration, + canUseSystemSettings, + canUseApiAccess, + LimitCheckResult, +} from '../license/check-limits'; + +// Mock license-service 模块 +jest.mock('../license/license-service', () => ({ + checkLimit: jest.fn(), + isFeatureEnabled: jest.fn(), + getLicenseType: jest.fn(), +})); + +import { checkLimit, isFeatureEnabled, getLicenseType } from '../license/license-service'; + +const mockCheckLimit = checkLimit as jest.MockedFunction; +const mockIsFeatureEnabled = isFeatureEnabled as jest.MockedFunction; +const mockGetLicenseType = getLicenseType as jest.MockedFunction; + +describe('canCreateUser', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('当未达到限制时应该返回 allowed: true', async () => { + mockCheckLimit.mockResolvedValue({ allowed: true, current: 5, max: 10 }); + + const result = await canCreateUser(); + + expect(result.allowed).toBe(true); + expect(result.current).toBe(5); + expect(result.max).toBe(10); + expect(result.message).toBeUndefined(); + expect(mockCheckLimit).toHaveBeenCalledWith('users'); + }); + + it('当达到限制时(FREE 版本)应该返回升级提示', async () => { + mockCheckLimit.mockResolvedValue({ allowed: false, current: 10, max: 10 }); + mockGetLicenseType.mockReturnValue('FREE'); + + const result = await canCreateUser(); + + expect(result.allowed).toBe(false); + expect(result.message).toContain('免费版'); + expect(result.message).toContain('10'); + expect(result.message).toContain('升级'); + }); + + it('当达到限制时(非 FREE 版本)应该返回联系管理员提示', async () => { + mockCheckLimit.mockResolvedValue({ allowed: false, current: 100, max: 100 }); + mockGetLicenseType.mockReturnValue('STANDARD'); + + const result = await canCreateUser(); + + expect(result.allowed).toBe(false); + expect(result.message).toContain('联系管理员'); + expect(result.message).toContain('100'); + }); +}); + +describe('canUploadMaterial', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('当未达到限制时应该返回 allowed: true', async () => { + mockCheckLimit.mockResolvedValue({ allowed: true, current: 50, max: 100 }); + + const result = await canUploadMaterial(); + + expect(result.allowed).toBe(true); + expect(result.current).toBe(50); + expect(result.max).toBe(100); + expect(mockCheckLimit).toHaveBeenCalledWith('materials'); + }); + + it('当达到限制时(FREE 版本)应该返回升级提示', async () => { + mockCheckLimit.mockResolvedValue({ allowed: false, current: 100, max: 100 }); + mockGetLicenseType.mockReturnValue('FREE'); + + const result = await canUploadMaterial(); + + expect(result.allowed).toBe(false); + expect(result.message).toContain('免费版'); + expect(result.message).toContain('素材'); + expect(result.message).toContain('升级'); + }); + + it('当达到限制时(非 FREE 版本)应该返回联系管理员提示', async () => { + mockCheckLimit.mockResolvedValue({ allowed: false, current: 1000, max: 1000 }); + mockGetLicenseType.mockReturnValue('PRO'); + + const result = await canUploadMaterial(); + + expect(result.allowed).toBe(false); + expect(result.message).toContain('联系管理员'); + }); +}); + +describe('功能开关检查', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('canUseCustomTemplates', () => { + it('当功能启用时应该返回 true', () => { + mockIsFeatureEnabled.mockReturnValue(true); + expect(canUseCustomTemplates()).toBe(true); + expect(mockIsFeatureEnabled).toHaveBeenCalledWith('customTemplates'); + }); + + it('当功能禁用时应该返回 false', () => { + mockIsFeatureEnabled.mockReturnValue(false); + expect(canUseCustomTemplates()).toBe(false); + }); + }); + + describe('canUseDepartmentIsolation', () => { + it('当功能启用时应该返回 true', () => { + mockIsFeatureEnabled.mockReturnValue(true); + expect(canUseDepartmentIsolation()).toBe(true); + expect(mockIsFeatureEnabled).toHaveBeenCalledWith('departmentIsolation'); + }); + + it('当功能禁用时应该返回 false', () => { + mockIsFeatureEnabled.mockReturnValue(false); + expect(canUseDepartmentIsolation()).toBe(false); + }); + }); + + describe('canUseIntranetPenetration', () => { + it('当功能启用时应该返回 true', () => { + mockIsFeatureEnabled.mockReturnValue(true); + expect(canUseIntranetPenetration()).toBe(true); + expect(mockIsFeatureEnabled).toHaveBeenCalledWith('intranetPenetration'); + }); + + it('当功能禁用时应该返回 false', () => { + mockIsFeatureEnabled.mockReturnValue(false); + expect(canUseIntranetPenetration()).toBe(false); + }); + }); + + describe('canUseSystemSettings', () => { + it('当功能启用时应该返回 true', () => { + mockIsFeatureEnabled.mockReturnValue(true); + expect(canUseSystemSettings()).toBe(true); + expect(mockIsFeatureEnabled).toHaveBeenCalledWith('systemSettings'); + }); + + it('当功能禁用时应该返回 false', () => { + mockIsFeatureEnabled.mockReturnValue(false); + expect(canUseSystemSettings()).toBe(false); + }); + }); + + describe('canUseApiAccess', () => { + it('当功能启用时应该返回 true', () => { + mockIsFeatureEnabled.mockReturnValue(true); + expect(canUseApiAccess()).toBe(true); + expect(mockIsFeatureEnabled).toHaveBeenCalledWith('apiAccess'); + }); + + it('当功能禁用时应该返回 false', () => { + mockIsFeatureEnabled.mockReturnValue(false); + expect(canUseApiAccess()).toBe(false); + }); + }); +}); + +describe('LimitCheckResult 接口', () => { + it('应该正确处理无限制情况 (max = -1)', async () => { + mockCheckLimit.mockResolvedValue({ allowed: true, current: 0, max: -1 }); + + const result = await canCreateUser(); + + expect(result.allowed).toBe(true); + expect(result.max).toBe(-1); + }); +}); diff --git a/lib/__tests__/crypto-service.test.ts b/lib/__tests__/crypto-service.test.ts new file mode 100644 index 0000000..64a2489 --- /dev/null +++ b/lib/__tests__/crypto-service.test.ts @@ -0,0 +1,171 @@ +/** + * 加密服务模块测试 + * 测试 lib/license/crypto-service.ts 中的加密/解密函数 + */ + +import { + encryptLocal, + decryptLocal, + hashData, + generateId, + verifySignature, + verifyLicenseSignature, +} from '../license/crypto-service'; + +describe('本地加密/解密', () => { + describe('encryptLocal', () => { + it('应该成功加密字符串', () => { + const plaintext = 'Hello, World!'; + const encrypted = encryptLocal(plaintext); + + expect(encrypted).toBeDefined(); + expect(encrypted).not.toBe(plaintext); + // 格式应该是 iv:authTag:encrypted + expect(encrypted.split(':').length).toBe(3); + }); + + it('应该对相同输入产生不同的加密结果(因为随机 IV)', () => { + const plaintext = 'Same text'; + const encrypted1 = encryptLocal(plaintext); + const encrypted2 = encryptLocal(plaintext); + + expect(encrypted1).not.toBe(encrypted2); + }); + + it('应该能加密空字符串', () => { + const encrypted = encryptLocal(''); + expect(encrypted).toBeDefined(); + expect(encrypted.split(':').length).toBe(3); + }); + + it('应该能加密中文字符', () => { + const plaintext = '你好,世界!'; + const encrypted = encryptLocal(plaintext); + expect(encrypted).toBeDefined(); + }); + + it('应该能加密 JSON 字符串', () => { + const jsonData = JSON.stringify({ key: 'value', number: 123 }); + const encrypted = encryptLocal(jsonData); + expect(encrypted).toBeDefined(); + }); + }); + + describe('decryptLocal', () => { + it('应该成功解密加密的字符串', () => { + const plaintext = 'Hello, World!'; + const encrypted = encryptLocal(plaintext); + const decrypted = decryptLocal(encrypted); + + expect(decrypted).toBe(plaintext); + }); + + it('应该成功解密中文字符', () => { + const plaintext = '你好,世界!'; + const encrypted = encryptLocal(plaintext); + const decrypted = decryptLocal(encrypted); + + expect(decrypted).toBe(plaintext); + }); + + it('应该成功解密 JSON 字符串', () => { + const jsonData = { key: 'value', number: 123, nested: { a: 1 } }; + const encrypted = encryptLocal(JSON.stringify(jsonData)); + const decrypted = JSON.parse(decryptLocal(encrypted)); + + expect(decrypted).toEqual(jsonData); + }); + + it('应该成功解密空字符串', () => { + const encrypted = encryptLocal(''); + const decrypted = decryptLocal(encrypted); + + expect(decrypted).toBe(''); + }); + + it('解密无效数据时应该抛出错误', () => { + expect(() => decryptLocal('invalid:data:here')).toThrow('数据解密失败'); + }); + + it('解密格式错误的数据时应该抛出错误', () => { + expect(() => decryptLocal('not-valid-format')).toThrow(); + }); + }); +}); + +describe('hashData', () => { + it('应该生成 SHA-256 哈希', () => { + const data = 'test data'; + const hash = hashData(data); + + expect(hash).toBeDefined(); + expect(hash.length).toBe(64); // SHA-256 produces 64 hex characters + }); + + it('相同输入应该产生相同哈希', () => { + const data = 'same data'; + const hash1 = hashData(data); + const hash2 = hashData(data); + + expect(hash1).toBe(hash2); + }); + + it('不同输入应该产生不同哈希', () => { + const hash1 = hashData('data1'); + const hash2 = hashData('data2'); + + expect(hash1).not.toBe(hash2); + }); + + it('应该能哈希空字符串', () => { + const hash = hashData(''); + expect(hash).toBeDefined(); + expect(hash.length).toBe(64); + }); +}); + +describe('generateId', () => { + it('应该生成 32 字符的十六进制 ID', () => { + const id = generateId(); + + expect(id).toBeDefined(); + expect(id.length).toBe(32); + expect(/^[0-9a-f]+$/.test(id)).toBe(true); + }); + + it('应该生成唯一的 ID', () => { + const ids = new Set(); + for (let i = 0; i < 100; i++) { + ids.add(generateId()); + } + + expect(ids.size).toBe(100); + }); +}); + +describe('签名验证', () => { + describe('verifySignature', () => { + it('无效签名应该返回 false', () => { + const data = 'test data'; + const invalidSignature = 'invalid-signature'; + + const result = verifySignature(data, invalidSignature); + expect(result).toBe(false); + }); + + it('空签名应该返回 false', () => { + const result = verifySignature('data', ''); + expect(result).toBe(false); + }); + }); + + describe('verifyLicenseSignature', () => { + it('无效签名应该返回 false', () => { + const licenseData = { type: 'FREE', expiresAt: null }; + const invalidSignature = 'invalid-signature'; + + const result = verifyLicenseSignature(licenseData, invalidSignature); + expect(result).toBe(false); + }); + }); +}); diff --git a/lib/__tests__/license-types.test.ts b/lib/__tests__/license-types.test.ts new file mode 100644 index 0000000..46f4143 --- /dev/null +++ b/lib/__tests__/license-types.test.ts @@ -0,0 +1,200 @@ +/** + * 许可证类型模块测试 + * 测试 lib/license/types.ts 中的类型定义和配置生成函数 + */ + +import { + LicenseType, + LicenseConfig, + LICENSE_CONFIGS, + generateLicenseConfig, + FeatureKey, +} from '../license/types'; + +describe('LICENSE_CONFIGS', () => { + describe('FREE 版本配置', () => { + const freeConfig = LICENSE_CONFIGS.FREE; + + it('应该有正确的类型标识', () => { + expect(freeConfig.type).toBe('FREE'); + }); + + it('应该限制最大用户数为 10', () => { + expect(freeConfig.limits.maxUsers).toBe(10); + }); + + it('应该限制最大素材数为 100', () => { + expect(freeConfig.limits.maxMaterials).toBe(100); + }); + + it('应该禁用所有高级功能', () => { + expect(freeConfig.features.customTemplates).toBe(false); + expect(freeConfig.features.systemSettings).toBe(false); + expect(freeConfig.features.departmentIsolation).toBe(false); + expect(freeConfig.features.apiAccess).toBe(false); + expect(freeConfig.features.intranetPenetration).toBe(false); + }); + }); + + describe('STANDARD 版本配置', () => { + const standardConfig = LICENSE_CONFIGS.STANDARD; + + it('应该有正确的类型标识', () => { + expect(standardConfig.type).toBe('STANDARD'); + }); + + it('应该不限制用户数 (-1)', () => { + expect(standardConfig.limits.maxUsers).toBe(-1); + }); + + it('应该不限制素材数 (-1)', () => { + expect(standardConfig.limits.maxMaterials).toBe(-1); + }); + + it('应该启用大部分高级功能', () => { + expect(standardConfig.features.customTemplates).toBe(true); + expect(standardConfig.features.systemSettings).toBe(true); + expect(standardConfig.features.departmentIsolation).toBe(true); + expect(standardConfig.features.apiAccess).toBe(true); + }); + + it('应该禁用内网穿透功能', () => { + expect(standardConfig.features.intranetPenetration).toBe(false); + }); + }); + + describe('PRO 版本配置', () => { + const proConfig = LICENSE_CONFIGS.PRO; + + it('应该有正确的类型标识', () => { + expect(proConfig.type).toBe('PRO'); + }); + + it('应该不限制用户数 (-1)', () => { + expect(proConfig.limits.maxUsers).toBe(-1); + }); + + it('应该不限制素材数 (-1)', () => { + expect(proConfig.limits.maxMaterials).toBe(-1); + }); + + it('应该启用所有高级功能', () => { + expect(proConfig.features.customTemplates).toBe(true); + expect(proConfig.features.systemSettings).toBe(true); + expect(proConfig.features.departmentIsolation).toBe(true); + expect(proConfig.features.apiAccess).toBe(true); + expect(proConfig.features.intranetPenetration).toBe(true); + }); + }); + + describe('版本功能递进关系', () => { + it('STANDARD 应该比 FREE 有更多功能', () => { + const freeFeatures = Object.values(LICENSE_CONFIGS.FREE.features).filter(Boolean).length; + const standardFeatures = Object.values(LICENSE_CONFIGS.STANDARD.features).filter(Boolean).length; + expect(standardFeatures).toBeGreaterThan(freeFeatures); + }); + + it('PRO 应该比 STANDARD 有更多功能', () => { + const standardFeatures = Object.values(LICENSE_CONFIGS.STANDARD.features).filter(Boolean).length; + const proFeatures = Object.values(LICENSE_CONFIGS.PRO.features).filter(Boolean).length; + expect(proFeatures).toBeGreaterThanOrEqual(standardFeatures); + }); + }); +}); + +describe('generateLicenseConfig', () => { + describe('基本功能', () => { + it('应该生成 FREE 版本配置', () => { + const config = generateLicenseConfig('FREE'); + expect(config.type).toBe('FREE'); + expect(config.limits.maxUsers).toBe(10); + expect(config.limits.maxMaterials).toBe(100); + }); + + it('应该生成 STANDARD 版本配置', () => { + const config = generateLicenseConfig('STANDARD'); + expect(config.type).toBe('STANDARD'); + expect(config.limits.maxUsers).toBe(-1); + }); + + it('应该生成 PRO 版本配置', () => { + const config = generateLicenseConfig('PRO'); + expect(config.type).toBe('PRO'); + expect(config.features.intranetPenetration).toBe(true); + }); + }); + + describe('过期时间处理', () => { + it('不传过期时间时应该返回 null', () => { + const config = generateLicenseConfig('FREE'); + expect(config.expiresAt).toBeNull(); + }); + + it('传入 null 时应该返回 null', () => { + const config = generateLicenseConfig('FREE', null); + expect(config.expiresAt).toBeNull(); + }); + + it('传入 Date 对象时应该返回 ISO 字符串', () => { + const expiresAt = new Date('2025-12-31T23:59:59.000Z'); + const config = generateLicenseConfig('STANDARD', expiresAt); + expect(config.expiresAt).toBe('2025-12-31T23:59:59.000Z'); + }); + + it('应该正确处理不同时区的日期', () => { + const expiresAt = new Date('2026-06-15T12:00:00.000Z'); + const config = generateLicenseConfig('PRO', expiresAt); + expect(config.expiresAt).toContain('2026-06-15'); + }); + }); + + describe('返回值结构', () => { + it('应该包含所有必需字段', () => { + const config = generateLicenseConfig('STANDARD', new Date()); + + expect(config).toHaveProperty('type'); + expect(config).toHaveProperty('limits'); + expect(config).toHaveProperty('limits.maxUsers'); + expect(config).toHaveProperty('limits.maxMaterials'); + expect(config).toHaveProperty('features'); + expect(config).toHaveProperty('features.customTemplates'); + expect(config).toHaveProperty('features.systemSettings'); + expect(config).toHaveProperty('features.departmentIsolation'); + expect(config).toHaveProperty('features.apiAccess'); + expect(config).toHaveProperty('features.intranetPenetration'); + expect(config).toHaveProperty('expiresAt'); + }); + + it('返回的配置应该是新对象,不影响原始配置', () => { + const config1 = generateLicenseConfig('FREE', new Date('2025-01-01')); + const config2 = generateLicenseConfig('FREE', new Date('2026-01-01')); + + expect(config1.expiresAt).not.toBe(config2.expiresAt); + expect(LICENSE_CONFIGS.FREE).not.toHaveProperty('expiresAt'); + }); + }); +}); + +describe('类型定义完整性', () => { + it('LicenseType 应该包含三种类型', () => { + const types: LicenseType[] = ['FREE', 'STANDARD', 'PRO']; + types.forEach(type => { + expect(LICENSE_CONFIGS[type]).toBeDefined(); + }); + }); + + it('FeatureKey 应该能正确索引功能', () => { + const featureKeys: FeatureKey[] = [ + 'customTemplates', + 'systemSettings', + 'departmentIsolation', + 'apiAccess', + 'intranetPenetration', + ]; + + const config = generateLicenseConfig('PRO'); + featureKeys.forEach(key => { + expect(typeof config.features[key]).toBe('boolean'); + }); + }); +}); diff --git a/lib/license/check-limits.ts b/lib/license/check-limits.ts new file mode 100644 index 0000000..beac9a8 --- /dev/null +++ b/lib/license/check-limits.ts @@ -0,0 +1,88 @@ +/** + * 许可证限制检查工具 + * 用于在各个 API 中检查是否达到许可证限制 + */ + +import { checkLimit, isFeatureEnabled, getLicenseType } from './license-service'; + +export interface LimitCheckResult { + allowed: boolean; + current: number; + max: number; + message?: string; +} + +/** + * 检查是否可以创建新用户 + */ +export async function canCreateUser(): Promise { + const result = await checkLimit('users'); + + if (!result.allowed) { + const licenseType = getLicenseType(); + return { + ...result, + message: + licenseType === 'FREE' + ? `免费版最多支持 ${result.max} 个用户,当前已有 ${result.current} 个用户。请升级到标准版或专业版以解除限制。` + : `已达到用户数量上限(${result.max}),请联系管理员。`, + }; + } + + return result; +} + +/** + * 检查是否可以上传新素材 + */ +export async function canUploadMaterial(): Promise { + const result = await checkLimit('materials'); + + if (!result.allowed) { + const licenseType = getLicenseType(); + return { + ...result, + message: + licenseType === 'FREE' + ? `免费版最多支持 ${result.max} 个素材,当前已有 ${result.current} 个素材。请升级到标准版或专业版以解除限制。` + : `已达到素材数量上限(${result.max}),请联系管理员。`, + }; + } + + return result; +} + +/** + * 检查自定义模板功能是否可用 + */ +export function canUseCustomTemplates(): boolean { + return isFeatureEnabled('customTemplates'); +} + +/** + * 检查部门隔离功能是否可用 + */ +export function canUseDepartmentIsolation(): boolean { + return isFeatureEnabled('departmentIsolation'); +} + +/** + * 检查内网穿透功能是否可用 + */ +export function canUseIntranetPenetration(): boolean { + return isFeatureEnabled('intranetPenetration'); +} + +/** + * 检查系统设置功能是否可用 + */ +export function canUseSystemSettings(): boolean { + return isFeatureEnabled('systemSettings'); +} + +/** + * 检查 API 访问功能是否可用 + */ +export function canUseApiAccess(): boolean { + return isFeatureEnabled('apiAccess'); +} diff --git a/lib/license/crypto-service.ts b/lib/license/crypto-service.ts new file mode 100644 index 0000000..1bc91e1 --- /dev/null +++ b/lib/license/crypto-service.ts @@ -0,0 +1,93 @@ +import * as crypto from 'crypto'; + +// RSA 公钥(从许可证服务器获取,嵌入客户端) +const RSA_PUBLIC_KEY = process.env.LICENSE_PUBLIC_KEY?.replace(/\\n/g, '\n') || `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQNKx95FWT0XKXcskHhv +MmBluxHYpmPfKSPETyCO1cMDhf1TuFYggwP51b2BkkAiCS1sD3Hswb9VEzEJwbG/ +nRF1cBNAGBLcDudnAWp5y8GTVr6voEEgsk88o0xVeQdNWwi3016XffB47Q235ixb +mr6cb0q9FEMMEQULKY9RFRcpVc2GvJup+c6/wIVV8Z0d7pGn+mD2eF+mucg+N+tJ +ocUfjgf9aE3dGitGYmA4xMU5OMEYXhydhNkOA+5YpHb7yrwQNuLAileGiq6aMp0i +RmWy9Pty+rygNRUK9kFDn0Fw4oZBNoVBkVebTqeeSZmrM8CJtE3KK0BrC5/TpFon +QQIDAQAB +-----END PUBLIC KEY-----`; + +// 本地加密密钥(用于加密本地存储的数据) +const LOCAL_ENCRYPTION_KEY = process.env.LOCAL_ENCRYPTION_KEY || 'flnxi-local-encryption-key-32bytes!'; + +/** + * 使用 RSA 公钥验证签名 + */ +export function verifySignature(data: string, signature: string): boolean { + try { + const verify = crypto.createVerify('RSA-SHA256'); + verify.update(data); + verify.end(); + return verify.verify(RSA_PUBLIC_KEY, signature, 'base64'); + } catch (error) { + console.error('签名验证失败:', error); + return false; + } +} + +/** + * 验证签名后的许可证数据 + */ +export function verifyLicenseSignature(licenseData: object, signature: string): boolean { + const dataString = JSON.stringify(licenseData); + return verifySignature(dataString, signature); +} + +/** + * 使用 AES-256-GCM 加密数据(用于本地存储) + */ +export function encryptLocal(plaintext: string): string { + const iv = crypto.randomBytes(12); + const key = crypto.scryptSync(LOCAL_ENCRYPTION_KEY, 'salt', 32); + const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); + + let encrypted = cipher.update(plaintext, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + + const authTag = cipher.getAuthTag(); + + // 格式: iv:authTag:encrypted + return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`; +} + +/** + * 使用 AES-256-GCM 解密数据(用于本地存储) + */ +export function decryptLocal(ciphertext: string): string { + try { + const [ivBase64, authTagBase64, encrypted] = ciphertext.split(':'); + + const iv = Buffer.from(ivBase64, 'base64'); + const authTag = Buffer.from(authTagBase64, 'base64'); + const key = crypto.scryptSync(LOCAL_ENCRYPTION_KEY, 'salt', 32); + + const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } catch (error) { + console.error('解密失败:', error); + throw new Error('数据解密失败'); + } +} + +/** + * 计算数据哈希 + */ +export function hashData(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex'); +} + +/** + * 生成随机 ID + */ +export function generateId(): string { + return crypto.randomBytes(16).toString('hex'); +} diff --git a/lib/license/fingerprint-service.ts b/lib/license/fingerprint-service.ts new file mode 100644 index 0000000..9eb4262 --- /dev/null +++ b/lib/license/fingerprint-service.ts @@ -0,0 +1,191 @@ +import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as crypto from 'crypto'; +import type { HardwareFingerprint } from './types'; + +// 实例 ID 存储路径 +const INSTANCE_ID_FILE = path.join(process.cwd(), 'data', '.instance-id'); + +/** + * 获取 machine-id + * Linux: /etc/machine-id 或 /var/lib/dbus/machine-id + * Windows: 使用 hostname + 网卡信息生成 + * Docker: 使用容器 ID 或挂载的 machine-id + */ +async function getMachineId(): Promise { + // 尝试读取 Linux machine-id + const machineIdPaths = [ + '/etc/machine-id', + '/var/lib/dbus/machine-id', + ]; + + for (const filePath of machineIdPaths) { + try { + const machineId = fs.readFileSync(filePath, 'utf8').trim(); + if (machineId) { + return machineId; + } + } catch { + // 文件不存在,继续尝试下一个 + } + } + + // 尝试读取 Docker 容器 ID + try { + const cgroupContent = fs.readFileSync('/proc/self/cgroup', 'utf8'); + const match = cgroupContent.match(/docker[/-]([a-f0-9]{64})/); + if (match) { + return match[1].substring(0, 32); + } + } catch { + // 不在 Docker 容器中 + } + + // Windows 或其他系统:使用 hostname + 网卡信息生成 + const hostname = os.hostname(); + const networkInterfaces = os.networkInterfaces(); + const macs: string[] = []; + + for (const interfaces of Object.values(networkInterfaces)) { + if (interfaces) { + for (const iface of interfaces) { + if (!iface.internal && iface.mac !== '00:00:00:00:00:00') { + macs.push(iface.mac); + } + } + } + } + + const data = `${hostname}-${macs.sort().join('-')}`; + return crypto.createHash('md5').update(data).digest('hex'); +} + +/** + * 获取主网卡 MAC 地址 + */ +function getPrimaryMacAddress(): string { + const networkInterfaces = os.networkInterfaces(); + + // 优先查找常见的网卡名称 + const preferredNames = ['eth0', 'en0', 'ens33', 'enp0s3', 'Ethernet', 'Wi-Fi']; + + for (const name of preferredNames) { + const interfaces = networkInterfaces[name]; + if (interfaces) { + for (const iface of interfaces) { + if (!iface.internal && iface.mac !== '00:00:00:00:00:00') { + return iface.mac; + } + } + } + } + + // 如果没有找到,返回第一个非内部网卡的 MAC + for (const interfaces of Object.values(networkInterfaces)) { + if (interfaces) { + for (const iface of interfaces) { + if (!iface.internal && iface.mac !== '00:00:00:00:00:00') { + return iface.mac; + } + } + } + } + + return '00:00:00:00:00:00'; +} + +/** + * 获取或创建实例 ID + * 实例 ID 在首次激活时生成,并持久化存储 + */ +async function getOrCreateInstanceId(): Promise { + // 确保目录存在 + const dir = path.dirname(INSTANCE_ID_FILE); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // 尝试读取现有的实例 ID + try { + const instanceId = fs.readFileSync(INSTANCE_ID_FILE, 'utf8').trim(); + if (instanceId) { + return instanceId; + } + } catch { + // 文件不存在 + } + + // 生成新的实例 ID + const instanceId = crypto.randomBytes(16).toString('hex'); + fs.writeFileSync(INSTANCE_ID_FILE, instanceId); + + return instanceId; +} + +/** + * 生成硬件指纹 + */ +export async function generateFingerprint(): Promise { + const [machineId, instanceId] = await Promise.all([ + getMachineId(), + getOrCreateInstanceId(), + ]); + + return { + machineId, + hostname: os.hostname(), + macAddress: getPrimaryMacAddress(), + instanceId, + }; +} + +/** + * 获取实例 ID + */ +export async function getInstanceId(): Promise { + return getOrCreateInstanceId(); +} + +/** + * 计算两个指纹的匹配分数 + * 返回 0-100 的分数 + */ +export function calculateFingerprintMatchScore( + stored: HardwareFingerprint, + current: HardwareFingerprint +): number { + let score = 0; + + // machineId 权重 40% + if (stored.machineId === current.machineId) { + score += 40; + } + + // hostname 权重 20% + if (stored.hostname === current.hostname) { + score += 20; + } + + // macAddress 权重 30% + if (stored.macAddress === current.macAddress) { + score += 30; + } + + // instanceId 权重 10% + if (stored.instanceId === current.instanceId) { + score += 10; + } + + return score; +} + +/** + * 检查指纹是否匹配(60% 阈值) + */ +export function isFingerprintMatch( + stored: HardwareFingerprint, + current: HardwareFingerprint +): boolean { + return calculateFingerprintMatchScore(stored, current) >= 60; +} diff --git a/lib/license/index.ts b/lib/license/index.ts new file mode 100644 index 0000000..3567cb8 --- /dev/null +++ b/lib/license/index.ts @@ -0,0 +1,47 @@ +// 导出类型 +export * from './types'; + +// 导出服务 +export { + activate, + activateFreeTrial, + verify, + heartbeat, + getLicense, + isFeatureEnabled, + checkLimit, + getLicenseType, + isExpired, + startHeartbeat, + stopHeartbeat, + initLicenseService, +} from './license-service'; + +// 导出限制检查工具 +export { + canCreateUser, + canUploadMaterial, + canUseCustomTemplates, + canUseDepartmentIsolation, + canUseIntranetPenetration, + canUseSystemSettings, + canUseApiAccess, +} from './check-limits'; + +// 导出指纹服务 +export { + generateFingerprint, + getInstanceId, + calculateFingerprintMatchScore, + isFingerprintMatch, +} from './fingerprint-service'; + +// 导出加密服务 +export { + verifySignature, + verifyLicenseSignature, + encryptLocal, + decryptLocal, + hashData, + generateId, +} from './crypto-service'; diff --git a/lib/license/license-service.ts b/lib/license/license-service.ts new file mode 100644 index 0000000..9974867 --- /dev/null +++ b/lib/license/license-service.ts @@ -0,0 +1,370 @@ +import { prisma } from '@/lib/prisma'; +import { generateFingerprint, getInstanceId } from './fingerprint-service'; +import { verifyLicenseSignature, encryptLocal, decryptLocal } from './crypto-service'; +import type { + LicenseConfig, + SignedLicense, + ActivateResponse, + VerifyResponse, + HeartbeatResponse, + FeatureKey, + LocalLicenseData, +} from './types'; +import { LICENSE_CONFIGS } from './types'; + +// 许可证服务器地址 +const LICENSE_SERVER_URL = process.env.LICENSE_SERVER_URL || 'http://45.249.208.118:3001'; + +// 宽限期(7天) +const GRACE_PERIOD_DAYS = 7; + +// 心跳间隔(1小时) +const HEARTBEAT_INTERVAL = 60 * 60 * 1000; + +// 缓存的许可证配置 +let cachedLicense: LicenseConfig | null = null; +let lastVerified: Date | null = null; +let heartbeatTimer: NodeJS.Timeout | null = null; + +/** + * 激活许可证 + */ +export async function activate(licenseKey: string): Promise<{ success: boolean; error?: string }> { + try { + const fingerprint = await generateFingerprint(); + + const response = await fetch(`${LICENSE_SERVER_URL}/api/license/activate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + licenseKey, + fingerprint, + timestamp: Date.now(), + }), + }); + + const data: ActivateResponse = await response.json(); + + if (!data.success || !data.license) { + return { success: false, error: data.error || '激活失败' }; + } + + // 验证签名 + if (!verifyLicenseSignature(data.license.data, data.license.signature)) { + return { success: false, error: '许可证签名验证失败' }; + } + + // 保存到本地 + await saveLicenseLocally(licenseKey, data.license); + + // 更新缓存 + cachedLicense = data.license.data; + lastVerified = new Date(); + + // 启动心跳 + startHeartbeat(); + + return { success: true }; + } catch (error) { + console.error('激活失败:', error); + return { success: false, error: '网络错误,请检查网络连接' }; + } +} + +/** + * 激活免费试用版 + */ +export async function activateFreeTrial(): Promise<{ success: boolean; error?: string }> { + try { + const instanceId = await getInstanceId(); + + // 创建免费版配置 + const freeConfig: LicenseConfig = { + ...LICENSE_CONFIGS.FREE, + expiresAt: null, + }; + + // 保存到本地(无需服务器验证) + const localData: LocalLicenseData = { + instanceId, + licenseKey: 'FREE-TRIAL', + licenseData: encryptLocal(JSON.stringify(freeConfig)), + lastVerified: new Date().toISOString(), + cachedConfig: freeConfig, + }; + + await prisma.systemConfig.upsert({ + where: { key: 'license' }, + update: { value: JSON.stringify(localData) }, + create: { key: 'license', value: JSON.stringify(localData) }, + }); + + // 更新缓存 + cachedLicense = freeConfig; + lastVerified = new Date(); + + return { success: true }; + } catch (error) { + console.error('免费试用激活失败:', error); + return { success: false, error: '激活失败' }; + } +} + +/** + * 验证许可证 + */ +export async function verify(): Promise<{ valid: boolean; error?: string }> { + try { + // 先检查本地缓存 + const localLicense = await getLocalLicense(); + + if (!localLicense) { + return { valid: false, error: '未找到许可证' }; + } + + // 免费试用版不需要在线验证 + if (localLicense.licenseKey === 'FREE-TRIAL') { + cachedLicense = localLicense.cachedConfig; + lastVerified = new Date(); + return { valid: true }; + } + + const fingerprint = await generateFingerprint(); + + const response = await fetch(`${LICENSE_SERVER_URL}/api/license/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + instanceId: fingerprint.instanceId, + fingerprint, + timestamp: Date.now(), + }), + }); + + const data: VerifyResponse = await response.json(); + + if (!data.valid || !data.license) { + // 检查宽限期 + if (isWithinGracePeriod(localLicense.lastVerified)) { + cachedLicense = localLicense.cachedConfig; + return { valid: true }; + } + return { valid: false, error: data.error || '验证失败' }; + } + + // 验证签名 + if (!verifyLicenseSignature(data.license.data, data.license.signature)) { + return { valid: false, error: '许可证签名验证失败' }; + } + + // 更新本地缓存 + await updateLocalLicense(data.license); + + // 更新内存缓存 + cachedLicense = data.license.data; + lastVerified = new Date(); + + return { valid: true }; + } catch (error) { + console.error('验证失败:', error); + + // 网络错误时检查宽限期 + const localLicense = await getLocalLicense(); + if (localLicense && isWithinGracePeriod(localLicense.lastVerified)) { + cachedLicense = localLicense.cachedConfig; + return { valid: true }; + } + + return { valid: false, error: '网络错误' }; + } +} + +/** + * 发送心跳 + */ +export async function heartbeat(): Promise { + try { + const localLicense = await getLocalLicense(); + if (!localLicense || localLicense.licenseKey === 'FREE-TRIAL') { + return; + } + + const fingerprint = await generateFingerprint(); + + // 获取使用统计 + const [userCount, materialCount] = await Promise.all([ + prisma.user.count(), + prisma.material.count(), + ]); + + const response = await fetch(`${LICENSE_SERVER_URL}/api/license/heartbeat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + instanceId: fingerprint.instanceId, + fingerprint, + usage: { userCount, materialCount }, + timestamp: Date.now(), + }), + }); + + const data: HeartbeatResponse = await response.json(); + + if (data.acknowledged && data.license) { + // 更新本地缓存 + await updateLocalLicense(data.license); + cachedLicense = data.license.data; + } + + lastVerified = new Date(); + } catch (error) { + console.error('心跳发送失败:', error); + } +} + +/** + * 获取当前许可证配置 + */ +export function getLicense(): LicenseConfig | null { + return cachedLicense; +} + +/** + * 检查功能是否可用 + */ +export function isFeatureEnabled(feature: FeatureKey): boolean { + if (!cachedLicense) { + return false; + } + return cachedLicense.features[feature]; +} + +/** + * 检查是否达到限制 + */ +export async function checkLimit(type: 'users' | 'materials'): Promise<{ allowed: boolean; current: number; max: number }> { + if (!cachedLicense) { + return { allowed: false, current: 0, max: 0 }; + } + + const max = type === 'users' ? cachedLicense.limits.maxUsers : cachedLicense.limits.maxMaterials; + + // -1 表示无限制 + if (max === -1) { + return { allowed: true, current: 0, max: -1 }; + } + + const current = type === 'users' + ? await prisma.user.count() + : await prisma.material.count(); + + return { + allowed: current < max, + current, + max, + }; +} + +/** + * 获取许可证类型 + */ +export function getLicenseType(): string { + return cachedLicense?.type || 'FREE'; +} + +/** + * 检查许可证是否已过期 + */ +export function isExpired(): boolean { + if (!cachedLicense || !cachedLicense.expiresAt) { + return false; + } + return new Date(cachedLicense.expiresAt) < new Date(); +} + +/** + * 启动心跳定时器 + */ +export function startHeartbeat(): void { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + } + heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL); +} + +/** + * 停止心跳定时器 + */ +export function stopHeartbeat(): void { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = null; + } +} + +/** + * 初始化许可证服务 + */ +export async function initLicenseService(): Promise { + const result = await verify(); + if (result.valid) { + startHeartbeat(); + } +} + +// ============ 内部辅助函数 ============ + +async function saveLicenseLocally(licenseKey: string, license: SignedLicense): Promise { + const instanceId = await getInstanceId(); + + const localData: LocalLicenseData = { + instanceId, + licenseKey, + licenseData: encryptLocal(JSON.stringify(license)), + lastVerified: new Date().toISOString(), + cachedConfig: license.data, + }; + + await prisma.systemConfig.upsert({ + where: { key: 'license' }, + update: { value: JSON.stringify(localData) }, + create: { key: 'license', value: JSON.stringify(localData) }, + }); +} + +async function updateLocalLicense(license: SignedLicense): Promise { + const existing = await getLocalLicense(); + if (!existing) return; + + const localData: LocalLicenseData = { + ...existing, + licenseData: encryptLocal(JSON.stringify(license)), + lastVerified: new Date().toISOString(), + cachedConfig: license.data, + }; + + await prisma.systemConfig.update({ + where: { key: 'license' }, + data: { value: JSON.stringify(localData) }, + }); +} + +async function getLocalLicense(): Promise { + try { + const config = await prisma.systemConfig.findUnique({ + where: { key: 'license' }, + }); + + if (!config) return null; + + return JSON.parse(config.value) as LocalLicenseData; + } catch { + return null; + } +} + +function isWithinGracePeriod(lastVerifiedStr: string): boolean { + const lastVerified = new Date(lastVerifiedStr); + const gracePeriodEnd = new Date(lastVerified.getTime() + GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1000); + return new Date() < gracePeriodEnd; +} diff --git a/lib/license/types.ts b/lib/license/types.ts new file mode 100644 index 0000000..44aec25 --- /dev/null +++ b/lib/license/types.ts @@ -0,0 +1,152 @@ +// 许可证类型 +export type LicenseType = 'FREE' | 'STANDARD' | 'PRO'; + +// 许可证配置 +export interface LicenseConfig { + type: LicenseType; + limits: { + maxUsers: number; // -1 表示无限 + maxMaterials: number; // -1 表示无限 + }; + features: { + customTemplates: boolean; + systemSettings: boolean; + departmentIsolation: boolean; + apiAccess: boolean; + intranetPenetration: boolean; + }; + expiresAt: string | null; // ISO 日期字符串,null 表示永久 +} + +// 签名后的许可证 +export interface SignedLicense { + data: LicenseConfig & { issuedAt: string }; + signature: string; + issuedAt: string; +} + +// 硬件指纹 +export interface HardwareFingerprint { + machineId: string; + hostname: string; + macAddress: string; + cpuId?: string; + instanceId: string; +} + +// 激活请求 +export interface ActivateRequest { + licenseKey: string; + fingerprint: HardwareFingerprint; + timestamp: number; +} + +// 激活响应 +export interface ActivateResponse { + success: boolean; + license?: SignedLicense; + error?: string; +} + +// 验证请求 +export interface VerifyRequest { + instanceId: string; + fingerprint: HardwareFingerprint; + timestamp: number; +} + +// 验证响应 +export interface VerifyResponse { + valid: boolean; + license?: SignedLicense; + error?: string; +} + +// 心跳请求 +export interface HeartbeatRequest { + instanceId: string; + fingerprint: HardwareFingerprint; + usage: { + userCount: number; + materialCount: number; + }; + timestamp: number; +} + +// 心跳响应 +export interface HeartbeatResponse { + acknowledged: boolean; + license?: SignedLicense; +} + +// 本地存储的许可证数据 +export interface LocalLicenseData { + instanceId: string; + licenseKey: string; + licenseData: string; // 加密存储的签名许可证 + lastVerified: string; // ISO 日期字符串 + cachedConfig: LicenseConfig; +} + +// 功能键 +export type FeatureKey = keyof LicenseConfig['features']; + +// 各版本的默认配置 +export const LICENSE_CONFIGS: Record> = { + FREE: { + type: 'FREE', + limits: { + maxUsers: 10, + maxMaterials: 100, + }, + features: { + customTemplates: false, + systemSettings: false, + departmentIsolation: false, + apiAccess: false, + intranetPenetration: false, + }, + }, + STANDARD: { + type: 'STANDARD', + limits: { + maxUsers: -1, + maxMaterials: -1, + }, + features: { + customTemplates: true, + systemSettings: true, + departmentIsolation: true, + apiAccess: true, + intranetPenetration: false, + }, + }, + PRO: { + type: 'PRO', + limits: { + maxUsers: -1, + maxMaterials: -1, + }, + features: { + customTemplates: true, + systemSettings: true, + departmentIsolation: true, + apiAccess: true, + intranetPenetration: true, + }, + }, +}; + +/** + * 根据许可证类型生成配置 + */ +export function generateLicenseConfig( + type: LicenseType, + expiresAt?: Date | null +): LicenseConfig { + const baseConfig = LICENSE_CONFIGS[type]; + return { + ...baseConfig, + expiresAt: expiresAt ? expiresAt.toISOString() : null, + }; +} diff --git a/license-server/.env.example b/license-server/.env.example new file mode 100644 index 0000000..dfa16db --- /dev/null +++ b/license-server/.env.example @@ -0,0 +1,15 @@ +# 数据库连接 +DATABASE_URL="postgresql://postgres:password@localhost:5432/license_server?schema=public" + +# RSA 密钥(生产环境请使用 generate-keys 脚本生成) +RSA_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" +RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" + +# AES 加密密钥(32字节 hex) +AES_SECRET_KEY="your-32-byte-hex-key-here-12345678901234567890" + +# JWT 密钥 +JWT_SECRET="your-jwt-secret-key" + +# 管理员初始密码 +ADMIN_INITIAL_PASSWORD="admin123" diff --git a/license-server/.gitignore b/license-server/.gitignore new file mode 100644 index 0000000..7ce8a51 --- /dev/null +++ b/license-server/.gitignore @@ -0,0 +1,41 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# prisma +prisma/migrations + +# keys +keys/ +*.key diff --git a/license-server/Dockerfile b/license-server/Dockerfile new file mode 100644 index 0000000..14bfe7b --- /dev/null +++ b/license-server/Dockerfile @@ -0,0 +1,49 @@ +# 构建阶段 - 使用 Debian slim +FROM node:20-slim AS builder +WORKDIR /app + +# 使用阿里云镜像源 +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \ + sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list + +# 安装 OpenSSL +RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +# 安装依赖(使用淘宝镜像) +COPY package*.json ./ +RUN npm config set registry https://registry.npmmirror.com && \ + npm install --legacy-peer-deps + +# 复制源码 +COPY . . + +# 生成 Prisma Client +RUN node node_modules/prisma/build/index.js generate + +# 构建(直接用 node 执行 next) +RUN node node_modules/next/dist/bin/next build + +# 生产阶段 +FROM node:20-slim AS runner +WORKDIR /app + +# 使用阿里云镜像源 +RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \ + sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list + +# 安装 OpenSSL +RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* + +ENV NODE_ENV=production + +# 复制必要文件 +COPY --from=builder /app/next.config.js ./ +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/license-server/app/admin/alerts/page.tsx b/license-server/app/admin/alerts/page.tsx new file mode 100644 index 0000000..110dc93 --- /dev/null +++ b/license-server/app/admin/alerts/page.tsx @@ -0,0 +1,203 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface Alert { + id: string; + alertType: string; + status: string; + details: Record | null; + createdAt: string; + resolvedAt: string | null; + license: { + licenseKey: string; + customer: { companyName: string } | null; + }; +} + +export default function AlertsPage() { + const router = useRouter(); + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState<'all' | 'pending' | 'resolved'>('pending'); + + useEffect(() => { + fetchAlerts(); + }, [filter]); + + const fetchAlerts = async () => { + try { + const res = await fetch(`/api/admin/alerts?status=${filter}`); + if (res.status === 401) { + router.push('/admin/login'); + return; + } + const data = await res.json(); + setAlerts(data.alerts || []); + } catch (error) { + console.error('获取告警列表失败:', error); + } finally { + setLoading(false); + } + }; + + const handleResolve = async (id: string) => { + try { + const res = await fetch(`/api/admin/alerts/${id}/resolve`, { method: 'POST' }); + if (res.ok) { + fetchAlerts(); + } + } catch (error) { + console.error('处理告警失败:', error); + } + }; + + const handleIgnore = async (id: string) => { + try { + const res = await fetch(`/api/admin/alerts/${id}/ignore`, { method: 'POST' }); + if (res.ok) { + fetchAlerts(); + } + } catch (error) { + console.error('忽略告警失败:', error); + } + }; + + const getAlertTypeLabel = (type: string) => { + const labels: Record = { + multi_device: '多设备激活', + fingerprint_mismatch: '指纹不匹配', + suspicious_activity: '可疑活动', + }; + return labels[type] || type; + }; + + return ( +
+ + +
+
+
+
+ + + +
+
+ + {loading ? ( +
加载中...
+ ) : ( +
+ {alerts.map((alert) => ( +
+
+
+
+ + {alert.status === 'pending' + ? '待处理' + : alert.status === 'resolved' + ? '已处理' + : '已忽略'} + + {getAlertTypeLabel(alert.alertType)} +
+

+ 许可证: {alert.license.licenseKey} +

+ {alert.license.customer && ( +

+ 客户: {alert.license.customer.companyName} +

+ )} +

+ 时间: {new Date(alert.createdAt).toLocaleString()} +

+ {alert.details && ( +
+ + 查看详情 + +
+                            {JSON.stringify(alert.details, null, 2)}
+                          
+
+ )} +
+ {alert.status === 'pending' && ( +
+ + +
+ )} +
+
+ ))} + {!alerts.length && ( +
暂无告警
+ )} +
+ )} +
+
+
+ ); +} diff --git a/license-server/app/admin/customers/page.tsx b/license-server/app/admin/customers/page.tsx new file mode 100644 index 0000000..93032a5 --- /dev/null +++ b/license-server/app/admin/customers/page.tsx @@ -0,0 +1,276 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface Customer { + id: string; + companyName: string; + contactName: string | null; + contactEmail: string | null; + contactPhone: string | null; + createdAt: string; + _count: { licenses: number }; +} + +export default function CustomersPage() { + const router = useRouter(); + const [customers, setCustomers] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + fetchCustomers(); + }, []); + + const fetchCustomers = async () => { + try { + const res = await fetch('/api/admin/customers'); + if (res.status === 401) { + router.push('/admin/login'); + return; + } + const data = await res.json(); + setCustomers(data.customers || []); + } catch (error) { + console.error('获取客户列表失败:', error); + } finally { + setLoading(false); + } + }; + + const filteredCustomers = customers.filter( + (c) => + c.companyName.toLowerCase().includes(search.toLowerCase()) || + c.contactName?.toLowerCase().includes(search.toLowerCase()) || + c.contactEmail?.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+ + +
+
+
+ setSearch(e.target.value)} + className="px-4 py-2 border rounded-lg w-64" + /> + +
+ + {loading ? ( +
加载中...
+ ) : ( + + + + + + + + + + + + + {filteredCustomers.map((customer) => ( + + + + + + + + + ))} + {!filteredCustomers.length && ( + + + + )} + +
+ 公司名称 + + 联系人 + + 邮箱 + + 许可证数 + + 创建时间 + + 操作 +
{customer.companyName}{customer.contactName || '-'}{customer.contactEmail || '-'}{customer._count.licenses} + {new Date(customer.createdAt).toLocaleDateString()} + + + 查看 + +
+ 暂无客户数据 +
+ )} +
+
+ + {showModal && ( + setShowModal(false)} + onSuccess={() => { + setShowModal(false); + fetchCustomers(); + }} + /> + )} +
+ ); +} + +function CustomerModal({ + onClose, + onSuccess, +}: { + onClose: () => void; + onSuccess: () => void; +}) { + const [form, setForm] = useState({ + companyName: '', + contactName: '', + contactEmail: '', + contactPhone: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const res = await fetch('/api/admin/customers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + + const data = await res.json(); + if (data.success) { + onSuccess(); + } else { + setError(data.error || '创建失败'); + } + } catch { + setError('网络错误'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

添加客户

+
+ +
+
+ + setForm({ ...form, companyName: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + required + /> +
+ +
+ + setForm({ ...form, contactName: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ +
+ + setForm({ ...form, contactEmail: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ +
+ + setForm({ ...form, contactPhone: e.target.value })} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ + {error && ( +
{error}
+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/license-server/app/admin/licenses/page.tsx b/license-server/app/admin/licenses/page.tsx new file mode 100644 index 0000000..1ce7e39 --- /dev/null +++ b/license-server/app/admin/licenses/page.tsx @@ -0,0 +1,398 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface License { + id: string; + licenseKey: string; + licenseType: string; + status: string; + expiresAt: string | null; + createdAt: string; + customer: { companyName: string } | null; + _count: { activations: number }; +} + +interface Customer { + id: string; + companyName: string; +} + +export default function LicensesPage() { + const router = useRouter(); + const [licenses, setLicenses] = useState([]); + const [customers, setCustomers] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const [licensesRes, customersRes] = await Promise.all([ + fetch('/api/admin/licenses'), + fetch('/api/admin/customers'), + ]); + + if (licensesRes.status === 401) { + router.push('/admin/login'); + return; + } + + const licensesData = await licensesRes.json(); + const customersData = await customersRes.json(); + + setLicenses(licensesData.licenses || []); + setCustomers(customersData.customers || []); + } catch (error) { + console.error('获取数据失败:', error); + } finally { + setLoading(false); + } + }; + + const filteredLicenses = licenses.filter( + (l) => + l.licenseKey.toLowerCase().includes(search.toLowerCase()) || + l.customer?.companyName.toLowerCase().includes(search.toLowerCase()) + ); + + const handleDisable = async (id: string) => { + if (!confirm('确定要禁用此许可证吗?')) return; + + try { + const res = await fetch(`/api/admin/licenses/${id}/disable`, { method: 'POST' }); + if (res.ok) { + fetchData(); + } + } catch (error) { + console.error('禁用失败:', error); + } + }; + + const handleReset = async (id: string) => { + if (!confirm('确定要重置硬件绑定吗?这将允许在新设备上激活。')) return; + + try { + const res = await fetch(`/api/admin/licenses/${id}/reset`, { method: 'POST' }); + if (res.ok) { + fetchData(); + } + } catch (error) { + console.error('重置失败:', error); + } + }; + + return ( +
+ + +
+
+
+ setSearch(e.target.value)} + className="px-4 py-2 border rounded-lg w-64" + /> + +
+ + {loading ? ( +
加载中...
+ ) : ( +
+ + + + + + + + + + + + + + {filteredLicenses.map((license) => ( + + + + + + + + + + ))} + {!filteredLicenses.length && ( + + + + )} + +
+ 许可证密钥 + + 类型 + + 状态 + + 客户 + + 激活数 + + 到期时间 + + 操作 +
{license.licenseKey} + + + + + {license.customer?.companyName || '-'} + {license._count.activations} + {license.expiresAt + ? new Date(license.expiresAt).toLocaleDateString() + : '永久'} + +
+ {license.status === 'active' && ( + + )} + +
+
+ 暂无许可证数据 +
+
+ )} +
+
+ + {showModal && ( + setShowModal(false)} + onSuccess={() => { + setShowModal(false); + fetchData(); + }} + /> + )} +
+ ); +} + +function TypeBadge({ type }: { type: string }) { + const colors: Record = { + FREE: 'bg-gray-100 text-gray-700', + STANDARD: 'bg-blue-100 text-blue-700', + PRO: 'bg-purple-100 text-purple-700', + }; + return ( + + {type} + + ); +} + +function StatusBadge({ status }: { status: string }) { + const colors: Record = { + active: 'bg-green-100 text-green-700', + disabled: 'bg-red-100 text-red-700', + expired: 'bg-yellow-100 text-yellow-700', + }; + const labels: Record = { + active: '正常', + disabled: '已禁用', + expired: '已过期', + }; + return ( + + {labels[status] || status} + + ); +} + +function LicenseModal({ + customers, + onClose, + onSuccess, +}: { + customers: Customer[]; + onClose: () => void; + onSuccess: () => void; +}) { + const [form, setForm] = useState({ + customerId: '', + licenseType: 'STANDARD', + expiresInDays: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [generatedKey, setGeneratedKey] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + const res = await fetch('/api/admin/licenses', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + customerId: form.customerId || null, + licenseType: form.licenseType, + expiresInDays: form.expiresInDays ? parseInt(form.expiresInDays) : null, + }), + }); + + const data = await res.json(); + if (data.success) { + setGeneratedKey(data.license.licenseKey); + } else { + setError(data.error || '生成失败'); + } + } catch { + setError('网络错误'); + } finally { + setLoading(false); + } + }; + + if (generatedKey) { + return ( +
+
+

许可证已生成

+
+

许可证密钥:

+

{generatedKey}

+
+

请妥善保存此密钥,它只会显示一次。

+ +
+
+ ); + } + + return ( +
+
+
+

生成许可证

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + setForm({ ...form, expiresInDays: e.target.value })} + placeholder="留空表示永久有效" + className="w-full px-3 py-2 border rounded-lg" + min="1" + /> +
+ + {error && ( +
{error}
+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/license-server/app/admin/login/page.tsx b/license-server/app/admin/login/page.tsx new file mode 100644 index 0000000..8f19d54 --- /dev/null +++ b/license-server/app/admin/login/page.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function AdminLoginPage() { + const router = useRouter(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + try { + const res = await fetch('/api/admin/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); + + const data = await res.json(); + + if (data.success) { + router.push('/admin'); + } else { + setError(data.error || '登录失败'); + } + } catch { + setError('网络错误'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

许可证管理后台

+ +
+
+ + setUsername(e.target.value)} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + required + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+ ); +} diff --git a/license-server/app/admin/page.tsx b/license-server/app/admin/page.tsx new file mode 100644 index 0000000..b745450 --- /dev/null +++ b/license-server/app/admin/page.tsx @@ -0,0 +1,217 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface DashboardStats { + customers: { total: number }; + licenses: { total: number; byType: Record; byStatus: Record }; + activations: { total: number; active: number }; + alerts: { pending: number }; + recentActivations: Array<{ + id: string; + licenseKey: string; + licenseType: string; + activatedAt: string; + companyName: string | null; + }>; +} + +export default function AdminDashboard() { + const router = useRouter(); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchStats(); + }, []); + + const fetchStats = async () => { + try { + const res = await fetch('/api/admin/dashboard'); + if (res.status === 401) { + router.push('/admin/login'); + return; + } + const data = await res.json(); + setStats(data); + } catch (error) { + console.error('获取统计数据失败:', error); + } finally { + setLoading(false); + } + }; + + const handleLogout = async () => { + await fetch('/api/admin/auth/logout', { method: 'POST' }); + router.push('/admin/login'); + }; + + if (loading) { + return ( +
+
加载中...
+
+ ); + } + + return ( +
+ {/* 顶部导航 */} + + +
+ {/* 统计卡片 */} +
+ + + + +
+ + {/* 许可证分布 */} +
+
+

许可证类型分布

+
+ {Object.entries(stats?.licenses.byType || {}).map(([type, count]) => ( +
+ {type} + {count} +
+ ))} +
+
+ +
+

许可证状态分布

+
+ {Object.entries(stats?.licenses.byStatus || {}).map(([status, count]) => ( +
+ + {status === 'active' ? '正常' : status === 'disabled' ? '已禁用' : '已过期'} + + {count} +
+ ))} +
+
+
+ + {/* 最近激活 */} +
+
+

最近激活

+
+
+ + + + + + + + + + + {stats?.recentActivations.map((activation) => ( + + + + + + + ))} + {!stats?.recentActivations.length && ( + + + + )} + +
+ 许可证密钥 + + 类型 + + 客户 + + 激活时间 +
{activation.licenseKey} + + + {activation.companyName || '-'} + + {new Date(activation.activatedAt).toLocaleString()} +
+ 暂无激活记录 +
+
+
+
+
+ ); +} + +function StatCard({ + title, + value, + highlight = false, +}: { + title: string; + value: number; + highlight?: boolean; +}) { + return ( +
+
{title}
+
+ {value} +
+
+ ); +} + +function TypeBadge({ type }: { type: string }) { + const colors: Record = { + FREE: 'bg-gray-100 text-gray-700', + STANDARD: 'bg-blue-100 text-blue-700', + PRO: 'bg-purple-100 text-purple-700', + }; + + return ( + + {type} + + ); +} diff --git a/license-server/app/admin/versions/page.tsx b/license-server/app/admin/versions/page.tsx new file mode 100644 index 0000000..b8600b4 --- /dev/null +++ b/license-server/app/admin/versions/page.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; + +interface Version { + id: string; + version: string; + releaseNotes: string | null; + minRequiredVersion: string | null; + isSecurityUpdate: boolean; + isCriticalUpdate: boolean; + downloadSize: string | null; + releaseDate: string; +} + +export default function VersionsPage() { + const router = useRouter(); + const [versions, setVersions] = useState([]); + const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(false); + + useEffect(() => { + fetchVersions(); + }, []); + + const fetchVersions = async () => { + try { + const res = await fetch('/api/admin/versions'); + if (res.status === 401) { + router.push('/admin/login'); + return; + } + const data = await res.json(); + setVersions(data.versions || []); + } catch (error) { + console.error('获取版本列表失败:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+ + +
+
+
+

版本列表

+ +
+ + {loading ? ( +
加载中...
+ ) : ( + + + + + + + + + + + + {versions.map((v) => ( + + + + + + + + ))} + {!versions.length && ( + + )} + +
版本号发布日期最低要求版本标签更新说明
{v.version}{new Date(v.releaseDate).toLocaleDateString()}{v.minRequiredVersion || '-'} + {v.isSecurityUpdate && 安全更新} + {v.isCriticalUpdate && 重要更新} + {v.releaseNotes || '-'}
暂无版本数据
+ )} +
+
+ + {showModal && setShowModal(false)} onSuccess={() => { setShowModal(false); fetchVersions(); }} />} +
+ ); +} + +function VersionModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) { + const [form, setForm] = useState({ version: '', releaseNotes: '', minRequiredVersion: '', isSecurityUpdate: false, isCriticalUpdate: false }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(''); + try { + const res = await fetch('/api/admin/versions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + const data = await res.json(); + if (data.success) onSuccess(); + else setError(data.error || '发布失败'); + } catch { setError('网络错误'); } + finally { setLoading(false); } + }; + + return ( +
+
+

发布新版本

+
+
+ + setForm({ ...form, version: e.target.value })} className="w-full px-3 py-2 border rounded-lg" placeholder="1.0.0" required /> +
+
+ +