# 双端本机数据格式与兼容

本页记录任务、网站、记忆、附件和本机授权状态在当前源码中的形状。示例均为合成值，只用于解释字段；不提供读取用户私有数据或导出密钥的操作。原生磁盘格式是实现细节，没有通用跨平台导入承诺，修改前应保留旧数据兼容和失败保护。

## 数据分类

| 类别 | iOS | Android |
| --- | --- | --- |
| 工作台 | `workspace.json`，JSON 对象快照 | SharedPreferences 名称 `starline_settings` 中的 JSON 字符串 |
| 记忆索引 | 逻辑文件 `vault/index.enc`，解密后为记录数组 | `memories.bin`，解密后为 JSON 数组 |
| 记忆附件 | `vault/files/<UUID>.enc` | `memory-<id>.bin` |
| 聊天会话 | 独立 Keychain 项 | `session.bin`，解密后为会话字符串 |
| 原生登录 pending | Keychain 内 JSON | `native-auth.bin`，解密后为 JSON |
| 开关与默认页 | UserDefaults / `@AppStorage` | 同一个私有 SharedPreferences |

这里的文件名是程序中的逻辑名称，不表示部署目录。任务、收藏和普通偏好不使用记忆 AES-GCM 容器；聊天 token、记忆正文和附件不能写回普通偏好或日志。记忆密钥为设备级随机值，同一安装中的聊天账号共用记忆库；改 Account 密码、退出或切换账号不会删除或重加密记忆。

## iOS 工作台快照

`LocalWorkspaceStore.Snapshot` 的三个顶层键是 `tasks`、`bookmarks`、`recentSites`。`tasks` 解码时必需；旧快照缺少后两项时按空数组兼容。当前没有单独的 schemaVersion。示例：

```json
{
  "tasks": [{
    "id": "11111111-1111-4111-8111-111111111111",
    "title": "示例任务",
    "notes": "合成说明",
    "dueDate": 3600,
    "sourceURL": "https://example.test/task",
    "isCompleted": false,
    "createdAt": 0,
    "updatedAt": 0
  }],
  "bookmarks": [{
    "id": "22222222-2222-4222-8222-222222222222",
    "title": "示例网站",
    "url": "https://example.test/",
    "createdAt": 0
  }],
  "recentSites": []
}
```

默认 `JSONEncoder` 把 Swift `Date` 编为从 2001-01-01 00:00:00 UTC 起算的秒数，可含小数；这里的 0 不是 1970 年，3600 也不是 Unix 毫秒。`URL` 编为字符串，UUID 编为字符串。可选 `dueDate`、`sourceURL` 为空时通常省略键。后端聊天时间使用 Unix 毫秒，不能把相同数字直接复制到本机 Date 字段。

快照一次编码三个数组，`.atomic` 写文件成功后再替换 Store 内存。损坏或缺少必需字段会抛出读取错误，根界面不会把它当新安装。它没有单记录日志、数据库事务回放或云端副本。具体筛选与网址规范化见[工作台](https://document.thestarline.cn/app/workbench.md)。

## iOS 记忆明文模型

加密索引解开后是 `[MemoryEntry]`。医疗示例只填写一个自定义输入值：

```json
{
  "id": "33333333-3333-4333-8333-333333333333",
  "title": "合成医疗记录",
  "text": "用于格式说明",
  "attachments": [{
    "id": "44444444-4444-4444-8444-444444444444",
    "name": "sample.txt",
    "storedName": "44444444-4444-4444-8444-444444444444",
    "byteCount": 6,
    "isImage": false
  }],
  "createdAt": 0,
  "updatedAt": 0,
  "kind": "health",
  "fields": {"healthSubject": "合成对象"}
}
```

| 字段 | 类型与语义 |
| --- | --- |
| `id` | UUID，编辑保持不变 |
| `title`、`text` | 字符串；正文名是 text，不能套用 Android 的 body |
| `attachments` | 元数据数组，附件正文另存；`byteCount` 是原始字节数 |
| `createdAt`、`updatedAt` | Swift Date 默认参考日期秒数 |
| `kind` | `note/health/visa/document/appointment/subscription` |
| `fields` | 字符串到字符串字典，日期和数字型表单值仍是字符串 |
| `generatedSchema` | 可选主题对象：`category`、`icon`、`fields`；每个生成字段含 `id/title/type/options` |

生成字段 `type` 可为 `text/choice/date/dateTime/number`。表单日期为 `yyyy-MM-dd`，带时间为 `yyyy-MM-dd'T'HH:mm`，不含时区；不能等同为 UTC 瞬间。旧记录缺少 kind 时取 note，缺少 fields 时取空字典；旧生成 schema 缺 icon 时按主题选择兼容图标。保存层只接受当前模板或生成 schema 允许的字段 ID，因此未知字段不会自动透传到新 iOS 记录。

## iOS vault 封装与密钥项

当前容器是 ASCII `SLMV2` 头加 CryptoKit `AES.GCM.SealedBox.combined`。combined 含 nonce、密文与认证标签；正文与索引不采用 JSON 内 Base64 封装。索引与附件使用同一随机 256 位密钥，但附加认证数据 AAD 分别是 UTF-8 的 `index` 与 `attachment:<附件UUID的uuidString>`，UUID 的具体字符串必须与保存时一致。换附件名不改变其绑定 ID；拿另一附件的密文替换而不匹配 purpose 会认证失败。

Keychain service 为 `cn.thestarline.workbench.memory`，account 为 `device-vault-v2`，属性为 `WhenUnlockedThisDeviceOnly`。这些名字不是密钥值，也没有基于账号密码的密钥推导或在线找回。正常写入使用 complete 文件保护；读取需设备安全存储与文件共同可用。文件头提供格式识别，真正的内容完整性由 GCM 认证负责。

## Android JSON 与加密容器

工作台偏好键 `tasks/bookmarks/recents` 对应 JSON 数组字符串。任务字段常见为 `id/title/notes/isCompleted/createdAt`，编辑时增加 `dueAt/updatedAt`；网址任务还含 `sourceURL`。日期是 `System.currentTimeMillis()` 的 Unix 毫秒，`dueAt=0` 或缺失表示无到期。收藏和历史含 `id/title/url/createdAt`。数组的元素是 JSON 对象，不是 Swift Snapshot。

Android 医疗记忆的真实形状为：

```json
{
  "id": "55555555-5555-4555-8555-555555555555",
  "title": "合成医疗记录",
  "body": "用于格式说明",
  "kind": "health",
  "createdAt": 978307200000,
  "updatedAt": 978307200000,
  "healthSubject": "合成对象",
  "attachments": [{
    "id": "66666666-6666-4666-8666-666666666666",
    "name": "sample.txt",
    "mimeType": "text/plain",
    "size": 6
  }]
}
```

正文为 `body`，模板字段直接位于记录对象中，没有 iOS 的 `fields` 外层。类型包含 `general/pet/health/visa/document/appointment/subscription`，本机通用推荐使用 `suggested` 加 `suggestedCategory`；旧 `travel/calendar` 仍可显示。附件使用 `size`、`mimeType`，没有 iOS 的 `byteCount/isImage/storedName`。`JSONObject` 的键顺序不是契约，未知历史键可能存在，不能按示例重建并丢弃其他字段。

`SecureFile` 从 Android Keystore 读取或生成别名 `starline_android_data_v1` 的 AES-256 密钥，使用 `AES/GCM/NoPadding`。磁盘字节为 `IV || ciphertext-and-tag`，读取按前 12 字节 IV、128 位 GCM 标签处理，至少 28 字节；当前没有 SLMV2 头，也没有 `updateAAD` 的索引／附件 purpose 绑定。格式上不与 iOS 互通，不能通过换后缀或复用 iOS key 读取。

写入先保存到同名 `.tmp` 文件，调用文件描述符同步，再重命名替换。这个流程不意味着多份附件与索引构成完整数据库事务。加密数组读失败会抛“原文件已保留”，`AppState` 将记忆设为 locked；普通工作台数组解析失败则回退空数组，两者保护级别不同。

## 偏好、会话与 pending

| 内容 | iOS 键或结构 | Android 键或结构 |
| --- | --- | --- |
| 默认页面 | `workbench.defaultTab` | `default_tab` |
| 底部记忆入口 | `workbench.memoryInTabBar` | `show_memory_tab` |
| 页面扫码开关 | `workbench.scanOnChat/scanOnHome/scanOnProfile` | `scan_on_chat/scan_on_work/scan_on_profile` |
| 聊天安全存储 | service `cn.thestarline.workbench.chat`；account `device-session` | 加密 `session.bin` |
| 登录 pending | `codeVerifier/state/expiresAt/apiBaseURL/ticket?` | `verifier/state/expiresAt` |

聊天 Keychain 的可访问属性为 `AfterFirstUnlockThisDeviceOnly`，与记忆密钥的 WhenUnlocked 不同。pending 的 expiresAt 两端都是协议 Unix 毫秒，不能套用 Swift Date 的默认 JSON 日期规则。Android 不持久化换票 ticket，也没有 pending API 基址字段；恢复边界详见[客户端生命周期](https://document.thestarline.cn/app/client-lifecycle.md)。本页不展示任何实际 token、verifier 或密钥。

## 旧数据迁移与失败处理

iOS 检测旧 `memories.json` 时先加载旧记录，在暂存 vault 中逐附件加密并回读验证，再写入并验证加密索引，随后移动成正式 vault，最后清理旧明文。旧附件 `storedName` 必须是安全单段文件名，不能包含路径穿越。清理旧明文失败有独立错误，不应描述为所有数据都已安全清理。

已有加密索引时不会为了读取失败而生成新密钥；出现无有效索引的残留 vault、认证失败或缺失密钥会留下错误状态，禁止覆盖。附件删除使用尽力清理，不能从索引删除成功推断闪存物理擦除。Android 兼容宠物和签证旧字段别名；同主题编辑保留未展示旧键，切换主题则明确替换相关字段。原子索引写入、旧格式解析和字段迁移需分别测试。

## 解析与扩展检查顺序

1. 先确认数据类别。一个带有相同记录标题的工作台任务和记忆没有共享 ID 或自动关联；一个 Base64 字符串也不能据此判断是密钥、附件还是密文。按容器与字段定义解释，不猜测内容。
2. 确认时间单位、基准和是否含时区。Swift 快照参考日期秒数、Android Unix 毫秒、协议 expiresAt 和表单日期分属不同规则。转换测试至少覆盖参考日期、非整秒、缺失截止、设备时区变化和日期型空值。
3. 确认空值策略。iOS 的可选键可省略，但必需数组或 UUID 损坏会使解码失败；Android `optString/optLong/optBoolean` 可能给出空串、0 或 false。缺键不应自动解释为用户主动清空，也不能用默认值覆盖已经读错的数据。
4. 确认元数据与文件一致。附件数、ID 和原字节大小应吻合；扩展名是名称信息，不能替代 MIME、图片解码或 GCM 认证。转出包附带的原始字节需要按记录关联；磁盘 vault 只靠元数据不能恢复丢失密钥。
5. 用独立合成夹具验证旧版读取、新版保存、进程重开和错误保护。添加字段时明确默认值和未知字段保留策略，不能将 Android 的宽松 JSON 行为推到 Swift Codable；改变 AAD 或密钥别名须有明确版本迁移，不能只改字符串。

检查成功也只说明该类夹具符合当前解析器，不构成通用导入器、跨端转换器或断电恢复保证。当前没有自动修复受损 vault 的流程；应保留原文件和可复核错误，避免直接编辑加密字节、复制安全存储、生成新密钥或用空库覆盖作为修复。

代码入口包括 iOS `LocalWorkspaceStore`、`MemoryStore`、`MemoryVaultCrypto`、`MemoryGeneratedSchema` 与 Android `AppData`、`MemoryScreens`、`MemoryTemplates`。2026-10-23 本轮仅静态核对，未读取用户磁盘数据、运行迁移或执行产品测试。
