Add shared prompt composition, custom style management, iCloud sync, and native iOS/macOS selection interfaces so users can keep a consistent writing voice across devices.
16 KiB
润色风格包(Polish Style Packs)实施计划
文档状态:实施计划(已评审,决策已冻结) 适用范围:iOS 主 App + 键盘扩展管线 + macOS(
OSGKeyboard/OSGKeyboardExt/OSGKeyboardMac/OSGKeyboardShared) 分支:feature/polish-style-packs参考竞品:OpenLess Style Pack(完整写作人格 + 运行时装配) 关联代码史:1bdb882(polish scenarios)→4ab60ba(删除手动场景,改依赖 AppContext) 创建日期:2026-07-25
1. Executive Summary
1.1 目标
为 OSGKeyboard 恢复并升级「多润色风格」能力:用户在主 App(及 Mac)选择 完整写作人格包,每次听写润色按 active pack 装配 system prompt;支持自定义包与 iCloud 同步。
对齐产品约束:
- 入口:主 App Tab(词库与设置之间)+ Mac 侧栏对称项;键盘顶栏不加 chip
- 形态:学 OpenLess — 每包是 整段可编辑 prompt,不是短 StyleDirective
- 横切能力保留:词典、Intensity、
globalOutputContract、TranscriptPostProcessor - 云端:active id 进设置同步;用户包列表学词库走独立 KVS blob
- 少冗余:一条装配管线、一套模型、一处导航枚举、一份云同步模式
1.2 核心结论(冻结)
| 决策 | 选择 |
|---|---|
| 产品单元 | Style Pack(完整写作人格),非旧 Scenario 短 directive |
| 内置包 | 4 个:builtin.light / builtin.structured / builtin.formal / builtin.chat |
| 默认 active | builtin.light(非法 / 缺失 id 回落至此) |
| 自定义上限 | ≤ 8 个 user pack;单包 prompt ≤ 6 000 字符 |
| Intensity | 保留全局 light/medium/heavy,装配时追加短 guideline(与包正交) |
| AppContext | 降级为可选上下文前提(短);不再充当风格人格 |
| 装配 | 唯一 PolishPromptComposer(由现 buildPrompt 演化);禁止平行 builder |
| 翻译 | 第一期 不把 Style Pack 拼进 TranslationPrompt |
| 键盘 UI | 第一期 不加 ScenarioChip / 风格切换 |
| Mac | 与 iOS 同迭代做侧栏入口 + Shared 数据层 |
| 云同步 | 跟随现有 iCloud 总开关;不新建独立 sync toggle |
| 旧 Scenario | 不复活 ScenarioPrompt / ScenarioStyleDirective;可复用部分 polishScenario.* 显示名 |
1.3 非目标(本期不做)
- OpenLess Marketplace / ZIP 导入导出 / 运行时 diagnostics 大页
- 键盘顶栏风格切换、热键轮换
- 把 Intensity 收进包内(可二期评估)
- 社交场景(小红书 / 微博 / 逗比 / TODO)作为内置包(可作「从模板新建」二期)
- Onboarding 新增风格步骤
- 新建第二套
StylePolishingService或把 styles 塞进PersonalDictionary - 在 Linux CI 上跑需 Xcode 的集成测试(见
AGENTS.md)
2. 背景与现状差距
2.1 历史
| Commit | 说明 |
|---|---|
1bdb882 |
完整多场景:PolishScenario + ScenarioPrompt + ScenarioStyleDirective + 键盘 ScenarioChip |
4ab60ba |
删除手动场景 UI/模型(~871 行),改依赖自动 AppContext |
| 残留 | polishScenario.* 等本地化字符串仍在;config.polishScenarioId / config.systemPrompt 可能仍在升级用户设备上 |
2.2 当前润色路径(问题)
ASR 文本
→ PolishingService.polish
→ buildPrompt:
globalOutputContract
+ Task1 纠错 + Task2 结构
+ Task3:AppContext.polishGuideline + Intensity
+ 词典 + 上文 + 原文
→ TranscriptPostProcessor → 插入
| 缺口 | 说明 |
|---|---|
| 无用户可选风格包 | 只能靠自动 AppContext + Intensity |
| 无自定义人格 | systemPrompt API 存在,生产 UI 已删 |
| 无风格云同步 | SyncedAppSettingsV2 无 style 字段 |
| 旧场景不可直接贴回 | 短 directive 与 v0.3 长 buildPrompt 双轨会打架 |
2.3 OpenLess 可学之处
OpenLess StylePack.prompt = 用户可见的 完整 system 正文;运行时再叠:
[可选] context_premise(工作语言 / 前台 App)
+ StylePack.prompt({{HOTWORDS}} → 热词块)
+ 注入防御 / 多轮指令
OSG 映射:
| OpenLess | OSG |
|---|---|
StylePack.prompt |
PolishStylePack.prompt |
{{HOTWORDS}} |
{{DICTIONARY}} → PersonalDictionary.promptFragment() |
context_premise |
可选 AppContext 短前提 |
| 系统尾部 | Intensity + globalOutputContract |
active_style_pack_id |
activePolishStyleId(SyncedAppSettingsV2) |
本地 style-packs.json |
App Group JSON + iCloud KVS(学词库,不学本机文件) |
| Style 导航页 | iOS Tab + Mac MacSection |
| Marketplace | 本期不做 |
3. 目标架构
3.1 数据流
[Styles Tab iOS / Mac Styles Section]
│ write user packs + activeId
▼
App Group ──► iCloud KVS(catalog 学词库;activeId 进 settings.v2)
│ read(主 App 写;Ext / 管线只读)
▼
FlowSessionManager / MacDictationPipeline
▼
PolishingService
→ PolishPromptComposer(active pack)
→ LLM
→ TranscriptPostProcessor
3.2 分层职责
flowchart TB
subgraph UI["UI 层"]
iOSTab["AppTab.styles"]
MacSec["MacSection.styles"]
Settings["Settings: Intensity + Translation only"]
end
subgraph Data["数据层 Shared"]
Pack["PolishStylePack"]
Catalog["PolishStyleCatalog user packs"]
Active["activePolishStyleId"]
Dict["PersonalDictionary"]
end
subgraph Sync["云同步"]
SettingsKVS["SyncedAppSettingsV2.activePolishStyleId"]
StylesKVS["polishStyles.v2 KVS blob"]
AppSync["AppCloudSync 一行接入"]
end
subgraph Pipeline["管线"]
Composer["PolishPromptComposer"]
Polish["PolishingService"]
Post["TranscriptPostProcessor"]
end
iOSTab --> Catalog
iOSTab --> Active
MacSec --> Catalog
MacSec --> Active
Settings --> Intensity
Catalog --> StylesKVS
Active --> SettingsKVS
StylesKVS --> AppSync
SettingsKVS --> AppSync
Active --> Composer
Catalog --> Composer
Dict --> Composer
Composer --> Polish
Polish --> Post
3.3 领域模型
PolishStylePack(克制字段)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
String |
builtin.light 或 user.<uuid> |
name |
String |
显示名;builtin 可用 l10n key 解析 |
prompt |
String |
完整人格正文,可含 {{DICTIONARY}} |
kind |
builtin | user |
内置 vs 用户 |
createdAt / updatedAt |
Date |
merge / UI |
首发不做:examples、marketplace、icon、author、enabled 轮换列表。
内置 4 包
| id | 角色 |
|---|---|
builtin.light |
轻度清理(默认 active) |
builtin.structured |
清晰结构 |
builtin.formal |
正式表达 |
builtin.chat |
日常聊天 |
- 正文:Swift 常量,不进
.strings(防翻译改变 LLM 行为) - 显示名:Shared / App l10n
- 不整包同步;用户「编辑内置」→ 另存为 user 包并设为 active
PolishStyleCatalog(仅用户资产)
镜像 PersonalDictionary:
entries: [PolishStylePack](仅kind == user)version,lastSyncedAtdeletedEntryIDs: [UUID: Date](或按 string id 的 tombstone;实现时与 id 方案一致)clearedAt
列表 UI = 代码内置 4 包 ∪ catalog.user entries。
硬上限
| 项 | 值 |
|---|---|
| User packs | ≤ 8 |
单包 prompt |
≤ 6 000 字符 |
| 超限 | UI 拦截 + store 写入拒绝 |
3.4 Prompt 装配(唯一路径)
规则:永远有 active pack(缺省 / 非法 → builtin.light)。
禁止「有 pack 走 A、无 pack 走旧 buildPrompt」双轨。
装配顺序:
1. [可选] AppContext 前提(短;unknown 可省略)
2. StylePack.prompt
- 含 {{DICTIONARY}} → 替换为词典块
- 无占位符且词典非空 → 追加词典块(兼容用户删占位符)
3. Intensity.promptGuideline(短)
4. globalOutputContract(强制尾部,用户包不可关闭)
5. precedingText(若有)
6. 「原文」+ transcript
| 保留 | 由 Composer 接管 / 替换 |
|---|---|
| API key / 超时 / skipLLM | 旧 Task3「风格要求」行(AppContext.polishGuideline 作为人格) |
globalOutputContract |
旧「角色 + Task1/2/3」整段骨架(人格改由 pack 提供) |
| Intensity 追加 | 平行 ScenarioPrompt |
| 词典注入 | systemPrompt 作为第三种风格旁路 |
TranscriptPostProcessor(.polish) |
— |
TranslationPrompt 分支不动 |
— |
自定义 = 编辑 user pack 的 prompt,不再单独暴露「系统提示」设置页。
占位符常量:
public static let dictionaryPlaceholder = "{{DICTIONARY}}"
3.5 存储与云同步
| 数据 | 存储 | Key |
|---|---|---|
| active id | App Group + SyncedAppSettingsV2 |
config.activePolishStyleId / field |
| user packs blob | App Group JSON | config.polishStyles.v1 |
| user packs iCloud | KVS 独立 key | polishStyles.v2 |
| builtin 正文 | 仅代码 | — |
规则:
activePolishStyleId:学polishIntensity进 V2(decodeIfPresent,不 bump schemaVersion)- Catalog sync:镜像
PersonalDictionaryCloudSync(tombstone、clearedAt、payload 上限、跟随settingsICloudSyncEnabled) AppCloudSync.pullAll/syncNow各加一行- Extension:只读;主 App / Mac:读写
- Styles 不塞进
SyncedAppSettingsV2JSON 本体(体积与 LWW 耦合)
迁移
若设备残留:
| 旧 key | 处理 |
|---|---|
config.polishScenarioId |
映射到最接近的 builtin id(无映射 → builtin.light) |
config.systemPrompt(非空) |
创建一个 user pack(名称「自定义」)并设为 active,然后停止读取旧 key |
一次性迁移,避免双源。
3.6 导航与 UI
iOS
当前:键盘 | 历史 | 词库 | 设置
目标:键盘 | 历史 | 词库 | **风格** | 设置
| 文件 | 改动 |
|---|---|
MinimalTabBar.swift |
AppTab.styles(插在 dictionary 与 settings 之间) |
MainTabContent.swift |
case .styles: PolishStylesView() |
MainSplitView.swift |
ForEach(AppTab.allCases) 自动带上 |
新页:PolishStylesView + PolishStyleEditorSheet
- 结构仿
PersonalDictionaryView(List / 选中 / sheet) - 不复制词库业务逻辑
Settings:
- 保留:Intensity、Translation
- 不放:风格列表 / 编辑器
- Section 文案:「词库与润色」→「润色偏好」(词库已有独立 Tab)
Mac(同迭代)
| 文件 | 改动 |
|---|---|
MacDictationViewModel.swift |
MacSection.styles |
MacRootView.swift |
detail switch |
| 新 | MacPolishStylesView(壳 + Shared 数据) |
键盘
第一期不加 chip;Ext 仅读 App Group 供管线使用。
3.7 Shared vs Target 边界
| 放 Shared | 放 App / Mac |
|---|---|
PolishStylePack / Catalog / +Merging |
PolishStylesView / Editor sheet |
PolishStyleCloudSync |
AppTab / MacSection wiring |
AppGroupStore accessors |
Settings 文案微调 |
SyncedAppSettingsV2 field |
— |
PolishPromptComposer + PolishingService 改造 |
— |
| Builtin prompt 常量 | — |
| 单测:merge / sync / composer | — |
4. 反模式清单(实施自检)
- 同时保留旧
buildPrompt全文骨架 与 Style Pack 全文(ASR/纠错规则写两遍) - 复活
ScenarioPrompt/ScenarioStyleDirective - Style blob 塞进
SyncedAppSettingsV2 - 新建独立 iCloud 开关
- Settings 与 Styles Tab 两处都能改 active
- Builtin 正文进 KVS
- 用「
styleGuideline ?? appContext」小补丁冒充完整包 - Extension 写 catalog
- 在
.strings里存 LLM prompt 正文 - 新建平行 nav enum / 平行 PolishingService
5. 实施顺序
| Phase | 内容 | 验收 |
|---|---|---|
| 1 Shared 模型 + App Group + activeId | 尚无 UI;读写测通 | unit:resolve default / 上限拒绝 |
2 Composer 替换 buildPrompt |
默认 builtin.light;管线行为可测 |
IntelligentPolishTests:contract / dictionary / intensity |
| 3 Cloud catalog sync | AppCloudSync 接入 |
merge / tombstone 测;对齐词库 checklist |
| 4 iOS Tab + Styles UI | 选中 / 新建 / 编辑 / 另存内置 | 手动:切换风格后听写输出差异可感知 |
| 5 Mac Section + UI | 与 iOS 同数据 | Mac 侧栏可选包 |
| 6 Settings 瘦身 + 旧 key 迁移 | 无双源 | 升级用户不丢自定义 prompt |
| 7 Changelog / 版本 | 按 AGENTS.md;有用户可见 feat 再 bump |
CHANGELOG 双语 |
建议 PR:可按 Phase 1–2、3、4–5、6–7 拆,避免巨型 diff。
6. 关键文件速查
现用(将改)
OSGKeyboardShared/Services/PolishingService.swift
OSGKeyboardShared/Models/PolishContext.swift
OSGKeyboardShared/Models/AppGroupConfiguration.swift
OSGKeyboardShared/Models/SyncedAppSettingsV2.swift
OSGKeyboardShared/Services/AppGroupStore.swift
OSGKeyboardShared/Core/Configuration/ConfigurationStore.swift
OSGKeyboardShared/Services/ICloudSync/AppCloudSync.swift
OSGKeyboard/Views/Components/MinimalTabBar.swift
OSGKeyboard/Views/MainTabContent.swift
OSGKeyboard/Views/SettingsView.swift
OSGKeyboardMac/MacDictationViewModel.swift
OSGKeyboardMac/MacRootView.swift
新建(建议)
OSGKeyboardShared/Models/PolishStylePack.swift
OSGKeyboardShared/Models/PolishStylePack+Merging.swift
OSGKeyboardShared/Services/PolishPromptComposer.swift # 或并入 PolishingService internal
OSGKeyboardShared/Services/PolishStyleCloudSync/PolishStyleCloudSync.swift
OSGKeyboard/Views/PolishStylesView.swift
OSGKeyboard/Views/PolishStyleEditorSheet.swift
OSGKeyboardMac/MacPolishStylesView.swift
OSGKeyboardTests/PolishStyleMergeTests.swift
OSGKeyboardTests/PolishStyleCloudSyncTests.swift
# IntelligentPolishTests.swift 扩展
已删勿复活(git 仅作文案参考)
PolishScenario.swift, ScenarioPrompt.swift, ScenarioStyleDirective.swift
ScenarioChip.swift, ScenarioPickerRow.swift, SystemPromptSettingsView.swift
可复用孤儿 l10n(显示名,非 prompt)
polishScenario.* / polishScenario.chip.*(Shared.strings)
settings.polishScenario.*(Localizable — 需改前缀或重写文案)
7. 测试与验证
7.1 自动化(macOS / Xcode)
- Catalog merge:增删、tombstone、跨设备 LWW
- Cloud sync:payload 过大拒绝;enable 跟随 settings
- Composer:默认 pack;
{{DICTIONARY}}替换;无占位符追加;contract 始终存在;Intensity 注入 - activeId 非法 →
builtin.light - user pack 超 8 / prompt 超 6k → 写入失败
7.2 手动(对齐词库 checklist 思路)
| # | 步骤 | 期望 |
|---|---|---|
| 1 | 启用 iCloud → 设备 A 新建自定义包并激活 | 本地立即生效 |
| 2 | 设备 B 打开风格 Tab | 自定义包出现;active 一致(eventually) |
| 3 | A 删包 | B 上 tombstone 生效,不复活 |
| 4 | 切换 builtin.structured 后听写含「第一点…第二点」 | 输出更偏结构化 |
| 5 | 键盘听写 | 使用主 App 写入的 active pack(无需 iCloud 等待) |
| 6 | Mac 侧栏改 active | iOS 随后同步(若 iCloud 开) |
8. 版本与 Changelog
- 用户可见功能 → Conventional Commit
feat(polish): … - 合并
main后按AGENTS.md评估 MINOR bump(0.x) CHANGELOG.md双语条目示例方向:- Polish style packs:主 App / Mac 可选完整润色人格;支持自定义与 iCloud。
9. 决策冻结摘要
| # | 问题 | 冻结答案 |
|---|---|---|
| 1 | 入口 | App Tab + Mac 侧栏;键盘不加 |
| 2 | Prompt 形态 | OpenLess 式完整包 + 运行时横切层 |
| 3 | 内置数量 | 4(light / structured / formal / chat) |
| 4 | Intensity | 保留全局档位 |
| 5 | 自定义上限 | 8 × 6 000 字符 |
| 6 | Mac | 同迭代 |
| 7 | 云 | active ∈ settings.v2;packs ∈ 独立 KVS;无新 toggle |
| 8 | 旧 Scenario 代码 | 不复活;可复用显示名 |
文档维护:实施过程中若装配顺序、KVS key 或内置包 id 变化,请同步更新本节与 CHANGELOG [Unreleased]。