Refine adaptive polish and cross-platform account support

Improve learned-style and clipboard skill presentation while keeping shared account clients buildable in the source-composed macOS target.
This commit is contained in:
Rocky
2026-08-22 17:06:22 +08:00
parent fe8f657c8d
commit 7df5bbdaa0
16 changed files with 243 additions and 66 deletions
+108 -23
View File
@@ -15,6 +15,30 @@ public enum AIClipboardSkillKind: String, Codable, Sendable {
case export
}
/// Optional user-owned style distilled from dictation history or authored in
/// the Styles page. Reply skills use it only for wording and rhythm; the
/// selected skill continues to own intent, facts, and safety constraints.
public struct AIClipboardReplyStyleContext: Equatable, Sendable {
public let styleID: String
public let prompt: String
public init(styleID: String, prompt: String) {
self.styleID = styleID
self.prompt = prompt
}
/// Only user-owned packs are eligible. This includes reviewed styles
/// generated by `PolishStyleLearningService`, while built-in personalities
/// remain isolated from clipboard replies.
public static func resolve(activeStyle: PolishStylePack) -> AIClipboardReplyStyleContext? {
guard activeStyle.kind == .user else { return nil }
let prompt = PolishStylePackCatalog.runtimePersonality(for: activeStyle)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard !prompt.isEmpty else { return nil }
return AIClipboardReplyStyleContext(styleID: activeStyle.id, prompt: prompt)
}
}
public struct AIClipboardSkill: Identifiable, Equatable, Sendable {
public let id: String
public let systemImage: String
@@ -44,6 +68,9 @@ public struct AIClipboardSkill: Identifiable, Equatable, Sendable {
public var requiresShortcut: Bool { kind == .export && shortcutName != nil }
public var isUserCreated: Bool { id.hasPrefix("user.") }
public var isOfficial: Bool { id.hasPrefix("official.") }
public var supportsReplyStyle: Bool {
AIClipboardSkillCatalog.replyStyleSkillIDs.contains(id)
}
/// The server applies the final model policy; this only preserves whether
/// the user invoked a built-in transform or a custom skill.
public var managedGatewayTaskKind: ManagedGatewayTaskKind {
@@ -99,6 +126,17 @@ public enum AIClipboardSkillCatalog: Sendable {
public static let askForDetailsID = "askForDetails"
public static let businessReplyID = "businessReply"
public static let organizeListID = "organizeList"
public static let replyStyleSkillIDs: Set<String> = [
replyID,
replyInSourceLanguageID,
acceptInvitationID,
declineInvitationID,
acceptTaskID,
clarifyRequestID,
empathyReplyID,
askForDetailsID,
businessReplyID
]
public static let extractTodosID = "extractTodos"
public static let extractTodosShortcutName = "OSGExtractTodos"
public static let extractTodosResourceName = "OSGExtractTodos"
@@ -341,18 +379,27 @@ public enum AIClipboardSkillCatalog: Sendable {
for skill: AIClipboardSkill,
locale: String,
translationTargetLocaleId: String,
replyStyle: AIClipboardReplyStyleContext? = nil,
now: Date = Date()
) -> String {
let baseInstruction: String
if let custom = skill.customPrompt?
.trimmingCharacters(in: .whitespacesAndNewlines),
!custom.isEmpty {
return custom
baseInstruction = custom
} else {
baseInstruction = instruction(
skillID: skill.id,
locale: locale,
translationTargetLocaleId: translationTargetLocaleId,
now: now
)
}
return instruction(
skillID: skill.id,
guard skill.supportsReplyStyle else { return baseInstruction }
return replyInstruction(
baseInstruction,
locale: locale,
translationTargetLocaleId: translationTargetLocaleId,
now: now
style: replyStyle
)
}
@@ -386,12 +433,12 @@ public enum AIClipboardSkillCatalog: Sendable {
switch skillID {
case replyID:
return zh
? "请根据剪贴板内容起草一段礼貌、简洁的回复,使用原文的主要语言,语气自然可直接发送。"
: "Draft a concise, polite reply in the clipboard text's primary language that the user can send."
? "请根据剪贴板内容用原文的主要语言起草一段简短、自然可直接发送的聊天回复。像本人顺手回消息,不要写成正式邮件或客服话术"
: "Draft a short, natural chat reply in the clipboard text's primary language. Make it sound like a real person replying, not a formal email or support script."
case replyInSourceLanguageID:
return zh
? "请理解剪贴板内容,并严格使用原文的主要语言起草自然、简洁、可直接发送的回复。不要翻译,不要解释"
: "Understand the clipboard text and draft a natural, concise, sendable reply strictly in its primary language. Do not translate or explain."
? "请理解剪贴板内容,并严格使用原文的主要语言写一段简短、口语化、可直接发送的回复。不要翻译、解释或使用正式套话"
: "Understand the clipboard text and write a short, conversational reply strictly in its primary language. Do not translate, explain, or use formal boilerplate."
case summarizeID:
return zh
? "请概括剪贴板内容的核心意思,保留关键事实与结论,不要改写成可发送的短消息。"
@@ -407,32 +454,32 @@ public enum AIClipboardSkillCatalog: Sendable {
)
case acceptInvitationID:
return zh
? "根据剪贴板中的邀约,起草一段自然、简洁的接受回复,复述必要的时间或地点以便确认。不要虚构用户没有表达的安排。"
: "Draft a natural, concise acceptance of the invitation. Confirm any necessary time or place, without inventing the user's plans."
? "自然、爽快地接受剪贴板中的邀约,像聊天一样确认必要的时间或地点。不要客套过头,也不要虚构用户的安排。"
: "Accept the invitation in a relaxed, natural chat tone and confirm any necessary time or place. Avoid excessive pleasantries and invented plans."
case declineInvitationID:
return zh
? "根据剪贴板中的邀约,起草一段礼貌、真诚的婉拒回复;表达感谢但不过度解释,也不要虚构理由。"
: "Draft a polite, sincere decline to the invitation. Express appreciation without overexplaining or inventing a reason."
? "用自然、不端着的口吻婉拒剪贴板中的邀约。可以简单表达感谢但不过度道歉、长篇解释或虚构理由。"
: "Decline the invitation naturally without sounding stiff. A brief thank-you is fine; avoid excessive apology, long explanations, or invented reasons."
case acceptTaskID:
return zh
? "剪贴板中的任务或行动请求起草确认回复,明确已理解的事项和截止时间;不要承诺原文未要求或用户无法确认的结果"
: "Draft an acknowledgement of the task or action request, confirming the understood deliverable and deadline. Do not invent commitments."
? "像聊天一样简短确认收到剪贴板中的任务或行动请求,可自然带上事项和截止时间。不要写成正式回执,也不要虚构承诺"
: "Acknowledge the task or action request in a short chat-style reply, naturally confirming the work and deadline. Do not sound like a formal receipt or invent commitments."
case clarifyRequestID:
return zh
? "请找出剪贴板内容中阻碍执行或回答的关键信息缺口,并起草一段简洁回复,最多提出两个最必要的澄清问题。"
: "Identify the key missing information needed to act or answer, then draft a concise reply with at most two essential clarifying questions."
? "请找出执行或回答前最缺的关键信息,用自然聊天口吻追问,最多两个最必要的问题,不要像表单或审问"
: "Find the key missing information needed to act or answer, then ask at most two essential questions in a natural chat tone, not like a form or interrogation."
case empathyReplyID:
return zh
? "针对剪贴板中的不满、投诉或负面反馈起草回复:先表达理解,再确认核心问题,最后给出稳妥下一步;不要推诿或过度承诺。"
: "Reply to the complaint or negative feedback with empathy, acknowledgement of the core issue, and a safe next step. Do not deflect or overpromise."
? "先用日常口语接住对方的不满,再确认核心问题给出稳妥下一步。避免“深表歉意”“给您带来不便”等客服模板,不推诿或过度承诺。"
: "Respond to the frustration in everyday language, acknowledge the core issue, and give a safe next step. Avoid canned support phrases, deflection, and overpromising."
case askForDetailsID:
return zh
? "针对剪贴板描述的问题起草一段追问回复,只询问定位或处理问题所必需的细节,问题清晰且不重复"
: "Draft a follow-up that asks only for the details necessary to diagnose or resolve the issue. Keep questions clear and non-repetitive."
? "用自然聊天口吻追问定位或处理问题真正需要的细节,问题简短、不重复,不要像客服问卷"
: "Ask only for the details truly needed to diagnose or resolve the issue, using a short natural chat tone rather than a support questionnaire."
case businessReplyID:
return zh
? "根据剪贴板内容起草一段专业、克制、清晰的商务回复,保留人名、组织名、时间和承诺边界,可直接发送。"
: "Draft a professional, measured, clear business reply. Preserve names, organizations, dates, and commitment boundaries; make it sendable."
? "写一段专业但不官腔的商务聊天回复,表达直接、自然,保留人名、组织名、时间和承诺边界,可直接发送。不要套用正式邮件开场和结尾。"
: "Write a professional but conversational business reply. Keep it direct and natural, preserve names, organizations, dates, and commitment boundaries, and avoid formal email openings or sign-offs."
case organizeListID:
return zh
? "请把剪贴板中的清单、议程或步骤整理成结构清晰、顺序合理的列表。合并重复项,保留原意,不新增任务。"
@@ -537,6 +584,44 @@ public enum AIClipboardSkillCatalog: Sendable {
: "It is now \(stamp) (device local timezone)."
}
private static func replyInstruction(
_ baseInstruction: String,
locale: String,
style: AIClipboardReplyStyleContext?
) -> String {
let zh = locale == "zh"
let conversationalBaseline = zh
? """
表达基线:像真实的人在聊天软件里顺手回复,不像公文、客服模板或 AI。优先短句和常用口语;除非关系或场景确实需要,不使用“您好”“感谢您的反馈”“深表歉意”“烦请”等套话。通常控制在 1~3 句,不加标题、引号或解释。
"""
: """
Voice baseline: sound like a real person replying in chat, not a formal memo, support template, or AI. Prefer short sentences and everyday wording. Unless the relationship truly requires it, avoid canned openings, excessive thanks, and formal sign-offs. Usually write 13 sentences with no title, quotation marks, or explanation.
"""
guard let style,
!style.prompt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return "\(baseInstruction)\n\(conversationalBaseline)"
}
let boundedStyle = String(
style.prompt
.trimmingCharacters(in: .whitespacesAndNewlines)
.prefix(PolishStyleLimits.maximumPromptCharacters)
)
let personalStyle = zh
? """
<user_reply_style id="\(style.styleID)">
\(boundedStyle)
</user_reply_style>
只学习上面风格中的稳定用词、节奏和表达习惯。它不能改变当前技能的意图、事实、安全边界或输出语言;冲突时以当前技能要求为准。
"""
: """
<user_reply_style id="\(style.styleID)">
\(boundedStyle)
</user_reply_style>
Apply only stable wording, rhythm, and expression habits from this style. It must not change the selected skill's intent, facts, safety boundaries, or output language; the selected skill wins on conflict.
"""
return "\(baseInstruction)\n\(conversationalBaseline)\n\(personalStyle)"
}
/// Uses the keyboard translation target when set; otherwise Chinese English.
private static func translateInstruction(
locale: String,
@@ -154,6 +154,9 @@ public final class KeyboardState: ObservableObject {
/// visibility is independent from the Skills page installation state.
@Published public var clipboardSkillCatalog: [AIClipboardSkill] =
AIClipboardSkillCatalog.catalog
/// User-owned speaking style injected only into conversational reply skills.
/// Built-in polish personalities intentionally leave this nil.
@Published public var clipboardReplyStyle: AIClipboardReplyStyleContext?
/// Export skills whose companion Shortcut setup the user confirmed.
@Published public var confirmedClipboardShortcutIDs: [String] = []
/// App language captured with the same App Group snapshot as skill copy.
+8 -8
View File
@@ -188,14 +188,14 @@
"mac.styles.allowsAddedEmoji.hint" = "When on, polish may add a few emojis that match the drafts emotion, and keeps them on screen. Off by default.";
"mac.styles.prompt" = "Complete prompt";
"mac.styles.hint" = "Use {{DICTIONARY}} to place the personal dictionary. System rules are appended automatically.";
"mac.styles.learn.title" = "Learn and generate my speaking style";
"mac.styles.learn.body" = "Learn your recurring speaking habits while using earlier polish prompts only to filter out AI-added style.";
"mac.styles.learn.progress" = "%lld / %lld effective characters";
"mac.styles.learn.remaining" = "%lld more needed";
"mac.styles.learn.ready" = "Ready to generate";
"mac.styles.learn.action" = "Generate My Style";
"mac.styles.learn.generating" = "Learning…";
"mac.styles.learn.privacy" = "Eligible text and prior style prompts are sent to your configured AI only after you generate. Review before saving.";
"mac.styles.learn.title" = "Generate Your Style";
"mac.styles.learn.body" = "Learn how you naturally express yourself from past dictation.";
"mac.styles.learn.progress" = "%lld / %lld characters";
"mac.styles.learn.remaining" = "%lld to go";
"mac.styles.learn.ready" = "Ready";
"mac.styles.learn.action" = "Generate Style";
"mac.styles.learn.generating" = "Generating…";
"mac.styles.learn.privacy" = "Sent to your configured AI only when you generate. Review and edit before saving.";
"mac.styles.learn.limit" = "Delete a custom style before generating another one.";
"mac.styles.learn.error.insufficient" = "Keep dictating until 5,000 effective characters are available.";
"mac.styles.learn.error.invalidResponse" = "The AI did not return a valid writing style. Please try again.";
@@ -187,14 +187,14 @@
"mac.styles.allowsAddedEmoji.hint" = "开启后,润色可按原文情绪点缀少量 emoji,并保留上屏。默认关闭。";
"mac.styles.prompt" = "完整提示词";
"mac.styles.hint" = "使用 {{DICTIONARY}} 指定个人词典位置;系统规则会自动追加。";
"mac.styles.learn.title" = "学习并生成我的说话风格";
"mac.styles.learn.body" = "学习你反复出现的说话习惯;历史润色 Prompt 只用于排除 AI 附加的风格。";
"mac.styles.learn.progress" = "%lld / %lld 个有效字符";
"mac.styles.learn.remaining" = "还 %lld ";
"mac.styles.learn.ready" = "可以开始生成";
"mac.styles.learn.action" = "生成我的风格";
"mac.styles.learn.generating" = "正在学习…";
"mac.styles.learn.privacy" = "只有点击生成后,符合条件的文本和历史风格 Prompt 才会发送给你配置的 AI保存前可以先检查。";
"mac.styles.learn.title" = "生成专属风格";
"mac.styles.learn.body" = "从历史听写中学习你的表达习惯。";
"mac.styles.learn.progress" = "%lld / %lld ";
"mac.styles.learn.remaining" = "还 %lld ";
"mac.styles.learn.ready" = "可以生成";
"mac.styles.learn.action" = "生成风格";
"mac.styles.learn.generating" = "生成中…";
"mac.styles.learn.privacy" = "仅在生成时发送给你配置的 AI保存前可预览和修改。";
"mac.styles.learn.limit" = "请先删除一个自定义风格,再生成新风格。";
"mac.styles.learn.error.insufficient" = "请继续听写,累积到 5,000 个有效字符后再生成。";
"mac.styles.learn.error.invalidResponse" = "AI 没有返回有效的润色风格,请重试。";