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:
@@ -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 1–3 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.
|
||||
|
||||
Reference in New Issue
Block a user