Files
KellyvvandClaude Opus 4.6 779be78843 refactor: split MLX service, introduce skill type & runtime budgets
Major restructuring of the agent stack to support content-type skills,
data-driven runtime budgets, and the new translate skill.

LLM/MLX/
- Split MLXLocalLLMService.swift (1500 → 700 lines) into Core/, Gemma4/,
  Installation/ subdirectories
- New BundledModel: LinearBudgetFormula replaces tier tables, model
  parameters fully data-driven via ModelRuntimeProfile
- New RuntimeBudgets: pure functions for text/thinking/multimodal budgets,
  zero hardcoded model IDs
- Drop adjustedTextOutputTokens prepared-subtraction (chunked prefill made
  it obsolete and triggered false-positive truncation on long prompts)

Skills/
- Add SkillType (.device / .content) metadata, parsed from SKILL.md
  frontmatter
- New translate skill (type: content, no tools) with anaphora handling
- Trim translate (4563→1804), device (2357→1680), calendar (2169→1790)
- contacts SKILL.md adds multi-turn disambiguation + cancel guidance,
  semantics owned by body not framework
- SkillRegistry validates allowed-tools against ToolRegistry at load time

Agent/
- ToolCallParser becomes registry-aware: drops tool_call names not in
  ToolRegistry or protocol set (load_skill / list_skills); ToolRegistry
  is now the single white list, validation moves to parser boundary
- Router sticky routing uses semantic user-turn boundary instead of
  fixed suffix(4); correctly handles long agent loops
- AgentEngine system prompt template differentiates device vs content
  skills with explicit invocation rules
- SYSPROMPT.md auto-migration: old single-placeholder template backed up
  to .bak when new dual-placeholder is detected
- PromptBuilder.buildLoadedSkillPrompt receives availableTools list,
  explicitly tells model which tools (or none) are callable

Tools/
- ToolRegistry split into Helpers/Stores/AppPermissions/Handlers/* for
  separation of concerns
- Mark Tools/Handlers/* with explicit allowed argument signatures

UI/
- Add long-press copy contextMenu on user and assistant bubbles
- Enable .textSelection on user bubble

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:22:20 +08:00

180 lines
7.1 KiB
Swift

import Foundation
import UIKit
enum DeviceTools {
static func register(into registry: ToolRegistry) {
// ── device-info ──
registry.register(RegisteredTool(
name: "device-info",
description: "使用 iOS 官方公开 API 汇总获取当前设备名称、设备类型、系统版本、内存和处理器数量",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let name = payload["name"] as? String ?? ""
let localizedModel = (payload["localized_model"] as? String)?.isEmpty == false
? (payload["localized_model"] as? String ?? "")
: (payload["model"] as? String ?? "")
let systemName = payload["system_name"] as? String ?? ""
let systemVersion = payload["system_version"] as? String ?? ""
let memoryGB = payload["memory_gb"] as? Double ?? 0
let processorCount = payload["processor_count"] as? Int ?? 0
let summary = [
name.isEmpty ? nil : "设备名称:\(name)",
localizedModel.isEmpty ? nil : "设备类型:\(localizedModel)",
systemVersion.isEmpty ? nil : "系统版本:\(systemName.isEmpty ? "" : systemName + " ")\(systemVersion)",
memoryGB > 0 ? String(format: "物理内存:%.1f GB", memoryGB) : nil,
processorCount > 0 ? "处理器核心数:\(processorCount)" : nil
].compactMap { $0 }.joined(separator: "\n")
var enriched = payload
enriched["result"] = summary
enriched["status"] = "succeeded"
return jsonString(enriched)
})
// ── device-name ──
registry.register(RegisteredTool(
name: "device-name",
description: "使用 UIDevice.current.name 获取当前设备名称",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let name = payload["name"] as? String ?? ""
return successPayload(
result: "这台设备的名称是 \(name)。",
extras: ["name": name]
)
})
// ── device-model ──
registry.register(RegisteredTool(
name: "device-model",
description: "使用 UIDevice.current.model 和 localizedModel 获取当前设备类型",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let model = payload["model"] as? String ?? ""
let localizedModel = payload["localized_model"] as? String ?? ""
return jsonString([
"success": true,
"status": "succeeded",
"result": "这台设备的官方设备类型是 \((localizedModel.isEmpty ? model : localizedModel))。",
"model": model,
"localized_model": localizedModel
])
})
// ── device-system-version ──
registry.register(RegisteredTool(
name: "device-system-version",
description: "使用 UIDevice.current.systemName 和 systemVersion 获取系统版本",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let systemName = payload["system_name"] as? String ?? ""
let systemVersion = payload["system_version"] as? String ?? ""
return jsonString([
"success": true,
"status": "succeeded",
"result": "当前系统版本是 \(systemName) \(systemVersion)。",
"system_name": systemName,
"system_version": systemVersion
])
})
// ── device-memory ──
registry.register(RegisteredTool(
name: "device-memory",
description: "使用 ProcessInfo.processInfo.physicalMemory 获取设备物理内存",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let memoryBytes = payload["memory_bytes"] as? Double ?? 0
let memoryGB = payload["memory_gb"] as? Double ?? 0
return jsonString([
"success": true,
"status": "succeeded",
"result": String(format: "这台设备的物理内存约为 %.1f GB。", memoryGB),
"memory_bytes": memoryBytes,
"memory_gb": memoryGB
])
})
// ── device-processor-count ──
registry.register(RegisteredTool(
name: "device-processor-count",
description: "使用 ProcessInfo.processInfo.processorCount 获取处理器核心数",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let processorCount = payload["processor_count"] as? Int ?? 0
return jsonString([
"success": true,
"status": "succeeded",
"result": "这台设备的处理器核心数是 \(processorCount)。",
"processor_count": processorCount
])
})
// ── device-identifier-for-vendor ──
registry.register(RegisteredTool(
name: "device-identifier-for-vendor",
description: "使用 UIDevice.current.identifierForVendor 获取当前 App 在该设备上的 vendor 标识",
parameters: "无",
isParameterless: true
) { _ in
let payload = await devicePayload()
let identifier = payload["identifier_for_vendor"] as? String ?? ""
return jsonString([
"success": true,
"status": "succeeded",
"result": "当前 App 在这台设备上的 identifierForVendor 是 \(identifier)。",
"identifier_for_vendor": identifier
])
})
}
// MARK: - Private Helpers
private static func devicePayload() async -> [String: Any] {
let info = ProcessInfo.processInfo
let device = await MainActor.run {
(
UIDevice.current.name,
UIDevice.current.model,
UIDevice.current.localizedModel,
UIDevice.current.systemName,
UIDevice.current.systemVersion,
UIDevice.current.identifierForVendor?.uuidString
)
}
var payload: [String: Any] = [
"success": true,
"name": device.0,
"model": device.1,
"localized_model": device.2,
"system_name": device.3,
"system_version": device.4,
"memory_bytes": Double(info.physicalMemory),
"memory_gb": Double(info.physicalMemory) / 1_073_741_824.0,
"processor_count": info.processorCount
]
if let identifierForVendor = device.5, !identifierForVendor.isEmpty {
payload["identifier_for_vendor"] = identifierForVendor
}
return payload
}
}