b5ab92a57e
来自 xhigh code review 的正确性/健壮性修复,覆盖全部五端: - server:鉴权 fail-closed、计量交叉校验与配额扣穿处理、WS 网关并发与关闭顺序、 billing 行锁、redis Lua 过期与设备槽刷新、config 解析 - desktop:会话 epoch 防串话、WS 重连与 401 处理、api 客户端复用、统一 usePoll 轮询 - android:握手时序、请求头封装、账户状态派生、按需重组 - ios:finalize 宽限、串行采集、错误文案服务端优先、删除死代码 CommitController Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.8 KiB
Swift
60 lines
2.8 KiB
Swift
// 14D deep link 听写的跨进程交接(App Group UserDefaults):
|
||
// 主 App 听写完成 → write(text);用户手动切回原 App 调起 dudu 键盘
|
||
// → consume() 取走并 insertText 自动上屏。
|
||
// 21G:App Group pending_text 是主通道(稳定、不污染用户剪贴板),write 回报是否
|
||
// 成功写入真正的 App Group;剪贴板降级为 App Group 不可用时的兜底(见 DictationController)。
|
||
//
|
||
// 为什么是"用户手动切回":iOS 不提供"跳回上一个 App"的公开 API,
|
||
// 键盘扩展经 extensionContext.open 调起主 App 时也无法携带可用于自动跳回的
|
||
// return 上下文 —— 平台限制,只能由用户自己切回,键盘被动消费。
|
||
// 120s 过期:防止旧听写文本在无关输入场景误上屏(过期后用户仍可手动粘贴剪贴板)。
|
||
import Foundation
|
||
|
||
public enum PendingTextStore {
|
||
private enum Key {
|
||
static let text = "dictation.pending_text"
|
||
static let ts = "dictation.pending_ts" // 写入时刻(秒级 Unix 时间戳)
|
||
}
|
||
|
||
/// 待上屏文本有效期(秒)
|
||
public static let maxAgeSeconds: TimeInterval = 120
|
||
|
||
/// App Group 是否可用:无签名本地调试场景拿不到 suite,键盘扩展(另一进程)读不到,
|
||
/// 此时主通道失效,需剪贴板兜底。
|
||
private static var appGroupDefaults: UserDefaults? {
|
||
UserDefaults(suiteName: AccountStore.appGroupID)
|
||
}
|
||
|
||
private static var defaults: UserDefaults {
|
||
appGroupDefaults ?? .standard
|
||
}
|
||
|
||
/// 主 App 听写完成时写入(空文本不写)。
|
||
/// 返回是否成功写入 App Group(主通道);false 表示 App Group 不可用,
|
||
/// 调用方应改用剪贴板兜底(21G:避免无条件污染用户剪贴板)。
|
||
@discardableResult
|
||
public static func write(_ text: String) -> Bool {
|
||
guard !text.isEmpty else { return false }
|
||
guard let group = appGroupDefaults else { return false } // App Group 不可用 → 主通道失效
|
||
group.set(text, forKey: Key.text)
|
||
group.set(Date().timeIntervalSince1970, forKey: Key.ts)
|
||
return true
|
||
}
|
||
|
||
/// 键盘扩展消费:取走并立即清除(只上屏一次)。
|
||
/// 返回文本与"写入 → 消费"的延迟(秒);过期 / 不存在返回 nil。
|
||
public static func consume() -> (text: String, delaySeconds: TimeInterval)? {
|
||
guard let text = defaults.string(forKey: Key.text), !text.isEmpty else { return nil }
|
||
let ts = defaults.double(forKey: Key.ts)
|
||
clear()
|
||
let age = Date().timeIntervalSince1970 - ts
|
||
guard age >= 0, age < maxAgeSeconds else { return nil }
|
||
return (text, age)
|
||
}
|
||
|
||
public static func clear() {
|
||
defaults.removeObject(forKey: Key.text)
|
||
defaults.removeObject(forKey: Key.ts)
|
||
}
|
||
}
|