mirror of
https://github.com/carey314/mio-plugin-clipper.git
synced 2026-08-09 23:34:32 +00:00
Quick AI actions on clipboard or selected text. Four actions: - 翻译 (中/英/日) - 总结 (3-bullet Chinese) - 改语气 (正式/口语/友好/严谨) - 邮件回复 Powered by DeepSeek with Server-Sent Events streaming for typewriter feedback (~600ms first byte). Two source modes: clipboard (no permission) and selected-text via NSAccessibility (graceful fallback to clipboard + permission banner if denied). Five-state UI machine: idle → input → loading (cancellable) → result → missing-config. Last 20 actions persisted to UserDefaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.6 KiB
Swift
76 lines
2.6 KiB
Swift
//
|
|
// ClipperConfig.swift
|
|
// 划词侠 plugin
|
|
//
|
|
// Loads ~/.config/codeisland/clipper-config.json on demand.
|
|
// Schema:
|
|
// { "deepseek_api_key": "sk-...",
|
|
// "default_translate_target": "zh" }
|
|
//
|
|
// Never bundles a key. If the file is missing/empty we surface a
|
|
// state-E "请先填" screen with a helper that copies the path to the
|
|
// clipboard so the user can `open -e` it.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct ClipperConfig {
|
|
var deepseekApiKey: String
|
|
var defaultTranslateTarget: String // "zh" | "en" | "ja"
|
|
|
|
static let configPath: String = {
|
|
let home = NSHomeDirectory()
|
|
return "\(home)/.config/codeisland/clipper-config.json"
|
|
}()
|
|
|
|
static var configDir: String {
|
|
let home = NSHomeDirectory()
|
|
return "\(home)/.config/codeisland"
|
|
}
|
|
|
|
/// Best-effort load. If anything is missing we return nil so the UI
|
|
/// can render the onboarding banner — never throws to the caller.
|
|
static func load() -> ClipperConfig? {
|
|
let url = URL(fileURLWithPath: configPath)
|
|
guard let data = try? Data(contentsOf: url) else {
|
|
ClipperDebugLog.write("config: file missing at \(configPath)")
|
|
return nil
|
|
}
|
|
guard let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
ClipperDebugLog.write("config: malformed JSON")
|
|
return nil
|
|
}
|
|
let key = (obj["deepseek_api_key"] as? String) ?? ""
|
|
let target = (obj["default_translate_target"] as? String) ?? "zh"
|
|
if key.trimmingCharacters(in: .whitespaces).isEmpty
|
|
|| key.trimmingCharacters(in: .whitespaces) == "TODO" {
|
|
ClipperDebugLog.write("config: deepseek_api_key empty/TODO")
|
|
return nil
|
|
}
|
|
return ClipperConfig(deepseekApiKey: key, defaultTranslateTarget: target)
|
|
}
|
|
|
|
/// Write a stub file the user can edit. Idempotent — won't clobber
|
|
/// an existing real config.
|
|
@discardableResult
|
|
static func ensureStubExists() -> Bool {
|
|
let fm = FileManager.default
|
|
if fm.fileExists(atPath: configPath) { return false }
|
|
try? fm.createDirectory(atPath: configDir, withIntermediateDirectories: true)
|
|
let stub = """
|
|
{
|
|
"deepseek_api_key": "TODO",
|
|
"default_translate_target": "zh"
|
|
}
|
|
"""
|
|
do {
|
|
try stub.write(toFile: configPath, atomically: true, encoding: .utf8)
|
|
ClipperDebugLog.write("config: wrote stub at \(configPath)")
|
|
return true
|
|
} catch {
|
|
ClipperDebugLog.write("config: stub write failed: \(error)")
|
|
return false
|
|
}
|
|
}
|
|
}
|