diff --git a/Directory.Packages.props b/Directory.Packages.props
index 444bacb..b9780b0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -27,6 +27,7 @@
+
diff --git a/README.md b/README.md
index 2fe608d..6ca5e4c 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@
- [`docs/migration/phase-4-external-provider-decoupling.md`](docs/migration/phase-4-external-provider-decoupling.md)
- [`docs/migration/phase-5-backoffice-worker-operations.md`](docs/migration/phase-5-backoffice-worker-operations.md)
- [`docs/migration/phase-7-student-experience-and-content-consumption.md`](docs/migration/phase-7-student-experience-and-content-consumption.md)
+- [`docs/migration/phase-8-ai-foundation.md`](docs/migration/phase-8-ai-foundation.md)
## 技术栈与分层
@@ -133,7 +134,7 @@ Tiku.IntegrationTests # API / EF 模型集成测试
- Provider 配置统一落 `TenantExternalProvider` + `TenantSecret`。
- `ConfigPublic` 只保存公开字段,例如 appId、merchantId、region、endpoint、bucketAlias、templateCode。
- 密钥只通过 `SecretRef` 关联 `TenantSecret`,禁止把 secret/token/key/privateKey 写入公开配置。
-- AI 暂不实现;后续独立阶段默认使用 Microsoft Semantic Kernel,租户模型 API Key 仍走 `TenantExternalProvider(capability=ai)` + `TenantSecret`,业务层不直接引用 SK namespace。
+- AI 底座开始引入 Microsoft Semantic Kernel,但包只放在 Infrastructure;租户模型 API Key 走 `TenantExternalProvider(capability=ai)` + `TenantSecret`,业务层不直接引用 SK namespace。当前优先场景是租户教师后台对话和 AI 审核题目反馈。
资源存储方向:
diff --git a/Tiku.Domain/Tenancy/TenantOperationsEntities.cs b/Tiku.Domain/Tenancy/TenantOperationsEntities.cs
index ee2778a..e5eecb6 100644
--- a/Tiku.Domain/Tenancy/TenantOperationsEntities.cs
+++ b/Tiku.Domain/Tenancy/TenantOperationsEntities.cs
@@ -168,7 +168,7 @@ public sealed class TenantStudentFollowup : AuditableTenantEntity
public Guid? UpdatedBy { get; set; }
}
-public enum TenantExternalProviderCapability { Identity, ObjectStorage, Sms, Payment, Notification }
+public enum TenantExternalProviderCapability { Identity, ObjectStorage, Sms, Payment, Notification, Ai }
public enum TenantExternalProviderStatus { Active, Disabled, Testing }
public enum TenantSecretStatus { Active, Disabled, Rotating }
public enum SmsPurpose { Login, BindPhone, ResetPassword }
diff --git a/Tiku.Infrastructure/Tiku.Infrastructure.csproj b/Tiku.Infrastructure/Tiku.Infrastructure.csproj
index 9793ad6..3ff3704 100644
--- a/Tiku.Infrastructure/Tiku.Infrastructure.csproj
+++ b/Tiku.Infrastructure/Tiku.Infrastructure.csproj
@@ -16,6 +16,7 @@
+
diff --git a/Tiku.IntegrationTests/Api/CrmEndpointTests.cs b/Tiku.IntegrationTests/Api/CrmEndpointTests.cs
index 87556f6..991be90 100644
--- a/Tiku.IntegrationTests/Api/CrmEndpointTests.cs
+++ b/Tiku.IntegrationTests/Api/CrmEndpointTests.cs
@@ -89,7 +89,7 @@ public sealed class CrmEndpointTests
LeadId = "lead-1",
Outcome = "failed",
ErrorMessage = "password leaked",
- RequestPayload = JsonSerializer.SerializeToElement(new { token = "abc" }),
+ RequestPayload = JsonSerializer.SerializeToElement(new { token = "crm-sensitive-token-value" }),
ResponseSummary = "failed"
});
using var client = factory.CreateClient();
@@ -109,7 +109,7 @@ public sealed class CrmEndpointTests
Assert.Equal(HttpStatusCode.OK, deadLetters.StatusCode);
Assert.Equal(HttpStatusCode.OK, logs.StatusCode);
Assert.DoesNotContain("hidden-value", queueBody, StringComparison.OrdinalIgnoreCase);
- Assert.DoesNotContain("abc", logsBody, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("crm-sensitive-token-value", logsBody, StringComparison.OrdinalIgnoreCase);
Assert.Equal(HttpStatusCode.OK, retry.StatusCode);
Assert.Equal("Pending", retried!.Status);
}
diff --git a/docs/migration-roadmap.md b/docs/migration-roadmap.md
index 25a161a..3d0b70f 100644
--- a/docs/migration-roadmap.md
+++ b/docs/migration-roadmap.md
@@ -11,6 +11,7 @@
- [`docs/migration/phase-4-external-provider-decoupling.md`](migration/phase-4-external-provider-decoupling.md)
- [`docs/migration/phase-5-backoffice-worker-operations.md`](migration/phase-5-backoffice-worker-operations.md)
- [`docs/migration/phase-7-student-experience-and-content-consumption.md`](migration/phase-7-student-experience-and-content-consumption.md)
+- [`docs/migration/phase-8-ai-foundation.md`](migration/phase-8-ai-foundation.md)
迁移原则:
@@ -82,7 +83,7 @@
剩余部分不建议再按“旧接口数量”机械推进。第六阶段已经把平台后台、租户后台、交易运营和 Worker 最小闭环打通;第七阶段补齐了学生端视频、签到积分流水和内容导入异步化。后续应按“能上线运营”和“体验增强”拆分。
-### 1. AI 推荐报告,独立阶段
+### 1. AI 底座与教师端对话,独立阶段
旧版相关模块:
@@ -90,6 +91,9 @@
目标能力:
+- 租户教师后台基础 AI 对话。
+- 对话 function calling 能力预留,但只允许调用受审计的后端业务函数。
+- AI 审核题目反馈,输出审核建议和风险等级,不直接改业务状态。
- 学校推荐报告列表。
- 推荐详情。
- 推荐报告导出。
@@ -103,7 +107,7 @@
- 租户 API Key 存入 `TenantSecret`,不进入业务 DTO。
- `Microsoft.SemanticKernel` 只允许出现在 Infrastructure AI provider 实现中。
- Application 层只暴露业务抽象,例如 `IAiRecommendationProvider` / `IAiKernelFactory`。
-- 不直接照搬旧 prompt 或推荐算法,先结合分数线动态字段、用户画像、目标院校和志愿规则重新设计。
+- 教师对话先做基础能力;推荐报告不直接照搬旧 prompt 或推荐算法,后续结合分数线动态字段、用户画像、目标院校和志愿规则重新设计。
### 2. 内容导出和导入处理器增强
@@ -150,7 +154,7 @@
## 推荐后续顺序
```text
-1. AI 推荐报告独立阶段:Semantic Kernel + 租户自带 API Key
+1. AI 底座独立阶段:Semantic Kernel + 租户自带 API Key + 教师端对话
2. 内容导出 + 导入处理器增强
3. Worker 统计、扫描、对账、域名复验处理器补强
4. 后台运营细化:secrets、监督、洞察、审计、发票/催缴
diff --git a/docs/migration/phase-8-ai-foundation.md b/docs/migration/phase-8-ai-foundation.md
new file mode 100644
index 0000000..4b3487e
--- /dev/null
+++ b/docs/migration/phase-8-ai-foundation.md
@@ -0,0 +1,144 @@
+# 第八阶段:AI 底座与教师端对话
+
+第八阶段从“只预留 AI”进入 AI 底座建设。AI 当前只面向租户教师和后台运营,不开放给学生端。
+
+## 当前 AI 使用场景
+
+### 1. 租户教师 AI 对话
+
+第一批先做基础对话能力:
+
+- 教师在租户后台发起对话。
+- AI 根据当前租户配置调用模型。
+- 对话历史按租户和教师隔离保存。
+- 响应不暴露模型 API Key、Provider 原始响应密钥、内部租户 ID 或对象存储细节。
+- 后续可追加 function calling,用于读取或操作受控业务能力。
+
+预留 function call 的原则:
+
+- Controller 不直接暴露任意 tool/function 名称给客户端调用。
+- Application 层定义可用业务函数目录,例如题目检索、题目草稿生成、班级学习概览、学生跟进建议。
+- Infrastructure 用 Semantic Kernel 把受审计的业务函数注册为 plugin。
+- 每个 function call 都必须记录租户、教师、会话、函数名、输入摘要、结果状态和耗时。
+- 有写操作的 function 必须复用现有 RBAC、DataScope、Tenant Scope 和 AuditLog,不允许 AI 绕过后台权限。
+
+### 对话存储模型
+
+不要把 Semantic Kernel 的 `ChatHistory`、`ChatMessageContent`、`KernelContent`、tool call object graph 或 provider 原始 response 直接作为 EF Core 持久化模型。原因:
+
+- SK 的对象模型适合运行时编排,不适合作为长期数据库 schema。
+- OpenAI-compatible provider 的消息格式并不完全等价;DeepSeek 这类接口对 `role`、`content`、`tool_calls`、`tool_call_id` 的结构要求更严格。
+- 如果把 SK metadata、内部 content item 或历史 tool 结构原样回放给 DeepSeek,容易触发请求参数错误。
+- 后续换 provider、增加 function call 或做消息压缩时,直接持久化 SK 对象会变成强耦合。
+
+数据库只保存 provider-neutral 的规范化消息:
+
+- `AiConversation`
+ - `TenantId`
+ - `TeacherUserId`
+ - `Title`
+ - `Scenario`:`teacher_chat`、后续可扩展。
+ - `ProviderCode`
+ - `Model`
+ - `Status`
+ - `Metadata`
+- `AiConversationMessage`
+ - `TenantId`
+ - `ConversationId`
+ - `Sequence`
+ - `Role`:固定为 `system`、`user`、`assistant`、`tool`。
+ - `ContentText`
+ - `ToolCallId`
+ - `ToolName`
+ - `ToolArguments`
+ - `ToolResultSummary`
+ - `ProviderMessageId`
+ - `TokenInput`
+ - `TokenOutput`
+ - `Metadata`
+- `AiToolCallLog`
+ - `TenantId`
+ - `ConversationId`
+ - `MessageId`
+ - `ToolCallId`
+ - `ToolName`
+ - `InputSummary`
+ - `ResultStatus`
+ - `DurationMs`
+ - `ErrorCode`
+
+运行时转换规则:
+
+1. Application 层读取规范化消息,不产生 SK 类型。
+2. Infrastructure adapter 把规范化消息转换成 Semantic Kernel `ChatHistory`。
+3. DeepSeek/OpenAI-compatible adapter 只发送 provider 接受的字段:
+ - 普通消息:`role + content`。
+ - assistant tool call:`role=assistant + tool_calls`。
+ - tool 结果:`role=tool + tool_call_id + content`。
+4. Provider 原始响应只保存必要摘要和可审计 ID,不作为下一轮请求的直接输入。
+5. 任何无法被目标 provider 表达的 SK metadata 都必须丢弃或写入内部 `Metadata`,不得回放给模型 API。
+
+### 2. AI 审核题目反馈
+
+这个场景保持简单,不做复杂扩展:
+
+- 输入:题目反馈内容、题目基本信息、反馈类型、提交用户上下文摘要。
+- 输出:审核建议、风险等级、归类标签、是否建议人工复核。
+- 不做 function calling。
+- 不直接修改题目、反馈状态或用户数据。
+- 只生成建议结果,最终状态变更仍由教师或运营人员确认。
+
+## Provider 与密钥边界
+
+- AI Provider 使用 `TenantExternalProvider(capability=ai)`。
+- 租户自己的模型 API Key 存入 `TenantSecret`,通过 `SecretRef` 关联。
+- `ConfigPublic` 只允许保存公开配置,例如 provider、model、endpoint、deployment、temperature 默认值、max token 限制。
+- `ConfigPublic` 禁止出现 `secret`、`token`、`apiKey`、`key`、`privateKey` 等敏感字段。
+- `Microsoft.SemanticKernel` NuGet 包只引用在 `Tiku.Infrastructure`。
+- `Tiku.Api`、`Tiku.Application`、`Tiku.Domain` 不直接引用 Semantic Kernel namespace。
+
+## 建议模块边界
+
+Application 层后续只放业务抽象:
+
+- `IAiConversationService`
+- `IAiFeedbackReviewService`
+- `IAiKernelFactory`
+- `IAiProviderConfigService`
+
+Infrastructure 层负责:
+
+- 根据当前租户 Provider 配置和 `TenantSecret` 创建 Kernel。
+- 注册受审计 plugin。
+- 调用 chat completion。
+- 处理 provider 错误、超时、重试和调用日志。
+
+Domain 层可增加持久化模型:
+
+- `AiConversation`
+- `AiConversationMessage`
+- `AiToolCallLog`
+- `AiFeedbackReview`
+
+## 第一批实施顺序
+
+1. 增加 AI Provider capability、Semantic Kernel 包和架构测试边界。
+2. 增加 AI 配置服务测试:租户 A/B 不能互读模型配置和密钥。
+3. 增加教师对话数据模型和最小 API:
+ - `POST /api/tenant-admin/ai/conversations`
+ - `GET /api/tenant-admin/ai/conversations`
+ - `GET /api/tenant-admin/ai/conversations/{conversationId}`
+ - `POST /api/tenant-admin/ai/conversations/{conversationId}/messages`
+4. 增加 fake AI provider,先跑通对话和日志,不接真实模型。
+5. 增加题目反馈审核最小 API:
+ - `POST /api/tenant-admin/ai/question-feedback/review`
+6. 最后再接真实模型 Provider。
+
+## 暂不做
+
+- 学生端 AI。
+- 复杂 RAG。
+- 自动改题、自动发布题目。
+- 自动处理反馈状态。
+- 让客户端指定任意 function call。
+- 生产环境使用明文 API Key 配置。