sync: d8d-dev 修复 slug 推导规则 + WebSocket 必检步骤 + 新增 d8d-skills-sync 技能
d8d-dev 变更: - phase-00-setup.md: 添加 project-slug 推导规则(目录名+group 后缀) - phase-00-setup.md: WebSocket 配置独立为所有 proxy hosts 必检步骤 - journey-mode.md, platform-nextjs.md, visual-verification.md: 同步更新 d8d-skills-sync(新增): - 自动同步所有 d8d-* 技能到模板仓库并创建 PR 的技能 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -394,14 +394,7 @@ Story 验证全部通过 ≠ 用户能正常使用系统。旅程模式弥补这
|
||||
|
||||
3. 证据阶段(⛔ 强制,不得跳过):
|
||||
- browser_take_screenshot → 保存最终截图到 tests/e2e/screenshots/journey-epic{N}-{M}-final.png
|
||||
- browser_network_requests(static=false) → 检查无 localhost API 调用
|
||||
- ⛔ **资源加载状态检查**(新增 — 每条旅程必须执行):
|
||||
- `browser_network_requests(static=true, filter="\\.(jpg|jpeg|png|gif|webp|svg|mp4|webm|mov)$")`
|
||||
- 过滤图片/视频资源请求,检查 HTTP 状态码
|
||||
- 任何 403/404 → 标记 `resource_status_check.found = FOUND` + 触发修复
|
||||
- 常见 403 原因:MinIO 私有 bucket 未用 presigned URL、防盗链、CORS
|
||||
- 常见 404 原因:资源路径错误、文件未上传
|
||||
- 记录到 pipeline-state.yaml 的 `verification_detail.resource_status_check`
|
||||
- browser_network_requests → 检查无 localhost API 调用
|
||||
|
||||
4. 修复阶段(⛔ 以下任一条件满足时必须执行,不得跳过):
|
||||
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
- 标记:Epic 0 完成后此文件才可生效(项目目录需先存在)
|
||||
|
||||
4.5. **外网域名发现与连通性验证(NPM MCP)**
|
||||
- project-slug 推导规则:取项目根目录名(basename of pwd),追加 `-group` 后缀
|
||||
- 示例:目录 `/mnt/code/231-251-template-6` → slug = `231-251-template-6-group`
|
||||
- ⛔ 禁止从项目描述或 PRD 内容猜测 slug
|
||||
- 通过 NPM MCP 工具查询已配置的代理主机:
|
||||
- 使用 `get_host_report` 快速确认代理数量
|
||||
- ⚠️ `list_proxy_hosts` 有已知 bug(locations 字段解析失败),用 `get_proxy_host` 逐个查询更可靠
|
||||
@@ -79,9 +82,10 @@
|
||||
```
|
||||
- 缺失的域名通过 `create_proxy_host` 创建(⛔ 参数要求见 `reference/platform-nextjs.md` §7 或 `reference/platform-uni-app.md`)
|
||||
- `block_exploits: false`(默认 true 阻断 WebSocket)
|
||||
- 创建后 `update_proxy_host` 启用 WebSocket Support
|
||||
- SSL 由 K8S ingress 处理,不需要在 NPM 中配置
|
||||
- 创建后验证:`get_proxy_host` 确认 `block_exploits: false` 且 `allow_websocket_upgrade: true`
|
||||
- ⛔ 对每个 proxy host(无论已有还是新建),必须验证并修复(此步骤不可跳过):
|
||||
- `get_proxy_host` 检查 `block_exploits: false` 且 `allow_websocket_upgrade: true`
|
||||
- 如不满足 → `update_proxy_host` 修正(MCP 工具不直接支持 allow_websocket_upgrade 时,用 NPM API:`PUT /api/nginx/proxy-hosts/{id}` 传 `{"allow_websocket_upgrade": true}`)
|
||||
- 将外网域名写入 `pipeline-context.md` 的"外网域名"部分
|
||||
- **E2E 测试优先使用外网域名**,localhost 作为最后 fallback
|
||||
|
||||
@@ -177,26 +181,6 @@
|
||||
- 此步骤创建的 .env.local 不应提交到 git(应在 .gitignore 中排除)
|
||||
- 更新 pipeline-context.md 记录注入状态
|
||||
|
||||
4.8. **MinIO 外网地址注入(⛔ 强制)**
|
||||
|
||||
检测容器环境变量 `MINIO_HOST`,将外网地址注入到 `.env` 中的 `S3_PUBLIC_URL`:
|
||||
|
||||
a. 检测 MINIO_HOST:
|
||||
```bash
|
||||
echo $MINIO_HOST
|
||||
```
|
||||
b. 探测 HTTPS 是否可用:
|
||||
```bash
|
||||
curl -sf --max-time 5 https://$MINIO_HOST/minio/health/live && echo "HTTPS" || echo "NO-HTTPS"
|
||||
```
|
||||
c. 注入到 `.env`(⚠️ 注意 K8S ingress 会将 HTTP 升级为 HTTPS,必须使用 HTTPS):
|
||||
```
|
||||
S3_PUBLIC_URL="https://{MINIO_HOST}"
|
||||
```
|
||||
d. 代码中对外暴露的 URL(presigned URL、封面图 URL)必须使用 `S3_PUBLIC_URL`
|
||||
e. 内部连接(S3Client endpoint)保持 `S3_ENDPOINT`(localhost:9000)
|
||||
f. 详见 `reference/platform-nextjs.md` §8
|
||||
|
||||
5. **初始化管道状态**
|
||||
- 创建 `{implementation_artifacts}/pipeline-state.yaml`:
|
||||
```yaml
|
||||
|
||||
@@ -230,89 +230,3 @@ NPM 代理主机默认配置会阻断 WebSocket,导致:
|
||||
### 验证
|
||||
|
||||
`get_proxy_host` 确认:`block_exploits: false` + `allow_websocket_upgrade: true`
|
||||
|
||||
---
|
||||
|
||||
## 8. MinIO 外网地址配置(⛔ 强制)
|
||||
|
||||
### 问题描述
|
||||
|
||||
`S3_ENDPOINT` 默认为 `http://localhost:9000`(容器内部连接)。但对外暴露的 URL(presigned URL、封面图 URL)
|
||||
如果使用 `localhost:9000`,外网用户无法访问。
|
||||
|
||||
容器环境提供 `MINIO_HOST` 环境变量(外网 MinIO 域名),需要注入为 `S3_PUBLIC_URL`。
|
||||
|
||||
### 关键区分
|
||||
|
||||
| 变量 | 用途 | 值 |
|
||||
|------|------|------|
|
||||
| `S3_ENDPOINT` | S3Client 内部连接 | `http://localhost:9000` |
|
||||
| `S3_PUBLIC_URL` | 对外暴露的 URL(presigned、封面) | `https://{MINIO_HOST}` |
|
||||
|
||||
### Dual S3Client 模式(⛔ 强制)
|
||||
|
||||
AWS SDK `getSignedUrl()` 生成的 presigned URL 签名包含 `X-Amz-SignedHeaders=host`,
|
||||
即签名绑定了 S3Client 的 endpoint host。**不能用字符串替换域名**(签名会失效返回 403)。
|
||||
|
||||
必须创建两个 S3Client 实例:
|
||||
|
||||
```typescript
|
||||
// 内部 S3Client(worker 下载、上传等)
|
||||
const s3 = new S3Client({ endpoint: "http://localhost:9000", ... });
|
||||
|
||||
// 外网 S3Client(生成外网 presigned URL)
|
||||
const s3Public = new S3Client({ endpoint: "https://minio-xxx.dev.d8d.fun", ... });
|
||||
|
||||
// 内部 presigned URL(worker 用)
|
||||
getDownloadUrl(key) → getSignedUrl(s3, ...) // host=localhost:9000
|
||||
|
||||
// 外网 presigned URL(浏览器/stream 路由用)
|
||||
getPublicDownloadUrl(key) → getSignedUrl(s3Public, ...) // host=minio-xxx.dev.d8d.fun
|
||||
```
|
||||
|
||||
### 代码规则
|
||||
|
||||
- **`getDownloadUrl()`**:用内部 `s3`,返回 localhost presigned URL(worker 下载用)
|
||||
- **`getPublicDownloadUrl()`**:用 `s3Public`,返回外网 presigned URL(stream 路由用)
|
||||
- **`uploadFile()` 返回值**:使用 `S3_PUBLIC_URL`(公开访问 URL)
|
||||
- **worker 中的封面 URL**:使用 `S3_PUBLIC_URL`
|
||||
- **禁止字符串替换域名**:`url.replace(localhost, publicUrl)` 会导致 403
|
||||
|
||||
### ⚠️ HTTPS 注意
|
||||
|
||||
K8S ingress 会将 HTTP 升级为 HTTPS。`S3_PUBLIC_URL` 必须使用 `https://`,
|
||||
否则 presigned URL 签名会因重定向而失效。
|
||||
|
||||
---
|
||||
|
||||
## 9. 资源 URL 可访问性检查(⛔ 强制)
|
||||
|
||||
### 问题描述
|
||||
|
||||
MinIO bucket 默认私有,所有通过 URL 直接访问的资源(图片、视频)都会返回 403。
|
||||
前端 `<img src>` 或 `<video src>` 使用直接 URL 会加载失败,但不会报 JS 错误(静默失败)。
|
||||
|
||||
### 受影响的资源类型
|
||||
|
||||
| 资源 | 存储位置 | 访问方式 |
|
||||
|------|---------|---------|
|
||||
| 视频文件 | MinIO `videos/` | stream API 路由 → presigned URL 重定向 |
|
||||
| 封面图 | MinIO `covers/` | API 返回时注入 presigned URL |
|
||||
| 其他上传文件 | MinIO | 同上 |
|
||||
|
||||
### 代码规则
|
||||
|
||||
- **禁止前端直接使用 MinIO URL**:`<img src="https://minio-xxx/bucket/key">` 会 403
|
||||
- **API 返回资源 URL 时**:必须用 `getPublicDownloadUrl(key)` 生成 presigned URL
|
||||
- **视频播放**:通过 stream API 路由(`/api/v1/videos/[id]/stream`)重定向到 presigned URL
|
||||
- **封面图显示**:在视频详情 API 中为 `coverImages[].imageUrl` 注入 presigned URL
|
||||
- **presigned URL 有效期**:默认 3600 秒(1 小时),前端需在过期前重新获取
|
||||
|
||||
### 检测方法
|
||||
|
||||
旅程验证时使用 `browser_network_requests(static=true)` 检查图片/视频资源状态码:
|
||||
```bash
|
||||
# 过滤图片/视频请求
|
||||
browser_network_requests(static=true, filter="\\.(jpg|jpeg|png|gif|webp|svg|mp4|webm)$")
|
||||
# 状态码 403/404 → 资源不可访问 → AUTO-FIX
|
||||
```
|
||||
|
||||
@@ -83,14 +83,6 @@
|
||||
- 无 broken image(加载失败的图片占位符)
|
||||
- 无 emoji 充当功能图标
|
||||
|
||||
2.5. **资源加载状态检查**(⛔ 强制 — 每次视觉验证关卡必须执行):
|
||||
- `browser_network_requests(static=true, filter="\\.(jpg|jpeg|png|gif|webp|svg|mp4|webm)$")`
|
||||
- 检查所有图片/视频资源的 HTTP 状态码
|
||||
- 状态码 403 → 私有存储未签名(如 MinIO bucket 需 presigned URL)
|
||||
- 状态码 404 → 资源不存在(路径错误或文件未上传)
|
||||
- 状态码 5xx → 服务端错误
|
||||
→ 发现非 200 状态码 → 标记 `resource_status_check.found = FOUND` → 按分流规则处理
|
||||
|
||||
3. **英文枚举检测**:
|
||||
- 页面可见文本中无英文大写枚举值(如 APPROVED/REJECTED/PENDING/PAID 等)
|
||||
|
||||
@@ -217,14 +209,6 @@ verification_detail:
|
||||
element_deformation: NONE/FOUND # e. 元素变形(按钮/输入框被挤压)
|
||||
alignment_issues: NONE/FOUND # f. 对齐问题
|
||||
details: "具体描述"
|
||||
resource_status_check: # ⛔ 新增 — 资源加载状态
|
||||
executed: true/false
|
||||
found: NONE/FOUND
|
||||
failed_resources: # FOUND 时列出失败资源
|
||||
- url: "https://..."
|
||||
status: 403/404
|
||||
type: "image/jpeg"
|
||||
details: "描述"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -246,7 +230,6 @@ verification_detail:
|
||||
| SVG data URI 充当图标/装饰 | Bug(平台不兼容 #17) | AUTO-IMPLEMENT: **⛔ 必须按 ./reference/platform-uni-app.md §1 完整安装 Iconify + tailwindcss-icons + weapp-tailwindcss/vite** | iron-laws L223 |
|
||||
| 英文枚举值显示 | Bug(#15) | AUTO-FIX: 添加中文映射函数 | iron-laws L239 |
|
||||
| Broken image | Bug | AUTO-FIX: 修复图片路径/占位符 | — |
|
||||
| 图片/视频资源 403/404 | Bug | AUTO-FIX: 使用 presigned URL(403)或修复路径(404) | — |
|
||||
| 布局溢出 | Bug | AUTO-FIX: CSS 修复 | iron-laws L222 |
|
||||
| 元素变形/挤压(按钮/输入框/卡片宽高比异常、被挤压、文字截断显示省略号) | Bug | AUTO-FIX: 修复 CSS flex/width/padding/gap | — |
|
||||
| 间距异常(导航栏与内容之间过大空白、相邻交互元素紧贴无间距、异常大空白区域) | Bug | AUTO-FIX: 修复 margin/padding/gap | — |
|
||||
|
||||
100
.claude/skills/d8d-skills-sync/SKILL.md
Normal file
100
.claude/skills/d8d-skills-sync/SKILL.md
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: d8d-skills-sync
|
||||
description: '将项目中 .claude/skills/ 下所有 d8d-* 技能目录(d8d-dev、d8d-requirements、d8d-prototype、d8d-skills-sync 等)的修改同步到 bmad-method-template 模板仓库并创建 PR。使用场景:修改了 .claude/skills/ 下任何 d8d-* 目录的文件后,需要将改动同步到上游模板仓库时触发。触发词:同步技能、提PR、sync skill、更新模板。'
|
||||
---
|
||||
|
||||
# D8D Skills Sync — 技能变更同步到模板仓库
|
||||
|
||||
将项目 `.claude/skills/` 下所有 `d8d-*` 目录的修改同步到 `https://gitea.d8d.fun/d8dfun/bmad-method-template` 并创建 PR。
|
||||
|
||||
同步范围:所有匹配 `d8d-*/` 的技能目录(包括 d8d-dev、d8d-requirements、d8d-prototype、d8d-skills-sync 自身等)
|
||||
|
||||
## 配置
|
||||
|
||||
```
|
||||
TEMPLATE_REPO_URL: https://gitea.d8d.fun/d8dfun/bmad-method-template.git
|
||||
TEMPLATE_REPO_API: https://gitea.d8d.fun/api/v1/repos/d8dfun/bmad-method-template
|
||||
LOCAL_CLONE_PATH: /tmp/bmad-method-template
|
||||
SYNC_PATTERNS: .claude/skills/d8d-*/
|
||||
```
|
||||
|
||||
Gitea 认证信息从项目 git remote URL 提取(oauth2 token 在 remote URL 中)。
|
||||
|
||||
## 工作流程
|
||||
|
||||
### Step 1: 准备模板仓库
|
||||
|
||||
```bash
|
||||
if [ -d /tmp/bmad-method-template ]; then
|
||||
cd /tmp/bmad-method-template && git pull
|
||||
else
|
||||
git clone https://gitea.d8d.fun/d8dfun/bmad-method-template.git /tmp/bmad-method-template
|
||||
fi
|
||||
```
|
||||
|
||||
### Step 2: 检测变更
|
||||
|
||||
对每个 `d8d-*` 技能目录逐一对比(自动发现,不硬编码):
|
||||
|
||||
```bash
|
||||
# 动态扫描项目中所有 d8d-* 技能目录
|
||||
for skill_dir in {PROJECT_ROOT}/.claude/skills/d8d-*/; do
|
||||
skill=$(basename $skill_dir)
|
||||
diff -rq $skill_dir /tmp/bmad-method-template/.claude/skills/$skill/ 2>/dev/null || true
|
||||
done
|
||||
```
|
||||
|
||||
- 如果无差异 → 告知用户"所有 d8d-* 技能文件已是最新,无需同步"
|
||||
- 如果有差异 → 列出变更文件清单,继续 Step 3
|
||||
|
||||
### Step 3: 创建分支并同步
|
||||
|
||||
```bash
|
||||
cd /tmp/bmad-method-template
|
||||
BRANCH_NAME="sync/d8d-skills-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout master && git pull
|
||||
git checkout -b $BRANCH_NAME
|
||||
|
||||
# 同步所有 d8d-* 技能目录(自动发现)
|
||||
for skill_dir in {PROJECT_ROOT}/.claude/skills/d8d-*/; do
|
||||
skill=$(basename $skill_dir)
|
||||
mkdir -p .claude/skills/$skill/
|
||||
cp -r $skill_dir* .claude/skills/$skill/
|
||||
done
|
||||
```
|
||||
|
||||
### Step 4: 提交
|
||||
|
||||
```bash
|
||||
git add .claude/skills/d8d-*/
|
||||
git commit -m "sync: {简要描述变更内容}"
|
||||
```
|
||||
|
||||
提交消息格式:`sync: {变更摘要}`,摘要从实际 diff 内容提炼。
|
||||
|
||||
### Step 5: 推送并创建 PR
|
||||
|
||||
```bash
|
||||
git push -u origin $BRANCH_NAME
|
||||
```
|
||||
|
||||
通过 Gitea API 创建 PR:
|
||||
|
||||
```bash
|
||||
TOKEN=$(git -C {PROJECT_ROOT} remote get-url origin | sed 's|https://oauth2:\([^@]*\)@.*|\1|')
|
||||
|
||||
curl -X POST "${TEMPLATE_REPO_API}/pulls" \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"head\": \"$BRANCH_NAME\",
|
||||
\"base\": \"master\",
|
||||
\"title\": \"sync: {变更摘要}\",
|
||||
\"body\": \"## 变更内容\\n\\n{变更文件列表和说明}\\n\\n## 测试\\n\\n- [ ] 在新项目中运行 SETUP 阶段验证\"
|
||||
}"
|
||||
```
|
||||
|
||||
### Step 6: 验证
|
||||
|
||||
- 确认 API 返回 `html_url`(PR 链接)
|
||||
- 输出 PR URL 给用户
|
||||
Reference in New Issue
Block a user