Sfoglia il codice sorgente

docs(repo): 添加 AGENTS.md、TODO.md 与 opencode 配置

- AGENTS.md:明确 docs/ 是唯一事实来源、代码与文档冲突以文档为准、docs/ 只读,附 Git 规范与按 TODO.md 顺序执行的规则
- TODO.md:按 docs/ 拆解的实施清单(脚手架清理到部署验证)
- opencode.json:instructions 仅加载 conventions.md 与 overview.md
kairo 3 ore fa
parent
commit
b8c83c0188
3 ha cambiato i file con 192 aggiunte e 0 eliminazioni
  1. 88 0
      AGENTS.md
  2. 100 0
      TODO.md
  3. 4 0
      opencode.json

+ 88 - 0
AGENTS.md

@@ -0,0 +1,88 @@
+# AGENTS.md
+
+## 最重要的三条
+
+1. **`docs/` 是唯一事实来源。** 所有代码按 `docs/` 生成,不做反向推导。
+2. **代码与文档冲突时,以文档为准。** 冲突的判定是"代码错了",修代码或重新生成;不得改文档去迁就代码。
+3. **`docs/` 下任何文件只读。** 不得创建、修改、删除、格式化 `docs/`;需求变更由人类先改文档。见 `docs/conventions.md` 第二、五节。
+
+遇到题面没覆盖的行为:不自己发明,写进回复里让人类决定补文档还是补禁令。
+
+## 严格按 `TODO.md` 执行
+
+- 根目录 `TODO.md` 是唯一的任务清单。**按章节顺序(0 → 8)从上往下做**,不要跳章节挑"有意思的"先做。
+- **一次只做一个条目**:改完、验证通过、把该条勾成 `[x]`(部分完成用 `[~]` 并在后面注明缺什么),再开始下一条。不要把多个章节的改动混在一次提交里。
+- 不擅自扩充范围:TODO 没列的功能不做;确实需要的先提出来让人类加条目。
+- 第 8 章「明确不做」永远不勾选,也不实现。
+- `TODO.md` 可以用 `[x]`/`[~]` 反映进度,但**不得删除条目或改动其含义**——需求变更走文档流程,由人类改。
+
+## 开工前按任务读文档
+
+- 永远:`docs/overview.md`、`docs/architecture.md`、`docs/conventions.md`(第四节是硬性禁令清单)
+- 后端:再加 `docs/api.md`、`docs/backend/{data-model,auth,audit}.md`
+- 前端:再加 `docs/api.md`、`docs/frontend/frontend.md`
+- `README.md` 只是索引,具体规则以它指向的文档为准;README 里写的 `deploy/` 目录当前不存在。
+
+## 硬性禁令(全部来自 `docs/conventions.md` 第四节,只摘最容易违反的)
+
+- 不用 Docker / K8s;不引入 PG 之外的存储作为真值源。
+- 不做 SSR / 预渲染;不做多用户(`user_id` 字段是预留,不实现注册)。
+- 后端只用 Gin;不用 ORM / sqlc,手写 SQL(`database/sql` + `pgx`)。
+- `*gin.Context` 只出现在 handler 层,业务函数只收 `context.Context`。
+- 创建笔记不接受 `visibility`;公开只走 publish。
+- 本期**不写**任何 TUI / Neovim 代码,也不写 LLM 实现(`internal/llm` 只有接口,不加 SDK 依赖)。
+- 读接口的 visibility 收敛统一由授权中间件注入,不在各 handler 里自己拼。
+
+## 当前代码状态(先看这里再动手,别假设已实现)
+
+仓库处于脚手架阶段:
+
+- `backend/cmd/haven/main.go` 只有 `GET /api/health` 和硬编码 `r.Run(":8080")`,与文档要求的 `HAVEN_LISTEN_ADDR` 等环境变量配置不符——按 `docs/architecture.md` 第四节重写,不要参考现有写法。
+- `backend/internal/*` 全是空目录;`backend/migrations/` 为空。
+- `frontend/` 是 Vite `create-vue` 模板(`HelloWorld.vue`、`src/views|router|stores|api` 空目录);缺 `vue-router`、`pinia` 的实际接线。
+- `frontend/dist/` 是上次构建产物,被 gitignore,不要手改、不要提交。
+
+## 常用命令
+
+后端(在 `backend/` 下执行):
+
+```bash
+go build ./...                  # 编译
+go vet ./...
+go test ./...                   # 单包:go test ./internal/auth/...
+go test ./internal/auth -run TestLoginTotp -v
+```
+
+前端(在 `frontend/` 下执行):包管理器是 **pnpm**(有 `pnpm-lock.yaml`,不要用 npm/yarn 生成新 lockfile)。
+
+```bash
+pnpm install
+pnpm dev                        # Vite dev server,需自行代理 /api 到后端
+pnpm build                      # = vue-tsc -b && vite build → frontend/dist/
+```
+
+没有 lint / format / CI 配置,不要自造工具链。
+
+认证相关的本地运行需要 PostgreSQL 并 `CREATE EXTENSION IF NOT EXISTS pg_trgm;`;其它 `HAVEN_*` 环境变量见 `docs/architecture.md` 第四节(缺必填项应拒绝启动)。
+
+## 容易踩的约定
+
+- **module 路径**是 `github.com/kairo/haven`;`go.mod` 用 `go 1.27.3`,`pgx` 是 v5。
+- **migration** 用 `golang-migrate` 命名 `0001_init.up.sql` / `.down.sql`,放 `backend/migrations/`;顺序为 `users` → `sessions`/`pending_logins`/`trusted_devices`/`tokens`/`audit_events` → `notes`/`tags`/`portfolio_items` → `note_tags`/`portfolio_repos`。**后端启动时不跑 migration**。
+- **后端分包按领域**(`notes`、`tags`、`search`、`portfolio`、`audit`),禁止建 `handlers/`、`services/`、`repositories/`;跨领域公共代码只放 `httpx` / `auth` / `config`。
+- **搜索**只查生成列 `notes.search_text`,应用层不写该字段;`ILIKE '%q%'`,无中文分词。
+- **公开可见条件唯一**:`visibility = 'public' AND published_at IS NOT NULL`;撤回 public→private 时保留 `published_at`。
+- **跨用户访问返回 404 而非 403**;403 只留给"已登录但权限不足"(如 read Token 做写操作)。
+- **JSON 一律 `snake_case`**,不转 camelCase;列表响应固定 `{items, total}`(空列表不能是 null);笔记列表 item 必须带后端截断好的 `content_preview`(按 rune 截 200)。
+- **凭证类操作**(Token / 授信设备 / 会话 / TOTP / 密码)Token 一律 403,只允许 Web 会话;其中 TOTP 关闭、改密码、增删改查 Token 需要会话已通过 TOTP。
+- cookie 名、有效期、bcrypt cost 等是代码常量,**不从环境变量读取**(清单在 `docs/architecture.md` 第四节)。
+- 审计只在业务成功后写、失败只打日志不影响响应;笔记 CRUD 不写审计,只有 publish/unpublish 写。
+
+## Git 规范
+
+- 提交信息沿用现有风格:`feat(backend): ...` / `fix(frontend): ...` / `doc(docs): ...`,**中文描述**,不加 AI 署名或 `Co-Authored-By`。
+- 只提交自己本次改动的文件;不要 `git add -A` / `git commit -a`,以免带上别人未完成的改动。
+- **未经用户明确要求,不要 commit / amend / push / rebase / reset**;不要改写已有提交历史。
+- `docs/` 只读,因此任何 AI 发起的提交都不应包含 `docs/` 变更;若 `git status` 里 `docs/` 有改动,视为人类未提交的编辑,必须原样保留。
+- 提交前先跑与改动范围对应的验证(`go build ./...` + `go test ./...`,或 `pnpm build`);`node_modules/`、`frontend/dist/` 已在 `.gitignore` 中,不要强制添加。
+- 不创建 PR、不推送到 `main`、不擅自切换或创建分支,除非用户要求。

+ 100 - 0
TODO.md

@@ -0,0 +1,100 @@
+# TODO
+
+> 执行清单,不是事实来源。所有实现细节以 `docs/` 为准;本文件与 `docs/` 冲突时以 `docs/` 为准。
+> `docs/` 只读,需求变更请先改文档,再勾选这里的条目。
+
+当前阶段:**脚手架已建,业务代码未开始**(`main.go` 只有 `/api/health`,`internal/*` 为空目录,前端仍是 Vite 模板)。
+
+图例:`[ ]` 未做 · `[x]` 已完成 · `[~]` 部分完成
+
+---
+
+## 0. 清理脚手架(最先做,避免旧写法被模仿)
+
+- [ ] `backend/cmd/haven/main.go` 重写为读环境变量的入口(见 `architecture.md` 第四节、5.2/5.3):删除硬编码 `:8080`,改用 `HAVEN_LISTEN_ADDR`,接 `log/slog`
+- [ ] 删除 `frontend/src/components/HelloWorld.vue` 与模板样式(`style.css` 里的 demo 变量、`src/assets/{vue.svg,vite.svg,hero.png}`)
+- [ ] 删除 `frontend/README.md`(Vite 模板说明)与 `frontend/public/icons.svg` 等模板残留,保留 `favicon.svg` 或替换
+- [ ] `frontend/index.html`:`lang="zh-CN"`,标题改为 Haven
+- [ ] 接入 `vue-router`(History 模式)与 `pinia` 到 `main.ts`
+- [ ] 根目录 `.gitignore`(当前只有子目录里的,`backend/.gitignore` 里 `/backend/...` 这类路径是按仓库根写的,实际匹配不到)
+
+- [ ] 根目录 `AGENTS.md`:写文档驱动规则、Git 规范、TODO 边界、技术栈锁定(**已有初版**,剩余是人工复核:重点核对“AI 只读不写 docs”和“禁止自动提交”两条措辞没被软化,并补上 TODO 边界)
+- [ ] 根目录 `opencode.json`:`instructions` 只加载 `docs/conventions.md` 和 `docs/overview.md`,其余文档按需手动喂,不全量加载(**尚未创建**)
+
+## 1. 配置与数据库基座
+
+- [ ] `internal/config`:读取并校验全部 `HAVEN_*` 变量,缺必填项拒绝启动;常量(cookie 名、有效期、bcrypt cost)写死不走配置
+- [ ] `internal/db`:`database/sql` + `pgx` 连接池,启动时 ping
+- [ ] `migrations/0001_init.up.sql` / `.down.sql`:按 `data-model.md` 依赖顺序建表(`users` → `sessions`/`pending_logins`/`trusted_devices`/`tokens`/`audit_events` → `notes`/`tags`/`portfolio_items` → `note_tags`/`portfolio_repos`),含全部索引与 CHECK
+- [ ] migration 要求 `CREATE EXTENSION IF NOT EXISTS pg_trgm;`;确认 `gen_random_uuid()` 可用
+- [ ] 初始化脚本:创建唯一用户(bcrypt cost 12)——**不暴露注册接口**
+- [ ] 后端启动**不**跑 migration(由脚本执行);在 `deploy/` 或文档待定处补执行方式
+
+## 2. 鉴权核心(`internal/auth`,按 `auth.md`)
+
+- [ ] 凭证生成与哈希工具:32 字节随机 + base64url,落库只存 SHA-256
+- [ ] 身份识别(auth.md 6.1):`Authorization: Bearer` 优先于 cookie;`sessions` / `tokens` 两张表;`pending_logins` 不参与
+- [ ] 授权中间件(auth.md 6.2–6.4):`anonymous_ok` / `login_required` / `write` / `totp_verified` 四档,并把可见性条件注入 context
+- [ ] cookie 下发:`haven_session` / `haven_trusted_device`,`HttpOnly`、`SameSite=Lax`、`Domain=HAVEN_COOKIE_DOMAIN`、`Secure` 跟随 `HAVEN_COOKIE_SECURE`
+- [ ] 登录两步:账密 → (未启用 TOTP / 授信设备有效则直接建会话)→ 否则 `pending_logins` + `temp_token`(5 分钟)
+- [ ] 每次请求更新 `sessions.last_used_at`,不延长 `expires_at`
+- [ ] TOTP:setup 不落库、verify 通过才落库并置 `totp_enabled`;disable 同时删除该用户全部授信设备
+- [ ] 改密码:同事务内删全部 `sessions` + `trusted_devices`,**不删** `tokens`,当前会话也失效
+- [ ] bcrypt cost 12;密码 ≥12 位且不能与旧密码相同
+
+## 3. 审计(`internal/audit`,按 `audit.md`)
+
+- [ ] `audit_events` 写入器:只追加;`actor_user_id` 可为 NULL,`actor_username` 存快照
+- [ ] 实现第十三类事件(login_success/login_failure/logout/totp_bound/totp_disabled/password_changed/session_revoked/trusted_device_added|revoked/token_created|revoked/note_published|unpublished)
+- [ ] 写入时机:业务成功后、响应前;**失败只打错误日志,不影响业务结果**
+- [ ] 笔记 CRUD 不写审计;不记录笔记内容、密码、Token 明文、TOTP secret
+
+## 4. 领域接口(按 `api.md` 逐条实现)
+
+- [ ] `/api/me`(11.1):匿名/会话/Token 三种响应
+- [ ] 认证 `/api/auth`(二):login、login/totp、logout、totp/setup|verify|disable、password
+- [ ] 会话 `/api/sessions`(三):列表带 `is_current`;禁止撤销当前会话(422);Token 访问 403
+- [ ] 授信设备 `/api/trusted-devices`(四):**只读 + 删除,不提供 POST**
+- [ ] Token `/api/tokens`(五):创建仅返回一次明文;撤销写 `revoked_at` 不删记录;需 TOTP
+- [ ] 笔记私有 `/api/notes`(六):列表 `updated_at DESC` + `content_preview`(rune 截 200 + `…`);创建不收 `visibility`;`tag_ids` 全量替换
+- [ ] publish / unpublish:`published_at` 首次写入、撤回保留;重复操作 409
+- [ ] 笔记公开 `/api/public/notes`(七):`published_at DESC`,不可见统一 404
+- [ ] 标签 `/api/tags`(八):`lower(name)` 唯一 → 409;无重命名接口;级联删除关联
+- [ ] 搜索(九):`/api/search` 笔记查 `notes.search_text ILIKE '%q%'`、标签查 `tags.name ILIKE '%q%'`,返回 `{notes, tags}` 两块;`/api/public/search` 只查公开笔记的 `search_text`,不返回标签列表
+- [ ] 作品集 `/api/public/portfolio` + `/api/portfolio/*`(十):`url` 由后端拼接(Gogs 用 `HAVEN_GOGS_BASE_URL`,GitHub 固定);主链接唯一
+- [ ] `internal/httpx`:统一错误响应(`error`/`message`/可选 `fields`)+ 统一分页(`limit` 默认 20 上限 100)
+- [ ] 响应字段全 `snake_case`;列表空值返回 `{"items": [], "total": 0}`
+- [ ] 跨用户访问一律 404;read Token 写操作 403;凭证类操作对任何 Token 一律 403
+
+## 5. 静态托管
+
+- [ ] `r.Static` 托管 `/assets`(长缓存),`NoRoute` 兜底 `index.html`;`HAVEN_FRONTEND_DIR`
+- [ ] 确认 `/api/*` 不被 SPA 兜底吞掉
+
+## 6. 前端(按 `frontend.md`)
+
+- [ ] `src/api/` 按领域拆文件(auth/notes/tags/search/portfolio/sessions/devices/tokens/me)+ 统一 `ApiError`;路径写相对 `/api/...`
+- [ ] `auth` store:启动调 `GET /api/me`;401 统一清空并跳 `/login`;403 只提示不跳转
+- [ ] 按 `window.location.hostname` 选择公开域/私有域路由表(同一份构建)
+- [ ] router guard:除 `/login` 外全部需登录,支持 `?redirect=`
+- [ ] 公开域页面:`/`、`/notes/:id`、`/search?q=`、`/portfolio`;**无登录入口、无后台链接**
+- [ ] 私有域页面:`/login`(含 TOTP 第二步,`temp_token` 只存内存)、`/`、`/notes/:id`、`/notes/new`、`/search`、`/tags`、`/portfolio`、`/settings`、`/settings/security`、`/settings/sessions`、`/settings/devices`、`/settings/tokens`
+- [ ] `/settings/tokens` 遇 403 时引导去 `/settings/security` 完成 TOTP,不登出
+- [ ] 公共组件:`NoteCard`、`TagBadge`、`SearchBar`、`LoginForm`、`TotpForm`、`SettingsNav`
+- [ ] 中文界面;不做深色模式切换、不做 PWA、不做多语言
+- [ ] 本地开发代理 `/api` 到后端(`vite.config.ts` 目前没有 server proxy)
+
+## 7. 验证与部署
+
+- [ ] `go vet ./...`、`go test ./...` 全绿;至少覆盖:可见性矩阵、Token scope、跨用户 404、publish 幂等性
+- [ ] `pnpm build`(含 `vue-tsc -b`)通过
+- [ ] 手工冒烟:匿名 → 公开列表/详情/搜索/作品集;登录 → TOTP → 笔记 CRUD → publish → 退出
+- [ ] `deploy/`(README 与 `conventions.md` 提到但尚不存在):构建脚本 + systemd unit + migration 执行
+- [ ] 部署形态待定项(`architecture.md` 第八节):端口、HTTPS/反代、`pg_dump` 备份定时任务
+- [ ] 更新根 `README.md` 的「快速开始」占位
+
+## 8. 明确不做(防止误开工)
+
+- ✗ TUI / Neovim 插件(第二版)
+- ✗ LLM 实现与 SDK 依赖(`internal/llm` 只保留接口)
+- ✗ Docker / K8s、ORM / sqlc、SSR、多用户注册、审计查询界面

+ 4 - 0
opencode.json

@@ -0,0 +1,4 @@
+{
+  "$schema": "https://opencode.ai/config.json",
+  "instructions": ["docs/conventions.md", "docs/overview.md"]
+}

苏ICP备2026082406号
苏公网安备 待审核