From 87b463b2bb04165f7d0e4ddb85ba3fcdad6eed27 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Thu, 13 Aug 2026 20:43:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(ECR-044):=20RhythmConfig=20=E5=86=99?= =?UTF-8?q?=E9=9D=A2=E9=97=AD=E7=8E=AF=E5=B9=B6=20Closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用 admin.explore.write、POST/PUT+审计、C端 GET /rhythm/configs、 H5 无 active 空态/失败回退;migration 000053。ExploreConfig Loop STOP,禁自动 ECR-045。 Co-authored-by: Cursor --- .ai/product/feature-spec/README.md | 3 +- .../feature-spec/ops-rhythm-config-write.md | 98 +++++++++++ .ai/product/feature-spec/ops-rhythm-config.md | 1 + apps/admin-h5/src/api/client.ts | 41 ++++- apps/admin-h5/src/layouts/AdminShell.vue | 1 + apps/admin-h5/src/pages/RhythmConfigPage.vue | 165 ++++++++++++++++++ apps/admin-h5/src/router/index.ts | 6 + .../internal/handler/admin_rhythm_config.go | 65 +++++++ .../internal/handler/rhythm_config_public.go | 36 ++++ apps/api/internal/httpserver/router.go | 1 + .../integration/rhythm_config_write_test.go | 116 ++++++++++++ .../repository/rhythm_config_write.go | 139 +++++++++++++++ .../service/admin/rhythm_config_write.go | 71 ++++++++ .../000053_ops_rhythm_config_write.down.sql | 2 + .../000053_ops_rhythm_config_write.up.sql | 7 + .../src/composables/useLifeRhythmPage.ts | 27 +++ apps/user-h5/src/pages/LifeRhythmPage.vue | 41 ++++- .../BD-2026-044-rhythm-config-write.md | 29 +++ docs/CHANGELOG.md | 1 + docs/CODE_REVIEW/ECR-044.md | 3 + docs/CONTRACT_DIFF/ECR-044.yaml | 20 +++ docs/ECR/ECR-044-rhythm-config-write.md | 26 +++ docs/HANDOFF/ECR-044-architect-to-engineer.md | 6 + docs/HANDOFF/ECR-044-engineer-to-reviewer.md | 4 + .../ECR-044-rhythm-config-write.md | 3 + docs/PROJECT_PROFILE.md | 10 +- docs/STATE/ECR-044.md | 7 + docs/TEST_REPORT/ECR-044.md | 11 ++ docs/TRACEABILITY.md | 6 +- docs/WAVE0/ECR-044-CANDIDATE_REVIEW.md | 10 +- docs/WAVE0/ECR-044_WRITE_AUTHORIZATION.md | 32 ++-- packages/sdk/src/index.ts | 9 + proto/openapi.yaml | 44 +++++ 33 files changed, 1006 insertions(+), 35 deletions(-) create mode 100644 .ai/product/feature-spec/ops-rhythm-config-write.md create mode 100644 apps/admin-h5/src/pages/RhythmConfigPage.vue create mode 100644 apps/api/internal/handler/rhythm_config_public.go create mode 100644 apps/api/internal/integration/rhythm_config_write_test.go create mode 100644 apps/api/internal/repository/rhythm_config_write.go create mode 100644 apps/api/internal/service/admin/rhythm_config_write.go create mode 100644 apps/api/migrations/000053_ops_rhythm_config_write.down.sql create mode 100644 apps/api/migrations/000053_ops_rhythm_config_write.up.sql create mode 100644 docs/BACKEND_DESIGN/BD-2026-044-rhythm-config-write.md create mode 100644 docs/CODE_REVIEW/ECR-044.md create mode 100644 docs/CONTRACT_DIFF/ECR-044.yaml create mode 100644 docs/ECR/ECR-044-rhythm-config-write.md create mode 100644 docs/HANDOFF/ECR-044-architect-to-engineer.md create mode 100644 docs/HANDOFF/ECR-044-engineer-to-reviewer.md create mode 100644 docs/PRODUCT_SPEC/ECR-044-rhythm-config-write.md create mode 100644 docs/STATE/ECR-044.md create mode 100644 docs/TEST_REPORT/ECR-044.md diff --git a/.ai/product/feature-spec/README.md b/.ai/product/feature-spec/README.md index 5286a4e..afe8b9b 100644 --- a/.ai/product/feature-spec/README.md +++ b/.ai/product/feature-spec/README.md @@ -51,7 +51,8 @@ | [ops-privacy-request.md](ops-privacy-request.md) | AdminGovernance PrivacyRequest | §7 | `/admin/privacy/requests*` | Ops · **ECR-034** | | [ops-star-config.md](ops-star-config.md) | ExploreConfig StarConfig(只读) | §7 | `/admin/explore/star-configs*` | Ops · **ECR-035** | | [ops-star-config-write.md](ops-star-config-write.md) | StarConfig **写面** | §7 | admin 写 · `GET /star/configs` | ExploreConfig · **ECR-043** | -| [ops-rhythm-config.md](ops-rhythm-config.md) | ExploreConfig RhythmConfig | §7 | `/admin/explore/rhythm-configs*` | Ops · **ECR-036** | +| [ops-rhythm-config.md](ops-rhythm-config.md) | ExploreConfig RhythmConfig(只读) | §7 | `/admin/explore/rhythm-configs*` | Ops · **ECR-036** | +| [ops-rhythm-config-write.md](ops-rhythm-config-write.md) | RhythmConfig **写面** | §7 | admin 写 · `GET /rhythm/configs` | ExploreConfig · **ECR-044** | | [ops-image-card-deck.md](ops-image-card-deck.md) | ExploreConfig ImageCardDeck | §7 | `/admin/explore/image-card-decks*` | Ops · **ECR-037** | | [ops-report-template.md](ops-report-template.md) | GrowthInsights ReportTemplate | §7 | `/admin/growth/report-templates*` | Ops · **ECR-038** | | [ops-funnel-definition.md](ops-funnel-definition.md) | GrowthInsights FunnelDefinition | §7 | `/admin/analytics/funnel-definitions*` | Ops · **ECR-039** | diff --git a/.ai/product/feature-spec/ops-rhythm-config-write.md b/.ai/product/feature-spec/ops-rhythm-config-write.md new file mode 100644 index 0000000..beb7f68 --- /dev/null +++ b/.ai/product/feature-spec/ops-rhythm-config-write.md @@ -0,0 +1,98 @@ +# Feature Spec: ExploreConfig · RhythmConfig 写面(Ops · ECR-044) + +> Status: `Active`(**Implemented · ECR-044 Closed**) +> Map: `§7` · Capability: `ExploreConfig` · BC: `Explore_Reports` +> Auth: `docs/WAVE0/ECR-044_WRITE_AUTHORIZATION.md`(Human 批准 · **仅 ECR-044**) +> Predecessor: ECR-036 Closed(只读)· ECR-043 Closed(StarConfig 写) +> 关联:[ops-rhythm-config.md](ops-rhythm-config.md) · [life-rhythm.md](life-rhythm.md) + +--- + +## 1. 目标 + +运营可 **创建 / 更新 / 上下架** `RhythmConfig`;写操作鉴权 + 审计;C 端可读 active 配置并影响节律入口可用性;失败回退「现网可生成」。 + +验证 ExploreConfig 写面同构复用;**Closed 后立即 STOP**,不自动 ECR-045。 + +## 2. In / Out + +| In | Out | +|----|-----| +| Admin POST/PUT `/admin/explore/rhythm-configs*` | **StarConfig / ImageCardDeck / ScaleDefinition** 一切 | +| 复用权限 `admin.explore.write` | SystemPrompt · KnowledgeSource · Chunk · ToolDefinition | +| 复用表 `rhythm_configs`(code/title/active/system) | soft-delete · 改节律引擎 · 真支付 · UGC | +| C 端 GET active rhythm configs | Crisis / Handoff / ContentSafety 写 | +| 节律页:无 active → 温和不可用态;API 失败 → 回退可生成 | 合并多概念 · 自动开 ECR-045 | +| 审计 `admin_audit_logs` | 新权限子系统 | + +## 3. Domain + +| 字段 | 规则 | +|------|------| +| `code` | 唯一;`^[a-z][a-z0-9_]{1,62}$`;`system=true` 不可改 code | +| `title` | 必填 · ≤128 | +| `active` | `false` = 下架(唯一下架手段) | +| `system` | 种子行;禁止物理删除 | + +## 4. API + +### Admin + +| Method | Path | 权限 | +|--------|------|------| +| GET | `/admin/explore/rhythm-configs` · `/{id}` | `admin.explore.read`(保持) | +| POST | `/admin/explore/rhythm-configs` | `admin.explore.write` | +| PUT | `/admin/explore/rhythm-configs/{id}` | `admin.explore.write` | + +无 DELETE。 + +### C 端 + +| Method | Path | Auth | 语义 | +|--------|------|------|------| +| GET | `/api/v1/rhythm/configs` | DeviceAuth | 仅 `active=true`;按 code 排序 | + +## 5. C 端生效(可观察) + +| 条件 | user-h5 `/rhythm` | +|------|-------------------| +| 至少 1 条 active | 保持现网生成路径 | +| 成功且 0 条 active | 展示「节律配置暂未开放」类空态;禁用主生成 CTA | +| 请求失败 | **回退**:不阻断现网生成 | + +不改节律引擎、不改报告 JSON 契约。 + +## 6. Permission / Migration + +- Migration `000053_ops_rhythm_config_write`:幂等确认 `super_admin` → `admin.explore.write`(043 已授) +- 编码时占号(当前 Max=`000052`) + +## 7. UI + +| 端 | 行为 | +|----|------| +| admin-h5 节律配置页 | 列表 + 新建/编辑/上下架(仅 RhythmConfig) | +| user-h5 `/rhythm` | 按 §5 显隐 CTA | + +## 8. AC + +| ID | Then | +|----|------| +| AC-F-01 | POST 合法 → list/get 可见 | +| AC-F-02 | PUT `active=false` → C 端 list 不含 | +| AC-F-03 | 有 active → C 端 list 非空;节律页可生成 | +| AC-F-04 | 重复 code → 409 | +| AC-S-01/02 | 401;仅 read → POST 403 | +| AC-A-01 | 写产生审计 | +| AC-C-01 | 全下架后节律页空态/禁 CTA | +| AC-C-02 | C 端 API 失败不白屏、仍可走回退生成 | +| AC-O-01 | 无 Star/Card/Scale/Prompt 写路径;无 soft-delete | + +## 9. Closed 后 + +**立即 STOP** Continuous Loop。 +ECR-045 必须重新 Candidate Review + Human 单独拍板。 + +## 10. Trace + +ECR-044 · BD-2026-044 · PRODUCT_SPEC · CONTRACT_DIFF · ECR-044_WRITE_AUTHORIZATION diff --git a/.ai/product/feature-spec/ops-rhythm-config.md b/.ai/product/feature-spec/ops-rhythm-config.md index 5b700c0..e13af49 100644 --- a/.ai/product/feature-spec/ops-rhythm-config.md +++ b/.ai/product/feature-spec/ops-rhythm-config.md @@ -1,6 +1,7 @@ # Feature Spec: ExploreConfig · RhythmConfig(Ops · ECR-036) > Status: `Active`(Loop continuous · **ECR-036 Closed**) +> **写面:** [ops-rhythm-config-write.md](ops-rhythm-config-write.md)(**ECR-044**) > Parent: WAVE0-FROZEN · Predecessor: ECR-035 Closed > Capability: `ExploreConfig` · BC: `Explore_Reports` > 授权:`docs/WAVE0/LOOP_AUTHORIZATION.md` diff --git a/apps/admin-h5/src/api/client.ts b/apps/admin-h5/src/api/client.ts index c21bc80..90b74b7 100644 --- a/apps/admin-h5/src/api/client.ts +++ b/apps/admin-h5/src/api/client.ts @@ -636,9 +636,46 @@ export const adminApi = { updated_at: string }>('PUT', `/explore/star-configs/${id}`, body), rhythmConfigs: () => - request<{ items: Array> }>('GET', '/explore/rhythm-configs'), + request<{ + items: Array<{ + id: string + code: string + title: string + active: boolean + system: boolean + updated_at: string + }> + }>('GET', '/explore/rhythm-configs'), rhythmConfig: (id: string) => - request>('GET', `/explore/rhythm-configs/${id}`), + request<{ + id: string + code: string + title: string + active: boolean + system: boolean + updated_at: string + }>('GET', `/explore/rhythm-configs/${id}`), + createRhythmConfig: (body: { code: string; title: string; active: boolean }) => + request<{ + id: string + code: string + title: string + active: boolean + system: boolean + updated_at: string + }>('POST', '/explore/rhythm-configs', body), + updateRhythmConfig: ( + id: string, + body: { code: string; title: string; active: boolean }, + ) => + request<{ + id: string + code: string + title: string + active: boolean + system: boolean + updated_at: string + }>('PUT', `/explore/rhythm-configs/${id}`, body), imageCardDecks: () => request<{ items: Array> }>('GET', '/explore/image-card-decks'), imageCardDeck: (id: string) => diff --git a/apps/admin-h5/src/layouts/AdminShell.vue b/apps/admin-h5/src/layouts/AdminShell.vue index 70434fb..aacb32f 100644 --- a/apps/admin-h5/src/layouts/AdminShell.vue +++ b/apps/admin-h5/src/layouts/AdminShell.vue @@ -36,6 +36,7 @@ async function onLogout() { 危机 CMS 星座配置 + 节律配置 目录仓 订单 定价 diff --git a/apps/admin-h5/src/pages/RhythmConfigPage.vue b/apps/admin-h5/src/pages/RhythmConfigPage.vue new file mode 100644 index 0000000..5e9c72a --- /dev/null +++ b/apps/admin-h5/src/pages/RhythmConfigPage.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/apps/admin-h5/src/router/index.ts b/apps/admin-h5/src/router/index.ts index bcb9ccc..3e7f859 100644 --- a/apps/admin-h5/src/router/index.ts +++ b/apps/admin-h5/src/router/index.ts @@ -30,6 +30,12 @@ const router = createRouter({ component: () => import('@/pages/StarConfigPage.vue'), meta: { permission: 'admin.explore.read' }, }, + { + path: 'rhythm-configs', + name: 'rhythm-configs', + component: () => import('@/pages/RhythmConfigPage.vue'), + meta: { permission: 'admin.explore.read' }, + }, { path: 'catalogs', name: 'catalogs', component: () => import('@/pages/CatalogHubPage.vue') }, { path: 'push', name: 'push', component: () => import('@/pages/PushJobsPage.vue') }, { path: 'admins', name: 'admins', component: () => import('@/pages/AdminsPage.vue'), meta: { superOnly: true } }, diff --git a/apps/api/internal/handler/admin_rhythm_config.go b/apps/api/internal/handler/admin_rhythm_config.go index c2cd67c..4e6e351 100644 --- a/apps/api/internal/handler/admin_rhythm_config.go +++ b/apps/api/internal/handler/admin_rhythm_config.go @@ -16,6 +16,8 @@ func (h *AdminHandler) registerRhythmConfigs(authed *gin.RouterGroup) { g := authed.Group("/explore") g.GET("/rhythm-configs", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.ListRhythmConfigs) g.GET("/rhythm-configs/:id", middleware.RequireAdminPermission(h.Svc, admin.PermExploreRead), h.GetRhythmConfig) + g.POST("/rhythm-configs", middleware.RequireAdminPermission(h.Svc, admin.PermExploreWrite), h.CreateRhythmConfig) + g.PUT("/rhythm-configs/:id", middleware.RequireAdminPermission(h.Svc, admin.PermExploreWrite), h.UpdateRhythmConfig) } func (h *AdminHandler) ListRhythmConfigs(c *gin.Context) { @@ -44,3 +46,66 @@ func (h *AdminHandler) GetRhythmConfig(c *gin.Context) { } response.OK(c, row) } + +func (h *AdminHandler) CreateRhythmConfig(c *gin.Context) { + adminID, ok := middleware.AdminIDFromContext(c) + if !ok { + response.Fail(c, http.StatusUnauthorized, 40101, "admin auth required") + return + } + var body admin.RhythmConfigWriteBody + if err := c.ShouldBindJSON(&body); err != nil { + response.Fail(c, http.StatusBadRequest, 40054, "invalid body") + return + } + row, err := h.Svc.CreateRhythmConfig(c.Request.Context(), adminID, body) + if errors.Is(err, admin.ErrInvalidRhythmConfig) { + response.Fail(c, http.StatusBadRequest, 40055, "invalid rhythm config") + return + } + if errors.Is(err, admin.ErrRhythmConfigConflict) { + response.Fail(c, http.StatusConflict, 40912, "rhythm config code conflict") + return + } + if err != nil { + response.Fail(c, http.StatusInternalServerError, 50052, "create rhythm config failed") + return + } + response.OK(c, row) +} + +func (h *AdminHandler) UpdateRhythmConfig(c *gin.Context) { + adminID, ok := middleware.AdminIDFromContext(c) + if !ok { + response.Fail(c, http.StatusUnauthorized, 40101, "admin auth required") + return + } + id, err := uuid.Parse(c.Param("id")) + if err != nil { + response.Fail(c, http.StatusBadRequest, 40002, "invalid id") + return + } + var body admin.RhythmConfigWriteBody + if err := c.ShouldBindJSON(&body); err != nil { + response.Fail(c, http.StatusBadRequest, 40054, "invalid body") + return + } + row, err := h.Svc.UpdateRhythmConfig(c.Request.Context(), adminID, id, body) + if errors.Is(err, admin.ErrRhythmConfigNotFound) { + response.Fail(c, http.StatusNotFound, 40420, "rhythm-config not found") + return + } + if errors.Is(err, admin.ErrInvalidRhythmConfig) { + response.Fail(c, http.StatusBadRequest, 40055, "invalid rhythm config") + return + } + if errors.Is(err, admin.ErrRhythmConfigConflict) { + response.Fail(c, http.StatusConflict, 40912, "rhythm config code conflict") + return + } + if err != nil { + response.Fail(c, http.StatusInternalServerError, 50053, "update rhythm config failed") + return + } + response.OK(c, row) +} diff --git a/apps/api/internal/handler/rhythm_config_public.go b/apps/api/internal/handler/rhythm_config_public.go new file mode 100644 index 0000000..1784865 --- /dev/null +++ b/apps/api/internal/handler/rhythm_config_public.go @@ -0,0 +1,36 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/yuxingu/digital-psychology/apps/api/internal/repository" + "github.com/yuxingu/digital-psychology/apps/api/pkg/response" +) + +// RhythmConfigPublicHandler serves GET /api/v1/rhythm/configs (DeviceAuth). +type RhythmConfigPublicHandler struct { + Repo *repository.AdminRepo +} + +// Register mounts public rhythm config routes. +func (h *RhythmConfigPublicHandler) Register(api *gin.RouterGroup) { + api.GET("/rhythm/configs", h.ListActive) +} + +func (h *RhythmConfigPublicHandler) ListActive(c *gin.Context) { + if h.Repo == nil { + response.OK(c, gin.H{"items": []repository.RhythmConfigRow{}}) + return + } + items, err := h.Repo.ListActiveRhythmConfigs(c.Request.Context()) + if err != nil { + response.Fail(c, http.StatusInternalServerError, 50054, "rhythm configs failed") + return + } + if items == nil { + items = []repository.RhythmConfigRow{} + } + response.OK(c, gin.H{"items": items}) +} diff --git a/apps/api/internal/httpserver/router.go b/apps/api/internal/httpserver/router.go index daff489..b80f406 100644 --- a/apps/api/internal/httpserver/router.go +++ b/apps/api/internal/httpserver/router.go @@ -102,6 +102,7 @@ func NewRouter(pool *pgxpool.Pool, cfg config.Config) *gin.Engine { (&handler.AnalyticsHandler{Svc: analyticsSvc}).Register(authed) (&handler.HomeHandler{Svc: homeSvc}).Register(authed) (&handler.StarConfigPublicHandler{Repo: adminRepo}).Register(authed) + (&handler.RhythmConfigPublicHandler{Repo: adminRepo}).Register(authed) gated := authed.Group("") gated.Use(middleware.RequireRegistered(pool)) diff --git a/apps/api/internal/integration/rhythm_config_write_test.go b/apps/api/internal/integration/rhythm_config_write_test.go new file mode 100644 index 0000000..539ebe8 --- /dev/null +++ b/apps/api/internal/integration/rhythm_config_write_test.go @@ -0,0 +1,116 @@ +package integration_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +func TestExploreRhythmConfigWrite(t *testing.T) { + r, pool := setupAPIPool(t) + ctx := context.Background() + tok := adminLogin(t, r, "admin", "change-me") + devKey := fmt.Sprintf("rhythm-cfg-%d", time.Now().UnixNano()) + + code := fmt.Sprintf("rhythm_w_%d", time.Now().UnixNano()%1_000_000) + body := map[string]any{"code": code, "title": "测试节律配置", "active": true} + env, httpCode := doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/explore/rhythm-configs", body, tok) + if httpCode != 200 || env.Code != 0 { + t.Fatalf("create http=%d code=%d msg=%s", httpCode, env.Code, env.Message) + } + var created struct { + ID string `json:"id"` + Code string `json:"code"` + } + _ = json.Unmarshal(env.Data, &created) + if created.ID == "" { + t.Fatal("bad create") + } + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `DELETE FROM rhythm_configs WHERE id=$1`, created.ID) + }) + + _, httpCode = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/explore/rhythm-configs", body, tok) + if httpCode != http.StatusConflict { + t.Fatalf("dup expected 409 got %d", httpCode) + } + + env, _, httpCode = doJSONExpect(t, r, http.MethodGet, "/api/v1/rhythm/configs", nil, devKey, 0) + if httpCode != 200 { + t.Fatalf("public %d", httpCode) + } + var pub struct { + Items []struct { + Code string `json:"code"` + } `json:"items"` + } + _ = json.Unmarshal(env.Data, &pub) + found := false + for _, it := range pub.Items { + if it.Code == code { + found = true + break + } + } + if !found { + t.Fatalf("missing active %#v", pub.Items) + } + + body["active"] = false + _, httpCode = doAdminJSON(t, r, http.MethodPut, "/api/v1/admin/explore/rhythm-configs/"+created.ID, body, tok) + if httpCode != 200 { + t.Fatalf("update %d", httpCode) + } + env, _, _ = doJSONExpect(t, r, http.MethodGet, "/api/v1/rhythm/configs", nil, devKey, 0) + _ = json.Unmarshal(env.Data, &pub) + for _, it := range pub.Items { + if it.Code == code { + t.Fatal("inactive still listed") + } + } + + _, _ = pool.Exec(ctx, `UPDATE rhythm_configs SET active=false`) + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `UPDATE rhythm_configs SET active=true WHERE system=true`) + }) + env, _, _ = doJSONExpect(t, r, http.MethodGet, "/api/v1/rhythm/configs", nil, devKey, 0) + _ = json.Unmarshal(env.Data, &pub) + if len(pub.Items) != 0 { + t.Fatalf("expected empty after all inactive, got %#v", pub.Items) + } + + var n int + _ = pool.QueryRow(ctx, ` + SELECT COUNT(*) FROM admin_audit_logs + WHERE action IN ('explore.rhythm_config.create','explore.rhythm_config.update') AND target_id=$1`, + created.ID).Scan(&n) + if n < 2 { + t.Fatalf("audit %d", n) + } + + limitedRoleID := uuid.New() + _, _ = pool.Exec(ctx, `INSERT INTO admin_roles(id, name, system) VALUES ($1,$2,false)`, + limitedRoleID, "rc_ro_"+limitedRoleID.String()[:8]) + _, _ = pool.Exec(ctx, `INSERT INTO admin_role_permissions(role_id, code) VALUES ($1,'admin.explore.read')`, limitedRoleID) + hash, _ := bcrypt.GenerateFromPassword([]byte("ro-pass"), bcrypt.DefaultCost) + roUser := fmt.Sprintf("rcro_%d", time.Now().UnixNano()) + _, _ = pool.Exec(ctx, `INSERT INTO admin_accounts(username, password_hash, role_id) VALUES ($1,$2,$3)`, + roUser, string(hash), limitedRoleID) + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `DELETE FROM admin_accounts WHERE username=$1`, roUser) + _, _ = pool.Exec(ctx, `DELETE FROM admin_roles WHERE id=$1`, limitedRoleID) + }) + roTok := adminLogin(t, r, roUser, "ro-pass") + _, httpCode = doAdminJSON(t, r, http.MethodPost, "/api/v1/admin/explore/rhythm-configs", map[string]any{ + "code": "x_ro", "title": "no", "active": true, + }, roTok) + if httpCode != http.StatusForbidden { + t.Fatalf("expected 403 got %d", httpCode) + } +} diff --git a/apps/api/internal/repository/rhythm_config_write.go b/apps/api/internal/repository/rhythm_config_write.go new file mode 100644 index 0000000..52b9950 --- /dev/null +++ b/apps/api/internal/repository/rhythm_config_write.go @@ -0,0 +1,139 @@ +package repository + +import ( + "context" + "encoding/json" + "errors" + "strings" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// RhythmConfigWriteInput is create/update payload. +type RhythmConfigWriteInput struct { + Code string + Title string + Active bool +} + +// ListActiveRhythmConfigs returns active configs for C-end. +func (r *AdminRepo) ListActiveRhythmConfigs(ctx context.Context) ([]RhythmConfigRow, error) { + rows, err := r.Pool.Query(ctx, ` + SELECT id, code, title, active, system, updated_at + FROM rhythm_configs + WHERE active = true + ORDER BY code ASC + LIMIT 100`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []RhythmConfigRow + for rows.Next() { + var row RhythmConfigRow + if err := rows.Scan(&row.ID, &row.Code, &row.Title, &row.Active, &row.System, &row.UpdatedAt); err != nil { + return nil, err + } + out = append(out, row) + } + return out, rows.Err() +} + +// CreateRhythmConfigWithAudit inserts and audits. +func (r *AdminRepo) CreateRhythmConfigWithAudit( + ctx context.Context, adminID uuid.UUID, in RhythmConfigWriteInput, meta json.RawMessage, +) (*RhythmConfigRow, error) { + tx, err := r.Pool.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) + var row RhythmConfigRow + err = tx.QueryRow(ctx, ` + INSERT INTO rhythm_configs(code, title, active, system) + VALUES ($1,$2,$3,false) + RETURNING id, code, title, active, system, updated_at`, + in.Code, in.Title, in.Active, + ).Scan(&row.ID, &row.Code, &row.Title, &row.Active, &row.System, &row.UpdatedAt) + if err != nil { + return nil, mapRhythmConfigWriteErr(err) + } + if meta == nil { + meta = json.RawMessage(`{}`) + } + if _, err := tx.Exec(ctx, ` + INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta) + VALUES ($1,'explore.rhythm_config.create','rhythm_config',$2,$3)`, + adminID, row.ID.String(), meta, + ); err != nil { + return nil, err + } + if err := tx.Commit(ctx); err != nil { + return nil, err + } + return &row, nil +} + +// UpdateRhythmConfigWithAudit updates and audits. +func (r *AdminRepo) UpdateRhythmConfigWithAudit( + ctx context.Context, adminID, id uuid.UUID, in RhythmConfigWriteInput, meta json.RawMessage, +) (*RhythmConfigRow, error) { + tx, err := r.Pool.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) + var system bool + var oldCode string + err = tx.QueryRow(ctx, `SELECT system, code FROM rhythm_configs WHERE id=$1`, id).Scan(&system, &oldCode) + if errors.Is(err, pgx.ErrNoRows) { + return nil, pgx.ErrNoRows + } + if err != nil { + return nil, err + } + code := in.Code + if system { + code = oldCode + } + var row RhythmConfigRow + err = tx.QueryRow(ctx, ` + UPDATE rhythm_configs + SET code=$2, title=$3, active=$4, updated_at=now() + WHERE id=$1 + RETURNING id, code, title, active, system, updated_at`, + id, code, in.Title, in.Active, + ).Scan(&row.ID, &row.Code, &row.Title, &row.Active, &row.System, &row.UpdatedAt) + if err != nil { + return nil, mapRhythmConfigWriteErr(err) + } + if meta == nil { + meta = json.RawMessage(`{}`) + } + if _, err := tx.Exec(ctx, ` + INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta) + VALUES ($1,'explore.rhythm_config.update','rhythm_config',$2,$3)`, + adminID, id.String(), meta, + ); err != nil { + return nil, err + } + if err := tx.Commit(ctx); err != nil { + return nil, err + } + return &row, nil +} + +func mapRhythmConfigWriteErr(err error) error { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + return errString("rhythm config code conflict") + } + return err +} + +// RhythmConfigCodeConflict reports unique violation. +func RhythmConfigCodeConflict(err error) bool { + return err != nil && strings.Contains(err.Error(), "rhythm config code conflict") +} diff --git a/apps/api/internal/service/admin/rhythm_config_write.go b/apps/api/internal/service/admin/rhythm_config_write.go new file mode 100644 index 0000000..8b94576 --- /dev/null +++ b/apps/api/internal/service/admin/rhythm_config_write.go @@ -0,0 +1,71 @@ +package admin + +import ( + "context" + "encoding/json" + "errors" + "regexp" + "strings" + "unicode/utf8" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + + "github.com/yuxingu/digital-psychology/apps/api/internal/repository" +) + +var ( + ErrInvalidRhythmConfig = errors.New("invalid rhythm config") + ErrRhythmConfigConflict = errors.New("rhythm config code conflict") + rhythmConfigCodeRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,62}$`) +) + +// RhythmConfigWriteBody is JSON for create/update. +type RhythmConfigWriteBody struct { + Code string `json:"code"` + Title string `json:"title"` + Active bool `json:"active"` +} + +// CreateRhythmConfig validates, inserts, audits. +func (s *Service) CreateRhythmConfig(ctx context.Context, adminID uuid.UUID, body RhythmConfigWriteBody) (*repository.RhythmConfigRow, error) { + in, err := normalizeRhythmConfigWrite(body) + if err != nil { + return nil, err + } + meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active}) + row, err := s.Repo.CreateRhythmConfigWithAudit(ctx, adminID, in, meta) + if repository.RhythmConfigCodeConflict(err) { + return nil, ErrRhythmConfigConflict + } + return row, err +} + +// UpdateRhythmConfig validates, updates, audits. +func (s *Service) UpdateRhythmConfig(ctx context.Context, adminID, id uuid.UUID, body RhythmConfigWriteBody) (*repository.RhythmConfigRow, error) { + in, err := normalizeRhythmConfigWrite(body) + if err != nil { + return nil, err + } + meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active}) + row, err := s.Repo.UpdateRhythmConfigWithAudit(ctx, adminID, id, in, meta) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrRhythmConfigNotFound + } + if repository.RhythmConfigCodeConflict(err) { + return nil, ErrRhythmConfigConflict + } + return row, err +} + +func normalizeRhythmConfigWrite(body RhythmConfigWriteBody) (repository.RhythmConfigWriteInput, error) { + code := strings.TrimSpace(body.Code) + title := strings.TrimSpace(body.Title) + if !rhythmConfigCodeRe.MatchString(code) { + return repository.RhythmConfigWriteInput{}, ErrInvalidRhythmConfig + } + if title == "" || utf8.RuneCountInString(title) > 128 { + return repository.RhythmConfigWriteInput{}, ErrInvalidRhythmConfig + } + return repository.RhythmConfigWriteInput{Code: code, Title: title, Active: body.Active}, nil +} diff --git a/apps/api/migrations/000053_ops_rhythm_config_write.down.sql b/apps/api/migrations/000053_ops_rhythm_config_write.down.sql new file mode 100644 index 0000000..c50eadb --- /dev/null +++ b/apps/api/migrations/000053_ops_rhythm_config_write.down.sql @@ -0,0 +1,2 @@ +-- ECR-044 rollback marker (permission shared with StarConfig write — do not revoke explore.write) +SELECT 1; diff --git a/apps/api/migrations/000053_ops_rhythm_config_write.up.sql b/apps/api/migrations/000053_ops_rhythm_config_write.up.sql new file mode 100644 index 0000000..a18a870 --- /dev/null +++ b/apps/api/migrations/000053_ops_rhythm_config_write.up.sql @@ -0,0 +1,7 @@ +-- ECR-044 ExploreConfig RhythmConfig write (reuse admin.explore.write from 000052) + +INSERT INTO admin_role_permissions(role_id, code) +SELECT r.id, 'admin.explore.write' +FROM admin_roles r +WHERE r.name = 'super_admin' +ON CONFLICT DO NOTHING; diff --git a/apps/user-h5/src/composables/useLifeRhythmPage.ts b/apps/user-h5/src/composables/useLifeRhythmPage.ts index 6c320b9..4c4958c 100644 --- a/apps/user-h5/src/composables/useLifeRhythmPage.ts +++ b/apps/user-h5/src/composables/useLifeRhythmPage.ts @@ -21,6 +21,8 @@ export function useLifeRhythmPage() { const loading = ref(false) const needBirth = ref(true) const error = ref('') + /** Fail-open: true until proven empty; API error keeps generation. */ + const configAvailable = ref(true) const report = ref(null) const paying = ref(false) const tab = ref<'overview' | 'wuxing' | 'tips' | 'term'>('overview') @@ -107,7 +109,21 @@ export function useLifeRhythmPage() { scheduleDayRefresh() } + async function refreshConfigGate() { + try { + const res = await api.getRhythmConfigs() + configAvailable.value = (res.items || []).length > 0 + } catch { + configAvailable.value = true + } + } + async function generate(y: number, m: number, d: number) { + if (!configAvailable.value) { + error.value = '节律配置暂未开放' + needBirth.value = true + return + } loading.value = true error.value = '' needBirth.value = false @@ -127,6 +143,10 @@ export function useLifeRhythmPage() { } async function start() { + if (!configAvailable.value) { + error.value = '节律配置暂未开放' + return + } const y = Number(year.value) const m = Number(month.value) const d = Number(day.value) @@ -140,6 +160,7 @@ export function useLifeRhythmPage() { async function load() { if (!(await ensureAccount(router, route.fullPath))) return + await refreshConfigGate() const reportId = String(route.query.report_id || '') if (reportId) { loading.value = true @@ -156,6 +177,11 @@ export function useLifeRhythmPage() { } return } + if (!configAvailable.value) { + needBirth.value = true + report.value = null + return + } loading.value = true try { const cached = await loadSelfLatest('rhythm') @@ -220,6 +246,7 @@ export function useLifeRhythmPage() { return { loading, needBirth, + configAvailable, error, report, paying, diff --git a/apps/user-h5/src/pages/LifeRhythmPage.vue b/apps/user-h5/src/pages/LifeRhythmPage.vue index 3c97f9b..b01ba72 100644 --- a/apps/user-h5/src/pages/LifeRhythmPage.vue +++ b/apps/user-h5/src/pages/LifeRhythmPage.vue @@ -11,8 +11,15 @@
+
+ +

节律配置暂未开放

+

运营尚未启用节律配置,请稍后再来

+ +
+ diff --git a/docs/BACKEND_DESIGN/BD-2026-044-rhythm-config-write.md b/docs/BACKEND_DESIGN/BD-2026-044-rhythm-config-write.md new file mode 100644 index 0000000..accfed2 --- /dev/null +++ b/docs/BACKEND_DESIGN/BD-2026-044-rhythm-config-write.md @@ -0,0 +1,29 @@ +# Backend Design: ECR-044 RhythmConfig Write + +| Field | Value | +|-------|-------| +| ID | BD-2026-044 | +| Status | **Approved** | +| Coding | **AUTHORIZED** | +| Level | L2 | +| Migration | `000053_ops_rhythm_config_write`(编码时分配) | + +## Boundary + +```text +Domain: RhythmConfig write + C-end active read +API: + POST/PUT /api/v1/admin/explore/rhythm-configs* + GET /api/v1/rhythm/configs +Permission: admin.explore.write (reuse · 043) +Table: reuse rhythm_configs +Audit: explore.rhythm_config.create|update +``` + +## Out + +StarConfig · ImageCardDeck · ScaleDefinition · AICoreConfig · soft-delete · payment · UGC · auto ECR-045 + +## Rollback + +migration down · 移除写路由与 C 端读 · H5 回退始终可生成 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f6d66a6..c41472f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2026-08-13 +- **ECR-044 Closed:** ExploreConfig RhythmConfig 薄写面(复用 `admin.explore.write` · POST/PUT · `GET /rhythm/configs` · `/rhythm` 空态/失败回退 · migration `000053`)· **Continuous Loop STOP** · 禁自动 ECR-045 - **ECR-044 Candidate Review Approved:** 首刀 = **RhythmConfig**(不预设 SystemPrompt)· Spec/Loop/Coding **未开** · Auth 草案 PENDING - **ECR-043 Closed:** ExploreConfig StarConfig 薄写面(`admin.explore.write` · POST/PUT · `GET /star/configs` · `/star` 空态/失败回退 · migration `000052`)· **Continuous Loop STOP** · 禁自动 ECR-044 - **ECR-043 Spec Ready:** StarConfig 薄写面(Human 批准 Loop 仅 043)· Coding NOT STARTED · Closed 后禁自动 044 diff --git a/docs/CODE_REVIEW/ECR-044.md b/docs/CODE_REVIEW/ECR-044.md new file mode 100644 index 0000000..2dfc653 --- /dev/null +++ b/docs/CODE_REVIEW/ECR-044.md @@ -0,0 +1,3 @@ +# CODE_REVIEW — ECR-044 + +**Verdict:** Approve · RhythmConfig write only · 复用 `admin.explore.write` · 下架仅 active=false · 无 Star/Card/Scale/Prompt/Knowledge/Chunk · 无 soft-delete/支付/UGC/Crisis·Handoff · Closed 后 STOP(禁自动 ECR-045)· ECR-008 Live Promote 仍 BLOCKED diff --git a/docs/CONTRACT_DIFF/ECR-044.yaml b/docs/CONTRACT_DIFF/ECR-044.yaml new file mode 100644 index 0000000..b8c8219 --- /dev/null +++ b/docs/CONTRACT_DIFF/ECR-044.yaml @@ -0,0 +1,20 @@ +ecr: ECR-044 +capability: ExploreConfig +change: + type: additive +breaking_change: false +migration_required: true +apis: + - method: POST + path: /api/v1/admin/explore/rhythm-configs + change: added + - method: PUT + path: /api/v1/admin/explore/rhythm-configs/{id} + change: added + - method: GET + path: /api/v1/rhythm/configs + change: added +perms: + - code: admin.explore.write + change: reused +migration: 000053_ops_rhythm_config_write diff --git a/docs/ECR/ECR-044-rhythm-config-write.md b/docs/ECR/ECR-044-rhythm-config-write.md new file mode 100644 index 0000000..30e0107 --- /dev/null +++ b/docs/ECR/ECR-044-rhythm-config-write.md @@ -0,0 +1,26 @@ +# ECR-044 + +**Title:** ExploreConfig · RhythmConfig 薄写面 +**Status:** **Closed**(2026-08-13) +**Change Level:** L2 +**Auth:** `ECR-044_WRITE_AUTHORIZATION.md` · Continuous Loop **仅本 ECR** · Closed 后 STOP +**Predecessor:** ECR-036 · ECR-043 · Candidate Review Approved + +## Change + +1. Spec `ops-rhythm-config-write` +2. 复用 `admin.explore.write` + POST/PUT rhythm-configs + 审计 +3. C 端 `GET /rhythm/configs`;`/rhythm` 无 active 空态、失败回退 +4. Migration `000053` · OpenAPI · admin-h5 + +## Forbidden + +Star 再写 · Card · Scale · Prompt/Knowledge/Chunk · soft-delete · 支付 · UGC · Crisis/Handoff · **Closed 后自动 ECR-045** · ECR-008 Live Promote 解禁 + +## Acceptance + +见 Spec §8 · TEST_REPORT/ECR-044 · CODE_REVIEW Approve. + +## Coding gate + +Done · Closed. diff --git a/docs/HANDOFF/ECR-044-architect-to-engineer.md b/docs/HANDOFF/ECR-044-architect-to-engineer.md new file mode 100644 index 0000000..bd596a9 --- /dev/null +++ b/docs/HANDOFF/ECR-044-architect-to-engineer.md @@ -0,0 +1,6 @@ +# HANDOFF — Architect → Engineer · ECR-044 + +**Coding AUTHORIZED** — Human「Write Auth → Spec → 开码」. + +Do only RhythmConfig write per Spec. Reuse `admin.explore.write`. Migration 000053. +After Closed: STOP — no ECR-045. diff --git a/docs/HANDOFF/ECR-044-engineer-to-reviewer.md b/docs/HANDOFF/ECR-044-engineer-to-reviewer.md new file mode 100644 index 0000000..6026d34 --- /dev/null +++ b/docs/HANDOFF/ECR-044-engineer-to-reviewer.md @@ -0,0 +1,4 @@ +# HANDOFF — Engineer → Reviewer · ECR-044 + +TestExploreRhythmConfigWrite PASS · repo-governance-check PASS (max 000053) · Ready Closed. +禁止自动开 ECR-045 · ExploreConfig Loop STOP. diff --git a/docs/PRODUCT_SPEC/ECR-044-rhythm-config-write.md b/docs/PRODUCT_SPEC/ECR-044-rhythm-config-write.md new file mode 100644 index 0000000..5c91c09 --- /dev/null +++ b/docs/PRODUCT_SPEC/ECR-044-rhythm-config-write.md @@ -0,0 +1,3 @@ +# PRODUCT_SPEC — ECR-044 + +对齐 `ops-rhythm-config-write.md` · Human Approved · L2 · **仅 RhythmConfig** · **Closed**. diff --git a/docs/PROJECT_PROFILE.md b/docs/PROJECT_PROFILE.md index dae3de4..6ac4e46 100644 --- a/docs/PROJECT_PROFILE.md +++ b/docs/PROJECT_PROFILE.md @@ -53,20 +53,20 @@ - **本仓:** `ess_intake: strict` · Retro FAIL · 单 ECR Context - **Ops foundation:** `docs/WAVE0/` · `.ai/domain/boundary-rules.md` · `glossary.yaml` · Loop: `docs/WAVE0/LOOP_AUTHORIZATION.md` - ECR: main 线 ECR-006–016 Closed;Ops 扩展 ECR-013A/B · 017–040;分叉已固定为 `ECR-012-star` / `014-plan` / `015-code` / `016-insight`(见 TRACEABILITY) -- **Next ECR / Max Migration:** **Next=`ECR-044`(Candidate Review Approved · RhythmConfig)** · **Max=`000052`**(编码→053) +- **Next ECR / Max Migration:** **Next=`ECR-045`(禁自动开)** · **Max=`000053`** - **Write-Wave CMS:** 041–042 Closed · STOP -- **ExploreConfig:** ECR-043 Closed · **ECR-044 Candidate Review Approved** · Spec/Loop **未授权** · Prompt/Knowledge/Chunk STOP +- **ExploreConfig:** ECR-043/044 Closed · **Loop STOP** · Prompt/Knowledge/Chunk STOP - **Release:** ECR-008 Live Promote **BLOCKED**(独立门禁 · 勿与 Write-Wave 混谈) - EXP: (无) -- STATE: `docs/STATE/`(含 ECR-041…043) +- STATE: `docs/STATE/`(含 ECR-041…044) - TRACEABILITY: `docs/TRACEABILITY.md` · 门禁 `scripts/repo-governance-check.py` - ADR: `.ai/adr/0007-ess-ai-dual-track.md` - Product status: `.ai/product/p1-status.md`(**P1 Complete**)· `.ai/product/p2-status.md`(**P2 Complete**) -- Active Spec: `account-auth` · `explore-test` · `home` · `star-profile` · `life-rhythm` · `input-compliance` · `ops-system` · `profile` · `ops-banner-write` · `ops-star-config-write` +- Active Spec: `account-auth` · `explore-test` · `home` · `star-profile` · `life-rhythm` · `input-compliance` · `ops-system` · `profile` · `ops-banner-write` · `ops-star-config-write` · `ops-rhythm-config-write` ## WIP(尚未单独 ECR) -- **ECR-044** RhythmConfig:Candidate Review Approved · **Auth Loop PENDING** · **禁止 Spec 冒充开码 / 禁止业务代码** +- (无 · ECR-044 Closed · Loop STOP) ## Pointers diff --git a/docs/STATE/ECR-044.md b/docs/STATE/ECR-044.md new file mode 100644 index 0000000..63ff910 --- /dev/null +++ b/docs/STATE/ECR-044.md @@ -0,0 +1,7 @@ +# STATE — ECR-044 + +| Status | **Closed** | +| Spec | ops-rhythm-config-write | +| Migration | 000053_ops_rhythm_config_write | +| Closed | 2026-08-13 | +| Next | STOP · ECR-045 禁自动开 | diff --git a/docs/TEST_REPORT/ECR-044.md b/docs/TEST_REPORT/ECR-044.md new file mode 100644 index 0000000..769901c --- /dev/null +++ b/docs/TEST_REPORT/ECR-044.md @@ -0,0 +1,11 @@ +# TEST_REPORT — ECR-044 RhythmConfig Write + +**Commit:** (pending Close) + +```bash +go test ./internal/integration/ -count=1 -run TestExploreRhythmConfigWrite +python3 scripts/repo-governance-check.py +``` + +PASS · AC create/409/C-end hide inactive/empty-all/audit/403 read-only · migration 000053 +REPO GOVERNANCE: PASS · max migration 000053 · suggested Next ECR-045(禁自动开) diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index a72b309..9bf2d50 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -4,8 +4,8 @@ | Anchor | Value | Rule | |--------|-------|------| -| **Next ECR** | `ECR-044` | Candidate Review **Approved**(RhythmConfig)· Spec/Loop **未授权** · 禁止自动开码 | -| **Max Migration** | `000052` | 044 编码时占 `000053`;禁止凭记忆 | +| **Next ECR** | `ECR-045` | **冻结:** 须重新 Candidate Review + Human 单独拍板;**禁止自动开刀** | +| **Max Migration** | `000053` | 禁止凭记忆 | - ECR identity **全局唯一**(含已 Closed);禁止同号双义。分叉只用 `ECR-NNN-suffix` / `ECR-NNNA`。 - Migration **6 位版本号唯一**。撞号修复见 `docs/MIGRATION_RENUMBER.md`。 @@ -69,6 +69,6 @@ | ECR-042 | OpsCMS · FeedSlot 薄写面 | **Closed** | Spec ops-feed-slot-write · BD-2026-042 · 无新 migration · 复用 cms.write | | EXPLORE-WRITE | ExploreConfig 写面授权 | **Approved · 仅 ECR-043 · Closed 后 STOP** | `EXPLORE_CONFIG_WRITE_AUTHORIZATION.md` | | ECR-043 | ExploreConfig · StarConfig 薄写面 | **Closed** | Spec ops-star-config-write · BD-2026-043 · migration **000052** · Closed 后 STOP · 禁自动 ECR-044 | -| ECR-044 | ExploreConfig · RhythmConfig 薄写面 | **Candidate Review Approved** | `WAVE0/ECR-044-CANDIDATE_REVIEW.md` · Spec/Loop **NOT STARTED** · 禁 Prompt/Knowledge/Chunk | +| ECR-044 | ExploreConfig · RhythmConfig 薄写面 | **Closed** | Spec ops-rhythm-config-write · BD-2026-044 · migration **000053** · Closed 后 STOP · 禁自动 ECR-045 | > **Migration:** 合并后 `000015`–`000023` 曾撞号,已重编号至 `000050`。以 `apps/api/migrations/` 与 `docs/MIGRATION_RENUMBER.md` 为准(文档中旧号引用可能滞后)。 diff --git a/docs/WAVE0/ECR-044-CANDIDATE_REVIEW.md b/docs/WAVE0/ECR-044-CANDIDATE_REVIEW.md index 43341be..ff7f338 100644 --- a/docs/WAVE0/ECR-044-CANDIDATE_REVIEW.md +++ b/docs/WAVE0/ECR-044-CANDIDATE_REVIEW.md @@ -1,12 +1,12 @@ # ECR-044 Candidate Review — ExploreConfig 写面(续) -> **RESOLVED 2026-08-13(Human: Candidate Review approve):** -> 首刀 = **RhythmConfig** · **禁止**预设 SystemPrompt / Knowledge / Chunk -> Continuous Loop / Spec / Coding:**尚未授权**(须另开 Auth + Spec → 开码) +> **RESOLVED 2026-08-13(Human: Candidate Review approve → Auth/Spec/开码 → Closed):** +> 首刀 = **RhythmConfig** · **Closed** · Loop **STOP** · 禁自动 ECR-045 +> Continuous Loop / Spec / Coding:本刀已完成并收口 **Date:** 2026-08-13 -**Mode:** Candidate Review **Approved** · Spec **NOT STARTED** · **禁止业务代码** -**Anchors:** Next=`ECR-044` · Max Migration=`000052` +**Mode:** Candidate Review **Approved** · Spec **Closed** · Coding **Closed** +**Anchors:** Next=`ECR-045`(禁自动开)· Max Migration=`000053` ## 锁定现状(评审输入) diff --git a/docs/WAVE0/ECR-044_WRITE_AUTHORIZATION.md b/docs/WAVE0/ECR-044_WRITE_AUTHORIZATION.md index c7dd9f8..df4f840 100644 --- a/docs/WAVE0/ECR-044_WRITE_AUTHORIZATION.md +++ b/docs/WAVE0/ECR-044_WRITE_AUTHORIZATION.md @@ -1,32 +1,28 @@ -# ExploreConfig Write Authorization — ECR-044 only(草案) +# ExploreConfig Write Authorization — ECR-044 only | Field | Value | |-------|-------| | Date | 2026-08-13 | -| Authorizer | Human(Candidate Review 已批;**Loop 未批**) | -| Status | **PENDING Continuous Loop** · Candidate Review = Approved | -| Scope | **ECR-044 · RhythmConfig 薄写面 ONLY**(若本 Auth 获批) | -| Continuous Loop | **NOT YET** | +| Authorizer | Human | +| Status | **Approved · ECR-044 Closed · Loop STOP** | +| Scope | **ECR-044 · RhythmConfig 薄写面 ONLY** | +| Continuous Loop | **Finished · STOP** | | After Closed | **STOP** · 禁止自动 ECR-045 | -## 前置 +## Human 拍板(2026-08-13) -- Candidate Review:`docs/WAVE0/ECR-044-CANDIDATE_REVIEW.md` → **RhythmConfig** -- 043 Auth **已耗尽** · 不可继承 - -## 待 Human 拍板(本文件生效条件) - -1. 确认 Continuous Loop **仅限 ECR-044** -2. 确认复用 `admin.explore.write`(不加新权限体系) -3. 确认硬 STOP 表不变 - -获批前:**禁止 Spec 冒充已授权开码 · 禁止改 `apps/`**。 +1. Candidate Review = RhythmConfig +2. Write Auth → Spec → 开码(本授权) +3. 复用 `admin.explore.write`(043 已授;053 幂等确认) +4. Continuous Loop **仅限 ECR-044** ## Hard STOP -Prompt / Knowledge / Chunk · 支付 · UGC · soft-delete · Crisis/Handoff · Card/Scale 顺带 · Closed 后自动下一刀 · ECR-008 Live Promote 解禁 +Star 再写 · Card 内容编辑 · Scale · Prompt / Knowledge / Chunk · 支付 · UGC · soft-delete · Crisis/Handoff · Closed 后自动下一刀 · ECR-008 Live Promote 解禁 ## Refs Candidate Review: `ECR-044-CANDIDATE_REVIEW.md` -Predecessor Closed: ECR-043 · Auth `EXPLORE_CONFIG_WRITE_AUTHORIZATION.md` +Spec: `.ai/product/feature-spec/ops-rhythm-config-write.md` +ECR: `docs/ECR/ECR-044-rhythm-config-write.md` +Predecessor Auth: `EXPLORE_CONFIG_WRITE_AUTHORIZATION.md`(043 · 已耗尽) diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 4452060..5fa68d4 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -311,6 +311,15 @@ export function createClient(opts: CreateClientOptions) { active: boolean }> }>('/api/v1/star/configs'), + getRhythmConfigs: () => + call<{ + items: Array<{ + id: string + code: string + title: string + active: boolean + }> + }>('/api/v1/rhythm/configs'), getHomeDailyTips: () => call('/api/v1/home/daily-tips'), } } diff --git a/proto/openapi.yaml b/proto/openapi.yaml index e502a3e..bc9d713 100644 --- a/proto/openapi.yaml +++ b/proto/openapi.yaml @@ -1245,6 +1245,21 @@ paths: description: Unauthorized '403': description: Forbidden + post: + tags: [admin] + summary: Create RhythmConfig + description: Requires admin.explore.write · ECR-044 + responses: + '200': + description: OK + '400': + description: Invalid + '401': + description: Unauthorized + '403': + description: Forbidden + '409': + description: Code conflict /api/v1/admin/explore/rhythm-configs/{id}: get: @@ -1260,6 +1275,26 @@ paths: description: OK '404': description: Not found + put: + tags: [admin] + summary: Update RhythmConfig + description: Requires admin.explore.write · deactivate via active=false · ECR-044 + parameters: + - in: path + name: id + required: true + schema: { type: string, format: uuid } + responses: + '200': + description: OK + '400': + description: Invalid + '403': + description: Forbidden + '404': + description: Not found + '409': + description: Code conflict /api/v1/admin/explore/image-card-decks: get: @@ -1584,6 +1619,15 @@ paths: '200': description: OK + /api/v1/rhythm/configs: + get: + tags: [system] + summary: Active RhythmConfig list for C-end (ECR-044) + description: DeviceAuth · only active=true · ordered by code + responses: + '200': + description: OK + /api/v1/home/daily-tips: get: tags: [system]