From 140b08ca01732883d0935ac8bfb41e0c1319a9fa Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sat, 8 Aug 2026 00:27:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(ECR-021):=20AICoreConfig=20SystemPrompt=20?= =?UTF-8?q?=E5=8F=AA=E8=AF=BB=E5=B9=B6=20Closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 system_prompts 目录、admin.ai_config.read 与 admin-h5「AI」页;本切片不改运行时 Prompt、禁写发布/UGC/真支付。 Co-authored-by: Cursor --- .ai/domain/domain-map.md | 2 +- .ai/domain/entity-catalog.md | 2 +- .ai/product/feature-map.md | 2 +- .ai/product/feature-spec/README.md | 1 + .ai/product/feature-spec/ops-admin.md | 3 +- .../feature-spec/ops-ai-core-config.md | 41 +++++++ apps/admin-h5/src/api/client.ts | 26 +++++ apps/admin-h5/src/layouts/AdminShell.vue | 1 + apps/admin-h5/src/pages/AIConfigPage.vue | 104 +++++++++++++++++ apps/admin-h5/src/router/index.ts | 1 + apps/api/internal/handler/admin.go | 1 + apps/api/internal/handler/admin_ai_config.go | 46 ++++++++ .../internal/integration/ai_config_test.go | 105 ++++++++++++++++++ .../api/internal/repository/ai_config_repo.go | 62 +++++++++++ apps/api/internal/service/admin/ai_config.go | 34 ++++++ apps/api/internal/service/admin/rbac.go | 2 + .../000022_ai_system_prompts.down.sql | 4 + .../000022_ai_system_prompts.up.sql | 42 +++++++ .../BD-2026-021-ai-core-config.md | 24 ++++ docs/CHANGELOG.md | 4 + docs/CODE_REVIEW/ECR-021.md | 8 ++ docs/CONTRACT_DIFF/ECR-021.yaml | 23 ++++ docs/ECR/ECR-021-ai-core-config.md | 19 ++++ .../ECR-021-ai-core-config.md | 7 ++ docs/HANDOFF/ECR-021-architect-to-engineer.md | 3 + docs/HANDOFF/ECR-021-engineer-to-reviewer.md | 3 + docs/PRODUCT_SPEC/ECR-021-ai-core-config.md | 3 + docs/STATE/ECR-021.md | 6 + docs/TASKS/TASK-021-ECR021.yaml | 12 ++ docs/TEST_REPORT/ECR-021.md | 32 ++++++ docs/TRACEABILITY.md | 1 + docs/WAVE0/LOOP_AUTHORIZATION.md | 2 +- proto/openapi.yaml | 28 +++++ 33 files changed, 649 insertions(+), 5 deletions(-) create mode 100644 .ai/product/feature-spec/ops-ai-core-config.md create mode 100644 apps/admin-h5/src/pages/AIConfigPage.vue create mode 100644 apps/api/internal/handler/admin_ai_config.go create mode 100644 apps/api/internal/integration/ai_config_test.go create mode 100644 apps/api/internal/repository/ai_config_repo.go create mode 100644 apps/api/internal/service/admin/ai_config.go create mode 100644 apps/api/migrations/000022_ai_system_prompts.down.sql create mode 100644 apps/api/migrations/000022_ai_system_prompts.up.sql create mode 100644 docs/BACKEND_DESIGN/BD-2026-021-ai-core-config.md create mode 100644 docs/CODE_REVIEW/ECR-021.md create mode 100644 docs/CONTRACT_DIFF/ECR-021.yaml create mode 100644 docs/ECR/ECR-021-ai-core-config.md create mode 100644 docs/ENGINEERING_SPEC/ECR-021-ai-core-config.md create mode 100644 docs/HANDOFF/ECR-021-architect-to-engineer.md create mode 100644 docs/HANDOFF/ECR-021-engineer-to-reviewer.md create mode 100644 docs/PRODUCT_SPEC/ECR-021-ai-core-config.md create mode 100644 docs/STATE/ECR-021.md create mode 100644 docs/TASKS/TASK-021-ECR021.yaml create mode 100644 docs/TEST_REPORT/ECR-021.md diff --git a/.ai/domain/domain-map.md b/.ai/domain/domain-map.md index b27ebd8..f5b7254 100644 --- a/.ai/domain/domain-map.md +++ b/.ai/domain/domain-map.md @@ -37,7 +37,7 @@ | Ops_Content | ExploreConfig (partial) | Shipped Ops-C | | Account_Risk | AccountLifecycle | Spec via ECR-013B(Identity_Profile owns UserStatus) | | Content_Safety | ContentSafety · CrisisCare | ContentSafety **ECR-019**(FilterRule);CrisisCare Draft | -| Ask_Ops | AskOperations · AICoreConfig | AskOperations **ECR-017/020**;AICoreConfig Draft | +| Ask_Ops | AskOperations · AICoreConfig | AskOperations **ECR-017/020**;AICoreConfig **ECR-021**(SystemPrompt 只读) | | Ops_CMS_NoUGC | OpsCMS | Draft | | Community | — | **Forbidden** | diff --git a/.ai/domain/entity-catalog.md b/.ai/domain/entity-catalog.md index 83cbd80..df782e8 100644 --- a/.ai/domain/entity-catalog.md +++ b/.ai/domain/entity-catalog.md @@ -31,7 +31,7 @@ Status: `Draft` | FilterRule | Content_Safety | ContentSafety | **ECR-019** 只读 + evaluate | | ModerationCase | Content_Safety | ContentSafety | 后置 | | CrisisEvent / CrisisPolicy | Content_Safety | CrisisCare | 后置 | -| SystemPrompt / Knowledge* / ToolDefinition | Ask_Ops | AICoreConfig | 后置 | +| SystemPrompt / Knowledge* / ToolDefinition | Ask_Ops | AICoreConfig | **ECR-021** SystemPrompt 只读;Knowledge/Tools 后置 | | AskSessionView / QualityFeedback | Ask_Ops | AskOperations | **ECR-017** AskSessionView;**ECR-020** QualityFeedback | | Banner / FeedSlot | Ops_CMS_NoUGC | OpsCMS | 后置 | | UGC* | Community | — | **Forbidden** | diff --git a/.ai/product/feature-map.md b/.ai/product/feature-map.md index eca066d..0c63e2e 100644 --- a/.ai/product/feature-map.md +++ b/.ai/product/feature-map.md @@ -235,7 +235,7 @@ UI **不出现「塔罗」**。禁止神谕吉凶、恐吓话术。 | Phase A `[Ops]` | 登录 · 用户/订单查询 · 会员授予 · 审计 · `apps/admin-h5`(ECR-006 Closed) | | Phase B `[Ops]` | 行为分析:自有埋点 + 管理端「数据」看板(**ECR-007 Closed** · Spec `ops-analytics.md`) | | Phase C `[Ops]` | 内容:首页宫格 CRUD · 测评上下架(**ECR-008 Closed** · Spec `ops-content.md`) | -| Phase D+ | **Contract-First**:**ECR-013A…019 Closed** → **ECR-020** QualityFeedback(Loop)。`docs/WAVE0/LOOP_AUTHORIZATION.md`。 | +| Phase D+ | **Contract-First**:**ECR-013A…020 Closed** → **ECR-021** AICoreConfig(Loop)。`docs/WAVE0/LOOP_AUTHORIZATION.md`。 | | 排除 | **UGC / 社区广场**(M10.2)仍 `[No]`;真支付最后 | 不计入 P1 Complete;不进入五 Tab。 diff --git a/.ai/product/feature-spec/README.md b/.ai/product/feature-spec/README.md index 2909af6..f3225f0 100644 --- a/.ai/product/feature-spec/README.md +++ b/.ai/product/feature-spec/README.md @@ -30,6 +30,7 @@ | [ops-entitlement.md](ops-entitlement.md) | 用户权益 Entitlement | §7 | `admin-h5` 用户详情「权益」· `GET /admin/users/:id/entitlements` | Ops-D · **ECR-018 Closed** | | [ops-content-safety.md](ops-content-safety.md) | 内容安全 ContentSafety | §7 | `admin-h5` `/safety` · `GET /admin/content-safety/*` | Ops-D · **ECR-019 Closed** | | [ops-quality-feedback.md](ops-quality-feedback.md) | 问答质量反馈 QualityFeedback | §7 | `admin-h5` `/ask` · `GET/POST /admin/ask/feedback*` | Ops-D · **ECR-020 Closed** | +| [ops-ai-core-config.md](ops-ai-core-config.md) | AI 核心配置 AICoreConfig | §7 | `admin-h5` `/ai` · `GET /admin/ai/system-prompts*` | Ops-D · **ECR-021 Closed** | 新功能:复制 `_TEMPLATE.md` → 填满 → 在本表登记 → 再编码。 diff --git a/.ai/product/feature-spec/ops-admin.md b/.ai/product/feature-spec/ops-admin.md index 1f32a32..e689fe1 100644 --- a/.ai/product/feature-spec/ops-admin.md +++ b/.ai/product/feature-spec/ops-admin.md @@ -217,6 +217,7 @@ Phase A 可先 `console`/本地;不挡验收。 | **K(ECR-018 Closed)** | Entitlement — Spec `ops-entitlement.md` | | **L(ECR-019 Closed)** | ContentSafety FilterRule — Spec `ops-content-safety.md` | | **M(ECR-020 Closed)** | QualityFeedback — Spec `ops-quality-feedback.md` | -| 后置 | AICoreConfig / CrisisCare(Loop 续跑) | +| **N(ECR-021 Closed)** | AICoreConfig SystemPrompt — Spec `ops-ai-core-config.md` | +| 后置 | CrisisCare / Knowledge·Tools 写面(Loop 续跑) | | D | 订单筛选 · 展示价 · 退款只读(另开 ECR) | | 后置 | 封禁加深(Account_Risk)· 推送占位 | diff --git a/.ai/product/feature-spec/ops-ai-core-config.md b/.ai/product/feature-spec/ops-ai-core-config.md new file mode 100644 index 0000000..773cc93 --- /dev/null +++ b/.ai/product/feature-spec/ops-ai-core-config.md @@ -0,0 +1,41 @@ +# Feature Spec: AICoreConfig · SystemPrompt(Ops · ECR-021) + +> Status: `Active`(Loop continuous · **ECR-021 Closed**) +> Parent: WAVE0-FROZEN · Predecessor: ECR-020 Closed +> Capability: `AICoreConfig` · BC: `Ask_Ops` +> 授权:`docs/WAVE0/LOOP_AUTHORIZATION.md` + +## Non-goals + +Prompt 在线编辑/发布 · KnowledgeSource/Chunk · ToolDefinition · 切换运行时引擎 · UGC · 真支付 + +## L2 Domain + +| 概念 | 语义 | +|------|------| +| `SystemPrompt` | code 唯一;body 为模板文本;active/system;本切片只读 | + +## L3 API + +| Method | Path | 权限 | 语义 | +|--------|------|------|------| +| GET | `/admin/ai/system-prompts` | `admin.ai_config.read` | 列表 | +| GET | `/admin/ai/system-prompts/:id` | 同上 | 详情(含 body) | + +## Migration + +`000022`:`system_prompts` + 种子 `ask_default` + 授予权限 + +## L4 AC + +| ID | Then | +|----|------| +| AC-F-01 | list 含 ask_default | +| AC-F-02 | get 返回 body 非空 | +| AC-F-03 | 未知 id → 404 | +| AC-S-01 | 无 Admin → 401 | +| AC-S-02 | 无 ai_config.read → 403 | +| AC-P-01 | list < 500ms | +| AC-O-01 | N/A 只读 | + +contract_diff: `docs/CONTRACT_DIFF/ECR-021.yaml` diff --git a/apps/admin-h5/src/api/client.ts b/apps/admin-h5/src/api/client.ts index 8b772df..aafb0e8 100644 --- a/apps/admin-h5/src/api/client.ts +++ b/apps/admin-h5/src/api/client.ts @@ -295,6 +295,32 @@ export const adminApi = { rating: number source: string }>('POST', `/ask/threads/${threadId}/feedback`, body), + systemPrompts: () => + request<{ + items: Array<{ + id: string + code: string + title: string + scene?: string + body: string + version: number + active: boolean + system: boolean + updated_at: string + }> + }>('GET', '/ai/system-prompts'), + systemPrompt: (id: string) => + request<{ + id: string + code: string + title: string + scene?: string + body: string + version: number + active: boolean + system: boolean + updated_at: string + }>('GET', `/ai/system-prompts/${id}`), orders: () => request<{ items: Array<{ diff --git a/apps/admin-h5/src/layouts/AdminShell.vue b/apps/admin-h5/src/layouts/AdminShell.vue index 84464b4..4268626 100644 --- a/apps/admin-h5/src/layouts/AdminShell.vue +++ b/apps/admin-h5/src/layouts/AdminShell.vue @@ -32,6 +32,7 @@ async function onLogout() { 兑换码 问答 安全 + AI 订单 审计 diff --git a/apps/admin-h5/src/pages/AIConfigPage.vue b/apps/admin-h5/src/pages/AIConfigPage.vue new file mode 100644 index 0000000..e8de86a --- /dev/null +++ b/apps/admin-h5/src/pages/AIConfigPage.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/apps/admin-h5/src/router/index.ts b/apps/admin-h5/src/router/index.ts index 4be8e1d..0965630 100644 --- a/apps/admin-h5/src/router/index.ts +++ b/apps/admin-h5/src/router/index.ts @@ -19,6 +19,7 @@ const router = createRouter({ { path: 'codes', name: 'codes', component: () => import('@/pages/RedemptionPage.vue') }, { path: 'ask', name: 'ask', component: () => import('@/pages/AskPage.vue') }, { path: 'safety', name: 'safety', component: () => import('@/pages/SafetyPage.vue') }, + { path: 'ai', name: 'ai', component: () => import('@/pages/AIConfigPage.vue') }, { path: 'audit', name: 'audit', component: () => import('@/pages/AuditPage.vue') }, ], }, diff --git a/apps/api/internal/handler/admin.go b/apps/api/internal/handler/admin.go index 14eb0a6..d86b383 100644 --- a/apps/api/internal/handler/admin.go +++ b/apps/api/internal/handler/admin.go @@ -51,6 +51,7 @@ func (h *AdminHandler) Register(api *gin.RouterGroup) { h.registerQualityFeedback(authed) h.registerEntitlement(authed) h.registerContentSafety(authed) + h.registerAIConfig(authed) } func (h *AdminHandler) Login(c *gin.Context) { diff --git a/apps/api/internal/handler/admin_ai_config.go b/apps/api/internal/handler/admin_ai_config.go new file mode 100644 index 0000000..9f8988f --- /dev/null +++ b/apps/api/internal/handler/admin_ai_config.go @@ -0,0 +1,46 @@ +package handler + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + "github.com/yuxingu/digital-psychology/apps/api/internal/middleware" + "github.com/yuxingu/digital-psychology/apps/api/internal/service/admin" + "github.com/yuxingu/digital-psychology/apps/api/pkg/response" +) + +func (h *AdminHandler) registerAIConfig(authed *gin.RouterGroup) { + g := authed.Group("/ai") + g.GET("/system-prompts", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.ListSystemPrompts) + g.GET("/system-prompts/:id", middleware.RequireAdminPermission(h.Svc, admin.PermAIConfigRead), h.GetSystemPrompt) +} + +func (h *AdminHandler) ListSystemPrompts(c *gin.Context) { + items, err := h.Svc.ListSystemPrompts(c.Request.Context()) + if err != nil { + response.Fail(c, http.StatusInternalServerError, 50027, "list system prompts failed") + return + } + response.OK(c, gin.H{"items": items}) +} + +func (h *AdminHandler) GetSystemPrompt(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + response.Fail(c, http.StatusBadRequest, 40002, "invalid id") + return + } + row, err := h.Svc.GetSystemPrompt(c.Request.Context(), id) + if errors.Is(err, admin.ErrSystemPromptNotFound) { + response.Fail(c, http.StatusNotFound, 40404, "system prompt not found") + return + } + if err != nil { + response.Fail(c, http.StatusInternalServerError, 50028, "get system prompt failed") + return + } + response.OK(c, row) +} diff --git a/apps/api/internal/integration/ai_config_test.go b/apps/api/internal/integration/ai_config_test.go new file mode 100644 index 0000000..c2553be --- /dev/null +++ b/apps/api/internal/integration/ai_config_test.go @@ -0,0 +1,105 @@ +package integration_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +func TestAICoreSystemPrompts(t *testing.T) { + r, pool := setupAPIPool(t) + ctx := context.Background() + tok := adminLogin(t, r, "admin", "change-me") + + _, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/ai/system-prompts", nil, "") + if code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d", code) + } + + limitedRoleID := uuid.New() + _, err := pool.Exec(ctx, ` + INSERT INTO admin_roles(id, name, system) VALUES ($1,$2,false)`, + limitedRoleID, "ai_lim_"+limitedRoleID.String()[:8]) + if err != nil { + t.Fatal(err) + } + _, err = pool.Exec(ctx, ` + INSERT INTO admin_role_permissions(role_id, code) VALUES ($1,'admin.users.read')`, limitedRoleID) + if err != nil { + t.Fatal(err) + } + hash, err := bcrypt.GenerateFromPassword([]byte("limited-pass"), bcrypt.DefaultCost) + if err != nil { + t.Fatal(err) + } + limUser := fmt.Sprintf("ailim_%d", time.Now().UnixNano()) + _, err = pool.Exec(ctx, ` + INSERT INTO admin_accounts(username, password_hash, role_id) VALUES ($1,$2,$3)`, + limUser, string(hash), limitedRoleID) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `DELETE FROM admin_accounts WHERE username=$1`, limUser) + _, _ = pool.Exec(ctx, `DELETE FROM admin_roles WHERE id=$1`, limitedRoleID) + }) + limTok := adminLogin(t, r, limUser, "limited-pass") + _, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/ai/system-prompts", nil, limTok) + if code != http.StatusForbidden { + t.Fatalf("expected 403, got %d", code) + } + + start := time.Now() + env, code := doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/ai/system-prompts", nil, tok) + if code != 200 || env.Code != 0 { + t.Fatalf("list http=%d msg=%s", code, env.Message) + } + if time.Since(start) > 500*time.Millisecond { + t.Fatalf("list too slow %v", time.Since(start)) + } + var list struct { + Items []struct { + ID string `json:"id"` + Code string `json:"code"` + Body string `json:"body"` + } `json:"items"` + } + _ = json.Unmarshal(env.Data, &list) + var askID string + for _, it := range list.Items { + if it.Code == "ask_default" { + askID = it.ID + if it.Body == "" { + t.Fatal("ask_default body empty in list") + } + break + } + } + if askID == "" { + t.Fatalf("missing ask_default: %#v", list.Items) + } + + env, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/ai/system-prompts/"+askID, nil, tok) + if code != 200 { + t.Fatalf("get %d", code) + } + var detail struct { + Body string `json:"body"` + Code string `json:"code"` + } + _ = json.Unmarshal(env.Data, &detail) + if detail.Code != "ask_default" || detail.Body == "" { + t.Fatalf("bad detail %#v", detail) + } + + _, code = doAdminJSON(t, r, http.MethodGet, "/api/v1/admin/ai/system-prompts/"+fakeUUID(), nil, tok) + if code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", code) + } +} diff --git a/apps/api/internal/repository/ai_config_repo.go b/apps/api/internal/repository/ai_config_repo.go new file mode 100644 index 0000000..06c126e --- /dev/null +++ b/apps/api/internal/repository/ai_config_repo.go @@ -0,0 +1,62 @@ +package repository + +import ( + "context" + "errors" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" +) + +// SystemPromptRow is AICoreConfig SystemPrompt catalog row. +type SystemPromptRow struct { + ID uuid.UUID `json:"id"` + Code string `json:"code"` + Title string `json:"title"` + Scene *string `json:"scene,omitempty"` + Body string `json:"body"` + Version int `json:"version"` + Active bool `json:"active"` + System bool `json:"system"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ListSystemPrompts returns prompt catalog (body included for ops read). +func (r *AdminRepo) ListSystemPrompts(ctx context.Context) ([]SystemPromptRow, error) { + rows, err := r.Pool.Query(ctx, ` + SELECT id, code, title, scene, body, version, active, system, updated_at + FROM system_prompts + ORDER BY active DESC, code ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + var out []SystemPromptRow + for rows.Next() { + var p SystemPromptRow + if err := rows.Scan( + &p.ID, &p.Code, &p.Title, &p.Scene, &p.Body, &p.Version, &p.Active, &p.System, &p.UpdatedAt, + ); err != nil { + return nil, err + } + out = append(out, p) + } + return out, rows.Err() +} + +// GetSystemPrompt loads one prompt by id. +func (r *AdminRepo) GetSystemPrompt(ctx context.Context, id uuid.UUID) (*SystemPromptRow, error) { + var p SystemPromptRow + err := r.Pool.QueryRow(ctx, ` + SELECT id, code, title, scene, body, version, active, system, updated_at + FROM system_prompts WHERE id=$1`, id, + ).Scan(&p.ID, &p.Code, &p.Title, &p.Scene, &p.Body, &p.Version, &p.Active, &p.System, &p.UpdatedAt) + if errors.Is(err, pgx.ErrNoRows) { + return nil, err + } + if err != nil { + return nil, err + } + return &p, nil +} diff --git a/apps/api/internal/service/admin/ai_config.go b/apps/api/internal/service/admin/ai_config.go new file mode 100644 index 0000000..efc3080 --- /dev/null +++ b/apps/api/internal/service/admin/ai_config.go @@ -0,0 +1,34 @@ +package admin + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + + "github.com/yuxingu/digital-psychology/apps/api/internal/repository" +) + +var ErrSystemPromptNotFound = errString("system prompt not found") + +// ListSystemPrompts returns SystemPrompt catalog. +func (s *Service) ListSystemPrompts(ctx context.Context) ([]repository.SystemPromptRow, error) { + items, err := s.Repo.ListSystemPrompts(ctx) + if err != nil { + return nil, err + } + if items == nil { + items = []repository.SystemPromptRow{} + } + return items, nil +} + +// GetSystemPrompt loads one prompt. +func (s *Service) GetSystemPrompt(ctx context.Context, id uuid.UUID) (*repository.SystemPromptRow, error) { + row, err := s.Repo.GetSystemPrompt(ctx, id) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrSystemPromptNotFound + } + return row, err +} diff --git a/apps/api/internal/service/admin/rbac.go b/apps/api/internal/service/admin/rbac.go index bfade24..4e513fe 100644 --- a/apps/api/internal/service/admin/rbac.go +++ b/apps/api/internal/service/admin/rbac.go @@ -27,6 +27,7 @@ const ( PermAskRead = "admin.ask.read" PermAskFeedbackWrite = "admin.ask.feedback.write" PermContentSafetyRead = "admin.content_safety.read" + PermAIConfigRead = "admin.ai_config.read" ) var knownPermissions = map[string]struct{}{ @@ -36,6 +37,7 @@ var knownPermissions = map[string]struct{}{ PermUsersStatusWrite: {}, PermMembershipPlansRead: {}, PermMembershipPlansWrite: {}, PermMembershipCodesRead: {}, PermMembershipCodesWrite: {}, PermAskRead: {}, PermAskFeedbackWrite: {}, PermContentSafetyRead: {}, + PermAIConfigRead: {}, } var ( diff --git a/apps/api/migrations/000022_ai_system_prompts.down.sql b/apps/api/migrations/000022_ai_system_prompts.down.sql new file mode 100644 index 0000000..7b86a6d --- /dev/null +++ b/apps/api/migrations/000022_ai_system_prompts.down.sql @@ -0,0 +1,4 @@ +-- ECR-021 down + +DELETE FROM admin_role_permissions WHERE code = 'admin.ai_config.read'; +DROP TABLE IF EXISTS system_prompts; diff --git a/apps/api/migrations/000022_ai_system_prompts.up.sql b/apps/api/migrations/000022_ai_system_prompts.up.sql new file mode 100644 index 0000000..c36b0c5 --- /dev/null +++ b/apps/api/migrations/000022_ai_system_prompts.up.sql @@ -0,0 +1,42 @@ +-- ECR-021 AICoreConfig SystemPrompt (read catalog) + +CREATE TABLE IF NOT EXISTS system_prompts ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code varchar(64) NOT NULL UNIQUE, + title varchar(128) NOT NULL, + scene varchar(32) NULL, + body text NOT NULL, + version int NOT NULL DEFAULT 1 CHECK (version > 0), + active boolean NOT NULL DEFAULT true, + system boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_system_prompts_active ON system_prompts(active); + +INSERT INTO system_prompts(code, title, scene, body, version, active, system) +VALUES ( + 'ask_default', + '问答默认系统提示', + NULL, + $prompt$你是「愈心谷」的 AI 成长助手。 + +定位:陪伴式成长对话,非医疗诊断、非心理咨询执业替代。 +语气:温和、具体、可执行;避免恐吓与宿命论话术。 + +运行时将注入:档案称呼、关系、生日、场景、已有报告摘要(若有)。 +占位约定:{{display_name}} · {{relation}} · {{birth_date}} · {{scene}} · {{report_ctx}} + +禁止:承诺疗效、开药、鼓励自伤、编造未生成的测评结论。$prompt$, + 1, + true, + true +) +ON CONFLICT (code) DO NOTHING; + +INSERT INTO admin_role_permissions(role_id, code) +SELECT r.id, 'admin.ai_config.read' +FROM admin_roles r +WHERE r.name = 'super_admin' +ON CONFLICT DO NOTHING; diff --git a/docs/BACKEND_DESIGN/BD-2026-021-ai-core-config.md b/docs/BACKEND_DESIGN/BD-2026-021-ai-core-config.md new file mode 100644 index 0000000..34f67a7 --- /dev/null +++ b/docs/BACKEND_DESIGN/BD-2026-021-ai-core-config.md @@ -0,0 +1,24 @@ +# Backend Design: ECR-021 AICoreConfig + +| ID | BD-2026-021 | +| Status | Approved | +| Coding | Loop authorized | +| Level | L2 | +| Migration | YES 000022 | + +## Backend Change Boundary + +```text +Domain: SystemPrompt (read catalog) +App: AdminHandler → admin.Service → AdminRepo +API: GET /admin/ai/system-prompts[+/:id] +Permission: admin.ai_config.read +Migration: 000022 table + seed ask_default + perm +UI: admin-h5 /ai +``` + +## Out of boundary + +Prompt write/publish · Knowledge* · ToolDefinition · runtime engine swap · UGC · Payment + +Rollback: down migration + remove routes/UI diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 62c71b5..5d021a4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG — ESS process artifacts +## 2026-08-08 + +- **ECR-021 Closed**:AICoreConfig SystemPrompt(`system_prompts` · admin-h5 `/ai` · migration 000022 · 只读) + ## 2026-08-07 - **ECR-020 Closed**:QualityFeedback(`ask_quality_feedback` · admin/user 评分 · migration 000021) diff --git a/docs/CODE_REVIEW/ECR-021.md b/docs/CODE_REVIEW/ECR-021.md new file mode 100644 index 0000000..0fdbab2 --- /dev/null +++ b/docs/CODE_REVIEW/ECR-021.md @@ -0,0 +1,8 @@ +# CODE_REVIEW — ECR-021 + +**Verdict:** Approve → Closed + +Date: 2026-08-08 · Loop continuous + +- SystemPrompt 目录只读;未改 Ask 运行时组装 +- Integration AC mapped · OpenAPI updated diff --git a/docs/CONTRACT_DIFF/ECR-021.yaml b/docs/CONTRACT_DIFF/ECR-021.yaml new file mode 100644 index 0000000..d255375 --- /dev/null +++ b/docs/CONTRACT_DIFF/ECR-021.yaml @@ -0,0 +1,23 @@ +ecr: ECR-021 +capability: AICoreConfig +bounded_context: Ask_Ops +parent: WAVE0-FROZEN +predecessor: ECR-020 +change: + type: additive +breaking_change: false +migration_required: true +compatibility_notes: > + Adds system_prompts catalog with ask_default seed and read-only admin APIs. + Does not change Ask runtime prompt assembly in this slice. + +apis: + - method: GET + path: /api/v1/admin/ai/system-prompts + change: added + - method: GET + path: /api/v1/admin/ai/system-prompts/{id} + change: added +perms: + - code: admin.ai_config.read + change: added diff --git a/docs/ECR/ECR-021-ai-core-config.md b/docs/ECR/ECR-021-ai-core-config.md new file mode 100644 index 0000000..3df442b --- /dev/null +++ b/docs/ECR/ECR-021-ai-core-config.md @@ -0,0 +1,19 @@ +# ECR-021 + +**Title:** AICoreConfig · SystemPrompt(只读薄切片) +**Status:** **Closed** +**Closed:** 2026-08-08(Loop continuous) +**Parent:** WAVE0-FROZEN · **Predecessor:** ECR-020 Closed +**Change Level:** L2 + +## Change + +`system_prompts` + `GET /admin/ai/system-prompts*`;权限 `admin.ai_config.read`;admin-h5「AI」页。 + +## Forbidden + +Prompt 写发布 · Knowledge/Tools · UGC · 真支付 + +## Linked + +Spec `ops-ai-core-config.md` · BD-2026-021 · CONTRACT_DIFF/ECR-021.yaml · TEST_REPORT/ECR-021.md diff --git a/docs/ENGINEERING_SPEC/ECR-021-ai-core-config.md b/docs/ENGINEERING_SPEC/ECR-021-ai-core-config.md new file mode 100644 index 0000000..a6b4f12 --- /dev/null +++ b/docs/ENGINEERING_SPEC/ECR-021-ai-core-config.md @@ -0,0 +1,7 @@ +# ENGINEERING_SPEC — ECR-021 + +1. migration 000022 system_prompts + perm +2. AdminRepo list/get +3. Admin API + OpenAPI +4. admin-h5 /ai +5. Integration · Closed diff --git a/docs/HANDOFF/ECR-021-architect-to-engineer.md b/docs/HANDOFF/ECR-021-architect-to-engineer.md new file mode 100644 index 0000000..cbd5533 --- /dev/null +++ b/docs/HANDOFF/ECR-021-architect-to-engineer.md @@ -0,0 +1,3 @@ +# HANDOFF — ECR-021 Architect → Engineer + +Loop continuous · Approved + Coding. Migration 000022. Forbidden: Prompt写/UGC/真支付. diff --git a/docs/HANDOFF/ECR-021-engineer-to-reviewer.md b/docs/HANDOFF/ECR-021-engineer-to-reviewer.md new file mode 100644 index 0000000..bcccc2e --- /dev/null +++ b/docs/HANDOFF/ECR-021-engineer-to-reviewer.md @@ -0,0 +1,3 @@ +# HANDOFF — ECR-021 Engineer → Reviewer + +TestAICoreSystemPrompts PASS · /ai · Ready for Closed. diff --git a/docs/PRODUCT_SPEC/ECR-021-ai-core-config.md b/docs/PRODUCT_SPEC/ECR-021-ai-core-config.md new file mode 100644 index 0000000..6060903 --- /dev/null +++ b/docs/PRODUCT_SPEC/ECR-021-ai-core-config.md @@ -0,0 +1,3 @@ +# PRODUCT_SPEC — ECR-021 + +对齐 ops-ai-core-config.md · Approved · Loop · L2 · SystemPrompt 只读 diff --git a/docs/STATE/ECR-021.md b/docs/STATE/ECR-021.md new file mode 100644 index 0000000..6b3e416 --- /dev/null +++ b/docs/STATE/ECR-021.md @@ -0,0 +1,6 @@ +# STATE — ECR-021 + +| Status | **Closed** | +| Phase | closed | +| Spec | ops-ai-core-config.md | +| Updated | 2026-08-08 | diff --git a/docs/TASKS/TASK-021-ECR021.yaml b/docs/TASKS/TASK-021-ECR021.yaml new file mode 100644 index 0000000..e8d0261 --- /dev/null +++ b/docs/TASKS/TASK-021-ECR021.yaml @@ -0,0 +1,12 @@ +id: TASK-021-ECR021 +ecr: ECR-021 +title: AICoreConfig SystemPrompt read +role: engineer +status: closed +change_level: L2 +parent: WAVE0-FROZEN +predecessor: ECR-020 +acceptance: + - Spec AC mapped + - SystemPrompt read-only + - No Knowledge/Tools write / UGC / payment diff --git a/docs/TEST_REPORT/ECR-021.md b/docs/TEST_REPORT/ECR-021.md new file mode 100644 index 0000000..cfb6fa6 --- /dev/null +++ b/docs/TEST_REPORT/ECR-021.md @@ -0,0 +1,32 @@ +# TEST_REPORT — ECR-021 AICoreConfig + +Date: 2026-08-08 · Loop continuous · commit: (pending) + +## Commands + +```bash +cd apps/api && go test ./internal/integration/ -run TestAICoreSystemPrompts -count=1 +npm run build:admin +python3 scripts/ess-validate.py --phase review --ecr ECR-021 +python3 scripts/ess-gate-check.py --ecr ECR-021 +``` + +## Results + +| Check | Result | +|-------|--------| +| TestAICoreSystemPrompts | PASS | +| build:admin | PASS | +| ess-validate review | PASS | + +## AC + +| ID | Evidence | +|----|----------| +| AC-F-01 | list 含 ask_default | +| AC-F-02 | get body 非空 | +| AC-F-03 | 未知 id → 404 | +| AC-S-01 | 无 token → 401 | +| AC-S-02 | 仅 users.read → 403 | +| AC-P-01 | list < 500ms | +| AC-O-01 | N/A 只读 | diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 1858d4a..df3582c 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -25,3 +25,4 @@ | ECR-018 | Entitlement | **Closed** | Spec ops-entitlement · BD-2026-018 · TEST_REPORT · CODE_REVIEW · Loop continuous | | ECR-019 | ContentSafety | **Closed** | Spec ops-content-safety · BD-2026-019 · migration 000020 · TEST_REPORT · CODE_REVIEW · Loop continuous | | ECR-020 | QualityFeedback | **Closed** | Spec ops-quality-feedback · BD-2026-020 · migration 000021 · TEST_REPORT · CODE_REVIEW · Loop continuous | +| ECR-021 | AICoreConfig | **Closed** | Spec ops-ai-core-config · BD-2026-021 · migration 000022 · TEST_REPORT · CODE_REVIEW · Loop continuous | diff --git a/docs/WAVE0/LOOP_AUTHORIZATION.md b/docs/WAVE0/LOOP_AUTHORIZATION.md index ce02a22..003c7d9 100644 --- a/docs/WAVE0/LOOP_AUTHORIZATION.md +++ b/docs/WAVE0/LOOP_AUTHORIZATION.md @@ -30,4 +30,4 @@ Human 明文:**直接用 Loop,不用人工确认。** | Done | Next | |------|------| -| ECR-013A…020 Closed | **ECR-021** AICoreConfig 薄切片(SystemPrompt 只读优先);禁真支付/UGC | +| ECR-013A…021 Closed | **ECR-022** CrisisCare 薄切片(CrisisPolicy 只读优先);禁真支付/UGC | diff --git a/proto/openapi.yaml b/proto/openapi.yaml index 3f3a227..431d275 100644 --- a/proto/openapi.yaml +++ b/proto/openapi.yaml @@ -527,6 +527,34 @@ paths: '200': description: OK + /api/v1/admin/ai/system-prompts: + get: + tags: [admin] + summary: List SystemPrompt catalog + description: Requires admin.ai_config.read + responses: + '200': + description: OK + '401': + description: Unauthorized + '403': + description: Forbidden + + /api/v1/admin/ai/system-prompts/{id}: + get: + tags: [admin] + summary: Get SystemPrompt + parameters: + - in: path + name: id + required: true + schema: { type: string, format: uuid } + responses: + '200': + description: OK + '404': + description: Not found + /api/v1/admin/orders: get: tags: [admin]