# Future Journal

> 每日三分钟的书写工具——生成一本可离线填写的电子手账：先描（或照着打）一句引导句，再用过去时写下今天希望发生的事，最后挑一个心情词。49 天一轮、每周一个主题，含 49 句原创引导句库、质量闸门与页面回归测试。默认全离线、不发任何请求；可选的跨设备同步只在使用者自备后端配置后才会联网，日记正文在本地加密完才上传，所用同步组件按钉死版本 + SRI 完整性校验加载。Generate an offline single-file daily journal page (49-day cycle, weekly themes, trace-or-type a guided sentence then write in the past tense) with a bundled original prompt library and quality gates. Use when the user wants a fillable diary or journal page, a 未来日记 / 提前日记 / 晨间日记 / 感恩日记 tool, a printable journal, or asks what sentence to write today. Not for 任务与待办管理、日程排程、心情打卡统计，也不做心理健康或危机干预——本工具只提供书写页与引导句，不诊断、不建议、不替用户做决定。适用于「想开始写未来日记」「想要一本能打印的手账」「今天该写哪一句」「想用过去时写愿望」「总往坏处想、想把自己拉回来」「想在平板上用触控笔描一句」「想电脑手机换着写」等场景。

- Skill: `bonniegeng-max/future-journal` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add bonniegeng-max/future-journal`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bonniegeng-max/future-journal/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: bonniegeng-max (https://skillmd.com/u/bonniegeng-max)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/bonniegeng-max/future-journal

---


# 未来日记 · Future Journal

## 这是什么

一个单页、离线、手机优先的电子手账。每天三分钟，三步：

1. 描摹一句浅灰引导句
2. 用过去时写下今天希望发生的一件事
3. 句末挑一个心情词

49 天为一轮，每周换一个主题（小确幸 → 身体节奏 → 关系 → 作品 → 金钱 → 空间 → 未来已来的我）。

方法与节奏参考山田弘美、滨田真由美所著的日记实践书。**本工具不是该书的官方产品，
49 句引导句全部为原创，书中正文内容未被使用。**

## 目录

| 文件 | 作用 |
|---|---|
| `assets/index.html` | 完整的手账页面。单文件、双击即可打开；不配同步时全程离线、不发任何请求 |
| `references/DESIGN.md` | 暖纸手账设计系统 + 无障碍基线。**改页面前先读这个** |
| `scripts/today.js` | 算今天是第几天、本周主题、今天要描哪一句 |
| `scripts/check-prompts.js` | 引导句质量闸门。改句库后必跑 |

以上 5 个文件（含 `SKILL.md` 自身）是 skill 的完整能力面，**发放给别人的就是这些**。

### 维护者工具（只在本仓库，不进 ClawHub 发布包）

| 文件 | 作用 |
|---|---|
| `scripts/test-journal.js` | 页面回归测试（数据不丢 / 键盘可达 / aria / 降级路径） |

它依赖 `jsdom`（开发期依赖），属于「改这个仓库的人用的」，不是「装这个 skill 的人用的」。
**不要把它打进 ClawHub 发布包** —— 维护者工具混进发布包被判过
`suspicious / DO_NOT_INSTALL`（见 `MAINTAINING.md` 里记的那次教训）。

## 改页面前后必跑的闸门

改句库：

```bash
node scripts/check-prompts.js
```

改页面逻辑或样式（仓库内，需要 `jsdom`）：

```bash
node scripts/check-prompts.js
NODE_PATH=~/.workbuddy/binaries/node/workspace/node_modules node scripts/test-journal.js
```

两个都要退出码 `0`。

`test-journal.js` 覆盖三组：A 数据不丢（打字自动落盘 / 刷新还原 / 防抖窗口内切 tab）、
B 无障碍与键盘可达、C 拿不到 2D 上下文时的降级。

> 一个顺手的巧合：**jsdom 不实现 canvas 2D，`getContext('2d')` 恒返回 `null`**，
> 所以 C 组那条降级分支在 jsdom 里是被真实执行的，不是 mock 出来的。


## 交付页面给用户

把这个文件复制到用户指定的位置就行，它没有任何依赖：

```bash
cp assets/index.html ~/Desktop/未来日记.html
```

- 双击用浏览器打开即可使用（`file://` 协议下完整可用）
- 手机端：把文件传到手机后用 Safari 打开
- 数据存在浏览器 localStorage，键名 `future-journal-v1`

**不要为它起本地服务器。** 页面不依赖服务器就能完整使用（唯一的例外是可选同步），
起服务器只会带来额外故障点。

> 例外：想用「跨设备同步」就必须走线上网址 —— 见下面的「同步」一节。

## 回答「今天写哪一句」

```bash
node scripts/today.js 2026-09-17           # 以该日期为第 1 天，算到今天
node scripts/today.js 2026-09-17 --day 8   # 直接指定第几天
```

输出第几天、本周主题、今天要描的句子。用户想写在实体本子上时用这个。

## 改引导句库

49 句内嵌在 `assets/index.html` 的 `<script id="prompt-data">` JSON 块里。
编辑后**必须**跑校验，退出码 `0` 才算通过：

```bash
node scripts/check-prompts.js
```

检查项：7 周 × 7 句的结构、重复句、将来时标志词、句末情绪词、单句长度 ≤ 30 字。

### 新句子的公式

```
时间锚点 + 一件具体的好事 + 完成态 + 句末情绪词
```

- 对：`今天的活一件件推进了，我很安心。`
- 错：`我希望今天工作顺利。` ← 将来时，方法明确要求写过去时

中文没有时态标记，肉眼判断「是不是过去时」极不可靠，所以校验脚本用
**禁止将来时标志词**这条硬规则兜底。不要跳过。

## 数据在哪（重要，别承诺错）

默认情况下，页面数据只存在用户浏览器的 localStorage 里，**skill 读不到**。因此：

- 用户问「我写了什么」→ 让他在页面的「回顾」页看，或从「设置」导出 JSON
- 用户要备份 / 换设备（不登录）→ 页面「设置」里有导出与导入按钮
- **清缓存 / 换浏览器，本机数据就没了** —— localStorage 的固有特性，务必如实告知
- 「进度」「3 个想要的」都在同一个 state 里，导出 / 导入是整包，不拆分

跨设备同步是**可选**功能，且必须先发布到线上网址，见下一节。

## 同步（可选，需先发布到线上）

页面内置了「账号登录 + 端到端加密」的跨设备同步，但有三条硬约束：

1. **只在线上网址可用。** 判断依据是 `location.protocol`：`file:` 开头时页面会直接提示
   「联网同步要在发布后的网址上用」，且不加载 SDK。双击本地文件打开 = 纯本地模式，
   这和以前完全一样，不要因此认为页面坏了。
2. **需要先激活云服务并建表。** 表是 `journal_sync`，一人一行，只存密文：

   ```
   id BIGINT identity PK / owner_id TEXT DEFAULT auth.uid() UNIQUE
   cipher TEXT NOT NULL / updated_at TIMESTAMPTZ
   ```

   `owner_id` 由数据库默认值填充（`TEXT`，不是 uuid），**客户端永远不要发这个字段**。
   RLS 要同时给 GRANT 和 POLICY 两道，缺一不可。

3. **加密在浏览器里做，服务器只有密文。** PBKDF2 20 万次派生 AES-GCM-256 密钥，
   密钥存本机 localStorage（`fj-dk`），不上传。所以**同步密码丢了，云端那份就解不开**，
   本机记录不受影响 —— 这句话必须原样告诉用户，不能含糊。

其他约定：

- 首次登录后要用户自己设「同步密码」，不自动生成 —— 他要能在别的设备上手输同一个
- 推送是**写完后延迟合并触发**（`schedulePush`），不是每次按键
- 冲突用 `mergeStates` 合并：按天取 `at` 较新的一条，「3 个想要的」按文本取并集
- 云端写入走「先查后定 insert/update」，不用 `upsert` —— 因为 `owner_id` 是数据库默认值，
  客户端不知道值，`onConflict` 无从下手

配置串（`endpoint` + `publishableKey`）存在本机 `fj-cfg`，用户换设备时粘贴一次即可。

### ⚠ 不要把配置串写死进页面

看起来最顺手的「优化」是把 `endpoint` / `publishableKey` 直接内嵌进 `assets/index.html`，
这样用户就不用粘贴了。**绝对不要这么做。**

这个页面是要分发给别人的 skill 资产。一旦写死，任何装了这个 skill 的人打开页面都会连上
**同一个后端**——虽然 RLS 按用户隔离、彼此看不到内容，但陌生人会在号主的环境里注册账号、
消耗号主的配额。作者自己用着舒服的代价，是所有人的数据都涌向同一个人的账上。

配置串必须由使用者自己提供（`fj-cfg` 或设置页粘贴）。号主自己的那份也走同一条路——
把配置串放在 skill 目录**外面**的文件里，这样它永远不会随 skill 分发出去。

## 这个 skill 会碰网络吗（如实清单）

发布审核最容易被卡的点不是「有联网」，而是**声称与实际不符**。所以这里列全，
一个不漏 —— 如实罗列比写得含糊安全。

| 行为 | 何时发生 | 去向 | 传了什么 |
|---|---|---|---|
| 下载云同步 SDK | **仅当**使用者自己粘贴了配置串、且页面从 `http(s)://` 打开 | `cdn.jsdelivr.net` 上的 `@tencent-ai/workbuddy-cloud-sdk`（版本号钉死 + SRI 完整性校验） | 只下载代码，不上传 |
| 发验证码 / 维持登录态 | 使用者主动点登录 | 使用者**自己配置的**后端 | 邮箱、会话令牌（会离开设备） |
| 上传日记 | 使用者设了同步密码之后写入内容 | 使用者**自己配置的**后端 | **只有密文**（在本机加密完才发） |

这个表以外，没有别的：

- 无统计、无埋点、无第三方字体 / 图标 CDN
- **无硬编码的服务器地址或密钥**：`endpoint` / `publishableKey` 一律由使用者自备
- 不配同步时：页面从 `file://` 打开，**一个网络请求都不发**（SDK 根本不加载）
- `scripts/today.js`、`scripts/check-prompts.js`、`references/DESIGN.md` 全程纯本地

### 远端 SDK 必须钉版本 + 校验完整性

`assets/index.html` 里的 `SDK_URL` / `SDK_SRI` 是一对**必须同进同退**的常量：

```js
var SDK_URL = '…/@tencent-ai/workbuddy-cloud-sdk@<具体版本>/lib/index.global.js';
var SDK_SRI = 'sha384-<该版本文件的 sha384>';
```

为什么这条是硬约束：这段远端代码和页面**同源执行**，能读到 localStorage 里的日记正文、
`fj-cfg` 里的后端地址与 key、以及导出的加密密钥 `fj-dk`。所以「加载哪一份」不能由 CDN
当下决定。用 `@dev` 这类漂移标签，等于**审核时的那份代码和用户实际执行的可以是两份**。

- 改版本 → 必须用 `hashlib.sha384` 对新文件重算哈希并写回 `SDK_SRI`
- 忘了改 → 同步会静默失效（`onerror` 会给出提示，不会留半截界面）
- `scripts/test-journal.js` 的 **D 组**会卡住这个约定；`@dev` / `@latest` 一出现就失败
- `<script>` 上的三个属性缺一不可：`integrity`（不放行改动过的字节）、
  `crossorigin="anonymous"`（SRI 需要 CORS 取文件）、`referrerpolicy="no-referrer"`
- 用 `setAttribute` 写而不是 `s.integrity = …`：后者的 IDL 反射不是所有引擎都有


> 那句话「加载远程脚本」确实是扫描器会多看两眼的模式——所以页面把它做成
> **必须先有人粘贴配置串才会发生**，不是打开就跑。这是刻意的，改的时候别把它提前。

### 登录用的「账号」到底是什么（别答错）

不是 WorkBuddy 账号，也不是微信 / Google / Apple 之类 —— 是**使用者自己那个云服务环境里的
终端用户账号**，用**邮箱**当唯一标识，登录靠邮箱收到的 6 位验证码（也可以另外设密码）。

要说准的四点：

- 邮箱只存在**使用者自己配的那个后端**里。分发 skill 的人拿不到任何东西。
- 平台目前**不提供匿名登录**，所以只要想跨设备，邮箱就是唯一的身份选项，没有更轻的替代。
- 让人填邮箱确实是审核会留意的一类信号（收集个人信息）。三条缓解都已到位：
  **完全可选**（不登录 = 纯本地，功能一个不缺）、**不是作者的服务器**、**日记内容端到端加密**。
- 反过来：配置串被别人拿到，他的邮箱就会进**那个人的**后端 —— 这是「配置串必须使用者自备」
  的又一个理由。

## 页面的几个设计决定（改动前先理解）

- **回看是延迟解锁的**：满 7 天才打开上一周。这是刻意设计，不是 bug ——
  方法本身建议不要天天回头翻。不要把解锁提前。
- **「过一遍」有两种方式，不要拿掉任何一种**：有触控笔走「用笔描」（canvas，只记录笔迹、
  不做对错判定，描歪也算过）；鼠标或触控板走「照着打一遍」（照着灰字逐字输入，打对变深、
  打错标红但不拦）。两种都记 `traced: true`，`traceMode` 存方式。
  触控板描摹是手眼分离，实测无法控制 —— 打字模式不是可选项，是必需项。
- **笔迹按归一化坐标存储**（0–1 区间），所以换设备、换屏幕尺寸后不会错位。
- **输入框字号锁 16px**：低于此值会触发 iOS Safari 自动缩放。不要改。
- **写作框必须自动落盘，不许退回「点按钮才存」。** 曾经只有「收好了」按钮会写
  `S.entries[day].text`，而 `renderToday()` 又会用已存的值覆盖输入框 —— 于是「打了一半切个
  tab」或「直接刷新」就把正在写的内容丢了。现在有 5 个落盘点：输入防抖 500ms、
  切 tab、`pagehide`、`beforeunload`、页面隐藏。**切 tab 前必须先 `flushWrite()` 再渲染。**
  描摹和心情词本来即时存，写作框过去是唯一的例外，别再制造这个例外。
- **同步卡片的 4 个状态都在 `renderSyncCard()` 一处渲染**：未填配置串 / 已填未登录 /
  已登录待设同步密码 / 已连接。改同步相关文案只改这一个函数，别散到各处。
- **可读性优先于纸感。** 正文一律 ≥ 4.5:1、UI ≥ 3:1。唯一豁免是描摹引导字
  （`--trace`，#ABA492，2.20:1）—— 它的身份是"盖着描的底稿"不是正文，真按 4.5:1 加深，
  描上去的笔迹就和底稿分不清了。等价路径已备齐：打字模式用弱墨（4.55:1）、打印版加深到 6.71:1。
  **别在别处复用 `--color-trace`。**
- **状态不能只靠颜色。** 已写 / 今天 / 未来的差异落到 `disabled` + `aria-label` 上；
  心情词的选中态落到 `aria-pressed`；当前 tab 落到 `aria-current`。
  读屏读不出"底色深浅"，只写 CSS 等于没做。
- **能力探测要在动 DOM 之前做。** 典型例子：`canvas.getContext('2d')` 会返回 `null`
  （浏览器禁用画布、或 iOS Safari 画布上下文超限）。原来 `openTrace()` 直接 `.font = …`
  就抛 `TypeError`，表现为**点「用笔描」毫无反应**。现在先探、拿不到就原地退回打字模式
  并给一句人话。**"点了没反应"比报错还差。**
- **焦点环必须是一层额外描边**，不能只靠改边框色（触屏上看不见）。
  全站 `:focus-visible` 用 2px 赤陶 + 2px offset；赤陶底上的按钮换成主墨描边。
  **不要写 `outline: none`。**

## 不做什么

- **不默认开同步。** 不登录 = 纯本地，这条不能被改动破坏。同步必须由用户主动粘贴配置串开启
- 不做推送提醒（页面关闭即止，做不到）。想每天被提醒，用 WorkBuddy 的定时任务推当天引导句
- 不做暗色模式（纸质手账没有暗色版）
- 不引入任何第三方图标库、字体 CDN、统计脚本 —— 离线可用是硬要求
  （云同步 SDK 是唯一例外，且只在线上模式按需加载，版本钉死 + SRI 校验）

## 语言与区域范围（Language & Locale Scope）

**默认简体中文（zh-CN）**，英文双语说明随包提供。界面与文档以中文为第一语言，英文可用。

- **为什么默认中文**：这套方法的语感建立在中文上 ——「过去时」在中文里靠词汇而非词形、
  「心情词」的调子、49 句原创引导句的措辞，都以中文为第一语言。
- **中英双语**：包内提供英文能力摘要（`skill-card.md`）；**用户书写内容不限语言，中英文皆可**；
  页面字体栈按中英双语定义（西文回退 `Georgia` / `-apple-system`），中英混排不会掉字体。
- **其他语言**：界面文案与引导句库目前以中文为主；**需要其他语言的界面或句库，可向作者提出**。

**Scope statement (English)** — Default locale is Simplified Chinese (`zh-CN`), with
English documentation bundled (`skill-card.md`). User entries are **not
language-restricted**: journal content may be written in Chinese or English. Font
stacks are bilingual and fall back to Latin families for English text. Other
interface languages or prompt libraries are **available on request**.

## 版本历史

- **1.0.3** — 语言声明统一为**「默认中文 + 中英双语」**口径（1.0.2 里写成「只支持中文、
  不提供语言切换」的方向是错的，反而压不下 `SQP-3`）。全文按同一口径重写：
  `SKILL.md`（新增「中英双语」条目）、`README.md`、`assets/index.html`（`<html lang>` 处）、
  `references/DESIGN.md`（引言 + 字体栈旁，如实说明**西文回退 `Georgia` / `-apple-system`、
  中英混排不掉字体**）、`scripts/check-prompts.js`、`scripts/today.js`。
  同时消除文档自相矛盾的「声称与行为不符」风险。

- **1.0.2** — 针对 ClawHub 安全扫描报告逐条复核后的修复（`skillspector` 13 条）：
  ① 把「语言」声明升级成**有界范围声明（Language & Locale Scope）**，并逐文件补齐 ——
  `assets/index.html`、`references/DESIGN.md`、`scripts/*.js` 此前只有 `SKILL.md`/`README.md`
  有声明，是 `SQP-3` 被判 MEDIUM 的原因（已有声明的那两个文件只判 LOW）。6 条 `SQP-3` 全部落到同一口径。
  ② 清掉包内全部 **default-ignorable 码点**（`U+FE0F` 变体选择符 5 处）与 `references/DESIGN.md`
  里混入的希腊大写字母（U+0394，原本写在亮度符号前面）—— 这是 `AE4`（Unicode / 混写异常）的直接触发特征。
  ③ 6 条 `AE1` 经查证**不予处理**：源码显示它只在「被引用文件未被扫描器完整检查」时生成，
  属覆盖率信号而非文件缺陷（已排除体积 / NUL / 编码 / 混淆四类真实成因），
  消除它只能靠删引用 —— 那是刻意规避。

- **1.0.1** — 修掉 ClawHub 安全扫描的 `suspicious`：云同步 SDK 从漂移的 `@dev` 标签改为
  钉死具体版本 + SRI 完整性校验（扫描报告 `SDI-2`）；补齐「开启同步后哪些数据会离开设备」的
  前端告知与文档说明（`SDI-1`）；声明中文优先是有意为之（`SQP-3`）。回归测试新增 D 组，44 条全过。
- **1.0.0** — 首次发布。


