feat(plan): 添加 P1-P4 四仓详细执行计划
This commit is contained in:
parent
e0834cf4f8
commit
4c02ab99fe
968
docs/superpowers/plans/2026-05-24-P1-muse-cloud.md
Normal file
968
docs/superpowers/plans/2026-05-24-P1-muse-cloud.md
Normal file
@ -0,0 +1,968 @@
|
||||
# P1: muse-cloud 后端搭建 — 执行计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**目标:** 在 Yudao Cloud fork 基础上搭建 6 个 Muse 业务模块,实现全量 API(229 接口),提供可调用的后端服务。
|
||||
|
||||
**架构:** Yudao Cloud 微服务 + PostgreSQL 16 + Flyway 迁移。6 个业务模块按依赖顺序实现:content+account → meta → ai+knowledge → market。Java 21 + Spring Boot 3 + JUnit 5 + Testcontainers。
|
||||
|
||||
**技术栈:** Java 21, Spring Boot 3, PostgreSQL 16, Flyway, Maven, Docker Compose, JUnit 5, Testcontainers, ArchUnit
|
||||
|
||||
---
|
||||
|
||||
## Step 1: 基础环境搭建
|
||||
|
||||
### Task 1.1: 确认 Yudao Cloud Fork 状态
|
||||
|
||||
**仓库路径:** `muse-cloud/`
|
||||
|
||||
- [ ] **Step 1: 检查当前模块结构**
|
||||
|
||||
```bash
|
||||
ls muse-cloud/muse-module-*/pom.xml
|
||||
```
|
||||
|
||||
预期输出:
|
||||
```
|
||||
muse-cloud/muse-module-ai/muse-module-ai-api/pom.xml
|
||||
muse-cloud/muse-module-ai/muse-module-ai-server/pom.xml
|
||||
muse-cloud/muse-module-system/muse-module-system-api/pom.xml
|
||||
muse-cloud/muse-module-system/muse-module-system-server/pom.xml
|
||||
muse-cloud/muse-module-infra/.../pom.xml
|
||||
muse-cloud/muse-module-bpm/.../pom.xml
|
||||
muse-cloud/muse-module-member/.../pom.xml
|
||||
muse-cloud/muse-module-mp/.../pom.xml
|
||||
muse-cloud/muse-module-pay/.../pom.xml
|
||||
muse-cloud/muse-module-report/.../pom.xml
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 确认包名是否已改为 com.muse**
|
||||
|
||||
```bash
|
||||
find muse-cloud -name "*.java" -path "*/com/muse/*" | head -10
|
||||
```
|
||||
|
||||
若仍为 `cn.iocoder.yudao`,需全局替换。
|
||||
|
||||
- [ ] **Step 3: 确认 pom.xml 中 Java 版本**
|
||||
|
||||
```bash
|
||||
grep -r "java.version\|maven.compiler" muse-cloud/pom.xml | head -5
|
||||
```
|
||||
|
||||
确保为 Java 21。
|
||||
|
||||
- [ ] **Step 4: 验证可编译**
|
||||
|
||||
```bash
|
||||
cd muse-cloud && mvn compile -q 2>&1 | tail -5
|
||||
```
|
||||
|
||||
### Task 1.2: Docker Compose 开发环境
|
||||
|
||||
**文件:**
|
||||
- 创建: `muse-cloud/docker-compose.yml`
|
||||
|
||||
- [ ] **Step 1: 编写 docker-compose.yml**
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: muse
|
||||
POSTGRES_PASSWORD: muse_dev
|
||||
POSTGRES_DB: muse
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U muse"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 启动并验证**
|
||||
|
||||
```bash
|
||||
cd muse-cloud && docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
预期输出: postgres 和 redis 均为 `running` 状态。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add docker-compose.yml
|
||||
git commit -m "feat(infra): 添加 Docker Compose 开发环境(PostgreSQL 16 + Redis 8)"
|
||||
```
|
||||
|
||||
### Task 1.3: 调整 Yudao 配置适配 PostgreSQL
|
||||
|
||||
**文件:**
|
||||
- 修改: `muse-cloud/muse-server/src/main/resources/application-local.yaml`
|
||||
|
||||
- [ ] **Step 1: 确认 PostgreSQL 数据源配置**
|
||||
|
||||
```bash
|
||||
grep -A10 "datasource" muse-cloud/muse-server/src/main/resources/application-local.yaml
|
||||
```
|
||||
|
||||
确保 `url` 为 `jdbc:postgresql://localhost:5432/muse`,driver 为 `org.postgresql.Driver`。
|
||||
|
||||
- [ ] **Step 2: 提交**
|
||||
|
||||
```bash
|
||||
git add muse-cloud/muse-server/src/main/resources/application-local.yaml
|
||||
git commit -m "chore(config): 确认 PostgreSQL 数据源配置"
|
||||
```
|
||||
|
||||
### Task 1.4: 配置 .idea 团队共享
|
||||
|
||||
**文件:**
|
||||
- 创建: `muse-cloud/.idea/` 下多个配置文件
|
||||
|
||||
- [ ] **Step 1: 创建 .idea/.gitignore**
|
||||
|
||||
```gitignore
|
||||
# 团队共享 IDE 配置
|
||||
# 个人配置不入库
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
/httpRequests/
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建代码风格配置**
|
||||
|
||||
```bash
|
||||
mkdir -p muse-cloud/.idea/codeStyles
|
||||
cat > muse-cloud/.idea/codeStyles/Project.xml << 'XML'
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
|
||||
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||
<value />
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
XML
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add muse-cloud/.idea/
|
||||
git commit -m "chore(ide): 添加团队共享 .idea 配置"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: 数据库 Migration
|
||||
|
||||
### Task 2.1: 编写全量 DDL
|
||||
|
||||
**文件:**
|
||||
- 创建: `muse-cloud/sql/muse/V1__init_content_schema.sql`
|
||||
- 创建: `muse-cloud/sql/muse/V2__init_account_schema.sql`
|
||||
- 创建: `muse-cloud/sql/muse/V3__init_meta_schema.sql`
|
||||
- 创建: `muse-cloud/sql/muse/V4__init_ai_schema.sql`
|
||||
- 创建: `muse-cloud/sql/muse/V5__init_knowledge_schema.sql`
|
||||
- 创建: `muse-cloud/sql/muse/V6__init_market_schema.sql`
|
||||
- 参考: `design-docs/后端-04-统一数据库Schema-v1.md`
|
||||
- 参考: `design-docs/后端-04a-完整建表SQL.sql`
|
||||
|
||||
- [ ] **Step 1: 创建 Migration 目录**
|
||||
|
||||
```bash
|
||||
mkdir -p muse-cloud/sql/muse
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写 Content 模块 DDL (V1)**
|
||||
|
||||
从 `design-docs/后端-04a-完整建表SQL.sql` 提取 Content 相关表(Work, Chapter, Block, BlockSourceAttribution 等),调整表名为 Yudao 约定(加 `muse_` 前缀),添加必备审计字段。
|
||||
|
||||
```sql
|
||||
-- V1__init_content_schema.sql
|
||||
-- Muse Content 模块初始 Schema
|
||||
-- 对应设计文档: design-docs/后端-04-统一数据库Schema-v1.md
|
||||
|
||||
-- ============================================================
|
||||
-- 作品表
|
||||
-- ============================================================
|
||||
CREATE TABLE muse_work (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description VARCHAR(2000),
|
||||
genre VARCHAR(100),
|
||||
cover_image_url VARCHAR(500),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
||||
word_count INT NOT NULL DEFAULT 0,
|
||||
chapter_count INT NOT NULL DEFAULT 0,
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted BIT NOT NULL DEFAULT FALSE,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_work_creator ON muse_work(creator);
|
||||
CREATE INDEX idx_muse_work_status ON muse_work(status);
|
||||
|
||||
-- ============================================================
|
||||
-- 章节表
|
||||
-- ============================================================
|
||||
CREATE TABLE muse_chapter (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
work_id BIGINT NOT NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
||||
word_count INT NOT NULL DEFAULT 0,
|
||||
block_count INT NOT NULL DEFAULT 0,
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted BIT NOT NULL DEFAULT FALSE,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_chapter_work ON muse_chapter(work_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Block 表
|
||||
-- ============================================================
|
||||
CREATE TABLE muse_block (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
chapter_id BIGINT NOT NULL,
|
||||
block_type VARCHAR(20) NOT NULL,
|
||||
title VARCHAR(500),
|
||||
content MEDIUMTEXT,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
revision INT NOT NULL DEFAULT 1,
|
||||
word_count INT NOT NULL DEFAULT 0,
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted BIT NOT NULL DEFAULT FALSE,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_block_chapter ON muse_block(chapter_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Block 来源归因表
|
||||
-- ============================================================
|
||||
CREATE TABLE muse_block_source_attribution (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
block_id BIGINT NOT NULL,
|
||||
revision INT NOT NULL,
|
||||
source_type VARCHAR(50) NOT NULL,
|
||||
source_id VARCHAR(100),
|
||||
source_version INT,
|
||||
authorization_snapshot_id VARCHAR(100),
|
||||
lineage JSON,
|
||||
license_restrictions JSON,
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_block_src_block ON muse_block_source_attribution(block_id);
|
||||
CREATE INDEX idx_muse_block_src_revision ON muse_block_source_attribution(block_id, revision);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 编写 Account 模块 DDL (V2)**
|
||||
|
||||
```sql
|
||||
-- V2__init_account_schema.sql
|
||||
-- Muse Account 模块初始 Schema
|
||||
|
||||
-- 权益表(可变表 + 审计日志)
|
||||
CREATE TABLE muse_entitlement (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
resource_type VARCHAR(100) NOT NULL,
|
||||
limit_value BIGINT NOT NULL,
|
||||
used_value BIGINT NOT NULL DEFAULT 0,
|
||||
expires_at DATETIME,
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted BIT NOT NULL DEFAULT FALSE,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_entitlement_user ON muse_entitlement(user_id);
|
||||
|
||||
-- 权益审计日志表
|
||||
CREATE TABLE muse_entitlement_audit_log (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
resource_type VARCHAR(100) NOT NULL,
|
||||
operation_type VARCHAR(20) NOT NULL,
|
||||
before_snapshot JSON,
|
||||
after_snapshot JSON,
|
||||
reason VARCHAR(500),
|
||||
command_id VARCHAR(100) NOT NULL,
|
||||
correlation_id VARCHAR(100),
|
||||
operator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_ent_audit_user ON muse_entitlement_audit_log(user_id);
|
||||
CREATE INDEX idx_muse_ent_audit_cmd ON muse_entitlement_audit_log(command_id);
|
||||
|
||||
-- 配额表
|
||||
CREATE TABLE muse_quota (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
resource_type VARCHAR(100) NOT NULL,
|
||||
total INT NOT NULL DEFAULT 0,
|
||||
remaining INT NOT NULL DEFAULT 0,
|
||||
reset_at DATETIME NOT NULL,
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted BIT NOT NULL DEFAULT FALSE,
|
||||
tenant_id BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_quota_user ON muse_quota(user_id);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写 Meta 模块 DDL (V3)**
|
||||
|
||||
```sql
|
||||
-- V3__init_meta_schema.sql
|
||||
-- MetaSchema 模块初始 Schema
|
||||
|
||||
CREATE TABLE muse_meta_schema (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
schema_key VARCHAR(100) NOT NULL,
|
||||
display_name VARCHAR(200) NOT NULL,
|
||||
field_type VARCHAR(20) NOT NULL,
|
||||
scope VARCHAR(20) NOT NULL,
|
||||
active_version INT NOT NULL DEFAULT 1,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updater VARCHAR(64) NOT NULL DEFAULT '',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted BIT NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_schema_key (schema_key)
|
||||
);
|
||||
|
||||
CREATE TABLE muse_meta_schema_version (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
schema_id BIGINT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
default_value JSON,
|
||||
is_required BIT NOT NULL DEFAULT FALSE,
|
||||
enum_values JSON,
|
||||
validation_rules JSON,
|
||||
ui_visible BIT NOT NULL DEFAULT TRUE,
|
||||
ai_context BIT NOT NULL DEFAULT FALSE,
|
||||
user_editable BIT NOT NULL DEFAULT TRUE,
|
||||
user_searchable BIT NOT NULL DEFAULT FALSE,
|
||||
exportable BIT NOT NULL DEFAULT FALSE,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
||||
change_note VARCHAR(500),
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_schema_version (schema_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_muse_meta_schema_ver_sid ON muse_meta_schema_version(schema_id);
|
||||
|
||||
CREATE TABLE muse_protection_node (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
node_key VARCHAR(100) NOT NULL,
|
||||
display_name VARCHAR(200) NOT NULL,
|
||||
required_permissions JSON,
|
||||
audit_required BIT NOT NULL DEFAULT TRUE,
|
||||
irremovable_reason VARCHAR(500),
|
||||
shadow_canonical_boundary VARCHAR(500),
|
||||
creator VARCHAR(64) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_protection_node_key (node_key)
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 5-7: 编写 AI, Knowledge, Market 模块 DDL (V4-V6)**
|
||||
|
||||
从 `design-docs/后端-04a-完整建表SQL.sql` 提取对应表结构,按 Yudao 约定(`muse_` 前缀、审计字段、租户字段)调整。
|
||||
|
||||
关键表:
|
||||
- AI: `muse_ai_generation`, `muse_ai_suggestion`, `muse_agent`, `muse_agent_slot_binding`, `muse_prompt`, `muse_quality_policy`
|
||||
- Knowledge: `muse_knowledge_base`, `muse_knowledge_document`, `muse_knowledge_entity`, `muse_knowledge_relation`, `muse_knowledge_draft`, `muse_knowledge_binding`
|
||||
- Market: `muse_market_asset`, `muse_market_installation`, `muse_market_publish_request`, `muse_market_appeal`
|
||||
|
||||
- [ ] **Step 8: 配置 Flyway**
|
||||
|
||||
修改 `muse-cloud/muse-server/src/main/resources/application.yaml`,确保 Flyway 启用并指向 `sql/muse` 目录。
|
||||
|
||||
- [ ] **Step 9: 运行 Migration 并验证**
|
||||
|
||||
```bash
|
||||
cd muse-cloud && mvn flyway:migrate -pl muse-server
|
||||
```
|
||||
|
||||
验证: 所有表创建成功,索引正确。
|
||||
|
||||
- [ ] **Step 10: 提交**
|
||||
|
||||
```bash
|
||||
git add muse-cloud/sql/
|
||||
git commit -m "feat(db): 添加 6 个模块全量 DDL + Flyway 迁移配置"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: 业务模块骨架
|
||||
|
||||
### Task 3.1: 创建 Content 模块
|
||||
|
||||
**文件:**
|
||||
- 创建: `muse-cloud/muse-module-content/pom.xml`
|
||||
- 创建: `muse-cloud/muse-module-content/muse-module-content-api/pom.xml`
|
||||
- 创建: `muse-cloud/muse-module-content/muse-module-content-server/pom.xml`
|
||||
- 创建: API 接口包 `com.muse.module.content.api`
|
||||
- 创建: Server 包结构 `com.muse.module.content.controller/service/dal`
|
||||
|
||||
- [ ] **Step 1: 创建父 pom.xml**
|
||||
|
||||
```xml
|
||||
<!-- muse-cloud/muse-module-content/pom.xml -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.muse</groupId>
|
||||
<artifactId>muse</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<artifactId>muse-module-content</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Muse Module Content</name>
|
||||
<description>作品、章节、Block、导出模块</description>
|
||||
<modules>
|
||||
<module>muse-module-content-api</module>
|
||||
<module>muse-module-content-server</module>
|
||||
</modules>
|
||||
</project>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 api 子模块 pom.xml**
|
||||
|
||||
```xml
|
||||
<!-- muse-cloud/muse-module-content/muse-module-content-api/pom.xml -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.muse</groupId>
|
||||
<artifactId>muse-module-content</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<artifactId>muse-module-content-api</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Muse Module Content API</name>
|
||||
<description>Content 模块 API 接口定义(Feign 接口 + DTO)</description>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.muse</groupId>
|
||||
<artifactId>muse-common</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 创建 server 子模块 pom.xml**
|
||||
|
||||
```xml
|
||||
<!-- muse-cloud/muse-module-content/muse-module-content-server/pom.xml -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.muse</groupId>
|
||||
<artifactId>muse-module-content</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<artifactId>muse-module-content-server</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Muse Module Content Server</name>
|
||||
<description>Content 模块服务实现</description>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.muse</groupId>
|
||||
<artifactId>muse-module-content-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.muse</groupId>
|
||||
<artifactId>muse-module-system-api</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Starter Web, MyBatis, PostgreSQL, Testcontainers -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 创建 API 接口类 (Feign)**
|
||||
|
||||
```java
|
||||
// muse-module-content/muse-module-content-api/src/main/java/com/muse/module/content/api/ContentApi.java
|
||||
package com.muse.module.content.api;
|
||||
|
||||
import com.muse.module.content.api.dto.*;
|
||||
import com.muse.common.result.CommonResult;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Content 模块 Feign API 接口
|
||||
* 对应 OpenAPI: content/openapi.yaml
|
||||
*/
|
||||
@FeignClient(name = "muse-module-content")
|
||||
public interface ContentApi {
|
||||
|
||||
// ===== Work =====
|
||||
@GetMapping("/app-api/muse/works")
|
||||
CommonResult<PageResult<WorkSummaryDTO>> listWorks(
|
||||
@RequestParam("pageNo") Integer pageNo,
|
||||
@RequestParam("pageSize") Integer pageSize,
|
||||
@RequestParam(value = "status", required = false) String status
|
||||
);
|
||||
|
||||
@PostMapping("/app-api/muse/works")
|
||||
CommonResult<WorkDTO> createWork(@RequestBody CreateWorkRequest request);
|
||||
|
||||
@GetMapping("/app-api/muse/works/{workId}")
|
||||
CommonResult<WorkDTO> getWork(@PathVariable("workId") Long workId);
|
||||
|
||||
@PutMapping("/app-api/muse/works/{workId}")
|
||||
CommonResult<Void> updateWork(@PathVariable("workId") Long workId, @RequestBody UpdateWorkRequest request);
|
||||
|
||||
@DeleteMapping("/app-api/muse/works/{workId}")
|
||||
CommonResult<Void> deleteWork(@PathVariable("workId") Long workId);
|
||||
|
||||
// ===== Chapter =====
|
||||
@GetMapping("/app-api/muse/works/{workId}/chapters")
|
||||
CommonResult<List<ChapterDTO>> listChapters(@PathVariable("workId") Long workId);
|
||||
|
||||
@PostMapping("/app-api/muse/works/{workId}/chapters")
|
||||
CommonResult<ChapterDTO> createChapter(@PathVariable("workId") Long workId, @RequestBody CreateChapterRequest request);
|
||||
|
||||
@GetMapping("/app-api/muse/chapters/{chapterId}")
|
||||
CommonResult<ChapterDetailDTO> getChapter(@PathVariable("chapterId") Long chapterId);
|
||||
|
||||
@PutMapping("/app-api/muse/chapters/{chapterId}")
|
||||
CommonResult<Void> updateChapter(@PathVariable("chapterId") Long chapterId, @RequestBody UpdateChapterRequest request);
|
||||
|
||||
@DeleteMapping("/app-api/muse/chapters/{chapterId}")
|
||||
CommonResult<Void> deleteChapter(@PathVariable("chapterId") Long chapterId);
|
||||
|
||||
// ===== Block =====
|
||||
@GetMapping("/app-api/muse/chapters/{chapterId}/blocks")
|
||||
CommonResult<List<BlockDTO>> listBlocks(@PathVariable("chapterId") Long chapterId);
|
||||
|
||||
@PostMapping("/app-api/muse/chapters/{chapterId}/blocks")
|
||||
CommonResult<BlockDTO> createBlock(@PathVariable("chapterId") Long chapterId, @RequestBody CreateBlockRequest request);
|
||||
|
||||
@PutMapping("/app-api/muse/blocks/{blockId}")
|
||||
CommonResult<SaveBlockResultDTO> saveBlock(@PathVariable("blockId") Long blockId, @RequestBody SaveBlockRequest request);
|
||||
|
||||
@DeleteMapping("/app-api/muse/blocks/{blockId}")
|
||||
CommonResult<Void> deleteBlock(@PathVariable("blockId") Long blockId);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 创建 DTO 类**
|
||||
|
||||
从 `docs/api-contracts/generated/java/com/muse/dto/` 复制 Content 相关 DTO 到 `muse-module-content-api/src/main/java/com/muse/module/content/api/dto/`。
|
||||
|
||||
- [ ] **Step 6: 创建 Server 包结构**
|
||||
|
||||
```bash
|
||||
mkdir -p muse-cloud/muse-module-content/muse-module-content-server/src/main/java/com/muse/module/content/{controller,service,dal/{mysql,convert}}
|
||||
mkdir -p muse-cloud/muse-module-content/muse-module-content-server/src/test/java/com/muse/module/content
|
||||
```
|
||||
|
||||
- [ ] **Step 7: 在父 pom.xml 注册模块**
|
||||
|
||||
```bash
|
||||
# 在 muse-cloud/pom.xml 的 <modules> 中添加
|
||||
# <module>muse-module-content</module>
|
||||
```
|
||||
|
||||
- [ ] **Step 8: 编译验证**
|
||||
|
||||
```bash
|
||||
cd muse-cloud && mvn compile -pl muse-module-content -am
|
||||
```
|
||||
|
||||
- [ ] **Step 9: 提交**
|
||||
|
||||
```bash
|
||||
git add muse-cloud/muse-module-content/ muse-cloud/pom.xml
|
||||
git commit -m "feat(content): 创建 Content 模块骨架(api+server)"
|
||||
```
|
||||
|
||||
### Task 3.2-3.6: 创建其余 5 个业务模块骨架
|
||||
|
||||
按相同模式创建 Account, Meta, AI, Knowledge, Market 五个模块。每个模块:
|
||||
1. 创建 api/server 子模块 pom.xml
|
||||
2. 创建 Feign API 接口类(对齐 OpenAPI 定义)
|
||||
3. 从 `docs/api-contracts/generated/java/com/muse/dto/` 复制 DTO
|
||||
4. 创建 server 包结构(controller/service/dal)
|
||||
5. 在父 pom.xml 注册
|
||||
6. 编译验证
|
||||
|
||||
每个模块单独提交。
|
||||
|
||||
---
|
||||
|
||||
## Step 4: API 实现(按依赖顺序)
|
||||
|
||||
### Task 4.1: Content API 实现
|
||||
|
||||
**依赖:** system(用户认证)
|
||||
|
||||
- [ ] **Step 1: 编写 WorkService**
|
||||
|
||||
```java
|
||||
// muse-module-content-server/src/main/java/com/muse/module/content/service/WorkService.java
|
||||
package com.muse.module.content.service;
|
||||
|
||||
import com.muse.module.content.api.dto.*;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
/**
|
||||
* 作品服务接口
|
||||
*/
|
||||
public interface WorkService {
|
||||
Page<WorkSummaryDTO> listWorks(Long userId, Integer pageNo, Integer pageSize, String status);
|
||||
WorkDTO createWork(Long userId, CreateWorkRequest request);
|
||||
WorkDTO getWork(Long workId);
|
||||
void updateWork(Long workId, UpdateWorkRequest request);
|
||||
void deleteWork(Long workId);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写 WorkService 实现**
|
||||
|
||||
```java
|
||||
// muse-module-content-server/src/main/java/com/muse/module/content/service/impl/WorkServiceImpl.java
|
||||
package com.muse.module.content.service.impl;
|
||||
|
||||
import com.muse.module.content.dal.mysql.WorkMapper;
|
||||
import com.muse.module.content.dal.dataobject.WorkDO;
|
||||
import com.muse.module.content.service.WorkService;
|
||||
import com.muse.module.content.api.dto.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class WorkServiceImpl implements WorkService {
|
||||
|
||||
private final WorkMapper workMapper;
|
||||
|
||||
public WorkServiceImpl(WorkMapper workMapper) {
|
||||
this.workMapper = workMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<WorkSummaryDTO> listWorks(Long userId, Integer pageNo, Integer pageSize, String status) {
|
||||
// 分页查询作品列表
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public WorkDTO createWork(Long userId, CreateWorkRequest request) {
|
||||
WorkDO work = new WorkDO();
|
||||
work.setTitle(request.getTitle());
|
||||
work.setDescription(request.getDescription());
|
||||
work.setGenre(request.getGenre());
|
||||
work.setCreator(String.valueOf(userId));
|
||||
workMapper.insert(work);
|
||||
return convertToDTO(work);
|
||||
}
|
||||
|
||||
// ... 其余方法
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 编写 WorkMapper (MyBatis)**
|
||||
|
||||
```java
|
||||
// muse-module-content-server/src/main/java/com/muse/module/content/dal/mysql/WorkMapper.java
|
||||
package com.muse.module.content.dal.mysql;
|
||||
|
||||
import com.muse.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.muse.module.content.dal.dataobject.WorkDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface WorkMapper extends BaseMapperX<WorkDO> {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写 WorkController**
|
||||
|
||||
```java
|
||||
// muse-module-content-server/src/main/java/com/muse/module/content/controller/WorkController.java
|
||||
package com.muse.module.content.controller;
|
||||
|
||||
import com.muse.module.content.service.WorkService;
|
||||
import com.muse.module.content.api.dto.*;
|
||||
import com.muse.common.result.CommonResult;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/app-api/muse/works")
|
||||
public class WorkController {
|
||||
|
||||
private final WorkService workService;
|
||||
|
||||
public WorkController(WorkService workService) {
|
||||
this.workService = workService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public CommonResult<PageResult<WorkSummaryDTO>> listWorks(
|
||||
@RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(defaultValue = "20") Integer pageSize,
|
||||
@RequestParam(required = false) String status
|
||||
) {
|
||||
Long userId = SecurityUtils.getLoginUserId();
|
||||
return CommonResult.success(workService.listWorks(userId, pageNo, pageSize, status));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public CommonResult<WorkDTO> createWork(@RequestBody CreateWorkRequest request) {
|
||||
Long userId = SecurityUtils.getLoginUserId();
|
||||
return CommonResult.success(workService.createWork(userId, request));
|
||||
}
|
||||
|
||||
// ... 其余端点
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 编写集成测试**
|
||||
|
||||
```java
|
||||
// muse-module-content-server/src/test/java/com/muse/module/content/WorkControllerTest.java
|
||||
package com.muse.module.content;
|
||||
|
||||
import com.muse.module.content.api.dto.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Testcontainers
|
||||
class WorkControllerTest {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
|
||||
.withDatabaseName("muse_test")
|
||||
.withUsername("test")
|
||||
.withPassword("test");
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Test
|
||||
void shouldCreateAndGetWork() {
|
||||
// 创建作品
|
||||
CreateWorkRequest request = new CreateWorkRequest();
|
||||
request.setTitle("测试作品");
|
||||
|
||||
ResponseEntity<CommonResult> createResp = restTemplate.postForEntity(
|
||||
"/app-api/muse/works", request, CommonResult.class
|
||||
);
|
||||
assertEquals(HttpStatus.OK, createResp.getStatusCode());
|
||||
assertNotNull(createResp.getBody().getData());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 运行测试**
|
||||
|
||||
```bash
|
||||
cd muse-cloud && mvn test -pl muse-module-content/muse-module-content-server
|
||||
```
|
||||
|
||||
预期: 集成测试通过。
|
||||
|
||||
- [ ] **Step 7: 提交**
|
||||
|
||||
```bash
|
||||
git add muse-cloud/muse-module-content/
|
||||
git commit -m "feat(content): Content API 实现(Work CRUD + 集成测试)"
|
||||
```
|
||||
|
||||
### Task 4.2: Chapter/Block API 实现
|
||||
|
||||
按相同 TDD 模式实现章节和 Block CRUD API:
|
||||
1. 编写测试(验证 CRUD + 乐观锁冲突 + 排序逻辑)
|
||||
2. 编写 Service + Controller + Mapper
|
||||
3. 测试通过
|
||||
4. 提交
|
||||
|
||||
### Task 4.3: Block 保存(乐观锁 + 版本管理)
|
||||
|
||||
核心:`PUT /app-api/muse/blocks/{blockId}` 实现乐观锁更新。
|
||||
|
||||
```java
|
||||
@Transactional
|
||||
public SaveBlockResultDTO saveBlock(Long userId, Long blockId, SaveBlockRequest request) {
|
||||
// 1. 查询当前 Block
|
||||
BlockDO block = blockMapper.selectById(blockId);
|
||||
if (block == null) throw new ResourceNotFoundException("Block not found");
|
||||
|
||||
// 2. 乐观锁校验:expectedRevision 必须匹配
|
||||
if (!request.getExpectedRevision().equals(block.getRevision())) {
|
||||
throw new RevisionConflictException(block.getRevision());
|
||||
}
|
||||
|
||||
// 3. 更新 Block 内容和版本号
|
||||
block.setContent(request.getContent());
|
||||
block.setRevision(block.getRevision() + 1);
|
||||
block.setUpdater(String.valueOf(userId));
|
||||
blockMapper.updateById(block);
|
||||
|
||||
// 4. 写入来源归因
|
||||
BlockSourceAttributionDO attribution = new BlockSourceAttributionDO();
|
||||
attribution.setBlockId(blockId);
|
||||
attribution.setRevision(block.getRevision());
|
||||
attribution.setSourceType(request.getSourceType());
|
||||
sourceAttributionMapper.insert(attribution);
|
||||
|
||||
// 5. 返回新版本号
|
||||
SaveBlockResultDTO result = new SaveBlockResultDTO();
|
||||
result.setNewRevision(block.getRevision());
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Task 4.4-4.11: 其余 Content 接口实现
|
||||
|
||||
按 OpenAPI 顺序逐步实现:
|
||||
4.4: Source Attribution 接口
|
||||
4.5: Meta Projections 接口
|
||||
4.6: Planning 接口(规划查询/保存/候选创建/确认/丢弃)
|
||||
4.7: Style Check 接口
|
||||
4.8: Import 接口(上传/解析/确认/批量确认)
|
||||
4.9: Export 接口(创建任务/查询/下载)
|
||||
4.10: Admin Content 接口(列表/详情/风险治理)
|
||||
4.11: apply-suggestion 接口(接受 AI 候选后写入 Canonical)
|
||||
|
||||
每个子任务:TDD → 实现 → 测试通过 → 提交。
|
||||
|
||||
### Task 4.12: Account API 实现
|
||||
|
||||
**依赖:** content 模块不相关,可独立开始。
|
||||
|
||||
实现权益查询、配额查询、用量摘要、安全事件、个人中心等接口。
|
||||
每个子任务按 TDD 模式:测试 → Service → Controller → Mapper → 验证 → 提交。
|
||||
|
||||
### Task 4.13: Meta API 实现
|
||||
|
||||
**依赖:** content + account 已稳定。
|
||||
|
||||
实现 MetaSchema CRUD、版本管理、草稿/校验/影响预览/发布/激活/回滚/废弃/灰度、保护节点查询、功能链路管理。
|
||||
|
||||
### Task 4.14: AI API 实现
|
||||
|
||||
**依赖:** content + meta 已稳定。
|
||||
|
||||
实现 AI 任务提交、SSE 流式输出、候选管理(列表/详情/接受/拒绝)、智能体管理(创建/版本/试用)、槽位绑定、Admin Prompt/Agent/质量策略管理、来源状态查询/重验。
|
||||
|
||||
### Task 4.15: Knowledge API 实现
|
||||
|
||||
**依赖:** content + meta 已稳定。
|
||||
|
||||
实现知识实体/关系、自动确认逻辑、全局KB管理、用户KB管理、文档上传/解析、发布流程、知识草稿确认/忽略、知识绑定/解绑。
|
||||
|
||||
### Task 4.16: Market API 实现
|
||||
|
||||
**依赖:** account + ai + knowledge 已稳定。
|
||||
|
||||
实现市场资产浏览/推荐、收藏、购买/安装、Handoff 创建/消费、发布草稿/检查/提交、管理端审核/下架/召回、申诉处理。
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 6 个模块全部 API 自测通过
|
||||
- [ ] Domain 层单元测试覆盖率 ≥90%(JUnit 5 + Mockito)
|
||||
- [ ] Application 层集成测试覆盖率 ≥80%(Testcontainers + PostgreSQL)
|
||||
- [ ] ArchUnit 规则通过(AI grant/runtime 包隔离检查)
|
||||
- [ ] Checkstyle / SpotBugs lint 通过
|
||||
- [ ] Maven build 成功(含所有测试)
|
||||
786
docs/superpowers/plans/2026-05-24-P2-muse-studio.md
Normal file
786
docs/superpowers/plans/2026-05-24-P2-muse-studio.md
Normal file
@ -0,0 +1,786 @@
|
||||
# P2: muse-studio 用户端搭建 — 执行计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**目标:** 从零搭建 muse-studio React SPA,实现写作台、作品管理、知识库、智能体、市场、个人中心 6 个功能域。
|
||||
|
||||
**架构:** Vite + React + TypeScript SPA,TanStack Query 数据层,Zustand 状态管理,Tiptap/ProseMirror 编辑器,MSW Mock API,SSE 双通道实时通信,IndexedDB 安全网。
|
||||
|
||||
**技术栈:** React 19, TypeScript 5, Vite 6, TanStack Query 5, Zustand 4, React Hook Form 7 + Zod 3, Tiptap 2, Tailwind CSS 3, MSW 2, Vitest + Testing Library + Playwright
|
||||
|
||||
---
|
||||
|
||||
## Step 1: 工程脚手架
|
||||
|
||||
### Task 1.1: 初始化 Vite + React + TS 项目
|
||||
|
||||
**仓库路径:** `muse-studio/`(新建)
|
||||
|
||||
- [ ] **Step 1: 创建项目**
|
||||
|
||||
```bash
|
||||
npm create vite@latest muse-studio -- --template react-ts
|
||||
cd muse-studio
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 安装核心依赖**
|
||||
|
||||
```bash
|
||||
pnpm add react@19 react-dom@19 react-router-dom@7
|
||||
pnpm add @tanstack/react-query@5
|
||||
pnpm add zustand@4
|
||||
pnpm add react-hook-form@7 zod@3 @hookform/resolvers@3
|
||||
pnpm add @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-placeholder
|
||||
pnpm add -D tailwindcss@3 postcss autoprefixer
|
||||
pnpm add -D @types/react @types/react-dom
|
||||
pnpm add -D vitest@2 @testing-library/react @testing-library/jest-dom jsdom
|
||||
pnpm add -D eslint@9 prettier @typescript-eslint/parser
|
||||
pnpm add -D msw@2
|
||||
pnpm add -D playwright@1 @playwright/test
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 配置 Vite**
|
||||
|
||||
```typescript
|
||||
// muse-studio/vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/app-api': {
|
||||
target: 'http://localhost:48080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
css: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 创建目录结构**
|
||||
|
||||
```bash
|
||||
mkdir -p muse-studio/src/{features/{editor,agent,knowledge,market,account},components/{ui,layout},hooks,stores,lib,test,types}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 配置 Tailwind**
|
||||
|
||||
```bash
|
||||
npx tailwindcss init -p
|
||||
```
|
||||
|
||||
```javascript
|
||||
// muse-studio/tailwind.config.js
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: { extend: {} },
|
||||
plugins: [],
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git init && git add -A && git commit -m "feat(init): 初始化 Vite + React + TS 项目脚手架"
|
||||
```
|
||||
|
||||
### Task 1.2: 路由框架 + 布局
|
||||
|
||||
**文件:**
|
||||
- 创建: `src/router.tsx`
|
||||
- 创建: `src/components/layout/AppLayout.tsx`
|
||||
- 创建: `src/pages/{Workspace,Editor,Knowledge,Agent,Market,Account}Page.tsx`
|
||||
|
||||
- [ ] **Step 1: 编写路由配置**
|
||||
|
||||
```tsx
|
||||
// src/router.tsx
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import AppLayout from '@/components/layout/AppLayout';
|
||||
import WorkspacePage from '@/pages/WorkspacePage';
|
||||
import EditorPage from '@/pages/EditorPage';
|
||||
import KnowledgePage from '@/pages/KnowledgePage';
|
||||
import AgentPage from '@/pages/AgentPage';
|
||||
import MarketPage from '@/pages/MarketPage';
|
||||
import AccountPage from '@/pages/AccountPage';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{ index: true, element: <WorkspacePage /> },
|
||||
{ path: 'works/:workId', element: <WorkspacePage /> },
|
||||
{ path: 'works/:workId/editor/:chapterId', element: <EditorPage /> },
|
||||
{ path: 'knowledge', element: <KnowledgePage /> },
|
||||
{ path: 'knowledge/:workId', element: <KnowledgePage /> },
|
||||
{ path: 'agents', element: <AgentPage /> },
|
||||
{ path: 'market', element: <MarketPage /> },
|
||||
{ path: 'account', element: <AccountPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写布局组件**
|
||||
|
||||
```tsx
|
||||
// src/components/layout/AppLayout.tsx
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
export default function AppLayout() {
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-50">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 实现侧边栏导航**
|
||||
|
||||
```tsx
|
||||
// src/components/layout/Sidebar.tsx
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
const navItems = [
|
||||
{ to: '/', label: '我的作品', icon: '📝' },
|
||||
{ to: '/knowledge', label: '知识库', icon: '📚' },
|
||||
{ to: '/agents', label: '智能体', icon: '🤖' },
|
||||
{ to: '/market', label: '市场', icon: '🏪' },
|
||||
{ to: '/account', label: '个人中心', icon: '👤' },
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<nav className="w-16 bg-white border-r flex flex-col items-center py-4 gap-2">
|
||||
{navItems.map(item => (
|
||||
<NavLink key={item.to} to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`p-2 rounded text-xs ${isActive ? 'bg-blue-100' : 'hover:bg-gray-100'}`
|
||||
}>
|
||||
<div className="text-xl">{item.icon}</div>
|
||||
<div>{item.label}</div>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 创建占位页面**
|
||||
|
||||
```tsx
|
||||
// src/pages/WorkspacePage.tsx
|
||||
export default function WorkspacePage() {
|
||||
return <div className="p-6"><h1 className="text-2xl font-bold">我的作品</h1></div>;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Main.tsx 集成路由**
|
||||
|
||||
```tsx
|
||||
// src/main.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { router } from './router';
|
||||
import './index.css';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 验证启动**
|
||||
|
||||
```bash
|
||||
cd muse-studio && pnpm dev
|
||||
```
|
||||
|
||||
浏览 `http://localhost:5173`,验证路由导航正常。
|
||||
|
||||
- [ ] **Step 7: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(router): 添加路由框架 + 布局组件 + 6 个功能域占位"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: 核心基础设施
|
||||
|
||||
### Task 2.1: API 客户端 + 类型集成
|
||||
|
||||
**文件:**
|
||||
- 创建: `src/lib/api.ts`
|
||||
- 创建: `src/types/openapi.ts`(从 OpenAPI 生成物导入)
|
||||
|
||||
- [ ] **Step 1: 创建 API 客户端**
|
||||
|
||||
```typescript
|
||||
// src/lib/api.ts
|
||||
import { CommonResult } from '@/types/openapi';
|
||||
|
||||
const BASE_URL = '/app-api/muse';
|
||||
|
||||
class ApiError extends Error {
|
||||
constructor(public code: string, message: string, public status: number) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const response = await fetch(`${BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-Version': '1',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
const body: CommonResult<T> = await response.json();
|
||||
|
||||
if (body.code !== 0) {
|
||||
throw new ApiError(String(body.code), body.msg, response.status);
|
||||
}
|
||||
|
||||
return body.data as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
post: <T>(path: string, data?: unknown) =>
|
||||
request<T>(path, {
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
put: <T>(path: string, data?: unknown) =>
|
||||
request<T>(path, {
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 复制类型包**
|
||||
|
||||
```bash
|
||||
cp docs/api-contracts/generated/typescript/*.ts muse-studio/src/types/
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 编写类型入口**
|
||||
|
||||
```typescript
|
||||
// src/types/openapi.ts
|
||||
export type * as Base from './base';
|
||||
export type * as Content from './content';
|
||||
export type * as AI from './ai';
|
||||
export type * as Knowledge from './knowledge';
|
||||
export type * as Market from './market';
|
||||
export type * as Account from './account';
|
||||
export type * as Meta from './meta';
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(api): 集成 API 客户端 + OpenAPI 类型包"
|
||||
```
|
||||
|
||||
### Task 2.2: MSW Mock API 配置
|
||||
|
||||
**文件:**
|
||||
- 创建: `src/test/mocks/handlers/` 下各模块 handler
|
||||
|
||||
- [ ] **Step 1: 安装 MSW**
|
||||
|
||||
```bash
|
||||
pnpm add -D msw@2
|
||||
npx msw init public/ --save
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写 Content Mock Handlers**
|
||||
|
||||
```typescript
|
||||
// src/test/mocks/handlers/content.ts
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const contentHandlers = [
|
||||
// GET /app-api/muse/works — 作品列表
|
||||
http.get('/app-api/muse/works', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const pageNo = parseInt(url.searchParams.get('pageNo') || '1');
|
||||
return HttpResponse.json({
|
||||
code: 0,
|
||||
msg: 'success',
|
||||
data: {
|
||||
total: 3,
|
||||
pageNo,
|
||||
pageSize: 20,
|
||||
list: [
|
||||
{
|
||||
id: '1',
|
||||
title: '星海迷途',
|
||||
status: 'active',
|
||||
genre: '科幻',
|
||||
wordCount: 52300,
|
||||
chapterCount: 12,
|
||||
updatedAt: '2026-05-20T10:30:00Z',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: '长安旧事',
|
||||
status: 'draft',
|
||||
genre: '历史',
|
||||
wordCount: 12800,
|
||||
chapterCount: 3,
|
||||
updatedAt: '2026-05-24T08:00:00Z',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: '深渊笔记',
|
||||
status: 'active',
|
||||
genre: '悬疑',
|
||||
wordCount: 89500,
|
||||
chapterCount: 24,
|
||||
updatedAt: '2026-05-23T18:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
// POST /app-api/muse/works — 创建作品
|
||||
http.post('/app-api/muse/works', async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const work = {
|
||||
id: crypto.randomUUID(),
|
||||
title: (body as any).title || '未命名作品',
|
||||
description: (body as any).description || '',
|
||||
status: 'draft',
|
||||
genre: (body as any).genre || '',
|
||||
wordCount: 0,
|
||||
chapterCount: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return HttpResponse.json({ code: 0, msg: 'success', data: work }, { status: 201 });
|
||||
}),
|
||||
|
||||
// GET /app-api/muse/works/:workId — 作品详情
|
||||
http.get('/app-api/muse/works/:workId', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
code: 0, msg: 'success',
|
||||
data: {
|
||||
id: params.workId,
|
||||
title: '星海迷途',
|
||||
description: '人类在星际探索中...',
|
||||
status: 'active',
|
||||
genre: '科幻',
|
||||
wordCount: 52300,
|
||||
chapterCount: 12,
|
||||
createdAt: '2026-05-01T00:00:00Z',
|
||||
updatedAt: '2026-05-20T10:30:00Z',
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 配置 MSW 入口**
|
||||
|
||||
```typescript
|
||||
// src/test/mocks/browser.ts
|
||||
import { setupWorker } from 'msw/browser';
|
||||
import { contentHandlers } from './handlers/content';
|
||||
import { aiHandlers } from './handlers/ai';
|
||||
import { knowledgeHandlers } from './handlers/knowledge';
|
||||
import { marketHandlers } from './handlers/market';
|
||||
import { accountHandlers } from './handlers/account';
|
||||
|
||||
export const worker = setupWorker(
|
||||
...contentHandlers,
|
||||
...aiHandlers,
|
||||
...knowledgeHandlers,
|
||||
...marketHandlers,
|
||||
...accountHandlers
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在开发环境启用 MSW**
|
||||
|
||||
```tsx
|
||||
// src/main.tsx 开头追加
|
||||
if (import.meta.env.DEV) {
|
||||
const { worker } = await import('./test/mocks/browser');
|
||||
await worker.start({ onUnhandledRequest: 'bypass' });
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(mock): 配置 MSW Mock API(Content handlers)"
|
||||
```
|
||||
|
||||
### Task 2.3: SSE 双通道客户端
|
||||
|
||||
**文件:**
|
||||
- 创建: `src/lib/sse.ts`
|
||||
|
||||
```typescript
|
||||
// src/lib/sse.ts
|
||||
|
||||
/**
|
||||
* Muse SSE 双通道设计:
|
||||
* 1. AI stream: 独立连接,用于 AI 生成实时流
|
||||
* 2. Event stream: 长连接,用于事件通知,支持 lastEventId 续传
|
||||
*/
|
||||
|
||||
export type SSEEventHandler = {
|
||||
onChunk?: (data: { content: string; sequenceNo: number }) => void;
|
||||
onQualityCheck?: (data: { dimension: string; score: number; passed: boolean }) => void;
|
||||
onDone?: (data: { generationId: string; candidateId: string }) => void;
|
||||
onError?: (data: { code: string; message: string }) => void;
|
||||
};
|
||||
|
||||
export function connectAIStream(
|
||||
url: string,
|
||||
handlers: SSEEventHandler
|
||||
): AbortController {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'text/event-stream',
|
||||
'X-API-Version': '1',
|
||||
},
|
||||
signal: controller.signal,
|
||||
}).then(async (response) => {
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
switch (data.type) {
|
||||
case 'chunk': handlers.onChunk?.(data); break;
|
||||
case 'quality_check': handlers.onQualityCheck?.(data); break;
|
||||
case 'done': handlers.onDone?.(data); break;
|
||||
case 'error': handlers.onError?.(data); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
handlers.onError?.({ code: 'SSE_ERROR', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
export function connectEventStream(
|
||||
lastEventId?: string
|
||||
): { close: () => void; on: (event: string, handler: (data: unknown) => void) => void } {
|
||||
const eventSource = new EventSource(
|
||||
`/app-api/muse/events${lastEventId ? `?lastEventId=${lastEventId}` : ''}`
|
||||
);
|
||||
|
||||
return {
|
||||
close: () => eventSource.close(),
|
||||
on: (event, handler) => {
|
||||
eventSource.addEventListener(event, (e) => {
|
||||
handler(JSON.parse(e.data));
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(sse): 实现 SSE 双通道客户端(AI stream + Event stream)"
|
||||
```
|
||||
|
||||
### Task 2.4: IndexedDB 持久化层
|
||||
|
||||
**文件:**
|
||||
- 创建: `src/lib/indexed-db.ts`
|
||||
|
||||
```typescript
|
||||
// src/lib/indexed-db.ts
|
||||
const DB_NAME = 'muse-studio';
|
||||
const BLOCK_STORE = 'block-drafts';
|
||||
const DB_VERSION = 1;
|
||||
|
||||
function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(BLOCK_STORE)) {
|
||||
db.createObjectStore(BLOCK_STORE, { keyPath: 'blockId' });
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveBlockDraft(blockId: string, content: Record<string, unknown>): Promise<void> {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction(BLOCK_STORE, 'readwrite');
|
||||
tx.objectStore(BLOCK_STORE).put({ blockId, content, savedAt: Date.now() });
|
||||
}
|
||||
|
||||
export async function getBlockDraft(blockId: string): Promise<Record<string, unknown> | null> {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction(BLOCK_STORE, 'readonly');
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = tx.objectStore(BLOCK_STORE).get(blockId);
|
||||
req.onsuccess = () => resolve(req.result?.content ?? null);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeBlockDraft(blockId: string): Promise<void> {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction(BLOCK_STORE, 'readwrite');
|
||||
tx.objectStore(BLOCK_STORE).delete(blockId);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(storage): 实现 IndexedDB 持久化层(Block draft 安全网)"
|
||||
```
|
||||
|
||||
### Task 2.5: Zustand 状态管理
|
||||
|
||||
**文件:**
|
||||
- 创建: `src/stores/editorStore.ts`
|
||||
- 创建: `src/stores/uiStore.ts`
|
||||
|
||||
```typescript
|
||||
// src/stores/editorStore.ts
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface EditorState {
|
||||
activeBlockId: string | null;
|
||||
isDirty: boolean;
|
||||
lastSavedAt: Date | null;
|
||||
setActiveBlock: (blockId: string | null) => void;
|
||||
markDirty: () => void;
|
||||
markSaved: () => void;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>((set) => ({
|
||||
activeBlockId: null,
|
||||
isDirty: false,
|
||||
lastSavedAt: null,
|
||||
setActiveBlock: (blockId) => set({ activeBlockId: blockId, isDirty: false }),
|
||||
markDirty: () => set({ isDirty: true }),
|
||||
markSaved: () => set({ isDirty: false, lastSavedAt: new Date() }),
|
||||
}));
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/stores/uiStore.ts
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UIState {
|
||||
sidebarCollapsed: boolean;
|
||||
candidatePanelOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
toggleCandidatePanel: () => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>((set) => ({
|
||||
sidebarCollapsed: false,
|
||||
candidatePanelOpen: false,
|
||||
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
|
||||
toggleCandidatePanel: () => set((s) => ({ candidatePanelOpen: !s.candidatePanelOpen })),
|
||||
}));
|
||||
```
|
||||
|
||||
- [ ] **Step: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(state): 配置 Zustand store(编辑器状态 + UI 状态)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: 功能域开发(按优先级)
|
||||
|
||||
### Feature 1: 写作台(核心路径)
|
||||
|
||||
**优先级 1 — 编辑器集成 + AI 生成 + 候选面板**
|
||||
|
||||
- [ ] **Task 3.1: Tiptap 编辑器集成**
|
||||
|
||||
文件: `src/features/editor/components/MuseEditor.tsx`
|
||||
|
||||
```tsx
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useEditorStore } from '@/stores/editorStore';
|
||||
import { saveBlockDraft, getBlockDraft, removeBlockDraft } from '@/lib/indexed-db';
|
||||
|
||||
interface MuseEditorProps {
|
||||
blockId: string;
|
||||
initialContent?: unknown;
|
||||
revision: number;
|
||||
}
|
||||
|
||||
export default function MuseEditor({ blockId, initialContent, revision }: MuseEditorProps) {
|
||||
const { markDirty, markSaved } = useEditorStore();
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Placeholder.configure({ placeholder: '开始创作...' }),
|
||||
],
|
||||
content: initialContent as any,
|
||||
onUpdate: ({ editor }) => {
|
||||
markDirty();
|
||||
// 自动保存到 IndexedDB(debounce 2s)
|
||||
debouncedSave(blockId, editor.getJSON());
|
||||
},
|
||||
autofocus: 'end',
|
||||
});
|
||||
|
||||
// 自动保存到后端 + IndexedDB 安全网(debounce 2s)
|
||||
const debouncedSave = useCallback(
|
||||
debounce(async (blockId: string, content: unknown) => {
|
||||
// 1. 先保存到 IndexedDB(即时,安全网)
|
||||
await saveBlockDraft(blockId, content as Record<string, unknown>);
|
||||
// 2. 再调用 API(乐观锁)
|
||||
try {
|
||||
const result = await api.put<{ newRevision: number }>(
|
||||
`/app-api/muse/blocks/${blockId}`,
|
||||
{ content: JSON.stringify(content), expectedRevision: revision }
|
||||
);
|
||||
await removeBlockDraft(blockId);
|
||||
markSaved();
|
||||
} catch (err) {
|
||||
// API 失败:IndexedDB 已保存,下次恢复
|
||||
}
|
||||
}, 2000),
|
||||
[revision]
|
||||
);
|
||||
|
||||
// 恢复 IndexedDB 草稿
|
||||
useEffect(() => {
|
||||
getBlockDraft(blockId).then((draft) => {
|
||||
if (draft && editor) {
|
||||
editor.commands.setContent(draft as any);
|
||||
}
|
||||
});
|
||||
}, [blockId, editor]);
|
||||
|
||||
return (
|
||||
<div className="prose max-w-3xl mx-auto min-h-screen py-8">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): T {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
return ((...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), ms);
|
||||
}) as T;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Task 3.2: AI 生成面板** — 调用 AI SSE stream,展示实时流式输出
|
||||
|
||||
- [ ] **Task 3.3: 候选面板** — 展示候选列表,支持接受/拒绝,显示 diff
|
||||
|
||||
- [ ] **Task 3.4: 编程式提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(editor): Tiptap 编辑器集成 + IndexedDB 安全网 + 自动保存"
|
||||
```
|
||||
|
||||
### Feature 2-6: 其余功能域
|
||||
|
||||
每个功能域按相同 TDD 模式开发:
|
||||
1. 编写组件测试(Vitest + Testing Library)
|
||||
2. 实现组件和 Hook
|
||||
3. 编写 E2E 测试(Playwright)
|
||||
4. 测试通过
|
||||
5. 提交
|
||||
|
||||
优先级顺序:
|
||||
- **F2 我的作品 + 工作台**: 作品列表/创建/删除、章节管理、Block 分割
|
||||
- **F3 知识库工作台**: 知识面板、实体/关系可视化、知识草稿管理
|
||||
- **F4 智能体工作台**: 智能体列表/创建/配置/槽位绑定
|
||||
- **F5 市场 + 个人中心**: 市场浏览/安装/绑定、个人中心
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 6 个功能域页面可交互
|
||||
- [ ] 组件单元测试覆盖率 ≥75%(Vitest + Testing Library)
|
||||
- [ ] Hook 测试覆盖率 ≥80%
|
||||
- [ ] E2E 测试覆盖核心写作流程(Playwright)
|
||||
- [ ] ESLint + Prettier + TypeScript 类型检查通过
|
||||
- [ ] Vite build 成功
|
||||
244
docs/superpowers/plans/2026-05-24-P3-muse-admin.md
Normal file
244
docs/superpowers/plans/2026-05-24-P3-muse-admin.md
Normal file
@ -0,0 +1,244 @@
|
||||
# P3: muse-admin 管理端搭建 — 执行计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**目标:** 在 Vben Admin fork 基础上搭建 muse-admin,实现 MetaSchema 管理、系统治理、AI 配置、市场治理、全局知识管理 5 个功能域。
|
||||
|
||||
**架构:** Vue 3 + Vben Admin + TypeScript,Composition API + SFC script setup,defHttp API 集成,Vben 内置表格/表单组件复用。
|
||||
|
||||
**技术栈:** Vue 3.5, TypeScript 5, Vben Admin 5, Pinia, Vitest + Vue Test Utils + Playwright
|
||||
|
||||
注: muse-admin 已 fork 完成,存在 `apps/web-antd` 和 `packages/` 结构。
|
||||
|
||||
---
|
||||
|
||||
## Step 1: 工程调整
|
||||
|
||||
### Task 1.1: 业务目录结构建立
|
||||
|
||||
**仓库路径:** `muse-admin/`
|
||||
|
||||
- [ ] **Step 1: 创建业务模块目录**
|
||||
|
||||
```bash
|
||||
mkdir -p muse-admin/apps/web-antd/src/views/{governance,ai,knowledge,market,account,jobs,audit}
|
||||
mkdir -p muse-admin/apps/web-antd/src/api/muse/{governance,ai,knowledge,market,account,jobs}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 检查现有结构**
|
||||
|
||||
```bash
|
||||
ls muse-admin/apps/web-antd/src/views/
|
||||
ls muse-admin/packages/@core/
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add muse-admin/apps/web-antd/src/views/governance/ muse-admin/apps/web-antd/src/api/muse/
|
||||
git commit -m "feat(struct): 建立管理端业务模块目录(governance/ai/knowledge/market/account)"
|
||||
```
|
||||
|
||||
### Task 1.2: API 类型集成
|
||||
|
||||
- [ ] **Step 1: 复制 OpenAPI 生成的类型**
|
||||
|
||||
```bash
|
||||
cp docs/api-contracts/generated/typescript/*.ts muse-admin/apps/web-antd/src/api/types/
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写 API 客户端**
|
||||
|
||||
```typescript
|
||||
// muse-admin/apps/web-antd/src/api/muse/client.ts
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const BASE = '/admin-api/muse';
|
||||
|
||||
export const museAdminApi = {
|
||||
get: <T>(url: string, params?: Record<string, unknown>) =>
|
||||
requestClient.get<T>(`${BASE}${url}`, { params }),
|
||||
|
||||
post: <T>(url: string, data?: unknown) =>
|
||||
requestClient.post<T>(`${BASE}${url}`, data),
|
||||
|
||||
put: <T>(url: string, data?: unknown) =>
|
||||
requestClient.put<T>(`${BASE}${url}`, data),
|
||||
|
||||
delete: <T>(url: string) =>
|
||||
requestClient.delete<T>(`${BASE}${url}`),
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(api): 集成管理端 API 客户端 + OpenAPI 类型包"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: 管理页面开发(按优先级)
|
||||
|
||||
### Task 2.1: MetaSchema 管理 — 列表页
|
||||
|
||||
**文件:**
|
||||
- 创建: `apps/web-antd/src/views/governance/MetaSchemaList.vue`
|
||||
- 创建: `apps/web-antd/src/api/muse/governance/meta-schema.ts`
|
||||
|
||||
- [ ] **Step 1: 编写 API service**
|
||||
|
||||
```typescript
|
||||
// apps/web-antd/src/api/muse/governance/meta-schema.ts
|
||||
import { museAdminApi } from '../client';
|
||||
|
||||
export interface MetaSchemaSummary {
|
||||
schemaKey: string;
|
||||
displayName: string;
|
||||
fieldType: string;
|
||||
scope: string;
|
||||
activeVersion: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function listMetaSchemas(params: {
|
||||
pageNo: number;
|
||||
pageSize: number;
|
||||
scope?: string;
|
||||
}) {
|
||||
return museAdminApi.get<{
|
||||
total: number;
|
||||
list: MetaSchemaSummary[];
|
||||
}>('/governance/meta-schemas', params);
|
||||
}
|
||||
|
||||
export function getMetaSchema(schemaKey: string) {
|
||||
return museAdminApi.get<any>(`/governance/meta-schemas/${schemaKey}`);
|
||||
}
|
||||
|
||||
export function createMetaSchemaDraft(schemaKey: string, data: any) {
|
||||
return museAdminApi.post<any>(`/governance/meta-schemas/${schemaKey}/drafts`, data);
|
||||
}
|
||||
|
||||
export function publishMetaSchemaDraft(schemaKey: string, draftVersion: number, data: any) {
|
||||
return museAdminApi.post<any>(
|
||||
`/governance/meta-schemas/${schemaKey}/drafts/${draftVersion}/publish`,
|
||||
data
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 编写列表页**
|
||||
|
||||
```vue
|
||||
<!-- apps/web-antd/src/views/governance/MetaSchemaList.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { BasicTable, useTable, BasicColumn } from '@vben/common-ui';
|
||||
import { listMetaSchemas, type MetaSchemaSummary } from '#/api/muse/governance/meta-schema';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [registerTable, { reload }] = useTable({
|
||||
api: async ({ page, size, scope }) => {
|
||||
const res = await listMetaSchemas({ pageNo: page, pageSize: size, scope });
|
||||
return { items: res.list, total: res.total };
|
||||
},
|
||||
columns: [
|
||||
{ title: 'Schema Key', dataIndex: 'schemaKey', width: 200 },
|
||||
{ title: '显示名称', dataIndex: 'displayName', width: 200 },
|
||||
{ title: '字段类型', dataIndex: 'fieldType', width: 100 },
|
||||
{ title: '作用域', dataIndex: 'scope', width: 100 },
|
||||
{ title: '活跃版本', dataIndex: 'activeVersion', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
] as BasicColumn[],
|
||||
});
|
||||
|
||||
function handleDetail(record: MetaSchemaSummary) {
|
||||
router.push(`/governance/meta-schemas/${record.schemaKey}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<BasicTable @register="registerTable" @row-click="handleDetail">
|
||||
<template #toolbar>
|
||||
<a-button type="primary">新建 MetaSchema</a-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 编写测试**
|
||||
|
||||
```typescript
|
||||
// apps/web-antd/src/views/governance/__tests__/MetaSchemaList.spec.ts
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import MetaSchemaList from '../MetaSchemaList.vue';
|
||||
|
||||
vi.mock('#/api/muse/governance/meta-schema', () => ({
|
||||
listMetaSchemas: vi.fn().mockResolvedValue({
|
||||
total: 2,
|
||||
list: [
|
||||
{ schemaKey: 'character_name', displayName: '角色名', fieldType: 'string', scope: 'work', activeVersion: 3, status: 'active' },
|
||||
{ schemaKey: 'world_setting', displayName: '世界设定', fieldType: 'text', scope: 'global', activeVersion: 1, status: 'active' },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('MetaSchemaList', () => {
|
||||
it('should render table', () => {
|
||||
const wrapper = mount(MetaSchemaList);
|
||||
expect(wrapper.find('.p-4').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "feat(governance): 实现 MetaSchema 列表页(Vben Table + API 集成)"
|
||||
```
|
||||
|
||||
### Task 2.2: MetaSchema 管理 — 详情/草稿编辑页
|
||||
|
||||
实现 MetaSchema 详情页,包含:
|
||||
- 版本历史时间线
|
||||
- 当前 active 版本字段展示
|
||||
- 草稿编辑 + 校验 + 影响预览 + 发布流程
|
||||
- 灰度规则配置
|
||||
|
||||
使用 Vben 内置表单组件 + 动态表单渲染。
|
||||
|
||||
### Task 2.3-2.5: 其余管理页面
|
||||
|
||||
按优先级依次实现:
|
||||
|
||||
**Task 2.3: 系统治理** — 用户管理(复用 Yudao system 模块)、角色权限配置、审计日志查看
|
||||
|
||||
**Task 2.4: AI 配置** — Prompt 模板管理、质量门控维度配置、保护节点注册管理、Tool Grant 配置
|
||||
|
||||
**Task 2.5: 市场治理 + 全局知识管理** — 资产审核(上架/驳回)、下架/召回、申诉处理、全局知识库维护、知识来源管理
|
||||
|
||||
每个页面遵循相同模式:API service → 组件实现 → 测试 → 提交。
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Mock API 配置
|
||||
|
||||
- [ ] **Step 1: 配置 MSW 或 Vben 内置 Mock**
|
||||
|
||||
为每个 `/admin-api/muse/**` 接口提供 mock 数据,使管理端可独立开发。
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 5 个功能域管理页面可交互
|
||||
- [ ] 组件单元测试覆盖率 ≥70%(Vitest + Vue Test Utils)
|
||||
- [ ] E2E 测试覆盖核心管理流程(Playwright)
|
||||
- [ ] ESLint + Prettier + TypeScript 类型检查通过
|
||||
- [ ] Vite build 成功
|
||||
206
docs/superpowers/plans/2026-05-24-P4-集成切换.md
Normal file
206
docs/superpowers/plans/2026-05-24-P4-集成切换.md
Normal file
@ -0,0 +1,206 @@
|
||||
# P4: 集成切换 + 质量加固 — 执行计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**目标:** 按模块将前端从 Mock API 切换到真实后端 API,完成端到端联调,修复契约缺陷,实现全量质量加固(测试/CI/安全)。
|
||||
|
||||
**架构:** 按依赖顺序逐个模块切换:content+account → meta → ai+knowledge → market。每模块切换后更新 OpenAPI 契约。最后进行全量质量加固。
|
||||
|
||||
**涉及仓库:** muse-cloud, muse-studio, muse-admin, muse-design-docs
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 集成切换
|
||||
|
||||
### Switch S1: content + account 集成切换
|
||||
|
||||
**依赖:** muse-cloud content + account API 自测通过
|
||||
|
||||
- [ ] **Step 1: 后端契约测试通过**
|
||||
|
||||
```bash
|
||||
cd muse-cloud && mvn test -pl muse-module-content,muse-module-account
|
||||
```
|
||||
|
||||
验证: Content (50 接口) + Account (33 接口) 全部集成测试通过。
|
||||
|
||||
- [ ] **Step 2: 移除前端 Content MSW handlers**
|
||||
|
||||
```typescript
|
||||
// muse-studio/src/test/mocks/browser.ts
|
||||
// 注释掉 contentHandlers,保留其他模块的 Mock
|
||||
export const worker = setupWorker(
|
||||
// ...contentHandlers, // ← 切换真实 API
|
||||
...aiHandlers,
|
||||
...knowledgeHandlers,
|
||||
...marketHandlers,
|
||||
...accountHandlers
|
||||
);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 前端联调验证**
|
||||
|
||||
```bash
|
||||
cd muse-studio && pnpm dev
|
||||
```
|
||||
|
||||
测试流程:
|
||||
1. 作品列表加载正常
|
||||
2. 创建新作品 → 自动跳转
|
||||
3. 章节列表 + 创建章节
|
||||
4. Block 编辑器加载 → 自动保存 → 验证 IndexedDB 安全网
|
||||
|
||||
- [ ] **Step 4: 修正接口不一致**
|
||||
|
||||
联调中发现的契约缺陷:
|
||||
1. 记录具体差异(字段名/类型/状态码不一致)
|
||||
2. 决定修后端还是修 OpenAPI
|
||||
3. 更新 OpenAPI 契约文件
|
||||
4. 重新生成 TS 类型包
|
||||
5. 前端更新类型引用
|
||||
|
||||
- [ ] **Step 5: 冒烟测试(写作流程)**
|
||||
|
||||
Playwright E2E:
|
||||
```typescript
|
||||
// muse-studio/e2e/writing-flow.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('完整写作流程', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173');
|
||||
|
||||
// 1. 查看作品列表
|
||||
await expect(page.locator('h1')).toContainText('我的作品');
|
||||
|
||||
// 2. 创建新作品
|
||||
await page.click('button:has-text("新建作品")');
|
||||
await page.fill('input[name="title"]', 'E2E 测试作品');
|
||||
await page.click('button:has-text("创建")');
|
||||
|
||||
// 3. 进入编辑器
|
||||
await expect(page.locator('.ProseMirror')).toBeVisible();
|
||||
|
||||
// 4. 输入文本
|
||||
await page.locator('.ProseMirror').fill('第一段测试文字');
|
||||
|
||||
// 5. 等待自动保存
|
||||
await page.waitForTimeout(3000);
|
||||
await expect(page.locator('[data-testid="save-indicator"]')).toContainText('已保存');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Account API 切换(同时)**
|
||||
|
||||
类似流程:移除 account MSW handlers → 联调验证个人中心/用量/配额 → 修正不一致 → 更新契约
|
||||
|
||||
- [ ] **Step 7: 提交契约更新**
|
||||
|
||||
```bash
|
||||
git add docs/api-contracts/
|
||||
git commit -m "fix(api): content+account 联调后契约修正"
|
||||
```
|
||||
|
||||
### Switch S2: meta API 集成切换
|
||||
|
||||
**依赖:** muse-cloud meta API 自测通过
|
||||
|
||||
1. MetaSchema 管理页面切换到真实 API
|
||||
2. 验证 CRUD + 草稿/发布/版本管理流程
|
||||
3. 修正不一致,更新 OpenAPI
|
||||
|
||||
### Switch S3: ai + knowledge API 集成切换
|
||||
|
||||
**依赖:** muse-cloud ai + knowledge API 自测通过
|
||||
|
||||
1. AI 生成 SSE 流验证(断线重连、超时)
|
||||
2. 候选接受/拒绝流程验证
|
||||
3. 知识库文档上传/解析/确认流程
|
||||
4. 知识绑定/解绑跨模块测试
|
||||
5. 修正不一致,更新 OpenAPI
|
||||
|
||||
### Switch S4: market API 集成切换
|
||||
|
||||
**依赖:** 前面所有模块已切换
|
||||
|
||||
1. 市场浏览/推荐验证
|
||||
2. Handoff 跨空间跳转测试
|
||||
3. 发布/审核流程验证
|
||||
4. 修正不一致,更新 OpenAPI
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 质量加固
|
||||
|
||||
### Quality G1: 后端全量测试
|
||||
|
||||
- [ ] **单元测试补充**
|
||||
- Domain 层 edge case 覆盖
|
||||
- Service 层 mock 测试(JUnit 5 + Mockito)
|
||||
- 目标: Domain ≥90%, Application ≥80%
|
||||
|
||||
- [ ] **集成测试补充**
|
||||
- Testcontainers + PostgreSQL
|
||||
- 跨模块事务边界测试(内容保存 → 知识提取 → AI 候选)
|
||||
- 并发冲突测试(乐观锁)
|
||||
|
||||
- [ ] **契约测试**
|
||||
- Spring Cloud Contract 或 Pact
|
||||
- 验证 API 响应与 OpenAPI 定义一致
|
||||
|
||||
### Quality G2: 前端全量测试
|
||||
|
||||
- [ ] **muse-studio 单元测试补充**
|
||||
- 组件测试 (Testing Library) ≥75%
|
||||
- Hook 测试 ≥80%
|
||||
|
||||
- [ ] **muse-admin 单元测试补充**
|
||||
- 组件测试 (Vue Test Utils) ≥70%
|
||||
- Composable 测试 ≥75%
|
||||
|
||||
- [ ] **E2E 测试**
|
||||
- Playwright 覆盖核心流程
|
||||
- studio: 写作流程、作品管理、知识确认
|
||||
- admin: MetaSchema 管理、资产审核
|
||||
|
||||
### Quality G3: CI/CD
|
||||
|
||||
- [ ] **后端 CI 流水线** (GitHub Actions / Jenkins)
|
||||
```yaml
|
||||
# .github/workflows/muse-cloud.yml
|
||||
steps:
|
||||
- compile (Maven)
|
||||
- unit-test (JUnit + Mockito)
|
||||
- integration-test (Testcontainers)
|
||||
- lint (Checkstyle + SpotBugs)
|
||||
- build-image (Docker)
|
||||
```
|
||||
|
||||
- [ ] **前端 CI 流水线**
|
||||
```yaml
|
||||
# .github/workflows/muse-studio.yml
|
||||
steps:
|
||||
- typecheck (tsc --noEmit)
|
||||
- lint (ESLint + Prettier)
|
||||
- test (Vitest)
|
||||
- build (Vite)
|
||||
```
|
||||
|
||||
### Quality G4: 安全加固
|
||||
|
||||
- [ ] **依赖漏洞扫描** (npm audit, OWASP Dependency Check)
|
||||
- [ ] **XSS 防护 + CSP 头配置**
|
||||
- [ ] **敏感数据脱敏**(日志中 token/密码脱敏)
|
||||
- [ ] **SSRF 防护**(Knowledge linkUrl allowlist + 私网拦截)
|
||||
- [ ] **AI 安全**(Prompt injection 防护、输出合规检查)
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
|
||||
- [ ] 6 个模块全部切换真实 API
|
||||
- [ ] 全量端到端冒烟测试通过
|
||||
- [ ] 后端测试覆盖率达标: Domain ≥90%, Application ≥80%
|
||||
- [ ] 前端测试覆盖率达标: Studio ≥75%, Admin ≥70%
|
||||
- [ ] CI 流水线全部通过
|
||||
- [ ] OpenAPI 契约与实现一致
|
||||
- [ ] 安全扫描无 Critical/High 新发现
|
||||
Loading…
x
Reference in New Issue
Block a user