# RuoVea.KM **Repository Path**: ruovea-tools/RuoVea.KM ## Basic Information - **Project Name**: RuoVea.KM - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-08 - **Last Updated**: 2026-10-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RuoVea.KM - 授权管理系统
## 📖 项目简介 RuoVea.KM 是一个完整的授权管理系统,基于国密算法(SM2/SM3/SM4)实现,提供授权码生成、验证、激活等功能,以及机器硬件指纹识别能力。 该库面向 **软件产品集成** 场景设计,可以直接作为组件被其它项目引用。典型用法是:产品内置一份由供应商签发的试用策略令牌,用户在机器上可直接试用;试用到期后必须由供应商按其机器码签发授权码才能继续使用。 支持 .NET 5/6/7/8/9/10。仓库另附两个 WinForms + AntdUI 界面工具:**`RuoVea.KM.Licenser`(客户端激活界面,只含公钥)** 与 **`RuoVea.KM.Issuer`(供应商签发界面,持有私钥)**。 ## ✨ 功能特性 - 🔒 **签名保护的试用策略**:试用天数由 SM2 私钥签名后随程序分发,客户端只验签,无法通过改常量或改配置延长试用期 - 🖥️ **机器指纹识别**:基于 CPU / 主板 / BIOS / 硬盘序列号 / MAC 地址 / 系统 MachineGuid 生成机器码,多因子排序归一,硬件小改动不会导致授权失效 - 🔐 **国密算法支持**:SM2 签名验签、SM3 摘要、SM4 加解密 - 🛡️ **防篡改状态存储**:本地状态经 SM4-CBC 加密 + HMAC 校验,密钥由机器码派生;同时写入多个相互独立的存储位置,读取时取"最不利"值合并 - ⏱️ **时间回撤检测**:记录已观测的最大系统时间作为高水位,配合程序安装时间下界识别系统时间被回拨 - 🚦 **激活限流**:失败次数超过阈值后进入指数退避锁定,抵御离线暴力枚举授权码 - 🔑 **密钥轮换**:授权码携带 KeyId,可在签名与公钥集合中同时维护新旧密钥,私钥疑似泄露时可平滑作废 - 🚫 **一码一机**:SM4 密钥由机器码派生、不随授权码传输,授权码复制到其它机器无法解密使用 - 📦 **多目标框架支持**:.NET 5.0 / 6.0 / 7.0 / 8.0 / 9.0 / 10.0 - 📱 **跨平台支持**:支持 Windows 和 Linux 系统(注册表副本为 Windows 专属增强,缺失时自动降级) ## 📦 安装 ### NuGet 包管理器 ```bash Install-Package RuoVea.KM ``` ### .NET CLI ```bash dotnet add package RuoVea.KM ``` ### PackageReference ```xml ``` ## 🚀 快速开始 集成分为两个角色,**务必分清**: | 角色 | 运行位置 | 持有密钥 | 主要 API | | --- | --- | --- | --- | | **供应商** | 签发服务器 / 离线签发机 | SM2 **私钥** | `LicenseIssuer` | | **客户端** | 用户机器上的产品 | 仅 SM2 **公钥** | `LicenseGuard` / `LicenseManager` | ### 一、供应商侧:签发试用策略与授权码 ```csharp using RuoVea.KM; // 1) 一次性生成密钥对 var (publicKey, privateKey) = LicenseIssuer.GenerateKeyPair(); // publicKey → 写进客户端代码或配置(可以公开) // privateKey → 存入密钥管理系统或硬件加密机(绝不下发,不要写进客户端) // 2) 整个产品只签发一次:2 年试用策略令牌 // 试用起算锚点默认是"用户首次运行",因此程序交付后长期未安装也不会白白消耗试用期 string trialToken = LicenseIssuer.CreateTrialToken( appId: "MyProduct", privateKey: privateKey, trialDays: 730); // 730 天 = 2 年 // 3) 客户购买后,按其提供的机器码签发正式授权 string licenseCode = LicenseIssuer.IssueLicense( appId: "MyProduct", customerName: "某某公司", machineCode: "8QH2-3K9M-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX", privateKey: privateKey, years: 1, keyId: "k1"); ``` > 不想写代码时,上述签发动作在 **服务端签发工具 `RuoVea.KM.Issuer`** 里有对应界面(见下方「🛠️ 服务端签发工具」)。 ### 二、客户端侧:校验与激活 ```csharp using RuoVea.KM; var options = new LicenseOptions { AppId = "MyProduct", PublicKey = publicKey, // 供应商公钥 TrialToken = trialToken, // 供应商签发的 2 年试用策略令牌 ExpiringSoonDays = 15 }; // ========== 启动校验 ========== LicenseState state = LicenseGuard.Check(options); if (state.IsValid) { Console.WriteLine($"可用,剩余 {(state.RemainingDays < 0 ? "永久" : state.RemainingDays + " 天")}"); } else { Console.WriteLine($"不可用:{state.Message}"); } // 也可在启动路径上一行完成"校验 + 终止" // LicenseGuard.EnforceOrExit(options); // 不可用时写错误到 stderr 并以退出码 78 结束 // ========== 展示机器码,供用户申请授权 ========== string machineCode = LicenseGuard.GetActivationRequestCode(); Console.WriteLine($"请将此机器码提供给供应商:{machineCode}"); // ========== 激活 ========== var activation = LicenseGuard.Activate("用户粘贴的授权码", options); if (activation.Code == RuoVea.ExEnum.CodeStatus.OK) { Console.WriteLine(activation.Message); } else { Console.WriteLine($"激活失败:{activation.Message}"); } // ========== 到期提醒 ========== if (state.IsExpiringSoon) { Console.WriteLine($"授权将在 {state.RemainingDays} 天后到期,请及时续期"); } ``` ### 三、售后换机:重置码 客户更换机器后需要释放旧机器的授权状态。由于本地状态受完整性保护,用户不能自行重置(否则等同于重开试用期),正确做法是由供应商签发一次性重置码: ```csharp // 供应商侧:为客户的旧机器码签发一个 3 天内有效的重置码 string resetCode = LicenseIssuer.CreateResetCode(oldMachineCode, privateKey, validDays: 3); // 客户端侧:校验通过后才清除本地状态 string error; if (LicenseIssuer.TryVerifyResetCode(resetCode, options, out error)) { LicenseGuard.ResetLocalState(options); } else { Console.WriteLine($"重置码无效:{error}"); } ``` ## 🖥️ 授权界面工具(RuoVea.KM.Licenser) `RuoVea.KM.Licenser` 是一个基于 AntdUI 的 WinForms 界面工具,用于替代"让用户自己找机器码、自己复制授权码"的原始流程。 它**不签发授权码,也不含私钥**:只展示机器码、接收用户粘贴的授权码并调用 `LicenseGuard` 完成激活。签发仍在供应商内部的离线流程里用 `LicenseIssuer` 完成。 **该工具运行时需要 .NET 8.0(Windows)**;授权组件本身仍支持 net6.0 ~ net10.0,界面工具与组件版本互相独立。 ### 方式一:独立运行 ```bash # 1) 在程序目录生成配置模板 RuoVea.KM.Licenser.exe --write-template # 2) 填写 licenser.json 后直接运行,弹出授权窗口 RuoVea.KM.Licenser.exe # 脚本化采集机器码(不弹界面) RuoVea.KM.Licenser.exe --machine-code ``` `licenser.json` 字段: | 字段 | 说明 | | --- | --- | | `appId` | 产品标识,必须与目标软件的 `AppId` 一致 | | `publicKey` | 供应商 SM2 公钥(十六进制) | | `trialToken` | 供应商签发的试用策略令牌 | | `publicKeys` | 多公钥字典(KeyId → 公钥),可选,用于密钥轮换 | | `windowTitle` | 窗口标题,留空使用内置多语言文案 | | `language` | 界面语言(`zh-CN` / `zh-TW` / `zh-HK` / `en-US` / `ja-JP` / `fr-FR` / `vi-VN`),留空跟随系统 | | `expiringSoonDays` | 距到期多少天开始提示续期 | ### 方式二:嵌进自己的软件 界面主体是 `LicensePanel`(一个 `UserControl`),可以放进设置页或关于页: ```csharp using RuoVea.KM; using RuoVea.KM.Licenser; var options = new LicenseOptions { AppId = "MyProduct", PublicKey = publicKey, TrialToken = trialToken }; // 第二个参数 showHeader 控制是否显示面板自带的标题区: // 宿主页面已有标题时传 false,避免出现两行同样的字。 var panel = new LicensePanel(options, showHeader: false) { Dock = DockStyle.Fill }; settingsPage.Controls.Add(panel); // 用户完成外部操作(如手工替换状态文件)后可主动刷新 panel.RefreshState(); ``` 需要独立弹窗时用 `LicenseDialog`: ```csharp LicenseDialog.Show(options); // 非模态 DialogResult r = LicenseDialog.ShowDialog(options); // 模态,激活成功返回 OK ``` ### 本地操作日志 工具会把"配置读取失败 / 激活成功 / 激活失败(含原因) / 复制机器码"追加写入状态目录下的 `licenser-log.txt`, 用于售后排障(客户说"激活了但用不了"时,可对照时间点确认当时究竟发生了什么)。 日志中的机器码**只保留 SHA256 指纹前 16 位**,不落机器码明文;日志只写不读,不参与任何授权判定。 单条记录形如: ``` 2026-10-16 16:37:43 | 0281 | MyProduct | 400D7ACB19E75FF2 | 激活失败(已记入本地日志) | 授权码与硬件不匹配 ``` ## 🛠️ 服务端签发工具(RuoVea.KM.Issuer) `RuoVea.KM.Licenser` 是给**客户**用的激活界面,只含公钥;`RuoVea.KM.Issuer` 是给**供应商自己**用的签发界面,**持有 SM2 私钥**,把 `LicenseIssuer` 的四个动作做成表单: | 功能区 | 作用 | 对应 API | | --- | --- | --- | | **密钥对** | 生成 / 校验 SM2 密钥对,加密保存到本机密钥库 | `GenerateKeyPair`、`ValidateKeyPair` | | **试用令牌** | 签发声明"试用天数 + 起算方式"的策略令牌,随客户端分发 | `CreateTrialToken` | | **正式授权码** | 绑定机器码签发授权码(永久 / 年限 / 天数 / 功能项) | `IssueLicense` | | **售后重置码** | 客户换硬件或重装后,让客户端清掉旧试用锚点与失败计数 | `CreateResetCode` | > ⚠️ **本工具持有私钥,仅供供应商内部使用:请勿随软件分发,切勿把私钥写入客户端配置、日志或聊天记录。** 私钥泄露等于整套授权体系失效。 **该工具运行时需要 .NET 8.0(Windows)**,与授权组件的多目标框架(net6.0 ~ net10.0)互相独立。 ### 运行 ```bash # 打开签发窗口(四个功能区,左侧导航切换) RuoVea.KM.Issuer.exe # 脚本化采集本机机器码(不弹界面) RuoVea.KM.Issuer.exe --machine-code # 生成一对密钥并打印(不落盘,自行抄录进密钥库) RuoVea.KM.Issuer.exe --gen-keypair # 端到端自检:签发试用令牌 / 授权码 / 重置码,再用客户端侧 API 反向验证 RuoVea.KM.Issuer.exe --self-test ``` ### 密钥库与口令 私钥以**口令加密**的形式保存在 `%LOCALAPPDATA%\RuoVea\KM.Issuer\keys.vault`(单行文本,SM3 派生密钥 + SM3 完整性校验)。口令**不落盘**:忘了口令只能重新生成密钥对,已下发的授权码随之全部失效。 密钥库字段:产品标识、密钥标识(KeyId,用于密钥轮换)、备注名称、公钥、私钥、密钥库口令。 ### 签发日志 每次签发都会追加一行到 `%LOCALAPPDATA%\RuoVea\KM.Issuer\issuer-log.txt`,用于事后核对"某天到底给谁签了什么"。 日志**绝不写入私钥明文,也绝不写入密钥库口令**;机器码只保留 SM3 指纹前 16 位(`IssueLog.MachineTag`),不落机器码明文。单条记录形如: ``` 2026-10-16 17:57:29 | 6528 | 生成密钥对 | 命令行 ``` 时间 | 随机序号 | 动作 | 参数摘要(`keyId=` / `appId=` / `machine=` / `years=` …)。 ### 与客户端工具的配对关系 ``` 密钥对页 ──生成──► 公钥 ──► 填进客户端 licenser.json └► 私钥 ──► 只留在本机密钥库 试用令牌页 ──签发──► 试用令牌 ──► 填进客户端 licenser.json(随软件分发) 正式授权码页 ──按客户机器码签发──► 授权码 ──► 客户在客户端界面粘贴激活 售后重置码页 ──按客户机器码签发──► 重置码 ──► 客户在客户端执行重置 ``` ## 🔐 安全能力与边界 ### 已提供的防护 | 需求 | 实现手段 | 说明 | | --- | --- | --- | | **用户可使用 2 年** | 签名试用策略令牌 + 首启时间锚定 | 试用天数由 `LicenseIssuer` 签名,客户端无法篡改;起算点为用户首次运行 | | **2 年后不能继续使用** | 到期校验 + 令牌硬截止时间双重约束 | 到期后 `LicenseGuard.Check` 返回 `Expired`,`EnforceOrExit` 直接终止进程 | | **防暴力破解** | 激活失败限流 + 指数退避锁定 | 授权码由 SM2 私钥签名,本来就无法伪造;限流进一步拖慢盲猜 | | **防时间回撤** | 高水位记录 + 程序安装时间下界 + 容忍窗口 | 把系统时间调回过去会被识别并拒绝使用 | | **防密钥泄露** | 客户端只持有公钥;KeyId 轮换 | 私钥不下发,公钥泄露无安全影响;私钥疑似泄露时可换 KeyId 平滑作废 | | 额外:防授权码转发 | SM4 密钥由机器码派生,不随授权码传输 | 授权码复制到别的机器后解密得到乱码,无法使用 | | 额外:防删状态重置试用 | 多副本冗余存储 + 对抗性合并 | 用户级文件、系统级文件、Windows 注册表三处互为备份,只删一处无法重置 | ### 必须知情的边界 **纯客户端授权方案无法抵御拥有本机管理员权限且愿意逆向的对手。** 本组件能可靠挡住的是低成本绕过:改系统时间、删授权文件、把授权码发给别人、复制授权文件到另一台机器、篡改试用天数常量。对"反编译后直接改 IL 跳过校验调用"这类攻击,本组件只能提高门槛,不能根除。 若业务上必须做到"绝对无法绕过",需要把关键校验移到服务端(由服务端下发短期票据),或使用硬件加密狗。本组件的定位是把常见绕过手段的收益降到接近零,同时保持离线可用、集成成本低。 ### 务必遵守的集成纪律 1. **私钥绝不下发**。`LicenseIssuer` 只能在供应商内部环境运行;客户端代码里不要出现私钥,也不要把私钥写进配置文件或环境变量。 2. **每个产品用独立的 AppId**。AppId 参与状态文件路径与密钥派生,同机多产品互不干扰;它同时是试用令牌的产品绑定依据。 3. **`ResetLocalState` 只在供应商指令下调用**。它会连同试用首启时间一起清掉,客户端流程里自行调用等于重开试用期。 4. **`MaxTrialDays` 与 `HardMaxTrialDays` 是编译期上限**。即便令牌被伪造,客户端也不会接受超过该值的试用期(`HardMaxTrialDays` 为 3650 天)。 5. **签发工具与客户端工具不可混发**。`RuoVea.KM.Issuer` 会解锁私钥,只能放在供应商内部机器上;对外分发的一律是 `RuoVea.KM.Licenser`。密钥库口令与私钥都不进版本库、不进聊天记录、不写日志。 ## 🏗️ 项目结构 ``` RuoVea.KM/ ├── RuoVea.KM/ # 核心库 │ ├── LicenseOptions.cs # 集成配置(AppId / 公钥 / 试用令牌 / 限流参数) │ ├── LicenseState.cs # 状态、错误码、本地状态记录 │ ├── LicenseInfo.cs # 授权信息实体 │ ├── LicenseGuard.cs # 客户端集成主入口 │ ├── LicenseManager.cs # 授权码生成、验证、激活 │ ├── LicenseIssuer.cs # 供应商签发工具(需私钥) │ ├── LicenseEngine.cs # 内部校验引擎 │ ├── TrialToken.cs # 试用策略令牌编解码与验签 │ ├── SecureStateStore.cs # 多副本防篡改状态存储 │ ├── ClockGuard.cs # 时间回撤检测 │ ├── ActivationThrottle.cs # 激活限流 │ ├── MachineFingerprint.cs # 机器码与硬件指纹 │ ├── CryptoUtil.cs # 密码学小工具 │ ├── Language/ # 8 种语言的提示资源 │ └── *.csproj # 多目标框架项目文件 ├── RuoVea.KM.Demo/ # 示例项目(含 16 项防护自检) ├── RuoVea.KM.Licenser/ # 授权界面工具(WinForms + AntdUI,net8.0-windows) │ ├── LicensePanel.cs # 可嵌入宿主软件的授权面板(含状态/机器码/激活三块) │ ├── LicenseDialog.cs # 独立授权窗口(含自绘标题栏与拖动) │ ├── LicenserConfig.cs # licenser.json 读写 │ ├── ActivationLog.cs # 本地操作日志(辅助售后排障,不参与授权判定) │ ├── LicenseVisualMap.cs # 授权状态 → 界面呈现的映射 │ ├── UiTheme.cs # AntdUI 主题色与字体 │ ├── Program.cs # 宿主入口与命令行参数 │ └── Language/ # 工具界面文案(7 种语言) ├── RuoVea.KM.Issuer/ # 服务端签发工具(WinForms + AntdUI,net8.0-windows,★持有私钥) │ ├── IssuerDialog.cs # 签发窗口:左侧导航 + 安全横幅 + 可缩放标题栏 │ ├── IssuerPage.cs # 功能页基类(标题 / 正文滚动 / 页尾提示) │ ├── KeyPairPage.cs # 密钥对:生成、校验、存取密钥库 │ ├── TrialTokenPage.cs # 试用令牌:试用天数、起算方式、硬截止日期 │ ├── LicensePage.cs # 正式授权码:绑定机器码签发 │ ├── ResetCodePage.cs # 售后重置码:按机器码签发短期重置码 │ ├── KeyVault.cs # 口令加密的密钥库(keys.vault) │ ├── KeySession.cs # 当前会话持有的密钥状态 │ ├── IssueLog.cs # 签发日志(不写私钥、机器码只留 SM3 前 16 位) │ ├── IssuerText.cs # 工具界面文案(中文) │ ├── UiTheme.cs / UiKit.cs # AntdUI 主题色、字体与控件工厂 │ └── Program.cs # 宿主入口与命令行参数 ├── doc/ # 图标等文档资源 ├── LICENSE ├── README.md ├── RuoVea.KM.sln └── dotnet build.bat # 逐目标框架 Release 构建脚本 ``` ## 📚 核心类说明 ### LicenseOptions(集成配置) | 属性 | 说明 | | --- | --- | | `AppId` | 产品标识,用于隔离状态目录与绑定试用令牌 | | `PublicKey` | 供应商 SM2 公钥(十六进制) | | `TrialToken` | 供应商签发的试用策略令牌 | | `PublicKeys` | 多公钥字典(KeyId → 公钥),用于密钥轮换 | | `MaxTrialDays` | 客户端额外施加的试用天数上限 | | `AllowTrial` | 是否允许试用,关闭后必须有正式授权 | | `ClockToleranceSeconds` | 时钟校验容忍秒数,默认 1 天 | | `Anchor` | 试用起算锚定方式(首启 / 令牌签发时间) | | `MaxFailedActivations` | 触发锁定的失败次数阈值,默认 5 | | `LockoutMinutes` | 基础锁定时长(分钟),默认 1440,按失败次数翻倍 | | `ExpiringSoonDays` | 距到期多少天开始提示续期,默认 15 | | `StateDirectory` | 自定义状态目录(测试或多实例部署时使用) | ### LicenseGuard(客户端主入口) - `Check(options)` — 执行一次完整校验,返回 `LicenseState` - `Check(options, now)` — 指定时间基准校验,便于接入可信时间源或编写确定性测试 - `EnsureLicensed(options)` — 校验失败时抛出 `LicenseException` - `EnforceOrExit(options)` — 校验失败时写错误到 stderr 并以退出码 78 结束 - `Activate(licenseCode, options)` — 激活正式授权码 - `GetActivationRequestCode()` — 获取本机机器码 - `ResetLocalState(options)` — 清除本地状态(**仅供供应商指令下的售后流程**) - `GetStateLocation(options)` — 获取状态存储位置,用于诊断 ### LicenseIssuer(供应商签发,需要私钥) - `GenerateKeyPair()` / `ValidateKeyPair(pub, priv)` — 生成与自检密钥对 - `CreateTrialToken(...)` — 签发试用策略令牌 - `IssueLicense(...)` — 按机器码签发正式授权码 - `CreateResetCode(...)` / `TryVerifyResetCode(...)` — 售后重置码 ### LicenseManager(授权码生成与验证) 签发方 API(需私钥):`GenerateSm2KeyPair`、`GenerateLicense` 客户端 API(需公钥):`ValidateLicense`、`ActivateLicense`、`LoadAndValidateStoredLicense`、`CheckExpiringSoon`、`GetActivationRequestCode` 客户端方法一律以 `LicenseOptions` 为参数。这是刻意的约束:`AppId` 参与产品绑定校验, 若允许"只传公钥"的简写形式,产品绑定就无从执行,一份为产品 A 签发的授权码将能在产品 B 上使用。 ### MachineFingerprint(机器指纹) - `GetMachineId()` / `GetActivationRequestCode()` — 获取机器码(32 位 Crockford Base32,按 4 字符分组) - `GetMachineFingerprint()` — 历史 SM3 指纹,**为兼容已发放授权而冻结算法,新代码不要使用** - `MachineTagHint()` — 生成授权码中的机器标识提示值 - `IsMatch(binding)` — 校验绑定标识(恒定时间比较,同时兼容新机器码与历史指纹) - `IsHardwareInfoReliable` — 硬件信息是否充足(容器环境下可能为 false) ## 🔧 支持的框架 | 框架版本 | 支持状态 | 包版本 | | --- | --- | --- | | .NET 10.0 | ✅ 支持 | 10.0.x | | .NET 9.0 | ✅ 支持 | 9.0.x | | .NET 8.0 | ✅ 支持 | 8.0.x | | .NET 7.0 | ✅ 支持 | 7.0.x | | .NET 6.0 | ✅ 支持 | 6.0.x | | .NET 5.0 | ✅ 支持 | 5.0.x | ## 📝 依赖项 - **RuoVea.SM**:国密算法实现(SM2/SM3/SM4) - **RuoVea.ExDto**:`RestfulResult` 数据传输对象 - **RuoVea.ExEnum**:`CodeStatus` 状态码 - **RuoVea.ExIdGen**:ID 生成 - **System.Management**:WMI 硬件信息采集(Windows) ## 🔒 明文与密文对照 授权码采用 **KM2** 格式(Base64Url 编码): ``` KM2 . . . <机器标识> . . . ``` - SM4 密钥不随授权码传输,而是由 `SM3(机器码 | nonce | KeyId)` 派生 - SM2 签名覆盖密文、IV、机器标识、nonce、keyId - 旧版授权码(`Base64("<密文>.<签名>.")`)仍可解析,但新签发一律使用 KM2 ## 🎯 应用场景 - **商业软件授权**:为客户端软件提供离线可用的试用与正式授权 - **企业内部软件**:部门级软件的使用期限管理 - **试用版管理**:2 年试用,到期后引导购买正式授权 - **按机器授权**:一码一机,防止授权在客户之间扩散 ## 🤝 贡献指南 欢迎提交 Pull Request 和 Issue。请确保: 1. 代码符合项目风格 2. 保持代码风格一致 3. 添加必要的注释 4. 更新相关文档 ## 📄 许可协议 本项目遵循 MIT 许可协议,详细信息请参见 [LICENSE](LICENSE) 文件。 ## 🔗 相关资源 - [国家密码管理局](https://www.oscca.gov.cn) - [国密算法标准文档](https://www.oscca.gov.cn) - [NuGet 包页面](https://www.nuget.org/packages/RuoVea.KM) - [Gitee 仓库](https://gitee.com/ruovea/ruovea-km) ## 📧 联系方式 如有问题或建议,欢迎通过以下方式联系: - 提交 [Issue](https://gitee.com/ruovea/ruovea-km/issues) - 发送邮件至项目维护者 *** **RuoVea.KM** - 让软件授权管理更简单!🇨🇳