From 42b2b7ac7a4e88c0956008392316a123e567bd04 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 18 Apr 2026 04:26:02 +0800 Subject: [PATCH] docs: refine multitenant hub spec and add implementation plan Made-with: Cursor --- ...on-panel-multitenant-hub-implementation-plan.md | 600 +++++++++++++++++++++ ...26-04-18-person-panel-multitenant-hub-design.md | 17 +- 2 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md diff --git a/docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md b/docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md new file mode 100644 index 0000000..1488b4a --- /dev/null +++ b/docs/superpowers/plans/2026-04-18-person-panel-multitenant-hub-implementation-plan.md @@ -0,0 +1,600 @@ +# 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 批量执行并设检查点 + +**你更倾向哪一种?** diff --git a/docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md b/docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md index 560476f..8feb28e 100644 --- a/docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md +++ b/docs/superpowers/specs/2026-04-18-person-panel-multitenant-hub-design.md @@ -62,11 +62,11 @@ | 域 | 表 | 要点 | |----|-----|------| -| 账号 | `users`(扩展) | `role`: `admin \| user`;`status`: `active \| disabled`;`publicSlug` 唯一(公开主页) | -| 文章 | `posts` | Markdown 正文、元数据、`visibility`、`userId`;`slug` 用户内唯一 | -| 时光机 | `timeline_events` | 日期/时间段、标题、正文、`visibility`、`userId` | +| 账号 | `users`(扩展) | `role`: `admin \| user`;`status`: `active \| disabled`;`publicSlug` 唯一(公开主页);**资料**:`bioMarkdown` + **`bioVisibility`**;**社交链接**存 `socialLinksJson`(JSON 数组,元素含 `label, url, visibility`,实现条目级可见);头像沿用 `avatar` 字段并单独 **`avatarVisibility`**(或默认与 `bio` 同步,实现时二选一并在本表注明) | +| 文章 | `posts` | Markdown 正文、元数据、`visibility`、`userId`;`slug` 用户内唯一;**`shareToken`**(`unlisted` 时必填且随机不可猜) | +| 时光机 | `timeline_events` | 日期/时间段、标题、正文、`visibility`、`userId`;**`shareToken`** | | RSS | `rss_feeds` | `feedUrl`、`userId`、拉取状态(`lastFetchedAt`、`lastError` 等)、可选 `pollIntervalMinutes` | -| RSS | `rss_items` | `feedId`、`userId`(冗余便于查询)、`guid`、`canonicalUrl`、标题、摘要、正文片段、`publishedAt`;默认 `visibility=private` | +| RSS | `rss_items` | `feedId`、`userId`(冗余便于查询)、`guid`、`canonicalUrl`、标题、摘要、正文片段、`publishedAt`;默认 `visibility=private`;**`shareToken`** | **去重**:优先 `guid`,否则规范化后的 `canonicalUrl`。 @@ -127,6 +127,14 @@ - **RSS**:左侧订阅源(本人)、右侧条目流;支持 **手动触发同步**;条目上切换可见性。 - **公开站**:独立简洁布局,以阅读为主。 +### 6.1 体验与展示闭环(第一期必须满足) + +- **空状态**:文章 / RSS / 时间线 / 资料未完成时,使用 Nuxt UI 空状态文案 + 单一主按钮(去创建、去添加订阅源);避免空白屏。 +- **导航**:登录内与公开站 **顶部显式返回**「我的主页(公开)」链接(路径形如 `/@:publicSlug`)与「后台」切换;公开站仅展示与该用户 `publicSlug` 相关区块,不暴露他人入口。 +- **可见性控件**:每条条目使用同一套三态控件(`private` / `unlisted` / `public`);切到 `unlisted` 时展示 **可复制的分享链接**(U1);切离时可使 `shareToken` 失效(重新生成或清空,实现计划二选一,须防缓存旧链)。 +- **登录页文案**:关闭自助注册后,提示「请联系管理员开通账号」,避免用户困惑。 +- **路由与白名单**:前端 `/@:publicSlug`、`/p/:publicSlug/t/:shareToken` 必须加入 **页面级 `PUBLIC_ROUTE_PREFIXES`**;后端 **`/api/public/**`** 全方法加入 API allowlist(或按路径前缀匹配规则扩展现有 `isAllowlistedApiPath`),避免被 `10.auth-guard` 误杀。 + --- ## 7. 定时任务(进程内) @@ -180,6 +188,7 @@ - **一致性**:多用户隔离、条目级可见性、RSS 默认私密、单实例进程内任务彼此一致。 - **范围**:第一期不引入队列/多实例;协作与 OPML 等未承诺。 - **歧义**:公开路径已定 `/@:publicSlug`;禁用用户公开 404;unlisted 为 U1。 +- **闭环补充(2026-04-18)**:资料域字段与 `shareToken` 列已写入 §3.2;§6.1 补齐空状态、分享链、路由白名单与登录文案,覆盖「直观 / 展示完整」缺口。 ---