From 491c3e7ef024dacedac866aa97cc5c56d04053b0 Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 6 Jun 2026 17:58:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20M-CHAT=20audit=20=E4=BF=AE=E5=A4=8D=207?= =?UTF-8?q?=20=E4=B8=AA=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../AIStudyApp/Core/Models/APIModels.swift | 3 +- .../AIStudyApp/Core/Services/APIService.swift | 8 +++-- .../AIStudyApp/Features/AI/AIChatPage.swift | 16 +++++++-- .../Features/AI/AIChatViewModel.swift | 35 ++++++++++++++----- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift index 83b6026..9bd4a84 100644 --- a/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift +++ b/AIStudyApp/AIStudyApp/Core/Models/APIModels.swift @@ -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 { diff --git a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift index 8c9a76c..a7f3faa 100644 --- a/AIStudyApp/AIStudyApp/Core/Services/APIService.swift +++ b/AIStudyApp/AIStudyApp/Core/Services/APIService.swift @@ -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 = try await client.request("/rag-chat/sessions", queryItems: items.isEmpty ? nil : items) + return page.data } func getMessages(sessionId: String) async throws -> [ChatMessage] { diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift index 11abeb5..0ccda93 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIChatPage.swift @@ -114,8 +114,12 @@ struct AIChatPage: View { .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { Task { - try? await RagChatService.shared.deleteSession(id: s.id) - sessions.removeAll { $0.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() } diff --git a/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift b/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift index e1f6ecf..9b8fb17 100644 --- a/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift +++ b/AIStudyApp/AIStudyApp/Features/AI/AIChatViewModel.swift @@ -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)