baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

42
.claude/rules/api.md Normal file
View File

@@ -0,0 +1,42 @@
---
paths:
- "src/api/**"
- "src/routers/**"
- "src/services/**"
---
# API 层开发规则Clover 后端)
> 承接架构方案v0.3 基石 + API契约.md冲突以架构方案为准。
## 路由规范
- RESTful 命名:资源用名词复数(/tasks, /products, /api-keys
- 版本前缀:/api/v1/
- 控制器只做:参数校验 → 调 service → 格式化响应,**不含业务逻辑**
## 响应格式(统一包络)
- 成功:`{ "code": 0, "data": {...} }`
- 失败:`{ "code": <错误码>, "message": "用户可读信息" }`
- HTTP 状态码与业务 code 分离见契约§0七类错误码
## 安全基石B/C
- 所有用户输入必须校验(类型、长度、范围)
- SQL 查询使用参数化,禁止字符串拼接
- **API Key 绝不进 Celery 参数**,只传 task_idworker 内查库→Fernet解密→局部变量
- FERNET_KEY 走环境变量,绝不进代码库;解密不落盘、不打日志
- 敏感操作记审计日志ai_call_logs
## 多租户隔离(红线)
- 所有查询强制带 workspace_id 条件
- 写操作 + 切 workspace 必须查 workspace_members 校验权限
- workspace_id / product_id 从 JWT + 上下文自动带,前端不传(防越权)
- 所有 workspace 相关 Repo 继承 base_workspace_repo
## 错误处理官网V1坑3
- service 错误不许静默吞,至少打 error log
- AI/token站调用失败归因到个人 key错误码50002
- DB 枚举约束要有对应代码侧常量文件
## 业务参数不写死基石A
- 禁止出现品类/数量/角度任何枚举常量
- "看起来像枚举"的业务概念 → 做成数据不做成代码

49
.claude/rules/db.md Normal file
View File

@@ -0,0 +1,49 @@
---
paths:
- "src/models/**"
- "alembic/**"
- "migrations/**"
---
# 数据库规则Clover · MySQL + Alembic
> 承接架构方案v0.3 第三章16表。一期建14张matrix_accounts/preference_profile 二期预留。
## 迁移
- 每次 schema 变更创建迁移文件,不手动改数据库
- 迁移文件不可修改已提交的,只能新增
- 迁移必须包含 up 和 down
- 按序001(banana搬3张) → 002(多租户基础) → 003(业务7张) → 004(飞轮)
## 查询
- 禁止 SELECT *,明确列出字段
- 大表查询必须有索引支撑,新增查询模式时同步加索引
- N+1 问题:关联查询用 JOIN 或批量加载
- 飞轮聚合走 MySQL JOINMongoDB 只存 AI debug trace业务不依赖
## 命名
- 表名snake_case 复数users, generation_tasks, preference_events
- 字段名snake_casecreated_at, workspace_id
- 索引名idx_表名_字段名
## 关键约束(架构方案)
- 任务主键:自增 BIGINT + mongo_trace_id VARCHAR(24)
- user_api_keysUNIQUE(user_id, workspace_id, provider)
- 所有业务表必须有 workspace_id多租户逻辑隔离
- preference_events 必须有 workspace_id + product_id跨公司隔离+按产品分开学)
## 状态机generation_tasks.status
pending → generating → pending_selection → pending_review → approved/rejected → archived
- 打回后回到 pending_selection
- 非法流转返错误码 40901
## 字段红线
- API Key 字段encrypted_keyFernet**不存 url 字段**token站固定自家站
- users 表:删除 banana 的 credits 字段
- eval_score留 NULL不接 banana 假评分)
- data_ownershipclient_data / platform_asset洞7分层归属
- banned_words.levelauto_fix / soft_warn / hard_block三级
## 枚举约束
- DB 枚举约束要有对应代码侧常量文件(消除三套命名打架)
- 品类products.category是纯数据字段禁止任何品类枚举常量

46
.claude/rules/frontend.md Normal file
View File

@@ -0,0 +1,46 @@
---
paths:
- "src/components/**"
- "src/app/**"
- "src/pages/**"
- "src/stores/**"
---
# 前端开发规则Clover · Next.js
> 承接 前端交互设计.md5屏金标准+ API契约.md §3 DTO。
## 组件规范
- 单一职责:一个组件只做一件事
- Props 必须定义类型TypeScript interface
- 避免 prop drilling 超 3 层,用 Context 或状态管理
## DTO 对接堵官网V1坑4
- 所有 DTO 字段严格按 API契约§3**不许自己假设嵌套结构**
- 后端响应与契约不符立即报 Lead不私下适配
- workspace_id / product_id 不在前端传
## SSE异步生图电影
- 按 event_seq 去重,断线重连补发历史事件
- 11类事件按契约§2渲染0/N进度 + 谁好谁先冒 + 单批失败重试 + 能离开
- flywheel_injected 事件 → 显"本次已注入:最近偏好/打回原因"
## 样式
- 用 CSS Modules 或 Tailwind不用全局样式
- 响应断点统一sm(640) md(768) lg(1024) xl(1280)
- 颜色/间距用 design token不硬编码
## 可访问性
- 交互元素必须有 aria-label
- 图片必须有 alt 文本
- 键盘导航可用
## 红线CLAUDE.md
- ❌ 不展示 Token 余额、不做积分页key 去中转站后台看)
- ❌ 飞轮隐形:无"训练AI"按钮,只在生成时显注入提示
- ❌ 不做"自动发布到小红书"——只到确认预览
- ❌ 不做移动端(一期)
## 状态管理
- 4 个 Store任务 / 候选 / 审核 / 全局
- 异步态用骨架屏错误态对接契约§0错误码

53
.claude/settings.json Normal file
View File

@@ -0,0 +1,53 @@
{
"//说明": "Clover 仓库根 Hooks 配置。四道质量门禁承接治理指南§9.3模板。退出码 0=放行 2=阻止 其他非零=脚本错误。母本在 启动包/.claude/。",
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
},
"hooks": {
"PreToolUse": [
{
"//门禁1": "危险命令拦截",
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "cmd=$(jq -r '.tool_input.command // empty'); if echo \"$cmd\" | grep -qE 'rm -rf|DROP TABLE|--force|--no-verify|reset --hard|git push.*main|git push.*master'; then echo \"危险命令被拦截: $cmd\" >&2; exit 2; fi; exit 0"
}
]
},
{
"//门禁2": "敏感文件保护 + 源目录只读保护",
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path // empty'); if echo \"$file\" | grep -qE '\\.env|\\.key|credentials|secrets|FERNET'; then echo \"禁止编辑敏感文件: $file\" >&2; exit 2; fi; if echo \"$file\" | grep -qE 'O2中间件平台/banana|产品包/worker|万牛会L1准备/worker'; then echo \"源目录只读,禁止修改(铁律): $file\" >&2; exit 2; fi; exit 0"
}
]
}
],
"PostToolUse": [
{
"//门禁3": "自动 lint",
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "file=$(jq -r '.tool_input.file_path // empty'); case \"$file\" in *.ts|*.tsx|*.js|*.jsx) npx eslint --fix \"$file\" 2>&1 || true;; *.py) ruff check --fix \"$file\" 2>&1 || true;; esac; exit 0"
}
]
}
],
"Stop": [
{
"//门禁4": "测试验证 + 临时文件提醒",
"hooks": [
{
"type": "command",
"command": "bash -c 'echo \"请确认是否已运行测试并通过(软件测试过≠内容质量过,内容需北哥抽检)。\" >&2; tmp=$(find . -name \"*_tmp*\" 2>/dev/null | head -5); if [ -n \"$tmp\" ]; then echo \"发现临时文件待清理:\" >&2; echo \"$tmp\" >&2; fi; exit 0'"
}
]
}
]
}
}

View File

@@ -0,0 +1,67 @@
---
name: clover-loop
version: 1.0.0
description: |
Clover 内容生产闭环开发 SKILL改造自 ralph-loop
Memory is Poison, Tests are Cure —— 但 Clover 的 Test 不止单元测试,还有北哥的眼睛。
六阶段闭环:调研→约束→生成→验证→校准→闭环。质量过关=机器过滤+北哥抽检两阶段。
triggers:
- clover-loop
- 内容闭环
- 生产闭环
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- AskUserQuestion
---
# Clover Loop SKILL
> 核心:机器层(machine_passed)只是进抽检队列,北哥点头(human_passed)才是真过关。
## 六阶段
### P0 调研ralph-loop 没有,北哥"先调研"
读架构方案v0.3 + 对应 PRD + 扒包落盘,确认本任务边界。
不充分就进 P2 = 在错误的层做对的事PSR-L 教训)。
### P1 约束
冻结 task_spec输入/输出/验证命令/期望exit_code/质量阈值。
`python scripts/loop_state.py create <task_id>`
### P2 生成
写代码 + 写验证脚本必须含断言、返回exit_code
`python scripts/loop_state.py advance`
### P3 验证(机器层)
跑机器门禁lint / test / 违禁词 / 五维≥90 / 去重。
全过 → status=machine_passed → 进 P4不过 → 记失败,回 P2。
### P4 校准ralph-loop 没有,专治"分高但烂"
机器过的内容样本,按维度比对北哥标准;同质化/跑偏单独标记。
产出**抽检样本包**交北哥 → 北哥判断回写质量标准(喂飞轮)。
### P5 闭环
- 北哥抽检通过 → status=human_passed → 归档 + 结晶
- 北哥打回 / 验证不过 → 分析根因
- 同层撞墙 **2 次** → 强制 Pivot换层不换参记 pivots[]
- Pivot 后仍不过 / 北哥连续否决同方向 → status=blocked停循环请人介入
## 状态机(.clover-loop-state.json
5 态in_progress / machine_passed / human_passed / pivoted / blocked
关键machine_passed ≠ human_passed
## 命令
| 命令 | 功能 |
|------|------|
| /clover-loop | 启动六阶段闭环 |
| /clover-loop:status | 查当前状态 |
| /clover-loop:pivot | 手动触发换层 |
## 铁律
- 撞墙 2 次必 Pivot对齐 CLAUDE.md 失败2次
- 北哥不耐烦/连续否决 = 路径选错层的信号,立即 Pivot 不调参
- 软件测试过 ≠ 内容质量过

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Clover Loop State Manager改造自 ralph-loop state_manager
六阶段 + 5态 + 机器/人工双层验收 + Pivot 追踪。
"""
import json
import sys
from datetime import datetime
from pathlib import Path
STATE_FILE = ".clover-loop-state.json"
PHASES = {"0": "调研", "1": "约束", "2": "生成", "3": "验证", "4": "校准", "5": "闭环"}
def _path(work_dir=None):
base = Path(work_dir) if work_dir else Path.cwd()
return base / STATE_FILE
def load(work_dir=None):
p = _path(work_dir)
return json.load(open(p, encoding="utf-8")) if p.exists() else None
def save(state, work_dir=None):
state["updated_at"] = datetime.now().isoformat()
json.dump(state, open(_path(work_dir), "w", encoding="utf-8"),
indent=2, ensure_ascii=False)
def create(task_id, work_dir=None):
state = {
"task_id": task_id,
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"current_phase": 0,
"max_retries_per_layer": 2, # 单层撞墙2次→Pivot
"machine_gate": {"lint": None, "test": None, "banned_word": None,
"score": None, "dedup": None},
"human_review": {"status": "pending", "sampled": 0,
"approved": 0, "rejected": 0},
"status": "in_progress", # in_progress/machine_passed/human_passed/pivoted/blocked
"layer_attempts": {}, # {层名: 失败次数}
"pivots": [],
"logs": [],
}
save(state, work_dir)
return state
def advance(state, work_dir=None):
cur = state["current_phase"]
if cur < 5:
state["current_phase"] = cur + 1
save(state, work_dir)
return state
def machine_gate(state, key, result, work_dir=None):
"""记录机器门禁单项结果。全过则置 machine_passed。"""
state["machine_gate"][key] = result
g = state["machine_gate"]
passed = all(v in ("pass", True) or (isinstance(v, (int, float)) and v >= 90)
for v in g.values() if v is not None)
if passed and all(v is not None for v in g.values()):
state["status"] = "machine_passed"
save(state, work_dir)
return state
def record_fail(state, layer, error=None, work_dir=None):
"""记录某层失败。同层2次→强制Pivot。"""
state["layer_attempts"][layer] = state["layer_attempts"].get(layer, 0) + 1
state["logs"].append({"ts": datetime.now().isoformat(),
"layer": layer, "error": error})
if state["layer_attempts"][layer] >= state["max_retries_per_layer"]:
state["status"] = "pivoted"
state["pivots"].append({"layer": layer, "ts": datetime.now().isoformat(),
"reason": f"{layer}层撞墙{state['layer_attempts'][layer]}"})
save(state, work_dir)
return state
def human_review(state, action, work_dir=None):
"""北哥抽检approve/reject。approve累计达标→human_passed。"""
hr = state["human_review"]
hr["sampled"] += 1
if action == "approve":
hr["approved"] += 1
hr["status"] = "approved"
state["status"] = "human_passed"
elif action == "reject":
hr["rejected"] += 1
hr["status"] = "rejected"
state["status"] = "blocked" # 打回=停循环请人介入
save(state, work_dir)
return state
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: loop_state.py <create|load|advance|gate|fail|review> [args]")
print(" create <task_id> / advance / gate <key> <result>")
print(" fail <layer> [error] / review <approve|reject>")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "create":
print(json.dumps(create(sys.argv[2]), ensure_ascii=False, indent=2))
elif cmd == "load":
s = load()
print(json.dumps(s, ensure_ascii=False, indent=2) if s else "无状态")
elif cmd == "advance":
s = load()
if s:
advance(s)
print(f"进入 P{s['current_phase']} {PHASES[str(s['current_phase'])]}")
elif cmd == "gate":
s = load()
if s:
key, raw = sys.argv[2], sys.argv[3]
val = float(raw) if raw.replace(".", "").isdigit() else raw
machine_gate(s, key, val)
print(f"门禁 {key}={val}, status={s['status']}")
elif cmd == "fail":
s = load()
if s:
record_fail(s, sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None)
print(f"{sys.argv[2]}层失败{s['layer_attempts'][sys.argv[2]]}次, status={s['status']}")
elif cmd == "review":
s = load()
if s:
human_review(s, sys.argv[2])
print(f"抽检{sys.argv[2]}, status={s['status']}")