工作区是 AIManage 的租户内配置边界:它聚合名称、状态、默认模型和系统提示词;Provider 是带密钥的非对称子表;AIModel 是独立统一聚合。三者目前都能持久化,但只有工作区和默认 Provider 真正进入聊天执行路径。
1. 数据模型与职责
Provider 与 Message 使用 *Record 命名,因为它们是聚合跨表的真实非对称子项,不是旧式 1:1 Entity 镜像。基础设施只依赖 IEntitySet<T>,由统一 ORM 元数据生成器向 SqlSugar 与 EF Core 投影。
2. 创建工作区
var result = await mediator.Send(new ManageWorkspace.CreateCommand( Name: "客服知识助手", Description: "只用于已审核知识库问答", DefaultModelId: "gpt-4o-mini", SystemPrompt: "回答必须引用资料;没有依据时明确说明。"), cancellationToken);
// Handler 从 ICurrentUser 获取 TenantId,调用 AIWorkspace.Create,// 最终 Id 由持久化适配器分配,而不是应用层自行生成。return result.Value;当前只拒绝空白名称。名称长度、租户内唯一、SystemPrompt 大小、内容分类和 DefaultModelId 是否存在都没有在 Handler 中验证。AIWorkspace.Create 的领域约束仍应以源码为准,但 HTTP 契约没有完整的验证错误矩阵。
3. 添加 Provider 与单默认项
POST /api/ai/workspaces/ws-001/providersContent-Type: application/json
{ "providerType": "OpenAICompatible", "name": "production-east", "endpoint": "https://ai-gateway.example.com/v1", "apiKey": "<secret-from-vault>", "isDefault": true}Store 先用 workspaceId + current TenantId 验证工作区,再生成 Provider Id。若新项为默认项,它读取同工作区其他默认项并逐个取消,然后写入新项。同一工作单元内 pending Provider 也会被同步处理。
这保证了普通串行用例的“只有一个默认项”,但数据库元数据只有 (TenantId, WorkspaceId) 普通索引,没有过滤唯一约束。两个并发事务仍可能都观察不到对方并各自写入默认项。GA 需要数据库可表达的唯一策略、串行化更新或租户工作区锁,并把冲突映射为稳定错误。
4. 凭据加密的真实语义
ApiKeyCipher 使用 ASP.NET Core Data Protection,purpose 为 AIManage.ApiKey。保存时 Protect,读取 Provider 时 Unprotect,列表 DTO 不返回密钥。
// 示例展示部署要求,不表示仓库已经完成该 Host 配置。services.AddDataProtection() // 多实例必须共享并持久化 key ring;具体存储由部署平台决定。 .SetApplicationName("BitzOrcas.Modern");当前 Decrypt 捕获所有异常并原样返回输入,用于兼容历史明文。这意味着 key ring 丢失、密文损坏或 purpose 不匹配时,密文可能被当作真实 API Key 发送给 Provider。生产应采用带版本/前缀的密文格式:明确识别历史明文,只迁移一次;已标记密文解密失败必须失败关闭并告警。
还要固定以下部署契约:
- key ring 跨重启和多实例共享;
- key 加密密钥来自 KMS/Key Vault,而非发布目录;
- 轮换前后均能解密存量凭据;
- 备份恢复同时恢复 key ring;
- 日志、异常、配置快照和诊断转储不得包含 ApiKey。
5. 模型配置与执行脱节
SaveModelAsync 会从 Provider 继承 TenantId,保存 ModelIdentifier、DisplayName、ModelType、MaxTokens 与 Temperature。值得注意的是外部 AIModelInfo 不携带 TenantId,Store 查 Provider 时只按 ProviderId;如果 Provider Id 不是全局唯一或该端口被错误暴露,租户边界不够自解释。
更重要的是 Send/Stream 根本不读取 AIModel。调用方可直接给任意 ModelId,执行选项固定为 4096/0.7。因此配置模型不会自动改变推理参数。
// 这是目标用例,不是当前源码逐字复制。var resolved = await modelPolicy.ResolveAsync( tenantId, workspaceId, requestedModelId, cancellationToken);
// 解析结果同时固定 Provider、模型标识和允许的执行上限。var options = new ChatOptions{ ModelId = resolved.ModelIdentifier, MaxOutputTokens = Math.Min(requestedMaxTokens, resolved.MaxTokens), Temperature = resolved.Temperature};6. 系统提示词优先级
创建对话时可传 SystemPrompt。未传时才复制工作区当前 SystemPrompt;发送时又使用 conversation.SystemPrompt ?? workspace.SystemPrompt。因此:显式对话 Prompt 是快照;空值对话会随工作区 Prompt 后续变化。需要在产品契约中明确“快照”还是“动态继承”,并保存 Prompt 版本/哈希,才能重放和审计历史结果。
7. 缓存与密钥变更
聊天客户端缓存键包含 ProviderId、ModelId,以及 Endpoint/ModelId/ApiKey 的 SHA-256 前 16 个十六进制字符;CacheEntry 还保存原始 ApiKey 以核对碰撞。密钥或端点变化会生成新键,但旧客户端只能等待 LRU 驱逐或工厂 Dispose。
并发创建相同键时,两个线程都可能在锁外构建客户端;后进入锁的线程返回已有实例,却没有 Dispose 自己新建但未使用的实例。这是资源泄漏窗口。缓存也没有显式凭据轮换通知、按 Provider 失效、构建失败退避或命中率指标。
8. 配置审查清单
| 主题 | 当前事实 | GA 要求 |
|---|---|---|
| Workspace Name | 仅非空 | 长度、唯一、字符策略 |
| Provider Endpoint | DTO 接收字符串 | HTTPS/allowlist/SSRF 策略 |
| ApiKey | Data Protection | key ring、版本迁移、失败关闭 |
| 默认 Provider | 应用层取消旧默认 | 并发唯一与冲突映射 |
| ModelId | 任意请求字符串 | 受管目录与权限/配额 |
| 参数 | 固定 4096 / 0.7 | 读取 AIModel 并设上下限 |
| SystemPrompt | 工作区/对话字符串 | 大小、版本、分类、审计 |
9. 测试重点
现有持久化测试覆盖租户 Provider 列表、默认项切换、模型保存和工作区归档;双 ORM parity 覆盖核心 Store 行为。仍需补:并发默认项、跨租户 ProviderId、密文损坏、key ring 丢失与轮换、Endpoint SSRF、缓存并发泄漏、模型覆盖拒绝、Prompt 版本和真实 Provider contract test。
10. 故障处置原则
Provider 密文无法解密、Endpoint 不受信任或模型不在目录时必须失败关闭,不能自动切换到另一个租户 Provider。缓存构建失败应返回可诊断的稳定错误并限制重试,且日志只记录 Provider/Model 标识与关联 ID,不记录 ApiKey、完整 Prompt 或用户内容。
AIManage 总览 · 对话与安全 · 测试与 GA