feat(storage): migrate Life OS cloud state to versioned records
This commit is contained in:
1 parent
6972a00445
commit
883ab1f504
17 files changed
+502
-72
No files matched your search
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
本项目按 `v主版本.次版本.修订版本` 标记发布。下列内容以对应 Git tag 和已提交功能为准。
|
本项目按 `v主版本.次版本.修订版本` 标记发布。下列内容以对应 Git tag 和已提交功能为准。
|
||||||
|
|
||||||
|
## v1.13.0 — 2026-09-28
|
||||||
|
|
||||||
|
- D1 新增按记录存储的记忆、标签、目标、待办和收藏表;资料与设置保留 JSON。
|
||||||
|
- 用户首次访问时从旧状态表回填数据,旧表保留为只读迁移来源;浏览器备份格式仍为 Schema v2。
|
||||||
|
- 云同步改为逐条提交与版本校验;跨设备修改同一记录时保存本地冲突副本并让用户选择。旧版整份状态写入接口停止接收写入。
|
||||||
|
- 增加 SQLite 集成验证,覆盖回填、标签更新、删除标记、版本冲突和用户隔离。
|
||||||
|
|
||||||
## v1.12.0 — 2026-09-28
|
## v1.12.0 — 2026-09-28
|
||||||
|
|
||||||
- 目标与待办可在终端逐项创建;记忆、目标、待办可先选择记录,再输入 `yes` 确认删除。
|
- 目标与待办可在终端逐项创建;记忆、目标、待办可先选择记录,再输入 `yes` 确认删除。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。
|
以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。
|
||||||
|
|
||||||
在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.12.0** · [更新日志](CHANGELOG.md)
|
在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.13.0** · [更新日志](CHANGELOG.md)
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
|
|
||||||
## 数据与备份
|
## 数据与备份
|
||||||
|
|
||||||
站点使用 ChatGPT 登录标识隔离不同用户的数据。登录后,资料通过同源 API 保存到 Sites 的 D1 数据库;浏览器 `localStorage` 充当缓存、草稿及首次迁移来源。首次云端恢复前,本地资料会保留一份迁移前副本。目标与任务存于 `lifeos_system.tasksBoard`,记忆及其可选信息存于 `lifeos_memories`。
|
站点使用 ChatGPT 登录标识隔离不同用户的数据。登录后,资料通过同源 API 保存到 Sites 的 D1 数据库;浏览器 `localStorage` 充当缓存、草稿及首次迁移来源。首次云端恢复前,本地资料会保留一份迁移前副本。浏览器缓存与 JSON 备份仍使用 `lifeos_system.tasksBoard`;云端将其中的目标和待办拆分为独立记录。
|
||||||
|
|
||||||
**系统设置 → 导出数据** 会生成 JSON 备份;导入支持合并或覆盖。备份格式使用 `schemaVersion: 2`,仍可导入 v1 备份。壁纸来源、快捷方式、开机 App 和小部件开关包含在同步与备份中;窗口布局与小部件格位分别保存在本设备的 `lifeos_window_layout_v1`、`lifeos_widget_layout_v1`,也包含在 JSON 导出中,覆盖导入时可恢复。未保存的今日草稿仅留在本机。
|
**系统设置 → 导出数据** 会生成 JSON 备份;导入支持合并或覆盖。备份格式使用 `schemaVersion: 2`,仍可导入 v1 备份。壁纸来源、快捷方式、开机 App 和小部件开关包含在同步与备份中;窗口布局与小部件格位分别保存在本设备的 `lifeos_window_layout_v1`、`lifeos_widget_layout_v1`,也包含在 JSON 导出中,覆盖导入时可恢复。未保存的今日草稿仅留在本机。
|
||||||
|
|
||||||
主要数据键:`lifeos_memories`、`lifeos_museum`、`lifeos_profile`、`lifeos_settings`、`lifeos_system`。D1 的初始迁移位于 `drizzle/0000_lifeos_cloud_state.sql`:它将各类数据作为 JSON 字段保存在每个用户的一行记录中;应用的备份 schema 版本与数据库迁移编号是两回事。建议定期导出备份。
|
主要本地数据键:`lifeos_memories`、`lifeos_museum`、`lifeos_profile`、`lifeos_settings`、`lifeos_system`。云端表结构见 `drizzle/0001_lifeos_records.sql`:记忆、标签、目标、待办、收藏各自按记录保存,资料与设置保留按用户存储的 JSON。旧版 `lifeos_states` 作为只读迁移来源保留;用户首次访问新版本时自动回填新表,旧表不会被清空。单条记录的修改使用版本号校验;冲突时会保存本地副本并要求选择。旧版浏览器标签页需刷新,整份状态写入接口已停用,避免覆盖新数据。应用的备份 `schemaVersion: 2` 与数据库迁移编号是两回事。建议定期导出备份。
|
||||||
|
|
||||||
## 构建与验证
|
## 构建与验证
|
||||||
|
|
||||||
@@ -42,9 +42,10 @@
|
|||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
npm run validate
|
npm run validate
|
||||||
|
npm run test:cloud
|
||||||
```
|
```
|
||||||
|
|
||||||
`build` 将页面及 App 脚本打包成 `dist/server/` 中的 Cloudflare Worker 产物;`validate` 检查产物、双语键与主要交互。生产托管配置在 `.openai/hosting.json`,含 Sites 项目 ID 和 D1 逻辑绑定。直接在其他环境部署时,需要提供兼容的 Worker 环境与 `DB` 绑定;云端 API 还依赖托管层提供的 `oai-authenticated-user-id` 请求头。
|
`build` 将页面及 App 脚本打包成 `dist/server/` 中的 Cloudflare Worker 产物;`validate` 检查产物、双语键与主要交互;`test:cloud` 使用内存 SQLite 检查旧数据回填、版本冲突与租户隔离。生产托管配置在 `.openai/hosting.json`,含 Sites 项目 ID 和 D1 逻辑绑定。直接在其他环境部署时,需要提供兼容的 Worker 环境与 `DB` 绑定;云端 API 还依赖托管层提供的 `oai-authenticated-user-id` 请求头。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -61,8 +62,9 @@ public/desktop-widgets.js 吸附网格与桌面小部件
|
|||||||
public/addons.js 导入导出、云同步与辅助操作
|
public/addons.js 导入导出、云同步与辅助操作
|
||||||
public/locales/ 中文、英文文案
|
public/locales/ 中文、英文文案
|
||||||
public/service-worker.js PWA 缓存
|
public/service-worker.js PWA 缓存
|
||||||
worker/index.js Worker 路由、数据 API、必应图片代理
|
worker/index.js Worker 路由与必应图片代理
|
||||||
drizzle/ D1 初始迁移
|
worker/cloud-state.js 云端记录迁移与同步 API
|
||||||
|
drizzle/ D1 数据表迁移
|
||||||
scripts/ 构建与产物验证
|
scripts/ 构建与产物验证
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
CREATE TABLE lifeos_migrations (
|
||||||
|
user_id TEXT PRIMARY KEY,
|
||||||
|
migrated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE lifeos_meta (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
kind TEXT NOT NULL CHECK (kind IN ('profile', 'settings', 'system')),
|
||||||
|
value_json TEXT NOT NULL,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, kind)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE lifeos_memories (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
memory_date TEXT,
|
||||||
|
value_json TEXT NOT NULL,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
deleted_at TEXT,
|
||||||
|
PRIMARY KEY (user_id, id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_lifeos_memories_user_date ON lifeos_memories(user_id, memory_date DESC) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE lifeos_memory_tags (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
memory_id TEXT NOT NULL,
|
||||||
|
tag TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (user_id, memory_id, tag),
|
||||||
|
FOREIGN KEY (user_id, memory_id) REFERENCES lifeos_memories(user_id, id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_lifeos_memory_tags_user_tag ON lifeos_memory_tags(user_id, tag);
|
||||||
|
CREATE TRIGGER lifeos_memory_tags_insert AFTER INSERT ON lifeos_memories
|
||||||
|
WHEN NEW.deleted_at IS NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT OR IGNORE INTO lifeos_memory_tags(user_id, memory_id, tag)
|
||||||
|
SELECT NEW.user_id, NEW.id, value FROM json_each(NEW.value_json, '$.tags')
|
||||||
|
WHERE type = 'text' AND value <> '';
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER lifeos_memory_tags_update AFTER UPDATE OF value_json, deleted_at ON lifeos_memories
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM lifeos_memory_tags WHERE user_id = NEW.user_id AND memory_id = NEW.id;
|
||||||
|
INSERT OR IGNORE INTO lifeos_memory_tags(user_id, memory_id, tag)
|
||||||
|
SELECT NEW.user_id, NEW.id, value FROM json_each(NEW.value_json, '$.tags')
|
||||||
|
WHERE NEW.deleted_at IS NULL AND type = 'text' AND value <> '';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TABLE lifeos_goals (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
target_date TEXT,
|
||||||
|
done INTEGER NOT NULL DEFAULT 0,
|
||||||
|
value_json TEXT NOT NULL,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
deleted_at TEXT,
|
||||||
|
PRIMARY KEY (user_id, id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_lifeos_goals_user_status_date ON lifeos_goals(user_id, done, target_date) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE lifeos_tasks (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
goal_id TEXT,
|
||||||
|
due_date TEXT,
|
||||||
|
done INTEGER NOT NULL DEFAULT 0,
|
||||||
|
value_json TEXT NOT NULL,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
deleted_at TEXT,
|
||||||
|
PRIMARY KEY (user_id, id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_lifeos_tasks_user_status_date ON lifeos_tasks(user_id, done, due_date) WHERE deleted_at IS NULL;
|
||||||
|
CREATE INDEX idx_lifeos_tasks_user_goal ON lifeos_tasks(user_id, goal_id) WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE TABLE lifeos_favorites (
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
memory_id TEXT,
|
||||||
|
value_json TEXT NOT NULL,
|
||||||
|
revision INTEGER NOT NULL DEFAULT 1,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
deleted_at TEXT,
|
||||||
|
PRIMARY KEY (user_id, id)
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_lifeos_favorites_user_memory ON lifeos_favorites(user_id, memory_id) WHERE deleted_at IS NULL;
|
||||||
Loaded 3 of 17 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user