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:
parent
2610bcf7f9
commit
491c3e7ef0
@ -480,7 +480,7 @@ enum ChatScopeType: String, Codable {
|
|||||||
case global = "global"
|
case global = "global"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatEntryContext {
|
struct ChatEntryContext: Hashable {
|
||||||
let scopeType: ChatScopeType
|
let scopeType: ChatScopeType
|
||||||
let scopeId: String?
|
let scopeId: String?
|
||||||
let scopeName: String
|
let scopeName: String
|
||||||
@ -541,6 +541,7 @@ struct CreateSessionRequest: Codable {
|
|||||||
let parentKnowledgeBaseId: String?
|
let parentKnowledgeBaseId: String?
|
||||||
let createdFrom: String
|
let createdFrom: String
|
||||||
let title: String?
|
let title: String?
|
||||||
|
let forceCreate: Bool?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UpdateChatSessionRequest: Codable {
|
struct UpdateChatSessionRequest: Codable {
|
||||||
|
|||||||
@ -344,13 +344,14 @@ class RagChatService {
|
|||||||
static let shared = RagChatService()
|
static let shared = RagChatService()
|
||||||
private let client = APIClient.shared
|
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(
|
let body = CreateSessionRequest(
|
||||||
scopeType: ctx.scopeType.rawValue,
|
scopeType: ctx.scopeType.rawValue,
|
||||||
scopeId: ctx.scopeId,
|
scopeId: ctx.scopeId,
|
||||||
parentKnowledgeBaseId: ctx.parentKnowledgeBaseId,
|
parentKnowledgeBaseId: ctx.parentKnowledgeBaseId,
|
||||||
createdFrom: ctx.createdFrom,
|
createdFrom: ctx.createdFrom,
|
||||||
title: nil
|
title: nil,
|
||||||
|
forceCreate: forceCreate ? true : nil
|
||||||
)
|
)
|
||||||
return try await client.request("/rag-chat/sessions", method: "POST", body: body)
|
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 st = scopeType { items.append(URLQueryItem(name: "scopeType", value: st)) }
|
||||||
if let si = scopeId { items.append(URLQueryItem(name: "scopeId", value: si)) }
|
if let si = scopeId { items.append(URLQueryItem(name: "scopeId", value: si)) }
|
||||||
if let pk = parentKnowledgeBaseId { items.append(URLQueryItem(name: "parentKnowledgeBaseId", value: pk)) }
|
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] {
|
func getMessages(sessionId: String) async throws -> [ChatMessage] {
|
||||||
|
|||||||
@ -114,8 +114,12 @@ struct AIChatPage: View {
|
|||||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
Task {
|
Task {
|
||||||
try? await RagChatService.shared.deleteSession(id: s.id)
|
do {
|
||||||
sessions.removeAll { $0.id == s.id }
|
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") }
|
} label: { Label("删除", systemImage: "trash") }
|
||||||
}
|
}
|
||||||
@ -128,7 +132,13 @@ struct AIChatPage: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.zxBg0)
|
.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.zxBg0)
|
||||||
.presentationDetents([.medium, .large])
|
.presentationDetents([.medium, .large])
|
||||||
.task {
|
.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() }
|
.task { await vm.load() }
|
||||||
|
|||||||
@ -19,12 +19,6 @@ struct AIMessage: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AIMessageCitation: Identifiable {
|
|
||||||
let id: String
|
|
||||||
let title: String?
|
|
||||||
let content: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AIMessageRole {
|
enum AIMessageRole {
|
||||||
case user, ai
|
case user, ai
|
||||||
}
|
}
|
||||||
@ -39,7 +33,6 @@ final class AIChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
private var sessionId: String?
|
private var sessionId: String?
|
||||||
private var entryContext: ChatEntryContext?
|
private var entryContext: ChatEntryContext?
|
||||||
private var itemIds: [String]?
|
|
||||||
|
|
||||||
var canSend: Bool {
|
var canSend: Bool {
|
||||||
!inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending && sessionId != nil
|
!inputText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending && sessionId != nil
|
||||||
@ -52,6 +45,11 @@ final class AIChatViewModel: ObservableObject {
|
|||||||
return "提问范围:\(ctx.scopeName)"
|
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 {
|
var scopeIcon: String {
|
||||||
guard let ctx = entryContext else { return "globe" }
|
guard let ctx = entryContext else { return "globe" }
|
||||||
switch ctx.scopeType {
|
switch ctx.scopeType {
|
||||||
@ -101,7 +99,7 @@ final class AIChatViewModel: ObservableObject {
|
|||||||
)
|
)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let session = try await RagChatService.shared.createSession(ctx: ctx)
|
let session = try await RagChatService.shared.createSession(ctx: ctx, forceCreate: true)
|
||||||
sessionId = session.id
|
sessionId = session.id
|
||||||
let scopeLabel = ctx.scopeType != .global ? "「\(ctx.scopeName)」的" : ""
|
let scopeLabel = ctx.scopeType != .global ? "「\(ctx.scopeName)」的" : ""
|
||||||
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于\(scopeLabel)内容回答问题。")]
|
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于\(scopeLabel)内容回答问题。")]
|
||||||
@ -167,8 +165,27 @@ final class AIChatViewModel: ObservableObject {
|
|||||||
isCreatingSession = true
|
isCreatingSession = true
|
||||||
do {
|
do {
|
||||||
let msgs: [ChatMessage] = try await RagChatService.shared.getMessages(sessionId: id)
|
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 {
|
if msgs.isEmpty {
|
||||||
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于你的知识库回答问题。")]
|
messages = [AIMessage(role: .ai, content: "你好!我是你的 AI 学习助手,基于\(scopePrefix)内容回答问题。")]
|
||||||
} else {
|
} else {
|
||||||
messages = msgs.map { m in
|
messages = msgs.map { m in
|
||||||
AIMessage(role: m.role == "user" ? .user : .ai, content: m.content, citations: m.citations)
|
AIMessage(role: m.role == "user" ? .user : .ai, content: m.content, citations: m.citations)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user