sync: d8d-dev slug 推导规则 + WebSocket 必检 + 新增 d8d-skills-sync #2

Closed
d8dfun wants to merge 1 commits from sync/d8d-skills-20260408-020738 into master
5 changed files with 107 additions and 133 deletions

View File

@@ -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. 修复阶段(⛔ 以下任一条件满足时必须执行,不得跳过):

View File

@@ -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` 有已知 buglocations 字段解析失败),用 `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. 代码中对外暴露的 URLpresigned 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

View File

@@ -230,89 +230,3 @@ NPM 代理主机默认配置会阻断 WebSocket导致
### 验证
`get_proxy_host` 确认:`block_exploits: false` + `allow_websocket_upgrade: true`
---
## 8. MinIO 外网地址配置(⛔ 强制)
### 问题描述
`S3_ENDPOINT` 默认为 `http://localhost:9000`(容器内部连接)。但对外暴露的 URLpresigned URL、封面图 URL
如果使用 `localhost:9000`,外网用户无法访问。
容器环境提供 `MINIO_HOST` 环境变量(外网 MinIO 域名),需要注入为 `S3_PUBLIC_URL`。
### 关键区分
| 变量 | 用途 | 值 |
|------|------|------|
| `S3_ENDPOINT` | S3Client 内部连接 | `http://localhost:9000` |
| `S3_PUBLIC_URL` | 对外暴露的 URLpresigned、封面 | `https://{MINIO_HOST}` |
### Dual S3Client 模式(⛔ 强制)
AWS SDK `getSignedUrl()` 生成的 presigned URL 签名包含 `X-Amz-SignedHeaders=host`
即签名绑定了 S3Client 的 endpoint host。**不能用字符串替换域名**(签名会失效返回 403
必须创建两个 S3Client 实例:
```typescript
// 内部 S3Clientworker 下载、上传等)
const s3 = new S3Client({ endpoint: "http://localhost:9000", ... });
// 外网 S3Client生成外网 presigned URL
const s3Public = new S3Client({ endpoint: "https://minio-xxx.dev.d8d.fun", ... });
// 内部 presigned URLworker 用)
getDownloadUrl(key) → getSignedUrl(s3, ...) // host=localhost:9000
// 外网 presigned URL浏览器/stream 路由用)
getPublicDownloadUrl(key) → getSignedUrl(s3Public, ...) // host=minio-xxx.dev.d8d.fun
```
### 代码规则
- **`getDownloadUrl()`**:用内部 `s3`,返回 localhost presigned URLworker 下载用)
- **`getPublicDownloadUrl()`**:用 `s3Public`,返回外网 presigned URLstream 路由用)
- **`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
```

View File

@@ -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 URL403或修复路径404 | — |
| 布局溢出 | Bug | AUTO-FIX: CSS 修复 | iron-laws L222 |
| 元素变形/挤压(按钮/输入框/卡片宽高比异常、被挤压、文字截断显示省略号) | Bug | AUTO-FIX: 修复 CSS flex/width/padding/gap | — |
| 间距异常(导航栏与内容之间过大空白、相邻交互元素紧贴无间距、异常大空白区域) | Bug | AUTO-FIX: 修复 margin/padding/gap | — |

View 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 给用户