import Foundation // 与 PromptBuilder / ChatModels 中的常量保持一致。 // 仅供 OutputCleaner 内部使用,外部使用方分别在自己的文件里维护副本。 private let thinkingOpenMarker = "[[PHONECLAW_THINK]]" private let thinkingCloseMarker = "[[/PHONECLAW_THINK]]" extension AgentEngine { // MARK: - 中间输出/Prompt 回声识别 func looksLikeStructuredIntermediateOutput(_ text: String) -> Bool { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } if trimmed.hasPrefix("```json") || trimmed.hasPrefix("```") { return true } if let regex = try? NSRegularExpression( pattern: "\"[A-Za-z_][A-Za-z0-9_]*\"\\s*:", options: [] ) { let matchCount = regex.numberOfMatches( in: trimmed, range: NSRange(trimmed.startIndex..., in: trimmed) ) if matchCount >= 2 && !trimmed.hasPrefix("{") { return true } } let suspiciousFragments = [ "tool_name\":", "result_for_user_name\":", "text_for_display\":", "tool_operation_success\":", "arguments_for_tool_no_skill\":", "memory_user_power_conversion\":" ] if suspiciousFragments.filter({ trimmed.contains($0) }).count >= 2 { return true } guard let data = trimmed.data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) else { return false } if let dict = json as? [String: Any] { if dict["name"] != nil { return false } let suspiciousKeys = [ "final_answer", "tool_call", "arguments", "device_call", "next_action", "action", "tool" ] return suspiciousKeys.contains { dict[$0] != nil } } return false } func looksLikePromptEcho(_ text: String) -> Bool { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } if trimmed.hasPrefix("user\n") || trimmed == "user" { return true } let suspiciousPhrases = [ "根据已加载的 Skill", "不要将任何关于工具、系统或该请求的描述变成 Markdown 代码或 JSON 模板", "如果需要,请直接调用", "package_name", "text_for_user" ] let hitCount = suspiciousPhrases.reduce(into: 0) { count, phrase in if trimmed.contains(phrase) { count += 1 } } return hitCount >= 2 } // MARK: - 输出清洗 func cleanOutputStreaming(_ text: String) -> String { var result = preserveThinkingChannels(in: text) if let tcRange = result.range(of: "") { result = String(result[result.startIndex..", "", ""] for pat in endPatterns { if let range = result.range(of: pat) { result = String(result[result.startIndex...*?", options: .dotMatchesLineSeparators) { result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: "") } result = result.replacingOccurrences( of: "<\\|?[/a-z_]+\\|?>", with: "", options: .regularExpression ) if result.hasPrefix("model\n") { result = String(result.dropFirst(6)) } else if result == "model" { return "" } else if result.hasPrefix("user\n") { result = String(result.dropFirst(5)) } else if result == "user" { return "" } result = String(result.drop(while: { $0.isWhitespace || $0.isNewline })) return normalizeSafetyTruncation(in: result) } func cleanOutput(_ text: String) -> String { var result = preserveThinkingChannels(in: text) if let regex = try? NSRegularExpression(pattern: ".*?", options: .dotMatchesLineSeparators) { result = regex.stringByReplacingMatches(in: result, range: NSRange(result.startIndex..., in: result), withTemplate: "") } if let tcRange = result.range(of: "") { result = String(result[result.startIndex..", "", ""] for pat in endPatterns { if let range = result.range(of: pat) { result = String(result[result.startIndex..", with: "", options: .regularExpression ) if let lastOpen = result.lastIndex(of: "<") { let tail = String(result[lastOpen...]) let tailBody = tail.dropFirst() if !tailBody.isEmpty && tailBody.allSatisfy({ $0.isLetter || $0 == "_" || $0 == "/" || $0 == "|" }) { result = String(result[result.startIndex.. String { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard let warningRange = trimmed.range(of: "> ⚠️ ") else { return trimmed } let body = String(trimmed[.. String { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return trimmed } if let paragraphBreak = trimmed.range(of: "\n\n", options: .backwards) { let tailLength = trimmed.distance(from: paragraphBreak.upperBound, to: trimmed.endIndex) if tailLength > 0 && tailLength <= 280 { return String(trimmed[.. 0 && tailLength <= 220 { return String(trimmed[.. String.Index? { let sentenceEndings: Set = ["。", "!", "?", ".", "!", "?"] var index = text.endIndex while index > text.startIndex { index = text.index(before: index) if sentenceEndings.contains(text[index]) { return text.index(after: index) } } return nil } // MARK: - Thinking 通道保留 private func preserveThinkingChannels(in text: String) -> String { let openTokens = ["<|channel|>thought\n", "<|channel>thought\n"] let closeToken = "" var result = "" var cursor = text.startIndex while cursor < text.endIndex { let nextOpen = openTokens .compactMap { token -> (Range, String)? in guard let range = text.range(of: token, range: cursor..