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>
88 lines
3.8 KiB
Swift
88 lines
3.8 KiB
Swift
import EventKit
|
|
import Foundation
|
|
|
|
enum CalendarTools {
|
|
|
|
static func register(into registry: ToolRegistry) {
|
|
|
|
// ── calendar-create-event ──
|
|
registry.register(RegisteredTool(
|
|
name: "calendar-create-event",
|
|
description: "创建新的日历事项,可写入标题、开始时间、结束时间、地点和备注",
|
|
parameters: "title: 事件标题, start: ISO 8601 开始时间, end: ISO 8601 结束时间(可选), location: 地点(可选), notes: 备注(可选)",
|
|
// 只有 start 是硬参 (EventKit API 强制要求)。
|
|
// title 是软参,缺失时 handler 用默认值,不强制 caller 提供。
|
|
requiredParameters: ["start"]
|
|
) { args in
|
|
// title 是软参: 没传或为空时使用默认标题, 不阻断流程
|
|
let rawTitle = (args["title"] as? String)?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
let title = rawTitle.isEmpty ? "新日历事项" : rawTitle
|
|
|
|
guard let startRaw = args["start"] as? String,
|
|
let startDate = parseISO8601Date(startRaw) else {
|
|
return failurePayload(error: "缺少有效的 start 参数,必须是 ISO 8601 时间字符串")
|
|
}
|
|
|
|
let endRaw = (args["end"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let endDate = endRaw.flatMap(parseISO8601Date) ?? startDate.addingTimeInterval(3600)
|
|
guard endDate >= startDate else {
|
|
return failurePayload(error: "end 不能早于 start")
|
|
}
|
|
|
|
let location = (args["location"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let notes = (args["notes"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
do {
|
|
guard try await ToolRegistry.shared.requestAccess(for: .calendar) else {
|
|
return failurePayload(error: "未获得日历写入权限")
|
|
}
|
|
|
|
guard let calendar = writableEventCalendar() else {
|
|
return failurePayload(error: "没有可用于新建事项的可写日历,请先在系统日历中启用或创建一个日历")
|
|
}
|
|
|
|
let event = EKEvent(eventStore: SystemStores.event)
|
|
event.calendar = calendar
|
|
event.title = title
|
|
event.startDate = startDate
|
|
event.endDate = endDate
|
|
if let location, !location.isEmpty {
|
|
event.location = location
|
|
}
|
|
if let notes, !notes.isEmpty {
|
|
event.notes = notes
|
|
}
|
|
|
|
try SystemStores.event.save(event, span: .thisEvent, commit: true)
|
|
|
|
return successPayload(
|
|
result: "已创建日历事项\u{201C}\(title)\u{201D},开始时间为 \(iso8601String(from: startDate))。",
|
|
extras: [
|
|
"eventId": event.eventIdentifier ?? "",
|
|
"title": title,
|
|
"start": iso8601String(from: startDate),
|
|
"end": iso8601String(from: endDate),
|
|
"location": location ?? "",
|
|
"notes": notes ?? ""
|
|
]
|
|
)
|
|
} catch {
|
|
return failurePayload(error: "创建日历事项失败:\(error.localizedDescription)")
|
|
}
|
|
})
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private static func writableEventCalendar() -> EKCalendar? {
|
|
if let calendar = SystemStores.event.defaultCalendarForNewEvents,
|
|
calendar.allowsContentModifications {
|
|
return calendar
|
|
}
|
|
|
|
return SystemStores.event.calendars(for: .event)
|
|
.first(where: \.allowsContentModifications)
|
|
}
|
|
}
|