feat(ai): unify reply center and refresh keyboard AI features

- Merge invitation, task, blessing, clarification, and empathy actions
  into a single Reply flow, with three fixed, clearly labeled stance
  choices whenever user intent must not be guessed.
- Refine clipboard semantic routing with bilingual schedule,
  confirmation, and follow-up models, conservative language thresholds,
  and explicit-assignment guard for complaint-only text.
- Persist Apple account refresh state, harden session recovery, and
  surface durable account diagnostics across keyboard and app.
- Derive personal-style prompts through two-stage corpus evidence and
  apply real low-confidence ASR tendencies instead of neutral templates.
- Localize the new reply center, clipboard semantics, and personal-style
  surfaces in both English and Simplified Chinese.
This commit is contained in:
Rocky
2026-08-29 11:51:21 +08:00
parent f91a8f2456
commit 3c10d73d7f
30 changed files with 2090 additions and 447 deletions
@@ -12,6 +12,18 @@ public struct ClipboardReplyCandidateSnapshot: Codable, Equatable, Identifiable,
case ordinary
case formal
case playful
case invitationAccept
case invitationDecline
case invitationTentative
case taskAcknowledge
case taskClarify
case taskNegotiate
case blessingReturn
case blessingWarm
case blessingPlayful
case clarificationDirect
case clarificationQuestion
case clarificationConfirm
}
public let id: UUID
@@ -121,12 +133,29 @@ public final class ClipboardReplyFeedbackStore {
now: Date = Date()
) -> [PolishStyleReplyLearningExample] {
records(now: now).compactMap { record in
guard record.outcome != .awaitingSelection,
let ordinary = record.candidates.first(where: {
$0.kind == .ordinary
}) else {
guard record.outcome != .awaitingSelection else {
return nil
}
guard let ordinary = record.candidates.first(where: {
$0.kind == .ordinary
}) else {
// Scene-specific choices express the user's decision, not a
// reusable tone preference. Only a later user-authored edit is
// valid personal-style evidence.
guard record.outcome == .selected,
let selected = record.selectedCandidate,
let finalText = record.finalText else {
return nil
}
return PolishStyleReplyLearningExample(
receivedMessage: record.sourceText,
ordinaryCandidate: selected.text,
selection: .contextual,
finalEdit: finalText,
createdAt: record.createdAt,
styleID: record.styleID
)
}
let selection: PolishStyleReplySelection
if record.outcome == .discarded {
selection = .discarded
@@ -279,6 +308,19 @@ public final class ClipboardReplyFeedbackStore {
return .formal
case .playful:
return .playful
case .invitationAccept,
.invitationDecline,
.invitationTentative,
.taskAcknowledge,
.taskClarify,
.taskNegotiate,
.blessingReturn,
.blessingWarm,
.blessingPlayful,
.clarificationDirect,
.clarificationQuestion,
.clarificationConfirm:
return .contextual
}
}