#!/usr/bin/env node /** * openclaw-license CLI — 生成 / 验证 / 查看 License * * 用法: * npx openclaw-license issue --merchant M001 --name "水果店" --plan basic --days 365 * npx openclaw-license verify --file ~/.openclaw/license.json * npx openclaw-license info --file ~/.openclaw/license.json * npx openclaw-license machine-id */ import * as fs from 'fs'; import * as path from 'path'; import { issueLicense } from '../src/issuer.js'; import { LicenseGuard, getMachineId } from '../src/license-guard.js'; import { PLAN_PRESETS } from '../src/types.js'; import type { LicensePlan } from '../src/types.js'; // ========== 配置 ========== // 签名密钥 — 生产环境应从环境变量读取 const SECRET = process.env.OPENCLAW_LICENSE_SECRET || 'openclaw-default-secret-change-me-in-production'; const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || '~', '.openclaw'); const DEFAULT_LICENSE_PATH = path.join(OPENCLAW_HOME, 'license.json'); // ========== 命令 ========== const [,, command, ...args] = process.argv; function getArg(name: string, defaultValue?: string): string { const idx = args.indexOf(`--${name}`); if (idx === -1 || idx + 1 >= args.length) { if (defaultValue !== undefined) return defaultValue; console.error(`Missing required argument: --${name}`); process.exit(1); } return args[idx + 1]; } function hasFlag(name: string): boolean { return args.includes(`--${name}`); } async function main() { switch (command) { case 'issue': await cmdIssue(); break; case 'verify': await cmdVerify(); break; case 'info': await cmdInfo(); break; case 'machine-id': cmdMachineId(); break; case 'plans': cmdPlans(); break; default: printUsage(); break; } } // ---------- issue ---------- async function cmdIssue() { const merchantId = getArg('merchant'); const merchantName = getArg('name'); const plan = getArg('plan', 'basic') as LicensePlan; const days = parseInt(getArg('days', '365'), 10); const bindMachine = hasFlag('bind-machine'); const output = getArg('output', DEFAULT_LICENSE_PATH); if (!PLAN_PRESETS[plan]) { console.error(`Invalid plan: ${plan}. Valid: free, basic, pro, enterprise`); process.exit(1); } const licenseFile = issueLicense({ merchantId, merchantName, plan, validDays: days, bindMachine, }, SECRET); // 确保目录存在 const dir = path.dirname(output); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(output, JSON.stringify(licenseFile, null, 2)); console.log(''); console.log('✅ License 已生成'); console.log(''); console.log(` 文件: ${output}`); console.log(` 商户: ${merchantName} (${merchantId})`); console.log(` 套餐: ${plan}`); console.log(` 有效期: ${days} 天`); console.log(` 过期时间: ${licenseFile.payload.expiresAt}`); console.log(` 绑机器: ${bindMachine ? '是 (' + licenseFile.payload.machineId + ')' : '否'}`); console.log(` License ID: ${licenseFile.payload.id}`); console.log(''); console.log(` 功能: FTS5=${licenseFile.payload.features.fts_search} | 向量=${licenseFile.payload.features.vector_search} | 多项目=${licenseFile.payload.features.multi_project}`); console.log(` 配额: 项目=${licenseFile.payload.quotas.maxProjects} | Agent/项目=${licenseFile.payload.quotas.maxAgentsPerProject} | 月Token=${licenseFile.payload.quotas.monthlyTokens} | 日消息=${licenseFile.payload.quotas.dailyMessages}`); console.log(''); } // ---------- verify ---------- async function cmdVerify() { const file = getArg('file', DEFAULT_LICENSE_PATH); const guard = new LicenseGuard(file, SECRET); const result = await guard.boot(); if (result.valid) { console.log(''); console.log('✅ License 有效'); if (result.inGracePeriod) { console.log(' ⚠️ 已过期,在宽限期内,降级为 free'); } console.log(` 剩余: ${result.daysRemaining} 天`); console.log(` 套餐: ${result.payload?.plan}`); console.log(` 商户: ${result.payload?.merchantName}`); console.log(''); } else { console.log(''); console.log('❌ License 无效'); console.log(` 原因: ${result.error}`); console.log(''); process.exit(1); } } // ---------- info ---------- async function cmdInfo() { const file = getArg('file', DEFAULT_LICENSE_PATH); try { const raw = fs.readFileSync(file, 'utf-8'); const data = JSON.parse(raw); const p = data.payload; console.log(''); console.log('📋 License 详情'); console.log(''); console.log(` ID: ${p.id}`); console.log(` 商户: ${p.merchantName} (${p.merchantId})`); console.log(` 套餐: ${p.plan}`); console.log(` 发放时间: ${p.issuedAt}`); console.log(` 过期时间: ${p.expiresAt}`); console.log(` 绑机器: ${p.machineId || '否'}`); console.log(''); console.log(' 功能:'); for (const [k, v] of Object.entries(p.features)) { console.log(` ${k}: ${v ? '✅' : '❌'}`); } console.log(''); console.log(' 配额:'); for (const [k, v] of Object.entries(p.quotas)) { console.log(` ${k}: ${v === -1 ? '无限' : v}`); } console.log(''); console.log(` 签名: ${data.signature.substring(0, 16)}...`); console.log(''); } catch (err: any) { console.error(`读取失败: ${err.message}`); process.exit(1); } } // ---------- machine-id ---------- function cmdMachineId() { console.log(''); console.log(`机器指纹: ${getMachineId()}`); console.log(''); } // ---------- plans ---------- function cmdPlans() { console.log(''); console.log('📊 可用套餐:'); console.log(''); console.log('Plan | 项目 | Agent | 月Token | 日消息 | FTS | Vec | 多项目'); console.log('------------|------|-------|-----------|--------|-----|-----|-------'); for (const [name, preset] of Object.entries(PLAN_PRESETS)) { const q = preset.quotas; const f = preset.features; const fmt = (v: number) => v === -1 ? '无限' : String(v); console.log( `${name.padEnd(12)}| ${fmt(q.maxProjects).padEnd(5)}| ${fmt(q.maxAgentsPerProject).padEnd(6)}| ${fmt(q.monthlyTokens).padEnd(10)}| ${fmt(q.dailyMessages).padEnd(7)}| ${f.fts_search ? '✅' : '❌'} | ${f.vector_search ? '✅' : '❌'} | ${f.multi_project ? '✅' : '❌'}`, ); } console.log(''); } // ---------- usage ---------- function printUsage() { console.log(` openclaw-license — License 管理工具 命令: issue 生成新 License verify 验证 License info 查看 License 详情 machine-id 显示当前机器指纹 plans 显示可用套餐 发放 License: openclaw-license issue --merchant M001 --name "水果店" --plan basic [--days 365] [--bind-machine] [--output path] 验证: openclaw-license verify [--file ~/.openclaw/license.json] 查看: openclaw-license info [--file ~/.openclaw/license.json] 环境变量: OPENCLAW_LICENSE_SECRET 签名密钥 (必须设置, 发放和验证要用同一个) OPENCLAW_HOME OpenClaw 根目录 (默认 ~/.openclaw) `); } main().catch((err) => { console.error(err); process.exit(1); });