- Assist/Agent 模式新增朗读开关 + NSSpeechSynthesizer 语音合成 - 朗读完成后弹窗延迟5秒带动画关闭(不再写死15/20秒) - 打字模式下不自动关闭弹窗 - 二次输入时取消旧朗读/旧延迟关闭,避免竞争问题 - Agent 进度计时器第二次输入时正确归零 - DMG 打包自动清空 models.json API Key - Node.js 运行时打包进 Agent/bin/,用户无需安装 - Agent system prompt 更新为 VoiceInput 身份 + 语音容错说明 - AGENTS.md 全面更新,补全 Agent 模式文档
107 lines
3.1 KiB
Swift
107 lines
3.1 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - Agent 进度跟踪
|
||
|
||
final class AgentProgressTracker: ObservableObject {
|
||
struct ToolEntry: Identifiable {
|
||
let id = UUID()
|
||
let tool: String
|
||
let display: String
|
||
var resultLines: [String] = []
|
||
}
|
||
|
||
@Published var mode: String = "idle" // idle / thinking / running / done / error
|
||
@Published var elapsedSeconds: Int = 0
|
||
@Published var toolEntries: [ToolEntry] = []
|
||
@Published var finalContent: String = ""
|
||
@Published var errorMessage: String = ""
|
||
@Published var streamingContent: String = "" // 实时流式输出
|
||
@Published var isTextInputMode: Bool = false // 打字输入模式标记(外部控制)
|
||
|
||
private var timer: Timer?
|
||
private var startTime: Date = Date()
|
||
private var toolPhaseActive = false // 工具后首段内容应替换
|
||
|
||
func start() {
|
||
startTime = Date()
|
||
elapsedSeconds = 0
|
||
mode = "thinking"
|
||
toolEntries = []
|
||
finalContent = ""
|
||
errorMessage = ""
|
||
streamingContent = ""
|
||
toolPhaseActive = false
|
||
|
||
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||
guard let self = self, self.mode != "done", self.mode != "error" else { return }
|
||
self.elapsedSeconds = Int(Date().timeIntervalSince(self.startTime))
|
||
}
|
||
}
|
||
|
||
/// 打字模式:清空状态但不进入 thinking
|
||
func resetForInput() {
|
||
mode = "idle"
|
||
elapsedSeconds = 0
|
||
toolEntries = []
|
||
finalContent = ""
|
||
errorMessage = ""
|
||
streamingContent = ""
|
||
toolPhaseActive = false
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
func appendChunk(_ chunk: String) {
|
||
if toolPhaseActive {
|
||
streamingContent = chunk
|
||
toolPhaseActive = false
|
||
} else {
|
||
streamingContent += chunk
|
||
}
|
||
}
|
||
|
||
func addTool(_ tool: String, display: String) {
|
||
mode = "running"
|
||
toolPhaseActive = true // 标记:下一段内容应替换
|
||
toolEntries.append(ToolEntry(tool: tool, display: display))
|
||
}
|
||
|
||
func updateToolResult(_ tool: String, resultLines: [String]) {
|
||
if let idx = toolEntries.lastIndex(where: { $0.tool == tool && $0.resultLines.isEmpty }) {
|
||
toolEntries[idx].resultLines = resultLines
|
||
}
|
||
}
|
||
|
||
func finish(content: String) {
|
||
mode = "done"
|
||
finalContent = content
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
func fail(_ message: String) {
|
||
mode = "error"
|
||
errorMessage = message
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
var formattedTime: String {
|
||
let m = elapsedSeconds / 60
|
||
let s = elapsedSeconds % 60
|
||
return String(format: "%02d:%02d", m, s)
|
||
}
|
||
|
||
var summary: String {
|
||
if mode == "done" {
|
||
return "工作完成 · \(formattedTime)"
|
||
} else if mode == "error" {
|
||
return "错误: \(errorMessage)"
|
||
} else if mode == "idle" {
|
||
return "准备就绪"
|
||
} else {
|
||
return "工作中 \(formattedTime)"
|
||
}
|
||
}
|
||
}
|