Soundiary · 工程技术文档 v1.0

给写代码的人看的那一份:工程结构、实现细节、可复制的配置。

本文是《Soundiary 技术方案 v2.0》的工程实现篇,面向开发、测试与上架执行。三条不可动摇的工程底线:① 音频不进任何长期存储,转写完成即删除(等待传输的临时文件也不例外);② 全部地区共享的唯一二进制中不存在任何第三方联网代码;③ 端侧模型随包分发,运行时零下载。所有设计决策如与这三条冲突,以本页为准。

⌚ Watch 抽样 · 占空比 3–8%
🔥 PCM 内存驻留 ≤ 90 秒后清零
🧠 Whisper-tiny int8 + Qwen2.5-0.5B int4
🔒 Secure Enclave + Data Protection A
🚫 构建期联网符号扫描(CI 卡口)
🌐 zh-Hans / en / es 三语

0 · 文档信息与约定

文档版本Engineering Spec v1.0
对应方案技术方案 v2.0(index.html)
最低系统iOS 26.0 / watchOS 26.0
语言Swift 6.2 · SwiftUI + SwiftData
三方依赖0(含 SPM / CocoaPods / 二进制 SDK 全部为 0)
本地化zh-Hans(基准)/ en / es

0.1 三条工程底线

#底线工程强制手段违反后果
①原始音频永不写入任何持久化介质禁用 AVAudioRecorder / AVAssetWriter / FileManager 写音频;CI 静态扫描 + 运行时断言 assertNoAudioFileAPIsLinked()PIPL 敏感个人信息违规;「不可回放」卖点失效
②全 App 零第三方联网(全球统一,无例外)云端 Provider 代码物理删除(不用 #if 包裹)+ URLSession 符号扫描 + NSAppTransportSecurity 全禁 + 无宿主白名单备案豁免 100% 驳回,虚假申报可致封号
③模型随包分发,运行时不下载模型内置于 IPA(不使用 On-Demand Resources / Background Assets),全球同一份触发网络请求,同 ②

✅ ②的当前状态:云端 Provider 与「用户自填 API Key」相关代码已按此条物理删除(不是用 #if 包裹)—— 设置页的密钥输入分组、洞察页的云端分派、顶栏承诺条的分流文案、以及 LLMConfig/LLMClient 等实现一并移除,归档在 Docs/archive/。核验方式(4 项全过才算数):nm -a 无 URLSession 符号、strings 无 chat/completions、otool -L 未链接 CFNetwork、打包后的 Info.plist 里不存在 NSAppTransportSecurity 键(连空字典都不留)。

📌 为什么只有「一个 Target」——App Store 不支持按地区下发不同二进制

早期方案设想过「CN Target(无联网)+ Global Target(含 Token),中国区只下发前者」。 这在技术上不成立:App Store 同一 bundle ID 的同一版本只有一个二进制,不支持按国家/商店分发不同构建 (Apple 官方:"You can only have one version of your app in the store at one time, regardless of region.")。

因此 v2 定为单一 iOS Target、单一 bundle ID(com.soundiary.app)、单一 ASC 记录: 全球用户拿到完全相同的二进制,放弃「用户自配云端 Token」功能,换取备案豁免最稳、长期维护最轻。 若日后确需 Token 功能,唯一路径是另发一个独立 App,而不是给现有包加开关。

0.2 术语

术语含义
快照(Snapshot)一次采样窗(默认 8 秒,含人声时最长 30 秒)加工后的产物:文本 + 声纹向量 + 环境标签 + 响度包络。不含音频
采集即弃PCM 只进内存缓冲区,转写完成即刻 memset 清零。不进任何长期存储——仅当需要在本人的手表与手机之间传输时,短暂生成一个受系统保护的临时文件,投递有结果即删除
抽样感知Watch 每 3–5 分钟唤醒采集一小段,而非连续录音。占空比 3–8%
L1 / L2 / L3摘要引擎的三级降级:L1 = Foundation Models(iOS 26+,需 A17 Pro+ 且非国行设备/大陆账号);L2 = 自带 Core ML 量化模型(全球兜底);L3 = 规则模板

1 · 工程结构与 Target

1.1 Xcode 工程拓扑

Soundiary.xcodeproj ├── SoundiaryKit // 共享核心(Swift Package,本地) │ ├── Sources/SoundiaryKit/ │ │ ├── Sensing/ // WatchSensingEngine, BudgetEstimator │ │ ├── Pipeline/ // EphemeralAudioPipeline, OnDeviceASR │ │ ├── Fusion/ // TimelineBus, HealthBridge, ContextMiner │ │ ├── Models/ // SwiftData schema: Day, Snapshot, Speaker, Insight │ │ ├── Inference/ // ModelHub, LLMBridge, VoiceprintExtractor │ │ ├── Security/ // SecureKeyStore, CryptoBox(无远程凭据) │ │ ├── Localization/ // L10n, PromptCatalog(三语) │ │ └── Privacy/ // ConsentFlow, TransparencyMeter, AuditLog │ └── Resources/ │ ├── Models/ // 随包分发,全球同一份:见 §5 │ │ ├── WhisperTiny_int8.mlmodelc │ │ ├── Qwen25_05B_int4.mlmodelc │ │ ├── Voiceprint_ECAPA_s.mlmodelc │ │ └── EnvNet_YAMNet_s.mlmodelc │ └── Localization/{zh-Hans,en,es}.lproj ├── Soundiary (iOS App) // 唯一 iOS Target:bundle ID com.soundiary.app,全球统一,零联网 ├── Soundiary Watch App // 嵌入 iOS Target,共用一套代码 ├── Soundiary WatchWidgets // WidgetKit 小组件(仅显示状态,无数据上传) └── SoundiaryIntents // App Intents(Siri/快捷指令,"开始感知/停止")
📌 Watch App 与 iOS App 的代码共享比例目标 ≥ 70%

把 SamplingPolicy、AudioFrame、SoundSnapshot、BudgetEstimator 全部放进 SoundiaryKit,Watch 与 iPhone 双端复用,避免两侧逻辑漂移导致时间轴对不齐。

1.2 能力矩阵(全球统一,无地区差异)

能力状态说明
声音抽样采集✅Watch 抽样 → iPhone 本地加工
端侧 ASR✅系统 Speech 优先,自带 Whisper 兜底
端侧 LLM(三级降级)✅L1 Foundation Models(需 A17 Pro+ 且非国行设备/大陆账号)→ L2 自带 Core ML(全球兜底)→ L3 规则模板
云端 Provider 配置不存在已放弃 Token 功能,见 §0.1
API Key 输入界面不存在包内不得出现任何密钥输入 UI
StoreKit / 内购已移除改为付费下载后,App 运行时完全不联网
Safari 外链(政策/支持页)✅系统浏览器打开,非 App 内获取内容
崩溃/分析 SDK0 个任何新增都需先在隐私标签声明并重估豁免风险

2 · 构建配置与离线审计

2.1 xcconfig(全局唯一配置)

// Config/Soundiary.xcconfig —— 唯一一份,没有 CN / Global 之分
PRODUCT_BUNDLE_IDENTIFIER          = com.soundiary.app
ENABLE_ON_DEMAND_RESOURCES         = NO
// 不设 INFOPLIST_KEY_NSAppTransportSecurity:没有任何网络请求,ATS 无用武之地

// ⚠️ 不要定义 SOUNDIARY_OFFLINE_ONLY 之类的「全球版」开关:
//    单一 App 没有第二个构建目标,宏无从区分,反而会让人误以为存在全球分支。

2.2 后端选择:运行时判断,不是编译期

public enum ModelHub {
    public static var availableBackends: [LLMBackend] {
        // L1:Foundation Models。必须用 isAvailable,不能按地区字符串分流——
        // 官方口径是「国行设备 或 大陆 Apple Account」二选一即不可用;
        // 非大陆设备 + 非大陆账号,人在中国大陆也能用。别误伤常驻海外的用户。
        if FoundationModels.isAvailable { return [.foundationModels, .coreMLBundled, .templateFallback] }
        // L2:自带模型是老设备与大陆用户的唯一路径;L3 兜底。
        return [.coreMLBundled, .templateFallback]
    }
}
🚨 不要用 #if 包裹,要「物理不存在」

审核员与备案审核可能反编译或抓包。要求:__TEXT 段中不出现 URLSession / NSURLSession / nw_connection / 第三方域名字符串。既然只有一个 Target,就把云端 Provider 相关文件从工程里彻底删除,而不是用 #if SOUNDIARY_OFFLINE_ONLY 包裹——Swift 的条件编译在 Release 下虽会剔除未使用分支,但符号表与字符串常量可能残留。

2.3 CI 卡口脚本(提交前必跑)

#!/bin/bash
# Scripts/audit_offline.sh —— 全 App 零联网审计
set -e
APP="$BUILD_DIR/Soundiary.app"
BIN="$APP/Soundiary"

echo "== 1) 网络符号扫描 =="
if nm -u "$BIN" | grep -E "URLSession|NSURLConnection|nw_connection_create|CFNetwork"; then
  echo "❌ 发现网络符号(需人工确认白名单)"; exit 1
fi

echo "== 2) 第三方域名字符串扫描 =="
if strings "$BIN" | grep -Ei "(\.com|\.cn|\.net|\.io)/|https?://" | grep -v "apple.com" | grep -v "soundiary"; then
  echo "❌ 发现疑似第三方域名"; exit 1
fi

echo "== 3) 可回放录音 API 扫描 =="
if nm -u "$BIN" | grep -E "AVAudioRecorder|AVAssetWriter|AVAudioFile"; then
  echo "❌ 出现可产出可回放录音的 API,违反底线①"; exit 1
fi

echo "== 4) 动态库清单 =="
otool -L "$BIN" | grep -v "/usr/lib|/System|@rpath.*Soundiary" || true
echo "✅ 全部通过"

把此脚本加入 Build Phases → Run Script(唯一 Target 即可),失败即构建失败,避免侥幸。

3 · Watch 声音抽样引擎

主录音设备 = Apple Watch。核心结论:不做连续录音,做声音抽样。

3.1 为什么只能是抽样

约束实测/官方结论抽样方案如何规避
watchOS 第三方 App 后台麦克风存活离开前台后麦克风被挂起,社区实测平均约 17 秒单次采样 6–8 秒 < 阈值,天然在存活窗口内完成
后台模式白名单无 audio 后台模式;HKWorkoutSession 保活会被其他运动 App 顶掉且属滥用不申请任何特殊后台模式,用 Background Refresh + 本地通知唤醒
电量连续录音约 3–4 小时耗尽占空比 3–8%,续航影响 < 8%/天
用户认知「全天录音」引起强烈隐私焦虑UI 明确写「全天抽样感知」,配透明度面板

3.2 采样策略参数

public struct SamplingPolicy {
    public var idleInterval: TimeInterval = 180          // 静默期基础间隔 3 分钟
    public var baseDuration: TimeInterval = 8             // 单次采样 8 秒(< 17s)
    public var voiceExtendedDuration: TimeInterval = 30   // 连续人声时延长上限
    public var voiceExtendThreshold: TimeInterval = 1.2   // 持续人声 1.2s 触发延长
    public var sampleRate: Double = 16_000                // 16 kHz,转写与声纹共用
    public var continuous: Bool = false                   // true = 连续采样档(需 HKWorkoutSession 保活)
    public var continuousWindowSeconds: TimeInterval = 30 // 连续档窗长
    public var bodySampleInterval: TimeInterval = 0       // 体征采样间隔(0 = 随音频窗采)
    public var bodyOnlyPushInterval: TimeInterval = 0     // 纯体征窗推送间隔(0 = 不推)

    public static let standard = SamplingPolicy()
    public static let lowPower = SamplingPolicy(idleInterval: 600, baseDuration: 6,
                                     voiceExtendedDuration: 20, voiceExtendThreshold: 1.5,
                                     bodySampleInterval: 60, bodyOnlyPushInterval: 300)
    public static let dense = SamplingPolicy(continuous: true, continuousWindowSeconds: 30)
    public var dutyCycle: Double { baseDuration / idleInterval }   // ≈ 4.4%
}
// ⚠️ 档位由用户在设置里选(标准 / 省电 / 连续),首版没有「夜间自动降频」——
//    入睡检测已于 2026-10-15 正式取消(见 §3.6),首版只按时间表采样,不判断用户是否入睡。

3.3 调度与唤醒时序

Watch 端一个调度周期(静默期,3 分钟) 静默等待(蓝牙低功耗,CPU idle) 唤醒 采样窗 8s 整窗 PCM 驻留内存 人声延长 最长 30s 🗑 内存清零 ① BGAppRefreshTask / 本地通知触发 → 申请音频会话(≤0.3s) ② 整窗累积:采样窗内 PCM 全量驻留内存,窗结束一次性封装为单文件(μ-law,1 B/样本) ③ WCSession.transferFile 投递:系统负责排队与重试,手机暂时不可达也不丢窗 ④ 投递回调到达即删除本地临时副本;App 被杀留下的孤儿在下次启动时清理 注:整段时长受 watchOS 后台存活窗口约束,超过则本次快照记为 truncated,不影响叙事完整性
图 3-1 · Watch 单次采样窗时序(静默期占空比 ≈ 4.4%)

3.4 后台调度的可行手段(按推荐度排序)

手段可靠性是否需特殊权限说明
BGAppRefreshTask(watchOS 支持有限)中否系统按使用习惯调度,间隔不可控;作为软触发而非唯一依赖
本地通知(UNNotification)静默唤醒中高通知权限用 UNCalendarNotificationTrigger 排队多个触发点,点击前 App 已在前台短暂存活
用户主动开始感知(大按钮 + 档位选择)高麦克风主路径:点「开始感知」→ 申请麦克风权限 → 按所选档位(标准 / 省电 / 连续)采样;开始前的用法说明与 watchOS 时长限制在 UI 里明示
HKWorkoutSession 保活高HealthKit连续档已采纳:watchOS 上长时间后台使用麦克风的唯一合法通道,结束即 discardWorkout()、不在「健身记录」留训练。⚠️ 能力滥用有 4.2 / 2.4 拒审风险 —— 本 App 真实读取心率 / HRV 做时间轴对齐,属健康向用途;且连续档由用户主动选择,不是默认行为
WKExtendedRuntimeSession——不可用:仅限正念、理疗、智能闹钟、物理治疗四类场景
💡 真正的产品化路径:把「熄屏后台」翻译成用户可理解的交互

Watch 上提供一个大按钮「开始感知一天」。点击后 App 保持前台运行、屏幕自动调暗(非锁屏),此时录音完全合法且稳定。用户抬腕看表即自动暂停并显示「感知暂停中」。这比追求隐形后台更诚实,也天然满足「用户知情同意」——审核员最在意的就是这一点。

3.5 电量与可感知时长估算

public enum BudgetEstimator {
    /// 返回:预计可感知小时数 + 档位文案 key
    public static func estimate(policy: SamplingPolicy = .standard,
                                battery: Double) -> (hours: Double, levelKey: String) {
        let health   = HealthCalibration.shared.degradationFactor   // 电池健康度 → 0.6~1.0
        let drainPerHour = policy.dutyCycle * 7.5              // 经验系数:满占空比 7.5%/h
                            + 1.2                                  // 蓝牙传输基线
        let usable   = max(0, battery - 0.15) * health          // 保留 15% 安全电量
        let hours    = usable / drainPerHour
        return (hours, hours > 10 ? "budget.long" : hours > 4 ? "budget.mid" : "budget.short")
    }
}
电池健康度当前电量标准档预计可感知UI 提示文案 key
100%100%≈ 43 小时budget.long ·「电量充足,可覆盖整天」
85%60%≈ 20 小时budget.long
80%30%≈ 6.5 小时budget.mid ·「可感知至傍晚,建议睡前充电」
75%20%≈ 2.6 小时budget.short ·「电量偏低,已自动切换省电抽样」

⚠️ 经验系数 7.5%/h 需用真机实测校准(建议 Series 7/9/Ultra 各测 3 轮),并在设置页以「估算值,非精确值」声明。

3.6 入睡检测(三重判据) 已取消 · 任何版本都不实现

⚠️ 本功能已于 2026-10-15 正式取消,任何版本都不会实现,本节设计仅作历史留存。代码中不存在任何入睡判断逻辑(sleep.* 界面文案零引用);HealthKit 只读心率 / HRV / 静息心率,不读睡眠数据。采样完全由用户选定的档位掌控——想停就停,不需要"检测入睡"。

判据数据源阈值权重
声学静默本引擎响度包络连续 3 个采样窗 dBFS < 阈值且无人声0.4
心率下探HealthKit heartRate较个人静息基线下降 ≥ 8%0.3
手腕静止CMMotionManager / 腕部姿态连续 10 分钟无显著位移0.3

(以下为历史设计,已取消)综合得分 ≥ 0.7 判定入睡 → 自动切换 nightInterval(10 分钟)并继续低频环境监测;连续 30 分钟无人声则完全停止采样,晨起(首次显著运动 + 光照/时间窗)自动恢复。

4 · 采集即弃流水线(iPhone 侧)

4.1 数据流

Watch PCM16kHz / 16bit μ-law 封装1 B/样本 整窗缓冲内存 · 单窗 ≤ 30s 端侧 ASRWhisper-tiny 声纹 / 环境192d + 标签 快照落库 SwiftData 20–80 KB/天 memset 清零 · 不进长期存储 discardBytes 计数器 +N 关键约束:PCM 只进内存缓冲区;收窗后整体序列化为一个文件交出,投递有结果后立即删除本地副本。 不使用 AVAudioRecorder / AVAssetWriter / AVAudioFile,单元测试含 assertNoAudioFileAPIsLinked()。 转写按序串行处理,不丢窗;手机暂时不在时窗以受保护的临时文件排队等待,送达即删。
图 4-1 · 采集即弃流水线(音频转成文字后立即删除;仅等待传输期间短时缓存)

4.2 关键实现要点

环节实现注意事项
接收WCSession 文件通道(transferFile)逐窗到达;Inbox 文件在回调内同步读完再删系统负责排队与自动重试;单窗元数据随文件原子到达,不存在「帧到了但结束标记丢了」的半截窗
缓冲整窗缓冲,容量 = 30 秒 × 16kHz × 2B ≈ 960 KB(解码后)窗内 PCM 只在内存;转写完成即释放并清零,不借助任何缓存写回磁盘
VAD能量 + 频谱平坦度双判据,1.2s 持续人声触发延长阈值对三语无差异;低信噪比场景降级为「有人声/无人声」二分类
ASRWhisper-tiny Core ML(int8),流式 chunk 2s 滑窗中文需确保模型含 zh token;language 由用户主语言设定
声纹每 3 秒取一次 192d embedding,窗内聚类只存 向量,不存音频;向量为派生特征,不可还原语音
落库SoundSnapshot 写入 SwiftData字段见 §7
清零成功后立即清零并 discardBytes += n计数器供透明度面板展示「今日已丢弃 4.2GB」
✅ 为什么「不可回放」反而是卖点

用户手机里没有任何一段可播放的录音,意味着:手机丢了、被取证、被云端同步,都拿不到你的声音。同时存储成本从「每天几百 MB」降到「每天几十 KB」。上架文案直接写:「你的声音,从未被保存。」

5 · 端侧模型与 Core ML 集成(全球统一)

5.1 三级降级

级别后端适用设备可用性质量
L1Foundation Models(iOS 26+,端侧 ~3B)支持 en / es / zh-Hans需 A17 Pro+ 且非国行设备、非大陆 Apple Account★★★★★
L2自带 Core ML 模型全部设备——老设备与大陆用户的唯一路径✅ 随包分发,全球同一份★★★☆☆
L3规则模板兜底低端设备 / 内存不足 / 模型加载失败✅★★☆☆☆

5.2 模型清单与实际体积

用途推荐模型精度体积输入/输出
语音转写Whisper-tiny(多语版,含 zh/es/en,39M 参数)int8≈ 40 MB16kHz mel → 文本
日记/总结生成Qwen2.5-0.5B-Instruct(494M 参数)int4(palettized)+ fp16 embedding≈ 300 MB≤1024 token → ≤512 token
声纹嵌入ECAPA-TDNN(small,512d→192d PCA)fp16≈ 8 MB3s 音频 → 192d 向量
环境声分类YAMNet-small / AudioSet 子集微调int8≈ 12 MB0.96s → 10 类标签
模型合计≈ 360 MBIPA 总量 ≈ 420 MB
📏 体积口径是怎么算出来的(避免再次低估)

Qwen2.5-0.5B-Instruct 共 494M 参数,其中 embedding 就占 136M。可核对的公开实测值:

  • GGUF Q4_K_M ≈ 379–398 MB、Q3_K_M ≈ 355 MB、Q8_0 ≈ 531 MB、fp16 ≈ 994 MB;
  • 转 Core ML 并做 4-bit palettization 后约 250–320 MB(embedding 常保留较高精度)。

本方案取 300 MB。Whisper-tiny 为 39M 参数,int8 约 40 MB。 早期文档写的「130 MB / 75 MB」是把参数总量误当作可全量 int4 的结果,实际偏低约 2.5 倍——若按那个数字做包体规划,上线后一定会超。

📌 关于 200 MB 蜂窝下载限制

iOS 对超过 200 MB 的 App 在蜂窝网络下会提示用户确认。这个限制从 iOS 13 起可由用户手动绕过 (设置 → App Store → App 下载 → 「始终允许」),不是硬阻断,不必当成不可逾越的红线。

应对:① App Store 描述首屏写明「首次安装建议连接 Wi-Fi(约 420 MB,此后完全离线运行)」; ② 首次启动检测到蜂窝网络时给一次友好提示;③ 绝不采用按需下载——那会直接破坏备案豁免。

5.3 模型转换与打包

# 1) Whisper → Core ML(使用 whisper.cpp 的 models/convert-whisper-to-coreml.py
#    或 HuggingFace 上已转换好的 aniemore/whisper-tiny-coreml)
python convert-whisper-to-coreml.py --model tiny --encoder-only false --output Models/
xcrun coremlcompiler compile WhisperTiny_int8.mlpackage Models/

# 2) Qwen2.5-0.5B → Core ML int4(coremltools + palettization)
python - <<'PY'
import coremltools as ct, torch, numpy as np
from transformers import AutoModelForCausalLM, AutoTokenizer
name = "Qwen/Qwen2.5-0.5B-Instruct"
m = AutoModelForCausalLM.from_pretrained(name, torch_dtype=torch.float16).eval()
tok = AutoTokenizer.from_pretrained(name)
ids = torch.tensor([tok("今天我").input_ids])          # 追踪用样例(长度决定 KV cache)
traced = torch.jit.trace(m, (ids,), strict=False)
mlmodel = ct.convert(traced, inputs=[ct.TensorType("input_ids", shape=(1, ct.RangeDim(1,1024)))],
                     compute_units=ct.ComputeUnit.CPU_AND_NE,
                     minimum_deployment_target=ct.target.iOS18)
op = ct.optimize.coreml.OpPalettizerConfig(mode="kmeans", nbits=4, weight_threshold=512)
mlmodel = ct.compression_utils.palettize_weights(mlmodel, config=op)
mlmodel.save("Qwen25_05B_int4.mlpackage")
PY
xcrun coremlcompiler compile Qwen25_05B_int4.mlpackage Models/

# 3) 声纹 / 环境分类同理(ECAPA 用 torchaudio 导出 ONNX 再转 Core ML)

# 4) 加入 Xcode:拖入 SoundiaryKit/Resources/Models/
#    Target Membership → 勾选 Soundiary(唯一 Target,全球同一份)

5.4 运行时加载与内存

public actor CoreMLBackend: LLMBackend {
    private var model: MLModel?
    public func load() async throws {
        // 首次推理前才加载;加载后常驻,直到收到内存警告
        let cfg = MLModelConfiguration()
        cfg.computeUnits = .cpuAndNeuralEngine      // NE 加速,CPU 兜底
        cfg.allowLowPrecisionAccumulationOnGPU = true
        guard let url = Bundle.main.url(forResource: "Qwen25_05B_int4", withExtension: "mlmodelc")
        else { throw ModelError.bundledModelMissing }
        model = try MLModel(contentsOf: url, configuration: cfg)
    }
    public func unloadOnMemoryPressure() { model = nil }   // 收到 UIApplicationDidReceiveMemoryWarning
}

推理时机:只在充电 + 连接 Wi-Fi(仅指局域网存在与否,不产生请求)或用户主动打开「今日故事」时批量执行;不在采样瞬间跑大模型,避免发热。

5.5 三语提示词

// PromptCatalog.swift —— 提示词必须随界面语言走,否则中文用户拿到英文腔日记
public enum PromptCatalog {
    public static func dailyStory(locale: Locale, snapshots: [String]) -> String {
        switch locale.language.languageCode {
        case .chinese:
            return """
            以下是用户一天的语音转写片段(按时间排序,已去除音频):
            \(snapshots.joined(separator: "\n"))
            请用简体中文写一段 300 字以内的第三人称日记,语气克制温柔,不要臆测未提及的事实。
            """
        case .spanish:
            return """
            Fragmentos transcritos del día (sin audio): ...
            Escribe un diario en tercera persona de menos de 300 palabras, en español, tono sobrio.
            """
        default: // .english
            return """
            Transcribed fragments of the day (audio discarded): ...
            Write a third-person diary under 300 words, in English, restrained tone.
            """
        }
    }
}

6 · 多源融合时间轴

6.1 数据源与对齐

来源API取数方式粒度是否离开设备
声音快照Watch 端本引擎实时8–30s 窗否
心率 / HRV / 静息心率HealthKit(手表端读取)节流查询:心率 20s / HRV 60s / 静息心率 30min单点否
气压Watch CMAltimeter本地气压计实时读数(NSMotionUsageDescription)连续否
备忘录 / 提醒 / 日历 v2 规划无直接 API → 用户手动导出或快捷指令见 6.3条目否

⚠️ 首版只读上表前三行。血压 / 睡眠 / 步数 / 运动均不读(早期文档写过,已按实现删除);位置与照片见文首范围声明,属完整版规划。

6.2 时间轴对齐算法

public struct TimelineBus {
    /// 统一时间基准:所有事件转为 UTC 后按本地时区分桶(桶宽 5 分钟)
    public func fuse(snapshots: [SoundSnapshot],
                     health: [HealthSample]) -> [TimelineBucket] {
        let bucketSize: TimeInterval = 300
        // 1) 各自按 startDate 落入桶
        // 2) 桶内做「跨模态共现」打分:声音×心率
        // 3) 生成自然语言线索,如「19:40 心率 92 + 地铁环境声 → 通勤途中」
    }
}

钟差校正:Watch 与 iPhone 的时间戳分别记录,用 WCSession 激活时的往返做一次偏移估计(通常 < 50ms),记录在 AudioFrame.capturedAt 校正值中。

6.3 备忘录的可行接入方式

📌 iOS 没有「读取备忘录」的公开 API —— 三条路
  1. App Intents + 快捷指令(推荐):提供一个 ImportNotesIntent,用户自己在「快捷指令」App 里配置「每天 22:00 取今日备忘录 → 传给 Soundiary」,全程本地;
  2. 文件导入:用户在备忘录里「存储到文件」→ 用 .fileImporter 导入 .txt,App 解析时间线;
  3. 粘贴板/分享面板:App 注册 NSExtension,从备忘录分享到 Soundiary。

不要尝试私有 API 或读取 ~/Library/Group Containers 的备忘录数据库——必被拒。

7 · 数据模型与保留策略

7.1 SwiftData Schema

@Model final class Day {
    var date: Date                        // 本地日历日(零点由用户时区决定)
    var story: String?                     // LLM 生成的当日故事
    var emotionScore: Double?               // -1.0 ~ 1.0
    var todos: [TodoItem]
    @Relationship(deleteRule: .cascade) var snapshots: [SoundSnapshotEntity]
    var discardedBytes: Int64               // 透明度面板用
    var sensedSeconds: Int
}

@Model final class SoundSnapshotEntity {
    var id: UUID
    var startAt: Date
    var endAt: Date
    var transcript: String                 // 可能为空(纯环境声)
    var envLabel: String                  // quiet / speech / crowd / traffic / music ...
    var loudness: [Float]                  // 每 5s 一个 dBFS,≈ 6 个值
    var voiceprint: Data?                 // 192 × Float32 = 768B,加密存储
    var clusterID: Int?
    var isPartial: Bool                   // 传输中断标记
    // ❗ 没有任何 audioURL / audioData 字段 —— 这是设计约束,不是遗漏
}

@Model final class Speaker {
    var clusterID: Int
    var nickname: String?                 // 用户确认后填写
    var centroid: Data                    // 声纹中心向量(加密)
    var confirmed: Bool = false           // 用户是否已确认身份
    var lastSeen: Date
}

@Model final class Insight {
    var title: String
    var body: String
    var kind: String                      // pattern / knowledge / habit / reminder
    var evidenceDayIDs: [UUID]
    var createdAt: Date
    var isRead: Bool
}

7.2 保留策略(默认,用户可改)

数据默认保留用户可选项存储位置
转写文本90 天30 / 90 / 365 天 / 永久App Group 容器,加密
声纹向量随说话人档案;删除说话人即清除可随时单删同上
响度/环境标签30 天—同上
HealthKit 缓存不落库,仅内存聚合后写入统计值——
音频0 秒(从不写入)不可开启—
洞察 / 故事永久(用户手动删除)—同上

7.3 导出与删除

  • 导出:UIActivityViewController 导出 JSON / Markdown(明文,用户自选保存位置)。导出内容不含任何音频,导出动作本身不联网。
  • 删除:设置 → 数据管理,提供「删除今天 / 删除 30 天前 / 删除全部」。删除时对加密 SQLite 执行 VACUUM 并覆写空闲页,避免残留。
  • 卸载即清空:所有数据存于 App Group 容器(非 iCloud、非 CloudKit),卸载 App 后系统自动清除。需在隐私政策中明确告知。

8 · 存储加密与密钥

层次方案说明
文件级NSFileProtectionComplete(Data Protection Class A)设备未解锁时文件不可读;用户若未设锁屏密码则降级为 B,需在首次启动时检测并提示
数据库级SQLCipher 或 SwiftData + 自定义 ValueTransformer 加密敏感字段不引入第三方库 → 用 CryptoKit 自实现 AES-GCM 字段加密
密钥Secure Enclave 生成 P-256 密钥对;对称密钥用 kSecAccessControlBiometryCurrentSet 封装私钥永不出 Enclave;Face ID 变更即失效
密钥存储Keychain kSecClassGenericPassword + WhenUnlockedThisDeviceOnlyThisDeviceOnly 是关键:iCloud 钥匙串同步会引入 Apple 服务器同步,虽属 Apple 但为审计清晰起见禁用
内存音频缓冲释放前 memset;声纹向量用后即弃Swift String 不可变,敏感文本用 [UInt8] 手动管理
// SecureKeyStore 关键片段
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
    [.privateKeyUsage, .biometryCurrentSet],   // 生物识别变更后密钥自动失效
    nil)!
let attrs: [String: Any] = [
    kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeySizeInBits as String: 256,
    kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,  // 硬件背书
    kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent: true, kSecAttrAccessControl: access]
]
ℹ️ 为何全 App 都没有「用户自填 API Key」

本项目放弃了云端 Provider 与用户自填 Token功能(换来备案豁免最稳 + 维护最轻,见 §0.1),因此不需要存储任何第三方 API Key——密钥体系反而更简单:Secure Enclave 只保护本地数据加密密钥,不存在远程凭据泄露面。

9 · 零联网保障工程化

9.1 允许的网络行为白名单(全 App)

行为目标是否允许依据
系统 Safari 打开政策/支持页用户自有站点✅由系统浏览器发起,App 内不获取内容
任何 App 内 URLSession 请求—❌底线②
崩溃上报 / 分析 / 归因 SDK—❌包括 Firebase、Sentry、AppsFlyer、友盟等
On-Demand Resources / Background Assets 下载模型Apple CDN❌虽连 Apple,但会被认定为「运行时下载内容」,风险高
iCloud / CloudKit 同步Apple谨慎建议关闭;若开启需在隐私标签声明,且可能被认定提供网络服务
App Store 更新检查 / 评分弹窗Apple✅(系统行为)使用 SKStoreReviewController,不自建

9.2 抓包自检流程(提交前必做)

1
准备:iPhone 与 Mac 同 Wi-Fi,Mac 上跑 Charles / mitmproxy,iPhone 安装并信任根证书。
2
冷启动 → 走完首次引导 → 开启感知 → 静默 30 分钟 → 查看今日故事 → 导出,全程录包。
3
判定:App 运行时不得有任何 TLS 连接(付费下载的付款在下载阶段由 App Store 在 App 之外完成,运行时不存在 StoreKit 通信)。
4
归档:把 .chls 文件与「0 第三方请求」结论写入提交包,作为豁免申请的佐证材料。
✅ 透明度面板(App 内可见,也是最好的营销素材)
今日已感知            8 小时 12 分
已丢弃原始音频        4.2 GB
本地占用              48 KB
第三方网络请求        0 次
最后联网行为          无(App 运行时零联网)

10 · 三语本地化工程

10.1 语言与区域

语言代码基准App Store 本地化说明
简体中文zh-Hans✅ 基准语言中国大陆基础语言,必须提供
英语en—美国 / 英国 / 澳大利亚 / 加拿大—
西班牙语es—西班牙 / 墨西哥 / 拉美注意 es-ES 与 es-419 用词差异,先做一套中性拉美西语,后续拆分

10.2 目录与命名

SoundiaryKit/Resources/Localization/ ├── zh-Hans.lproj/ │ ├── Localizable.strings // 界面文案 │ ├── InfoPlist.strings // 权限描述(NSMicrophoneUsageDescription 等) │ └── Prompts.strings // LLM 提示词(关键!随语言切换) ├── en.lproj/ … └── es.lproj/ …

10.3 易错点清单

问题说明处理
提示词未本地化界面是中文,模型却用英文提示词 → 日记带英文腔Prompts.strings 三语 + 单元测断言「输出语言 == 界面语言」
西语文案溢出西语平均比英语长 20–25%SwiftUI 用 .minimumScaleFactor(0.8) + fixedSize 禁用;截图逐语言目视检查
日期/数字格式zh 用「10月11日」,es 用「11 oct」,en 用「Oct 11」一律用 Formatter / .formatted(),禁止手写拼接
复数es/en 有复数规则,zh 无使用 .stringsdict
ASR 语言设定Whisper 需指定语言跟随系统首选语言顺序;不支持的语言回退英文
权限文案InfoPlist.strings 需按语言本地化,否则审核员看到英文描述中文 App三语齐全
App Store 元数据标题/副标题/关键词/描述/宣传文本需各语言单独填中文关键词注意逗号分隔无空格

11 · Info.plist 与权限清单

11.1 iOS(iPhone Target)

Key值(zh-Hans / 建议)用途必需
NSMicrophoneUsageDescriptionSoundiary 通过 Apple Watch 感知环境声音,用于生成当日故事。声音仅在设备内存中实时处理,不会保存为录音文件。麦克风。实际录音发生在 Watch 端(iPhone target 零 AVAudioEngine 引用,本机不会弹框);键保留是因为麦克风是产品核心能力,删了将来任何 iPhone 侧录音都会直接崩✅
NSHealthShareUsageDescription / NSHealthUpdateUsageDescription不声明iPhone target 零 HK* 引用 —— 健康数据全部由手表端读取、手表自己弹框;iPhone 只经 WatchConnectivity 收体征值。若将来看见这两条,见 SoundiaryApp/Info.plist 注释里的恢复说明—
NSSpeechRecognitionUsageDescription在设备上把声音转成文字,用于生成日记。全部在您的设备内完成。Speech 框架(iPhone 侧转写与自检都要)必填
NSAppTransportSecurity不声明(App 内不存在任何网络请求,连本地网络例外也不设)网络—
UIBackgroundModes不声明(iOS 端无后台音频需求;watchOS 侧见 §11.2)后台—

⚠️ 本地通知(UNUserNotificationCenter)不需要用途描述键 —— 早期文档列的 NSUserNotificationsUsageDescription 不存在也不需要。

11.2 watchOS(Watch App Target)

Key值注意
NSMicrophoneUsageDescriptionSoundiary 每隔几分钟短暂采样一段声音,并立即转成文字。原始音频默认不会被保存(主动录音保存的文件仅存本机)。Watch 与 iOS 各自声明
NSHealthShareUsageDescription读取心率与心率变异性,为时间轴补充身体信号并估算压力。数据只留在您的设备上,绝不上传。只读心率 / HRV / 静息心率,不读睡眠
NSHealthUpdateUsageDescription开启连续采样期间启动一次体能训练会话,让 watchOS 在后台保留麦克风。停止后会丢弃该会话,不会在「健身记录」里留下训练。不能少:授权里声明了「写入体能训练」,缺这条 startActivity 直接抛错
NSMotionUsageDescription读取手表的气压计,为时间轴补充气压读数与天气趋势。不记录任何运动行为,也不上传任何数据。气压计属「运动与健身」,缺了会崩
WKBackgroundModes不声明 audio(该模式在 watchOS 上不向第三方开放)声明会直接导致审核被拒

11.3 HealthKit 授权请求代码要点

// 手表端实际请求的范围(WatchSensingEngine.BodySignals + WorkoutKeepAlive)
let toRead: Set<HKObjectType> = [
    HKObjectType.quantityType(forIdentifier: .heartRate)!,
    HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!,
    HKObjectType.quantityType(forIdentifier: .restingHeartRate)!
]
let toShare: Set<HKSampleType> = [HKObjectType.workoutType()]  // 仅为后台执行权,结束即 discardWorkout()
try await healthStore.requestAuthorization(toShare: toShare, read: toRead)
// ⚠️ 审核红线:HealthKit 数据不得用于广告、数据经纪、不得写入 iCloud、不得披露给第三方
// ⚠️ 血压 / 睡眠 / 步数 / 运动一律不读 —— 权限范围与 §6.1 数据来源表严格一致

12 · 知情同意流程

12.1 四步引导(首次启动)

⚠️ 状态说明(2026-10-15 核对):这四步目前是设计稿,代码中尚未实现——iPhone 端搜不到任何首次启动引导页(无 onboard / firstLaunch / walkthrough 相关实现)。因此 不要把「四步引导」「逐项可关的同意流程」写进 App Store 描述,否则属虚假宣称。当前真实的授权流程是:Watch 端采样时由系统弹窗逐项询问麦克风与 HealthKit,用户可逐项拒绝;设置页常驻感知状态开关与「今日透明度」面板。

#页面核心文案(zh-Hans)强制动作
1这是什么Soundiary 用 Apple Watch「听」你的一天,再把它写成故事。它不录音,你的声音不会被保存。继续
2声音怎么被处理手表每 3–5 分钟采集 6–8 秒声音 → 立刻转成文字 → 声音立即丢弃。全程在你自己的设备上完成,没有任何服务器参与。勾选「我已理解」
3权限麦克风 / 健康。每一项都独立可关。逐项授权
4开始你可以随时在设置里停止感知、导出或删除全部数据。启动

12.2 持续可见的同意状态(PIPL / GDPR 要求「可撤回」)

  • 设置页顶部常驻「感知状态」开关,一键停止且立即清零缓冲;
  • 每次采样窗开始时,Watch 端可显示极轻量的状态点(可选,用户可关);
  • 通知栏常驻(可选):「Soundiary 正在感知 · 点击停止」——iOS 26 支持持续通知;
  • 任何时刻进入 App 都能看到今天的透明度面板(§9)。
🚨 涉及他人:必须在 UI 与政策中写明

录音(即使是加工成文本)会采集到第三方(家人、同事、路人)的语音。PIPL 第十三条要求处理个人信息需取得同意,而用户对他人声音无法取得同意。工程上的缓解措施:

  1. 默认在「单人环境」标签下才生成含说话内容的文本;检测到多人会话时,只保留「多人交谈 · N 人」的抽象标签,不转写内容;
  2. 提供「隐私模式」:在任何非白名单地点(如办公场所、他人住所)自动降为环境标签模式;
  3. 隐私政策明确告知用户:你有责任在开启感知前告知同处一室的人,App 提供一键生成的告知卡片。

13 · 付费下载配置(无内购)

✅ Soundiary 是付费 App,App 内没有支付代码

本 App 采用付费下载(Paid App)模型:用户在 App Store 付费后下载,下载即完整版。因此不存在任何内购项目,代码里也没有 import StoreKit、没有 IAPManager、没有解锁状态、没有权益页、没有「恢复购买」按钮。

13.1 定价配置(在 App Store Connect 中设置,不在代码里)

字段值
App 定价方式付费(Paid),不是免费
中国区价格¥1(最低档)
美国区价格$0.99(见方案文档 §13.1)
其他区各区最低档
家庭共享建议开启(提升转化)
内购项目无。整个「App 内购买」菜单跳过
订阅无。不创建任何订阅组、不设置自动续费
⚠️ 定价不可逆

App 一旦设为免费,无法再改回付费。提交前务必确认定价档位。

13.2 换设备后如何「恢复」

付费 App 不需要 App 内的「恢复购买」按钮,也因此不触发 Guideline 3.1.1(该条款只针对 App 内购买)。换设备后:

  1. 用原 Apple ID 打开 App Store;
  2. 进入「已购项目 / Purchases」;
  3. 找到 Soundiary,点云下载图标重新下载——无需重复付费。

恢复流程完全由 Apple 侧处理,App 不参与、不存储任何支付凭据。

14 · 备案豁免提交包

14.1 提交路径

  1. 登录 App Store Connect → 页面底部 「联系我们 / Contact Us」;
  2. 依次选择:App Store 分发 → 其他疑问 / Additional Questions;
  3. 在附件中上传《不适用备案说明函》(PDF,含 Team ID 与手写签名);
  4. 正文写明:App 名称、Apple ID、Bundle ID、Team ID、中国区上架诉求、以及「本 App 不提供互联网信息服务」的事实依据。

14.2 说明函模板(可直接复制打印)

📄 关于 Soundiary 不适用移动互联网应用程序备案的说明函

致:苹果公司(Apple Inc.)App Store 审核团队

开发者:[开发者主体全称] Apple Developer Team ID:[XXXXXXXXXX]
应用名称:Soundiary Bundle ID:com.soundiary.app

本函就上述应用在中国大陆地区分发所涉《移动互联网应用程序信息服务管理规 定》项下的备案要求,作如下事实说明与承诺:

一、无互联网信息服务提供行为。本应用为纯本地工具类应用。全部数据采集、 加工、存储均在用户设备本地完成。开发者不运营、不托管、不部署任何用于 接收、存储、处理或分发用户数据的后端服务器。

二、无用户内容与社区功能。本应用不提供用户注册、登录、账号体系、用户 社区、内容发布、评论、转发、信息流、即时通讯、直播、搜索等任何互联网信 息服务功能。应用内不存在任何可由用户上传并由开发者分发的内容。

三、网络行为。本应用为付费下载,付款在下载阶段由 App Store 在应用之外完成,应用运行时不发起任何网络连接。唯一的联网行为是(1)通过系统 Safari 浏览器打开本应用隐私政策与技术支持网页,该行为由系统浏览器发起,应用内不获取、不渲染任何网络内容。除上述外,本应用不存在任何网络连接行为。

四、无第三方 SDK。本应用未集成任何第三方软件开发工具包,包括但不限于 统计分析、崩溃上报、广告归因、推送、登录分享类 SDK。

五、数据处理承诺。本应用采集的声音信息仅用于设备内实时转写,原始音频 在设备本地处理完毕后立即删除,不进任何长期存储、不生成任何可回放的录音 文件;仅当需要在本人的 Apple Watch 与 iPhone 之间传输时,短暂生成一个 受系统保护的临时文件,投递有结果即删除,用户亦无法回放;心率、HRV 与静息 心率等健康数据仅用于时间轴对齐,全部留存于本机。本应用不收集、不上传、 不共享、不出售任何个人信息。

综上,本应用不属于《移动互联网应用程序信息服务管理规定》所规制的「互联 网信息服务提供者」,恳请贵司依据相关规定与既有实践,豁免本应用在中国大 陆地区的备案号填报要求。

本函所述内容真实、准确、完整。如与事实不符,开发者愿承担由此产生的全部 法律责任,并接受贵司依据开发者计划许可协议采取的处理措施。

开发者(签名):____________________ 日期:______年____月____日
[加盖开发者主体公章]

14.3 提交前自检(全部 ✅ 才可提交)

  • 抓包报告显示 0 次第三方请求(含冷启动、后台、IAP 全流程)
  • 二进制符号扫描通过(§2.3 脚本 4 项全绿)
  • 无第三方 SDK(检查 Pods/、SPM 依赖、Frameworks/ 为空)
  • 隐私政策已上线且 URL 可访问(HTTPS)
  • App 隐私标签填写为「未收集数据」或仅 Apple 支付相关(见 §15)
  • App 内无「登录 / 注册 / 账号」字样
  • App 内无用户生成内容入口(无评论、无分享社区、无反馈上传附件)
  • 技术支持 URL 与隐私政策 URL 均已填写且有效
🚨 被驳回的两个常见原因

① 包名/主体不一致:说明函中的 Bundle ID、Team ID 必须与 ASC 后台完全一致。
② 隐私政策里有「我们可能收集……」这类兜底措辞:审核员会直接判定存在收集行为。政策必须绝对化表述("不收集" 而非 "不主动收集")。

15 · App Store 隐私标签填写对照

ASC → App 信息 → App 隐私 → 按下列填写。

数据类型本应用是否收集用途是否关联身份是否用于追踪
联系人否———
健康与健身否(仅本地读取并立即聚合)———
财务信息否(支付由 Apple 处理)———
位置否(App 不使用定位)———
敏感信息否———
联系方式否———
用户内容否(不离开设备、不上传)———
浏览历史否———
搜索历史否———
标识符否———
购买记录由 Apple 处理App 功能(解锁)否(开发者不可见)否
使用情况数据否———
诊断否(无崩溃 SDK)———

✅ 目标状态:「此 App 未收集数据」(Data Not Collected)。这是最有说服力的隐私标签,也是豁免申请的有力佐证。

ℹ️ 如果 ASC 强制要求至少勾一项

勾选 「购买记录 → App 功能(下载)→ 不关联身份 → 不用于追踪」,并在备注中说明「付费下载的购买由 Apple 在 App Store 侧处理,开发者不获取」。切勿为了「看起来合规」而勾选健康/音频数据——那会被认定为存在收集行为,直接导致豁免失败。

16 · 测试与验收

16.1 测试矩阵

类别用例验收标准
功能采样窗触发、人声延长、体征节拍窗节奏符合档位设定(入睡检测已于 2026-10-15 取消,不纳入测试)
中断手表后台被挂起、iPhone 远离、蓝牙断开、来电打断快照标记 partial,不崩溃、不丢当天叙事
电量连续 8 小时标准档Watch 耗电增量 ≤ 20%(Series 9 实测)
转写中/英/西三语各 200 条样本CER / WER ≤ 20%(Whisper-tiny 合理区间)
声纹5 人 × 各 20 段聚类纯度 ≥ 0.8,误合并率 < 10%
隐私抓包、符号扫描、文件系统快照0 第三方请求;Documents 目录无音频文件
本地化三语 × 全部界面 + 截图无截断、无硬编码英文、日期格式正确
降级低端设备(iPhone 11 / Watch SE)内存压力自动卸载模型 → 模板兜底,不 OOM
定价付费下载、换机重新下载App Store「已购项目」可重新下载,无需重复付费

16.2 隐私回归测试(每次发版必跑)

func testNoAudioArtifactsOnDisk() throws {
    let urls = try FileManager.default.contentsOfDirectory(
        at: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0],
        includingPropertiesForKeys: nil)
    let audio = urls.filter { ["m4a","caf","wav","mp3","opus"].contains($0.pathExtension) }
    XCTAssertEqual(audio.count, 0, "磁盘上出现音频文件,违反底线①")
}

func testNoNetworkBackendExists() throws {
    // 单一 App:不存在任何云端后端,连枚举 case 都不该有。
    // 由 CI 脚本 audit_offline.sh 在构建期扫描 __TEXT 符号表保证。
    XCTAssertEqual(ModelHub.availableBackends.count, 3,
                   "只允许 foundationModels / coreMLBundled / templateFallback 三个后端")
}

17 · 发布清单

17.1 上架前(T-14 天)

  • 隐私政策三语上线(HTTPS,需含「生效日期」「联系邮箱」)
  • 技术支持页三语上线
  • 《不适用备案说明函》PDF 完成并签名盖章
  • 抓包报告 + 符号扫描报告归档
  • App 图标 1024×1024(无 alpha、无圆角)
  • 三语宣传图各 6 屏 iPhone(1290×2796)+ 2 屏 Watch(410×502)
  • 三语元数据:标题(≤30)、副标题(≤30)、关键词(≤100)、描述、宣传文本
  • 年龄分级问卷填写(注意:涉及健康数据,如实填写)
  • 出口合规(加密导出):使用免申报例外(5.D.992.c)——仅使用标准加密且不提供网络安全功能
  • IAP 商品审核通过

17.2 提审后

  • 若收到「需备案号」驳回 → 回复工单并附说明函 PDF(同一工单内追加,不要新建)
  • 若收到 5.1.1(数据收集最小化)→ 附上隐私政策链接与透明度面板截图
  • 若收到 4.2(最低限度功能)→ 演示完整日流程录屏(感知→故事→洞察)
  • 记录每次驳回模板与应对话术,形成团队知识库

17.3 版本记录

版本日期主要变更
Eng Spec 1.02026-10-11首版。确立采集即弃架构、单一 App 零联网、端侧模型随包分发、三语工程与备案豁免提交包