fix: M-CHAT audit 修复 7 个缺陷

- A1/A2(P0): listSessions 响应格式 + ChatEntryContext Hashable
- A3: forceCreate 支持,新对话按钮创建新会话
- A4: loadSession 更新 entryContext (scope 标签)
- A6: 死代码清理 (itemIds + AIMessageCitation)
- A8: sessions sheet scope 过滤
- A10: deleteSession 错误处理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-06 17:58:45 +08:00
parent 2610bcf7f9
commit 491c3e7ef0
4 changed files with 46 additions and 16 deletions

View File

@ -480,7 +480,7 @@ enum ChatScopeType: String, Codable {
case global = "global"
}
struct ChatEntryContext {
struct ChatEntryContext: Hashable {
let scopeType: ChatScopeType
let scopeId: String?
let scopeName: String
@ -541,6 +541,7 @@ struct CreateSessionRequest: Codable {
let parentKnowledgeBaseId: String?
let createdFrom: String
let title: String?
let forceCreate: Bool?
}
struct UpdateChatSessionRequest: Codable {

View File

@ -344,13 +344,14 @@ class RagChatService {
static let shared = RagChatService()
private let client = APIClient.shared
func createSession(ctx: ChatEntryContext) async throws -> ChatSession {
func createSession(ctx: ChatEntryContext, forceCreate: Bool = false) async throws -> ChatSession {
let body = CreateSessionRequest(
scopeType: ctx.scopeType.rawValue,
scopeId: ctx.scopeId,
parentKnowledgeBaseId: ctx.parentKnowledgeBaseId,
createdFrom: ctx.createdFrom,
title: nil
title: nil,
forceCreate: forceCreate ? true : nil
)
return try await client.request("/rag-chat/sessions", method: "POST", body: body)
}
@ -360,7 +361,8 @@ class RagChatService {
if let st = scopeType { items.append(URLQueryItem(name: "scopeType", value: st)) }
if let si = scopeId { items.append(URLQueryItem(name: "scopeId", value: si)) }
if let pk = parentKnowledgeBaseId { items.append(URLQueryItem(name: "parentKnowledgeBaseId", value: pk)) }
return try await client.request("/rag-chat/sessions", queryItems: items.isEmpty ? nil : items)
let page: PaginatedResponse<ChatSession> = try await client.request("/rag-chat/sessions", queryItems: items.isEmpty ? nil : items)
return page.data
}
func getMessages(sessionId: String) async throws -> [ChatMessage] {

View File

@ -114,8 +114,12 @@ struct AIChatPage: View {
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
Task {
try? await RagChatService.shared.deleteSession(id: s.id)
do {
try await RagChatService.shared.deleteSession(id: s.id)
sessions.removeAll { $0.id == s.id }
} catch {
// Keep session in list if delete failed
}
}
} label: { Label("删除", systemImage: "trash") }
}
@ -128,7 +132,13 @@ struct AIChatPage: View {
.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.zxBg0)
.presentationDetents([.medium, .large])
.task {
if let s = try? await RagChatService.shared.listSessions() { sessions = s }
let s: [ChatSession]
if let filter = vm.sessionListFilter {
s = (try? await RagChatService.shared.listSessions(scopeType: filter.scopeType, scopeId: filter.scopeId)) ?? []
} else {
s = (try? await RagChatService.shared.listSessions()) ?? []
}
sessions = s
}
}
.task { await vm.load() }

View File

@ -19,12 +19,6 @@ struct AIMessage: Identifiable {
}
}
struct AIMessageCitation: Identifiable {
let id: String
let title: String?
let content: String?
}
enum AIMessageRole {
case user, ai
}
@ -39,7 +33,6 @@ final class AIChatViewModel: ObservableObject {
private var sessionId: String?
private var entryContext: ChatEntryContext?
private var itemIds: [String]?
var canSend: Bool {
!inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending && sessionId != nil
@ -52,6 +45,11 @@ final class AIChatViewModel: ObservableObject {
return "提问范围:\(ctx.scopeName)"
}
var sessionListFilter: (scopeType: String, scopeId: String)? {
guard let ctx = entryContext, ctx.scopeType != .global, let sid = ctx.scopeId else { return nil }
return (ctx.scopeType.rawValue, sid)
}
var scopeIcon: String {
guard let ctx = entryContext else { return "globe" }
switch ctx.scopeType {
@ -101,7 +99,7 @@ final class AIChatViewModel: ObservableObject {
)
do {
let session = try await RagChatService.shared.createSession(ctx: ctx)
let session = try await RagChatService.shared.createSession(ctx: ctx, forceCreate: true)
sessionId = session.id
let scopeLabel = ctx.scopeType != .global ? "\(ctx.scopeName)」的" : ""
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于\(scopeLabel)内容回答问题。")]
@ -167,8 +165,27 @@ final class AIChatViewModel: ObservableObject {
isCreatingSession = true
do {
let msgs: [ChatMessage] = try await RagChatService.shared.getMessages(sessionId: id)
// Update entryContext from the first message's scope snapshot (if available)
if let snapshot = msgs.first?.scopeSnapshot,
let scopeTypeStr = snapshot.scopeType,
let scopeType = ChatScopeType(rawValue: scopeTypeStr) {
entryContext = ChatEntryContext(
scopeType: scopeType,
scopeId: snapshot.scopeId,
scopeName: scopeLabel ?? "AI 对话",
parentKnowledgeBaseId: snapshot.parentKnowledgeBaseId,
createdFrom: "session_switch"
)
}
let scopePrefix: String = {
guard let ctx = entryContext, ctx.scopeType != .global else { return "" }
return "\(ctx.scopeName)」的"
}()
if msgs.isEmpty {
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于你的知识库回答问题。")]
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于\(scopePrefix)内容回答问题。")]
} else {
messages = msgs.map { m in
AIMessage(role: m.role == "user" ? .user : .ai, content: m.content, citations: m.citations)