chore(release): bump version to 1.0.1 (build 27)

Release the current PiP reliability, polish style, macOS dictionary, and UI updates.
This commit is contained in:
Rocky
2026-07-27 00:09:30 +08:00
parent 5b1283c3ed
commit 956331a2af
41 changed files with 1241 additions and 295 deletions
@@ -10,6 +10,8 @@ public enum EngineServiceLabel {
engineMode: String,
providerId: String,
model: String,
asrProviderId: String? = nil,
asrModel: String? = nil,
language: AppUILanguage? = nil
) -> String {
let lang = language ?? AppGroupStore().uiLanguage
@@ -17,8 +19,17 @@ public enum EngineServiceLabel {
let asrName = SharedL10n.string("engine.asr.appleSpeech", language: lang)
return SharedL10n.format("engine.summary.local", language: lang, asrName)
}
let providerName = ProviderDisplayName.name(for: providerId, language: lang)
let trimmedModel = model.trimmingCharacters(in: .whitespacesAndNewlines)
// Cloud status line should name the speech engine, not the polish LLM.
let resolvedASRProvider: String = {
if let asrProviderId, !asrProviderId.isEmpty { return asrProviderId }
return providerId
}()
let resolvedASRModel: String = {
if let asrModel, !asrModel.isEmpty { return asrModel }
return model
}()
let providerName = ProviderDisplayName.name(for: resolvedASRProvider, language: lang)
let trimmedModel = resolvedASRModel.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedModel.isEmpty {
return SharedL10n.format("engine.summary.cloud", language: lang, providerName)
}
@@ -26,7 +26,9 @@ public enum FlowColdStartOverlayDecision: Equatable, Sendable {
public enum FlowHandoffPolicy {
/// Proactive keyboard auto-launch of the host is intentionally disabled.
/// Opening the host must be driven by an explicit mic press (or Live Activity).
/// Opening the host must be driven by an explicit mic press (or a Live
/// Activity tap when that keep-alive mode is selected). PiP sessions
/// never auto-jump once `hostReady` is published.
public static let allowsProactiveHostAutoLaunch = false
/// Samples of "host truly dead" required before a cold-start jump is allowed
@@ -6,15 +6,15 @@
import Foundation
public enum FlowKeepAliveMode: String, CaseIterable, Identifiable, Sendable, Codable {
/// Continuous audio capture + Live Activity (current default behaviour).
/// Continuous audio capture + Live Activity.
case liveActivity = "liveActivity"
/// Picture-in-picture waveform keep-alive; mic released between utterances.
case pictureInPicture = "pictureInPicture"
public var id: String { rawValue }
/// Existing installs keep the Live Activity / continuous-capture path.
public static let `default`: FlowKeepAliveMode = .liveActivity
/// Used when no valid keep-alive preference has been stored.
public static let `default`: FlowKeepAliveMode = .pictureInPicture
public var labelKey: String {
switch self {
+40 -20
View File
@@ -57,26 +57,46 @@ public enum PolishIntensity: String, Codable, Sendable, CaseIterable {
/// but must not override the style pack's length and format rules.
public func promptGuideline(styleID: String?) -> String {
let base: String
switch self {
case .light:
base = """
Light rewrite: remove isolated filler words (嗯, 呃, 那个, 就是, 然后, 对, ok, um, uh) and obvious duplicated fragments only. \
Do not rephrase otherwise-clear wording. \
Still restore punctuation and sentence breaks per the global output contract and active style pack.
"""
case .medium:
base = """
Medium rewrite: fix obvious ASR errors (homophones, missing/extra characters), remove fillers and duplicated fragments, \
adjust obviously-broken word order. Preserve the speaker's voice. \
Still restore punctuation and breaks per the global output contract and active style pack. \
Do not invent facts or change numbers/proper nouns.
"""
case .heavy:
base = """
Heavy rewrite: apply medium corrections, then you may reorganize paragraphs, split long sentences, and listify enumerated content when the active style pack allows it. \
Punctuation is mandatory at every intensity. \
Preserve every fact, number, and proper noun. Do not add information.
"""
if styleID == "builtin.dating" {
switch self {
case .light:
base = """
Dating Light: apply ASR cleanup, then soften interrogation, lecturing, commands, dismissal, blame, or pressure even when the original wording is otherwise clear. \
Make the message warm and respectful without adding flirtation, teasing, romantic intent, or relationship escalation. Preserve the speaker's recognizable voice.
"""
case .medium:
base = """
Dating Medium: apply Light corrections, then add at most one grounded observation, affiliative joke, natural self-disclosure, follow-up question, or restrained signal of interest when supported by the transcript or preceding context. \
Keep any flirtation light, interpretable at face value, and easy to decline. Do not invent relationship context.
"""
case .heavy:
base = """
Dating Heavy: apply Medium corrections and, only when the transcript already expresses romantic interest or invitation and the context shows no rejection, discomfort, vulnerability, or power imbalance, make appreciation, longing, expectation, or invitation more proactive and clear. \
Increase romantic tension and directness, not ambiguity, pressure, sexual escalation, or message length. Preserve the speaker's recognizable voice and every fact.
"""
}
} else {
switch self {
case .light:
base = """
Light rewrite: remove isolated filler words (嗯, 呃, 那个, 就是, 然后, 对, ok, um, uh) and obvious duplicated fragments only. \
Do not rephrase otherwise-clear wording. \
Still restore punctuation and sentence breaks per the global output contract and active style pack.
"""
case .medium:
base = """
Medium rewrite: fix obvious ASR errors (homophones, missing/extra characters), remove fillers and duplicated fragments, \
adjust obviously-broken word order. Preserve the speaker's voice. \
Still restore punctuation and breaks per the global output contract and active style pack. \
Do not invent facts or change numbers/proper nouns.
"""
case .heavy:
base = """
Heavy rewrite: apply medium corrections, then you may reorganize paragraphs, split long sentences, and listify enumerated content when the active style pack allows it. \
Punctuation is mandatory at every intensity. \
Preserve every fact, number, and proper noun. Do not add information.
"""
}
}
guard self == .heavy,
+55 -21
View File
@@ -292,46 +292,80 @@ public enum PolishStylePackCatalog {
name: "直男癌拯救器",
prompt: """
# 角色
你是成熟、风趣、高情商的恋爱沟通教练。把生硬、无聊、像审问、过度自我中心或带有压力的聊天,改成自然、有温度、有分寸、让对方容易回应的表达。吸引力来自真诚、松弛和关注,不来自套路或操控。
你是成熟、风趣、高情商的恋爱沟通编辑。把生硬、敷衍、像审问、只讲道理、过度自我中心或带有压力的聊天,改成自然、有温度、有分寸、让对方容易回应的表达。吸引力来自让人感到被理解、被认可、被在乎,不来自套路或操控。
\(dictionaryPlaceholder)
\(sharedASRRules)
# 核心目标
保留用户真实目的和个人语气,同时优化情绪体验:让关心不像盘问,让邀请不带压力,让赞美具体自然,让道歉承担责任,让暧昧保持轻盈
输出长度应贴近原句(± 20% 以内);提升吸引力 ≠ 扩写成长段或连续追问。
# 决策优先级
清晰与尊重 > 对方感受与边界 > 用户真实意图 > 当前关系信号 > 本次改写力度 > 趣味和暧昧。只放大原文已有的沟通意图,不凭空创造好感、承诺、共同经历、对方反应或关系状态
# 沟通策略
1. **日常开启话题**:避免连续封闭式提问;优先使用轻松观察、自然分享或容易接住的开放表达
2. **表达关心**:关注对方感受,不居高临下地指导,不把关心写成查岗
3. **发出邀请**:明确但松弛,给对方真实选择空间;不使用道德绑架或制造亏欠
4. **表达好感**:原文已有好感时,可加入克制的俏皮、反差或轻微暧昧;原文没有暧昧意图时,不擅自升级关系
5. **赞美**:基于原文已有细节,赞美气质、选择、能力或带来的感受;避免只评价身体和外貌。
6. **道歉或冲突**:承认具体影响,表达真实态度;不狡辩,不用玩笑逃避责任。
7. **对方冷淡、拒绝或不适**:降低强度,礼貌收束,不追问、不纠缠。
# 本风格的力度解释
本节定义恋爱聊天中的 light、medium、heavy;它优先于通用力度中「清楚措辞不改写」或「重组段落」等说明,但不得覆盖全局输出契约
- **Light(暖而不撩)**:允许为消除盘问、说教、命令、敷衍、推责或压迫感而改写清楚的措辞。先接住感受,保留用户口吻;不主动新增暧昧、调侃或关系推进
- **Medium(温度与趣味)**:在 Light 基础上,可加入一处具体观察、亲和幽默、自然分享、跟进问题或克制的偏好信号,让人更想回应。暧昧必须轻、可退、可按字面理解
- **Heavy(主动而明确)**原文已有好感或邀约意图,且语境没有拒绝、冷淡、权力不对等等风险时,可更主动地表达欣赏、想念、期待或约会意图。浪漫张力可以更强,但直接度应上升、猜谜应减少;仍不得擅自表白、许诺或升级身体和性边界
# 风格校准
# 关系许可闸
- 没有恋爱信号:即使 Heavy 也只增强温度、趣味和表达力,不主动制造暧昧。
- 原文已有单向好感:可以表达己方感受或邀请,但必须保留真实拒绝空间。
- 上文显示双方已有稳定玩笑、追问或暧昧:可按力度增强俏皮、画面感和期待。
- 对方短答、回避、改话题、拒绝、不适,或原文在催回复、讨价还价:任何力度都立即降为礼貌、直接、低压力的表达,不把冷淡解释成欲擒故纵。
- 涉及上下级、师生、医疗照护等权力不对等,或酒精、疾病、悲伤等脆弱状态:锁定 Light,不推进关系。
# 改写方法
先识别原文是在开启话题、回应情绪、关心、赞美、邀约、想念、道歉、冲突、确认关系还是接受拒绝,再做最少但最有效的改动:
1. **评判改为回应**:先体现听懂对方的事实或感受,再分享看法;对方未求建议时,不用「你应该」「早听我的」开头。
2. **索取改为表达**:把「在吗」「想我没」「怎么不回」改成带有自身信息、感受或来意的表达,不让对方独自承担开启和维持对话。
3. **控制改为选择**:关心、建议和邀约要明确但不命令;不替对方决定,不先斩后奏,不用请客、付出或失望换取答应。
4. **空话改为具体**:只根据原文或上文已有细节赞美选择、能力、气质、努力或给人的感受;没有细节时宁可朴素,不硬编赞美。
5. **提问体现倾听**:优先追问对方刚说过的细节、感受或意义;一条短消息最多一个主要问题,禁止查户口式连问。
6. **幽默保持亲和**:可用轻自嘲、共同笑点、反差或双关;不拿对方的外貌、能力、身份、性史、家庭或创伤开玩笑。
7. **自我披露保持对等**:只分享与当前话题和关系深度相称的一小步,不倾倒创伤,不抢走对话中心。
8. **道歉承担责任**:说清具体行为与影响,不用「但」「如果你觉得」「不是故意的」撤回责任,不索取立即原谅。
9. **冲突描述具体**:用具体事件、感受、需要和请求替代「你总是」「你从来不」;需要暂停时说明原因和返回时间。
10. **拒绝干净收束**:接受「不」「算了」「只是朋友」及同义表达,不追问、不谈判、不贬低、不换平台纠缠。
# 长度与风格
- 保留用户可辨认的词汇、直接程度和个性;这是润色,不是替用户扮演另一个人格。
- 15 个字以内的短句可增加一个短分句以补足来意、温度或退路;Medium 最多一个互动钩子;Heavy 最多两个自然语言节拍。
- 较长消息贴近原有信息量,不扩写成长段、情书、鸡汤或连续追问;普通聊天不分标题、不列清单。
- 自信但不自恋,主动但不强迫,幽默但不冒犯,暧昧但不露骨。
- 使用自然口语和适度留白,不堆形容词,不写成情书、鸡汤或网络土味情话
- 可以改善语气和提问方式,但不编造共同经历、对方反应、关系状态、邀约安排或用户没有表达过的感情。
- 使用当代自然口语,不堆形容词、排比、网络土味情话或故作深情的比喻
- 不凭空使用「宝贝」「美女」「乖」等亲昵称呼,不主动新增 emoji。
# 安全边界
禁止 PUA、操控、试探服从、贬低、嫉妒诱导、施压、骚扰、物化、露骨性暗示,以及利用年龄、权力、酒精或脆弱状态推进关系。任何吸引力规则都不得凌驾于尊重和同意之上
禁止 PUA、忽冷忽热、故意延迟回复、贬低后安抚、卖惨绑架、竞争或嫉妒诱导、试探服从、未经同意定义关系、物化、露骨性暗示、骚扰,以及利用年龄、权力、酒精或脆弱状态推进关系。暧昧不能代替明确同意;涉及身体、性、关系确认或边界时,必须使用清楚、可拒绝的表达
# 示例
# 示例(按本次力度只采用对应方向)
原:你今天干嘛怎么这么久不回我
:今天是不是有点忙?你先忙,有空了再和我说
Light:今天是不是有点忙?你先忙,有空再聊
Medium:刚想找你说说话,猜你今天可能有点忙。有空了再来找我。
Heavy:最近感觉我们联系少了些。你方便时,我想和你聊聊彼此更舒服的联系节奏。
原:周六有时间吗我想约你吃饭
:周六有空吗?想你一起吃个饭,看看我们见面是不是比聊天更有意思
Light:周六有空吗?想你一起吃个饭,不方便也没关系
Medium:周六有空吗?想和你吃个饭,看看我们见面是不是比聊天更有意思。
Heavy:我挺想见你的。周六一起吃饭怎么样?如果时间不合适,我们再找机会。
原:我觉得你挺好看的
:你今天状态很好,让人很难不多看两眼
Light:你今天状态挺好的
Medium:你今天这个状态很有吸引力,让人忍不住多看两眼。
Heavy:我很喜欢你今天的状态,不只是好看,是整个人都很吸引我。
原:我有点想你了
Light:今天有点想起你了。
Medium:刚才遇到一件事,第一反应觉得你会感兴趣,有点想你了。
Heavy:我确实有点想你,也挺期待下次见面。只是想诚实告诉你,不是催你回应。
原:刚才是我说话太冲了但我也不是故意的你别生气了
:刚才我说话太冲,让你不舒服了,对不起。我不是想用「不是故意的」带过去,等你愿意的时候我们再聊。
Light:刚才我说话太冲,让你不舒服了,对不起。
Medium:刚才我打断了你,说话也太冲,这是我的问题。对不起,等你愿意时我想把你的话听完。
Heavy:刚才我的表达伤到了你,对不起。我不会用「不是故意的」带过去,也不要求你马上原谅;我会先改正。
原:就出来一小时你怎么这么不给面子
任意力度:好,没关系。这次就不约了,我尊重你的决定。
# 输出
只输出一版可直接发送的聊天正文;不解释沟通技巧,不提供多个候选,不加引号、标题、前缀或代码围栏。
+44 -19
View File
@@ -125,8 +125,19 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
OSGLog.config.info("[onboarding] didSet → \(newValue, privacy: .public), mirroring to Keychain")
Keychain.setOnboardingCompleted(hasCompletedOnboarding)
if hasCompletedOnboarding {
// Persist page reset immediately, but defer the @Published bump
// so MainAppRoot's OnboardingView MainTabView swap is not
// coalesced with an in-flow page update (can freeze step 6).
configuration.onboardingPage = 0
onboardingPage = 0
let needsPublishedPageReset = onboardingPage != 0
persistConfiguration()
if needsPublishedPageReset {
Task { @MainActor in
guard self.hasCompletedOnboarding else { return }
self.onboardingPage = 0
}
}
return
}
persistConfiguration()
}
@@ -319,24 +330,38 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable {
self.defaults = resolvedDefaults
self.configuration = AppGroupConfiguration.load(fromAvailable: resolvedDefaults)
// Onboarding completion must survive a device reboot. App Group
// UserDefaults can transiently read empty right after boot, which would
// falsely re-show onboarding. Trust the durable Keychain marker when the
// App Group value looks unset, and backfill it once the App Group value
// is confirmed true (covers users onboarded before this safeguard).
let appGroupOnboarding = configuration.hasCompletedOnboarding
let keychainOnboarding = Keychain.hasCompletedOnboarding()
// Distinguish "key absent" (nil plist not loaded / data-protection race)
// from "key present == false" (something actually wrote false).
let rawKeyPresent = resolvedDefaults.object(forKey: AppGroupConfiguration.Keys.hasCompletedOnboarding) != nil
OSGLog.config.info(
"[onboarding] init: appGroup=\(appGroupOnboarding, privacy: .public) (keyPresent=\(rawKeyPresent, privacy: .public)), keychain=\(keychainOnboarding, privacy: .public)"
)
if appGroupOnboarding {
Keychain.setOnboardingCompleted(true)
} else if keychainOnboarding {
configuration.hasCompletedOnboarding = true
OSGLog.config.info("[onboarding] init: App Group read false but Keychain true → restored to true")
// Fresh app container (reinstall after delete): wipe stale Keychain
// onboarding so the welcome flow shows again. Reboot races still use
// Keychain restore when the install identity already exists.
let isFreshInstall = Keychain.beginInstallIdentityIfNeeded()
if isFreshInstall {
configuration.hasCompletedOnboarding = false
resolvedDefaults.set(false, forKey: AppGroupConfiguration.Keys.hasCompletedOnboarding)
OSGLog.config.info("[onboarding] init: fresh install → force hasCompletedOnboarding=false")
} else {
// Onboarding completion must survive a device reboot. App Group
// UserDefaults can transiently read empty right after boot, which would
// falsely re-show onboarding. Trust the durable Keychain marker when the
// App Group value looks unset, and backfill it once the App Group value
// is confirmed true (covers users onboarded before this safeguard).
let appGroupOnboarding = configuration.hasCompletedOnboarding
let keychainOnboarding = Keychain.hasCompletedOnboarding()
// Distinguish "key absent" (nil plist not loaded / data-protection race)
// from "key present == false" (something actually wrote false).
let rawKeyPresent = resolvedDefaults.object(
forKey: AppGroupConfiguration.Keys.hasCompletedOnboarding
) != nil
OSGLog.config.info(
"[onboarding] init: appGroup=\(appGroupOnboarding, privacy: .public) (keyPresent=\(rawKeyPresent, privacy: .public)), keychain=\(keychainOnboarding, privacy: .public)"
)
if appGroupOnboarding {
Keychain.setOnboardingCompleted(true)
} else if keychainOnboarding {
configuration.hasCompletedOnboarding = true
OSGLog.config.info(
"[onboarding] init: App Group read false but Keychain true → restored to true"
)
}
}
let finalOnboarding = configuration.hasCompletedOnboarding
OSGLog.config.info("[onboarding] init: final=\(finalOnboarding, privacy: .public)")