diff --git a/.agents/design/admin/license-instance-id-redesign.md b/.agents/design/admin/license-instance-id-redesign.md new file mode 100644 index 000000000000..baebbb286b84 --- /dev/null +++ b/.agents/design/admin/license-instance-id-redesign.md @@ -0,0 +1,465 @@ +# License 重构设计 — LicenseDataType 重构 + 实例 ID 绑定 + +> 状态:**部分确认(§6 实例 ID 方案已定;§8 其余项待评审)** +> 背景:pro/admin 独立管理后台下线,管理员能力并入前台 app;LicenseDataType 结构重构 +> 范围:License 类型结构、签发格式、验证逻辑、绑定标记、前端激活流程 +> 关联代码:`packages/global/common/system/types/index.ts`(类型)、`pro/admin/src/service/common/license/auth.ts`(验证)、`pro/admin/src/components/common/License/Input.tsx`(激活 UI)、`projects/app/src/web/common/license/api.ts`(app 激活入口) + +--- + +## 1. 背景与问题 + +### 1.1 管理员迁移 + +原商业版部署形态:pro/admin 作为**独立管理后台**,有独立域名,License 通过 `hosts` 字段绑定"管理端有效域名",前端在 `Header.tsx` 校验 `location.host` 是否在 `hosts` 中,不在则清空 license 并跳登录。 + +管理员主页迁移到 app 后: + +1. **没有独立管理域名**:app 可能被多域名、多 IP、反向代理访问,`location.host` 无法唯一标识一个部署实例。 +2. **前端校验天然可绕过**:现有 hosts 校验只在浏览器 JS 里做,后端 `authLicense()` 从不校验 hosts。换标记的同时应把校验放到后端,强度反而提升。 + +需要一个**与部署形态无关、后端可验证**的部署唯一标记。 + +### 1.2 类型重构 + +现有 `LicenseDataType` 的问题: + +1. **限制字段散落顶层**:`maxUsers`/`maxApps`/`maxDatasets` 平铺,与身份字段混在一起,不直观。 +2. **功能开关扁平**:`functions` 里功能与自定义模板市场等营销性开关混放,无版本概念,市场无法灵活扩展。 +3. **无版本区分**:试用版/正式版只能靠 `expiredTime` 隐含表达,无法在数据层面区分。 +4. **默认值语义不便扩展**:现有验证逻辑缺失功能字段时补 `false`(默认关),市场新增功能必须改签发侧并重签所有 license 才能启用。 + +目标:功能与限制对象化、增加版本字段、功能开关默认 `true`(激活后全开,市场扩展零成本)。 + +## 2. 现状分析 + +### 2.1 License 结构(签发侧,官方私钥生成) + +``` +license = signature(684 字符 base64) + payload(base64 JSON) +``` + +- payload = `LicenseDataType` +- 签名 = RSA-4096 私钥对 **base64 payload 字符串字节**做 RSA-SHA256 +- 公钥硬编码在 `pro/admin/src/service/common/license/auth.ts` 的 `LICENSE_PUBLIC_KEY` + +### 2.2 LicenseDataType 字段全景(现行) + +| 类别 | 字段 | 说明 | 消费位置 | +|---|---|---|---| +| 身份 | `company` | 客户公司名 | 前端展示、`/license/auth` 未登录返回 | +| 时间 | `startTime` / `expiredTime` | 生效/过期时间 | `expiredTime` 后端校验过期;其余展示 | +| 备注 | `description?` | 描述 | **无消费点**(死字段) | +| 绑定 | `hosts?` | 管理端有效域名 | 仅 pro/admin 前端 `Header.tsx` 校验,后端不校验 | +| 配额 | `maxUsers?` | 最大用户数,不填不限 | `auth.ts` `licenseAuth.authMaxUsers`:`users > maxUsers` 拒绝 | +| 配额 | `maxApps?` | 最大应用数,不填不限 | `teamLimit.ts`:`apps > maxApps` 拒绝(`>` 等额允许) | +| 配额 | `maxDatasets?` | 最大数据集数,不填不限 | `teamLimit.ts`:`datasets >= maxDatasets` 拒绝(`>=` 等额拒绝) | +| 功能 | `functions.sso` | 企业登录 | pro 配置页登录方式选项 | +| 功能 | `functions.pay` | 计费/套餐 | pro `Navbar.tsx`、app `AdminContainer.tsx` 导航 | +| 功能 | `functions.customTemplates` | 自定义模板市场 | pro `Navbar.tsx`、app `AdminContainer.tsx` 导航 | +| 功能 | `functions.datasetEnhance` | 数据集增强 | 定时任务开关(autoTraining/imageIndex/imageParse)+ 前端 `show_dataset_enhance` | +| 功能 | `functions.batchEval` | 批量评估 | 前端 `show_batch_eval` | + +### 2.3 验证流程(`authLicense`) + +1. 取 license 字符串(入参优先,否则从 `MongoSystemConfigs` type=license 读已存) +2. `signature = license.substring(0, 684)`,`payload = license.substring(684)` +3. `crypto.createVerify('RSA-SHA256').update(payload).verify(PUBLIC_KEY, signature, 'base64')` 验签 +4. `JSON.parse(Buffer.from(payload, 'base64').toString('utf8'))` 解析 +5. `expiredTime < now` → "License 已过期" +6. 缺 `functions` → "License 内容错误" +7. 缺失 function 字段与 `licenseDefaultData` 合并补 `false`(**待改为默认 true**) +8. 成功写入 `global.licenseData`;失败置 `undefined` + +### 2.4 激活与加载链路 + +| 时机 | 逻辑 | 位置 | +|---|---|---| +| 激活 | `POST /admin/common/license/active`:验签 → upsert `MongoSystemConfigs`(type=license,value=`{license, data}`)→ 写 `global.licenseData` | `pro/admin/src/pages/api/admin/common/license/active.ts` | +| 启动 | 从 DB 读已存 license 重验 | `instrumentation-node.ts` | +| 定时 | 每小时 `15 */1 * * *` 重验(刷新过期) | `service/system/cron.ts` | +| watch | 配置变更时重验 | `middleware/volumnMongoWatch.ts` | +| 读取 | `GET /admin/common/license/auth`:未激活空;管理员全量;未登录仅 `company` | `pages/api/admin/common/license/auth.ts` | + +### 2.5 拦截层 + +- `licenseCheck` middleware:仅检查 `global.licenseData` 存在,不校验 hosts。 +- 配额/功能开关:消费方直接读 `global.licenseData`(`teamLimit.ts`、`licenseAuth.authMaxUsers`、定时任务、前端导航)。 + +### 2.6 迁移现状(app 侧已就位) + +- app 已有 `components/admin/License/{Input,LicenseData}.tsx`、`web/common/license/api.ts`(走 `/proApi` 代理到 pro/admin) +- `useSystemStore` 已支持 `licenseData/initLicenseData/clearLicenseData` +- 未接入:app Layout 的"未激活弹 LicenseInput"逻辑;`AdminContainer.tsx` 的 `adminLicenseFunctions` 仍为硬编码 `{pay:false, customTemplates:false}` 占位 + +## 3. 问题定义 + +1. 绑定标记从 `hosts`(域名)换成什么,才能在"无独立管理域名、多域名访问、后端可验证"的形态下唯一标识部署实例? +2. LicenseDataType 如何对象化(功能/限制分组)、版本化(试用/正式)、功能开关可扩展(默认 true)? +3. 如何兼容存量 license(老客户已签发、旧结构)? +4. 离线/内网客户必须继续可用(不能强制联网)。 + +## 4. 绑定方案选型 + +| 方案 | 绑定对象 | 优点 | 缺点 | 结论 | +|---|---|---|---|---| +| A. 部署实例 ID | 首次启动生成的随机 ID(持久化) | 离线可用、任意部署形态、后端校验、改动最小 | ID 可被清除 → 需官方解绑流程 | **推荐** | +| B. 机器指纹 | MAC/CPU/磁盘哈希 | 绑定强、无需上报 | Docker/K8s/云主机指纹不稳定,重建即变 | 不推荐 | +| C. 在线激活 | 官方 license server | 可吊销、可订阅化 | 破坏离线/内网客户,架构大改 | 暂缓(可预留) | +| D. 保留域名绑 app 域名 | app 域名 | 改动最小 | 多域名/IP 失效,与前提矛盾 | 排除 | + +**选 A**:RSA 验签体系不变,仅把绑定标记从 `hosts` 换成 `instanceId`,校验从"前端域名检查"升级为"后端实例校验"。商业信任模型下足够。 + +## 5. LicenseDataType 重构 + +### 5.1 新类型定义(`packages/global/common/system/types/index.ts`) + +```ts +export type LicenseVersionType = 'trial' | 'official'; // 试用版 / 正式版 + +export type LicenseDataType = { + // —— 版本 —— + version?: LicenseVersionType; // 缺省按 official 处理(存量兼容) + + // —— 身份与时间 —— + startTime: string; + expiredTime: string; + company: string; + description?: string; + + // —— 绑定(实例 ID 方案)—— + instanceId?: string; // 新:绑定部署实例(替代 hosts) + hosts?: string[]; // 存量兼容,新签发不再使用 + + // —— 限制(对象)—— + limits: { + maxUsers?: number; // 最大用户数,不填默认不上限 + maxApps?: number; // 最大应用数,不填默认不上限 + maxDatasets?: number; // 最大数据集数,不填默认不上限 + // 预留:后续配额(如 maxDatasetSize、requestsPerMinute)在此扩展 + }; + + // —— 功能(对象,默认 true)—— + functions: { + sso: boolean; // 企业登录(保持) + pay: boolean; // 计费/套餐(保持) + eval: boolean; // 评估(保持,原 batchEval,命名待确认) + datasetEnhance?: boolean; // 数据集增强(去留待确认) + // customTemplates 已移除(自定义模板市场) + assistantGenerate: boolean; // 辅助生成(新增,命名待确认) + portal: boolean; // 门户(新增,命名待确认) + sandboxSkills: boolean; // 沙盒与技能(新增,命名待确认) + }; +}; +``` + +### 5.2 语义变化:功能默认 true + +| 项 | 现行 | 新 | +|---|---|---| +| 缺失功能字段 | 补 `false`(默认关) | 补 `true`(默认开) | +| 未激活(无 license) | `global.licenseData = undefined` → 全关 | 不变(undefined → 全关) | +| 新功能上线 | 需改签发侧 + 重签所有 license | 零成本,老 license 自动获得 | + +**商业语义**:购买 license = 默认全功能开启,个别客户特批降级时显式写 `false`。签发侧必须注意:`sso`/`pay` 等收费功能**不写即开启**,需在签发工具中显式声明关闭项。 + +> ⚠️ 待确认:默认 true 是否覆盖全部功能(含 sso/pay),还是仅新增功能?见 §8。 + +### 5.3 存量 license 兼容与归一化 + +验证逻辑在 `authLicense` 验签后、写 `global.licenseData` 前,做一次归一化 `normalizeLicenseData(raw)`: + +```ts +const normalizeLicenseData = (raw: any): LicenseDataType => ({ + version: raw.version ?? 'official', + startTime: raw.startTime, + expiredTime: raw.expiredTime, + company: raw.company, + description: raw.description, + instanceId: raw.instanceId, + hosts: raw.hosts, // 存量读取,不再校验 + limits: raw.limits ?? { + maxUsers: raw.maxUsers, // 旧结构顶层字段映射 + maxApps: raw.maxApps, + maxDatasets: raw.maxDatasets + }, + functions: { + sso: raw.functions?.sso ?? true, // 默认 true + pay: raw.functions?.pay ?? true, + eval: raw.functions?.batchEval ?? true, // 旧字段名映射 + datasetEnhance: raw.functions?.datasetEnhance ?? true, + assistantGenerate: raw.functions?.assistantGenerate ?? true, + portal: raw.functions?.portal ?? true, + sandboxSkills: raw.functions?.sandboxSkills ?? true + } +}); +``` + +- 旧结构(顶层 `maxUsers` + `functions.{sso,pay,customTemplates,datasetEnhance,batchEval}`)自动映射 +- `customTemplates` 读取后忽略(功能已移除) +- 存量 license 的 functions 均显式写过值,默认 true 不影响其取值 + +### 5.4 消费点改动清单 + +| 文件 | 现行 | 改为 | +|---|---|---| +| `teamLimit.ts` | `licenseData.maxApps` / `maxDatasets` | `licenseData.limits.maxApps` / `maxDatasets` | +| `auth.ts` `authMaxUsers` | `licenseData.maxUsers` | `licenseData.limits.maxUsers` | +| `auth.ts` `authDatasetEnhance` | `functions.datasetEnhance` | 取决于 datasetEnhance 去留 | +| `auth.ts` 默认值合并 | 补 `false` | 补 `true` + 归一化 | +| `system/index.ts` | `functions.datasetEnhance` / `batchEval` | `functions.datasetEnhance` / `eval` | +| `LicenseData.tsx`(pro + app) | 顶层 maxUsers 等 + 5 功能 | `limits.*` + 新 functions 展示(含 version 标签) | +| `AdminContainer.tsx` | 硬编码 `{pay, customTemplates}` | 读 `licenseData.functions`(pay + 新功能,移除 customTemplates) | +| `pro Navbar.tsx` | `functions.pay` / `customTemplates` | 移除 customTemplates 项 | +| 签发工具(仓库外) | 旧结构 | 新结构 + 默认 true 语义 | + +## 6. 实例 ID 绑定方案 + +### 6.1 总体思路 + +``` +签发侧(官方,持私钥): + payload 增加 instanceId = 客户部署实例 ID + license = RSA4096 签名(payload) + payload // 格式不变 + +验证侧(app 内,持公钥): + 1. 验签 / 过期检查(不变) + 2. 归一化(§5.3) + 3. 新:license.instanceId && license.instanceId !== 本地实例 ID → 拒绝 + 4. 兼容:license 无 instanceId(存量)→ 用 DB 中已记录的 boundInstanceId 校验 + 5. 通过后写 global.licenseData +``` + +### 6.2 systemConfigs 集合写入机制(现状澄清) + +`MongoSystemConfigs` 按 `type` 存 6 类配置,索引 `{ type: 1 }` **非唯一**,DB 层无"每 type 单条"约束。写入分两种模式: + +| type | 写入方式 | 文档数 | +|---|---|---| +| `fastgpt` / `fastgptPro` | **append 历史式**:`config.ts:35-44` 每次保存 `create()` 插新文档;读取 `findOne().sort({ _id: -1 })` 取最新;`updateConfig.ts:53-59` 顺带删除 `createTime ≤ 1 个月前` 的旧文档 | 近一月保存次数 N 条(**正常设计**,非备份) | +| `license` / `systemMsgModal` / `operationalAd` / `activityAd` | **upsert 单文档**:`updateOne({ type }, { $set }, { upsert: true })` | 恒 1 条 | + +- `license` 激活(`active.ts:25-33`):**整体覆盖** `value: { license, data }`;`value.data` 是签名 payload,验签后只读 +- change stream(`volumnMongoWatch.ts:30-33`):`update` 无条件触发全量配置重载;`insert` 仅 `fastgptPro`/`license` 两个 type 触发 → **新增 type 的 insert 不触发重载** +- 集合多条 = fastgpt/fastgptPro 的近一月历史(正常);`license` 出现多条才是异常(并发激活竞态或历史遗留) + +### 6.3 实例 ID 存储方案对比 + +| 方案 | 做法 | 优点 | 缺点 | 结论 | +|---|---|---|---|---| +| X. 独立 type | 新增枚举 `instanceId`;`findOneAndUpdate({ type }, { $setOnInsert }, { upsert: true })` 原子生成一次,之后只读 | 集群一致、原子防竞态、insert 不触发 watch 重载、身份与授权生命周期解耦、恒 1 条 | 需改 schema 枚举(对外契约小改) | **推荐** | +| Y. 写进 license 文档 | `value` 顶层加 `instanceId` | 不加枚举 | `active.ts` 激活整体 `$set` 覆盖 value → 续签/重签/删 license 都会冲掉身份;必须改 active.ts 防覆盖,身份与授权状态耦合 | 不推荐 | +| Z. 文件 | 持久卷 `data/instanceId` | 不依赖 DB | 多实例各自文件系统 → N 个容器 N 个 ID;只迁 Mongo 不带文件 → ID 丢失 | 不推荐 | + +### 6.4 实例 ID 生成(推荐:独立 type + `$setOnInsert` + 纯 DB) + +``` +instanceId = crypto.randomBytes(16).toString('hex') // 32 位 hex +``` + +- 存储:`MongoSystemConfigs` 新增枚举 `SystemConfigsTypeEnum.instanceId`,`value: { instanceId }` +- 生成(首次启动,幂等防竞态,全集群只生成一次): + +```ts +// 固定 _id(24 位 hex 合法 ObjectId):主键唯一约束保证并发下只生成一次。 +// 不能用 type 做并发去重 —— systemConfigs 的 type 索引非唯一(fastgpt/fastgptPro 需同 type 多文档历史)。 +const INSTANCE_ID_DOC_ID = '000000000000000000000001'; + +await MongoSystemConfigs.findOneAndUpdate( + { _id: INSTANCE_ID_DOC_ID }, + { + $setOnInsert: { + _id: INSTANCE_ID_DOC_ID, + type: SystemConfigsTypeEnum.instanceId, + value: { instanceId }, + createTime: new Date() + } + }, + { upsert: true, new: true } +); +``` + +- 之后**永不 update**:换 license / 重签 / 删 license 均不影响身份;change stream 收到 `insert`(非 fastgptPro/license)→ 不触发配置重载,零副作用 +- 校验读取:`findOne({ _id: INSTANCE_ID_DOC_ID })`,与 license 验签并行 +- **放弃文件双写**(多实例不一致 + 迁移丢失,见方案 Z) +- ⚠️ 并发正确性验证:`type` 索引非唯一,`findOneAndUpdate({ type })` 并发 upsert 会插入多条(测试实测 2 条);改用固定 `_id` 后并发 8 路仅 1 条(`instanceId.test.ts` 覆盖) + +> 防丢策略说明:实例 ID 清除 = 客户失去绑定凭据,需联系官方在签发侧解绑/重签。这是离线方案的固有代价,需在售卖/文档中明确。 + +### 6.5 验证逻辑(`authLicense` 增强) + +顺序:验签 → 过期 → **实例校验** → 归一化 → functions 合并。 + +实例校验规则(后端读自己的 instanceId,**不信任前端传参**): + +```ts +const localInstanceId = await getInstanceId(); +const boundInstanceId = licenseData.instanceId + ?? (await getBoundInstanceId()); // 存量:独立 type licenseBind 中记录的绑定 +if (boundInstanceId && boundInstanceId !== localInstanceId) { + reject('License 与当前实例不匹配'); +} +``` + +### 6.6 存量 license 迁移(老客户零干预) + +1. 删除 `pro/admin Header.tsx` 的前端 hosts 校验(后端本就不校验,移除无损) +2. 存量 license(有 `hosts` 无 `instanceId`)首次验证通过后:自动绑定当前实例,绑定关系写入 **独立 type `licenseBind`**(`value: { boundInstanceId, boundAt }`,upsert)——**不动 license 文档、不改签名 payload** +3. 校验规则(存在即校验,堵传播): + - license 有 `instanceId` → 与本地实例 ID 比对 + - license 无 `instanceId` → 读 `licenseBind.boundInstanceId`:无 → 绑定当前实例并写库;有且不等于本机 → 拒绝 + - 两处都没有 → 放行(未绑定) +4. 解绑/换机:客户换服务器 → 官方在签发侧重签带 `instanceId` 的新 license;`licenseBind` 随旧 license 过期自然失效(记录保留作审计) + +### 6.7 激活流程(app 内) + +1. app Layout 未激活时弹 `LicenseInput`(接入现有组件) +2. 弹窗展示"当前实例 ID: xxx"(替代"当前域名为"),客户把实例 ID 发给官方 +3. 官方签发绑定该实例 ID 的 license +4. 激活接口 `POST /proApi/admin/common/license/active` 验签后落库 + +### 6.8 消费方适配 + +- `licenseCheck`、配额校验、功能开关:**无需改动**(仍读 `global.licenseData`,仅字段路径变化见 §5.4) +- 实例校验失败时 `authLicense` 置 `global.licenseData = undefined`,自然拦截 + +## 7. 边界与风险 + +| 风险 | 影响 | 缓解 | +|---|---|---| +| 实例 ID 被清除 | license 失效,需官方解绑 | 文档明确 + 官方侧解绑流程;DB 单写降低概率(纯 DB 无文件丢失路径) | +| 存量 license 无 instanceId | 校验跳过或读 boundInstanceId | 自动绑定迁移(§6.6) | +| 同一 license 多实例共用 | 无法阻止(离线方案天然限制) | 接受;未来在线验证可解 | +| 功能默认 true | sso/pay 忘写 false 即免费开启 | 签发工具默认值模板显式列出全部开关 | +| 存量 license 归一化 | customTemplates 丢失(功能已移除) | 预期行为;旧客户不受影响(该功能已下线) | +| DB 迁移/容器重建 | instanceId 变化 | 纯 DB:dump/restore 必须携带 systemConfigs 集合;部署文档强调 | +| 存量 license 首次绑定前可传播 | 绑定发生在首次激活时 | 迁移窗口接受(现状本就无后端绑定);续签换新格式后消除 | +| 前端拿到 instanceId 泄露 | 仅用于签发绑定,无敏感能力 | 非机密,可公开展示 | + +## 8. 待确认项 + +1. **datasetEnhance 去留**:用户未提及。保留(现有 3 个定时任务 + 前端开关仍消费)还是移除(与 customTemplates 一起清理)? +2. **默认 true 范围**:全部功能(含 sso/pay)默认 true,还是仅新增功能(assistantGenerate/portal/sandboxSkills)默认 true?影响签发侧默认模板。 +3. **评估命名**:`eval`(更通用,未来可扩展其他评估)还是保持 `batchEval`(避免改名迁移)? +4. **新功能命名**:辅助生成 / 门户 / 沙盒与技能 的英文字段名(暂定 `assistantGenerate` / `portal` / `sandboxSkills`)? +5. **version 语义**:`trial` 与 `official` 的差异规则?建议:trial = 功能全开 + 固定短过期时间 + UI 展示"试用版"标签;official = 正式授权。是否还需要 trial 专用配额档? +6. **新增功能是否接入 UI**:assistantGenerate/portal/sandboxSkills 当前无消费点,本期只做类型 + 展示,还是同时接导航/开关? +7. **签发工具**:仓库内无签发工具(私钥在官方),是否需要新建内部签发 CLI 以支持新结构、version、instanceId 签发与解绑? +8. **存量客户**:自动绑定实例(推荐)还是要求换新 license? +9. **instanceId 存储**:✅ 已确认(2026-08-31):独立 type + `$setOnInsert` + 纯 DB(§6.4);`licenseBind` 存在即校验(§6.6) +10. **签发服务部署位置**:独立私有仓库(推荐,私钥绝不进公开仓库)还是 pro/ 目录? +11. **签发鉴权**:保留 psw 签发人映射,还是升级管理员账号 + session? +12. **密钥对**:复用现有(公钥已在 auth.ts,存量兼容)还是更换(需验证侧双公钥灰度)? +13. **签发记录库**:MongoDB(原 laf 一致)还是 SQLite(轻量单机)? +14. **签发页面范围**:仅表单,还是含记录查询/重签/验证? + +## 9. 签发服务重构 + +### 9.1 原签发云函数(Laf,仓库外,已记录) + +```ts +// @lafjs/cloud 云函数,部署于 Laf 平台,FastGPT 官方内部使用 +// 鉴权:psw 密码 → 签发人映射(余金隆/老根/王天赐/杨道升/深信服) +// 私钥:privateKey(laf 环境注入,不在代码中) +// 记录:MongoDB new_fastgpt_license 集合,含 creator 签发人审计 +``` + +**流程**: + +1. 校验:`psw` 在签发人映射中、`hosts` 非空数组、`expiredTime > startTime`、`functions` 为对象、`company` + `description` 必填 +2. 构建 `licenseData`:`{ company, description, hosts, networkIds, maxUsers, maxApps, maxDatasets, functions }` +3. `payload = base64(JSON.stringify({ ...licenseData, startTime, expiredTime }))` +4. `signature = RSA-SHA256 签名(payload, privateKey, base64)` +5. `license = signature + payload` +6. 落库(含 creator)+ 返回 `{ license }` + +**遗留问题**: + +| 问题 | 说明 | +|---|---| +| `licenseType` 未进 payload | 入参有 `licenseType: "poc" \| "official"`,但构建 licenseData 时被丢弃,payload 无版本字段 | +| `networkIds` 无类型定义 | 进 payload(深信服网关),但 `LicenseDataType` 未声明 | +| `hosts` 必填校验 | 新方案换 `instanceId` 后校验对象变化 | +| functions 无默认值 | 签发侧必须写全 5 个开关,新功能上线要改签发工具 | +| 纯 API 无界面 | curl/Postman 操作,靠 psw 鉴权 | +| 记录只 insert | 无查询/重签/审计界面 | + +### 9.2 新签发服务设计(Hono + JSX 页面) + +**技术栈**:Hono + `@hono/node-server`(复用 code-sandbox 先例:Node 22 + tsx dev + vitest),`hono/jsx` 渲染页面。 + +**定位**:官方内部工具,独立部署(Docker)。**私钥不入仓库/镜像**——以环境变量 `LICENSE_PRIVATE_KEY` 或挂载 `private_key.pem` 注入。代码放独立私有仓库(FastGPT 为公开仓库,pro/ 亦在其中,私钥绝不能进)。 + +**页面(hono/jsx)**: + +| 路由 | 功能 | +|---|---| +| `/` | 签发表单:company、description、version(trial/official)、instanceId(绑定)、startTime/expiredTime、limits(maxUsers/maxApps/maxDatasets)、functions 开关(默认全开,可关) | +| `/list` | 签发记录:分页、按 company 过滤、查看 payload 与 license、复制按钮 | +| `/verify` | 输入 license 字符串 → 公钥验签 + 展示 payload(排查/自检) | + +**API**: + +| 接口 | 说明 | +|---|---| +| `POST /api/license/create` | 签发:body = 新 LicenseDataType + psw,返回 `{ license, record }` | +| `GET /api/license/list` | 记录查询(鉴权 + 分页) | +| `POST /api/license/reissue` | 重签:客户换 instanceId / 延期,基于原记录生成新 license,旧记录保留 | +| `GET /api/license/verify` | 公钥验签 + payload 展示 | + +**服务端校验**(签发前): + +- `version ∈ {trial, official}`,缺省 `official` +- `instanceId` 格式 32 位 hex(是否必填待定:trial 是否可免绑定) +- `startTime < expiredTime` +- `company` 必填 +- `limits` 数值 ≥ 0 +- `functions` 只接受已知键,未知键拒绝(防旧字段 `customTemplates` 混入) +- 功能开关 UI 默认全开,关闭需显式勾掉 + +**密钥管理**: + +- 默认**复用现有密钥对**(公钥已在 `auth.ts`,换对 = 存量 license 全失效) +- 确需更换:验证侧支持双公钥(payload 加 `pubkeyVersion`,按版本选公钥),灰度过渡 + +**数据库**: + +- MongoDB(原 laf 一致、FastGPT 生态一致)或 SQLite(单机工具更轻) +- 集合/表 `licenses`:`license`(全文)、`payload`(解析后)、`creator`、`company`、`instanceId`、`version`、`expiredTime`(索引)、`createTime`、`status`(active/revoked,仅内部审计标记,**无法远程吊销已签发 license**——离线验签体系决定) + +**与验证侧配合**: + +- 新签发结构 → `auth.ts` 归一化(§5.3)读取 +- 公钥不变 → 存量 license 兼容 +- instanceId 流程:客户提供实例 ID → 签发 → 激活(§6.5) + +**部署**: + +- Dockerfile(node:22-alpine + tsc build + @hono/node-server serve) +- 独立于 FastGPT 主仓库部署;内网/VPN 访问 + +### 9.3 签发服务风险 + +| 风险 | 影响 | 缓解 | +|---|---|---| +| 私钥泄露 | 可伪造任意 license | 不入仓库/镜像,env/挂载注入;签发记录审计(creator);轮换流程 | +| 换密钥对 | 存量 license 全失效 | 默认复用现有对;确需更换走双公钥灰度(§9.2 密钥管理) | +| psw 弱鉴权 | 未授权签发 | 服务仅内网/VPN;页面 session;待确认加强方案(§8.10) | +| 签发记录丢失 | 审计缺失 | Mongo 持久化 + 备份 | +| 无法远程吊销 | 已发 license 无法作废 | 接受(离线体系固有);记录 status 标记 + 到期自然失效;续费靠重签 | + +## 10. 后续步骤 + +1. 确认 §8 决策 +2. 按 AGENTS.md 流程:需求文档 → 开发文档 → TODO → 实施 +3. 实施涉及文件: + - `packages/global/common/system/types/index.ts`(类型重构) + - `pro/admin/src/service/common/license/auth.ts`(归一化 + 默认 true + 实例校验) + - app 侧 `getInstanceId` 工具(新) + - `teamLimit.ts`、`authMaxUsers`(limits 路径) + - `system/index.ts`(functions 新字段) + - `LicenseData.tsx`(pro + app 展示层) + - `AdminContainer.tsx`、`pro Navbar.tsx`(customTemplates 移除 + 新功能接入) + - `pro/admin Header.tsx`(删除 hosts 校验) + - app `components/admin/License/Input.tsx`(实例 ID 展示) + - 官方签发工具(仓库外) diff --git a/.agents/design/admin/pro-admin-ui-migrate.md b/.agents/design/admin/pro-admin-ui-migrate.md new file mode 100644 index 000000000000..45e1cc67f8cd --- /dev/null +++ b/.agents/design/admin/pro-admin-ui-migrate.md @@ -0,0 +1,436 @@ +# Pro Admin UI 迁移到前台 App 管理员侧栏 — 设计方案 + +> 状态:**已确认,进入实施(方案 A)** +> 范围:**仅 UI 部分**(页面组件、导航壳层、路由、前端数据访问封装) +> 关联代码:`pro/admin/`(源)、`projects/app/`(目标) + +### 实施进度(2026 更新) + +**✅ 全部完成(P0~P3 UI 迁移 + 方案 B 后端接口迁移)**,tsc 全量 0 error、eslint 0 error(warning 为迁移代码风格警告,与 pro/admin 源一致)。 + +**方案 B 后端接口迁移(2026 追加)**: + +- **T0**:`adminCert`(root 鉴权)迁移到 `projects/app/src/service/support/permission/`,复用 app 的 NextAPI middleware(无 licenseCheck) +- **T1(零依赖 18 个)**:users/teams/apps/datasets 列表与编辑、log、templates×6、templateType×3、audit adminList +- **T2(自包含 schema 10 个)**:dashboard×7、pays、invoice×2 + `MongoBill`/`MongoInvoice` schema(`service/support/wallet/bill/`) +- **T3(少量 service 7 个)**:plans×3 + `wallet/sub/controller`、`wallet/controller`(含 `global.reduceAiPointsQueue` 等声明,迁移到 `service/support/wallet/type.ts`)、inform×4 + inform controller(含 `sendInformQueue` 全局声明);users addUser/delete(依赖 license auth + 账号注销/验证子系统:`common/license/auth`、`user/controller`、`account/cancellation` 全套、`verification/{code,oauth,wechat}`、`wecom/{const,utils,type,accessToken}`) +- **T4(settings/config 2 个)**:getConfig/updateConfig + `common/system/config`、`admin/settings/hooks`(adminEnv→process.env 适配)、`init.ts`(仅 applyProRuntimeFeConfigs)、`enterpriseAuth/env`、`admin/settings/type.ts`(SystemConfigType/ConfigStoreType 服务端类型) +- **新增依赖**:`@tanstack/react-table`、`nodemailer`、`@alicloud/*`、`xml2js`、`canvas`(catalog 注册) +- **类型适配**:`global.systemConfig` 宽类型断言(packages/service 声明为 `Record`,迁移代码按 pro 结构本地断言)、`adminEnv`/`SMS_PROXY`/`BATCH_UPDATE_TIME`/`WECHAT_AUTH_TOKEN` 等 env → `process.env` 读取 +- **验证**:dev server 实测所有迁移接口从 404 → 403(root 认证拦截)或 500(body 校验),路由真实存在;app 首页/登录正常。 + +**license 认证迁移(追加)**:激活/校验接口(`admin/common/license/active|auth`)+ 前端 License 输入组件(`components/admin/License/Input.tsx`)+ app `useSystemStore` 增加 `licenseData/initLicenseData/clearLicenseData` + `web/common/license/api.ts`。开源版后续可在未激活时提示购买商业版(LicenseInput 弹窗),迁移完成且接口实测 200/500(假 license 激活校验失败为预期)。 + +### 接口调用架构调整(2026 评审):UI 改调 pro/admin 接口 + +**决策**:迁移后的 UI **不再调用 app 侧复制的接口**,改为通过 `/proApi` 代理调用 **pro/admin 服务的接口**(`FastGPTProUrl` 配置)。app 侧迁移的后端副本已删除,恢复开源版纯净。 + +**改动**: + +- **前端 API 封装**(`web/admin/*`、`web/core/app/templates`、`web/common/system/inform`、`web/support/wallet/invoice`、`web/common/license`)所有接口路径加 `/proApi` 前缀;页面内直接调用的 `GET/POST('/admin/...')`(dashboard、plans/users/teams 的 modal 组件)同步加前缀。前端路由(`/admin/*` 跳转)不受影响。 +- **降级逻辑**(`web/admin/common/request.ts`):识别 `/proApi` 请求在 pro 服务未配置(500 + 未配置商业版链接 / ECONNREFUSED)或 404 时静默降级为空数据,保证开源版(不部署 pro/admin)页面骨架可渲染。 +- **license 检测**(`web/common/license/api.ts`):`getLicenseData` 识别降级空结构返回 `undefined`(视为未激活),保证开源版 root 正确触发激活/购买弹窗。 +- **删除 app 侧迁移副本**:`pages/api/admin/{common,core,routes,support/user,support/wallet}`、`getFeConfigs.ts`、`getTemplateTypes.ts`、`pages/api/support/user/{audit,inform}`;`service/{admin,common/license,common/system/config*,core/changeOwner,init,support/permission/adminCert,support/user/account 迁移部分,support/user/controller,support/user/inform 迁移部分,support/user/team,support/wallet 迁移部分,support/wecom}`。**保留** app 原有:`appRegistration`、`initv*`、`dataClean`、`4160/4161`、`service/support/wallet/usage/utils.ts`(回退 git 原始版,仅 `authType2UsageSource`)、`service/support/user/account/password.ts`(误删后恢复)、`service/support/user/inform/api.ts`。 + +**结果**:UI 调 pro/admin 接口(单一后端,无双份漂移);pro 服务未配置时开源版页面骨架可用 + root 弹 license 激活/购买提示。tsc 0 错误。 + +### 管理员主页(2026 新增) + +- 侧栏**一级菜单项"管理员主页"**(`/admin/home`,位于审计日志之后) +- 页面展示:当前版本状态(开源社区版/商业版 Tag)+ 激活/变更 License 按钮 + license 详细信息(复用迁移的 `LicenseData` 组件) +- 未激活:显示开源版说明 + 激活按钮触发 LicenseInput;已激活:显示 license 完整信息(公司/过期时间/用户数/应用数/知识库数/功能开关) +- 依赖:`components/admin/License/{LicenseData,Input}.tsx`、`public/icon/user.svg`(从 admin 复制) + +### 菜单结构调整(2026 评审) + +- **系统工具**(/admin/config/plugin)、**模型提供商**(/admin/config/modelProvider)从"系统配置"子项提升为**独立一级菜单项**(位于系统配置之后、模板&工具之前)。原因:这两个是原有的 /config 能力(root 配置入口),与 pro/admin 迁移过来的"系统配置"子项(基础/功能/安全/第三方/用户)性质不同,独立成项更清晰。 + +### 剩余工作清单(2026 待办) + +1. **LicenseInput 接入 app Layout**:组件已迁移(`components/admin/License/Input.tsx`)+ useSystemStore 已支持 license,但未在 app Layout 接入"未激活时弹 LicenseInput"逻辑(pro/admin 是 `!licenseData && `)。接入后即可实现"开源版未激活提示购买商业版"(结合现有 ProModal)。 +2. **迁移接口单测**:pro/admin 有 7 个迁移接口的测试(updateConfig/getPays/getPlans/updateUser/getUsers/getTeams/login),迁移到 `projects/app/test/api/admin/` 对应路径(接口与依赖已迁移,测试路径一致,主要改 import 的 mock 路径)。 +3. **eslint warnings 清理**:50 个 no-unused-vars warning(迁移代码风格,不影响功能,可批量 `--fix` 或保留)。 +4. **dev 端到端验证**:接口已实测(403 认证拦截 / 200 公开接口),但 root 登录后的完整 UI 流程(列表渲染、配置保存)需人工验证。 +5. **audit 页面 getTeamMembers 依赖 /proApi**:app 既有行为(需 FastGPTProUrl 配置),非迁移引入。 + +关键产出: + +- **侧栏壳层**:`SecondaryNavigationContainer` 增加两级分组能力(新组件 `components/SideTabs/Group.tsx`,向后兼容 account 页);`pageComponents/admin/AdminContainer.tsx`(root 校验 + 两级菜单 + license 常量开关) +- **路由**:25 个 `/admin/*` 页面(dashboard 5 子页、users 5、resources 2、audit、inform、log、config 6 + plugin/modelProvider、templates 2);旧 `/config/*` 重定向 + 三处旧跳转指向修正 +- **通用组件**:`components/admin/`(BoxCard、markdown)、`pageComponents/admin/settings/`(表单系列)、`components/admin/Settings/PlanComponents.tsx`(迁移时把 `@tanstack/react-table` 加入 catalog) +- **前端 API 封装**:`web/admin/`(request 静默降级 + 各模块 api + config/adapt),`web/core/app/templates/api.ts`(类型内联),`web/common/system/inform/api.ts`,`web/support/wallet/invoice/api.ts` +- **配置页类型**:`pageComponents/admin/config/type.ts`(SystemConfigType/ConfigStoreType/ConfigFormType/TeamModeEnum 本地组装,去 pg 依赖与 declare global) + +迁移中保留的技术债(未重构,避免行为变化): + +- `react-hooks/incompatible-library`(12 处,`watch()` 不能 memoize 的用法)与 `exhaustive-deps`(2 处)——pro/admin 源文件同样存在 +- `@ts-expect-error`/`setState-in-effect` 等已在迁移文件中用行级注释处理并说明 +- License 相关 UI(套餐/支付/开票/模板市场)侧栏默认隐藏,`AdminContainer` 中 `adminLicenseFunctions` 常量开关预留,商业版接入 license 时替换 + +### 已确认决策(2025 评审结论) + +1. **API 策略:方案 B(全量迁移)** — 2026 评审后从方案 A 升级:UI 迁移完成且评审通过后,确认后端接口也需要全部迁移(用户选择"全部迁移"),使 `/admin/*` 接口在 app 后端真实可用(不再 404)。按 T1(零依赖)→ T2(自包含 schema)→ T3(少量 service)→ T4(深耦合 settings/config)分层推进,license/login 接口不迁。 +2. **侧栏形态:两级分组侧栏** — 对齐 pro/admin 视觉,父级可展开子项;需扩展 `SecondaryNavigationContainer`(向后兼容,不影响 account 页)。 +3. **现有 /config 并入 /admin/config** — 旧路由保留重定向;Navbar 的 root 入口替换为"管理"。 +4. **移动端:仅 PC 提供管理员入口** — 移动端 navbarPhone 不加管理员入口。 +5. **空数据态:静默降级** — 方案 A 阶段前端 request 封装对 `/admin/*` 接口统一拦截;方案 B 迁移完成后 404 变为真实响应,降级自动失效,前端封装无需改动。 + +--- + +## 1. 背景与目标 + +FastGPT 商业版(`pro/`)中有一个独立的管理后台应用 `pro/admin`(`@fastgpt/admin`,运行在 3001 端口)。它拥有自己的一套后端 API、登录认证和 License 系统,是一个完整的独立 NextJS 应用。 + +目标:把 pro/admin 的 **UI 部分**迁移到前台主应用 `projects/app` 中,以"管理员侧栏"的形式集成,使 root 用户在 app 内即可完成管理员操作,无需单独部署/访问 admin 应用。 + +本次任务范围明确为 **仅 UI**:迁移页面组件、导航壳层、路由挂载和前端数据访问封装;**后端 API 业务逻辑的迁移不在本次范围内**(详见 §8 风险与待确认项)。 + +## 2. 现状分析 + +### 2.1 pro/admin(源)结构 + +**应用骨架** + +- `src/pages/_app.tsx`:QueryClient + ChakraProvider + SystemStoreContextProvider + appWithTranslation +- `src/components/Layout/index.tsx`:Header(60px 顶栏,含 Admin logo、License 信息、退出登录)+ Navbar(217px 左侧导航)+ 内容区 +- `src/components/Layout/Navbar.tsx`:两级导航(带子菜单展开),8 大模块 +- `src/components/Layout/Auth.tsx`:通过 `useAdminStore.initAdminInfo()`(`/admin/support/user/adminCert`)鉴权 +- `src/store/useAdminStore.ts`:admin 登录态(zustand + persist) +- `src/web/common/system/useSystemStore.ts`:`licenseData` + `feConfigs`(License 系统,app 没有) + +**导航模块(Navbar 的 LIST)** + +| # | 模块 | 子项 | License 控制 | +| --- | ------------------------------- | ---------------------------------------------------------- | ------------------------------------- | +| 1 | 数据面板 /dashboard | 全局统计/流量/付费/活跃/成本(页内 tab) | — | +| 2 | 通知管理 /inform | — | — | +| 3 | 日志管理 /log | — | — | +| 4 | 用户管理 /users | 用户信息/团队管理/套餐管理/支付记录/开票管理 | 后 3 项需 `licenseData.functions.pay` | +| 5 | 资源管理 /resources | 应用管理/知识库管理 | — | +| 6 | 系统配置 /settings/config | 基础配置/功能清单/安全配置/第三方提供商/用户配置/套餐&充值 | 套餐&充值需 pay | +| 7 | 模板 & 工具 /settings/templates | 模板市场/工具箱 | 需 `customTemplates` | +| 8 | 审计日志 /audit | — | — | + +**页面/组件规模(纯 UI 部分)** + +| 模块 | 页面文件 | 行数 | 说明 | +| ------------------ | ---------------------------------------------------------------------------------------- | ----- | ------------------------------ | +| dashboard | 5 页 + Header.tsx + utils.ts | ~790 | 统计卡片 + tab 切换 + 日期范围 | +| inform | 1 页 | 350 | 系统通知/广告位管理 | +| log | 1 页 | 271 | 日志列表 | +| users | 5 页 + 7 个 modal 组件 | ~2432 | 用户/团队/套餐/支付/开票 | +| resources | 2 页 | 308 | 应用、知识库列表 | +| settings/config | 6 页 + FormField×6 + FormLabel + ImportModal | ~4830 | 最大模块,含通用表单组件 | +| settings/templates | 2 页 + 4 个组件 | ~1385 | 模板市场/工具箱 | +| audit | 1 页 | 302 | 审计日志 | +| **通用组件** | BoxCard / Pagination / markdown / Settings 表单系列 | ~1645 | 需一并迁移 | +| **前端 API 封装** | `web/admin/*/api.ts`、`web/core/config/api.ts`、`web/common/{license,system,file,i18n}/` | ~707 | 页面数据访问 | + +**页面通用模式** + +```tsx +'use client'; +import { GET } from '@/service/common/request'; // admin 自己的请求封装 +import BoxCard from '@/components/common/BoxContainer/Card'; +import { useRequest, usePagination } from '@fastgpt/web/hooks/...'; +import { serviceSideProps } from '@/web/common/i18n/utils'; // SSR i18n +export async function getServerSideProps(content) { ... } // 每个页面都有 +``` + +- 绝大多数页面中文写死(62 个 tsx 中仅 7 个使用 i18n) +- 组件依赖集中在 `@fastgpt/web`(MyIcon/MyModal/useRequest/usePagination/useSystem),app 可用 +- 数据全部来自 `/admin/...` 接口(见 §8.1) + +### 2.2 前台 app(目标)现状 + +**导航骨架** + +- `src/components/Layout/navbar.tsx`:64px 图标侧栏。现有入口:Chat / Studio / Datasets / Account,以及 root 用户的 **Config**(`/config/plugin/tool`、`/config/model`) +- `src/components/Layout/navbarPhone.tsx`:移动端底部栏 +- `src/pageComponents/common/SecondaryNavigationContainer.tsx`:**账号与管理员页面共用的二级导航壳层**(PC 220px 固定侧栏 / 移动端顶部横向 tab)——直接复用为管理员侧栏 +- `src/pageComponents/config/ConfigContainer.tsx`:root 的 Config 二级导航,目前 2 个 tab(plugin=系统工具、model=模型提供商) + +**权限模型** + +- 无独立 admin 认证;root 判定为 `userInfo?.username === 'root'`(Navbar、ConfigContainer 中已有先例) +- 无 License 系统(`useSystemStore` 无 `licenseData`) + +**已有 admin API(app 后端)** + +- 仅 init 迁移脚本类(`initv4xxx.ts`、`dataClean/*`、`4160/4161`),**没有 pro/admin 的运营管理接口** + +## 3. 目标架构 + +``` +projects/app +├── src/pages/admin/ # 新增管理员路由树(root 可见) +│ ├── dashboard/index.tsx # 数据面板 +│ ├── inform.tsx # 通知管理 +│ ├── log.tsx # 日志管理 +│ ├── users.tsx / teams.tsx / plans.tsx / pays.tsx / invoice.tsx +│ ├── apps.tsx / datasets.tsx # 资源管理 +│ ├── config/ # 系统配置(并入现有 /config 能力) +│ │ ├── basic.tsx / feature.tsx / model.tsx / thirdParty.tsx / user.tsx / pay.tsx +│ │ ├── plugin.tsx # ← 现有 /config/plugin/tool 迁入 +│ │ └── modelProvider.tsx # ← 现有 /config/model 迁入 +│ ├── templates/ # 模板 & 工具(license) +│ └── audit.tsx # 审计日志 +├── src/pageComponents/admin/ # 迁移的页面级组件(含模块私有子组件) +├── src/components/admin/ # 迁移的通用组件(BoxCard、Settings 表单系列等) +├── src/web/admin/ # 迁移的前端 API 封装 +└── src/pageComponents/common/AdminContainer.tsx # 管理员二级导航壳层 +``` + +**入口改造** + +- `navbar.tsx`:root 用户新增"管理"图标(icon 复用 `common/administrator`),activeLink 覆盖 `/admin` +- 现有 `/config/plugin/tool`、`/config/model` 迁移进 `/admin/config/` 后,保留旧路由 302 跳转(避免破坏历史链接) +- 移动端 `navbarPhone.tsx` 同步加"管理"入口(或仅 PC 提供,待确认,见 §8.5) + +## 4. 路由映射 + +| pro/admin(源) | app(目标) | 说明 | +| --------------------------------------------------- | ------------------------------- | ---------------------------------------------------------- | +| /dashboard(含 traffic/payment/active/cost 子 tab) | /admin/dashboard | 页内 tab 保留 | +| /inform | /admin/inform | | +| /log | /admin/log | | +| /users/users | /admin/users | | +| /users/teams | /admin/teams | | +| /users/plans | /admin/plans | license.pay | +| /users/pays | /admin/pays | license.pay | +| /users/invoice | /admin/invoice | license.pay | +| /resources/apps | /admin/apps | | +| /resources/datasets | /admin/datasets | | +| /settings/config/basic | /admin/config/basic | | +| /settings/config/feature | /admin/config/feature | | +| /settings/config/model | /admin/config/model(安全配置) | 注意与现有 /config/model(模型提供商)重名,目标路由需区分 | +| /settings/config/thirdParty | /admin/config/thirdParty | | +| /settings/config/user | /admin/config/user | | +| /settings/config/pay | /admin/config/pay | license.pay | +| /settings/templates/app | /admin/templates/app | license.customTemplates | +| /settings/templates/toolkit | /admin/templates/toolkit | license.customTemplates | +| /audit | /admin/audit | | +| — | /admin/config/plugin | 现有 /config/plugin/tool 迁入 | +| — | /admin/config/modelProvider | 现有 /config/model 迁入 | + +> 命名冲突提醒:pro/admin 的 `/settings/config/model` 是"安全配置"(安全校验相关),app 现有 `/config/model` 是"模型提供商"。迁入 /admin 后必须区分(建议 `model` = 安全配置,`modelProvider` = 模型提供商,具体命名待确认,见 §8.4)。 + +## 5. 管理员侧栏设计 + +复用 `SecondaryNavigationContainer` 作为壳层,新增 `AdminContainer.tsx`: + +``` +数据面板 admin/dashboard +用户管理 admin/users · admin/teams · admin/plans · admin/pays · admin/invoice +资源管理 admin/apps · admin/datasets +系统配置 admin/config/basic · feature · model · thirdParty · user · pay + └ 系统工具(plugin) · 模型提供商(modelProvider) [并入现有 Config] +模板&工具 admin/templates/app · toolkit [license.customTemplates] +通知管理 admin/inform +日志管理 admin/log +审计日志 admin/audit +``` + +**结构差异与处理** + +- `SecondaryNavigationContainer` 当前是**单层** tab 列表(SideTabs);pro/admin 的 Navbar 是**两级**(父级可展开子项)。两个选项: + - **A. 两级侧栏**:扩展 SideTabs/壳层支持分组(父项 + 子项),视觉与 pro/admin 一致 —— 推荐 + - **B. 扁平化**:8 个一级项平铺(数据面板/通知/日志/审计单独成项,用户/资源/系统配置展开为子项时用分隔线或二级分组) +- 选项 A 需要给 `SecondaryNavigationContainer` 增加分组能力;注意它同时被 account 使用,改动需向后兼容 + +**权限控制** + +- `AdminContainer` 与 root 校验逻辑对齐 `ConfigContainer`:非 root 跳回 `/account/info` +- Navbar 的管理员入口仅 root 可见 + +## 6. UI 迁移清单 + +### 6.1 页面组件(pro/admin → app) + +| 源 | 目标 | 备注 | +| --------------------------------------------------------------------- | -------------------------------------------- | -------------------- | +| pages/dashboard/{index,traffic,payment,active,cost}.tsx | pageComponents/admin/dashboard/\* | 共用 DashboardHeader | +| pageComponents/core/dashboard/Header.tsx + utils.ts | pageComponents/admin/dashboard/Header.tsx | tab + 日期范围 | +| pages/inform/index.tsx | pageComponents/admin/inform/ | | +| pages/log/index.tsx | pageComponents/admin/log/ | | +| pages/users/users/\*(含 3 个 modal) | pageComponents/admin/users/\* | | +| pages/users/teams/\*(含 2 个 modal) | pageComponents/admin/teams/\* | | +| pages/users/plans/_、pays/_、invoice/\* | pageComponents/admin/{plans,pays,invoice}/\* | license | +| pages/resources/apps,datasets | pageComponents/admin/{apps,datasets}/\* | | +| pages/settings/config/{basic,feature,model,thirdParty,user,pay}.tsx | pageComponents/admin/config/\* | | +| pages/settings/config/components/FormField/\*、FormLabel、ImportModal | pageComponents/admin/config/components/\* | | +| pages/settings/templates/\* | pageComponents/admin/templates/\* | license | +| pages/audit/index.tsx | pageComponents/admin/audit/ | | + +### 6.2 通用组件(pro/admin → app) + +| 源 | 目标 | 备注 | +| ------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | -------------------------------- | +| components/common/BoxContainer/Card.tsx | components/admin/BoxContainer/Card.tsx | BoxCard,被多数页面使用 | +| components/common/markdown/\* | components/admin/markdown/\* | 通知管理使用 | +| components/Pagination/index.tsx | components/admin/Pagination/ | 检查是否能直接复用 app 已有分页 | +| pageComponents/Settings/{SettingPage,FormItem,FormLabel,Input,Select,Switch,ImageInput,FirstTitle,SecondTitle}.tsx | pageComponents/admin/settings/\* | 系统配置表单系列 | +| components/common/License/\* | 不迁移 | app 无 License,逻辑替换(§7.4) | + +### 6.3 前端数据访问封装 + +| 源 | 目标 | 备注 | +| -------------------------------------------------------------------- | ------------------------------ | ------------------------------- | +| web/admin/{apps,audit,common,config,datasets,pays,team,users}/api.ts | web/admin/\*/api.ts | 接口前缀 `/admin/...` 保持不变 | +| web/core/config/{api,adapt,utils}.ts | web/admin/config/\* 或直接并入 | dashboard 的 getInitFormData 等 | +| web/common/license/api.ts | 不迁移 | License 逻辑替换 | +| web/common/system/{useSystemStore,utils}.ts | 不迁移 | 改用 app 的 useSystemStore | + +### 6.4 改造点汇总(源 → 目标差异) + +| 项 | pro/admin | app | 处理 | +| -------- | -------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- | +| 请求封装 | `@/service/common/request` 的 GET/POST | app 的请求方式(`@/web/common/api` / fetch) | 页面里 `GET(...)` 改写成 app 请求工具(封装一个 admin 专用 request 也可) | +| SSR i18n | 每个页面 `getServerSideProps` + `serviceSideProps` | app 页面多无 SSR i18n | 删除 getServerSideProps,`useClientTranslation` 按需 | +| 认证 | `useAdminStore.initAdminInfo()` | `useUserStore` + `username==='root'` | AdminContainer 层校验,页面不再各自鉴权 | +| License | `useSystemStore.licenseData.functions.*` | 无 | 默认关闭对应导航/UI(§7.4) | +| 中文文案 | 写死 | 写死 | 本期保留写死中文,不做 i18n(§8.3) | +| Icon | MyIcon name(如 `support/user/userLight`) | 同一套 @fastgpt/web MyIcon | 确认 app 侧 icon 集存在,缺的补到 app 的 icon 库 | +| 路由跳转 | `router.push('/dashboard')` 等 | `/admin/...` | 统一改前缀 | + +## 7. 关键实现细节 + +### 7.1 请求层 + +app 内新增 `src/web/admin/common/request.ts`,封装 `GET/POST/DELETE` 指向 `/admin/...` 前缀接口,统一错误处理(对齐 app 现有 request 约定),页面迁移时把 `@/service/common/request` 替换为该封装,**接口路径保持与 pro/admin 一致**(后端迁移后即通,见 §8.1)。 + +### 7.2 AdminContainer 壳层 + +``` +AdminContainer(/admin 下所有页面的父容器) +├─ 权限:非 root → router.replace('/account/info')(对齐 ConfigContainer) +├─ 侧栏:SecondaryNavigationContainer 扩展分组能力(选项 A) +└─ tab 定义:§5 的菜单结构,license 项按默认值隐藏 +``` + +### 7.3 现有 Config 的并入 + +- `/config/plugin/tool` → `/admin/config/plugin`;`/config/model` → `/admin/config/modelProvider` +- `ConfigContainer` 的 tab 并入 AdminContainer 的"系统配置"分组 +- 旧路由 `/config/*` 保留重定向(`router.replace` 或 next.config redirects),避免 root 用户历史书签失效 +- Navbar 里 root 的 Config 入口替换为"管理"入口(activeLink 覆盖 `/admin` 与旧 `/config`) + +### 7.4 License 相关 UI + +app 无 licenseData。方案:AdminContainer 中维护一份常量开关(默认与开源版一致:`pay: false, customTemplates: false`),依赖项(套餐管理/支付记录/开票管理/模板&工具/套餐&充值 tab)**默认不渲染**;预留开关位,后续商业版接入 license 时替换为真实 licenseData。这样开源版 app 集成后不出现空页面。 + +### 7.5 移动端 + +`navbarPhone.tsx` 增加"管理"入口(root 可见),进入后侧栏走 SecondaryNavigationContainer 的移动端顶部横向 tab 模式。仪表盘子 tab(流量/付费/活跃/成本)在移动端用 FillRowTabs 横向滚动(pro/admin 已有先例)。 + +## 8. 风险、依赖与待确认问题 + +### 8.1 【关键决策】后端 API 处理策略(详细方案) + +#### 8.1.1 关键架构事实(调研结论) + +对 pro/admin 后端的深入调研发现以下事实,它们决定了选项空间: + +**事实 1:两个应用共享同一 MongoDB 与同一套鉴权体系。** +pro/admin 的鉴权 `adminCert`(`pro/admin/src/service/support/permission/adminCert.ts`,仅 30 行)= 共享包 `@fastgpt/service` 的 `authCert` + `username === 'root'` 检查——与 app 前端的 root 判断**完全同源**。root 在 app 登录后,其 token 理论上可直接通过 adminCert。 + +**事实 2:API 层是薄壳,且 middleware 与 app 几乎等价。** +pro/admin 的每个 API handler 模式为 `NextAPI(adminCert + Mongoose 查询 + 返回)`。其 `NextAPI` middleware(`pro/admin/src/service/middleware/entry.ts`)与 app 的(`projects/app/src/service/middleware/entry.ts`)仅差一个 `licenseCheck`(全局 license 未激活则拒绝全部请求)。接口的业务逻辑大部分直接写在 handler 里,依赖共享包 schema(`@fastgpt/service/...`)。 + +**事实 3:pro/admin 的 service 层是独立实现,但 UI 只依赖其中一小角。** +`pro/admin/src/service/` 共 169 文件 / 2.4 万行,含 license、wecom、支付、SSO、k8s、bullmq 等重基础设施。但 UI 实际调用的接口只有约 **36 个**(见 8.1.2 分层),依赖的 pro 专属 service 文件仅约 10~15 个,且多为自包含(schema/controller 级)。 + +**事实 4:import 路径基本可平移。** pro/admin service 文件的 `@/service/...` 前缀在 app 中同样指向 `projects/app/src/service/`,adminCert 等文件可近乎原样放入 app。差异仅在 `@/global/...`(pro/admin 本地 global → app 需改 `@fastgpt/global/...`)、`@/env`(adminEnv → app env)等少数 alias。 + +#### 8.1.2 UI 依赖接口的依赖分层 + +| 层级 | 接口(数量) | 后端依赖 | 迁移难度 | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------- | +| **T1 零依赖** | users CRUD×4、teams×3、apps×1、datasets×1、log×1、templates×6、templateType×3、audit adminList×1(约 **20 个**) | 仅 adminCert + 共享包 schema | 机械搬运,改 import | +| **T2 自包含 schema** | dashboard×7、pays×1、invoice×2(约 **10 个**) | + pro 专属 schema 文件(`MongoBill`(pays)、`MongoInvoice`),schema 本身自包含 | 低,需检查 app 侧 mongoose model 名无冲突 | +| **T3 少量 service** | plans×3、inform×4(约 **7 个**) | + `wallet/sub/controller`、pro 版 `inform/controller`(含 templates) | 中,需带 2~3 个 controller 及其传递依赖 | +| **T4 深耦合(不建议本期迁)** | settings(config)×2、license×2、login×1 | `applyProRuntimeFeConfigs`、license hooks、fastgptPro 配置体系、启动期 license 校验 | 高,牵涉 pro 的系统配置/启动体系合并 | + +> 说明:login 接口不需要迁——root 在 app 内已有登录态;license×2 不迁——License UI 默认隐藏(§7.4)。settings/config 的两张页面(基础配置等)UI 可以先迁,接口后补,或整体放到最后一阶段与 T4 一起处理。 + +#### 8.1.3 候选方案对比 + +**方案 A:纯 UI,接口全部不迁** + +- 范围:UI + 前端 API 封装(接口路径保持 `/admin/...` 不变) +- 结果:页面骨架/导航/交互可渲染,但所有数据请求 404,列表为空、表单提交失败 +- 优点:范围最小,纯前端工作 +- 缺点:**UI 迁移质量无法端到端验证**——分页、loading 态、错误提示、modal 提交后的刷新等行为全部验不了;后续接接口时几乎必然返工(真实错误码/数据形态和预期不符);"验收完成"是假象 +- 适合:只做视觉/结构评审,不打算让这批页面短期可用 + +**方案 B:UI + T1~T3 接口分层同步迁移(推荐)** + +- 范围:UI + 前端封装 + 约 37 个轻中量接口 + adminCert + 约 10~15 个自包含 service/schema 文件;T4(settings config)与 license 不迁 +- 落点:pro/admin 的 service 文件平移到 `projects/app/src/service/`(`@/service/...` import 路径不变),handler 平移到 `projects/app/src/pages/api/`(middleware 直接用 app 已有的 NextAPI,天然去掉 licenseCheck) +- 优点:**除系统配置页外全部页面端到端真实可用**;T1/T2 接口是薄壳机械搬运,风险低;adminCert 复用 app 已有 authCert,鉴权一致 +- 缺点:超出"仅UI"字面范围,后端工作量约为 +30%(37 个薄壳接口 + 少量 schema);需要做 mongoose model 注册冲突检查(pays、invoice 等 model 名在 app 侧确认不存在);两处同名 service 需防止行为漂移(pro/admin 与 app 各一份 controller 的后续同步成本) +- 关键注意点: + 1. invoice 接口牵涉 `wecom/controllers/invoice` 与 `sendEmail`(T2/T3 边界),若依赖过重可降级为"先迁 UI 不迁接口" + 2. pro 专属 schema(如 MongoBill)与 app/packages 中已有 schema 是否重复注册同一 collection,迁移时逐一核对 + 3. `readFromSecondary` 等查询选项依赖部署拓扑(secondary 节点),单机部署需确认行为 + +**方案 C:UI + 网关代理转发到独立 pro/admin 服务** + +- 做法:app 的 `next.config.ts` 加 rewrites,把 `/admin/*`(及 `/support/user/audit` 等)转发到独立部署的 pro/admin 服务(环境变量 `ADMIN_SERVICE_URL`) +- 前提:两应用共享 MONGODB_URI 和同一 TOKEN 签名体系(事实 1 已验证鉴权同源,部署上需保证 TOKEN_KEY 一致) +- 优点:后端零迁移,页面**立刻全功能可用**(含 settings/license);pro/admin 可独立演进;改动完全集中在 app 前端 + 部署配置 +- 缺点:部署拓扑长期复杂化(永远多一个服务);商业版交付物从"一个 app"变回"app + admin",与"合并入口"的初衷部分矛盾;跨服务 cookie/CORS 需要配置正确 +- 适合:pro/admin 在商业版部署中本来就会长期独立存在的场景,作为**过渡方案** + +**方案 D:UI + 前端 mock 数据层** + +- 做法:前端 API 封装内置 mock 开关(dev 环境返回假数据) +- 优点:可端到端演示,无后端工作 +- 缺点:mock 维护成本高(36 个接口的数据形态都要造);真实性差,验不出错误分支;后续仍需完整对接 +- 评价:除非要做产品演示,否则性价比低于 B + +#### 8.1.4 推荐与理由 + +**推荐方案 B(T1~T3 分层同步迁移),settings/config 页面与 T4 一起压到最后一个阶段。** + +理由: + +1. pro/admin 的接口是薄壳,T1 层 20 个接口基本是"改 import 就能跑"的机械工作,边际成本低(相对纯 UI 约 +30% 工作量,换来的是除配置页外全部页面可真实验证) +2. 方案 A 的"验收完成"无法兑现为可用功能,接口对接阶段的返工风险(loading/错误态/数据形态)会吞掉省下的工作量 +3. 方案 C 可作为部署过渡手段保留——即使选 B,商业版部署上 pro/admin 与 app 短期并存时也可用 rewrites 兜底;但长期目标仍是后端合并,避免双份 service 漂移 + +若确认方案 B,实施顺序建议调整为:**T0 骨架(AdminContainer + Navbar)→ T1 接口+页面(users/teams/apps/datasets/log/audit/templates)→ T2(dashboard/pays/invoice)→ T3(plans/inform)→ 最后 settings/config UI+接口(T4)**。每个 T 层完成后即可端到端验收,风险逐层释放。 + +### 8.2 【关键决策】侧栏形态 + +- A. 两级分组侧栏(对齐 pro/admin 视觉,需扩展 SecondaryNavigationContainer)— 推荐 +- B. 扁平单层侧栏(改动最小,但 8 模块 + 子项层级不直观) + +### 8.3 【范围】i18n + +- A. 保留写死中文(本次最小改动)— 推荐 +- B. 迁移时接入 app i18n(工作量 +,涉及全部文案 key) + +### 8.4 【命名】/admin/config 内页命名 + +pro/admin "安全配置"(model)与 app "模型提供商"(model)重名,需确定目标路由命名。 + +### 8.5 【范围】移动端管理员入口 + +- A. 仅 PC 提供管理员入口(移动端隐藏)— 推荐(管理员操作多在 PC) +- B. PC + 移动端都提供 + +### 8.6 其他风险 + +- app 与 pro/admin 的 `@fastgpt/web` 版本可能存在差异,迁移组件时需按 app 的依赖版本适配(usePagination/useRequest 签名等) +- pro/admin 依赖的 `@fastgpt/global/openapi/admin/*` 类型在 packages/global 中已存在,可直接复用,无需搬迁 +- icon 资源:pro/admin 用到的 MyIcon name 需在 app 的 icon 集中核对,缺失的补齐 + +## 9. 建议实施步骤(评审后) + +1. **P0 骨架**:新增 `/admin` 路由树 + AdminContainer 壳层 + Navbar 入口 + 旧 /config 并入与重定向;root 权限校验 +2. **P1 通用组件**:BoxCard、Settings 表单系列、Pagination、markdown 迁入 +3. **P2 核心模块**:dashboard(含子 tab)→ users → resources → audit(数据访问封装同步迁入) +4. **P3 其余模块**:inform → log → settings/config(6 页 + FormField 系列)→ templates +5. **P4 收尾**:移动端入口、icon 核对、空数据态、路由跳转复查 +6. **验证**:root 登录 app → 管理员侧栏 → 各页面渲染;非 root 不可见;旧 /config 链接跳转正常 diff --git a/.gitignore b/.gitignore index f1aa0474d63d..905ff48159d2 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ pro/admin/worker/ /pro/llm_benchmark/content_benchmark/eval-runs/ /pro/llm_benchmark/content_benchmark/.cache/ .gstack/ + +.pi diff --git a/packages/global/common/system/config/constants.ts b/packages/global/common/system/config/constants.ts index f679e3c9cf93..78b25b99bdd8 100644 --- a/packages/global/common/system/config/constants.ts +++ b/packages/global/common/system/config/constants.ts @@ -4,7 +4,8 @@ export enum SystemConfigsTypeEnum { systemMsgModal = 'systemMsgModal', license = 'license', operationalAd = 'operationalAd', - activityAd = 'activityAd' + activityAd = 'activityAd', + instanceId = 'instanceId' } export const SystemConfigsTypeMap = { @@ -25,5 +26,8 @@ export const SystemConfigsTypeMap = { }, [SystemConfigsTypeEnum.activityAd]: { label: 'activityAd' + }, + [SystemConfigsTypeEnum.instanceId]: { + label: '部署实例 ID' } }; diff --git a/packages/global/common/system/license/schema.ts b/packages/global/common/system/license/schema.ts new file mode 100644 index 000000000000..c6465810f7c4 --- /dev/null +++ b/packages/global/common/system/license/schema.ts @@ -0,0 +1,87 @@ +/** + * License 数据结构单一来源(决策版)— zod schema + 类型派生。 + * + * 职责(方案 A): + * - 字段名 / 枚举 / 键清单的唯一权威,签发侧(license-server)与验证侧(normalize)共享, + * 杜绝两侧字段漂移(历史上 version vs licenseType 已漂移一次) + * - 签发侧用 licensePayloadSchema(严格:functions 必填全字段,默认关语义在 UI 层) + * - 验证侧只用这里的类型 + 键清单,宽容归一化在 normalize 模块(zod 不适合表达旧→新多分支映射) + * + * 决策版结构(飞书决策版 §2): + * { schemaVersion: 1|2, licenseType: trial|official, company, startTime, expiredTime, + * instanceId?, description?, limits{maxUsers,maxApps,maxDatasets}, functions{7 项} } + * 有效期 startTime <= now < expiredTime + */ +import { z } from 'zod'; + +/** License schema 版本:1 = 旧结构(存量),2 = 决策版(新签发) */ +export const LicenseSchemaVersionSchema = z.union([z.literal(1), z.literal(2)]); +export type LicenseSchemaVersionType = z.infer; + +/** 授权类型 */ +export const LicenseTypeSchema = z.enum(['trial', 'official']); +export type LicenseType = z.infer; + +/** functions 键清单(决策版 7 项,全部显式签发) */ +export const licenseFunctionKeys = [ + 'sso', + 'pay', + 'eval', + 'datasetEnhance', + 'assistantGenerate', + 'portal', + 'sandboxSkills' +] as const; +export type LicenseFunctionKey = (typeof licenseFunctionKeys)[number]; + +/** limits 键清单 */ +export const licenseLimitsKeys = ['maxUsers', 'maxApps', 'maxDatasets'] as const; +export type LicenseLimitsKey = (typeof licenseLimitsKeys)[number]; + +/** functions:布尔开关,决策版要求签发时显式全部给出(默认关在 UI 层,非 schema 默认)。 + * strictObject:拒绝未知键(防旧字段 customTemplates/batchEval 混入新 license) */ +export const LicenseFunctionsSchema = z.strictObject({ + sso: z.boolean(), + pay: z.boolean(), + eval: z.boolean(), + datasetEnhance: z.boolean(), + assistantGenerate: z.boolean(), + portal: z.boolean(), + sandboxSkills: z.boolean() +}); +export type LicenseFunctions = z.infer; + +/** limits:配额,0 = 不限制。strictObject:拒绝未知配额键 */ +export const LicenseLimitsSchema = z.strictObject({ + maxUsers: z.number().int().min(0), + maxApps: z.number().int().min(0), + maxDatasets: z.number().int().min(0) +}); +export type LicenseLimits = z.infer; + +/** + * 决策版 License payload(签发侧严格校验用)。 + * - instanceId:32 位 hex,绑定部署实例(决策版 §3) + * - 不做 startTime/expiredTime 顺序校验的 default —— 由调用方 superRefine(签发服务)与 + * 验证侧 isLicenseExpired 各自处理 + */ +export const LicensePayloadSchema = z.object({ + schemaVersion: LicenseSchemaVersionSchema.default(2), + licenseType: LicenseTypeSchema.default('official'), + company: z.string().min(1), + description: z.string().optional(), + startTime: z.iso.datetime(), + expiredTime: z.iso.datetime(), + instanceId: z + .string() + .regex(/^[0-9a-f]{32}$/) + .optional(), + limits: LicenseLimitsSchema, + functions: LicenseFunctionsSchema +}); +export type LicensePayload = z.infer; + +/** 签发侧强制未知键拒绝(决策版:customTemplates/networkIds 不入新 license) */ +export const StrictLicensePayloadSchema = z.strictObject({ + ...LicensePayloadSchema.shape +}); diff --git a/packages/global/common/system/types/index.ts b/packages/global/common/system/types/index.ts index bce2098eb2cf..b74457806785 100644 --- a/packages/global/common/system/types/index.ts +++ b/packages/global/common/system/types/index.ts @@ -1,5 +1,18 @@ import type { SubPlanType } from '../../../support/wallet/sub/type'; import type { AccountCancellationVerificationCapabilities } from '../../../support/user/account/cancellation/type'; +import type { LicensePayload, LicenseSchemaVersionType, LicenseType, LicenseFunctions, LicenseLimits } from '../license/schema'; + +export type { LicensePayload, LicenseSchemaVersionType, LicenseType, LicenseFunctions, LicenseLimits }; +export { + licenseFunctionKeys, + licenseLimitsKeys, + LicensePayloadSchema, + StrictLicensePayloadSchema, + LicenseFunctionsSchema, + LicenseLimitsSchema, + LicenseSchemaVersionSchema, + LicenseTypeSchema +} from '../license/schema'; export type NavbarItemType = { id: string; @@ -216,20 +229,22 @@ export type customPdfParseType = { price?: number; }; -export type LicenseDataType = { - startTime: string; - expiredTime: string; - company: string; - description?: string; // 描述 - hosts?: string[]; // 管理端有效域名 - maxUsers?: number; // 最大用户数,不填默认不上限 - maxApps?: number; // 最大应用数,不填默认不上限 - maxDatasets?: number; // 最大数据集数,不填默认不上限 - functions: { - sso: boolean; - pay: boolean; - customTemplates: boolean; - datasetEnhance: boolean; - batchEval: boolean; +/** + * 运行时 License 数据(global.licenseData / 前端展示)= + * 决策版 payload(schema 单一来源派生)+ deprecated 兼容视图。 + * + * deprecated 视图(旧 UI/消费方直接读顶层字段,避免回归;新代码一律用 limits/functions 决策版键): + * - 顶层 maxUsers/maxApps/maxDatasets(旧结构,归一化回填自 limits) + * - hosts(仅兼容读取,不参与校验) + * - functions.batchEval / functions.customTemplates(旧 UI 展示兜底) + */ +export type LicenseDataType = LicensePayload & { + hosts?: string[]; + maxUsers?: number; + maxApps?: number; + maxDatasets?: number; + functions: LicenseFunctions & { + batchEval?: boolean; + customTemplates?: boolean; }; }; diff --git a/packages/service/common/system/config/controller.ts b/packages/service/common/system/config/controller.ts index eee3ba3a810e..6d4560dff4b0 100644 --- a/packages/service/common/system/config/controller.ts +++ b/packages/service/common/system/config/controller.ts @@ -1,8 +1,8 @@ +import { verifyLicenseSignature, normalizeLicenseData, isLicenseExpired } from '../license/verify'; import { SystemConfigsTypeEnum } from '@fastgpt/global/common/system/config/constants'; import { MongoSystemConfigs } from './schema'; -import { type FastGPTConfigFileType } from '@fastgpt/global/common/system/types'; +import type { FastGPTConfigFileType, LicenseDataType } from '@fastgpt/global/common/system/types'; import { FastGPTProUrl } from '../constants'; -import { type LicenseDataType } from '@fastgpt/global/common/system/types'; export const getFastGPTConfigFromDB = async (): Promise<{ fastgptConfig: FastGPTConfigFileType; @@ -29,7 +29,21 @@ export const getFastGPTConfigFromDB = async (): Promise<{ ]); const config = fastgptConfig?.value || {}; - const licenseData = licenseConfig?.value?.data as LicenseDataType | undefined; + // 决策版 §4:DB 只存原始 license 字符串;启动时从原始 license 重新验签, + // 验签/归一化失败或过期则视为未激活(undefined),不信任落库的解析结果。 + let licenseData: LicenseDataType | undefined; + const licenseStr = licenseConfig?.value?.license as string | undefined; + if (licenseStr) { + try { + const raw = verifyLicenseSignature(licenseStr); + const normalized = normalizeLicenseData(raw); + if (!isLicenseExpired(normalized)) { + licenseData = normalized; + } + } catch (error) { + licenseData = undefined; + } + } const fastgptConfigTime = fastgptConfig?.createTime.getTime().toString(); const licenseConfigTime = licenseConfig?.createTime.getTime().toString(); diff --git a/packages/service/common/system/config/instanceId.ts b/packages/service/common/system/config/instanceId.ts new file mode 100644 index 000000000000..fe528fb19588 --- /dev/null +++ b/packages/service/common/system/config/instanceId.ts @@ -0,0 +1,36 @@ +/** + * 部署实例 ID 工具 — 替代原 hosts(域名)绑定标记。 + * + * 设计(见 .agents/design/admin/license-instance-id-redesign.md §6.4): + * - 首次调用用 findOneAndUpdate + $setOnInsert 原子生成;固定 _id 利用主键唯一约束, + * 保证多实例并发首启时只有一个插入成功,其余返回已存在文档(type 索引非唯一, + * 不能依赖 type 做并发去重——fastgpt/fastgptPro 需要同 type 多文档历史) + * - 之后永不 update:换 license / 重签 / 删 license 均不影响身份 + * - change stream 只对 fastgptPro/license 的 insert 触发配置重载,instanceId 的 insert 零副作用 + * - 纯 DB 存储,不做文件双写(多实例不一致 + 迁移丢失) + */ +import crypto from 'crypto'; +import { SystemConfigsTypeEnum } from '@fastgpt/global/common/system/config/constants'; +import { MongoSystemConfigs } from './schema'; + +/** 固定 _id(24 位 hex,合法 ObjectId):主键唯一约束保证并发下只生成一次 */ +const INSTANCE_ID_DOC_ID = '000000000000000000000001'; + +export const getInstanceId = async (): Promise => { + const doc = await MongoSystemConfigs.findOneAndUpdate( + { _id: INSTANCE_ID_DOC_ID }, + { + $setOnInsert: { + _id: INSTANCE_ID_DOC_ID, + type: SystemConfigsTypeEnum.instanceId, + value: { + instanceId: crypto.randomBytes(16).toString('hex') // 32 位 hex + }, + createTime: new Date() + } + }, + { upsert: true, new: true } + ); + + return doc.value.instanceId as string; +}; diff --git a/packages/service/common/system/license/verify.ts b/packages/service/common/system/license/verify.ts new file mode 100644 index 000000000000..509e0c3dc8b4 --- /dev/null +++ b/packages/service/common/system/license/verify.ts @@ -0,0 +1,116 @@ +/** + * License 验证核心(决策版)— 无 DB/业务全局依赖,pro/admin 与 app 共用;公钥从服务环境读取。 + * + * 设计(决策版 §2/§3): + * - payload 决策版结构:schemaVersion=2、licenseType、limits{}、functions 显式全字段 + * - 旧 license(schemaVersion=1,无 instanceId/limits,functions 含 batchEval/customTemplates) + * 通过 normalizeLicenseData 归一化为决策版结构;customTemplates/networkIds 不入新结构 + * - 有效期:startTime <= now < expiredTime + * - 验签对象是 base64 payload 字符串字节(RSA-SHA256),与签发侧一致 + */ +import crypto from 'crypto'; +import { readFileSync } from 'fs'; +import { serviceEnv } from '../../../env'; +import type { LicenseDataType, LicenseFunctions } from '@fastgpt/global/common/system/types'; + +/** 读取与 license-server 配对的公钥;不支持多把 key,轮换由重新部署配置完成。 */ +const getLicensePublicKey = () => { + if (serviceEnv.LICENSE_PUBLIC_KEY_PATH) { + return readFileSync(serviceEnv.LICENSE_PUBLIC_KEY_PATH, 'utf8'); + } + if (serviceEnv.LICENSE_PUBLIC_KEY) return serviceEnv.LICENSE_PUBLIC_KEY.replace(/\\n/g, '\n'); + throw new Error('未配置 LICENSE_PUBLIC_KEY 或 LICENSE_PUBLIC_KEY_PATH'); +}; + +/** RSA-4096 签名 base64 长度(512 字节) */ +export const signatureLength = 684; + +/** 决策版 functions 全字段默认关闭(缺失字段归一化补 false)。类型标注 LicenseFunctions,schema 键变化即编译报错 */ +export const licenseDefaultFunctions: LicenseFunctions = { + sso: false, + pay: false, + eval: false, + datasetEnhance: false, + assistantGenerate: false, + portal: false, + sandboxSkills: false +}; + +/** + * 校验签名并解析 payload。 + * @returns 原始解析对象(未归一化),供 normalizeLicenseData 消费 + * @throws 验签失败 / 非合法 base64 JSON + */ +export const verifyLicenseSignature = (license: string) => { + const signature = license.substring(0, signatureLength); + const payloadBase64 = license.substring(signatureLength); + + const verified = crypto + .createVerify('RSA-SHA256') + .update(payloadBase64) + .verify(getLicensePublicKey(), signature, 'base64'); + + if (!verified) { + throw new Error('License 签名不合法'); + } + + return JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8')) as Record; +}; + +/** + * 归一化为决策版结构(旧 schemaVersion=1 → 决策版)。 + * - schemaVersion 缺省视为 1;licenseType 缺省 official + * - 旧顶层 maxUsers/maxApps/maxDatasets → limits;旧 batchEval → eval + * - customTemplates/networkIds/hosts 不进入新结构(hosts 仅读兼容,不校验) + * - functions 缺失字段补 false(决策版:functions 是唯一授权来源,默认关闭) + */ +export const normalizeLicenseData = (raw: Record): LicenseDataType => { + const limits = raw.limits ?? { + maxUsers: raw.maxUsers ?? 0, + maxApps: raw.maxApps ?? 0, + maxDatasets: raw.maxDatasets ?? 0 + }; + + const oldFunctions = raw.functions ?? {}; + const functions = { + sso: oldFunctions.sso ?? false, + pay: oldFunctions.pay ?? false, + eval: oldFunctions.eval ?? oldFunctions.batchEval ?? false, // 旧 batchEval 归一化 + datasetEnhance: oldFunctions.datasetEnhance ?? false, + assistantGenerate: oldFunctions.assistantGenerate ?? false, + portal: oldFunctions.portal ?? false, + sandboxSkills: oldFunctions.sandboxSkills ?? false + }; + + return { + schemaVersion: raw.schemaVersion ?? 1, + licenseType: raw.licenseType ?? 'official', + startTime: raw.startTime, + expiredTime: raw.expiredTime, + company: raw.company, + description: raw.description, + instanceId: raw.instanceId, + limits, + functions: { + ...functions, + // deprecated 兼容键:旧 UI 读 batchEval/customTemplates,回填避免回归(新 license 不签发) + batchEval: functions.eval, + customTemplates: false + }, + // deprecated 兼容视图:旧 UI 读顶层 maxUsers/maxApps/maxDatasets,回填避免回归(新代码读 limits) + maxUsers: limits.maxUsers || undefined, + maxApps: limits.maxApps || undefined, + maxDatasets: limits.maxDatasets || undefined, + hosts: raw.hosts // 仅兼容读取,不参与校验(决策版:新 license 不用 hosts) + }; +}; +/** 决策版有效期判定:startTime <= now < expiredTime。 */ +export const isLicenseExpired = ( + data: { startTime: string; expiredTime: string }, + now = new Date() +) => { + const start = new Date(data.startTime).getTime(); + const end = new Date(data.expiredTime).getTime(); + const current = now.getTime(); + return current < start || current >= end; +}; diff --git a/packages/service/env.ts b/packages/service/env.ts index 07eb1ad4fea9..7ab134f74466 100644 --- a/packages/service/env.ts +++ b/packages/service/env.ts @@ -45,6 +45,10 @@ export const serviceEnv = createEnv({ // Invoke 反向调用相关。该密钥用于签发/校验插件反向调用 JWT,必须显式配置,避免未配置时落到公开默认值。 INVOKE_TOKEN_SECRET: z.string().min(32, 'INVOKE_TOKEN_SECRET must be at least 32 characters'), + // License-server 公钥:与签发服务使用同一密钥对;支持内联 PEM 或文件路径。 + LICENSE_PUBLIC_KEY: z.string().optional(), + LICENSE_PUBLIC_KEY_PATH: z.string().optional(), + // ==================== 服务地址与集成 ==================== // 插件 PLUGIN_BASE_URL: UrlSchema.default('http://localhost:3004'), diff --git a/packages/service/test/common/system/instanceId.test.ts b/packages/service/test/common/system/instanceId.test.ts new file mode 100644 index 000000000000..62ff1b2fa625 --- /dev/null +++ b/packages/service/test/common/system/instanceId.test.ts @@ -0,0 +1,48 @@ +/** + * 部署实例 ID(getInstanceId)测试 — 见 .agents/design/admin/license-instance-id-redesign.md §6.4 + * 验证:32 位 hex 格式、幂等(两次调用同 ID)、并发原子生成(全集群只生成一次)。 + */ +import { describe, expect, it } from 'vitest'; +import { getInstanceId } from '@fastgpt/service/common/system/config/instanceId'; +import { SystemConfigsTypeEnum } from '@fastgpt/global/common/system/config/constants'; +import { MongoSystemConfigs } from '@fastgpt/service/common/system/config/schema'; + +describe('getInstanceId', () => { + it('生成 32 位 hex 格式 ID,且落库为独立 type instanceId', async () => { + const id = await getInstanceId(); + + expect(id).toMatch(/^[0-9a-f]{32}$/); + + const doc = await MongoSystemConfigs.findOne({ + type: SystemConfigsTypeEnum.instanceId + }); + expect(doc).toBeTruthy(); + expect(doc?.value?.instanceId).toBe(id); + }); + + it('幂等:连续两次调用返回相同 ID(不重新生成)', async () => { + const first = await getInstanceId(); + const second = await getInstanceId(); + + expect(second).toBe(first); + + const count = await MongoSystemConfigs.countDocuments({ + type: SystemConfigsTypeEnum.instanceId + }); + expect(count).toBe(1); + }); + + it('并发原子生成:多个实例同时首启只生成一个 ID', async () => { + // 清理已存在的 instanceId 记录,模拟首次启动 + await MongoSystemConfigs.deleteMany({ type: SystemConfigsTypeEnum.instanceId }); + + const ids = await Promise.all(Array.from({ length: 8 }, () => getInstanceId())); + + // $setOnInsert + upsert 保证全集群只有一个 ID + expect(new Set(ids).size).toBe(1); + const count = await MongoSystemConfigs.countDocuments({ + type: SystemConfigsTypeEnum.instanceId + }); + expect(count).toBe(1); + }); +}); diff --git a/packages/service/test/common/system/licenseVerify.test.ts b/packages/service/test/common/system/licenseVerify.test.ts new file mode 100644 index 000000000000..893ee967b8db --- /dev/null +++ b/packages/service/test/common/system/licenseVerify.test.ts @@ -0,0 +1,113 @@ +/** + * License 验证核心(决策版)测试 — 见飞书决策版 §2/§3 + * 验证:旧 license(schemaVersion=1)归一化映射、functions 默认关闭、实例 ID 透传、 + * 有效期判定(startTime <= now < expiredTime)。 + */ +import { describe, expect, it } from 'vitest'; +import { + normalizeLicenseData, + isLicenseExpired +} from '@fastgpt/service/common/system/license/verify'; + +describe('normalizeLicenseData(旧 license → 决策版结构)', () => { + it('旧结构(schemaVersion=1 + 顶层配额 + batchEval)归一化为决策版结构', () => { + const raw = { + startTime: '2026-01-01T00:00:00.000Z', + expiredTime: '2027-01-01T00:00:00.000Z', + company: '测试公司', + hosts: ['admin.example.com'], + maxUsers: 100, + maxApps: 10, + maxDatasets: 5, + functions: { + sso: true, + pay: false, + customTemplates: true, + datasetEnhance: true, + batchEval: true + } + }; + + const data = normalizeLicenseData(raw); + + expect(data.schemaVersion).toBe(1); // 旧 license 保留 schemaVersion=1 + expect(data.licenseType).toBe('official'); // 缺省 official + expect(data.limits).toEqual({ maxUsers: 100, maxApps: 10, maxDatasets: 5 }); + expect(data.functions.eval).toBe(true); // batchEval → eval + expect(data.functions.assistantGenerate).toBe(false); // 缺失补默认关 + expect(data.functions.portal).toBe(false); + expect(data.functions.sandboxSkills).toBe(false); + // deprecated 兼容视图 + expect(data.functions.batchEval).toBe(true); + expect(data.functions.customTemplates).toBe(false); // 已移除功能不入新结构 + expect(data.maxUsers).toBe(100); + expect(data.hosts).toEqual(['admin.example.com']); + }); + + it('决策版新结构(schemaVersion=2 + limits + 全 functions)透传', () => { + const raw = { + schemaVersion: 2, + licenseType: 'trial', + startTime: '2026-06-01T00:00:00.000Z', + expiredTime: '2026-09-01T00:00:00.000Z', + company: '新客户', + instanceId: 'a'.repeat(32), + limits: { maxUsers: 0, maxApps: 0, maxDatasets: 0 }, + functions: { + sso: true, + pay: true, + eval: true, + datasetEnhance: true, + assistantGenerate: true, + portal: false, + sandboxSkills: false + } + }; + + const data = normalizeLicenseData(raw); + + expect(data.schemaVersion).toBe(2); + expect(data.licenseType).toBe('trial'); + expect(data.instanceId).toBe('a'.repeat(32)); + expect(data.limits).toEqual({ maxUsers: 0, maxApps: 0, maxDatasets: 0 }); + expect(data.functions.eval).toBe(true); + expect(data.functions.portal).toBe(false); + }); + + it('无 functions 时全部默认关闭', () => { + const data = normalizeLicenseData({ + startTime: '2026-01-01T00:00:00.000Z', + expiredTime: '2027-01-01T00:00:00.000Z', + company: 'x' + }); + expect(data.functions.sso).toBe(false); + expect(data.functions.pay).toBe(false); + expect(data.functions.eval).toBe(false); + expect(data.functions.datasetEnhance).toBe(false); + expect(data.functions.assistantGenerate).toBe(false); + expect(data.functions.portal).toBe(false); + expect(data.functions.sandboxSkills).toBe(false); + expect(data.limits).toEqual({ maxUsers: 0, maxApps: 0, maxDatasets: 0 }); + }); +}); + +describe('isLicenseExpired(决策版有效期:startTime <= now < expiredTime)', () => { + const base = { startTime: '2026-01-01T00:00:00.000Z', expiredTime: '2026-12-31T00:00:00.000Z' }; + + it('有效期内不过期', () => { + expect(isLicenseExpired(base, new Date('2026-06-01T00:00:00.000Z'))).toBe(false); + }); + + it('未到 startTime 视为过期(未生效)', () => { + expect(isLicenseExpired(base, new Date('2025-12-01T00:00:00.000Z'))).toBe(true); + }); + + it('达到 expiredTime 视为过期(含边界)', () => { + expect(isLicenseExpired(base, new Date('2026-12-31T00:00:00.000Z'))).toBe(true); + expect(isLicenseExpired(base, new Date('2027-01-01T00:00:00.000Z'))).toBe(true); + }); + + it('startTime 边界:恰好等于 startTime 视为生效', () => { + expect(isLicenseExpired(base, new Date('2026-01-01T00:00:00.000Z'))).toBe(false); + }); +}); diff --git a/packages/web/i18n/en/admin_plugin.json b/packages/web/i18n/en/admin_plugin.json index a44188e876bf..291dd674747b 100644 --- a/packages/web/i18n/en/admin_plugin.json +++ b/packages/web/i18n/en/admin_plugin.json @@ -13,5 +13,27 @@ "toolkit_runtime_config_min_pods_tip": "The minimum number of Pods to keep for this tool runtime. Set to 0 to allow scaling down to 0", "toolkit_runtime_config_min_value": "{{label}} cannot be less than {{min}}", "toolkit_runtime_config_pod_timeout": "Pod timeout", - "toolkit_runtime_config_pod_timeout_tip": "A Pod will be recycled after it stays idle longer than this value" + "toolkit_runtime_config_pod_timeout_tip": "A Pod will be recycled after it stays idle longer than this value", + "license_admin_home": "Admin Home", + "license_admin_home_description": "View this tenant's License authorization, resource quotas, and capabilities", + "license_tenant_name": "Tenant name", + "license_current_tenant": "Current tenant", + "license_business": "Business", + "license_trial": "Trial", + "license_active": "License active", + "license_expiring_soon": "License expiring soon", + "license_inactive": "License inactive", + "license_expires_at": "Expires on", + "license_change": "Change License", + "license_activate": "Activate License", + "license_limits": "Resource quotas", + "license_max_users": "Max users", + "license_max_apps": "Max apps", + "license_max_datasets": "Max knowledge bases", + "license_unlimited": "Unlimited", + "license_capabilities": "Capabilities", + "license_sso": "Single sign-on", + "license_pay": "Payment system", + "license_templates": "Custom templates and system tools", + "license_dataset_enhance": "Knowledge base enhancement" } diff --git a/packages/web/i18n/ko-KR/admin_plugin.json b/packages/web/i18n/ko-KR/admin_plugin.json index 0485ef78772d..b6ea4183f027 100644 --- a/packages/web/i18n/ko-KR/admin_plugin.json +++ b/packages/web/i18n/ko-KR/admin_plugin.json @@ -13,5 +13,27 @@ "toolkit_runtime_config_min_pods_tip": "이 도구 런타임을 위해 유지할 최소 Pod 수입니다. 0으로 설정하면 0까지 축소할 수 있습니다", "toolkit_runtime_config_min_value": "{{label}}은(는) {{min}}보다 작을 수 없습니다", "toolkit_runtime_config_pod_timeout": "Pod 타임아웃", - "toolkit_runtime_config_pod_timeout_tip": "Pod가 이 값보다 오래 유휴 상태로 있으면 회수됩니다" + "toolkit_runtime_config_pod_timeout_tip": "Pod가 이 값보다 오래 유휴 상태로 있으면 회수됩니다", + "license_admin_home": "관리자 홈", + "license_admin_home_description": "현재 테넌트의 License 권한, 리소스 한도 및 기능을 확인합니다", + "license_tenant_name": "테넌트 이름", + "license_current_tenant": "현재 테넌트", + "license_business": "비즈니스 버전", + "license_trial": "체험판", + "license_active": "License 활성화됨", + "license_expiring_soon": "License 만료 임박", + "license_inactive": "License 비활성화됨", + "license_expires_at": "만료일", + "license_change": "License 변경", + "license_activate": "License 활성화", + "license_limits": "리소스 한도", + "license_max_users": "최대 사용자 수", + "license_max_apps": "최대 앱 수", + "license_max_datasets": "최대 지식베이스 수", + "license_unlimited": "무제한", + "license_capabilities": "권한 기능", + "license_sso": "싱글 사인온", + "license_pay": "결제 시스템", + "license_templates": "사용자 지정 템플릿 및 시스템 도구", + "license_dataset_enhance": "지식베이스 강화" } diff --git a/packages/web/i18n/zh-CN/admin_plugin.json b/packages/web/i18n/zh-CN/admin_plugin.json index 95f968be1ff5..33606ae6bfac 100644 --- a/packages/web/i18n/zh-CN/admin_plugin.json +++ b/packages/web/i18n/zh-CN/admin_plugin.json @@ -13,5 +13,27 @@ "toolkit_runtime_config_min_pods_tip": "该工具运行时需要保留的最小 Pod 数量,填 0 表示允许缩容到 0", "toolkit_runtime_config_min_value": "{{label}} 不能小于 {{min}}", "toolkit_runtime_config_pod_timeout": "Pod 超时时间", - "toolkit_runtime_config_pod_timeout_tip": "Pod 空闲超过该时间后会被回收" + "toolkit_runtime_config_pod_timeout_tip": "Pod 空闲超过该时间后会被回收", + "license_admin_home": "管理员主页", + "license_admin_home_description": "查看当前租户的 License 授权、资源额度与授权能力", + "license_tenant_name": "租户名称", + "license_current_tenant": "当前租户", + "license_business": "商业版", + "license_trial": "试用版", + "license_active": "License 生效中", + "license_expiring_soon": "License 即将到期", + "license_inactive": "License 未激活", + "license_expires_at": "有效期至", + "license_change": "变更 License", + "license_activate": "激活 License", + "license_limits": "资源额度", + "license_max_users": "最大用户数", + "license_max_apps": "最大应用数", + "license_max_datasets": "最大知识库数量", + "license_unlimited": "不限", + "license_capabilities": "授权能力", + "license_sso": "单点登录", + "license_pay": "支付系统", + "license_templates": "自定义模板和系统工具", + "license_dataset_enhance": "知识库增强" } diff --git a/packages/web/i18n/zh-Hant/admin_plugin.json b/packages/web/i18n/zh-Hant/admin_plugin.json index 412a476f4aac..2b61d5054109 100644 --- a/packages/web/i18n/zh-Hant/admin_plugin.json +++ b/packages/web/i18n/zh-Hant/admin_plugin.json @@ -13,5 +13,27 @@ "toolkit_runtime_config_min_pods_tip": "該工具執行階段需要保留的最小 Pod 數量,填 0 表示允許縮容到 0", "toolkit_runtime_config_min_value": "{{label}} 不能小於 {{min}}", "toolkit_runtime_config_pod_timeout": "Pod 超時時間", - "toolkit_runtime_config_pod_timeout_tip": "Pod 閒置超過該時間後會被回收" + "toolkit_runtime_config_pod_timeout_tip": "Pod 閒置超過該時間後會被回收", + "license_admin_home": "管理員首頁", + "license_admin_home_description": "查看目前租戶的 License 授權、資源額度與授權能力", + "license_tenant_name": "租戶名稱", + "license_current_tenant": "目前租戶", + "license_business": "商業版", + "license_trial": "試用版", + "license_active": "License 生效中", + "license_expiring_soon": "License 即將到期", + "license_inactive": "License 未啟用", + "license_expires_at": "有效期至", + "license_change": "變更 License", + "license_activate": "啟用 License", + "license_limits": "資源額度", + "license_max_users": "最大使用者數", + "license_max_apps": "最大應用程式數", + "license_max_datasets": "最大知識庫數量", + "license_unlimited": "不限", + "license_capabilities": "授權能力", + "license_sso": "單一登入", + "license_pay": "支付系統", + "license_templates": "自訂範本和系統工具", + "license_dataset_enhance": "知識庫增強" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bd89b67bb2b..a85122bc7235 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,15 @@ settings: catalogs: default: + '@alicloud/dysmsapi20170525': + specifier: ^2.0.24 + version: 2.0.24 + '@alicloud/openapi-client': + specifier: ^0.4.15 + version: 0.4.15 + '@alicloud/tea-util': + specifier: ^1.4.11 + version: 1.4.11 '@chakra-ui/anatomy': specifier: ^2 version: 2.3.6 @@ -57,6 +66,9 @@ catalogs: '@tanstack/react-query': specifier: ^4.24.10 version: 4.36.1 + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -72,12 +84,18 @@ catalogs: '@types/node': specifier: ^24.13.3 version: 24.13.3 + '@types/nodemailer': + specifier: ^6.4.24 + version: 6.4.24 '@types/proxy-addr': specifier: 2.0.3 version: 2.0.3 '@types/request-ip': specifier: ^0.0.38 version: 0.0.38 + '@types/xml2js': + specifier: ^0.4.14 + version: 0.4.14 '@vitest/coverage-v8': specifier: ^4.1.5 version: 4.1.5 @@ -165,6 +183,9 @@ catalogs: next-i18next: specifier: 15.4.2 version: 15.4.2 + nodemailer: + specifier: ^7.0.13 + version: 7.0.13 postcss: specifier: ^8.5.12 version: 8.5.22 @@ -219,6 +240,9 @@ catalogs: vitest: specifier: ^4.1.5 version: 4.1.5 + xml2js: + specifier: ^0.6.2 + version: 0.6.2 zod: specifier: ^4 version: 4.1.12 @@ -990,7 +1014,7 @@ importers: version: 1.14.3 '@scalar/api-reference-react': specifier: ^0.9.59 - version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12) + version: 0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12) '@t3-oss/env-core': specifier: 'catalog:' version: 0.13.10(typescript@6.0.3)(zod@4.1.12) @@ -1243,6 +1267,37 @@ importers: specifier: 'catalog:' version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.5)(jsdom@26.1.0(bufferutil@4.1.0)(canvas@3.2.3)(utf-8-validate@5.0.10))(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.4)) + pro/license-server: + dependencies: + '@fastgpt/global': + specifier: workspace:* + version: link:../../packages/global + '@hono/node-server': + specifier: 'catalog:' + version: 2.0.12(hono@4.12.27) + '@t3-oss/env-core': + specifier: 'catalog:' + version: 0.13.10(typescript@6.0.3)(zod@4.1.12) + hono: + specifier: 'catalog:' + version: 4.12.27 + zod: + specifier: 'catalog:' + version: 4.1.12 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.13.3 + tsdown: + specifier: 'catalog:' + version: 0.21.10(typescript@6.0.3) + tsx: + specifier: 'catalog:' + version: 4.20.6 + typescript: + specifier: 'catalog:' + version: 6.0.3 + pro/llm_benchmark/content_benchmark: dependencies: '@fastgpt/global': @@ -1325,6 +1380,15 @@ importers: projects/app: dependencies: + '@alicloud/dysmsapi20170525': + specifier: 'catalog:' + version: 2.0.24(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@alicloud/openapi-client': + specifier: 'catalog:' + version: 0.4.15(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@alicloud/tea-util': + specifier: 'catalog:' + version: 1.4.11(bufferutil@4.1.0)(utf-8-validate@5.0.10) '@chakra-ui/anatomy': specifier: 'catalog:' version: 2.3.6 @@ -1403,6 +1467,15 @@ importers: '@tanstack/react-query': specifier: 'catalog:' version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-table': + specifier: 'catalog:' + version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/nodemailer': + specifier: 'catalog:' + version: 6.4.24 + '@types/xml2js': + specifier: 'catalog:' + version: 0.4.14 '@xterm/addon-fit': specifier: 'catalog:' version: 0.10.0(@xterm/xterm@5.5.0) @@ -1496,6 +1569,9 @@ importers: next-i18next: specifier: 'catalog:' version: 15.4.2(i18next@23.16.8)(next@16.3.0(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + nodemailer: + specifier: 'catalog:' + version: 7.0.13 nprogress: specifier: ^0.2.0 version: 0.2.0 @@ -1574,6 +1650,9 @@ importers: vfile: specifier: ^6.0.3 version: 6.0.3 + xml2js: + specifier: 'catalog:' + version: 0.6.2 zod: specifier: 'catalog:' version: 4.1.12 @@ -2441,10 +2520,6 @@ packages: resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} @@ -6891,9 +6966,6 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.17.24': - resolution: {integrity: sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==} - '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -9699,9 +9771,6 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -14305,9 +14374,6 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -15957,7 +16023,7 @@ snapshots: '@babel/code-frame@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -15985,16 +16051,16 @@ snapshots: '@babel/generator@7.26.10': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@8.0.0-rc.3': dependencies: - '@babel/parser': 8.0.0-rc.3 - '@babel/types': 8.0.0-rc.3 + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 '@types/jsesc': 2.5.1 @@ -16002,7 +16068,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.25.9': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/helper-compilation-targets@7.26.5': dependencies: @@ -16046,14 +16112,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.25.9': dependencies: '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.25.9': dependencies: '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -16061,14 +16127,14 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.25.9': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/helper-plugin-utils@7.26.5': {} @@ -16093,7 +16159,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.25.9': dependencies: '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -16103,8 +16169,6 @@ snapshots: '@babel/helper-string-parser@8.0.0': {} - '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-identifier@7.29.7': {} '@babel/helper-validator-identifier@8.0.0-rc.3': {} @@ -16117,18 +16181,18 @@ snapshots: dependencies: '@babel/template': 7.26.9 '@babel/traverse': 7.26.10 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helpers@7.26.10': dependencies: '@babel/template': 7.26.9 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/parser@7.29.2': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 '@babel/parser@7.29.8': dependencies: @@ -16136,7 +16200,7 @@ snapshots: '@babel/parser@8.0.0-rc.3': dependencies: - '@babel/types': 8.0.0-rc.3 + '@babel/types': 8.0.4 '@babel/parser@8.0.4': dependencies: @@ -16369,7 +16433,7 @@ snapshots: '@babel/core': 7.26.10 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) '@babel/helper-plugin-utils': 7.26.5 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.26.10 transitivePeerDependencies: - supports-color @@ -16482,7 +16546,7 @@ snapshots: '@babel/helper-module-imports': 7.25.9 '@babel/helper-plugin-utils': 7.26.5 '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.10) - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -16650,7 +16714,7 @@ snapshots: dependencies: '@babel/core': 7.26.10 '@babel/helper-plugin-utils': 7.26.5 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 esutils: 2.0.3 '@babel/preset-react@7.26.3(@babel/core@7.26.10)': @@ -16683,16 +16747,16 @@ snapshots: '@babel/template@7.26.9': dependencies: '@babel/code-frame': 7.26.2 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@babel/traverse@7.26.10': dependencies: '@babel/code-frame': 7.26.2 '@babel/generator': 7.26.10 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.8 '@babel/template': 7.26.9 - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 debug: 4.4.3 globals: 11.12.0 transitivePeerDependencies: @@ -16701,7 +16765,7 @@ snapshots: '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/types@7.29.8': dependencies: @@ -16711,7 +16775,7 @@ snapshots: '@babel/types@8.0.0-rc.3': dependencies: '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.4 '@babel/types@8.0.4': dependencies: @@ -17788,10 +17852,6 @@ snapshots: dependencies: tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.4) - '@headlessui/tailwindcss@0.2.2(tailwindcss@4.2.4)': - dependencies: - tailwindcss: 4.2.4 - '@headlessui/vue@1.7.23(vue@3.5.40(typescript@6.0.3))': dependencies: '@tanstack/vue-virtual': 3.13.12(vue@3.5.40(typescript@6.0.3)) @@ -18108,7 +18168,7 @@ snapshots: '@kubernetes/client-node@1.4.0(bufferutil@4.1.0)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@types/js-yaml': 4.0.9 - '@types/node': 24.13.3 + '@types/node': 24.0.13 '@types/node-fetch': 2.6.13 '@types/stream-buffers': 3.0.8 form-data: 4.0.5 @@ -18418,8 +18478,8 @@ snapshots: '@mistralai/mistralai@1.14.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': dependencies: ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - zod: 4.1.12 - zod-to-json-schema: 3.25.1(zod@4.1.12) + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -19793,44 +19853,6 @@ snapshots: - universal-cookie - zod - '@scalar/agent-chat@0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@ai-sdk/vue': 3.0.33(vue@3.5.40(typescript@6.0.3))(zod@4.1.12) - '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/json-magic': 0.12.19 - '@scalar/openapi-types': 0.9.4 - '@scalar/schemas': 0.8.0 - '@scalar/themes': 0.17.2 - '@scalar/types': 0.17.0 - '@scalar/use-toasts': 0.10.4(typescript@6.0.3) - '@scalar/validation': 0.6.2 - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - ai: 6.0.33(zod@4.1.12) - js-base64: 3.7.8 - neverpanic: 0.0.8 - truncate-json: 3.0.1 - vue: 3.5.40(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - zod - '@scalar/api-client@3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)': dependencies: '@headlessui/tailwindcss': 0.2.2(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4)) @@ -19879,54 +19901,6 @@ snapshots: - typescript - universal-cookie - '@scalar/api-client@3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.2.4) - '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/blocks': 0.1.9(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/oas-utils': 0.19.9(typescript@6.0.3) - '@scalar/openapi-types': 0.9.4 - '@scalar/sidebar': 0.9.34(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/snippetz': 0.9.24 - '@scalar/themes': 0.17.2 - '@scalar/typebox': 0.1.3 - '@scalar/types': 0.17.0 - '@scalar/use-codemirror': 0.14.14(typescript@6.0.3) - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@scalar/use-toasts': 0.10.4(typescript@6.0.3) - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - '@vueuse/integrations': 13.9.0(axios@1.18.1)(focus-trap@7.8.0)(fuse.js@7.1.0)(nprogress@0.2.0)(qrcode@1.5.4)(vue@3.5.40(typescript@6.0.3)) - focus-trap: 7.8.0 - fuse.js: 7.1.0 - js-base64: 3.7.8 - jsonc-parser: 3.3.1 - nanoid: 5.1.16 - pretty-ms: 9.3.0 - radix-vue: 1.9.17(vue@3.5.40(typescript@6.0.3)) - set-cookie-parser: 3.1.0 - vue: 3.5.40(typescript@6.0.3) - yaml: 2.8.4 - zod: 4.4.3 - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12)': dependencies: '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12) @@ -19949,28 +19923,6 @@ snapshots: - universal-cookie - zod - '@scalar/api-reference-react@0.9.60(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(react@18.3.1)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@scalar/api-reference': 1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12) - '@scalar/types': 0.17.0 - react: 18.3.1 - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - zod - '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.4))(typescript@6.0.3)(zod@4.1.12)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) @@ -20015,50 +19967,6 @@ snapshots: - universal-cookie - zod - '@scalar/api-reference@1.64.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12)': - dependencies: - '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/agent-chat': 0.12.23(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3)(zod@4.1.12) - '@scalar/api-client': 3.14.0(axios@1.18.1)(nprogress@0.2.0)(qrcode@1.5.4)(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/blocks': 0.1.9(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/code-highlight': 0.4.3 - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/oas-utils': 0.19.9(typescript@6.0.3) - '@scalar/schemas': 0.8.0 - '@scalar/sidebar': 0.9.34(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/snippetz': 0.9.24 - '@scalar/themes': 0.17.2 - '@scalar/types': 0.17.0 - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@scalar/use-toasts': 0.10.4(typescript@6.0.3) - '@scalar/validation': 0.6.2 - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@unhead/vue': 2.1.17(vue@3.5.40(typescript@6.0.3)) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - fuse.js: 7.1.0 - microdiff: 1.5.0 - nanoid: 5.1.16 - vue: 3.5.40(typescript@6.0.3) - yaml: 2.8.4 - transitivePeerDependencies: - - '@vue/composition-api' - - async-validator - - axios - - change-case - - drauu - - idb-keyval - - jwt-decode - - nprogress - - qrcode - - sortablejs - - supports-color - - tailwindcss - - typescript - - universal-cookie - - zod - '@scalar/asyncapi-upgrader@0.1.4': dependencies: '@scalar/helpers': 0.9.2 @@ -20081,24 +19989,6 @@ snapshots: - tailwindcss - typescript - '@scalar/blocks@0.1.9(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/snippetz': 0.9.24 - '@scalar/themes': 0.17.2 - '@scalar/types': 0.17.0 - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - '@types/har-format': 1.2.16 - js-base64: 3.7.8 - vue: 3.5.40(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - tailwindcss - - typescript - '@scalar/code-highlight@0.4.3': dependencies: hast-util-to-text: 4.0.2 @@ -20141,28 +20031,6 @@ snapshots: - tailwindcss - typescript - '@scalar/components@0.27.10(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@floating-ui/utils': 0.2.10 - '@floating-ui/vue': 1.1.9(vue@3.5.40(typescript@6.0.3)) - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.2.4) - '@headlessui/vue': 1.7.23(vue@3.5.40(typescript@6.0.3)) - '@scalar/code-highlight': 0.4.3 - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/themes': 0.17.2 - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@vueuse/core': 13.9.0(vue@3.5.40(typescript@6.0.3)) - cva: 1.0.0-beta.4(typescript@6.0.3) - radix-vue: 1.9.17(vue@3.5.40(typescript@6.0.3)) - vue: 3.5.40(typescript@6.0.3) - vue-component-type-helpers: 3.3.9 - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - tailwindcss - - typescript - '@scalar/helpers@0.9.2': {} '@scalar/icons@0.7.5(typescript@6.0.3)': @@ -20218,21 +20086,6 @@ snapshots: - tailwindcss - typescript - '@scalar/sidebar@0.9.34(tailwindcss@4.2.4)(typescript@6.0.3)': - dependencies: - '@scalar/components': 0.27.10(tailwindcss@4.2.4)(typescript@6.0.3) - '@scalar/helpers': 0.9.2 - '@scalar/icons': 0.7.5(typescript@6.0.3) - '@scalar/themes': 0.17.2 - '@scalar/use-hooks': 0.4.9(typescript@6.0.3) - '@scalar/workspace-store': 0.56.0(typescript@6.0.3) - vue: 3.5.40(typescript@6.0.3) - transitivePeerDependencies: - - '@vue/composition-api' - - supports-color - - tailwindcss - - typescript - '@scalar/snippetz@0.9.24': dependencies: '@scalar/helpers': 0.9.2 @@ -20783,7 +20636,7 @@ snapshots: '@svgr/hast-util-to-babel-ast@6.5.1': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.8 entities: 4.5.0 '@svgr/plugin-jsx@6.5.1(@svgr/core@6.5.1)': @@ -21125,7 +20978,7 @@ snapshots: '@types/cors@2.8.19': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/d3-array@3.2.1': {} @@ -21252,7 +21105,7 @@ snapshots: '@types/dns-packet@5.6.5': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/estree-jsx@1.0.5': dependencies: @@ -21264,7 +21117,7 @@ snapshots: '@types/express-serve-static-core@4.19.9': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/qs': 6.9.18 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -21318,7 +21171,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -21378,7 +21231,7 @@ snapshots: '@types/node-fetch@2.6.13': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 form-data: 4.0.5 '@types/node-int64@0.4.32': @@ -21391,10 +21244,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.17.24': - dependencies: - undici-types: 6.19.8 - '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -21417,7 +21266,7 @@ snapshots: '@types/nodemailer@6.4.24': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/nprogress@0.2.3': {} @@ -21491,7 +21340,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/send@0.17.4': dependencies: @@ -21506,7 +21355,7 @@ snapshots: '@types/stream-buffers@3.0.8': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/thrift@0.10.17': dependencies: @@ -21543,11 +21392,11 @@ snapshots: '@types/xml-encryption@1.2.4': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/xml2js@0.4.14': dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 '@types/yauzl@2.10.3': dependencies: @@ -23922,8 +23771,8 @@ snapshots: '@next/eslint-plugin-next': 16.3.0(eslint@9.39.4(jiti@2.7.0)) eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.4(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) @@ -23945,33 +23794,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.4(jiti@2.7.0) + eslint: 9.39.4(jiti@1.21.7) get-tsconfig: 4.14.0 is-bun-module: 1.3.0 oxc-resolver: 5.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.16 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@1.21.7)): + eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.4(jiti@1.21.7) + eslint: 9.39.4(jiti@2.7.0) get-tsconfig: 4.14.0 is-bun-module: 1.3.0 oxc-resolver: 5.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.16 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@1.21.7))(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@1.21.7)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -23986,13 +23835,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: debug: 3.2.7 optionalDependencies: eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-import-resolver-typescript: 3.9.0(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -24025,7 +23874,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -24036,7 +23885,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0(eslint-plugin-import@2.32.0(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.9.0)(eslint@9.39.4(jiti@2.7.0)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -24096,8 +23945,8 @@ snapshots: '@babel/parser': 7.29.2 eslint: 9.39.4(jiti@1.21.7) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -24107,8 +23956,8 @@ snapshots: '@babel/parser': 7.29.2 eslint: 9.39.4(jiti@2.7.0) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -24521,10 +24370,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -24959,10 +24804,6 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.6: - dependencies: - resolve-pkg-maps: 1.0.0 - get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -25480,7 +25321,7 @@ snapshots: httpx@2.3.3: dependencies: - '@types/node': 20.17.24 + '@types/node': 20.19.43 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -30413,8 +30254,8 @@ snapshots: tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyglobby@0.2.17: dependencies: @@ -30559,7 +30400,7 @@ snapshots: tsx@4.20.6: dependencies: esbuild: 0.25.12 - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.3 optionalDependencies: fsevents: 2.3.3 @@ -30716,8 +30557,6 @@ snapshots: undici-types@5.26.5: {} - undici-types@6.19.8: {} - undici-types@6.21.0: {} undici-types@7.18.2: {} @@ -31659,13 +31498,17 @@ snapshots: dependencies: zod: 4.1.12 + zod-to-json-schema@3.25.1(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod-validation-error@3.4.0(zod@3.25.76): dependencies: zod: 3.25.76 - zod-validation-error@4.0.2(zod@4.1.12): + zod-validation-error@4.0.2(zod@4.4.3): dependencies: - zod: 4.1.12 + zod: 4.4.3 zod@3.23.8: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8048c8fc7e42..01f0990e1456 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,12 +10,16 @@ packages: - pro/admin - pro/sso - pro/browser-sandbox + - pro/license-server - document/ - scripts/icon - sdk/* catalog: + '@alicloud/dysmsapi20170525': ^2.0.24 + '@alicloud/openapi-client': ^0.4.15 + '@alicloud/tea-util': ^1.4.11 '@chakra-ui/anatomy': ^2 '@chakra-ui/icons': ^2 '@chakra-ui/next-js': ^2 @@ -26,21 +30,25 @@ catalog: '@emotion/react': ^11 '@emotion/styled': ^11 '@fastgpt-sdk/anydoc': 0.2.5 + '@hono/node-server': ^2.0.10 '@llamaindex/liteparse-wasm': 2.0.8 '@modelcontextprotocol/sdk': ^1 '@node-rs/jieba': 2.0.1 '@svgr/webpack': ^6.5.1 '@t3-oss/env-core': 0.13.10 '@tanstack/react-query': ^4.24.10 + '@tanstack/react-table': ^8.21.3 '@types/js-yaml': ^4.0.9 '@types/jsonwebtoken': ^9.0.3 '@types/lodash-es': ^4 '@types/mime-types': ^3.0.1 '@types/node': ^24.13.3 + '@types/nodemailer': ^6.4.24 '@types/proxy-addr': 2.0.3 '@types/react': ^18 '@types/react-dom': ^18 '@types/request-ip': ^0.0.38 + '@types/xml2js': ^0.4.14 '@vitest/coverage-v8': ^4.1.5 '@xterm/addon-fit': ^0.10.0 '@xterm/xterm': ^5.5.0 @@ -55,13 +63,14 @@ catalog: file-type: 21.3.0 gpt-tokenizer: 3.4.0 hono: 4.12.27 - '@hono/node-server': ^2.0.10 i18next: 23.16.8 ipaddr.js: ^2.4.0 js-yaml: ^4.1.1 json5: ^2.2.3 jsonwebtoken: ^9.0.3 lodash-es: ^4.17.21 + marked: ^18.0.9 + mermaid: ^11.16.1 mime: ^4.1.0 mime-types: 3.0.2 minio: 8.0.7 @@ -70,6 +79,8 @@ catalog: nanoid: ^5.1.3 next: 16.3.0 next-i18next: 15.4.2 + nodemailer: ^7.0.13 + postcss: ^8.5.12 proxy-addr: 2.0.7 proxy-agent: ^6 react: ^18 @@ -88,10 +99,9 @@ catalog: undici: ^7.29.0 vaul: ^1.1.2 vitest: ^4.1.5 + xml2js: ^0.6.2 zod: ^4 - postcss: ^8.5.12 - marked: ^18.0.9 - mermaid: ^11.16.1 + qs: ^6.16.0 catalogMode: prefer @@ -122,11 +132,11 @@ onlyBuiltDependencies: - vue-demi overrides: - '@tootallnate/once': 2.0.1 - 'proxy-agent>pac-proxy-agent': 7.2.0 '@swc/helpers': ^0.5.23 + '@tootallnate/once': 2.0.1 '@types/react': ^18 '@types/react-dom': ^18 + 'proxy-agent>pac-proxy-agent': 7.2.0 react: ^18 react-dom: ^18 diff --git a/pro b/pro index bb9abfd636a0..a549e99f22bd 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit bb9abfd636a0b5e0fed09d4f73cdd34963bed990 +Subproject commit a549e99f22bd57c6fe2511e1d4ed243c5993c7e7 diff --git a/projects/app/.env.template b/projects/app/.env.template index 14e7ba1694fb..19ca032baa59 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -21,6 +21,9 @@ ROOT_KEY=fdafasd # 商业版地址 # PRO_URL= # PRO_TOKEN= +# License-server 公钥(与签发服务的 LICENSE_PRIVATE_KEY 配对,二选一) +# LICENSE_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY----- +# LICENSE_PUBLIC_KEY_PATH=../../legacy_public_key.pem # 官网访客归因 CRM(地址未配置时不进行身份上报) # CRM_API_URL=https://crm.example.com/api/v1 diff --git a/projects/app/AGENTS.md b/projects/app/AGENTS.md new file mode 100644 index 000000000000..643577dfaef3 --- /dev/null +++ b/projects/app/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/projects/app/CLAUDE.md b/projects/app/CLAUDE.md new file mode 100644 index 000000000000..43c994c2d361 --- /dev/null +++ b/projects/app/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/projects/app/next.config.ts b/projects/app/next.config.ts index 39c1265f2436..f6ae9c973336 100644 --- a/projects/app/next.config.ts +++ b/projects/app/next.config.ts @@ -61,6 +61,21 @@ const nextConfig: NextConfig = { } ]; }, + // 旧 root 配置路由迁移到 /admin 后的重定向(避免历史链接/书签失效) + async redirects() { + return [ + { + source: '/config/plugin/tool', + destination: '/admin/config/plugin', + permanent: false + }, + { + source: '/config/model', + destination: '/admin/config/modelProvider', + permanent: false + } + ]; + }, turbopack: { root: path.join(__dirname, '../../'), rules: { diff --git a/projects/app/package.json b/projects/app/package.json index b05077b173ed..1284e023588e 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -29,6 +29,9 @@ "pnpm": "10.x" }, "dependencies": { + "@alicloud/dysmsapi20170525": "catalog:", + "@alicloud/openapi-client": "catalog:", + "@alicloud/tea-util": "catalog:", "@chakra-ui/anatomy": "catalog:", "@chakra-ui/icons": "catalog:", "@chakra-ui/next-js": "catalog:", @@ -55,6 +58,11 @@ "@scalar/api-reference-react": "^0.9.59", "@t3-oss/env-core": "catalog:", "@tanstack/react-query": "catalog:", + "@tanstack/react-table": "catalog:", + "@types/nodemailer": "catalog:", + "@types/xml2js": "catalog:", + "@xterm/addon-fit": "catalog:", + "@xterm/xterm": "catalog:", "ahooks": "catalog:", "archiver": "^7.0.1", "axios": "catalog:", @@ -84,6 +92,7 @@ "nanoid": "catalog:", "next": "catalog:", "next-i18next": "catalog:", + "nodemailer": "catalog:", "nprogress": "^0.2.0", "p-limit": "^7.2.0", "qrcode": "^1.5.4", @@ -92,8 +101,6 @@ "react-hook-form": "catalog:", "react-i18next": "catalog:", "react-markdown": "catalog:", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.1", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.4", "reactflow": "^11.7.4", @@ -103,6 +110,8 @@ "remark-breaks": "^4.0.0", "remark-gfm": "catalog:", "remark-math": "^6.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.1", "remend": "catalog:", "sass": "^1.58.3", "undici": "catalog:", @@ -110,9 +119,8 @@ "use-context-selector": "^1.4.4", "vaul": "catalog:", "vfile": "^6.0.3", - "zod": "catalog:", - "@xterm/addon-fit": "catalog:", - "@xterm/xterm": "catalog:" + "xml2js": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@next/bundle-analyzer": "16.1.6", diff --git a/projects/app/public/icon/user.svg b/projects/app/public/icon/user.svg new file mode 100644 index 000000000000..267c1c1c796a --- /dev/null +++ b/projects/app/public/icon/user.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx index ebe84df64b3f..74fdc8b7c94e 100644 --- a/projects/app/src/components/Layout/index.tsx +++ b/projects/app/src/components/Layout/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Box } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import { useLoading } from '@fastgpt/web/hooks/useLoading'; @@ -54,6 +54,9 @@ const ActivityAdModal = dynamic(() => import('@/components/support/activity/Acti const ProModal = dynamic(() => import('@/components/ProTip/ProModal'), { ssr: false }); +const LicenseInput = dynamic(() => import('@/components/admin/License/Input'), { + ssr: false +}); const pcUnShowLayoutRoute: Record = { '/': true, @@ -92,13 +95,34 @@ const Layout = ({ children }: { children: JSX.Element }) => { const { toast } = useToast(); const { t } = useClientTranslation('price'); const { Loading } = useLoading(); - const { setLastRoute, loading, feConfigs, showProModal, setShowProModal } = useSystemStore(); + const { + setLastRoute, + loading, + feConfigs, + showProModal, + setShowProModal, + licenseData, + licenseLoading, + initLicenseData + } = useSystemStore(); const { isPc } = useSystem(); const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore(); const modelLoginGeneration = useUserModelStore((state) => state.loginGeneration); const { setUserDefaultLng, setShareDefaultLng } = useI18nLng(); const checkedModelIdentityRef = useRef(); + // root 登录后检测 license 状态(开源版未激活时提示激活/购买商业版) + const [dismissLicenseModal, setDismissLicenseModal] = useState(false); + const isRoot = userInfo?.username === 'root'; + + useEffect(() => { + if (!userInfo || !isRoot) return; + void initLicenseData(); + }, [initLicenseData, isRoot, userInfo]); + + // 检测完成前不弹窗,避免已有 license 时刷新页面闪烁激活弹窗 + const showLicenseModal = isRoot && !licenseLoading && !licenseData && !dismissLicenseModal; + // Auto redeem coupon useCheckCoupon(); @@ -226,6 +250,9 @@ const Layout = ({ children }: { children: JSX.Element }) => { )} + {/* 开源版未激活 license 时,root 提示激活/购买商业版(可取消) */} + {showLicenseModal && setDismissLicenseModal(true)} />} + {showProModal && setShowProModal(false)} />} diff --git a/projects/app/src/components/Layout/navbar.tsx b/projects/app/src/components/Layout/navbar.tsx index 504039cb232f..77acec93a3cb 100644 --- a/projects/app/src/components/Layout/navbar.tsx +++ b/projects/app/src/components/Layout/navbar.tsx @@ -104,13 +104,10 @@ const Navbar = ({ unread }: { unread: number }) => { label: t('common:navbar.Config'), icon: 'support/config/configLight', activeIcon: 'support/config/configFill', - link: '/config/plugin/tool', - activeLink: [ - '/config/plugin/tool', - '/config/plugin/marketplace', - '/config/model', - '/config/system/migrations' - ] + link: '/admin/dashboard', + // 管理员区域路由前缀匹配;旧 /config 路由已重定向,activeLink 保留以兼容重定向生效前的瞬时路径 + activePrefix: ['/admin'], + activeLink: ['/config/plugin/tool', '/config/model'] } ] : []) @@ -143,7 +140,9 @@ const Navbar = ({ unread }: { unread: number }) => { {/* 导航列表 */} {navbarList.map((item) => { - const isActive = item.activeLink.includes(router.pathname); + const isActive = + (item.activePrefix?.some((prefix) => router.pathname.startsWith(prefix)) ?? false) || + item.activeLink.includes(router.pathname); return ( { label: t('common:navbar.Config'), icon: 'support/config/configLight', activeIcon: 'support/config/configFill', - link: '/config/plugin/tool', - activeLink: [ - '/config/plugin/tool', - '/config/plugin/marketplace', - '/config/model', - '/config/system/migrations' - ] + link: '/admin/dashboard', + activePrefix: ['/admin'], + activeLink: ['/config/plugin/tool', '/config/model'] } ] : []) diff --git a/projects/app/src/components/SideTabs/Group.tsx b/projects/app/src/components/SideTabs/Group.tsx new file mode 100644 index 000000000000..f3f3a853abf4 --- /dev/null +++ b/projects/app/src/components/SideTabs/Group.tsx @@ -0,0 +1,195 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Box, Flex } from '@chakra-ui/react'; +import type { GridProps } from '@chakra-ui/react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import type { IconNameType } from '@fastgpt/web/components/common/Icon/type'; + +/** + * 支持两级分组的侧栏 tab 定义:父级可携带 children 子项。 + * 无 children 时行为与 SideTabs 单级一致,用于兼容账号页等单层使用。 + */ +export type GroupTab = { + value: ValueType; + label: string; + icon: string; + children?: GroupTab[]; +}; + +export type Props = Omit & { + list: GroupTab[]; + value: ValueType; + size?: 'sm' | 'md' | 'lg'; + onChange: (value: ValueType) => void; +}; + +const SideTabsGroup = ({ + list, + size = 'md', + value, + onChange, + ...props +}: Props) => { + const sizeMap = useMemo(() => { + switch (size) { + case 'sm': + return { + fontSize: 'xs', + inlineP: 1 + }; + case 'md': + return { + fontSize: 'sm', + inlineP: 2 + }; + case 'lg': + return { + fontSize: 'md', + inlineP: 3 + }; + } + }, [size]); + + // 当前激活项所在父级默认展开 + const defaultExpand = useMemo( + () => + list + .filter((item) => item.children?.some((child) => child.value === value)) + .map((item) => item.value), + [list, value] + ); + const [expandValues, setExpandValues] = useState(defaultExpand); + + // 路由切换后保证激活父级展开(setState 同步于外部 value 变化,是必要的派生展开逻辑) + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect -- 父级展开状态需跟随路由 value 同步更新 + setExpandValues((prev) => Array.from(new Set([...prev, ...defaultExpand]))); + }, [defaultExpand]); + + const toggleExpand = (itemValue: ValueType) => { + setExpandValues((prev) => + prev.includes(itemValue) ? prev.filter((i) => i !== itemValue) : [...prev, itemValue] + ); + }; + + const isActive = (itemValue: ValueType) => value === itemValue; + + return ( + + {list.map((item) => { + const hasChildren = !!item.children && item.children.length > 0; + const isExpanded = expandValues.includes(item.value); + + // 单级项:与 SideTabs 行为一致 + if (!hasChildren) { + return ( + { + if (isActive(item.value)) return; + onChange(item.value); + }} + > + + {item.label} + + ); + } + + // 分组父级:可展开/收起,激活子项时高亮并自动展开 + const children = item.children ?? []; + const isGroupActive = children.some((child) => child.value === value); + return ( + + toggleExpand(item.value)} + > + + {item.label} + + + {isExpanded && ( + + {children.map((child) => ( + { + if (isActive(child.value)) return; + onChange(child.value); + }} + > + + {child.label} + + ))} + + )} + + ); + })} + + ); +}; + +export default SideTabsGroup; diff --git a/projects/app/src/components/admin/BoxContainer/Card.tsx b/projects/app/src/components/admin/BoxContainer/Card.tsx new file mode 100644 index 000000000000..595fd7ab7ddc --- /dev/null +++ b/projects/app/src/components/admin/BoxContainer/Card.tsx @@ -0,0 +1,26 @@ +import type { BoxProps } from '@chakra-ui/react'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import type React from 'react'; + +const BoxCard = ({ + children, + ...props +}: BoxProps & { + children: React.ReactNode; + isLoading?: boolean; +}) => { + return ( + + {children} + + ); +}; + +export default BoxCard; diff --git a/projects/app/src/components/admin/License/Input.tsx b/projects/app/src/components/admin/License/Input.tsx new file mode 100644 index 000000000000..e2fd0c693441 --- /dev/null +++ b/projects/app/src/components/admin/License/Input.tsx @@ -0,0 +1,86 @@ +import React, { useEffect, useState } from 'react'; +import MyModal from '@fastgpt/web/components/v2/common/MyModal'; +import { Box, Button, Flex, HStack, Textarea } from '@chakra-ui/react'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import Icon from '@fastgpt/web/components/common/Icon'; +import Markdown from '@/components/admin/markdown'; +import { useRequest } from '@fastgpt/web/hooks/useRequest'; +import { getInstanceId, postActiveLicense } from '@/web/common/license/api'; + +const LicenseInput = ({ onClose }: { onClose?: () => void }) => { + const { initLicenseData } = useSystemStore(); + const [license, setLicense] = useState(''); + // 决策版:绑定标记从 hosts(域名)改为 instanceId,激活时把实例 ID 提供给官方签发 + const [instanceId, setInstanceId] = useState(); + + useEffect(() => { + getInstanceId() + .then(setInstanceId) + .catch(() => setInstanceId(undefined)); + }, []); + + const { runAsync: activeLicense, loading } = useRequest(postActiveLicense, { + onSuccess: () => { + initLicenseData(); + onClose?.(); + }, + successToast: '激活成功' + }); + + return ( + + {onClose && ( + + )} + + + } + > + + + + + + 当前实例 ID: + + {instanceId ?? '加载中…'} + + + + 请把上方实例 ID 提供给官方,官方将签发绑定该实例的 License。 + + + +