feat: add Feishu user authorization flow documentation and update frontend guidelines

- Added new documentation for the Feishu user authorization flow, detailing the backend protocol, authorization initiation, callback handling, and token management.
- Updated frontend index to link to the new authorization flow documentation for better accessibility and guidance on UI design consistency.
This commit is contained in:
yuxuanhui
2026-08-31 09:18:03 +08:00
parent 3d838866cd
commit dcd6d44960
15 changed files with 3817 additions and 732 deletions
+3 -1
View File
@@ -1 +1,3 @@
{} {
"alwaysUpdateLinks": true
}
+27 -29
View File
@@ -4,21 +4,17 @@
"type": "split", "type": "split",
"children": [ "children": [
{ {
"id": "95aa7b7e869f44b5", "id": "f78f28cd220415fc",
"type": "tabs", "type": "tabs",
"children": [ "children": [
{ {
"id": "8b8d7a02c4ac5b2d", "id": "a42c24f4b8ea2969",
"type": "leaf", "type": "leaf",
"state": { "state": {
"type": "markdown", "type": "empty",
"state": { "state": {},
"file": "notes/工作流整理.md",
"mode": "source",
"source": false
},
"icon": "lucide-file", "icon": "lucide-file",
"title": "工作流整理" "title": "新标签页"
} }
} }
] ]
@@ -41,7 +37,9 @@
"type": "file-explorer", "type": "file-explorer",
"state": { "state": {
"sortOrder": "alphabetical", "sortOrder": "alphabetical",
"autoReveal": false "autoReveal": false,
"showSearch": false,
"searchQuery": ""
}, },
"icon": "lucide-folder-closed", "icon": "lucide-folder-closed",
"title": "文件列表" "title": "文件列表"
@@ -180,10 +178,27 @@
"bases:新建数据库": false "bases:新建数据库": false
} }
}, },
"active": "8b8d7a02c4ac5b2d", "active": "a42c24f4b8ea2969",
"lastOpenFiles": [ "lastOpenFiles": [
"docs/Matt 工作流 × 飞书 CLI 全流程总结.md", "AGENTS.md",
"study",
"TODO.md",
"AI Coding/index.md",
"AI Coding/maintenance/index.md",
"AI Coding/maintenance/conflicts.md",
"AI Coding/maintenance/promotion-queue.md",
"AI Coding/patterns/index.md",
"AI Coding/experiments/index.md",
"AI Coding/inbox/index.md",
"AI Coding/learnings/index.md",
"AI Coding/inbox/20260725-feishu-cli-base-wiki-tracker.md",
"AI Coding/inbox/20260724-bounded-list-flex-height-chain.md",
"AI Coding/inbox/20260627-cloud-runtime-project-root.md",
"AI-RD-Workflow/index.md",
"projects/feishu/飞书用户态接口授权流程(Auth -> Callback -> DataApi).md",
"notes/midscenejs的llm-full.txt.md",
"notes/工作流整理.md", "notes/工作流整理.md",
"docs/Matt 工作流 × 飞书 CLI 全流程总结.md",
"AGENTS.md.md", "AGENTS.md.md",
"TODO.md.md", "TODO.md.md",
"docs/Matt 工作流 × 飞书 Skills × Trellis Coding 闭环总结.md", "docs/Matt 工作流 × 飞书 Skills × Trellis Coding 闭环总结.md",
@@ -191,24 +206,7 @@
"docs/MonoProxy订阅信息获取工作流.md", "docs/MonoProxy订阅信息获取工作流.md",
"docs/MidScene 配置.md", "docs/MidScene 配置.md",
"docs/Trellis × 飞书实现闭环初步方案.md", "docs/Trellis × 飞书实现闭环初步方案.md",
"AI-RD-Workflow/index.md",
"AI-RD-Workflow/40-workflows/ai-development-workflow.md",
"AI-RD-Workflow/40-workflows/pm-workflow.md",
"AI-RD-Workflow/40-workflows/rd-workflow.md",
"AI-RD-Workflow/40-workflows/se-workflow.md",
"AI Coding/AI-RD-Workflow/40-workflows/trellis-matt/workflow.md",
"AI Coding/AI-RD-Workflow/40-workflows/trellis-matt/AGENTS.md",
"AI-RD-Workflow/40-workflows/trellis-matt/CN", "AI-RD-Workflow/40-workflows/trellis-matt/CN",
"AI-RD-Workflow/00-meta/artifact-model.md",
"AI-RD-Workflow/20-skills/pm-requirement-refine/SKILL.md",
"AI-RD-Workflow/20-skills/rd-design-generate/SKILL.md",
"AI-RD-Workflow/20-skills/pm-requirement-review/SKILL.md",
"AI-RD-Workflow/00-meta/glossary.md",
"AI-RD-Workflow/00-meta/naming-conventions.md",
"AI-RD-Workflow/10-standards/review-gates.md",
"AI-RD-Workflow/10-standards/lifecycle.md",
"AI-RD-Workflow/00-meta/roadmap.md",
"AI-RD-Workflow/30-templates/intake.md",
"docs", "docs",
"notes", "notes",
"projects", "projects",
@@ -37,7 +37,7 @@ Base 适合保存一行一个产物及其可查询状态;Wiki/Docs 适合保
- 在不删除或转换原字段的前提下,将目标表验证为共 23 个字段,包含状态、类型、负责人、进度、证据、父项和依赖等工作流字段。 - 在不删除或转换原字段的前提下,将目标表验证为共 23 个字段,包含状态、类型、负责人、进度、证据、父项和依赖等工作流字段。
- 创建并分段追加 Wiki Docx,最终回读 revision 6,确认写入内容可取回。 - 创建并分段追加 Wiki Docx,最终回读 revision 6,确认写入内容可取回。
- 创建 Base POC 记录并用真实 record ID 回读,确认标题、文档链接、状态、完成度、验收标准和验证证据。 - 创建 Base POC 记录并用真实 record ID 回读,确认标题、文档链接、状态、完成度、验收标准和验证证据。
- 执行经验固化位置:`/Users/yuxuanhui/.agents/skills/setup-matt-pocock-skills-feishu/references/issue-tracker-feishu.md`。 - 执行经验固化位置:`/Users/yuxuanhui/.agents/skills/setup-workflow-skills-feishu/references/issue-tracker-feishu.md`。
- 已省略实际 Base/Wiki URL、token、record ID 和组织信息;这些值只属于目标环境,不属于通用经验。 - 已省略实际 Base/Wiki URL、token、record ID 和组织信息;这些值只属于目标环境,不属于通用经验。
- 第二轮将原文本字段原地重命名为“产物文档”,保持同一 field ID、`text/plain` 类型和既有链接值;新增来源链接、外部编号、报告人、最后反馈时间、最后分诊时间后,完整字段回读为 28。 - 第二轮将原文本字段原地重命名为“产物文档”,保持同一 field ID、`text/plain` 类型和既有链接值;新增来源链接、外部编号、报告人、最后反馈时间、最后分诊时间后,完整字段回读为 28。
- 第二轮创建并回读 1 条 Spec、2 条 Ticket 和 1 条 Issue;Ticket 采用“两遍写入”,先建所有记录,再写父项和 blocker,逐条确认链接字段。 - 第二轮创建并回读 1 条 Spec、2 条 Ticket 和 1 条 Issue;Ticket 采用“两遍写入”,先建所有记录,再写父项和 blocker,逐条确认链接字段。
@@ -0,0 +1,130 @@
---
id: 20260805-request-package-multi-instance-dev-backend
title: 统一 request 包并按目标系统创建多个请求实例
created: 2026-08-05
updated: 2026-08-05
status: candidate
scope: global
category: frontend-architecture
confidence: medium
last_verified: 2026-08-05
promotion_target: pattern
projects:
- oppein-knowledge
tags:
- request-package
- http-client
- multi-instance
- oauth2
- vite-proxy
- environment-config
---
# 统一 request 包并按目标系统创建多个请求实例
## Trigger
前端项目同时调用多个后端系统,且不同接口需要不同的 `baseURL`、请求头、认证令牌或开发代理路径;尤其是本地开发时,业务服务可能需要访问本地后端,而登录、用户、菜单等接口仍需要访问外部网关。
## Context
本次欧派学习平台前端改造中,项目引入统一的 `@oppein-react/request` 包,替换业务侧分散的请求封装。项目存在两类请求目标:
1. 当前项目后端接口,例如课程、讲师、资料和题库接口。
2. 欧派外部网关接口,例如 OAuth 登录、退出、当前用户、菜单和用户中心接口。
本地开发还存在两种运行方式:
- 当前项目后端运行在 `http://abc.oppein.com:8080`;
- 当前项目后端通过 UAT 网关的 `https://apigatewayuat.oppein.com/knowledge` 路由访问。
如果所有请求共用一个客户端,容易把外部网关接口发到项目后端,或者把项目后端接口发到网关;如果把环境变量的上游目标地址直接当作浏览器请求地址,又会绕过 Vite proxy,产生跨域或本地端口不可达问题。
## Evidence
- `assp-knowledge-frontend/src/utils/request-client.ts:3-9`:统一从 `@oppein-react/request` 引入 `createRequest`、错误类型和请求类型。
- `assp-knowledge-frontend/src/utils/request-client.ts:100-105`:`knowledgeRequestClient` 使用当前项目后端请求基址,并按本地后端目标决定是否注入本地 JWT。
- `assp-knowledge-frontend/src/utils/request-client.ts:123-145`:分别暴露 `knowledgeRequest` 和 `gatewayRequest` 两个请求实例;前者服务当前项目后端,后者服务欧派外部网关。
- `assp-knowledge-frontend/src/utils/request-client.ts:59-80`:固定应用头、`Oauth2-AccessToken` 和本地 `Oauth2-Jwt` 由动态 Header provider 统一生成,业务 API 不再重复拼装。
- `assp-knowledge-frontend/src/common/config.ts:23-34`:把环境变量提供的上游目标地址与浏览器实际请求基址分开;开发环境分别使用 `/knowledge` 和同源空基址进入 Vite proxy。
- `assp-knowledge-frontend/vite.config.ts:29-38`:`/knowledge` proxy 根据目标地址是否包含路径前缀决定 rewrite;本地 8080 去掉 `/knowledge`,网关目标保留 `/knowledge`。
- `assp-knowledge-frontend/vite.config.ts:90-98`:当前项目后端统一使用一个 `/knowledge` proxy,外部网关仅保留 `/oauth`、`/ucenterapi`、`/user` 三类入口。
- `assp-knowledge-frontend/.env.example:3-36`:已记录本地 8080、UAT 网关和生产网关的配置差异,明确网关模式的 `VITE_API_BASE_URL` 必须包含 `/knowledge`。
- 2026-08-05:通过 TypeScript 检查、UAT/生产构建、`git diff --check` 和临时 mock upstream proxy smoke test 验证请求实例与 rewrite 方案;本地目标收到 `/auth/currentUser`,网关目标保留 `/oauth/logout`、`/ucenterapi/*` 和 `/user/*` 原始路径。
- 2026-08-05:实际 UAT 路由对比验证:`https://apigatewayuat.oppein.com/course/list` 和不带前缀的讲师接口返回 `404`,带 `/knowledge` 的对应接口返回 `400`,证明网关路由前缀必须保留;`400` 来自缺少真实认证或业务参数,不代表路由不存在。
## Root cause
已验证:请求客户端的职责不只是发送 HTTP,还携带目标系统的认证和路径契约。当前项目后端与外部网关的请求头、令牌来源、错误处理和 URL 入口不同,因此必须使用多个由同一 `request` 包创建的实例,而不是在业务 API 中手写不同的 `fetch` 或修改单个全局客户端。
已验证:开发环境需要把“上游目标地址”和“浏览器请求地址”分离。浏览器只能请求同源的 `/knowledge`、`/oauth`、`/ucenterapi`、`/user`,Vite proxy 再根据 `VITE_API_BASE_URL` 和 `VITE_EXTERNAL_API_BASE_URL` 转发到本地后端或网关。
已验证:网关路由前缀是目标地址的一部分。`VITE_API_BASE_URL=https://apigatewayuat.oppein.com` 会使 `/knowledge/course/list` 被 rewrite 成 `/course/list`,从而命中网关 `404`;正确的网关配置必须是 `https://apigatewayuat.oppein.com/knowledge`。
推断:以后遇到“一个前端项目在开发态切换本地后端与远程网关”的项目,优先设计多个语义明确的 request 实例,并让 proxy 根据目标 URL 的 pathname 处理前缀,比为每个业务接口复制代理规则更稳定。
## Preferred action
1. 先按目标系统划分请求实例,而不是按页面或接口数量划分:
- `knowledgeRequest`:当前项目后端;
- `gatewayRequest`:外部 OAuth、用户、菜单和权限网关。
2. 所有实例都通过统一 `request` 包创建,复用请求方法、错误类型、响应处理和动态 Header provider。
3. 把认证差异放在实例配置或 Header provider 中:
- 网关实例统一注入 `AppCode`、`SubAppCode`、`X-Requested-With` 和 `Oauth2-AccessToken`;
- 本地项目后端按需注入 `Oauth2-Jwt`;
- 业务 API 不重复手写认证头。
4. 在配置层保留两组概念:
- `apiBaseUrl` / `externalApiBaseUrl`:环境变量提供的上游目标地址;
- `apiRequestBaseUrl` / `externalRequestBaseUrl`:浏览器实际请求基址。
5. 开发环境始终优先使用同源相对路径:
- 当前项目后端使用 `/knowledge`;
- 外部网关使用 `/oauth`、`/ucenterapi`、`/user`。
6. 统一 proxy 时,让目标 URL 的 pathname 决定是否追加或保留服务前缀:
- 本地 `http://abc.oppein.com:8080`:`/knowledge/foo` → `/foo`;
- 网关 `https://apigatewayuat.oppein.com/knowledge`:`/knowledge/foo` → `/knowledge/foo`。
7. `.env.example` 必须同时给出本地后端和网关模式,并明确“本地目标不带 `/knowledge`、网关目标必须带 `/knowledge`”。
8. 验证时至少分别检查一个项目后端接口和一个外部网关接口的最终上游路径,不要只检查浏览器看到的相对 URL。
## Boundaries
- 这是“多个目标系统共享同一 request 包”的模式,不意味着所有项目都必须命名为 `knowledgeRequest` 和 `gatewayRequest`。
- 如果所有接口确实属于同一个后端系统,单一 request 实例更简单;不要为了形式创建多个实例。
- `VITE_EXTERNAL_API_BASE_URL` 的具体网关路径取决于网关契约;本条只约束当前项目后端的 `/knowledge` 路由必须与目标地址保持一致。
- 代理只能解决开发环境的同源转发,不能替代生产环境的网关路由、鉴权配置或 CORS 策略。
- 没有真实登录态时,网关接口返回 `400`、`401` 或业务错误不能单独证明代理失败;应先区分“路由是否命中”和“鉴权/业务参数是否有效”。
## Failed approaches
- 用一个 request 实例承载当前项目后端和外部网关:请求可能命中错误服务,且认证头边界不清晰。
- 在每个业务 API 中直接写 `fetch`、`axios` 或手工拼接 `Oauth2-AccessToken`:会绕过统一错误处理和动态令牌注入,后续修改难以收敛。
- 把 `VITE_API_BASE_URL=https://apigatewayuat.oppein.com` 当作 UAT 项目后端配置:Vite 会把 `/knowledge/course/list` rewrite 成 `/course/list`,网关返回 `404`。
- 把绝对上游地址直接作为开发环境浏览器 base URL:会绕过 Vite proxy,导致跨域或无法访问本地端口。
- 为 `/material`、`/course`、`/lecturer` 等每个业务前缀分别复制 proxy:本地后端和网关环境的路径规则容易逐条漂移,新增接口还要重复改配置。
## Examples
```ts
const knowledgeRequestClient = createRequest({
baseURL: apiRequestBaseUrl,
headers: createKnowledgeHeaders,
});
const gatewayRequestClient = createRequest({
baseURL: externalRequestBaseUrl,
headers: createGatewayHeaders,
});
```
```env
# 本地后端
VITE_API_BASE_URL=http://abc.oppein.com:8080
VITE_EXTERNAL_API_BASE_URL=https://apigatewayuat.oppein.com
# 或:UAT 网关
VITE_API_BASE_URL=https://apigatewayuat.oppein.com/knowledge
VITE_EXTERNAL_API_BASE_URL=https://apigatewayuat.oppein.com
```
## Promotion record
- Not promoted. 当前先作为候选经验保存在中央知识库;如果后续在另一个项目或第二次独立改造中复用并验证,再考虑提升为通用 pattern、模板或自动检查。
@@ -0,0 +1,101 @@
---
id: 20260814-docker-postgresql-gitea-same-host-deployment
title: Docker Compose、外部 PostgreSQL 与 Gitea 同机部署需要显式拓扑契约
created: 2026-08-14
updated: 2026-08-14
status: promoted
scope: global
category: deployment
confidence: medium
last_verified: 2026-08-14
promotion_target: skill
projects:
- zhixing-system
tags:
- docker-compose
- postgresql
- gitea-actions
- self-hosted-runner
- single-host-deployment
---
# Docker Compose、外部 PostgreSQL 与 Gitea 同机部署需要显式拓扑契约
## Trigger
创建新的个人 Web 项目,并准备继续采用 Docker、PostgreSQL、Gitea Actions 与单机反向代理组合;或者需要从已有项目复制部署文件,但不确定哪些内容是通用骨架、哪些内容绑定当前服务器或业务 Job。
## Context
`zhixing-system` 使用前后端分离源码、Docker Compose 编排和 Gitea Actions 部署。开发环境由 Compose 启动项目专属 PostgreSQL;生产环境复用 1Panel 管理的 PostgreSQL,通过外部 Docker 网络连接。Gitea Runner 在目标 Docker 主机上检出代码、原地构建镜像并更新同一个 Compose 项目,不经过镜像仓库。
这套方案适合低运维成本的单机个人项目,但真正可复用的不是项目名和端口,而是以下契约:开发与生产数据库拓扑分离、生产配置失败即停止、常驻服务与一次性 Job 分离、容器身份与数据卷权限匹配、内部服务不直接暴露公网、部署流程必须验证配置和健康状态。
## Evidence
- 2026-08-14 检查 `zhixing-system/docker-compose.dev.yml`:开发 Compose 包含 `postgres:16-alpine`、数据库健康检查、持久卷、后端和前端热更新卷;`migrate` 与 `market-sync` 放在 `jobs` profile 中。
- 2026-08-14 检查 `zhixing-system/docker-compose.prod.yml`:生产 Compose 不启动 PostgreSQL,后端与 Job 通过外部 `1panel-network` 访问数据库;`ZHIXING_DATABASE_URL` 使用 Compose 的 `${VAR:?message}` 形式禁止回退到开发数据库;Web 仅绑定 `127.0.0.1:8111`。
- 2026-08-14 检查 `zhixing-system/.gitea/workflows/deploy-production.yaml`:`main` push 或手动触发后,执行 checkout、两种 profile 的 Compose 校验、原地 build/up、迁移、Web 健康检查,并在成功或失败时执行 `docker compose ps`。
- 2026-08-14 检查 `zhixing-system/zhixing-server/Dockerfile` 与 `zhixing-system/zhixing-web/Dockerfile`:开发/生产多阶段构建;生产后端使用 UID 10001 非 root 用户;前端以 Nginx 提供静态文件,并在容器启动时注入后端 upstream;两个镜像都包含健康检查。
- Git 提交 `42942af` 将生产 Web 端口从所有地址改为只绑定 loopback,并删除完成使命的 Runner smoke-test workflow。
- Git 提交 `afdc5ca` 将生产内置 PostgreSQL 改为 1Panel 外部 PostgreSQL,增加 Gitea secrets、生产连接串强制校验、Job profile 校验和数据库迁移步骤。
- Git 提交 `eabf102` 证明镜像构建阶段对目录 `chown` 不足以覆盖运行时新建的 named volume;项目增加 root 身份的 `market-data-init` 一次性容器,将卷目录归属修正为 UID 10001。
- Git 提交 `9023e00` 在 Debian 与 Alpine 生产镜像中安装时区数据,同时通过镜像和 Compose 显式统一 `Asia/Shanghai`。
- 使用无敏感值占位连接串实际执行以下四项配置校验,退出码均为 0:开发默认 profile、开发 `jobs` profile、生产默认 profile、生产 `jobs` profile;命令均为 `docker compose --env-file /dev/null -f <file> [--profile jobs] config --quiet`。
- 中央知识库在写入前搜索 `docker`、`compose`、`postgresql`、`gitea` 等关键词,未发现同根因条目。
## Root cause
已验证:
- 开发与生产的 PostgreSQL 拓扑不同。开发依赖 Compose 内部服务名 `postgres`;生产依赖预先存在的外部网络、稳定数据库网络别名和单独管理的凭据。试图让同一份默认连接串覆盖两者,会把开发便利性带入生产并掩盖配置错误。
- Docker 镜像内的目录所有权与运行时 named volume 的所有权不是同一件事。卷首次挂载时可能由 root 初始化,导致非 root 应用无法写入;需要显式的卷初始化或宿主机预置步骤。
- Gitea 同机部署依赖稳定的 `COMPOSE_PROJECT_NAME` 维持容器、网络和 named volume 身份,而不是依赖 Runner 每次 checkout 的绝对路径。
- `depends_on.condition` 只有在依赖服务定义了可用的 healthcheck 或一次性 Job 能正确返回退出码时才有意义。
- 只把生产入口绑定到 loopback,可以让 1Panel/Nginx/Caddy 承担公网 TLS 与路由,避免应用容器端口绕过反向代理直接暴露。
推断:
- 当前 Gitea workflow 先 `up -d --build`、后执行迁移。若未来应用启动依赖新 schema,或者迁移包含破坏性变更,这个顺序可能让新容器先进入不兼容状态。新项目必须显式选择“先迁移再切换”或 expand/contract 迁移,而不是机械复制当前顺序。
- 当前 workflow 没有在仓库内表达部署串行化、自动回滚、数据库备份、镜像留档或 Runner 配置。单 Runner、低流量个人项目可能暂时接受,但这些能力不能被误认为已由 Gitea/Compose 自动提供。
- 当前直接在目标主机构建镜像,部署简单,但缺少不可变镜像版本和快速回滚点;当构建变慢、主机增多或可用性要求提高时,应改为构建并推送带提交 SHA 的镜像,再由生产 Compose 拉取指定版本。
## Preferred action
新建同类型项目时,先复制结构,再替换项目参数,不要直接复制当前业务变量:
1. 建立 `Dockerfile` 的 development/production target;依赖使用 lockfile 固定,生产阶段只包含运行依赖,服务进程使用固定的非 root UID,并为每个容器提供不依赖额外调试工具的健康检查。
2. 保留独立的 `docker-compose.dev.yml` 与 `docker-compose.prod.yml`。开发 Compose 可以内置 PostgreSQL、源码热更新卷和有边界的本地默认凭据;生产 Compose 不应包含开发凭据或隐式数据库 fallback。
3. 生产 PostgreSQL 通过外部网络接入时,显式声明 `external: true`,使用稳定网络别名,不引用易变化的容器实例名;连接串由 Gitea secret 注入,并用 `${DATABASE_URL:?Set ...}` 在配置展开阶段失败。
4. 把 migration、seed、backup、cron task 等一次性命令建模为 Compose profile 下的 Job。Job 通过退出码报告结果,不在常驻 Web 进程内部偷偷执行迁移或调度。
5. named volume 由非 root 服务写入时,增加幂等的 init Job,或在首次部署 runbook 中显式预置 UID/GID;不能只依赖 Dockerfile 中对镜像目录的 `chown`。
6. Web 容器只绑定 `127.0.0.1:<port>`,由宿主机反向代理统一处理公网域名与 TLS;浏览器使用同源 `/api`,Nginx upstream 通过运行时环境变量指向 Compose 后端服务名。
7. Gitea workflow 至少包含:checkout、默认与 Job profile 的 `docker compose config --quiet`、build、迁移策略、`up -d --remove-orphans`、应用级健康检查、`if: always()` 的服务状态输出。固定 `COMPOSE_PROJECT_NAME`,并明确 Runner 必须运行在目标主机且具备所需 Docker 权限。
8. 在第一个会修改 schema 的版本之前确定发布顺序:低停机项目可在维护窗口执行备份、迁移、启动、健康检查;需要连续可用时使用向后兼容的 expand/contract migration。任何顺序都必须提供失败后的数据库与应用回退说明。
9. 为同一生产环境增加部署串行化,并记录可回滚版本。若继续目标主机本地构建,至少保留上一个 Git commit 和镜像 tag;若改用镜像仓库,则以 commit SHA 标记并部署固定 tag。
10. 首次部署 runbook 单独检查:外部网络存在、数据库别名可解析、数据库/用户已创建、密码已 URL 编码、TLS 模式与服务器一致、迁移可执行、volume 权限正确、反向代理只转发 loopback 端口、健康检查能从容器内和代理入口通过。
建议将未来模板参数化为:`PROJECT_NAME`、`COMPOSE_PROJECT_NAME`、`DATABASE_NAME`、`DATABASE_USER`、`DATABASE_HOST_ALIAS`、`EXTERNAL_NETWORK`、`WEB_LOOPBACK_PORT`、`TIMEZONE`、`BACKEND_HEALTH_PATH`。业务专用 token、同步 Job、数据目录和并发参数不进入通用模板。
## Boundaries
- 这套模式面向单机、单环境、低到中等流量的个人项目。多主机、高可用、蓝绿/金丝雀发布或受监管数据不应继续使用目标主机原地构建作为默认方案。
- `1panel-network`、`postgresql`、`8111`、`Asia/Shanghai`、UID 10001 和 `ZHIXING_*` 都是当前项目参数,不是全局约定。
- 生产 PostgreSQL 是否与应用同一台主机不是核心要求;核心是把数据库拓扑、TLS、凭据来源和网络边界显式化。远程数据库不需要 Docker external network。
- 时区是否使用 `Asia/Shanghai` 取决于业务契约。数据库时间戳仍应明确 UTC/带时区语义,不能仅靠容器 `TZ` 推断数据含义。
- healthcheck 只能证明探针覆盖的最小路径可用,不能替代迁移验证、关键业务 smoke test、数据库备份恢复演练和外部反向代理检查。
- 当前证据来自一个项目及其多次演进,尚未在第二个独立项目复用;在新项目完整验证前,不晋升为全局强制模板或自动生成 skill。
## Failed approaches
- 在生产 Compose 中保留内置 PostgreSQL 和开发默认连接串:后来改为外部数据库并用必填变量阻止错误回退。
- 将 Web 端口绑定到所有宿主机地址:后来收敛到 `127.0.0.1`,由反向代理作为唯一公网入口。
- 只在 Dockerfile 中创建并 `chown` 数据目录:运行时 named volume 仍可能由 root 初始化,最终需要单独的 init Job。
- 把一次性 Runner 能力检查长期保留在部署 workflow 集合中:验证完成后删除 smoke-test workflow,避免每次 push 运行无业务价值的检查。
- 把当前部署顺序视为通用答案:迁移与应用切换顺序必须由 schema 兼容契约决定。
## Promotion record
- 2026-08-14:按用户明确请求推广为单文件 Skill:`zhixing-system/bootstrap-docker-postgres-gitea/SKILL.md`。
- 验证:`python3 /Users/yuxuanhui/.codex/skills/.system/skill-creator/scripts/quick_validate.py /Users/yuxuanhui/bcc-github/quant-project/zgnb/zhixing-system/bootstrap-docker-postgres-gitea`,结果为 `Skill is valid!`。
- 当前仍只有 `zhixing-system` 的实战证据;Skill 保留单机个人项目边界和未验证项提示。待第二个项目复用并验证首次部署、升级、回退和数据库恢复后,再考虑提升为 `patterns/` 下的跨项目规范模式。
@@ -0,0 +1,144 @@
---
id: 20260822-extension-slot-portal-three-column-grid
title: 用生命周期插槽、稳定布局接缝和 CSS Grid 扩展宿主三列工作台
created: 2026-08-22
updated: 2026-08-24
status: candidate
scope: global
category: frontend-architecture
confidence: medium
last_verified: 2026-08-24
promotion_target: pattern
projects:
- oh-story-dsh
tags:
- host-extension
- session-slot
- react-portal
- css-grid
- container-responsive
- native-chat
---
# 用生命周期插槽、稳定布局接缝和 CSS Grid 扩展宿主三列工作台
## Trigger
插件需要在不修改宿主源码、不复制宿主 Chat 的前提下,把文件树、编辑器等工作区与宿主原生会话界面组成三列布局;宿主提供的官方扩展点在视觉上是 overlay,但真正需要改造的是 Session 内已有内容区的排版。
## Context
`oh-story-dsh` 需要在 DeepSeek Harness(DSH)中提供“文件树 / 编辑器 / 官方 Chat”三列创作工作台,同时继续使用 DSH 原生 Chat、streaming、tools、Todo、approvals、history 和 Composer。
这个实现没有修改 DSH Host 源码,也没有把整个工作台直接画成覆盖宿主内容的浮层。它把两个职责不同的接缝组合起来:
1. 通过官方 `shell.overlay` 注册 Session 级子插槽 `oh-story.workspace`,取得 `SessionProvider`、`sessionId`、`useSession` 和 Session 生命周期内的 Store。
2. 子插槽组件定位 DSH 明确作为稳定地址接缝的 `conversation.session`,再用 React portal 把工作台挂进该会话容器。
3. CSS 只在工作台 portal 确实存在时,把 conversation scroller 切成三列 Grid;文件树占第 1 列,编辑器占第 2 列,仍然挂载着的官方 Chat 和 Composer 占第 3 列。
关键不是“用 Grid 画三列”,而是先把状态生命周期、DOM 布局落点和原生能力所有权分开,再用最小的布局改造把三者拼起来。
这也说明应先区分两类需求:`conversation.view` 适合增加一个与 Chat 并列、切换显示的整页 Tab;当文件树、编辑器和原生 Chat 必须同时可见时,单独注册 view 不够,需要让新增工作区进入 Chat 当前视图的共同布局上下文。
## Evidence
- 仓库:[worldwonderer/oh-story-dsh](https://github.com/worldwonderer/oh-story-dsh)。2026-08-22 初次核对提交 [`8fa6786`](https://github.com/worldwonderer/oh-story-dsh/commit/8fa6786aa9d107caf3072c66ab5df334f07b69c0);2026-08-24 再次浅克隆并核对 HEAD [`fce73ca`](https://github.com/worldwonderer/oh-story-dsh/commit/fce73cafff535ab80316b74e427c539351759fbc),关键架构仍一致。
- [`docs/ARCHITECTURE.md`](https://github.com/worldwonderer/oh-story-dsh/blob/8fa6786aa9d107caf3072c66ab5df334f07b69c0/docs/ARCHITECTURE.md):明确 Browser entry 使用 `shell.overlay`,把文件树和编辑器 portal 到稳定的 `conversation.session` 布局接缝,并保留官方 conversation view。
- [`packages/dsh-plugin/src/client/index.tsx#L796-L895`](https://github.com/worldwonderer/oh-story-dsh/blob/fce73cafff535ab80316b74e427c539351759fbc/packages/dsh-plugin/src/client/index.tsx#L796-L895):`apply()` 在 `shell.overlay` 下声明 `{ "oh-story.workspace": { kind: "single", scope: "session" } }`;`WorkbenchSeat` 通过 `SessionProvider` 渲染子插槽;`CreativeSplitBridge` 查找 `[data-conversation-scroll] > [data-slot='conversation.session']` 并调用 `createPortal()`。该入口没有注册或替换 `conversation.view`。
- 同一文件中的 `CreativeSplitBridge` 使用 `ResizeObserver` 读取 conversation scroller 的 `clientWidth`,以 `<620`、`<900` 和其余宽度写入 `compact`、`medium`、`wide` 容器布局状态;卸载时移除 CSS 变量和 `data-oh-story-layout`。
- [`packages/dsh-plugin/src/client/plugin.css#L1-L75`](https://github.com/worldwonderer/oh-story-dsh/blob/fce73cafff535ab80316b74e427c539351759fbc/packages/dsh-plugin/src/client/plugin.css#L1-L75):只有当 `conversation.session` 中存在 `.oh-story-split-surface` 时,`:has()` 选择器才把 scroller 设为 Grid。wide 列轨为 `clamp(184px, 16%, 200px) minmax(240px, 1fr) clamp(408px, 40%, 520px)`;tree、editor、官方 Chat 分别进入第 1、2、3 列。
- 同一 CSS 把 `[data-composer-seat]` 放到第 3 列并设为 sticky,同时给 `[data-chat-flow]` 留出 Composer 高度,避免原生 Composer 覆盖最终消息;`min-width: 0`、`min-height: 0` 和各列 overflow 规则负责允许 Grid 子项正确收缩和滚动。
- [`plugin.css#L352-L358`](https://github.com/worldwonderer/oh-story-dsh/blob/fce73cafff535ab80316b74e427c539351759fbc/packages/dsh-plugin/src/client/plugin.css#L352-L358) 定义 medium/compact 列轨;源码未发现拖拽分隔条或 pointer/mouse move 调宽逻辑,因此当前列宽由 `clamp()` 和容器断点决定。
- [`scripts/native-dsh-smoke.ts`](https://github.com/worldwonderer/oh-story-dsh/blob/8fa6786aa9d107caf3072c66ab5df334f07b69c0/scripts/native-dsh-smoke.ts):原生 DSH smoke test 读取 tree、editor、Chat 和 Composer 的 bounding box,验证三列顺序、列宽至少 120px、Composer 位于 Chat 列内,并在滚动回归中检查 Composer 不消失。
- 2026-08-22 用户明确纠正:该项目“没有修改 DSH Host,也不是单纯浮层”;这是本条要保留的关键架构辨析。
- 2026-08-22 本次 capture 通过浅克隆核对当前提交、上述源码和测试代码;没有实际运行仓库的原生 DSH smoke test。
- 2026-08-24 用户提供该项目实际 UI 截图,视觉上与源码一致:左侧项目/文件树、中间 Markdown 预览、右侧原生消息流与 Composer 同时出现。截图证明最终呈现,不单独证明交互和卸载行为。
- 2026-08-24 Context7 将官方 DSH 解析为 `/deepseek-ai/deepseek-harness`;官方 [`ui-layout`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-layout/src/client/index.ts) 将 `shell.overlay` 声明为 root-scoped list slot,官方 [`ui-conversation` SlotMap](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/contract/slots.ts) 将 `conversation.view` 声明为 session-scoped list slot,验证两者职责边界。
- 项目侧完整复核记录:`dsh-lark-cli/.scratch/research/oh-story-dsh-three-column-layout.md`。
## Root cause
已验证:`shell.overlay` 在此实现中承担的是官方扩展注册和 Session 生命周期桥接,不是最终三列布局的空间父节点。真正的布局落点是 `conversation.session` 所在的稳定 DOM 接缝。
已验证:官方 Chat 和 Composer 没有被插件重写。插件保留它们现有的挂载和状态所有权,只通过 Grid placement 把它们排到第 3 列;因此 Chat 内部能力仍由 DSH 实现。
已验证:三列布局的启用条件与 portal 内容共存。`.oh-story-split-surface` 消失后,`:has()` 不再命中,scroller 会自然退出插件 Grid 规则;组件清理逻辑同时移除容器状态。
已验证:该方案不是“利用 `shell.overlay` 直接画出三栏”。`shell.overlay` 只提供 root registration、SessionProvider 和自定义 Session slot;三栏空间关系发生在 portal target 所在的 conversation scroller。
已验证:注册新的 `conversation.view` 会得到一个与 Chat 并列、按 active id 单独渲染的视图,不能直接实现“工作区与原生 Chat 同时可见”。并存需求与切换需求必须使用不同扩展策略。
推断:这套设计稳定的原因是把“在哪里取得上下文”和“在哪里参与布局”拆成两层。扩展 API 的名字或默认视觉形式不必成为最终布局结构,只要宿主另有明确、稳定、可寻址的布局接缝。
推断:保留官方 Chat 的 DOM 和状态,比复制一套 Chat UI 或代理内部状态更能降低宿主升级时的兼容成本;但 portal 目标和 CSS 选择器仍属于宿主契约,必须由文档和回归测试保护。
## Preferred action
1. 先判断产品需要的是“切换到插件整页”还是“插件 UI 与宿主原生视图同时可见”。前者优先使用 view/tab slot;只有后者才需要组合生命周期 slot 与布局 seam。
2. 画清所有权边界:插件只拥有新增工作区,宿主继续拥有 Chat、Composer、审批、工具调用和历史等原生能力。
3. 把接入拆成两种接缝:
- 生命周期接缝:只负责获得 Session 上下文、Store 和卸载边界;
- 布局接缝:只负责让新增 UI 与宿主现有 UI 参与同一个排版上下文。
4. 用官方扩展槽声明 Session scoped child slot,不自行订阅全局 Session,也不把跨 Session 的编辑状态放进单例 Store。
5. 只把 portal 挂到宿主明确承诺稳定的地址节点。若只能找到内部类名或偶然 DOM 层级,先补宿主契约或适配层,不要把猜测当扩展 API。
6. 在稳定内容容器上建立 Grid,显式给新增区域和原生区域分配列;不要复制或重新挂载宿主 Chat。
7. 用 portal 内容自身作为布局启用标记,例如父容器 `:has(.plugin-surface)`。这样插件未加载、切换 Session 或卸载时,不需要额外 JavaScript class toggle 就能恢复原布局。
8. Grid 轨道同时表达最小可用宽度、弹性和上限:窄导航列适合 `clamp()`,主编辑区适合 `minmax(0, 1fr)`,需要保障可用性的原生 Chat 适合带下限和上限的 `clamp()`。
9. 给可收缩 Grid 子项设置 `min-width: 0`,给内部滚动区域设置 `min-height: 0` 和明确 overflow;否则内容的固有尺寸可能撑破轨道。
10. 宿主 Composer 若与 Chat 流不是同一个 Grid item,应单独放入 Chat 列,并为消息流预留 Composer 高度;滚动时验证其可见性,而不只验证静态截图。
11. 响应式判断优先观察实际布局容器宽度,而不是浏览器 viewport。插件可能运行在侧栏、分屏或可变宽宿主中,`ResizeObserver + data-layout` 比 viewport media query 更贴近真实约束。
12. 几何回归至少检查:tree/editor/Chat 的左右顺序、每列最小宽度、Composer 是否完全位于 Chat 列、长 Chat 滚动时 Composer 是否保持可见,以及插件卸载后宿主是否恢复原布局。
## Boundaries
- 该模式只适用于宿主提供官方生命周期扩展点,并明确承诺某个 DOM 节点或 `data-slot` 是稳定布局接缝的情况;不能把任意 DOM 查询合理化为公共 API。
- `shell.overlay`、`conversation.session`、`SessionProvider` 和具体选择器是 DSH 当前扩展契约,不是 React 或插件系统的通用命名。
- `:has()` 需要目标浏览器版本支持;若宿主兼容范围包含旧浏览器,需要等价的受控 class/data attribute 退出机制。
- `display: contents` 会改变元素生成布局盒的方式,并可能影响可访问性、定位和浏览器兼容;应用前必须在宿主实际浏览器上验证。
- 三列最小宽度和 620/900 断点来自当前创作工作台,不应原样复制到内容密度、字体和宿主宽度不同的产品。
- portal 保留 React 上下文,但不会自动保证宿主 CSS、焦点层级、z-index、滚动和可访问性正确;这些仍需在真实宿主中做集成测试。
- 当前证据验证了源码设计和已有 smoke test 的检查项,但本次没有运行需要真实 DSH 环境的 smoke test,因此保留 `candidate` 和 `medium` confidence。
## Failed approaches
- 把 `shell.overlay` 的名字直接理解成最终视觉实现:会漏掉它在本项目中实际承担的 Session 生命周期和子插槽声明职责。
- 为了获得三列而复制或替换官方 Chat:会接管 streaming、工具、审批、历史和 Composer 等宿主内部状态,扩大维护边界。
- 只把工作台 portal 进会话但不改变共同父容器的布局:它仍只是会话中的普通内容或覆盖层,不能与原生 Chat 形成真正并列的三列。
- 用 viewport media query 推导列宽:宿主内部 conversation 容器可能与窗口宽度不同,分屏或侧栏变化时会得到错误布局。
- 只做视觉截图、不测 DOM 几何和滚动:容易遗漏 Composer 跨列、末尾消息被遮挡、窄列低于可用宽度等回归。
## Examples
下面是该模式的结构化伪代码,不是可直接复制的 DSH API:
```tsx
registerLifecycleSlot({
children: { workspace: { scope: "session" } },
}, ({ SessionProvider }) => (
<SessionProvider>{() => renderSlot("workspace")}</SessionProvider>
));
function WorkspaceBridge() {
const target = findDocumentedConversationSeam();
return target ? createPortal(<WorkspaceSurface />, target) : null;
}
```
```css
.conversation-scroller:has(.workspace-surface) {
display: grid;
grid-template-columns:
clamp(var(--tree-min), 16%, var(--tree-max))
minmax(0, 1fr)
clamp(var(--chat-min), 40%, var(--chat-max));
}
.workspace-tree { grid-column: 1; min-width: 0; }
.workspace-editor { grid-column: 2; min-width: 0; }
.native-chat,
.native-composer { grid-column: 3; min-width: 0; }
```
## Promotion record
- Not promoted. 当前只有 `oh-story-dsh` 一个项目的源码和测试证据;待在第二个宿主扩展中独立复用并通过真实集成测试后,再考虑提升为通用 pattern 或宿主扩展布局检查清单。
@@ -0,0 +1,106 @@
---
id: 20260824-composed-slot-before-dom-seam
title: 先查宿主组合包 SlotMap,再退回 DOM seam
created: 2026-08-24
updated: 2026-08-24
status: candidate
scope: global
category: api-discovery
confidence: high
last_verified: 2026-08-24
promotion_target: pattern
projects:
- dsh-lark-cli
tags:
- host-extension
- slotmap
- declaration-merging
- native-tab
- api-discovery
- dsh
---
# 先查宿主组合包 SlotMap,再退回 DOM seam
## Trigger
需要判断一个模块化 Web 宿主的插件能否原生增加页面、Tab、导航项或面板,但当前插件仓库和它直接依赖的 runtime 类型里没有找到对应扩展 API。
## Context
调研 DeepSeek Harness(DSH)插件能否增加与 Chat、Trajectory 并列的 Session 顶部 Tab 时,先检索当前插件仓库和直接安装的 `@deepseek-ai/dsh-client-runtime`,只看到了 `root`、`shell.overlay` 以及仓库已有的 DOM Portal 路径,一度推断“没有原生 Tab API”。
继续检查实际 DSH Web Host 组合的 UI packages 后,`@deepseek-ai/dsh-client-ui-conversation` 的 TypeScript declaration merging 明确补充了 `conversation.view`:这是 `kind: 'list'`、`scope: 'session'` 的视图环,一个注册项就是一个顶部 Tab。第一方 `ui-trajectory` 插件正是通过同一 slot 注册 Trajectory。
这次有意义的复利点不是记住一个 slot 名,而是:扩展契约可能由“视觉表面的所有者包”声明,不在核心 runtime,也不一定出现在业务插件的直接依赖树中。否定一个扩展能力前,必须检查宿主最终组合和第一方同类贡献者。
## Evidence
- 2026-08-24,本机 `dsh --version` 返回 `0.1.0-rc.7`;Web UI packages 为 `0.1.0-rc.8`。
- 官方 `ui-conversation` SlotMap 在 [`packages/client/ui-conversation/src/client/contract/slots.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/contract/slots.ts) 声明 `conversation.view`,注释说明它是 “one list entry per view tab”。
- 官方会话组装在 [`packages/client/ui-conversation/src/client/apply.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/apply.ts) 遍历 `slots.entries('conversation.view')`,从 entry 的 `id` 和 `label` 生成 Tab;Chat 以 `id: 'chat'`、`order: 0` 注册。
- 第一方 [`packages/client/ui-trajectory/src/client/index.ts`](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-trajectory/src/client/index.ts) 使用 `ctx.slots.inject('conversation.view', ...)` 注册 `id: 'trajectory'`、`order: 10` 的 Trajectory Tab。
- 当前 `dsh-lark-cli` 只注册 `sidebar.footer.action` 和 `shell.overlay`,再通过 React Portal 使用 Conversation DOM seam;它证明了另一种布局扩展方式,但不能证明原生 Tab slot 不存在。
- 本次同时核对了已安装包的 `.d.ts`、编译后 `client.js`、官方中文 README、Context7 官方库 `/deepseek-ai/deepseek-harness` 和 GitHub `master` 原始源码。
- 项目内完整调研记录:`dsh-lark-cli/.scratch/research/dsh-plugin-custom-tab.md`。
## Root cause
已验证:DSH 的 `SlotMap` 使用 TypeScript declaration merging,由各 UI owner package 分散声明。`dsh-client-runtime` 只声明内建 `root` slot;`conversation.view` 由 `dsh-client-ui-conversation` 声明,因此只查 runtime 会得到不完整的扩展面。
已验证:宿主最终装配包含的包可以多于业务插件的直接依赖。当前项目没有直接安装 `dsh-client-ui-conversation`,但全局 DSH Web bundle 已组合该包及 `ui-trajectory`,所以项目局部 `node_modules` 不是宿主能力全集。
已验证:slot 类型声明只能证明 seat 存在;宿主如何把 seat 投影为 UI,需要继续核对 owner 实现。`ui-conversation` 的 header/render 代码证明 `id`、`order`、`label` 会形成 Tab,active id 会选择唯一视图。
推断:在其他可组合插件宿主中,同类误判也容易发生:核心 SDK 暴露注册机制,具体贡献点由 feature package 增补;从业务仓库向内搜索会漏掉最终装配才拥有的契约。
## Preferred action
1. 先确定目标 UI 的所有者:Tab 属于 Conversation、Settings、Sidebar 还是根 Layout;不要默认所有扩展点都由 runtime 声明。
2. 枚举实际宿主装配的 feature/client packages。优先读取 bundle manifest、profile dump、全局安装目录或已发布 package metadata,而不是只看业务插件的直接依赖。
3. 搜索所有 `SlotMap` declaration merging、slot declaration 和 `slots.register` 调用,区分:
- `single`:替换整个表面;
- `list`:可追加贡献;
- `keyed` / `chain`:按 key 或选择器扩展;
- slot 的 `root` / `session` scope。
4. 用三类一手证据闭环:
- owner package 的契约声明;
- owner 如何把注册项投影为实际 UI;
- 第一方同类插件的最小注册示例。
5. 分别核对“当前部署版本”和“上游当前版本”。前者回答现在能否工作,后者回答接口是否仍存在;不要用其中一个替代另一个。
6. 只有确认没有满足需求的 additive slot 后,才评估 DOM seam、Portal、CSS 重排或宿主 fork。若已有原生 slot,用它承担生命周期、排序、卸载和 active state。
7. 实现时把 owner package 加入插件的 client inject,并按宿主版本固定 peer/dev dependency;RC API 必须做真实 Web profile smoke test。
## Boundaries
- 该方法适用于由多个包组合、扩展契约分散声明的插件宿主。单体应用或拥有集中式完整 schema 的 SDK 不需要扫描全部 feature packages。
- 发现内部 slot 不等于它是稳定公共 API。至少需要 package 导出、类型声明、owner 文档或第一方插件用例之一;仅从 minified DOM、私有类名或未导出符号推断时仍按内部实现处理。
- `conversation.view` 适合“切换到插件拥有的整页视图”。若需求是文件树、编辑器与官方 Chat 同时可见,原生 Tab 会隐藏非 active view,仍应评估 [[20260822-extension-slot-portal-three-column-grid|生命周期 slot + 稳定布局 seam + Portal]]。
- `conversation.view`、`dsh.client.inject` 和具体 order 值是 DSH 当前契约,不应推广成其他宿主的通用命名。
- 当前只完成源码、类型、包版本和上游 master 核验,没有为 `dsh-lark-cli` 实际注册新 Tab,也没有运行浏览器集成测试。实现层面的兼容性结论仍需 smoke test。
## Failed approaches
- 只检索业务仓库和直接依赖的 runtime:会把“当前项目没有导入 owner package”误判成“宿主没有扩展点”。
- 从 `root` 或 `conversation.session` 是 `single` slot 推导不能新增 Tab:这混淆了“替换整棵表面”和其内部声明的 additive child slot。
- 看到当前项目已经使用 DOM Portal,就把它当作该视觉需求的唯一方案:既有实现只能证明一条可行路径,不能穷举宿主扩展面。
- 只搜 `tab`、`route`、`router`:slot 系统可能把 Tab 表达成抽象的 `view` contribution,应同时沿 UI owner、slot ledger 和第一方插件反向定位。
## Examples
DSH 当前原生 Session Tab 的最小形态:
```tsx
ctx.slots.inject('conversation.view', () => ctx.slots.register({
name: 'conversation.view',
id: 'plugin-view',
order: -10,
label: () => 'Plugin View',
}, PluginView))
```
验证顺序应是:`SlotMap declaration` → `host tab projection` → `first-party trajectory registration` → `target version smoke test`。
## Promotion record
- Not promoted. 当前已有一次高置信度纠错和一套可复用调查流程;待在另一个分包式宿主扩展调研中独立复用后,再考虑提升为通用 host-extension discovery pattern。
@@ -2,7 +2,7 @@
> 状态:已通过真实 Feishu Base / Wiki POC 验证 > 状态:已通过真实 Feishu Base / Wiki POC 验证
> 更新时间:2026-07-26 > 更新时间:2026-07-26
> 覆盖范围:`setup-matt-pocock-skills-feishu`、`to-spec-feishu`、`to-tickets-feishu`、`triage-feishu` > 覆盖范围:`setup-workflow-skills-feishu`、`to-spec-feishu`、`to-tickets-feishu`、`triage-feishu`
## 1. 结论 ## 1. 结论
@@ -65,7 +65,7 @@ flowchart LR
| Skill | 何时使用 | 核心输入 | 核心产物 | Base / Wiki 策略 | | Skill | 何时使用 | 核心输入 | 核心产物 | Base / Wiki 策略 |
|---|---|---|---|---| |---|---|---|---|---|
| `setup-matt-pocock-skills-feishu` | 仓库接入或切换到飞书 tracker | Base 表格/视图 URL、Wiki 根节点 URL、Setup 模式 | repo tracker 合约;Bootstrap 另含 Base schema、Wiki setup 文档、POC 记录 | 默认 Reuse 只读复核既有设施;Bootstrap 才初始化并验证写路径 | | `setup-workflow-skills-feishu` | 仓库接入或切换到飞书 tracker | Base 表格/视图 URL、Wiki 根节点 URL、Setup 模式 | repo tracker 合约;Bootstrap 另含 Base schema、Wiki setup 文档、POC 记录 | 默认 Reuse 只读复核既有设施;Bootstrap 才初始化并验证写路径 |
| `to-spec-feishu` | 把已澄清的对话转成可执行 Spec | 对话、代码库上下文、测试 seam、可选来源 Issue | 完整 Wiki Spec + 一条 Base Spec | Wiki 保存完整规格;Base 保存身份、状态、摘要、验收和父项 | | `to-spec-feishu` | 把已澄清的对话转成可执行 Spec | 对话、代码库上下文、测试 seam、可选来源 Issue | 完整 Wiki Spec + 一条 Base Spec | Wiki 保存完整规格;Base 保存身份、状态、摘要、验收和父项 |
| `to-tickets-feishu` | 把 Spec / 计划拆成 tracer-bullet Tickets | 已确认 Spec、垂直切片、依赖图 | 每个切片一条 Base Ticket | V1 不创建 Ticket Wiki;通过父 Spec 取得文档;两遍写入关系 | | `to-tickets-feishu` | 把 Spec / 计划拆成 tracer-bullet Tickets | 已确认 Spec、垂直切片、依赖图 | 每个切片一条 Base Ticket | V1 不创建 Ticket Wiki;通过父 Spec 取得文档;两遍写入关系 |
| `triage-feishu` | 处理 Issue / PR 的分类、澄清和委派 | Issue/PR、代码验证、维护者决定、报告人反馈 | Base Issue 状态 + 可选 Triage Wiki dossier | Base 保存当前状态;Wiki 追加 Notes / Brief;拒绝决定以 repo 为准 | | `triage-feishu` | 处理 Issue / PR 的分类、澄清和委派 | Issue/PR、代码验证、维护者决定、报告人反馈 | Base Issue 状态 + 可选 Triage Wiki dossier | Base 保存当前状态;Wiki 追加 Notes / Brief;拒绝决定以 repo 为准 |
@@ -76,7 +76,7 @@ flowchart LR
```mermaid ```mermaid
flowchart TD flowchart TD
Setup["setup-matt-pocock-skills-feishu\n建立 repo 合约;按模式复用或初始化飞书资源"] --> Intake{"工作从哪里进入?"} Setup["setup-workflow-skills-feishu\n建立 repo 合约;按模式复用或初始化飞书资源"] --> Intake{"工作从哪里进入?"}
Intake -->|"想法 / 已澄清对话"| SpecDraft["to-spec-feishu"] Intake -->|"想法 / 已澄清对话"| SpecDraft["to-spec-feishu"]
Intake -->|"外部 Issue / PR / 表单"| Triage["triage-feishu"] Intake -->|"外部 Issue / PR / 表单"| Triage["triage-feishu"]
@@ -43,7 +43,7 @@ flowchart LR
| Skill | 负责的阶段 | 主要输入 | 主要产物 | 明确不负责 | | Skill | 负责的阶段 | 主要输入 | 主要产物 | 明确不负责 |
|---|---|---|---|---| |---|---|---|---|---|
| `setup-matt-pocock-skills-feishu` | 仓库接入 | Base URL、Wiki 根 URL、Reuse/Bootstrap 选择 | repo tracker 合约;Bootstrap 时创建 schema、setup Wiki 文档和 POC 记录 | 不在 Reuse 模式修 schema;不猜资源地址 | | `setup-workflow-skills-feishu` | 仓库接入 | Base URL、Wiki 根 URL、Reuse/Bootstrap 选择 | repo tracker 合约;Bootstrap 时创建 schema、setup Wiki 文档和 POC 记录 | 不在 Reuse 模式修 schema;不猜资源地址 |
| `to-spec-feishu` | 对话 → 可执行规格 | 已澄清对话、代码库、领域词汇、测试 seam | 一份 Wiki Spec + 一条 Base `PRD/Spec` | 不采访式重新澄清;不在 Wiki 未回读前创建 Base Spec | | `to-spec-feishu` | 对话 → 可执行规格 | 已澄清对话、代码库、领域词汇、测试 seam | 一份 Wiki Spec + 一条 Base `PRD/Spec` | 不采访式重新澄清;不在 Wiki 未回读前创建 Base Spec |
| `to-tickets-feishu` | Spec → 垂直切片 | 已批准 Spec、Ticket 粒度、依赖边 | 每个切片一条 Base `实现 Ticket` | V1 不为 Ticket 创建 Wiki;不关闭或修改父 Spec | | `to-tickets-feishu` | Spec → 垂直切片 | 已批准 Spec、Ticket 粒度、依赖边 | 每个切片一条 Base `实现 Ticket` | V1 不为 Ticket 创建 Wiki;不关闭或修改父 Spec |
| `triage-feishu` | Issue/PR 分诊 | 外部请求、代码验证、维护者决定、报告人反馈 | Base Issue 状态 + 可选 Wiki Triage dossier | 不在推荐阶段写状态;不重复创建 dossier | | `triage-feishu` | Issue/PR 分诊 | 外部请求、代码验证、维护者决定、报告人反馈 | Base Issue 状态 + 可选 Wiki Triage dossier | 不在推荐阶段写状态;不重复创建 dossier |
@@ -54,7 +54,7 @@ flowchart LR
```mermaid ```mermaid
flowchart TD flowchart TD
Setup["setup-matt-pocock-skills-feishu<br/>建立项目级 tracker 合约"] --> Intake{"工作从哪里进入?"} Setup["setup-workflow-skills-feishu<br/>建立项目级 tracker 合约"] --> Intake{"工作从哪里进入?"}
Intake -->|"已澄清想法 / 对话"| Spec["to-spec-feishu<br/>Wiki Spec + Base Spec"] Intake -->|"已澄清想法 / 对话"| Spec["to-spec-feishu<br/>Wiki Spec + Base Spec"]
Intake -->|"Issue / PR / 表单"| Triage["triage-feishu<br/>分类、验证、澄清、Brief"] Intake -->|"Issue / PR / 表单"| Triage["triage-feishu<br/>分类、验证、澄清、Brief"]
@@ -575,7 +575,7 @@ stateDiagram-v2
|---:|---|---|---|---| |---:|---|---|---|---|
| 1 | 文本 | `fldzLHLTca` | text,主字段 | 记录的业务标题。标题可用于创建前精确查重,但更新必须使用 record ID。 | | 1 | 文本 | `fldzLHLTca` | text,主字段 | 记录的业务标题。标题可用于创建前精确查重,但更新必须使用 record ID。 |
| 2 | 产物类型 | `fldlwQ46Ks` | single select:需求/Issue、PRD/Spec、实现 Ticket、Wayfinder Map、决策 Ticket、原型、研究、Handoff、ADR、领域词汇、Bug 诊断、代码评审、架构候选、教学资产 | 说明该记录代表哪类工作流产物。 | | 2 | 产物类型 | `fldlwQ46Ks` | single select:需求/Issue、PRD/Spec、实现 Ticket、Wayfinder Map、决策 Ticket、原型、研究、Handoff、ADR、领域词汇、Bug 诊断、代码评审、架构候选、教学资产 | 说明该记录代表哪类工作流产物。 |
| 3 | 来源技能 | `fldUMyVtoS` | multi-select:setup-matt-pocock-skills、setup-matt-pocock-skills-feishu、grill-with-docs、grill-me、triage、diagnosing-bugs、wayfinder、to-spec、to-tickets、implement、tdd、code-review、improve-codebase-architecture、domain-modeling、prototype、research、handoff、teach | 记录创建或推进产物的逻辑流程。业务记录通常写逻辑 skill 名;当前表也保留 setup 的 Feishu 适配器选项。 | | 3 | 来源技能 | `fldUMyVtoS` | multi-select:setup-matt-pocock-skills、setup-workflow-skills-feishu、grill-with-docs、grill-me、triage、diagnosing-bugs、wayfinder、to-spec、to-tickets、implement、tdd、code-review、improve-codebase-architecture、domain-modeling、prototype、research、handoff、teach | 记录创建或推进产物的逻辑流程。业务记录通常写逻辑 skill 名;当前表也保留 setup 的 Feishu 适配器选项。 |
| 4 | 工作流阶段 | `fldjAkNPqD` | single select:探索/澄清、决策、规格、拆票/规划、实现、验证/评审、交付、维护/学习、分诊 | 表示产物处于端到端流程哪一阶段;与`状态`是不同维度。 | | 4 | 工作流阶段 | `fldjAkNPqD` | single select:探索/澄清、决策、规格、拆票/规划、实现、验证/评审、交付、维护/学习、分诊 | 表示产物处于端到端流程哪一阶段;与`状态`是不同维度。 |
| 5 | 状态 | `fldHjFrlwJ` | single select:草拟中、待确认、待分诊、needs-triage、needs-info、ready-for-agent、ready-for-human、进行中、阻塞、待评审、已完成、wontfix、out-of-scope、已取代 | 统一业务状态机。英文值保留 Matt triage canonical role。 | | 5 | 状态 | `fldHjFrlwJ` | single select:草拟中、待确认、待分诊、needs-triage、needs-info、ready-for-agent、ready-for-human、进行中、阻塞、待评审、已完成、wontfix、out-of-scope、已取代 | 统一业务状态机。英文值保留 Matt triage canonical role。 |
| 6 | 类别 | `fldQWuceDX` | single select:bug、enhancement | Triage 分类;每个已分诊项必须恰好一个。 | | 6 | 类别 | `fldQWuceDX` | single select:bug、enhancement | Triage 分类;每个已分诊项必须恰好一个。 |
@@ -1,694 +0,0 @@
# 三阶段研发工作流:产物链路、Skill 组合与飞书 CLI 管理方案
> 状态:研究与方案评审
>
> 日期:2026-07-28
>
> 范围:需求探索 → 需求转化 → 开发收口;Matt skills、Feishu Base/Wiki、`lark-cli` 与 Trellis 的产物衔接。
>
> 结论先行:三阶段方向成立,`Spec → Tickets → Start → Inline/Trellis → Close` 已有真实 POC 支撑;当前主要断点是探索阶段没有正式发布出口、`ready-for-agent` 与负责人分派之间断链、开发阶段缺少显式实现方法,以及 Wiki Spec 与 Trellis `prd.md` 的事实源边界尚未完全统一。来源 Issue 的后续状态由人类在 Base 中自行检查,不纳入自动化范围。
## 1. 研究问题与判断
本研究回答五个问题:
1. 三个阶段中每个 skill 实际负责什么,不负责什么?
2. 每个阶段必须产出什么,才能被下一阶段稳定消费?
3. 当前流程中的 `handoff`、原型、Spec、Tickets、Trellis artifacts 是否形成了单一、可追踪的链路?
4. 哪些环节已经经过真实 Feishu/Trellis POC,哪些仍是建议?
5. 如何在不把所有内容都复制进飞书的前提下,用 `lark-cli` 提升可查询性、恢复能力和审计性?
核心判断如下:
- `handoff` 是临时会话运输层,不是需求事实源,也不应是每一步必经的业务状态。
- `grill-me` 只负责消除决策歧义,不会自动生成需求文档;第一阶段缺少一个正式的“探索结论发布”动作。
- `prototype` 产出的是“回答一个问题的可运行原型”,不保证是 HTML。UI 问题可能产出浏览器多变体,逻辑问题应产出 TUI/状态模型。
- 第一阶段的文档应叫 **Discovery Brief(需求探索结论)**;第二阶段的 `to-spec` 才产出 **Engineering Spec(工程执行规格)**。二者不能都叫 PRD 并同时声称权威。
- `to-spec-feishu` 和 `to-tickets-feishu` 当前都不写 `负责人`,而 `start-work-feishu` 只查询当前用户负责的记录,存在明确的 Dispatch Gate 断链。
- `start-work-feishu` 只负责选择、路由、绑定和开始状态;`close-work-feishu` 只负责证据收口。中间必须存在 `standard implement`、`tdd`、诊断或评审等明确的方法 owner。
- 复杂探索更适合使用 `wayfinder` 的 Map + Decision Tickets,而不是用多份临时 `handoff` 串成长链。
- 飞书适合继续做管理控制面,不应复制完整 Trellis task 或 repo 内容。
## 2. 证据范围
### 2.1 本仓库总结文档
本研究以以下两篇总结为主:
- [Matt 工作流 × 飞书 CLI 全流程总结](<./Matt 工作流 × 飞书 CLI 全流程总结.md>):覆盖 Setup、Spec、Tickets、Triage、29 字段、身份和 CLI 写后回读。
- [Matt 工作流 × 飞书 Skills × Trellis Coding 闭环总结](<./Matt 工作流 × 飞书 Skills × Trellis Coding 闭环总结.md>):覆盖 Start、Inline/Trellis 路由、`1 Spec = 1 task`、Ticket 部分收口、Spec 最终收口和真实 POC。
两篇总结共同确认的事实源分工是:
| 事实 | 事实源 |
|---|---|
| 当前状态、负责人、父子、依赖、队列 | Feishu Base |
| Discovery、Spec、Triage 等长文档 | Feishu Wiki / Docs |
| 复杂开发的 planning、checkpoint、archive | Trellis |
| 代码、测试、ADR、领域词汇、实现证据 | repo |
| 选择、产品决策、Ticket review、Spec 最终 review | 人类 |
### 2.2 Skill 一手定义
本研究逐一核对了以下本地定义:
- `/Users/yuxuanhui/.agents/skills/grilling/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/grill-me/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/grill-with-docs/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/domain-modeling/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/handoff/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/prototype/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/prototype/LOGIC.md`
- `/Users/yuxuanhui/.agents/skills/prototype/UI.md`
- `/Users/yuxuanhui/.agents/skills/to-spec/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/to-spec-feishu/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/to-tickets/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/to-tickets-feishu/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/start-work-feishu/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/close-work-feishu/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/wayfinder/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/implement/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/tdd/SKILL.md`
- `/Users/yuxuanhui/.agents/skills/code-review/SKILL.md`
同时核对了飞书 tracker 的一手适配约定:
- `/Users/yuxuanhui/.agents/skills/setup-matt-pocock-skills-feishu/references/issue-tracker-feishu.md`
- `/Users/yuxuanhui/.agents/skills/to-spec-feishu/references/feishu.md`
- `/Users/yuxuanhui/.agents/skills/to-tickets-feishu/references/feishu.md`
### 2.3 Trellis 与 CLI 证据
Trellis 路由和产物规则来自:
- [Trellis × Matt 全局规则](<../AI-RD-Workflow/40-workflows/trellis-matt/CN/AGENTS.md>)
- [Trellis × Matt workflow](<../AI-RD-Workflow/40-workflows/trellis-matt/CN/workflow.md>)
本机只读验证:
```text
lark-cli --version
→ lark-cli version 1.0.76
```
当前 CLI 的 `base --help` 明确提供 Base records、views、dashboard、workflow、record history 和 data query;`docs --help`、`wiki --help`提供文档内容和知识库节点操作。`record-history-list` 是单条记录历史,不是整表审计;Workflow 创建后默认 disabled,启用应是单独动作。
Context7 命中的官方一手来源为 [`/larksuite/cli`](https://github.com/larksuite/cli),其 README 确认 Lark CLI 采用 shortcut、API command、Universal API 三层调用,并为 Base、Docs、Wiki 等域提供版本匹配的 agent skills。
本研究没有写入 Feishu,也没有重新执行已有业务 POC。实际闭环证据沿用两篇总结中记录的“经销商政策”项目验证。
## 3. 推荐的总产物图
```mermaid
flowchart TD
Issue["Base 需求/Issue<br/>稳定业务根 ID"] --> Discovery["Wiki Discovery Brief<br/>探索结论"]
Issue --> Prototype["原型资产/结论<br/>HTML、应用路由或逻辑 TUI"]
Issue --> Wayfinder["可选:Wayfinder Map"]
Wayfinder --> Decision["Decision Tickets<br/>grilling / prototype / research / task"]
Spec["Base PRD/Spec"] -->|"所属父项"| Issue
Spec --> SpecDoc["Wiki Engineering Spec"]
Tickets["Base 实现 Tickets"] -->|"所属父项"| Spec
Tickets -->|"前置依赖"| Tickets
Start["start-work-feishu"] --> Inline["Inline<br/>会话内冻结 IDs"]
Start --> Trellis["Trellis task<br/>1 Spec = 1 task"]
Spec --> Start
Tickets --> Start
Inline --> Repo["repo 代码 / 测试 / review 证据"]
Trellis --> Repo
Repo --> Close["close-work-feishu"]
Close -->|"部分收口"| Tickets
Close -->|"最终收口"| Spec
```
关系方向必须明确:
- Spec 的 `所属父项`指向来源 Issue。
- Ticket 的 `所属父项`指向 Spec。
- Decision Ticket 的 `所属父项`指向 Wayfinder Map。
- `前置依赖`保存真实 Base record ID,不保存标题或文本编号。
## 4. 第一阶段:需求探索
用户原设想:
```text
grill-me → handoff → prototype → handoff → grill-me
=> 需求文档 + HTML 一次性原型
```
方向合理,但需要把固定流水线改成按问题选路。
### 4.1 每个 skill 的实际作用
| Skill | 作用 | 直接产物 | 不负责 |
|---|---|---|---|
| `grill-me` | 包装 `grilling`;一次问一个产品/范围决策,并给推荐答案 | 当前会话中的共同理解 | 自动写 PRD、自动发布飞书、替用户做决策 |
| `handoff` | 跨 session/agent 传递恢复所需指针和下一步 | OS 临时目录中的 Markdown | 长期事实源、需求审批、业务状态 |
| `prototype` | 用一次性代码回答一个 UI、状态或数据形状问题 | UI 多变体,或逻辑 TUI/纯模块;运行方式;问题与 verdict | 默认 HTML、生产实现、完整需求文档 |
| 第二次 `grill-me` | 针对使用原型后出现的新冲突,确认 winner 和边界 | Prototype verdict、修订后的产品决策 | 从头重做采访、自动综合文档 |
`grilling` 明确要求环境可查的事实应直接查,不应反问用户;属于用户的决策才逐项确认。`handoff` 明确写到 OS 临时目录,并要求引用已有 artifacts,不复制其正文。这两点决定了 handoff 只能是运输层。
### 4.2 Prototype 不应等同于 HTML
`prototype` 有两个完全不同的分支:
| 需要回答的问题 | 原型形态 |
|---|---|
| “这个页面应该长什么样?” | UI 分支:同一路由的 3 个左右结构差异明显的变体,使用 `?variant=`切换 |
| “这个状态模型/业务逻辑合理吗?” | Logic 分支:小型 TUI 驱动纯 reducer、state machine 或函数集合 |
因此第一阶段的正式产物名称应是“可运行一次性原型”,而不是无条件要求 HTML。
还有一个 brownfield 边界:UI skill 强烈偏好把变体嵌入现有页面,复用真实数据、auth、header/sidebar 和信息密度。如果是已有产品,完全不读代码库就制作独立 HTML,容易得到在真页面中不成立的方案。建议:
- greenfield 或纯概念演示:第一阶段可以制作独立 HTML。
- brownfield UI:第一阶段允许只读代码库并嵌入真实页面;或把 UI prototype 推迟到第二阶段。
- 逻辑/状态问题:不要为了满足“HTML”形式而绕过 Logic prototype。
### 4.3 三种合理组合
#### 简单探索:同一会话
```text
grill-me →(必要时 prototype)→ targeted verdict review → Discovery Brief
```
没有会话切换时省略两个 `handoff`。
#### 跨会话探索
```text
grill-me
→ 更新 canonical Discovery dossier
→ handoff(只带 record ID / URL / path / next question)
→ prototype
→ 更新 prototype question、asset、verdict
→ handoff
→ targeted verdict review
→ Discovery Gate
```
临时 handoff 即使丢失,也可以从 Base/Wiki/repo 指针恢复。
#### 多条相互依赖的不确定性
```text
Wayfinder Map:Destination = 通过 Discovery Gate
├─ grilling ticket(HITL)
├─ prototype ticket(HITL)
├─ research ticket(AFK)
└─ task ticket(HITL / AFK)
```
Wayfinder 原生提供 Map、Decision Tickets、blocker、frontier 和 fog of war,更适合多会话探索。现有 29 字段已包含 `Wayfinder Map`、`决策 Ticket`和四种`决策票类型`,但 Wayfinder 尚不在六个已完成真实 Feishu POC 的 workflow skills 中;正式默认采用前应补一次 create/update/read-back POC。
### 4.4 第一阶段最终产物
第一阶段不建议产出第二份 PRD,建议产出:
#### A. Wiki `Discovery Brief — <标题>`
至少包含:
1. Problem、目标用户和期望结果。
2. 已确认的产品规则与用户决策。
3. 假设、证据、开放问题和明确延期问题。
4. 原型要回答的精确问题。
5. 原型运行方式、变体键或逻辑操作方式。
6. Prototype verdict:选了什么、为什么、拒绝什么。
7. 初步可观察验收语言,但不写模块、文件和实现设计。
8. Out of scope。
9. 来源 Issue、Prototype、Research、Wayfinder record IDs/URLs。
10. 人类确认时间和当前版本。
#### B. Base 根记录
```text
产物类型 = 需求/Issue
来源技能 = grill-me / prototype / research(按实际追加)
工作流阶段 = 探索/澄清
状态 = 草拟中 → 待确认 → ready-for-agent
产物文档 = Discovery Wiki URL
结论/摘要 = 一段式探索结论
验收标准 = Discovery Gate
下一步 = grill-with-docs / to-spec
```
#### C. 原型资产
- 简单探索:`原型` Base child record 指向 Issue,保存问题、运行方式、path/URL 和 verdict。
- Wayfinder 路径:优先把 prototype 作为 Decision Ticket 的 asset,避免再创建一份重复 Base 记录。
- 需要长期复现时,保存 `repo@commit:path`。commit/branch/发布都需要独立用户授权。
- 未获 Git 授权时,可以保存本地路径和内容 hash,但必须标注“仅同机可恢复”。
- 只有跨团队、需要独立审计的移交才创建 `Handoff` Base 记录;普通 session handoff 不进入业务表。
### 4.5 Discovery Gate
只有以下条件同时满足,第一阶段才可交给第二阶段:
- 问题、目标用户和期望结果明确。
- In scope / Out of scope 明确。
- 原型问题有 verdict,或明确记录“不需要原型”。
- 阻塞性产品决策为空。
- 开放风险已显式列出并有 owner/next step。
- 用户确认 Discovery Brief 代表当前共同理解。
- Base/Wiki 写入如已执行,已经 read-back。
## 5. 第二阶段:需求转化
用户原设想:
```text
grill-with-docs → to-spec → to-tickets
```
这条序列基本正确,但每一步应有不同问题域,避免第二次采访。
### 5.1 Skill 作用与产物
| Skill | 输入 | 作用 | 产物 |
|---|---|---|---|
| `grill-with-docs` | Discovery Brief、prototype verdict、代码库、现有 glossary/ADR | 只处理探索结论与现有代码/领域模型的碰撞;校正术语;确认难逆技术决策 | `CONTEXT.md` 术语更新;少量 ADR;已确认工程约束 |
| `to-spec-feishu` | 已澄清上下文、代码库、领域词汇、ADR、来源 Issue、原型决策 | 不再采访;确认最高可用测试 seam;综合并发布 | Wiki `Spec — 标题` + Base `PRD/Spec`,状态 `ready-for-agent` |
| `to-tickets-feishu` | 已批准 Spec、代码库、切片和依赖 | 拆 tracer-bullet 垂直切片;让用户确认粒度与 blocker;两遍发布关系 | N 个 Base `实现 Ticket`,父项和依赖完整回读 |
`domain-modeling` 的边界需要保留:
- `CONTEXT.md`只保存稳定领域词汇,不复制 Spec。
- ADR 只用于难逆、反直觉且经过真实取舍的决策。
- 普通 task 细节继续留在 Discovery/Spec/Trellis,而不是推广成长期知识。
### 5.2 第一阶段如何成为第二阶段输入
建议 `to-spec-feishu` 的输入 manifest 至少包含:
```text
requestRecordId
discoveryWikiUrl
prototypeRefs[]
researchRefs[]
wayfinderMapRecordId(可选)
repositoryRef
discoveryUpdatedAt / digest
```
现有 `to-spec` 已允许把原型中最能表达决策的 state machine、reducer、schema 或 type shape 精简后写入 Spec。工作 Demo 本身不应复制进 Spec。
当前 adapter 允许 Spec 关联来源 Issue,但没有强制读取 Issue 的 Discovery Wiki 和全部 prototype/research children。建议把“按来源 Issue 读取并核对探索包”加入 precondition,并在 Spec 的 Sources/Further Notes 中保存稳定 record IDs/URLs。
### 5.3 第二阶段最终产物
- repo:更新后的领域词汇和必要 ADR。
- Wiki:唯一 Engineering Spec 长文档。
- Base:一个 Spec record、N 个 Ticket records、完整 parent/blocker 图。
- 人类确认:测试 seam、Spec 内容、Ticket 粒度和依赖。
- 分派信息:owner、priority、collaboration mode,或明确进入“待分派”队列。
### 5.4 明确断点:Dispatch Gate
一手适配契约显示:
- `to-spec-feishu/references/feishu.md` 的 Spec 字段表没有`负责人`。
- `to-tickets-feishu/references/feishu.md` 的 Ticket 字段表也没有`负责人`。
- `start-work-feishu` 只查询`负责人=当前用户`的 Spec/Ticket,并要求保持 owner 不变。
因此:
```text
ready-for-agent ≠ 已分派 ≠ 可被 start-work 查询
```
当前最小方案:
1. 建立 Base “待分派”视图:`状态=ready-for-agent AND 负责人为空`。
2. 人类在 Base UI 分派 owner、priority 和 collaboration mode。
3. `start-work-feishu` 只消费已分派队列。
CLI 化的两个可选演进:
- 新增轻量 `dispatch-work-feishu`:列出未分派 frontier,用户选择 owner,精确 patch 并 read-back。
- 扩展 `start-work-feishu` 支持“未分派且无 blocker”的自助认领,在同一次“选择并开始”确认中原子写 owner + in-progress;它不能认领已分派给他人的工作。
管理者分派和开发者自助认领都需要,不能只实现其中一种。
### 5.5 Spec Gate 与 Ticket Gate
Spec Gate:
- Discovery 来源完整。
- 测试 seam 已确认。
- Wiki 内容 fetch 完整。
- Base Spec 的 type/status/parent/link/acceptance 已 read-back。
Ticket Gate:
- 每张 Ticket 是单上下文可验证的垂直切片。
- 用户确认粒度、拆合和依赖。
- 父项与精确 blocker set 已逐条 read-back。
- 无 orphan Ticket。
- 进入 Dispatch Gate,而不是假设已经能 Start。
## 6. 第三阶段:开发与收口
用户原设想:
```text
start work → trellis / inline → close work
```
建议展开为:
```text
Dispatch Gate
→ start-work-feishu
├─ Inline
│ → standard implement / tdd / diagnose
│ → full-diff review + acceptance evidence
│ → Ticket partial close ↔ continue
│ → Spec final close
└─ Trellis
→ bind/create planning task
→ snapshot/delta review
→ Base start patch + read-back
→ task.py start
→ standard implement / tdd
→ checkpoint + full-diff review
→ Ticket partial close ↔ continue
→ archive + status=completed
→ Spec final close
```
### 6.1 各角色的边界
| 角色/skill | 负责 | 不负责 |
|---|---|---|
| `start-work-feishu` | 查询、选择、刷新、路由、Inline 冻结或 Trellis mapping、开始 patch | 实现代码、完成 Ticket、初始化不存在的 Trellis |
| Inline | 小、明确、单上下文的生命周期选择 | 具体工程方法 |
| Trellis | 跨会话 planning、checkpoint、archive、`1 Spec = 1 task` | 替代 Spec、决定 Ticket 已完成 |
| `standard implement` / `tdd` | 实际编码和验证方法 | Base 状态和最终业务授权 |
| `code-review` / 主会话 acceptance check | 对完整 diff 做 Standards/Spec 检查 | 替代人类产品 review |
| `close-work-feishu` | 证据映射、Ticket 部分收口、Spec 最终收口、幂等对账 | 实现缺失代码、代替用户 review、代替 archive |
`implement` 原 skill 中的无条件 commit 与当前本机规则冲突;在 Trellis × Matt workflow 中已经被覆盖。commit、push、PR 仍分别需要用户明确授权。
### 6.2 Trellis 与 Wiki Spec 的事实源冲突
当前总结把 Wiki Spec 定义为叙述事实源,而 Trellis workflow 又把 `prd.md`称为 task-level spec source of truth。如果不分层,实施中修改 Trellis `prd.md`可能产生第二份业务 Spec。
建议统一为:
| 范围 | 权威来源 |
|---|---|
| 已批准的产品需求与验收 | Wiki Engineering Spec |
| 当前业务状态和关系 | Base |
| 当前实现批次的执行基线、技术计划和 checkpoint | Trellis `prd/design/implement` |
| 实现结果 | repo |
Trellis mapping 应增加或明确维护 `specUpdatedAt`/digest。发现 Wiki/Base Spec 变化时:
1. 停止使用旧 snapshot。
2. 重新读取并确认变更。
3. 刷新 Trellis artifacts。
4. 只 review delta。
不要让 Trellis 反向静默覆盖 Wiki,也不要双向自动同步长文档。
### 6.3 Trellis planning review 不应重复审全文
Trellis workflow 要求 planning artifacts 在 `task.py start` 前完成 review,而现有 `start-work-feishu` 在 artifact persistence 后可以直接写 Base 并 start;总结中的真实 POC还记录了一次 planning review 豁免。
更高效的门禁是:
- Feishu Spec/Tickets 已经完成用户 review,Trellis 只是忠实 snapshot:做自动一致性检查和 start confirmation,不重新 grill 全文。
- Trellis 新增了 Spec 中没有的技术决策、兼容策略、rollout/rollback 或重大执行取舍:只对新增 delta 做 `grill-with-docs` review,再 start。
- Artifact persistence 或 mapping read-back 失败:不写 Base。
这既关闭了 workflow 规则缺口,也避免重复审批。
### 6.4 Close 实际是循环,不是一次动作
`close-work-feishu` 有三个 route:
1. Ticket 部分收口。
2. Spec 最终收口。
3. 失败后的幂等对账。
Ticket 可以在 Trellis task 尚未 archive 时逐张完成。父 Spec 只有在以下门禁全部通过后才能完成:
- 所有 child Tickets 恰好为`已完成`。
- 每条 Spec acceptance 都有直接证据。
- 用户完成 Spec 最终 review。
- Trellis 路径的 task 已 archive 且 `status=completed`。
- 没有 Ticket write/read-back failure。
- 用户单独确认 Spec-only patch。
`task.py finish`只解除当前 session 指针,不能替代 archive 或 Base closure。
### 6.5 第三阶段最终产物
- repo:代码、测试、必要文档、真实命令结果、完整 diff review。
- Inline:冻结的 Spec/Ticket IDs 和当前会话证据;跨会话时必须重新按 ID 选择。
- Trellis:mapping、planning artifacts、checkpoint、archive task。
- Base Tickets:每张 Ticket 的验收映射、验证证据、代码引用和终态 read-back。
- Base Spec:最终验收证据、最终人类 review、终态 read-back。
- 来源 Issue:保留与 Spec 的父子关系供人类追踪;其状态由人类在 Base 中按需检查。
当前 `close-work-feishu` 关闭到 Spec 为止,不自动关闭或报告来源 Issue。此边界视为有意设计,不作为自动化断点。
## 7. 跨阶段传递性总表
| 阶段 | 最终产物 | 稳定身份/位置 | 下阶段如何消费 | 当前成熟度 |
|---|---|---|---|---|
| Setup(一次性) | tracker contract | repo `docs/agents/issue-tracker.md` | 所有 Feishu skills 读取 | 已验证 |
| 探索 | Base Issue + Wiki Discovery Brief | Issue record ID + Wiki URL | `grill-with-docs`、`to-spec` | 缺正式探索发布 adapter |
| 原型决策 | Prototype record/decision ticket + asset + verdict | record ID、path/branch/URL | Spec 的 Decisions/Sources | schema 已支持,发布 POC 待补 |
| 复杂探索 | Wayfinder Map + Decision Tickets | Map/Ticket record IDs | Discovery Gate 汇总 | skill 已有,Feishu POC 待补 |
| 领域转化 | `CONTEXT.md` + 必要 ADR | repo path/commit(如获授权) | Spec 使用词汇与决策 | 已有 skill |
| 工程规格 | Wiki Spec + Base Spec | Spec record ID + Wiki URL;parent=Issue | Tickets、Start、Trellis snapshot | 已真实 POC |
| 实施计划 | Base Tickets + dependency graph | Ticket IDs;parent=Spec;blocker IDs | Start 计算 frontier;Close 逐票验收 | 已真实 POC |
| 分派 | owner/priority/mode | Base fields | 进入当前用户 Start 队列 | **当前断点** |
| Inline 执行 | 会话冻结 IDs + repo 证据 | record IDs、diff/test refs | Close | 已设计;跨会话需重选 |
| Trellis 执行 | mapping + artifacts + checkpoint | task path + `meta.feishuTracker` | Close 解析 Spec、证据和 archive | 已真实 POC |
| Ticket 收口 | evidence/code refs + terminal read-back | Ticket record ID | 解锁下一 frontier | 已真实 POC |
| Spec 收口 | 全 Ticket、acceptance、review、archive、read-back | Spec record ID | 阶段交付完成;来源 Issue 由人类按需处理 | 已真实 POC |
## 8. 组合场景建议
| 场景 | 推荐组合 |
|---|---|
| 简单、无重大不确定性的需求 | `grill-me → Discovery Brief → grill-with-docs → to-spec → 少量 Tickets/直接 Start` |
| Greenfield UI 探索 | `grill-me → 静态 HTML 多变体 → verdict review → Discovery Brief` |
| Brownfield 页面调整 | `grill-me → 只读代码库 → 在真实路由做 UI variants → verdict → to-spec` |
| 状态机/业务逻辑不确定 | `grill-me → Logic TUI prototype → verdict → Spec 中吸收 decision-rich 状态模型` |
| 多会话、多条互相依赖的不确定性 | `Wayfinder Map → grilling/prototype/research/task frontier → Discovery Gate` |
| 外部 Issue/PR 进入 | `triage → needs-info/ready-for-agent → 必要时 Discovery/Spec → Tickets` |
| 小型已明确实现 | `start-work → Inline → standard implement → review → close` |
| 跨模块、长验收链 | `start-work → Trellis → delta review → implement/checkpoint → partial close → archive → final close` |
| 用户明确要求 test-first | `to-spec 确认公开 seam → TDD mode → vertical red/green slices → review → close` |
## 9. 更好的飞书 CLI 管理方案
### 9.1 保留四事实源,不做大一统同步
应继续坚持:
- Base 管“是什么状态、谁负责、依赖谁”。
- Wiki 管“为什么做、达成了什么共同理解”。
- Trellis 管“复杂执行如何恢复和归档”。
- repo 管“实际上实现和验证了什么”。
同步只传稳定 ID、版本、摘要、验收边界和证据指针。不要在 Base 复制全文,不要在 Trellis 复制业务状态,不要让 Wiki 保存一份代码侧事实副本。
### 9.2 现有 29 字段足以做 P0
P0 不建议先加字段。可以直接使用:
- `产物类型`
- `来源技能`
- `工作流阶段`
- `状态`
- `负责人`
- `最后更新人`
- `产物文档`
- `结论/摘要`
- `验收标准`
- `验证证据`
- `下一步`
- `代码引用`
- `所属父项`
- `前置依赖`
只有多个真实查询场景证明有价值时,才考虑新增多值`来源产物`或显式 version/digest 字段。
### 9.3 建议的 Base 视图/报告
| 视图/报告 | 条件或用途 |
|---|---|
| 探索中 | Issue/Wayfinder;阶段=探索/决策;状态=草拟/待确认 |
| 决策 frontier | Decision Ticket 未完成、未阻塞、未认领 |
| 待分派 | `ready-for-agent`且 owner 为空 |
| 我的可开始 | owner=当前用户且 blockers 全部恰好`已完成` |
| 进行中/阻塞 | 阻塞项必须有`阻塞原因`和`下一步` |
| 待评审 | Ticket 和 Spec 分组显示 |
| 待对账 | 写失败、read-back mismatch、Trellis archived 但 Base 未闭环 |
| 一致性异常 | 完成无证据、Ticket 无父项、Spec 完成但 child 未全完成、进行中但 owner 为空 |
Frontier 和一致性判断应由 CLI 完整分页后计算,不依赖第一页或肉眼判断。
### 9.4 Base Workflow 只做提醒,不做完成决策
当前 `lark-cli 1.0.76`支持 Base Workflow、views、record history 和 data query。推荐的自动化边界:
- 可以:状态变为待确认/待评审时通知 owner;未分派 ready item 超时提醒;阻塞项定期提醒。
- 不可以:因为测试通过、Trellis archive 或所有可见 Tickets 看似完成,就自动把 Spec 设为已完成。
- 不可以:用 Workflow 取代人类 review、写前重读和写后回读。
- Workflow 新建后保持 disabled;经过 dry-run/定义检查和用户确认后再单独 enable。
### 9.5 增加机器可读 tracker contract
当前 `docs/agents/issue-tracker.md`适合人读,但自动化需要解析自然语言。建议 Setup 稳定后同时生成:
```text
docs/agents/issue-tracker.md
docs/agents/issue-tracker.json
```
JSON 建议保存:
```text
schemaVersion
baseToken / tableId / viewIds / wikiRoot
semantic field key → field ID
enum values
completion gates
```
不保存 API credentials,也不保存固定个人 open ID;操作者身份仍在每次运行时通过 verified auth 解析。
### 9.6 抽机械 helper,不抽业务判断
六个 Feishu skills 已重复使用身份门禁、schema 验证、完整分页、record-ID patch、写前重读、写后回读、ignored-fields 检测和 batch reconciliation。可以在更多 POC 后抽取薄 helper:
```text
tracker auth-check
tracker contract-validate
tracker list-all
tracker patch-and-verify
tracker batch-patch-and-reconcile
tracker wiki-append-and-fetch
```
Helper 只包装当前 `lark-cli`并输出结构化 JSON/exit code。以下判断必须继续留在人类和 skill:
- 该问哪个产品问题。
- 谁应该负责。
- Inline 还是 Trellis。
- Ticket 是否满足验收。
- 是否允许关闭 Spec/Issue。
不要构建会自行推进全部状态的“大一统 Agent workflow”。
### 9.7 统一外部写入协议
所有 Feishu 记录写入继续遵守:
1. 读取 tracker contract。
2. 验证 bot 与当前人类 user,冻结本轮 user open ID。
3. 完整查询并用 record ID 定位。
4. 展示稳定 IDs 和精确 patch。
5. 用户确认外部写入。
6. 写前按 record ID 重读,发生漂移则确认失效。
7. 最小 patch;`负责人`和`最后更新人`语义分离。
8. 写后 `record-get`/完整分页回读。
9. 分别报告 updated、already synchronized、failed、mismatched。
Base record history 可用于单记录审计,但不能替代应用侧的全表一致性报告。
## 10. 推荐的 V2 主流程
```mermaid
flowchart TD
Setup["一次性 Setup<br/>tracker contract"] --> Intake["创建/接收 Base Issue"]
Intake --> Explore{"探索复杂度"}
Explore -->|"简单"| Grill["grill-me"]
Explore -->|"多会话/多决策"| Map["Wayfinder Map + Decision Tickets"]
Grill --> Proto{"需要可运行反馈?"}
Map --> Proto
Proto -->|"UI"| UI["HTML / 真实路由多变体"]
Proto -->|"Logic"| Logic["TUI / state model"]
Proto -->|"否"| Brief
UI --> Verdict["targeted verdict review"]
Logic --> Verdict
Verdict --> Brief["Discovery Brief + Discovery Gate"]
Brief --> GWD["grill-with-docs<br/>只处理代码库碰撞与 durable decisions"]
GWD --> Spec["to-spec-feishu<br/>Wiki Spec + Base Spec"]
Spec --> Tickets["to-tickets-feishu<br/>vertical Tickets + blockers"]
Tickets --> Dispatch["Dispatch Gate<br/>owner / priority / mode"]
Dispatch --> Start["start-work-feishu"]
Start -->|"Inline"| Impl["standard / tdd implementation"]
Start -->|"Trellis"| Bind["bind + snapshot/delta review"]
Bind --> Impl
Impl --> Review["full-diff review + evidence"]
Review --> Partial["close-work:Ticket 部分收口"]
Partial -->|"仍有开放 Ticket"| Impl
Partial -->|"全部完成"| Archive{"Trellis?"}
Archive -->|"是"| TArchive["archive + status=completed"]
Archive -->|"否"| Final
TArchive --> Final["close-work:Spec 最终收口"]
```
流程不是不可逆直线:
- 第二阶段发现产品结论与代码事实冲突:回到 Discovery Brief,重新确认受影响决策。
- 开发中发现 Spec/acceptance 错误:回第二阶段修订 Spec/Tickets并重新 read-back。
- 只在实现偏离而需求正确时留在第三阶段修代码。
- 任何回退都更新 owning artifact,不依赖聊天摘要。
## 11. 落地优先级
### P0:先补断链,不改大架构
> 决策(2026-07-28):实施以下 1–5;不增加来源 Issue 状态报告或聚合关闭,由人类在 Base 中自行检查。
1. 明确第一阶段产物叫 Discovery Brief,不叫第二份 PRD。
2. 把 `handoff` 从必经节点改成“发生 session 边界时才使用”。
3. 建立 Base “待分派”视图,补 Dispatch Gate。
4. 让 `to-spec-feishu` 显式读取来源 Issue、Discovery Wiki 和 prototype/research refs。
5. 为 Feishu-bound Trellis task 定义 snapshot/delta review,消除重复全文审批。
### P1:让复杂探索可管理
1. 为 Discovery/Prototype/Wayfinder 做一次真实 Base/Wiki create/update/read-back POC。
2. 增加 CLI 分派或 start self-claim 能力。
3. 建立探索、决策 frontier、待分派、待对账和一致性异常视图。
### P2:稳定后工程化
1. 生成机器可读 tracker JSON contract。
2. 抽取 `lark-cli` 机械 helper。
3. 多个真实查询证明需要后,再扩展`来源产物`或 version/digest schema。
4. 仅把提醒类流程沉淀为 Base Workflow,不自动做完成决策。
## 12. 最终评价
这套方案不需要推翻。它已经有一个很好的骨架:
```text
Issue → Spec → Tickets → Start → Inline/Trellis → Evidence → Partial/Final Close
```
真正需要调整的是五个语义:
1. `handoff` 是会话运输,不是业务产物。
2. Discovery Brief 是产品探索结论,不是 Engineering Spec。
3. 可运行原型不必然是 HTML。
4. `ready-for-agent` 不等于已分派,也不等于可以被 Start 查询。
5. Trellis 是执行基线和生命周期,不应成为第二份业务 Spec。
补上 Discovery 发布、Dispatch Gate、明确 implementation owner 和 Trellis delta review 后,三个阶段的产物就能形成可追踪、可恢复、可审计的闭环;来源 Issue 的后续状态继续由人类管理。
@@ -0,0 +1,689 @@
# 基于飞书的产物管理工作流
这套流程把需求澄清、规格、拆票、代码实现和收口串到飞书 Base/Wiki 与 Trellis 上。
本文以当前安装的 skill 名称为准:
- `setup-feishu` 指 `$setup-workflow-skills-feishu`;
- `start-feishu-work` 指 `$start-work-feishu`;
- `close-feishu-work` 指 `$close-work-feishu`;
- `to-sepc-feishu` 的正确名称是 `$to-spec-feishu`。
当前版本有三个明确边界:
1. Issue 不进入这套 Base;需求在进入 Base 前,通过对话、研究、原型或其他人工确认完成澄清。
2. 整个流程不再使用 `triage-feishu`,也不创建 Triage dossier、Agent Brief 或 Human Brief。
3. Base 只保留一个状态轴和 6 个状态,不再维护 `工作流阶段`。
## 事实源怎么分工
| 载体 | 负责保存 | 不负责保存 |
|---|---|---|
| 飞书 Base | Spec/Ticket 身份、项目、状态、负责人、父子关系、依赖、验收和验证摘要 | 大段需求正文、完整设计文档 |
| 飞书 Wiki / Docx | Engineering Spec、Map、研究、决策、长篇说明 | 可计算的工作队列和关系状态 |
| 代码仓库 | 代码、测试、ADR、领域文档和实现证据 | Base 当前状态 |
| Trellis | 多会话执行上下文、计划、源快照、归档证据 | 第二份 Spec 或 Ticket 主数据 |
几个始终有效的规则:
- Base 管状态和关系,Wiki 管长文,代码仓库管实现和验证。
- Trellis 只保存执行上下文,不能静默改写 Wiki Spec 或 Base Ticket。
- Feishu CLI 的 Base、Wiki、Docs 操作统一使用 `--as bot --format json`。
- API 由 bot 执行,但 `最后更新人` 必须写当前已验证用户。
- 每次写入都要回查,不能只看 `ok: true`。
# 一、初始化
## 1.1 初始化飞书 CLI
### 安装
官方推荐安装方式:
```bash
npx @larksuite/cli@latest install
```
安装后先确认实际版本:
```bash
command -v lark-cli
lark-cli --version
```
本文验证时使用的是 `lark-cli 1.0.76`。换机器或升级版本后,先查看当前版本和子命令 `--help`,不要直接照搬旧参数。
### 初始化应用身份
```bash
lark-cli config init --new
```
需要进入交互流程时可以使用:
```bash
lark-cli config init
```
注意:
- `app_secret` 不能写入文档、Trellis 产物或命令历史;
- 非交互环境优先使用 `--app-secret-stdin`;
- 如果当前环境已经绑定应用,先确认是否应使用 `lark-cli config bind`,不要未经确认创建平行应用。
### 登录用户身份
推荐登录:
```bash
lark-cli auth login --recommend
```
按业务域登录:
```bash
lark-cli auth login --domain base,docs,wiki
```
不能阻塞等待授权时,使用设备码:
```bash
lark-cli auth login --domain base,docs,wiki --no-wait --json
lark-cli auth login --device-code <DEVICE_CODE>
```
### 验证双身份
```bash
lark-cli auth status --json --verify
```
必须同时满足:
- `identities.bot.verified=true`;
- `identities.user.verified=true`;
- `identities.user.openId` 非空。
飞书操作由 bot 执行。用户 open ID 只用于查询当前用户的工作和填写 `最后更新人`,不能固化成全局配置。
最小检查:
```bash
lark-cli --version
lark-cli auth status --json --verify
lark-cli base --help
lark-cli wiki --help
lark-cli docs --help
```
官方入口:[Lark CLI README](https://github.com/larksuite/cli/blob/main/README.md)。
## 1.2 项目初始化:`setup-workflow-skills-feishu`
CLI 初始化解决“能不能访问飞书”,Setup 解决“这个仓库应该访问哪张 Base、哪个 Wiki 项目目录”。
```text
$setup-workflow-skills-feishu
```
### 提前准备的飞书内容
需要明确提供:
1. Base 表格或视图 URL;
2. Wiki 工作区根节点 URL;
3. 项目目录名称,或已有项目节点 URL。
还应提前确认:
- 一张用于管理 Spec、Ticket 和进度的 Base 表;
- 一个明确的 Base 视图;
- bot 可访问的 Wiki 知识空间;
- 一个已确认的项目名,项目名不必等于仓库名;
- bot 对 Base、Wiki、Docs 的应用权限和资源 ACL;
- 用户 OAuth 已完成,bot 和 user 都能通过验证。
推荐的 Wiki 层级:
```text
<WORKSPACE_ROOT>
├── 知识文档
└── 项目目录
└── <PROJECT_NAME>
```
当前只使用以下路由,均指向项目节点:
| 路由 | 用途 |
|---|---|
| `setup` | 项目配置说明 |
| `spec` | Engineering Spec |
| `wayfinder` | Map 与决策文档 |
不再创建 `triage` 路由,也不把 `知识文档` 当作 skill 写入失败后的兜底位置。
### Setup 的两个独立选择
| 维度 | 推荐模式 | 可写模式 | 作用 |
|---|---|---|---|
| Tracker | Reuse tracker | Bootstrap tracker | 只读校验或补齐 Base 标准字段 |
| Project Binding | Reuse existing binding | Provision missing binding | 校验或创建 Wiki 项目目录,并补充项目选项 |
- **Reuse tracker**:只读检查 Base 坐标、字段和选项;不创建字段、不创建配置文档。
- **Bootstrap tracker**:经过确认后,只新增缺失字段并创建一份项目配置说明。
- **Reuse existing binding**:只读检查 Wiki 目录和 `所属项目` 选项。
- **Provision missing binding**:经过独立确认后,只创建缺失节点或追加缺失项目选项。
两个授权范围互不包含。字段修复不等于允许创建 Wiki 节点,项目目录授权也不等于允许改表结构。
### Setup 的产出
Setup 会留下:
- `CLAUDE.md` 或 `AGENTS.md` 中的 `## Agent skills` 区块;
- `docs/agents/issue-tracker.md`;
- `docs/agents/domain.md`;
- Bootstrap 模式下的缺失字段和项目配置说明文档;
- Provision 模式下的项目 Wiki 节点或项目选项。
不再生成 `docs/agents/triage-labels.md`。后续 Feishu skill 读取的是 `docs/agents/issue-tracker.md`,其中应记录契约版本、Base/Wiki 坐标、真实主字段、项目绑定、24 个字段、路由和可执行命令。
## 1.3 当前 Base 字段:24 个
旧契约有 30 个字段。根据当前流程,删除了 6 个:
- `工作流阶段`;
- `类别`;
- `外部编号`;
- `报告人`;
- `最后反馈时间`;
- `最后分诊时间`。
原因是 Issue 不进入 Base,triage 已移除,而且状态本身已经足够表达下一步动作。当前契约共 24 个字段:
| # | 字段 | 类型 / 典型值 | 含义与功能 |
|---:|---|---|---|
| 1 | 实际主字段 | 主字段文本 | 记录标题。创建前用于查重,更新必须使用 record ID。 |
| 2 | 产物文档 | 文本 / URL | Wiki Spec、Map 或其他长文产物的规范链接。 |
| 3 | 产物类型 | 单选 | `PRD/Spec`、`实现 Ticket`、`Wayfinder Map`、`决策 Ticket`、`原型`、`研究`、`Handoff`、`ADR`、`领域词汇`、`Bug 诊断`、`代码评审`、`架构候选`、`教学资产`。不再有 `需求/Issue`。 |
| 4 | 所属项目 | 单选,`multiple=false` | 项目边界。父项和依赖必须属于同一项目。 |
| 5 | 来源技能 | 多选 | 记录由哪个 skill 创建或维护,如 `to-spec`、`to-tickets`、`start-work`、`close-work`、`wayfinder`。不再包含 `triage`。 |
| 6 | 来源链接 | 文本 / URL | 当前对话、PR、表单、研究或外部系统的来源链接。 |
| 7 | 状态 | 单选 | 只允许 6 个状态:`ready-for-agent`、`进行中`、`阻塞`、`待评审`、`已完成`、`wontfix`。 |
| 8 | 决策票类型 | 单选 | `research`、`prototype`、`grilling`、`task`。 |
| 9 | 协作模式 | 单选 | `HITL` 或 `AFK`。表示执行过程中人工参与的强度,不替代状态。 |
| 10 | 优先级 | 单选 | `P0`—`P3`,用于排序和资源安排。 |
| 11 | 负责人 | 单用户 | 当前执行人。Start 不擅自覆盖。 |
| 12 | 最后更新人 | 单用户 | 最近一次通过 CLI 改动记录的真实操作者。 |
| 13 | 完成度 | 数字 / 百分比 | 进度量化,收口时写为 `1`。 |
| 14 | 验收标准 | 长文本 | 可观察、可验证的完成条件。 |
| 15 | 验证证据 | 长文本 | 实际命令、测试结果、回查事实和未运行项。 |
| 16 | 结论/摘要 | 长文本 | Base 中的简洁摘要;`wontfix` 的具体原因也写在这里。 |
| 17 | 阻塞原因 | 长文本 | `状态=阻塞` 时解释为什么不能继续。 |
| 18 | 下一步 | 长文本 | 解除阻塞或继续推进的明确动作。 |
| 19 | 代码引用 | 长文本 | 文件路径、分支、commit、PR、评审链接或 Trellis 证据。 |
| 20 | 截止时间 | 日期时间 | 业务期望完成时间。 |
| 21 | 创建时间 | 系统字段,只读 | 排序和审计。 |
| 22 | 更新时间 | 系统字段,只读 | 并发变更检测和源快照比较。 |
| 23 | 所属父项 | 同表关联 | Ticket → Spec,决策 Ticket → Map。 |
| 24 | 前置依赖 | 同表关联 | blocker 的真实 record ID,用于计算可执行 frontier。 |
### 6 个状态的含义
```text
ready-for-agent → 进行中 → 待评审 → 已完成
↘ 阻塞 ↗
任一未完成状态 ─────────────→ wontfix
```
| 状态 | 含义 | 是否进入 Start 队列 |
|---|---|---:|
| `ready-for-agent` | 已澄清、可以交给 Agent 开始 | 是,前置依赖满足时可选 |
| `进行中` | 已认领,正在实现或恢复工作 | 是 |
| `阻塞` | 被依赖、决策或外部条件卡住 | 是,作为可恢复项展示 |
| `待评审` | 实现完成,等待人工评审和 Close | 否,转给 Close |
| `已完成` | 验收和人工确认完成 | 否 |
| `wontfix` | 不再交付;原因写入 `结论/摘要` | 否 |
`wontfix` 是唯一非交付终态。它不能满足其他 Ticket 的前置依赖;只有 `已完成` 才表示依赖交付。
---
# 二、需求澄清阶段
当前流程不再把需求先写成 Issue 再分诊。需求澄清发生在对话、研究、原型、grilling 或人工决策中;只有形成明确方案后,才写入 Spec/Ticket Base。
## 2.1 `to-spec-feishu`
```text
$to-spec-feishu
```
`to-spec-feishu` 使用已经形成的上下文,不重新访谈用户:
1. 读取当前对话、领域术语、代码库和 ADR;
2. 找到尽可能高层、数量尽可能少的测试 seam;
3. 让用户确认 seam 和 Spec 方向;
4. 生成 Problem Statement、Solution、User Stories、Implementation Decisions、Testing Decisions、Out of Scope 和 Further Notes;
5. 将完整 Spec 发布到 Wiki;
6. 创建关联的 `PRD/Spec` Base 记录。
发布顺序是先 Wiki、后 Base。Wiki fetch 回查正文完整后,才创建 Base 记录。
Base Spec 的初始值:
```text
产物类型 = PRD/Spec
状态 = ready-for-agent
协作模式 = AFK
产物文档 = Wiki Spec 链接
```
如果决定不再交付已经创建的 Spec,可以把它设为 `wontfix`,并在 `结论/摘要` 写明原因;这不是 triage,而是产物生命周期中的人工决策。
## 2.2 `to-tickets-feishu`
```text
$to-tickets-feishu
```
`to-tickets-feishu` 把已批准的 Spec 拆成 tracer-bullet 垂直切片:
- 每张 Ticket 穿过完成行为所需的各层;
- 单独完成后可演示或验证;
- 能在一个新上下文窗口内完成;
- 只声明真实的阻塞关系;
- 宽范围机械重构使用 `expand → migrate batches → contract`。
发布前必须让用户确认粒度、拆分和依赖。发布分两步:
1. 按依赖顺序创建全部 Ticket,保存真实 record ID;
2. 用 record ID 回写 `所属父项` 和 `前置依赖`,逐条回查。
每条 Ticket 的初始值:
```text
产物类型 = 实现 Ticket
状态 = ready-for-agent
协作模式 = AFK
所属父项 = 父 Spec record ID
前置依赖 = blocker record IDs
产物文档 = 留空
```
V1 不给每张 Ticket 单独建 Wiki 文档;Ticket 的切片事实保存在 Base,长文仍归属于父 Spec。
## 2.3 需求澄清的全部操作可能
```mermaid
flowchart TD
A["当前对话、研究、原型或其他需求来源"] --> B{"是否已经有明确的目标、约束和验收?"}
B -- "否" --> C["HITL:继续澄清、grilling 或补充研究"]
C --> B
B -- "是" --> D{"是否决定继续交付?"}
D -- "否" --> E["不创建 Issue;若已有 Spec/Ticket,则设为 wontfix 并记录原因"]
D -- "是" --> F["to-spec-feishu:发布 Wiki Engineering Spec"]
F --> G["Base Spec:PRD/Spec + ready-for-agent"]
G --> H{"是否需要多个独立垂直切片?"}
H -- "否" --> I["直接进入 start-work-feishu"]
H -- "是" --> J["to-tickets-feishu:起草 Ticket 和依赖图"]
J --> K{"用户是否批准粒度和依赖?"}
K -- "否" --> J
K -- "是" --> L["创建 Ticket、回写父子关系和前置依赖"]
L --> M["Spec + Tickets 进入开发队列"]
```
本阶段的正式产出只有:
- Wiki Engineering Spec;
- 一个 `PRD/Spec` Base 记录;
- 可选的多个 `实现 Ticket` Base 记录;
- 父子关系、依赖关系和可执行 frontier。
不再产出:
- `需求/Issue` Base 记录;
- Triage dossier;
- `needs-triage`、`needs-info`、`ready-for-human` 等状态;
- `类别`、`最后分诊时间` 等 triage 字段。
---
# 三、代码开发阶段
代码开发阶段由 `$start-work-feishu` 开始,由 `$close-work-feishu` 收口。Start 负责“选中并认领”,Close 负责“证据核验并关闭”。
## 3.1 `start-work-feishu`
```text
$start-work-feishu
```
### 功能
Start 会:
- 验证项目契约、双身份、Base 坐标和 24 个字段;
- 查询当前用户负责的 Spec 和 Ticket;
- 解析父项、依赖、负责人和更新时间;
- 把记录分为可开始、可恢复、被阻塞和待评审;
- 让用户选择具体 record ID;
- 推荐 Inline 或 Trellis;
- 经确认后把目标记录改为 `状态=进行中`,并回写 `最后更新人`。
它不会自动改负责人、完成度、兄弟 Ticket,也不会因为看到了 `ready-for-human` 而转派工作,因为当前状态机已经删除这个状态。
### 工作队列
| 条件 | 分类 | 是否可选 |
|---|---|---:|
| `进行中` | 可恢复 | 是 |
| `阻塞` | 可恢复,但展示阻塞原因 | 是 |
| `ready-for-agent`,无依赖或依赖全为 `已完成` | 可开始 | 是 |
| `ready-for-agent`,存在未完成依赖 | 被阻塞 | 否 |
| `待评审` | 待收口 | 否,转 Close |
| `wontfix`、`已完成` 或其他不匹配记录 | 不进入开发队列 | 否 |
只有 `已完成` 能满足依赖。`wontfix` 不是依赖完成证明。
选择 Spec 时只更新 Spec。选择 Ticket 时,同时更新 Ticket 和唯一父 Spec:
```text
状态 = 进行中
最后更新人 = 当前已验证用户
```
负责人、所属项目、完成度和未选中的兄弟 Ticket 保持不变。
### Inline 与 Trellis
适合 Inline:
- 范围窄,路径已知;
- 一个上下文可以完成;
- 验证命令能在当前会话运行。
适合 Trellis:
- 跨模块或多会话;
- 有多个稳定决策和交付物;
- 需要保存长验收链;
- 仓库已经有 `.trellis/`。
仓库没有 `.trellis/` 时,Start 不会自行初始化;需要用户单独授权后再初始化,或选择 Inline。
## 3.2 Start 如何承接 Trellis
### 一对一绑定
```text
1 个飞书 Spec = 1 个 Trellis task
```
Ticket 是该 task 内的计划和验收单元,不为每张 Ticket 创建独立 Trellis 子任务。直接选择 Ticket 时,也绑定到父 Spec 的同一个 task。
映射保存在:
```text
task.json.meta.feishuTracker
```
至少保存 `specRecordId`、`ticketRecordIds`、`selectedRecordIds`、`specWikiUrl`、`trackerContractPath`、`boundAt` 和 `lastRefreshAt`。映射里不保存凭证、Base token 或固定用户 ID。
### 创建和启动顺序
```bash
python3 ./.trellis/scripts/task.py create "<SPEC_TITLE>" \
--slug "feishu-<NORMALIZED_SPEC_RECORD_ID>" \
--description "Implement Feishu Spec <SPEC_RECORD_ID>" \
--no-start
```
正确顺序:
1. 在 `prd.md` 维护飞书 Spec 来源区块;
2. 在 `implement.md` 维护 Ticket 快照;
3. 写入并回查 `task.json.meta.feishuTracker`;
4. 比较 Trellis 快照与飞书批准源;
5. 用户确认选择、路由、稳定 ID 和 Base patch;
6. 更新 Base 并回查;
7. Base 认领成功后启动 Trellis:
```bash
python3 ./.trellis/scripts/task.py start <TASK_DIR>
```
这样可以把失败范围控制在一个边界内:本地映射失败不碰 Base,Base 回查失败不启动 Trellis。
### 快照判定
| 判定 | 含义 | 动作 |
|---|---|---|
| `snapshot-only` | 只是已批准 Spec/Ticket 的忠实镜像 | 做机械一致性检查 |
| `delta-reviewed` | 新增实现顺序、兼容、迁移、安全或回滚决策 | 只评审新增技术差量 |
| `source-revision-required` | 改变产品行为、范围、验收或依赖语义 | 回到 Wiki/Base 修订正式源 |
Trellis 能补充执行计划,不能静默变更正式业务产物。
### Trellis 生命周期
| 命令 | 语义 |
|---|---|
| `task.py create --no-start` | 创建 `planning` 任务,不设为活动任务 |
| `task.py start <TASK_DIR>` | 将任务设为活动并改为 `in_progress` |
| `task.py finish` | 只清除当前会话活动指针,不代表完成 |
| `task.py archive <TASK_DIR> --no-commit` | 写入 `completed` 并移动到月度归档目录 |
Trellis 路线最终关闭 Spec 时,要求归档树下存在 `task.json` 且 `status=completed`。按本机约定,归档使用 `--no-commit`。
## 3.3 `close-work-feishu`
```text
$close-work-feishu
```
### 功能
Close 会:
- 从 Trellis 映射、归档 task 或 Inline 冻结 ID 解析 Spec;
- 每次重新查询全部子 Ticket;
- 从代码、测试、命令结果、Trellis 产物和 Base 记录收集直接证据;
- 将 Ticket 分为建议可收口、待补证/待验证、明确未完成/阻塞;
- 只关闭用户明确选择的 Ticket;
- Ticket 回查通过后,再判断 Spec 是否满足最终门禁;
- 需要时对部分成功的写入进行幂等重放。
Close 不补代码、不自动归档 Trellis,也不因为测试通过或 Agent 说“完成”就改变 Base 状态。
### Ticket 证据分类
| 分类 | 条件 | 默认动作 |
|---|---|---|
| 建议可收口 | 每条验收都有直接证据,且没有未解决阻塞 | 交给人审 |
| 待补证/待验证 | 可能已实现,但至少一条验收缺少直接证据 | 不关闭 |
| 明确未完成/阻塞 | 行为、依赖、决策、实现或验证仍未完成 | 不关闭 |
### Ticket 收口
用户必须明确两件事:
1. 代码和功能的人审已经完成;
2. 哪些 Ticket record ID 允许关闭。
选中的 Ticket 更新为:
```text
状态 = 已完成
完成度 = 1
阻塞原因 = 空
下一步 = 空
验证证据 = 保留原文并追加本次 closure 证据
代码引用 = 保留原文并追加去重后的真实引用
最后更新人 = 当前已验证用户
```
每条记录都按 ID 回查。未选择的 Ticket 保持原状。只要还有 Ticket 不是 `已完成`,Spec 就不能完成。
### Spec 最终收口
必须同时满足:
- 所有子 Ticket 恰好为 `已完成`;或没有 Ticket,但 Spec 自身验收已有直接证据;
- Spec 每条验收都有直接证据;
- 没有 Ticket 写入失败或回查不一致;
- 用户明确确认最终人审完成;
- Trellis 任务已归档且 `task.json.status=completed`;Inline 无此门禁。
Ticket 关闭确认不能自动授权 Spec 关闭。Spec 必须再展示一次独立 patch 并取得确认。
## 3.4 Start → Inline / Trellis → Close
```mermaid
flowchart TD
A["start-work-feishu:验证身份、24 字段、Base 和队列"] --> B["用户选择 Spec 或 Ticket"]
B --> C["刷新 record ID、父项、依赖、负责人和更新时间"]
C --> D{"选择执行路由"}
D -- "Inline" --> E["冻结 ID 和最新快照"]
E --> F["确认选择、路由和 Base patch"]
F --> G["Base:状态改为进行中并回查"]
G --> H["当前会话实现、测试和整理证据"]
D -- "Trellis" --> I{"已有唯一 Spec task?"}
I -- "有" --> J["复用 task"]
I -- "没有" --> K["create --no-start:planning"]
I -- "冲突" --> L["停止并处理映射冲突"]
J --> M["刷新 Spec 来源、Ticket 快照和映射"]
K --> M
M --> N{"快照判定"}
N -- "snapshot-only" --> O["机械一致性检查"]
N -- "delta-reviewed" --> P["评审新增技术决策"]
N -- "source-revision-required" --> Q["回到 Wiki/Base 修订正式源"]
Q --> M
O --> R["确认选择、路由、判定和 Base patch"]
P --> R
R --> S["Base:状态改为进行中并回查"]
S --> T["task.py start:planning → in_progress"]
T --> U["多会话实现、测试、评审和证据"]
U --> V["task.py archive --no-commit:completed"]
H --> W["close-work-feishu:查询全部 Ticket 并收集证据"]
V --> W
W --> X["输出证据表和收口建议"]
X --> Y{"人工评审并选择 Ticket"}
Y -- "证据不足或未选择" --> Z["保留未完成记录并给出下一步"]
Y -- "确认关闭" --> AA["更新所选 Ticket 并逐条回查"]
AA --> AB{"全部 Ticket 完成且 Spec 验收满足?"}
AB -- "否" --> AC["部分收口:Spec 保持进行中"]
AB -- "是" --> AD{"Trellis 已归档?"}
AD -- "未归档" --> AE["阻塞 Spec 收口;归档后重跑 Close"]
AD -- "Inline 或已归档" --> AF["第二次人审:确认 Spec 最终关闭"]
AF --> AG["更新 Spec 为已完成并回查"]
```
---
# 四、全流程总结
## 4.1 从初始化到收口
```mermaid
flowchart LR
A["CLI 初始化"] --> B["Setup:解析 Base、Wiki 和项目"]
B --> C["24 个字段 + 项目绑定 + spec/wayfinder 路由"]
C --> D["HITL:对话、研究、原型和决策完成需求澄清"]
D --> E["to-spec-feishu:Wiki Spec + Base PRD/Spec"]
E --> F{"是否需要拆票?"}
F -- "否" --> G["start-work-feishu:直接开始 Spec"]
F -- "是" --> H["to-tickets-feishu:Base Tickets + 依赖图"]
H --> I["start-work-feishu:选择 Spec 或 Ticket"]
G --> J{"Inline / Trellis"}
I --> J
J --> K["代码、测试、评审和验证证据"]
K --> L["close-work-feishu:关闭 Ticket"]
L --> M{"Trellis 路线?"}
M -- "是" --> N["确认 task 已归档"]
M -- "否" --> O["跳过归档门禁"]
N --> P["第二次人审:关闭 Spec"]
O --> P
P --> Q["Base、Wiki、代码和 Trellis 形成闭环"]
```
## 4.2 每个环节的产出
| 环节 | 主要输入 | 主要产出 | 事实源 |
|---|---|---|---|
| CLI 初始化 | 飞书应用、用户授权 | bot/user 双身份可验证 | Lark CLI 配置 |
| 项目 Setup | Base URL、Wiki 根 URL、项目名 | 24 字段契约、项目绑定、路由、`docs/agents/*` | Base + Wiki + Repo |
| 需求澄清 | 对话、研究、原型、人工决策 | 已批准的产品目标、约束和验收 | Conversation/Wiki |
| To Spec | 已澄清上下文、领域术语、测试 seam | Wiki Engineering Spec、Base `PRD/Spec` | Wiki + Base |
| To Tickets | 已批准 Spec | Base Ticket、父子关系、依赖图、frontier | Base |
| Start | 当前用户队列、record ID、依赖 | `进行中` 状态、Inline 上下文或 Trellis 映射 | Base + Conversation/Trellis |
| 实现 | Spec、Ticket、代码库、Trellis 计划 | 代码、测试、命令结果、验证证据 | Repo + Trellis |
| Close Ticket | 实时 Ticket、直接证据、人审选择 | Ticket `已完成` 或保留原状态 | Base |
| Trellis Archive | 已完成的多会话任务 | `status=completed` 和归档路径 | Trellis |
| Close Spec | 全部 Ticket、Spec 验收、最终人审 | Spec `已完成` | Base |
## 4.3 HITL 与 AFK 的边界
### HITL:必须有人拍板
| 环节 | 人工门禁 |
|---|---|
| Setup | 选择 Reuse/Bootstrap、Reuse/Provision,并确认具体外部写入 |
| 需求澄清 | 确认目标、范围、约束、验收和是否继续交付 |
| To Spec | 确认测试 seam 和正式 Spec |
| To Tickets | 确认 Ticket 粒度、拆分和阻塞边 |
| Start | 选择 record ID,确认 Inline/Trellis 和 Base patch |
| Trellis 差量 | 评审新增技术决策;影响产品行为时回到正式源 |
| Close Ticket | 确认代码/功能人审完成,并选择允许关闭的 Ticket |
| Close Spec | 在所有门禁满足后进行最终关闭确认 |
### AFK:确认后可以交给 Agent
- 校验 CLI 身份、Base 坐标、字段和项目绑定;
- 分页查询、关系解析、frontier 计算和状态分类;
- 根据已确认上下文起草 Spec 和 Ticket;
- 创建批准后的 Wiki/Base 产物并回查;
- 建立、刷新和校验 Trellis 映射;
- 在 Inline 或 Trellis 中实现代码、运行测试并整理证据;
- 为 Close 生成验收映射、证据表和候选 patch。
`协作模式=AFK` 只代表执行阶段适合交给 Agent,不代表全程无人参与。当前没有 `ready-for-human` 状态;人工参与由 HITL 门禁、`待评审` 和显式确认表达。
## 4.4 运行时边界
1. 不用标题更新记录,始终使用稳定 record ID。
2. 不猜 Base、table、view、Wiki 节点或项目名,全部来自项目契约和真实解析。
3. 不把未知 JSON 响应当成空结果。
4. 不因 `ok: true` 宣称成功,所有写入都要回查。
5. 不把 bot ID 写进 `最后更新人`。
6. 不覆盖 `负责人` 来表达归因。
7. 不跨项目建立父子或依赖关系。
8. 不把 Wiki Spec 全文复制成另一份 Trellis 业务 Spec。
9. 不把 `task.py finish` 当作 Trellis 完成。
10. 不因测试通过、Agent 评审通过或 Trellis 归档自动关闭飞书记录。
11. Ticket 收口和 Spec 最终收口分别获得人工确认。
12. 没有直接验证证据时,记录“未运行”及原因,不能声称完成。
## 4.5 各系统各管一段
```text
对话 / 研究 / 原型
│ 需求决定、验收和风险
▼
飞书 Wiki:Engineering Spec、Map、研究和决策长文
│ 长文链接
▼
飞书 Base:Spec/Ticket 状态、负责人、父子关系、依赖和证据摘要
│ 执行上下文映射
▼
Trellis:计划、多会话上下文、快照和归档
│ 代码与验证
▼
代码仓库:实现、测试、ADR 和真实引用
```
最终闭环是:需求在进入 Base 前已经明确,Wiki 保存可读的正式产物,Base 保存可查询的工作状态,Trellis 保存执行上下文,代码仓库保存实现事实;人工只在关键决策、认领和收口处拍板。
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
https://midscenejs.com/llms-full.txt
@@ -0,0 +1,182 @@
后端提供**协议**,前端提供**组件**。任何用户态数据接口(DataApi)在令牌缺失时返回统一错误码 `FS_AUTH_REQUIRED`,前端拦截器识别后驱动授权流程,授权完成自动重放原请求。业务方感知为零。
**授权发起方式变更**:废弃「后端发送飞书卡片消息引导授权」的交互(`POST /fs/auth/sendAuthCard` 不再用于本流程)。改为后端提供纯数据接口「准备授权」,前端拿到授权 URL 后自行决定打开方式与 UI 呈现。IM 卡片仅保留为后台任务(如公共用户令牌失效)的兜底通知手段。
```mermaid
sequenceDiagram
participant U as 用户
participant FE as 前端组件
participant GW as appcenter 网关
participant FS as 飞书开放平台
U->>FE: 触发功能(如搜索云文档)
FE->>GW: POST /fs/docs/search
GW->>GW: mainUserId -> openId -> Redis 取 user_access_token
alt 令牌缺失/失效
GW-->>FE: code=FS_AUTH_REQUIRED { corpNo, appId, scopes }
FE->>FE: 弹授权引导层(用户点击触发,防浏览器拦截)
FE->>GW: POST /fs/auth/prepare { corpNo, appId, scopes }
GW->>GW: 生成 authId,授权会话预存 Redis(绑定 mainUserId)
GW-->>FE: { authId, authUrl }(state=authId)
FE->>FS: window.open / applink 打开授权页
U->>FS: 点击授权
FS->>GW: 回调 /anonymous/fs/auth/callback.cl?code&state
GW->>FS: code 换 token(authen/v2/oauth/token)
GW->>GW: 缓存 per-user token(+refresh_token) + 授权结果
GW-->>FS: 返回自动 window.close() 页面
loop 轮询(2s 间隔,120s 超时)
FE->>GW: GET /fs/auth/authResult?messageId=authId
end
GW-->>FE: FSUserInfoCO(授权成功)
end
FE->>GW: 重放 POST /fs/docs/search
GW-->>FE: 搜索结果
```
## 2. 后端协议设计
### 2.1 统一未授权信号(改造点 A)
新增响应码 `FS_AUTH_REQUIRED`(`AppCenterResponseCode`)。所有用户态 DataApi 在取不到有效 `user_access_token` 时返回:
```json
{
"success": false,
"code": "FS_AUTH_REQUIRED",
"message": "需要飞书授权",
"data": {
"corpNo": "FEBG3X19R5J",
"appId": "cli_xxx",
"scopes": ["search:docs:read", "drive:drive:readonly"]
}
}
```
- 此响应只携带授权上下文;`authId` 由前端随后调用 `/fs/auth/prepare`(见 2.2)时生成,充当授权流程的 `state` 与结果查询的 `messageId`。
- 落地方式:在 `BaseFeishuController` 增加 `requireUserToken(corpNo, appId, openId, scopes)` 帮助方法,DataApi 统一调用,避免每个 Controller 重复判断。
- 首个改造对象:`FSDocsController.search`(顺带落实 `FSDocSearchQry.corpNo` 的 fixme,由后端根据绑定租户推导)。
### 2.2 授权发起接口(新增,替代卡片模式)
新增 `POST /fs/auth/prepare`(需登录态),**一步完成**「创建授权会话 + 构造授权 URL」,前端无需再分别调两个接口:
请求体:
```json
{
"corpNo": "FEBG3X19R5J",
"appId": "cli_xxx",
"scopes": ["search:docs:read", "drive:drive:readonly"]
}
```
响应 `data`:
```json
{
"authId": "uuid",
"authUrl": "https://accounts.feishu.cn/open-apis/authen/v1/authorize?...&state=<authId>",
"expireSeconds": 600
}
```
服务端内部逻辑:
1. 从登录态取 `mainUserId`,生成 `authId`(UUID);
2. 授权会话预存 Redis:`COMMON_FS_SELF_BUILT_APP_AUTH_CARD + authId` -> `{ corpNo, appId, businessId=authId, mainUserId, scopes }`,TTL 10 分钟;
3. scope 拼接:`业务 scopes + offline_access`(为 refresh_token 做准备),空格分隔;
4. `redirectUri` 固定为 `{gatewayHost}/appcenter/anonymous/fs/auth/callback.cl`(必须已在开发者后台「重定向 URL」白名单中,否则换 token 时报 20071),复用 `FSService.oauth2buildAuthorizationUrl` 构造 URL。
说明:
- 前端可以在收到 `FS_AUTH_REQUIRED` 后带着响应里的上下文直接调本接口;也可以在进入功能页时**预检**(可选调用,scopes 由前端声明),提前完成授权再使用功能。
- 旧的 `POST /fs/auth/sendAuthCard` 与 `POST /anonymous/fs/auth/authorizationUrl` 保留不动(兼容存量调用方),新流程不使用。
### 2.3 授权回调(复用现有 callback.cl,改造点 B)
现有 `FSBridgeController.callback` 已完成:state 取会话 -> `fsService.userInfo()` 换 token -> 写 `AUTH_CARD_RESULT + businessId` -> 返回自闭合页面。需要补两点:
1. **回写 per-user token 缓存**:callback 拿到 `FSUserInfoCO` 后,调用新增的 token 存储(见 2.4),把 `access_token + refresh_token + expires_in` 持久化。这是当前链路缺失的一环。
2. **会话校验**:比对会话中的 `mainUserId` 与授权返回的用户是否一致(防授权串号),不一致则结果标记失败。
### 2.4 per-user 令牌存储与刷新(改造点 C,核心新增)
现状 `FSService.setUserAccessToken` 只存 token 字符串,无法支撑刷新。设计:
- **缓存结构升级**:key 不变(`COMMON_FS_SELF_BUILT_APP_USER_ACCESS_TOKEN + corpNo:appId:openId`),value 从 `String` 升级为对象 `FSUserTokenCO{ accessToken, refreshToken, expiresIn, refreshTokenExpiresIn }`。读侧做兼容(旧值为 String 时视为仅 accessToken)。
- **新增 `FSUserAuthService`**(或扩展 `FSService`)提供 `getValidUserAccessToken(corpNo, appId, openId)`:
1. 缓存命中且剩余有效期 >= 360s -> 直接返回 accessToken;
2. 临期且有 refresh_token -> 调 `/open-apis/authen/v2/oauth/token`(grant_type=refresh_token)刷新后回写(refresh_token 一次性,必须整体覆盖缓存);
3. 刷新失败且错误码为 20037/20064/20073(refresh_token 失效)-> 清缓存,返回 null,由 DataApi 抛 `FS_AUTH_REQUIRED` 引导重新授权。
- 刷新逻辑从 `FSCommonUserAuthService.refreshUserAccessToken` 抽取共用方法,公共用户与 per-user 两条线复用同一段换 token 代码。
### 2.5 授权结果查询(复用)
复用 `GET /fs/auth/authResult?messageId=authId`。建议响应增加失败态(当前只有 null/有值两种),`FSUserInfoCO` 增加可选 `error` 字段,前端据此区分「还在等」与「授权失败」。
## 3. Redis key 一览
| Key | Value | TTL |
|-----|-------|-----|
| `COMMON_FS_SELF_BUILT_APP_AUTH_CARD + authId` | 授权会话 { corpNo, appId, mainUserId, businessId, scopes } | 600s |
| `COMMON_FS_SELF_BUILT_APP_AUTH_CARD_RESULT + authId` | `FSUserInfoCO`(含 error 可选) | 600s |
| `COMMON_FS_SELF_BUILT_APP_USER_ACCESS_TOKEN + corpNo:appId:openId` | `FSUserTokenCO` | expiresIn(用 Redis TTL 表达) |
## 4. 前端组件行为规格
> 本仓库不含前端代码,以下为前端实现须遵守的规格(接口协议 + 状态机)。
### 4.1 封装入口
提供一个高阶封装(如 axios 响应拦截器或 `callFsUserApi(fn)`):
1. 调用 DataApi;
2. 响应 `code === 'FS_AUTH_REQUIRED'` -> 进入授权流程(4.2),成功后**自动重放**原请求(最多重放 1 次,防循环);
3. 其他错误原样抛出。
### 4.2 授权流程状态机
```
idle -> preparing(调 /fs/auth/prepare) -> authorizing(打开授权窗口 + 轮询) -> success -> 重放原请求
\-> failed(超时120s / 用户关窗 / 结果含 error / prepare 失败) -> 提示重试
```
- **环境判断**:飞书客户端内(`window.h5sdk` 存在)用 applink `mode=sidebar-semi` 打开;浏览器用 `window.open(url, '_blank', 'width=600,height=700')`。
- **去重**:同一 `appId + scopes` 已有进行中的授权流程时,复用该流程的 Promise,不重复调 prepare、不重复开窗。
- **轮询**:每 2s 调 `/fs/auth/authResult?messageId=authId`,拿到 `FSUserInfoCO` 即成功;120s 超时判失败。
- **成功展示**:可用返回的 `name`/`avatar` 提示「已授权:张三」。
- **UI 自主**:授权引导层(文案、按钮、弹窗样式)完全由前端实现,后端只提供 prepare/authResult 两个数据接口,不发卡片、不推消息。
### 4.3 使用方契约
```ts
// 伪代码,前端仓库实现
const result = await callFsUserApi(() =>
post('/appcenter/fs/docs/search', { searchKey, count: 20, offset: 0 })
);
// 未授权时自动弹授权,授权后自动返回搜索结果
```
## 5. 边界与风险
| 风险 | 对策 |
|------|------|
| 授权码 5 分钟过期、一次性 | callback 立即换 token;20003/20004/20065 记日志并在结果中标记失败 |
| redirect_uri 不一致(20071) | 构造 URL 与 callback 换 token 使用同一 `gatewayHost + 固定路径`,不从前端传 |
| refresh_token 一次性 | 刷新成功后必须整体覆盖缓存;刷新与读取加用户级锁(Redis 分布式锁,key 含 openId)防并发刷新互相覆盖 |
| 用户更换飞书账号授权 | 会话绑定 mainUserId,callback 校验 openId 与 UC 绑定关系,不一致判失败 |
| 弹窗被浏览器拦截 | 授权引导层用「点击按钮打开」而非自动 window.open |
| scope 裁剪 | 以 token 接口返回的 `scope` 字段为准,授权结果中落库/缓存实际授予范围 |
## 6. 落地拆分建议(实现阶段子任务)
1. **后端-令牌层**:`FSUserTokenCO` 缓存结构 + 刷新共用抽取 + `getValidUserAccessToken`。
2. **后端-协议层**:`FS_AUTH_REQUIRED` + 新增 `POST /fs/auth/prepare`(authId 会话预存 + 授权 URL)+ callback 回写 per-user 缓存与校验 + authResult 失败态。
3. **后端-首个 DataApi 改造**:`FSDocsController.search` 接入新协议(含 corpNo fixme)。
4. **前端组件**:拦截器 + 授权引导 UI + 轮询重放(前端仓库实施)。
## 7. 验证方式
- 单测:令牌刷新分支(命中/临期刷新/refresh 失效);callback 串号校验。
- 联调:UAT 环境用真实飞书账号走通「未授权 -> 弹窗授权 -> 自动重放搜索 -> 返回文档列表」全链路;构造 refresh_token 过期场景验证二次授权引导。
+1 -1
View File
@@ -49,7 +49,7 @@
| 规范 | 作用 | 适用场景 | | 规范 | 作用 | 适用场景 |
|---|---|---| |---|---|---|
| [前端结构规范](./frontend-structure-guidelines.md) | 约束 `src` 下各层目录职责、页面私有结构和命名方式 | 新建页面、重构目录、抽离公共能力前必读 | | [前端结构规范](./frontend-structure-guidelines.md) | 约束 `src` 下各层目录职责、页面私有结构和命名方式 | 新建页面、重构目录、抽离公共能力前必读 |
| [UI 设计规范](../../../DESIGN.md) | 规范界面实现方式、视觉一致性和交互呈现 | 新做页面、优化样式、补充组件展示时阅读 | | [UI 设计规范](飞书用户态接口授权流程(Auth%20->%20Callback%20->%20DataApi).md) | 规范界面实现方式、视觉一致性和交互呈现 | 新做页面、优化样式、补充组件展示时阅读 |
| [设计变量使用规范](./design-tokens-guidelines.md) | 规范 `@oppein-react/design-tokens` 的接入、变量消费、主题切换与 UnoCSS 使用方式 | 新增样式、主题切换、替换硬编码颜色、接入 UnoCSS token 时必读 | | [设计变量使用规范](./design-tokens-guidelines.md) | 规范 `@oppein-react/design-tokens` 的接入、变量消费、主题切换与 UnoCSS 使用方式 | 新增样式、主题切换、替换硬编码颜色、接入 UnoCSS token 时必读 |
| [接口契约规范](./api-guidelines.md) | 规范前端接口文件、请求封装、错误处理和兼容性 | 新增接口、调整请求参数、封装请求工具时阅读 | | [接口契约规范](./api-guidelines.md) | 规范前端接口文件、请求封装、错误处理和兼容性 | 新增接口、调整请求参数、封装请求工具时阅读 |
| [类型定义规范](./dto-guidelines.md) | 规范请求参数、响应数据、页面消费模型和类型边界 | 新增类型、重构数据结构、拆分页面模型时阅读 | | [类型定义规范](./dto-guidelines.md) | 规范请求参数、响应数据、页面消费模型和类型边界 | 新增类型、重构数据结构、拆分页面模型时阅读 |