# Person Panel 多用户数据中心与 RSS 实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在现有 Nuxt 4 + Nitro + SQLite + Drizzle + `@nuxt/ui` 项目上,实现多用户隔离的个人资料/文章/时光机/RSS 收件箱、条目级可见性与分享链接、实例管理员开号与禁用、以及进程内 RSS 定时同步(符合 `docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md`)。 **Architecture:** 单体 Nitro API 分 `public` / `me` / `admin` 三层;内容域按 `userId` 强隔离;可见性三态统一;RSS 抓取与调度放在 `server/service/rss` + `server/plugins` 进程内定时器;前端用 Nuxt UI 统一空状态与表单,公开路由与 API 白名单同步放开。 **Tech Stack:** Nuxt 4.4、Nitro、`@nuxt/ui` 4.x、Drizzle ORM、SQLite、better-sqlite3、bcryptjs、Zod、(新增)适用于 RSS 解析的轻量 XML 解析库、原生 `fetch`。 --- ## File Structure Map(新建 / 修改) | 路径 | 职责 | |------|------| | `packages/drizzle-pkg/database/sqlite/schema/auth.ts` | 扩展 `users`:`role`、`status`、`publicSlug`、`bioMarkdown`、`bioVisibility`、`socialLinksJson`、`avatarVisibility` 等 | | `packages/drizzle-pkg/database/sqlite/schema/content.ts`(新建) | `posts`、`timeline_events` 表定义 | | `packages/drizzle-pkg/database/sqlite/schema/rss.ts`(新建) | `rss_feeds`、`rss_items` | | `packages/drizzle-pkg/lib/schema/*` | 重新导出新建表 | | `packages/drizzle-pkg/seed.ts` | Bootstrap 管理员创建(读 env,仅在无 admin 时插入) | | `server/constants/visibility.ts`(新建) | `Visibility` 枚举与 Zod schema | | `server/utils/share-token.ts`(新建) | 生成/校验 `shareToken`(crypto 随机 24+ bytes URL-safe) | | `server/utils/rss-url.ts`(新建) | SSRF 前置校验:scheme、主机、禁止私网 IP(解析后)、重定向上限策略的入口 | | `server/utils/admin-guard.ts`(新建) | `requireAdmin(event)` | | `server/middleware/10.auth-guard.ts` | 配合扩展 allowlist;`getCurrent` 已登录但用户 disabled → 401 + 清 cookie(见 Task 3) | | `server/utils/auth-api-routes.ts` | 增加 `/api/public` 前缀规则、`/api/admin` 是否走会话(admin 需登录但不 allowlist 全开放) | | `server/service/auth/index.ts` | `getCurrentUser` join `users.status`,`active` 以外返回 `null`;`MinimalUser` 扩展 `role` | | `server/service/auth/context.ts` | `MinimalUser` 类型扩展 | | `server/api/auth/me.get.ts` | 返回 `role`、`publicSlug` 等安全字段 | | `server/api/admin/users.post.ts` 等 | 管理员创建/列表/禁用 | | `server/api/me/posts.*.ts` | 文章 CRUD | | `server/api/me/timeline.*.ts` | 时间线 CRUD | | `server/api/me/profile.put.ts`、`me/profile.get.ts` | 资料读写 | | `server/api/me/rss/feeds.*.ts`、`me/rss/items.*.ts`、`me/rss/sync.post.ts` | 订阅与同步触发 | | `server/api/public/profile/[publicSlug].get.ts` | 聚合公开字段与 `public` 内容列表 | | `server/api/public/unlisted.get.ts` 或 `[publicSlug]/[token].get.ts` | 按 slug+token 解析条目类型并返回 | | `server/service/rss/fetch.ts`(新建) | fetch feed、解析、入库、去重 | | `server/service/rss/scheduler.ts`(新建) | 到期扫描、并发池、写回 `lastError` | | `server/plugins/04.rss-scheduler.ts`(新建) | `nitroApp.hooks.hook("ready", ...)` 注册 `setInterval` + 启动补跑 | | `app/utils/auth-routes.ts` | `PUBLIC_ROUTE_PREFIXES`: `/@`, `/p/`;移除或条件化 `/register` | | `app/pages/@/[publicSlug]/index.vue`(新建) | 公开主页 | | `app/pages/p/[publicSlug]/t/[shareToken].vue`(新建) | 仅链接阅读页 | | `app/pages/me/**` 或 `app/pages/dashboard/**` | 后台列表与编辑(按你偏好的前缀统一) | | `app/components/AppShell.vue` | 侧栏导航、空状态入口、admin 菜单 | | `server/service/auth/index.test.ts` 或新测试文件 | 扩展测试 | --- ### Task 1: Drizzle schema — 用户扩展与内容/RSS 表 **Files:** - Modify: `packages/drizzle-pkg/database/sqlite/schema/auth.ts` - Create: `packages/drizzle-pkg/database/sqlite/schema/content.ts` - Create: `packages/drizzle-pkg/database/sqlite/schema/rss.ts` - Modify: `packages/drizzle-pkg/lib/schema/auth.ts`(若需聚合导出) - Create: `packages/drizzle-pkg/lib/schema/content.ts`、`rss.ts` 导出 - [ ] **Step 1: 扩展 `users` 列** 在 `auth.ts` 的 `users` 表增加(类型与默认值与迁移一致): - `role`:`text`,默认 `'user'`,非空 - `status`:`text`,默认 `'active'`,非空 - `publicSlug`:`text`,唯一索引(nullable 允许首登后填写) - `bioMarkdown`:`text`,可选 - `bioVisibility`:`text`,默认 `'private'` - `socialLinksJson`:`text`,默认 `'[]'`(存 JSON 字符串) - `avatarVisibility`:`text`,默认 `'private'` - [ ] **Step 2: 定义 `posts`** 字段建议:`id`、`userId`、`title`、`slug`、`bodyMarkdown`、`excerpt`、`coverUrl`、`tagsJson`、`publishedAt`、`visibility`、`shareToken`、`createdAt`、`updatedAt`;唯一索引 `(userId, slug)`。 - [ ] **Step 3: 定义 `timeline_events`** 字段:`id`、`userId`、`occurredOn`(timestamp)、`title`、`bodyMarkdown`、`linkUrl`、`visibility`、`shareToken`、`createdAt`、`updatedAt`。 - [ ] **Step 4: 定义 `rss_feeds` / `rss_items`** `rss_feeds`:`id`、`userId`、`feedUrl`(唯一 per user)、`title`、`siteUrl`、`lastFetchedAt`、`lastError`、`pollIntervalMinutes` 默认 null(用全局配置)、`createdAt`。 `rss_items`:`id`、`userId`、`feedId`、`guid`、`canonicalUrl`、`title`、`summary`、`contentSnippet`、`author`、`publishedAt`、`visibility` 默认 `private`、`shareToken`、`createdAt`;索引 `(feedId, guid)`、`(userId, canonicalUrl)` 辅助去重。 - [ ] **Step 5: 生成并应用迁移** Run: ```bash cd /home/dash/projects/person-panel && bun run db:generate -- add-multitenant-content-rss bun run db:migrate ``` Expected: 生成新 SQL 迁移且无报错。 - [ ] **Step 6: Commit** ```bash git add packages/drizzle-pkg git commit -m "feat(db): add multitenant profile, posts, timeline, and rss tables" ``` --- ### Task 2: Bootstrap 首个管理员(env + seed) **Files:** - Modify: `packages/drizzle-pkg/seed.ts` - Modify: `packages/drizzle-pkg/env.ts`(若集中定义 env) - [ ] **Step 1: 在 `seed.ts` 增加幂等 bootstrap** 逻辑: 1. 查询是否存在 `users.role = 'admin'`。 2. 若不存在,读取 `process.env.BOOTSTRAP_ADMIN_USERNAME` 与 `BOOTSTRAP_ADMIN_PASSWORD`;缺一则 **跳过并 log warn**(避免误创建)。 3. 使用与线上一致的 `bcrypt` hash 流程插入管理员:`role='admin'`、`status='active'`、`publicSlug` 可由 username 派生(小写+校验)或留空。 - [ ] **Step 2: 文档化运维步骤** 在 `docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md` 已描述;在 README 或 `.env.example` 增加变量名(若仓库允许)。 - [ ] **Step 3: 手动验证** Run: ```bash BOOTSTRAP_ADMIN_USERNAME=admin BOOTSTRAP_ADMIN_PASSWORD=adminadmin bun run db:seed ``` Expected: 第二次执行不应再创建第二个 admin(幂等)。 - [ ] **Step 4: Commit** ```bash git add packages/drizzle-pkg git commit -m "feat(db): bootstrap first admin from environment" ``` --- ### Task 3: 会话用户加载 role/status,禁用用户拒绝访问 **Files:** - Modify: `server/service/auth/index.ts` - Modify: `server/service/auth/context.ts` - Modify: `server/api/auth/me.get.ts` - [ ] **Step 1: 扩展 `getCurrentUser` 查询列** `select` 增加 `users.role`、`users.status`,`where` 保持 session 有效条件,并加 `eq(users.status, 'active')`(或在内存判断)。 若 `status !== 'active'`:**删除该 session** 或保留 session 但返回 null(推荐删除 session + null,避免僵尸会话)。 - [ ] **Step 2: 扩展 `MinimalUser`** `{ id, username, role }`。 - [ ] **Step 3: `/api/auth/me` 返回** `user: { id, username, role, publicSlug }`(`publicSlug` 来自 DB)。 - [ ] **Step 4: 运行现有测试** Run: ```bash cd /home/dash/projects/person-panel && bun test server/service/auth/index.test.ts ``` Expected: PASS;如失败则更新测试夹具。 - [ ] **Step 5: Commit** ```bash git add server/service/auth server/api/auth/me.get.ts git commit -m "feat(auth): expose role and enforce disabled users" ``` --- ### Task 4: API 白名单与 `public` 前缀 **Files:** - Modify: `server/utils/auth-api-routes.ts` - Modify: `server/middleware/10.auth-guard.ts`(若需 trace) - [ ] **Step 1: 实现前缀 allowlist** 增加函数:`isPublicApiPath(path: string): boolean`,当 `path.startsWith('/api/public/')` 时为 true(含 GET/POST 中只读方法,禁止开放写)。 将 `isAllowlistedApiPath` 改为:`rule match OR isPublicApiPath(path)`。 保留 `/api/auth/login`、`/api/auth/register`(后续 Task 会收敛 register)。 - [ ] **Step 2: 手动探测** 启动 dev 后 `curl -i http://localhost:3000/api/public/health`(若实现)应 200 无 cookie。 - [ ] **Step 3: Commit** ```bash git add server/utils/auth-api-routes.ts server/middleware/10.auth-guard.ts git commit -m "feat(api): allow unauthenticated access to /api/public" ``` --- ### Task 5: 管理员守卫 + 简易限流 **Files:** - Create: `server/utils/admin-guard.ts` - Create: `server/utils/simple-rate-limit.ts`(可选内联于 admin 首行) - [ ] **Step 1: `requireAdmin`** ```ts import type { H3Event } from "h3"; export async function requireAdmin(event: H3Event) { const user = await event.context.auth.requireUser(); if (user.role !== "admin") { throw createError({ statusCode: 403, statusMessage: "Forbidden" }); } return user; } ``` - [ ] **Step 2: 内存限流(每 IP 每分钟 60 次 admin 写操作)** 使用 `event.node.req.socket.remoteAddress` 为 key,`Map` 存计数与时间窗;超限 `429`。 - [ ] **Step 3: Commit** ```bash git add server/utils/admin-guard.ts server/utils/simple-rate-limit.ts git commit -m "feat(admin): require admin role and basic rate limiting" ``` --- ### Task 6: Admin 用户 API **Files:** - Create: `server/api/admin/users.post.ts` - Create: `server/api/admin/users.get.ts` - Create: `server/api/admin/users/[id].patch.ts` - [ ] **Step 1: POST 创建用户** Body:`{ username, password, email? }`;Zod 校验;`role` 固定 `user`;`status` `active`;密码 bcrypt;冲突返回 409。 - [ ] **Step 2: GET 列表** 分页:`limit/offset`;仅返回 `id, username, email, role, status, publicSlug, createdAt`。 - [ ] **Step 3: PATCH 禁用/启用** `{ status: 'active' | 'disabled' }`;若禁用,删除该用户所有 `sessions`(SQLite `delete from sessions where user_id = ?`)。 - [ ] **Step 4: 保护路由** 这些路径 **不要** allowlist;依赖全局 auth guard + `requireAdmin`。 - [ ] **Step 5: Commit** ```bash git add server/api/admin git commit -m "feat(admin): user provisioning and status endpoints" ``` --- ### Task 7: 可见性与 shareToken 工具 + 单元测试 **Files:** - Create: `server/constants/visibility.ts` - Create: `server/utils/share-token.ts` - Create: `server/utils/visibility.test.ts` - [ ] **Step 1: 定义 `VISIBILITY` 与 Zod** `private | unlisted | public` 三值。 - [ ] **Step 2: `ensureShareToken(visibility, existing?: string | null)`** `unlisted` 且无 existing → 生成;`public/private` → 返回 `null`(或保留策略:清 token)。 - [ ] **Step 3: 测试** Run: ```bash bun test server/utils/visibility.test.ts ``` - [ ] **Step 4: Commit** ```bash git add server/constants/visibility.ts server/utils/share-token.ts server/utils/visibility.test.ts git commit -m "test: visibility and share token helpers" ``` --- ### Task 8: RSS URL 校验 + 测试 **Files:** - Create: `server/utils/rss-url.ts` - Create: `server/utils/rss-url.test.ts` - [ ] **Step 1: 实现 `assertSafeHttpUrl(url: string)`** - 仅 `http:`/`https:` - 使用 `new URL(url)`;hostname 非空 - `dns.lookup` 或先 `fetch` 前解析 IP(若环境限制,可先用 `isHostnameIpLiteral` 分支处理 IP 字面量禁止私网段) - 拒绝 obvious 内网 host(`localhost`、`.local` 可选) - [ ] **Step 2: 测试用例** 允许 `https://example.com/feed`,拒绝 `http://127.0.0.1/`、`http://192.168.1.1/`。 - [ ] **Step 3: Commit** ```bash git add server/utils/rss-url.ts server/utils/rss-url.test.ts git commit -m "feat(rss): block obvious SSRF targets for feed URLs" ``` --- ### Task 9: Posts 服务与 `me`/`public` API **Files:** - Create: `server/service/posts/index.ts` - Create: `server/api/me/posts.get.ts`、`me/posts.post.ts`、`me/posts/[id].get.ts`、`me/posts/[id].put.ts`、`me/posts/[id].delete.ts` - Create: `server/api/public/posts/[publicSlug].get.ts`(或聚合在 profile API) - [ ] **Step 1: 服务层函数** `listForUser`、`create`、`update`、`delete`,均在 SQL 层带 `userId`。 - [ ] **Step 2: `public` 列表** 仅 `visibility='public'` 且用户 `status=active`;按 `publishedAt` 排序。 - [ ] **Step 3: unlisted 不进入列表** 由 Task 10 的专用路由处理。 - [ ] **Step 4: Commit** ```bash git add server/service/posts server/api/me/posts server/api/public git commit -m "feat(posts): CRUD and public listing" ``` --- ### Task 10: Timeline 与 APIs **Files:** - Create: `server/service/timeline/index.ts` - Create: `server/api/me/timeline.*.ts` - 扩展: `server/api/public/profile/[publicSlug].get.ts` 合并时间线 - [ ] **Step 1: CRUD 与可见性字段同步** 更新 `visibility` 时调用 `ensureShareToken`。 - [ ] **Step 2: Commit** ```bash git add server/service/timeline server/api/me/timeline git commit -m "feat(timeline): CRUD with visibility" ``` --- ### Task 11: Profile `me` + 公开聚合 API **Files:** - Create: `server/service/profile/index.ts` - Create: `server/api/me/profile.get.ts`、`me/profile.put.ts` - Create: `server/api/public/profile/[publicSlug].get.ts` - [ ] **Step 1: 公开聚合返回结构** ```json { "user": { "publicSlug", "nickname", "avatar" }, "bio": { "markdown", "visibility" }, "links": [{ "label", "url", "visibility" }], "posts": [ ...public only ], "timeline": [ ...public only ], "rssItems": [ ...public only ] } ``` 仅包含 `visibility === 'public'` 的块;`bio`/`avatar` 遵循各自 visibility。 - [ ] **Step 2: disabled 用户** 查询用户时若 `status !== 'active'` → 404。 - [ ] **Step 3: Commit** ```bash git add server/service/profile server/api/me/profile server/api/public/profile git commit -m "feat(profile): editable profile and public aggregate endpoint" ``` --- ### Task 12: RSS 订阅、抓取、去重、`me` API **Files:** - Modify: `package.json`(添加 XML 解析依赖,例如 `fast-xml-parser`) - Create: `server/service/rss/fetch.ts` - Create: `server/service/rss/parser.ts` - Create: `server/api/me/rss/feeds.*.ts`、`me/rss/items.*.ts`、`me/rss/sync.post.ts` - [ ] **Step 1: 安装依赖** Run: ```bash cd /home/dash/projects/person-panel && bun add fast-xml-parser ``` - [ ] **Step 2: 抓取流程** 对每个 feed:`assertSafeHttpUrl` → `fetch`(timeout + max bytes 读取截断)→ 解析 item → `insert` 忽略唯一冲突;新行 `visibility: private`。 - [ ] **Step 3: `sync.post.ts`** 触发当前用户所有到期源或指定 `feedId`。 - [ ] **Step 4: Commit** ```bash git add package.json server/service/rss server/api/me/rss git commit -m "feat(rss): feeds, items, fetch, and manual sync" ``` --- ### Task 13: 进程内调度器 **Files:** - Create: `server/service/rss/scheduler.ts` - Create: `server/plugins/04.rss-scheduler.ts` - [ ] **Step 1: `ready` 钩子** `setInterval` 每 `RSS_SYNC_INTERVAL_MINUTES` 扫描;`setTimeout` 启动 5s 后补跑。 - [ ] **Step 2: 并发池** 例如 `p-limit` 风格手写 semaphore:`RSS_MAX_CONCURRENT_FEEDS`。 - [ ] **Step 3: Commit** ```bash git add server/service/rss/scheduler.ts server/plugins/04.rss-scheduler.ts git commit -m "feat(rss): in-process polling scheduler" ``` --- ### Task 14: Unlisted 公开详情 API **Files:** - Create: `server/api/public/unlisted/[publicSlug]/[shareToken].get.ts`(路径按 Nitro 约定调整) - [ ] **Step 1: 查询顺序** 依次尝试 `posts`、`timeline_events`、`rss_items`(可加 `type` 查询参数减少歧义;若 token 全局唯一概率足够可单次 union)。 推荐:**token 全局唯一**(随机 32 byte)→ 单表查询 `where shareToken = ? AND user.publicSlug = ? join users`。 - [ ] **Step 2: 返回 404** 用户 disabled 或 `visibility !== 'unlisted'`。 - [ ] **Step 3: Commit** ```bash git add server/api/public/unlisted git commit -m "feat(public): resolve unlisted share links" ``` --- ### Task 15: 前端路由与 auth 规则 **Files:** - Modify: `app/utils/auth-routes.ts` - Modify: `app/middleware/auth.global.ts`(若需把 `/me` 纳入保护) - Modify: `app/pages/login/index.vue`(文案) - [ ] **Step 1: `PUBLIC_ROUTE_PREFIXES`** 加入 `"/@"`、`"/p/"`。 - [ ] **Step 2: 登录后落地页** 指向 `/me` 或 `/dashboard`(与侧栏一致)。 - [ ] **Step 3: 注册页策略** 若关闭自助注册:`allowRegister` 配置为 false(沿用 `useGlobalConfig`),`register` 路由 middleware 重定向登录或 404。 - [ ] **Step 4: Commit** ```bash git add app/utils/auth-routes.ts app/middleware/auth.global.ts app/pages/login/index.vue git commit -m "feat(app): public routes for profile and unlisted links" ``` --- ### Task 16: Nuxt UI — 公开页与后台壳 **Files:** - Create: `app/pages/@/[publicSlug]/index.vue` - Create: `app/pages/p/[publicSlug]/t/[shareToken].vue` - Create: `app/layouts/public.vue` - Modify: `app/components/AppShell.vue` - Create: `app/pages/me/index.vue`、`app/pages/me/posts/**` 等(按最终 IA) - [ ] **Step 1: 公开主页** `useFetch('/api/public/profile/' + slug)`;`UPage`、`UCard` 列表;空状态 `UEmpty`。 - [ ] **Step 2: 分享页** `useFetch` unlisted API;403/404 用 `UAlert`。 - [ ] **Step 3: `AppShell` 侧栏** 使用 `UNavigationMenu`;admin 显示「用户管理」。 - [ ] **Step 4: Commit** ```bash git add app/pages app/layouts/public.vue app/components/AppShell.vue git commit -m "feat(ui): public and authenticated shells with Nuxt UI" ``` --- ### Task 17: 集成验证与文档收尾 - [ ] **Step 1: 手测脚本(记录在 PR 描述)** 1. 创建 admin + 普通用户;普通用户登录创建 `publicSlug`。 2. 写文章 `public`,匿名访问 `/@slug` 可见。 3. 写 `unlisted`,匿名访问 `/p/slug/t/token` 可见且不在列表。 4. 添加 RSS,手动 sync,条目默认 `private`,改 `public` 后出现在公开页。 5. 禁用用户,公开页 404。 - [ ] **Step 2: 文档** 如有实现偏差,更新 `docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md` 对应段落;**全部验收通过后再**将 spec 顶部状态改为「已实现」。 - [ ] **Step 3: Commit(若有文档变更)** ```bash git add docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md git commit -m "docs: sync spec with implementation" ``` --- ## Plan Self-Review(对照 spec) | Spec 章节 | 覆盖任务 | |-----------|----------| | §1–2 目标/模块 | Task 1–17 | | §3 数据模型/可见性/token | Task 1,7,9–12,14 | | §4 API 分层 | Task 4–6,9–14 | | §5 管理员/bootstrap/注册 | Task 2,6,15 | | §6 UI/§6.1 体验 | Task 15–16 | | §7 定时任务 | Task 13 | | §8 安全 | Task 5,8,12 | | §9–11 错误/测试/配置 | Task 7–8 测试 + Task 17 | **占位符扫描:** 无 TBD;具体页面路径在 Task 16 允许按实现微调但需保持单一路由风格。 **类型一致性:** `MinimalUser` 在 Task 3 增加 `role`;`visibility` 三态在 Task 7 单列常量。 --- **Plan complete and saved to `docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md`. Two execution options:** 1. **Subagent-Driven (recommended)** — 每个 Task 派生子代理并在任务间复核,迭代快 2. **Inline Execution** — 本会话用 executing-plans 批量执行并设检查点 **你更倾向哪一种?**