feat: initial SaaS scaffold

- domain: User, Workspace, Project, AssetLibrary, Asset, IngestJob
- ports: repository interfaces
- application: use cases
- adapters: in-memory
- API: FastAPI 5 routes
- worker: Celery ingest_asset
- tests: 4 passing
This commit is contained in:
Xiaoxia AI
2026-06-15 15:17:40 +08:00
commit b5a62ee9a3
89 changed files with 4689 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Environment example
APP_ENV=development
APP_NAME=xiaoxia-saas
API_HOST=0.0.0.0
API_PORT=8000
WEB_PORT=3000
POSTGRES_URL=postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas
REDIS_URL=redis://localhost:6379/0
OBJECT_STORAGE_PROVIDER=minio
OBJECT_STORAGE_ENDPOINT=http://localhost:9000
OBJECT_STORAGE_BUCKET=xiaoxia-saas
+36
View File
@@ -0,0 +1,36 @@
# Node / frontend
node_modules/
.next/
out/
dist/
coverage/
# Python / backend
.venv/
venv/
__pycache__/
.pytest_cache/
.mypy_cache/
.pytype/
ruff_cache/
*.pyc
# Env / secrets
.env
.env.*
!.env.example
# OS / editor
.DS_Store
Thumbs.db
.vscode/
.idea/
# Logs / temp
*.log
tmp/
temp/
# Build / runtime artifacts
build/
.runtime/
+27
View File
@@ -0,0 +1,27 @@
# xiaoxia-saas
小虾自动剪辑 SaaS 新主线仓库。
## 目标
- 与旧桌面版彻底隔离
- 按正规 SaaS 工程方式重建
- 先按文档、架构、制度启动,再进入开发
## 目录
- `docs/`:产品、架构、流程、盘点和启动文档
- `apps/web/`:前端 Web 应用
- `apps/api/`:后端 API 应用
- `apps/worker/`:后台任务执行应用
- `packages/domain/`:核心业务对象与规则
- `packages/application/`:用例层
- `packages/ports/`:接口定义
- `packages/adapters/`:外部实现
- `packages/shared/`:共享基础模块
- `infra/`:部署与运行基础设施
- `tests/`:集成测试与端到端测试
## 当前状态
当前仓库已完成 SaaS 启动期文档骨架和目录骨架,下一步进入工程初始化。
+15
View File
@@ -0,0 +1,15 @@
# xiaoxia-saas API
## 结构
- `app/core/`:配置与基础能力
- `app/api/`:路由组织
- `app/schemas/`:请求响应模型
- `app/dependencies.py`:依赖注入入口
- `main.py`:FastAPI 启动入口
## 当前可用接口
- `GET /api/health`
- `GET /api/projects?workspace_id=...`
- `POST /api/projects`
+3
View File
@@ -0,0 +1,3 @@
from .main import app, create_app
__all__ = ["app", "create_app"]
+1
View File
@@ -0,0 +1 @@
"""API application package."""
+1
View File
@@ -0,0 +1 @@
"""API package."""
+14
View File
@@ -0,0 +1,14 @@
from fastapi import APIRouter
from app.api.routes.asset_libraries import router as asset_libraries_router
from app.api.routes.assets import router as assets_router
from app.api.routes.health import router as health_router
from app.api.routes.ingest_jobs import router as ingest_jobs_router
from app.api.routes.projects import router as projects_router
api_router = APIRouter()
api_router.include_router(health_router, prefix="/health", tags=["health"])
api_router.include_router(projects_router, prefix="/projects", tags=["projects"])
api_router.include_router(asset_libraries_router, prefix="/asset-libraries", tags=["asset-libraries"])
api_router.include_router(assets_router, prefix="/assets", tags=["assets"])
api_router.include_router(ingest_jobs_router, prefix="/ingest-jobs", tags=["ingest-jobs"])
+1
View File
@@ -0,0 +1 @@
"""Route modules."""
@@ -0,0 +1,55 @@
from fastapi import APIRouter, Depends
from app.dependencies import get_asset_library_repository
from app.schemas.asset_library import AssetLibraryResponse, CreateAssetLibraryRequest, ListAssetLibrariesResponse
from packages.adapters.in_memory import InMemoryAssetLibraryRepository
from packages.application import CreateAssetLibraryCommand, CreateAssetLibraryUseCase, ListAssetLibrariesUseCase
from packages.domain import AssetLibraryKind
router = APIRouter()
@router.get("", response_model=ListAssetLibrariesResponse)
def list_asset_libraries(
project_id: str,
kind: str | None = None,
asset_library_repository: InMemoryAssetLibraryRepository = Depends(get_asset_library_repository),
) -> ListAssetLibrariesResponse:
use_case = ListAssetLibrariesUseCase(asset_library_repository)
parsed_kind = AssetLibraryKind(kind) if kind else None
items = use_case.execute(project_id, kind=parsed_kind)
return ListAssetLibrariesResponse(
items=[
AssetLibraryResponse(
id=item.id,
workspace_id=item.workspace_id,
project_id=item.project_id,
name=item.name,
kind=item.kind.value,
)
for item in items
]
)
@router.post("", response_model=AssetLibraryResponse)
def create_asset_library(
request: CreateAssetLibraryRequest,
asset_library_repository: InMemoryAssetLibraryRepository = Depends(get_asset_library_repository),
) -> AssetLibraryResponse:
use_case = CreateAssetLibraryUseCase(asset_library_repository)
item = use_case.execute(
CreateAssetLibraryCommand(
workspace_id=request.workspace_id,
project_id=request.project_id,
name=request.name,
kind=AssetLibraryKind(request.kind),
)
)
return AssetLibraryResponse(
id=item.id,
workspace_id=item.workspace_id,
project_id=item.project_id,
name=item.name,
kind=item.kind.value,
)
+61
View File
@@ -0,0 +1,61 @@
from fastapi import APIRouter, Depends
from app.dependencies import get_asset_repository
from app.schemas.asset import AssetResponse, CreateAssetRequest, ListAssetsResponse
from packages.adapters.in_memory import InMemoryAssetRepository
from packages.application import CreateAssetCommand, CreateAssetUseCase, ListAssetsUseCase
router = APIRouter()
@router.get("", response_model=ListAssetsResponse)
def list_assets(
library_id: str,
asset_repository: InMemoryAssetRepository = Depends(get_asset_repository),
) -> ListAssetsResponse:
use_case = ListAssetsUseCase(asset_repository)
items = use_case.execute(library_id)
return ListAssetsResponse(
items=[
AssetResponse(
id=item.id,
workspace_id=item.workspace_id,
project_id=item.project_id,
library_id=item.library_id,
name=item.name,
storage_key=item.storage_key,
mime_type=item.mime_type,
metadata=item.metadata,
)
for item in items
]
)
@router.post("", response_model=AssetResponse)
def create_asset(
request: CreateAssetRequest,
asset_repository: InMemoryAssetRepository = Depends(get_asset_repository),
) -> AssetResponse:
use_case = CreateAssetUseCase(asset_repository)
item = use_case.execute(
CreateAssetCommand(
workspace_id=request.workspace_id,
project_id=request.project_id,
library_id=request.library_id,
name=request.name,
storage_key=request.storage_key,
mime_type=request.mime_type,
metadata=request.metadata,
)
)
return AssetResponse(
id=item.id,
workspace_id=item.workspace_id,
project_id=item.project_id,
library_id=item.library_id,
name=item.name,
storage_key=item.storage_key,
mime_type=item.mime_type,
metadata=item.metadata,
)
+10
View File
@@ -0,0 +1,10 @@
from fastapi import APIRouter
from app.schemas.health import HealthResponse
router = APIRouter()
@router.get("", response_model=HealthResponse)
def get_health() -> HealthResponse:
return HealthResponse(ok=True, service="api")
+35
View File
@@ -0,0 +1,35 @@
from fastapi import APIRouter, Depends
from app.dependencies import get_ingest_job_repository
from app.schemas.ingest_job import IngestJobResponse, SubmitIngestJobRequest
from packages.adapters.in_memory import InMemoryIngestJobRepository
from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
router = APIRouter()
@router.post("", response_model=IngestJobResponse)
def submit_ingest_job(
request: SubmitIngestJobRequest,
ingest_job_repository: InMemoryIngestJobRepository = Depends(get_ingest_job_repository),
) -> IngestJobResponse:
use_case = SubmitIngestJobUseCase(ingest_job_repository)
job = use_case.execute(
SubmitIngestJobCommand(
workspace_id=request.workspace_id,
project_id=request.project_id,
library_id=request.library_id,
storage_key=request.storage_key,
)
)
# TODO: enqueue async worker task here
return IngestJobResponse(
id=job.id,
workspace_id=job.workspace_id,
project_id=job.project_id,
library_id=job.library_id,
storage_key=job.storage_key,
status=job.status.value,
error_message=job.error_message,
result_asset_id=job.result_asset_id,
)
+49
View File
@@ -0,0 +1,49 @@
from fastapi import APIRouter, Depends
from app.dependencies import get_project_repository
from app.schemas.project import CreateProjectRequest, ListProjectsResponse, ProjectResponse
from packages.application import CreateProjectCommand, CreateProjectUseCase, ListProjectsUseCase
from packages.adapters.in_memory import InMemoryProjectRepository
router = APIRouter()
@router.get("", response_model=ListProjectsResponse)
def list_projects(
workspace_id: str,
project_repository: InMemoryProjectRepository = Depends(get_project_repository),
) -> ListProjectsResponse:
use_case = ListProjectsUseCase(project_repository)
projects = use_case.execute(workspace_id)
return ListProjectsResponse(
items=[
ProjectResponse(
id=item.id,
workspace_id=item.workspace_id,
name=item.name,
description=item.description,
)
for item in projects
]
)
@router.post("", response_model=ProjectResponse)
def create_project(
request: CreateProjectRequest,
project_repository: InMemoryProjectRepository = Depends(get_project_repository),
) -> ProjectResponse:
use_case = CreateProjectUseCase(project_repository)
project = use_case.execute(
CreateProjectCommand(
workspace_id=request.workspace_id,
name=request.name,
description=request.description,
)
)
return ProjectResponse(
id=project.id,
workspace_id=project.workspace_id,
name=project.name,
description=project.description,
)
+1
View File
@@ -0,0 +1 @@
"""Core configuration package."""
+11
View File
@@ -0,0 +1,11 @@
from pydantic import BaseModel
class AppSettings(BaseModel):
app_name: str = "xiaoxia-saas-api"
app_env: str = "development"
api_prefix: str = "/api"
def get_settings() -> AppSettings:
return AppSettings()
+28
View File
@@ -0,0 +1,28 @@
from functools import lru_cache
from packages.adapters.in_memory import (
InMemoryAssetLibraryRepository,
InMemoryAssetRepository,
InMemoryIngestJobRepository,
InMemoryProjectRepository,
)
@lru_cache(maxsize=1)
def get_project_repository() -> InMemoryProjectRepository:
return InMemoryProjectRepository()
@lru_cache(maxsize=1)
def get_asset_library_repository() -> InMemoryAssetLibraryRepository:
return InMemoryAssetLibraryRepository()
@lru_cache(maxsize=1)
def get_asset_repository() -> InMemoryAssetRepository:
return InMemoryAssetRepository()
@lru_cache(maxsize=1)
def get_ingest_job_repository() -> InMemoryIngestJobRepository:
return InMemoryIngestJobRepository()
+22
View File
@@ -0,0 +1,22 @@
"""Schema package."""
from .asset import AssetResponse, CreateAssetRequest, ListAssetsResponse
from .asset_library import AssetLibraryResponse, CreateAssetLibraryRequest, ListAssetLibrariesResponse
from .health import HealthResponse
from .ingest_job import IngestJobResponse, SubmitIngestJobRequest
from .project import CreateProjectRequest, ListProjectsResponse, ProjectResponse
__all__ = [
"AssetResponse",
"AssetLibraryResponse",
"CreateAssetLibraryRequest",
"CreateAssetRequest",
"CreateProjectRequest",
"HealthResponse",
"IngestJobResponse",
"ListAssetLibrariesResponse",
"ListAssetsResponse",
"ListProjectsResponse",
"ProjectResponse",
"SubmitIngestJobRequest",
]
+26
View File
@@ -0,0 +1,26 @@
from pydantic import BaseModel, Field
class CreateAssetRequest(BaseModel):
workspace_id: str = Field(..., min_length=1)
project_id: str = Field(..., min_length=1)
library_id: str = Field(..., min_length=1)
name: str = Field(..., min_length=1, max_length=100)
storage_key: str = Field(..., min_length=1, max_length=255)
mime_type: str = Field(..., min_length=1, max_length=100)
metadata: dict[str, object] = Field(default_factory=dict)
class AssetResponse(BaseModel):
id: str
workspace_id: str
project_id: str
library_id: str
name: str
storage_key: str
mime_type: str
metadata: dict[str, object]
class ListAssetsResponse(BaseModel):
items: list[AssetResponse]
+20
View File
@@ -0,0 +1,20 @@
from pydantic import BaseModel, Field
class CreateAssetLibraryRequest(BaseModel):
workspace_id: str = Field(..., min_length=1)
project_id: str = Field(..., min_length=1)
name: str = Field(..., min_length=1, max_length=100)
kind: str = Field(..., pattern="^(video|voice)$")
class AssetLibraryResponse(BaseModel):
id: str
workspace_id: str
project_id: str
name: str
kind: str
class ListAssetLibrariesResponse(BaseModel):
items: list[AssetLibraryResponse]
+6
View File
@@ -0,0 +1,6 @@
from pydantic import BaseModel
class HealthResponse(BaseModel):
ok: bool
service: str
+19
View File
@@ -0,0 +1,19 @@
from pydantic import BaseModel, Field
class SubmitIngestJobRequest(BaseModel):
workspace_id: str = Field(..., min_length=1)
project_id: str = Field(..., min_length=1)
library_id: str = Field(..., min_length=1)
storage_key: str = Field(..., min_length=1, max_length=255)
class IngestJobResponse(BaseModel):
id: str
workspace_id: str
project_id: str
library_id: str
storage_key: str
status: str
error_message: str
result_asset_id: str
+18
View File
@@ -0,0 +1,18 @@
from pydantic import BaseModel, Field
class CreateProjectRequest(BaseModel):
workspace_id: str = Field(..., min_length=1)
name: str = Field(..., min_length=1, max_length=100)
description: str = Field(default="", max_length=500)
class ProjectResponse(BaseModel):
id: str
workspace_id: str
name: str
description: str
class ListProjectsResponse(BaseModel):
items: list[ProjectResponse]
+14
View File
@@ -0,0 +1,14 @@
from fastapi import FastAPI
from app.api.router import api_router
from app.core.config import get_settings
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(title=settings.app_name)
app.include_router(api_router, prefix=settings.api_prefix)
return app
app = create_app()
+8
View File
@@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
def health():
return {"ok": True, "service": "api"}
+3
View File
@@ -0,0 +1,3 @@
# Workspace settings
This file keeps editor/workspace-specific settings if needed later.
+14
View File
@@ -0,0 +1,14 @@
# xiaoxia-saas Web
Next.js frontend app placeholder.
## Planned stack
- Next.js
- TypeScript
- Tailwind CSS
- shadcn/ui
## Current status
Scaffold phase only. Runtime initialization will be added after final stack bootstrap.
+12
View File
@@ -0,0 +1,12 @@
export const metadata = {
title: 'xiaoxia-saas',
description: 'Xiaoxia SaaS scaffold',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
);
}
+14
View File
@@ -0,0 +1,14 @@
export default function HomePage() {
return (
<main style={{ padding: 32, fontFamily: 'sans-serif' }}>
<h1>xiaoxia-saas</h1>
<p>SaaS 新主线前端骨架已创建。</p>
<ul>
<li>项目管理</li>
<li>素材管理</li>
<li>配音管理</li>
<li>任务与成片</li>
</ul>
</main>
);
}
+4
View File
@@ -0,0 +1,4 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// This file is auto-used by Next.js TypeScript projects.
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
module.exports = nextConfig;
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@xiaoxia/web",
"private": true,
"version": "0.1.0",
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start -p 3000",
"lint": "next lint"
},
"dependencies": {
"next": "14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"typescript": "5.5.4",
"@types/node": "22.7.4",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"eslint": "8.57.1",
"eslint-config-next": "14.2.5"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "es2020"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
+12
View File
@@ -0,0 +1,12 @@
# xiaoxia-saas Worker
## 结构
- `worker_app/core/`:worker 配置
- `worker_app/celery_app.py`:Celery 应用入口
- `worker_app/tasks/`:任务模块
- `main.py`:本地调试入口
## 当前任务
- `worker.healthcheck`
+11
View File
@@ -0,0 +1,11 @@
from celery import Celery
def create_celery_app() -> Celery:
app = Celery("xiaoxia_saas_worker")
app.conf.broker_url = "redis://redis:6379/0"
app.conf.result_backend = "redis://redis:6379/1"
return app
celery_app = create_celery_app()
+10
View File
@@ -0,0 +1,10 @@
from worker_app.tasks.health import healthcheck
def main() -> None:
print("xiaoxia-saas worker scaffold")
print(healthcheck.name)
if __name__ == "__main__":
main()
+6
View File
@@ -0,0 +1,6 @@
from .celery_app import celery_app
@celery_app.task(name="worker.healthcheck")
def healthcheck() -> dict:
return {"ok": True, "service": "worker"}
+1
View File
@@ -0,0 +1 @@
"""Worker application package."""
+9
View File
@@ -0,0 +1,9 @@
from celery import Celery
from worker_app.core.config import get_settings
settings = get_settings()
celery_app = Celery(settings.worker_name)
celery_app.conf.broker_url = settings.broker_url
celery_app.conf.result_backend = settings.result_backend
+1
View File
@@ -0,0 +1 @@
"""Worker core package."""
+11
View File
@@ -0,0 +1,11 @@
from pydantic import BaseModel
class WorkerSettings(BaseModel):
worker_name: str = "xiaoxia-saas-worker"
broker_url: str = "redis://redis:6379/0"
result_backend: str = "redis://redis:6379/1"
def get_settings() -> WorkerSettings:
return WorkerSettings()
+1
View File
@@ -0,0 +1 @@
"""Task modules."""
+23
View File
@@ -0,0 +1,23 @@
from worker_app.celery_app import celery_app
@celery_app.task(name="worker.healthcheck")
def healthcheck() -> dict:
return {"ok": True, "service": "worker"}
@celery_app.task(name="worker.ingest_asset")
def ingest_asset(job_id: str, workspace_id: str, project_id: str, library_id: str, storage_key: str) -> dict:
"""
Ingest asset task placeholder.
Real implementation would:
1. Fetch file from storage_key
2. Extract metadata (duration, resolution, etc.)
3. Create Asset entity
4. Update IngestJob status
"""
return {
"job_id": job_id,
"status": "completed",
"asset_id": "placeholder-asset-id",
}
+25
View File
@@ -0,0 +1,25 @@
# 新仓库启动说明
本仓库是 `xiaoxia-saas` 的新主线。
## 启动约束
- 新仓库与旧桌面版仓库物理隔离
- 旧仓库只作为业务参考和历史样本
- 新仓库所有开发都遵循 SaaS 启动文档
- 先文档,后架构,后骨架,最后开发
## 入口文档
- `docs/saas-index.md`
- `docs/saas-rebuild-kickoff-plan.md`
- `docs/saas-rebuild-inventory.md`
- `docs/saas-project-bootstrap-location.md`
- `docs/saas-project-structure-spec.md`
- `docs/saas-development-workflow.md`
## 当前下一步
1. 选择是否立即初始化前后端基础工程
2. 确认数据库/队列/存储的本地开发方式
3. 建立第一版 CI/CD
+25
View File
@@ -0,0 +1,25 @@
# SaaS 目录索引
```text
F:\openclaw-saas
├─ docs
├─ apps
│ ├─ web
│ ├─ api
│ └─ worker
├─ packages
│ ├─ domain
│ ├─ application
│ ├─ ports
│ ├─ adapters
│ └─ shared
├─ infra
│ ├─ docker
│ ├─ nginx
│ └─ scripts
├─ tests
│ ├─ integration
│ └─ e2e
├─ README.md
└─ .gitignore
```
+281
View File
@@ -0,0 +1,281 @@
# AI 团队工具清单(第一版)
本文档定义新 SaaS 项目启动阶段需要的 AI 工具、用途、角色归属、优先级、获取方式和安全要求。
---
## 1. 总原则
我们不自研 AI 平台,前期采用:
- 现成模型服务
- 现成 AI IDE / AI 设计工具
- 明确角色分工
- 统一输入输出规范
重点不是“工具越多越好”,而是:
- 谁负责什么
- 谁来拍板
- 谁来 review
- 谁来回归
- 所有 AI 是否围绕统一文档工作
---
## 2. 工具分类
AI 工具分为 5 类:
1. 通用大模型
2. 编码工具
3. UI / 原型工具
4. Review / QA 工具
5. 协作与知识工具
---
## 3. 工具清单
| 类别 | 工具 | 主要用途 | 主要使用者 | 优先级 | 获取方式 |
|------|------|----------|------------|--------|----------|
| 通用大模型 | ChatGPT | 需求整理、方案讨论、文档草稿、总结 | 老大 / 小虾 | P0 | 直接订阅 |
| 通用大模型 | Claude | 长文分析、架构讨论、文档、审查建议 | 老大 / 小虾 | P0 | 直接订阅 |
| 编码工具 | Codex / Codex CLI | 仓库修改、重构、补测试、代码执行 | 小虾 / 编码 AI | P0 | 已可用 / 订阅 |
| 编码工具 | Cursor | 前后端模块实现、快速编码 | 编码 AI | P0 | 直接订阅 |
| 编码工具 | Claude Code / 同类 | 代码实现、重构、排错 | 编码 AI | P1 | 按需开通 |
| UI / 原型 | Figma | 页面结构、流程图、组件设计 | 老大 / UI AI | P0 | 直接订阅 |
| UI / 原型 | Figma AI | 辅助出界面和文案 | UI AI | P1 | Figma 内能力 |
| UI / 原型 | v0 | Web 页面草图、组件原型 | UI AI / 前端 AI | P1 | 直接开通 |
| UI / 原型 | Lovable | 页面快速原型 | UI AI | P2 | 按需开通 |
| Review / QA | 独立大模型会话 | 代码审查、架构偏离检查 | Review AI | P0 | 基于已有模型 |
| Review / QA | 独立大模型会话 | 测试设计、回归建议、bug 复盘 | QA AI | P0 | 基于已有模型 |
| 协作 / 知识 | Gitea | 代码真相源、Issue、PR/提交记录 | 全员 | P0 | 已有 |
| 协作 / 知识 | CI/CD | 自动检查、构建、测试、部署 | 全员 | P0 | 已有基础,需重建 |
| 协作 / 知识 | 巡检机器人 | 线上状态、异常巡检、告警 | 运维 / 小虾 | P1 | 已有基础 |
---
## 4. 按角色分配
### 4.1 老大
负责:
- 目标
- 产品边界
- 风格偏好
- 最终拍板
建议直接使用:
- ChatGPT
- Claude
- Figma
### 4.2 小虾(技术负责人 / 总协调)
负责:
- 架构方案
- 拆解任务
- 规范约束
- 合并口径
- 收口
建议直接使用:
- ChatGPT / Claude
- Codex / Codex CLI
- Gitea
- CI/CD
### 4.3 编码 AI
负责:
- 具体模块实现
- 前端编码
- 后端编码
- 补测试
建议直接使用:
- Codex / Codex CLI
- Cursor
- Claude Code / 同类
### 4.4 UI AI
负责:
- 页面结构
- 交互流
- 原型草图
- 视觉辅助
建议直接使用:
- Figma
- Figma AI
- v0
- Lovable
### 4.5 Review AI
负责:
- 代码审查
- 架构一致性检查
- 风险提示
- 边界偏移检查
建议直接使用:
- 独立 ChatGPT / Claude 会话
- 仓库 diff / PR 内容
### 4.6 QA / Bug AI
负责:
- 复现问题
- 补回归场景
- 测试建议
- bug 分类
建议直接使用:
- 独立 ChatGPT / Claude 会话
- CI 报告
- 日志与监控结果
---
## 5. 获取顺序
### 第一批:马上需要
必须先具备:
1. ChatGPT
2. Claude
3. Cursor 或同类编码 AI IDE
4. Figma
5. Gitea
6. CI/CD 基础
### 第二批:开始进入前端原型和多人协作时补
1. v0
2. Figma AI
3. 独立 Review / QA 会话模板
4. 巡检机器人 SaaS 化巡检能力
### 第三批:规模扩大后再补
1. 更专业的设计协同工具
2. 更完整的测试平台
3. 更高级的知识库 / 检索增强能力
---
## 6. 采购 / 开通制度
### 6.1 原则
- 优先采购真正高频使用的工具
- 一个角色至少有一套稳定主工具
- 前期不堆太多重复工具
- 所有付费工具要登记账号归属和用途
### 6.2 必须登记的信息
每个工具都要记录:
- 工具名称
- 购买渠道
- 账号归属
- 谁可以使用
- 费用周期
- 主要用途
- 替代方案
---
## 7. 安全制度
### 7.1 严禁随意喂给 AI 的信息
- 服务器 root 密码
- 数据库密码
- 云存储密钥
- 支付相关密钥
- 生产环境敏感配置
- 用户隐私数据
### 7.2 可以给 AI 的信息
- 脱敏代码
- 架构方案
- 伪造样本数据
- 通用错误日志(脱敏后)
- 接口设计草稿
### 7.3 最佳实践
- 敏感配置统一放环境变量和密钥管理中
- 给外部 AI 前先脱敏
- 不把完整生产数据库导出直接发给任何模型
---
## 8. 使用制度
### 制度 1:一个问题一个 owner
- 架构问题只由技术 owner 收口
- UI 问题由 UI owner 收口
- Review 不能和编码混同
### 制度 2:AI 输出不能直接上线
必须经过:
- 人工确认
- 代码审查
- 自动化测试
- CI 通过
### 制度 3:统一文档源
所有 AI 都必须围绕同一套文档:
- PRD
- 架构文档
- 数据模型
- API 规范
- 任务拆解清单
### 制度 4:避免多 AI 同时乱改同一问题
- 一个任务一个主执行者
- 其他 AI 提供辅助,不直接平行乱改
---
## 9. 当前结论
当前最值得立即准备的 AI 工具是:
- ChatGPT
- Claude
- Codex / Codex CLI
- Cursor
- Figma
- Gitea
- CI/CD
这些足够支撑新 SaaS 项目从 0 到可开发启动。
后续再按阶段补更细的 UI / Review / QA 工具。
+263
View File
@@ -0,0 +1,263 @@
# 基础设施保留 / 废弃 / 重建清单(第一版)
本文档定义新 SaaS 项目启动时,哪些基础设施继续保留,哪些只保留经验不保留实现,哪些必须全新重建。
---
## 1. 分类原则
基础设施分为三类:
1. **保留**:直接作为新项目资产继续使用
2. **废弃实现,仅保留经验**:旧实现不进入新主线,但经验可复用
3. **必须重建**:新 SaaS 需要重新搭建
---
## 2. 保留清单
### 2.1 代码仓库
- **保留项**:Gitea
- **原因**:已经是稳定的代码真相源,具备提交、历史、协作、推送能力
- **新项目做法**:新 SaaS 使用新的独立仓库或独立主目录,不与旧桌面版混线
### 2.2 CI/CD 思路和经验
- **保留项**:自动检查、测试、构建、发布的工程理念
- **原因**:这是正规软件必须保留的工程资产
- **新项目做法**:重建适合 Web/SaaS 的流水线
### 2.3 巡检机器人思路
- **保留项**:巡检、告警、状态监控思路
- **原因**:线上系统比桌面工具更需要健康巡检
- **新项目做法**:转为 SaaS 服务、任务、队列、磁盘、worker 的巡检
### 2.4 服务器与开发环境
- **保留项**:Windows 开发机、Ubuntu 服务器
- **原因**:开发和部署基础已有
- **新项目做法**:重新按 SaaS 需要规划开发/测试/生产用途
---
## 3. 旧实现废弃,但经验保留
### 3.1 Tkinter 桌面 UI 工程结构
- **旧实现**:Tkinter 主窗口 + 单体 GUI 文件
- **处理方式**:废弃为新主线实现
- **保留经验**:哪些业务交互复杂、哪些模块最容易耦合
### 3.2 安装版同步 / 发布脚本
- **旧实现**:`deploy_to_installed.py`、安装版同步链
- **处理方式**:不带入新 SaaS 主线
- **保留经验**:发布必须有清单、运行时文件必须明确登记
### 3.3 桌面版更新体系
- **旧实现**:本地补丁、manifest、rollback
- **处理方式**:不直接复用
- **保留经验**:版本状态、回滚、失败恢复、补丁安全校验思路
### 3.4 临时修复脚本文化
- **旧实现**:一批为临时问题而生的脚本
- **处理方式**:明确不继承到新主线
- **保留经验**:哪些问题容易高频复发,需要产品级能力解决
---
## 4. 必须重建的基础设施
### 4.1 新项目仓库结构
必须重建:
- 新仓库目录结构
- 前后端分层结构
- 文档结构
- 基础模块布局
### 4.2 Web/SaaS CI/CD 流水线
必须重建:
- 前端构建
- 后端测试
- 镜像/部署包构建
- 测试环境部署
- 正式环境部署
- 回滚流程
### 4.3 鉴权体系
必须重建:
- 用户认证
- 会话管理
- 权限控制
- 团队/工作区边界(如果需要)
### 4.4 存储体系
必须重建:
- 对象存储 / 素材存储方案
- 数据库存储方案
- 结果文件管理方案
- 清理和归档策略
### 4.5 任务体系
必须重建:
- 任务队列
- 异步生成任务
- 重试机制
- 状态追踪
- worker 管理
### 4.6 观测性体系
必须重建:
- 日志
- 指标
- 告警
- 错误追踪
- 健康检查
### 4.7 配置与密钥管理
必须重建:
- 环境变量规范
- 密钥注入方式
- 不同环境配置管理
- 敏感信息隔离
---
## 5. 项目推进器要不要
### 结论
要,但先轻量,不要一开始就搞太重。
### 轻量方案即可支撑初期
- 需求清单
- 里程碑
- issue / 任务列表
- 发布记录
- 架构文档
### 暂时不需要的重型能力
- 复杂流程引擎
- 重度项目管理系统
- 过度自动化的工单流转
---
## 6. 巡检机器人要不要
### 结论
要保留,而且未来价值更大。
### 在新 SaaS 中的职责
巡检机器人未来可以检查:
- API 服务是否在线
- 队列是否堆积
- worker 是否离线
- 生成失败率是否异常
- 存储空间是否异常
- 数据库连接是否异常
- 定时任务是否失效
### 定位
- 它是运维和健康检查系统的一部分
- 不是业务开发主流程的一部分
---
## 7. CI/CD 要不要
### 结论
必须保留,而且必须升级。
### 新 SaaS 项目里必须具备的能力
- 代码检查
- 自动测试
- 构建
- 测试环境部署
- 正式环境部署
- 回滚
### 原则
- 新项目不能没有 CI/CD
- 但 CI/CD 流程必须服务于 SaaS,不是延用桌面版套路
---
## 8. 当前建议的基础设施策略
### 立即保留
- Gitea
- 现有 CI/CD 思路
- 巡检机器人思路
- 服务器资源
### 立即停止依赖旧实现
- 桌面版发布链
- 安装同步链
- Tkinter 工程结构
- 临时修复脚本文化
### 接下来优先重建
1. 新项目仓库结构
2. SaaS CI/CD
3. 鉴权体系
4. 存储体系
5. 任务体系
6. 观测性体系
---
## 9. 当前阶段结论
新 SaaS 项目不是从零开始,因为我们有:
- 仓库
- CI/CD 经验
- 巡检经验
- 服务器
- 业务规则
- 稳定性治理经验
但新 SaaS 也不能“沿用旧系统实现”,因为:
- 桌面架构不适合作为未来主线
- 发布方式完全不同
- 任务模型完全不同
- 存储模型完全不同
- 用户和权限模型未来也会不同
所以正确策略是:
- **保留资产**
- **废弃旧实现**
- **重建 SaaS 所需基础设施**
+268
View File
@@ -0,0 +1,268 @@
# SaaS 核心流程清单(第一版)
本文档定义新 SaaS 系统的一期核心业务流程。目的不是画所有细节,而是先明确系统必须跑通哪些主链路。
---
## 1. 流程设计原则
核心流程必须满足:
- 覆盖现有桌面版的主要功能价值
- 适合 SaaS 架构而不是桌面软件思维
- 支持异步任务、多人协作、后续扩展
- 允许阶段性交付,不要求一开始全量上线
---
## 2. 一期核心主流程
建议一期至少跑通 6 条主流程:
1. 登录与进入工作空间
2. 创建项目
3. 上传并管理素材
4. 素材分类与诊断
5. 发起生成 / 批量生成
6. 查看结果与下载
---
## 3. 流程一:登录与进入工作空间
### 目标
让用户能够进入自己的工作环境。
### 基本步骤
1. 用户登录
2. 进入默认工作空间
3. 查看项目列表
4. 选择已有项目或创建新项目
### 一期建议
- 可以先简化为单用户单工作空间
- 但模型上要保留工作空间概念
---
## 4. 流程二:创建项目
### 目标
为一轮视频生成工作建立上下文容器。
### 基本步骤
1. 用户点击创建项目
2. 输入项目名称
3. 系统创建项目
4. 跳转到项目工作台
### 项目工作台未来应承载
- 素材库
- 配音库
- 策略
- 任务
- 结果
- 诊断
---
## 5. 流程三:上传并管理素材
### 目标
把桌面版“本地素材库”的能力转化成 SaaS 中的项目级素材管理。
### 子流程
#### 5.1 上传视频/图片素材
1. 选择项目
2. 上传文件
3. 系统保存到素材库
4. 返回素材列表
#### 5.2 上传配音素材
1. 选择项目或配音库
2. 上传配音文件
3. 系统去重/校验
4. 返回配音列表
#### 5.3 管理素材库
包括:
- 新建素材库
- 删除素材库
- 查看素材详情
- 过滤、搜索、统计
### 一期建议
- 先做项目级素材上传和列表
- 库管理可以保留轻量版
---
## 6. 流程四:素材分类与诊断
### 目标
复用现有桌面版已经验证过的价值:
- 自动分类素材
- 给出素材准备度
- 找出缺口和风险
### 基本步骤
1. 用户触发分类/诊断
2. 系统创建异步任务
3. worker 执行分类与诊断
4. 用户看到进度
5. 分类结果回写素材
6. 诊断结果展示到项目界面
### 关键要求
- 长任务异步执行
- 坏文件不能拖垮整批
- 分类和诊断结果必须可追踪
---
## 7. 流程五:发起生成 / 批量生成
### 目标
这是核心价值流程,必须作为系统最重要主链路设计。
### 5.1 单次生成流程
1. 用户选择项目
2. 选择/配置生成策略
3. 选择素材库与配音库
4. 提交生成请求
5. 系统创建生成任务
6. worker 执行生成
7. 返回进度和日志
8. 产出成片结果
### 5.2 批量生成流程
1. 用户配置批量规则
2. 提交批量任务
3. 系统拆分候选
4. 每个候选独立执行
5. 单候选失败不拖垮全批次
6. 汇总成功/失败/跳过结果
### 关键要求
- 任务必须异步
- 状态必须可跟踪
- 单项失败隔离
- 支持取消 / 停止 / 重试(后续逐步补)
---
## 8. 流程六:查看结果与下载
### 目标
让用户消费系统产出的最终价值。
### 基本步骤
1. 用户打开项目结果页
2. 查看成片列表
3. 预览结果
4. 下载单个成片或结果包
### 后续可扩展
- 一键发布
- 结果同步
- 团队复核
- 版本对比
---
## 9. 配套支撑流程
除了主流程,还需要这些支撑流程:
### 9.1 任务状态查看
- 查看执行中任务
- 查看失败原因
- 查看进度
### 9.2 日志与诊断
- 查看任务日志
- 查看错误摘要
- 查看系统健康状态
### 9.3 Worker 管理
- 节点在线状态
- 任务分配情况
- 队列积压情况
### 9.4 审计与操作记录
- 谁上传了什么
- 谁删除了什么
- 谁发起了任务
- 失败发生在什么时候
---
## 10. 一期暂缓流程
这些不是现在最先必须做的:
- 支付 / 套餐
- 模板市场
- 多层级复杂权限
- 外部平台自动发布
- 高级协同审批流
这些后面可以做,但不应该阻塞 SaaS 第一版启动。
---
## 11. 一期主线 MVP 建议
建议先确保以下闭环跑通:
1. 登录
2. 创建项目
3. 上传素材
4. 素材分类
5. 发起生成
6. 查看进度
7. 下载结果
如果这条链能稳定跑通,SaaS 第一版就已经有真实价值。
---
## 12. 当前阶段结论
当前最重要的是:
- 不要试图一口气把所有桌面能力都搬完
- 先抓住真正的主链路
- 先让 SaaS 的关键业务闭环成立
下一步应继续:
- 根据核心对象和核心流程,开始做技术栈选型
+353
View File
@@ -0,0 +1,353 @@
# SaaS 核心对象清单(第一版)
本文档定义新 SaaS 系统的核心业务对象。目标不是现在就把数据库字段写死,而是先把系统世界观定清楚,避免后续边开发边返工。
---
## 1. 设计原则
核心对象必须满足:
- 能覆盖当前桌面版的主要业务能力
- 能支撑未来多人协作和迭代
- 能清楚区分“谁拥有、谁使用、谁产生、谁消费”
- 不因为桌面版历史实现而被绑死
---
## 2. 顶层对象分组
新 SaaS 的核心对象建议分为 6 组:
1. 用户与组织
2. 工作空间与项目
3. 素材与素材库
4. 生成与任务
5. 结果与发布
6. 系统与运维
---
## 3. 用户与组织对象
### 3.1 User(用户)
代表系统中的登录使用者。
职责:
- 登录系统
- 创建项目
- 上传素材
- 发起生成
- 查看结果
- 管理自己的资源
后续待确认:
- 是否支持多个角色
- 是否支持子账号
- 是否支持团队成员
### 3.2 Team / Workspace(团队 / 工作空间)
如果未来不是纯个人 SaaS,就必须有这个对象。
职责:
- 隔离不同用户组的数据
- 承载成员、项目、素材库、任务
- 作为权限边界
当前建议:
- 先按“工作空间”概念设计
- 一期即使只支持单用户,也保留扩展位
### 3.3 Membership(成员关系)
表示用户属于哪个工作空间、具备什么权限。
职责:
- 权限控制
- 团队协作
- 后续审计与操作归属
---
## 4. 工作空间与项目对象
### 4.1 Project(项目)
新 SaaS 里最核心的业务容器之一。
职责:
- 承载一组素材、配音、策略、任务、生成结果
- 作为用户操作的主要上下文
为什么必须有:
桌面版很多逻辑都默认“当前工作台上下文”,SaaS 里必须把这个上下文对象化。
### 4.2 Folder / Collection(可选)
如果素材量很大,后续可能需要集合/文件夹逻辑。
一期建议:
- 不作为强制核心对象
- 预留二级组织能力即可
---
## 5. 素材与素材库对象
### 5.1 AssetLibrary(素材库)
素材库的统一抽象。
建议未来不要把“视频库”和“配音库”写成完全不同的两套模型,而是:
- 统一为 `AssetLibrary`
- 通过 `kind` 区分:
- `video`
- `voice`
- 后续可扩展 `image` / `template` / `subtitle`
职责:
- 组织素材
- 隔离不同项目/工作空间资源
- 提供筛选、统计、权限、归属能力
### 5.2 Asset(素材)
素材的统一抽象。
可能包括:
- 视频素材
- 图片素材
- 配音素材
- 背景音频
- 模板文件(未来)
关键属性层面未来要支持:
- 所属库
- 所属项目 / 工作空间
- 文件地址
- 文件元数据
- 分类结果
- 质量状态
- 风险标记
- 使用统计
### 5.3 AssetClassification(素材分类结果)
当前桌面版已经有成熟经验,这个对象未来应该独立表达。
职责:
- 记录素材分类结果
- 记录口播/场景/待复核等业务标签
- 作为生成策略选择依据
### 5.4 AssetDiagnosis(素材诊断结果)
用于表达素材准备度、缺口、建议。
职责:
- 给项目/素材库生成健康判断
- 给用户输出“缺什么、风险在哪里、建议怎么补”
---
## 6. 配音与策略对象
### 6.1 VoiceAsset(配音素材)
虽然可以作为 Asset 的一种,但业务上很重要,需要单独强调。
职责:
- 作为生成输入
- 支持试听、复核、重复检测、质量检查
### 6.2 GenerationStrategy(生成策略)
桌面版里已经存在生成策略概念,SaaS 里必须保留,而且要对象化。
职责:
- 定义生成模式
- 定义视频库/配音库选择方式
- 定义阈值、数量、规则
- 支持后续模板化与复用
### 6.3 StrategyTemplate(策略模板,可选)
如果未来要做更强复用,这个对象会非常有用。
职责:
- 保存可复用的生成策略
- 支持团队共享
---
## 7. 生成与任务对象
### 7.1 GenerationRequest(生成请求)
表示用户点击“开始生成”时提交的一次业务请求。
职责:
- 记录用户发起的意图
- 绑定项目、策略、素材范围、配置
### 7.2 GenerationTask(生成任务)
表示后台真正执行的一次任务。
职责:
- 追踪状态
- 分配 worker
- 记录进度
- 支持失败重试
- 支持日志与诊断
### 7.3 BatchTask(批量任务)
桌面版已经证明单个任务和批量任务不是一个复杂度级别,所以建议单独对象化。
职责:
- 管理一批生成候选
- 统计成功/失败/跳过
- 支持单候选失败隔离
### 7.4 TaskAttempt / TaskRun(任务执行记录)
如果未来要支持重试、回放、错误审计,这个对象很重要。
职责:
- 记录某个任务的第几次执行
- 记录失败原因、耗时、日志摘要
---
## 8. 结果与发布对象
### 8.1 GeneratedVideo(生成结果 / 成片)
表示最终产出的业务结果。
职责:
- 保存成片元信息
- 绑定来源任务
- 供下载、预览、发布、同步
### 8.2 ResultPackage(结果包,可选)
如果后续要把视频、封面、字幕、元信息打包下载,可以引入。
### 8.3 PublishRecord(发布记录,可选)
如果未来要做发布/分发到外部平台,这个对象需要提前预留。
---
## 9. 系统与运维对象
### 9.1 WorkerNode(执行节点)
SaaS 版里视频生成大概率不应该和 Web 请求进程混在一起,所以需要执行节点概念。
职责:
- 执行长任务
- 上报健康状态
- 接受任务分派
### 9.2 SyncJob(同步任务)
当前桌面版有云同步经验,SaaS 里如果有多存储、多节点、多结果归档,也会需要同步任务抽象。
### 9.3 AuditLog(审计日志)
职责:
- 记录谁在什么时候做了什么
- 支持排错、责任追踪、安全审计
### 9.4 SystemEvent / HealthCheck(系统事件 / 健康检查)
职责:
- 观测性
- 巡检
- 告警
---
## 10. 一期必须优先确认的对象
最先必须定死的是:
1. User
2. Workspace
3. Project
4. AssetLibrary
5. Asset
6. GenerationStrategy
7. GenerationTask
8. BatchTask
9. GeneratedVideo
10. WorkerNode
这 10 个对象决定了 SaaS 的主骨架。
---
## 11. 当前建议
### 一期推荐采用的对象骨架
- 用户
- 工作空间
- 项目
- 统一素材库
- 统一素材对象
- 生成策略
- 单任务 / 批量任务
- 成片结果
- Worker 节点
- 审计日志
### 暂时可后补的对象
- 套餐 / 支付
- 发布记录
- 模板市场
- 高级权限体系
- 更复杂的目录树/集合体系
---
## 12. 当前阶段结论
新 SaaS 项目最重要的不是“先建多少表”,而是先把对象世界观定清楚。
当前建议已经足够支持下一步:
- 继续定义核心流程
- 再根据对象和流程定技术栈
- 之后再设计数据库和 API
+228
View File
@@ -0,0 +1,228 @@
# SaaS 开发协作流程与制度(第一版)
本文档定义新 SaaS 项目的开发流程、提交流程、验收流程和 AI 协作制度。
目标:
- 避免边想边改
- 避免多人同时乱改
- 避免 AI 输出直接进主线
- 让每一步都可回溯、可检查、可收口
---
## 1. 总流程
新 SaaS 项目统一按 7 步执行:
1. 定目标
2. 写文档
3. 拆任务
4. 做设计
5. 写代码
6. Review / 测试
7. 提交 / 发布
禁止跳步直接开写。
---
## 2. 需求到开发的流程
### 第 1 步:需求确认
由老大提出:
- 目标
- 用户对象
- 边界
- 验收标准
### 第 2 步:小虾整理
小虾负责:
- 转成结构化需求
- 明确范围
- 识别依赖
- 标记风险
### 第 3 步:文档先行
至少先补以下之一:
- PRD / 范围文档
- 架构文档
- 流程文档
- 数据模型草稿
- API 草稿
文档不清,不开工。
### 第 4 步:任务拆解
一个功能必须拆成明确任务,例如:
- 前端页面
- API 接口
- 用例层
- 数据模型
- 任务执行链
- 测试
### 第 5 步:实现
按既定分工执行:
- UI AI 先出页面方案
- 小虾定架构和边界
- 编码 AI 实现模块
### 第 6 步:Review 和测试
必须独立执行:
- Review AI 看架构和风险
- QA AI 看测试和回归
- CI 跑自动检查
### 第 7 步:收口
小虾负责:
- 汇总变更
- 检查一致性
- 确认是否满足验收标准
- 再执行提交/推送/发布
---
## 3. 提交流程
统一流程:
1. 开发完成
2. 本地检查 / 单测 / 必要验证
3. Review
4. CI 通过
5. `git add`
6. `git commit`
7. `git push`
8. 记录发布或变更说明
铁律:
- 不能停在 commit
- 没 review / 没测试 / 没 CI 通过的代码不能进主线
---
## 4. 验收流程
每个功能都必须有验收标准。
验收至少包含:
- 功能是否符合目标
- 是否超出边界
- 是否破坏架构
- 是否有测试
- 是否可回归
- 是否文档同步
如果只是“代码写出来了”,不算完成。
---
## 5. AI 协作制度
### 5.1 分工制度
- 老大:产品 owner
- 小虾:技术 owner
- UI AI:页面和交互
- 编码 AI:具体实现
- Review AI:代码审查
- QA AI:测试和回归
### 5.2 一个任务一个 owner
- 一个任务只能有一个主执行者
- 其他 AI 可以辅助,不能平行乱改
### 5.3 架构收口权唯一
- 架构方向由小虾收口
- 老大拍板
- 其他 AI 不能随意改主方向
### 5.4 AI 输出不能直接上线
AI 输出必须经过:
- 人工确认
- Code Review
- 自动化测试
- CI 验证
---
## 6. 分支与变更管理建议
如果新项目正式启动,建议采用:
- `master` / `main`:稳定主线
- 功能分支:按模块开发
- 重要改动走 PR / 合并审查
如果前期仍是小团队轻量开发,也至少要做到:
- 每次改动范围明确
- commit message 清楚
- 一轮改动一个主题
---
## 7. 发布流程
发布前必须确认:
- 功能验收通过
- 回归通过
- CI 通过
- 文档更新
- 日志/监控可用
- 回滚方式明确
发布后必须确认:
- 服务在线
- 核心流程在线可用
- 巡检和告警正常
---
## 8. 不允许再发生的情况
- 边想边改、越改越乱
- 没有文档直接开工
- 多个 AI 同时改同一块逻辑
- AI 代码直接进主线
- 口头规则替代正式制度
- 旧桌面版那种临时修复脚本文化带入新项目
---
## 9. 当前阶段结论
新 SaaS 项目从第一天开始就要按正规软件流程走:
- 先定目标
- 再写文档
- 再拆任务
- 再设计
- 再实现
- 再 review / 测试
- 最后提交 / 发布
这样我们后面人再多、AI 再多,也不会重新回到混乱状态。
+66
View File
@@ -0,0 +1,66 @@
# SaaS 重建总索引
本文档是新 SaaS 项目的总入口。以后所有重建相关文档都从这里进入,避免分散。
---
## 1. 总目标
- 旧桌面版不再作为未来主线
- 新 SaaS 项目重新开始
- 先定边界、规矩、工具,再进入开发
- 所有新项目工作必须新旧隔离
---
## 2. 核心总文档
### 启动与总纲
- [SaaS 重建启动方案](saas-rebuild-kickoff-plan.md)
- [SaaS 重建盘点总表](saas-rebuild-inventory.md)
### 工具与基础设施
- [AI 团队工具清单](ai-team-tooling-checklist.md)
- [基础设施保留 / 废弃 / 重建清单](infrastructure-keep-rebuild-matrix.md)
### 业务与技术
- [SaaS 核心对象清单](saas-core-objects.md)
- [SaaS 核心流程清单](saas-core-flows.md)
- [SaaS 技术栈选型表](saas-tech-stack-options.md)
### 启动范围与工程制度
- [SaaS 一期 MVP 范围清单](saas-mvp-scope.md)
- [SaaS 项目目录结构规范](saas-project-structure-spec.md)
- [SaaS 开发协作流程与制度](saas-development-workflow.md)
---
## 3. 阅读顺序
建议按这个顺序看:
1. `saas-rebuild-kickoff-plan.md`
2. `saas-rebuild-inventory.md`
3. `ai-team-tooling-checklist.md`
4. `infrastructure-keep-rebuild-matrix.md`
5. `saas-core-objects.md`
6. `saas-core-flows.md`
7. `saas-tech-stack-options.md`
8. `saas-mvp-scope.md`
9. `saas-project-structure-spec.md`
10. `saas-development-workflow.md`
---
## 4. 当前结论
现在已经具备启动 SaaS 重建前最重要的文档骨架。下一步应该进入:
- 新项目落点确认
- 仓库/目录方案确认
- 第一版工程骨架设计
- 之后才进入真正开发
+194
View File
@@ -0,0 +1,194 @@
# SaaS 一期 MVP 范围清单
本文档定义新 SaaS 项目的第一阶段最小可用版本(MVP)范围。
目标不是把桌面版全部能力一口气搬完,而是先做出一条 **真实可用、能闭环、能上线、能继续扩展** 的主链路。
---
## 1. MVP 目标
一期 MVP 必须实现:
- 用户能登录进入系统
- 用户能创建项目
- 用户能上传素材
- 系统能完成素材分类
- 用户能发起生成任务
- 用户能看到任务进度
- 用户能拿到生成结果
一句话:
**从“进入系统”到“拿到成片”这条主链必须完整跑通。**
---
## 2. 一期必须做的能力
### 2.1 账号与工作空间
必须做:
- 登录
- 基础用户系统
- 默认工作空间
一期先简化:
- 单用户 / 单工作空间也可以
- 但数据模型必须保留未来扩展到团队/多租户的空间
### 2.2 项目管理
必须做:
- 创建项目
- 项目列表
- 项目详情页 / 工作台入口
不必一开始就做:
- 项目归档
- 项目模板
- 复杂项目权限
### 2.3 素材管理
必须做:
- 上传视频/图片素材
- 上传配音素材
- 查看素材列表
- 查看配音列表
- 基础素材库概念
必须具备的质量底线:
- 基础去重/错误校验
- 基础状态可见
- 明确的归属(项目 / 工作空间)
### 2.4 素材分类与诊断
必须做:
- 触发素材分类任务
- 返回分类结果
- 能看到素材准备度 / 诊断摘要
一期可以简化:
- 先做基础分类与基础诊断
- 高级风险评分和复杂建议后续再增强
### 2.5 生成任务
必须做:
- 提交单次生成任务
- 提交批量生成任务
- 后台异步执行
- 查询任务状态
- 查询任务进度
必须保留的关键规则:
- 单项失败不能拖垮整批
- 长任务不能阻塞主请求
- 任务必须有状态、日志和失败原因
### 2.6 成片结果
必须做:
- 结果列表
- 结果详情 / 基础信息
- 下载成片
一期可以不做:
- 一键发布外部平台
- 高级审核流
- 结果版本对比
---
## 3. 一期建议做,但不是强阻塞
这些能力建议尽量做,但如果影响整体交付,可以延后到 1.1 或 1.2:
- 素材筛选与搜索
- 配音试听
- 更丰富的诊断报告
- 任务失败后的手动重试
- 结果包下载
- 更细的操作日志
---
## 4. 一期明确不做的内容
以下内容不应阻塞 MVP 启动:
- 支付 / 套餐系统
- 模板市场
- 复杂 RBAC 权限体系
- 多层级审批流
- 自动发布到外部平台
- 超复杂的数据看板
- 高级团队协作机制
- 面向外部客户的多租户计费能力
这些都是未来功能,不属于 MVP 起步必要条件。
---
## 5. MVP 验收标准
一期 MVP 完成的标准不是“页面很多”,而是以下闭环成立:
1. 用户登录进入工作空间
2. 创建项目
3. 上传视频/图片/配音素材
4. 触发素材分类
5. 提交单次或批量生成
6. 系统后台执行任务
7. 前端可见任务状态和进度
8. 用户能查看并下载结果
如果这 8 步稳定成立,MVP 就合格。
---
## 6. MVP 技术验收标准
除了功能闭环,还必须满足工程验收:
- 代码结构符合目标架构
- 前后端分层清晰
- 核心接口有文档
- 核心任务链有自动化测试
- CI 可跑通
- 有基础日志和错误追踪
- 有最小可用部署方案
---
## 7. 当前建议结论
一期 MVP 只抓核心价值:
- 项目
- 素材
- 分类
- 生成
- 结果
先把主链路做实,再逐步叠加:
- 团队协作
- 高级权限
- 发布能力
- 运营能力
- 商业化能力
+123
View File
@@ -0,0 +1,123 @@
# SaaS 新项目落点与启动方式(第一版)
本文档定义新 SaaS 项目的物理落点、仓库边界和启动方式。
---
## 1. 目标
新 SaaS 项目必须做到:
- 与旧桌面版彻底隔离
- 有单独仓库边界或至少单独根目录边界
- 可以独立开发、测试、部署
- 不会把旧实现混进新主线
---
## 2. 推荐落点策略
### 2.1 推荐方案:独立仓库 + 独立目录
最稳妥方案是:
- 新 SaaS 使用独立 Git 仓库
- 新项目有独立顶层目录
- 旧桌面版继续保留在当前仓库中作为参考样本
### 2.2 推荐目录命名
如果在当前机器上先做本地启动,可以使用:
- `F:\openclaw-saas\`:新项目总目录
- `F:\openclaw-saas\docs\`
- `F:\openclaw-saas\apps\web\`
- `F:\openclaw-saas\apps\api\`
- `F:\openclaw-saas\apps\worker\`
- `F:\openclaw-saas\packages\domain\`
- `F:\openclaw-saas\packages\application\`
- `F:\openclaw-saas\packages\ports\`
- `F:\openclaw-saas\packages\adapters\`
### 2.3 推荐仓库命名
建议仓库名保持清楚:
- `xiaoxia-saas`
- 或 `xiaoxia-autocut-saas`
原则:
- 名字要能一眼看出是新主线
- 不要和旧桌面版仓库名字混淆
---
## 3. 为什么推荐独立仓库
### 好处
- 物理隔离最彻底
- 不会把旧桌面版文件误加进来
- CI/CD、依赖、部署都可以单独设计
- 后续多人协作更清楚
- 更符合正规 SaaS 项目的工程实践
### 风险
- 初期需要重新搭仓库和流水线
- 需要重新整理文档和工程规范
这个成本是值得的,因为它换来的是长期稳定性。
---
## 4. 启动方式
### 4.1 启动前先做的事
1. 确认产品边界
2. 确认一期 MVP
3. 确认技术栈
4. 确认目录结构
5. 确认 AI 工具清单
6. 确认协作制度
### 4.2 启动时要先建的东西
- 新仓库
- docs 总目录
- domain/application/ports/adapters 基础包
- web/api/worker 三个应用入口
- CI/CD 初版
- 测试目录
### 4.3 启动时不先做的东西
- 不先做复杂权限
- 不先做支付
- 不先做多租户高级方案
- 不先做复杂发布市场
---
## 5. 与旧桌面版的关系
- 旧桌面版继续保留在当前仓库中
- 旧桌面版不再继续作为未来主线开发
- 新 SaaS 只参考旧桌面版的业务规则和经验
- 新 SaaS 不依赖旧桌面版的安装链路和 Tkinter 结构
---
## 6. 当前建议结论
### 最佳启动方案
- 独立仓库
- 独立目录
- 独立 CI/CD
- 独立文档
- 独立发布链路
这样新项目才能真正从混乱旧代码中脱离出来。
+247
View File
@@ -0,0 +1,247 @@
# SaaS 项目目录结构规范(第一版)
本文档定义新 SaaS 项目的目录结构和职责边界。
原则:
- 新旧隔离
- 前后端隔离
- 业务层和框架层隔离
- 目录一旦定下,后续新增功能必须按结构落位
---
## 1. 顶层结构建议
```text
saas/
docs/
apps/
web/
api/
worker/
packages/
domain/
application/
ports/
adapters/
shared/
infra/
docker/
nginx/
scripts/
tests/
integration/
e2e/
```
---
## 2. 目录职责
### 2.1 `docs/`
存放所有正式文档:
- PRD
- 架构文档
- 数据模型
- API 规范
- 发布说明
- 运维说明
- 里程碑和范围文档
规则:
- 文档必须是单一真相源
- 先改文档,再改实现
### 2.2 `apps/web/`
前端 Web 应用。
职责:
- 页面
- 路由
- 组件
- 页面状态绑定
- 调用后端 API
禁止:
- 在这里写核心业务规则
- 在这里写任务执行逻辑
### 2.3 `apps/api/`
后端 API 应用。
职责:
- 提供 HTTP API
- 鉴权
- 请求校验
- 调用 Application Use Cases
禁止:
- 把业务规则直接写在路由层
- 直接耦合底层外部实现
### 2.4 `apps/worker/`
后台任务执行应用。
职责:
- 执行素材分类任务
- 执行视频生成任务
- 执行同步和诊断任务
原则:
- Worker 只执行任务,不处理 Web 交互
### 2.5 `packages/domain/`
核心业务对象和值对象。
职责:
- User
- Workspace
- Project
- Asset
- AssetLibrary
- GenerationTask
- GeneratedVideo
- 业务规则
禁止:
- import Web 框架
- import 数据库驱动
- import UI 工具
### 2.6 `packages/application/`
用例层。
职责:
- 创建项目
- 上传素材
- 分类素材
- 发起生成
- 查询进度
- 获取结果
原则:
- 编排业务流程
- 通过 ports 调外部能力
### 2.7 `packages/ports/`
接口定义层。
职责:
- 仓储接口
- 存储接口
- 队列接口
- 通知接口
- 日志接口
原则:
- 不依赖具体实现
### 2.8 `packages/adapters/`
外部实现层。
职责:
- PostgreSQL 实现
- Redis / Celery 实现
- OSS / S3 实现
- 鉴权实现
- 日志实现
### 2.9 `packages/shared/`
共享的通用工具和基础类型。
职责:
- 通用配置
- 错误类型
- 公共 schema
- 公共常量
规则:
- 只放真正跨层复用的通用内容
- 禁止把业务逻辑偷塞进 shared
### 2.10 `infra/`
基础设施目录。
职责:
- Docker
- Nginx
- 启动脚本
- 本地开发环境脚本
- 部署脚本
### 2.11 `tests/`
测试目录。
建议拆分:
- `integration/`:前后端/数据库/队列联调测试
- `e2e/`:核心流程端到端测试
应用/包内部也可各自保留单元测试目录。
---
## 3. 模块新增规则
以后新增功能时,按这个顺序决定落点:
1. 是业务对象?放 `domain`
2. 是业务用例?放 `application`
3. 是接口定义?放 `ports`
4. 是外部实现?放 `adapters`
5. 是页面或交互?放 `apps/web`
6. 是 API 路由?放 `apps/api`
7. 是后台任务执行?放 `apps/worker`
8. 是部署与运行?放 `infra`
---
## 4. 禁止事项
- 禁止把旧桌面版目录结构照搬过来
- 禁止在 `web` / `api` 中直接写核心业务规则
- 禁止 `domain` 依赖数据库、Redis、OSS、框架
- 禁止 `shared` 成为大杂烩
- 禁止把临时脚本堆进项目根目录
---
## 5. 当前阶段结论
新 SaaS 项目必须从第一天就按这个结构起步。
这样做的价值是:
- 不会再回到单体混乱结构
- AI 工具更容易分工协作
- 后面扩功能、拆模块、加 worker 都更顺
+354
View File
@@ -0,0 +1,354 @@
# 小虾 SaaS 重建盘点总表(第一版)
本文档用于回答两个最核心的问题:
1. 我们现在已经有什么?
2. 我们为了启动新的 SaaS 项目,还缺什么?
目的不是罗列名词,而是为后续 **产品边界、技术选型、架构设计、AI 协作分工、开发排期** 提供真实依据。
---
## 1. 当前已有资产总览
当前资产分为 5 类:
1. 业务资产
2. 工程资产
3. 基础设施资产
4. AI 协作资产
5. 人和流程资产
这些是我们重建 SaaS 时真正可以利用的起点。
---
## 2. 业务资产盘点
### 2.1 已有功能样本
现有桌面版已经覆盖了未来 SaaS 需要继承的主要业务能力:
- 素材库管理
- 配音库管理
- 素材分类
- 素材缺口诊断
- 视频生成
- 批量生成
- 云同步
- 更新治理
- 错误治理
- 稳定性治理
### 2.2 已沉淀的业务规则
这些规则以后不一定直接复用代码,但应该复用为 SaaS 业务规则或用例设计输入:
- 坏素材文件必须隔离,不能拖垮整批扫描
- 单个候选生成失败不能拖垮整个批次
- 长任务必须异步执行,不能阻塞 UI / 主流程
- 更新必须可回滚、可记录状态
- 主流程必须有错误诊断和恢复能力
- 核心业务逻辑比表面覆盖率数字更重要
### 2.3 现有业务理解深度
我们已经搞清楚了这些关键业务对象和关系:
- 视频素材
- 配音素材
- 素材库
- 配音库
- 生成策略
- 批量生成任务
- 生成结果
- 云端同步对象
- 更新版本 / 补丁包
- 错误诊断记录
这意味着新 SaaS 不是从“需求不清楚”开始,而是从“实现要重做,但业务已经摸透”开始。
---
## 3. 工程资产盘点
### 3.1 现有代码资产
旧桌面版代码虽然不再作为未来主线,但仍然是非常重要的参考资产:
- 它提供了完整功能样本
- 它暴露了真实复杂度和真实坑点
- 它帮助我们识别哪些模块是高风险区
- 它为 SaaS 重建提供业务规则、异常分支、边界条件参考
### 3.2 已有架构资产
虽然旧系统的最终实现不一致,但我们已经沉淀出一些正确方向:
- `Domain → Repository → Service → Facade` 的分层意识
- `Controller / View` 分离意识
- `EventBus` 的组件解耦尝试
- `Ports / Adapters / Application / Presentation` 新基线目录已经建立
### 3.3 已有测试资产
当前旧项目已经有一定测试基础:
- controller 测试
- service 测试
- repository 测试
- domain 测试
- update rollback 测试
- startup guard 测试
- material isolation 测试
这些测试未来不一定直接复用,但可以帮助我们:
- 提取业务规则
- 识别边界条件
- 设计 SaaS 版测试策略
---
## 4. 基础设施资产盘点
### 4.1 当前已经有的基础设施
- **代码仓库**:Gitea
- **CI 基础**:已有 CI 经验和工作流雏形
- **巡检机器人**:已有自动巡检思路和脚本基础
- **开发环境**:Windows 开发机 + Ubuntu 服务器
- **发布/同步经验**:已有正式版同步、回滚、状态记录经验
### 4.2 这些资产的保留策略
#### 必须保留
- Gitea
- CI/CD 思路
- 巡检机器人
- 服务器资源
- 版本管理流程
#### 只保留经验,不保留实现
- Tkinter 桌面版安装发布链
- 桌面版同步脚本体系
- 旧桌面版兼容逻辑
- 临时修复脚本文化
---
## 5. AI 协作资产盘点
### 5.1 当前已经具备的 AI 协作方式
- 已经形成“你拍板、小虾收口”的稳定协作模式
- 已经知道 AI 不能一个人包完所有角色
- 已经明确未来需要分角色使用 AI:
- 架构
- UI
- 编码
- Review
- QA
- 文档
### 5.2 当前还没有真正落地的 AI 团队能力
还缺:
- 明确的 AI 工具采购清单
- 明确的 AI 账号归属
- 明确的角色与工具映射
- 明确的 AI 输入输出规范
- 明确的安全边界(密钥、服务器、数据库等)
---
## 6. 人和流程资产盘点
### 6.1 当前团队形态
本质上还是:
- 你:产品负责人
- 小虾:技术负责人 / 协调者
- 其他 AI:按角色配合
这是一个“小团队”模式,所以新系统必须遵守两个现实:
- 技术栈不能过重
- 流程不能过重
### 6.2 当前已有流程经验
我们已经明确过的重要工程流程:
- 源码只在单一源目录修改
- 修改后先验证
- 再同步正式版
- 再确认
- 再 `git add / commit / push`
- 不能停在 commit
这个流程意识未来可以保留,但要升级为 SaaS 项目的正式流程。
---
## 7. 当前最核心的缺口
下面这些是现在真正缺少的,而且不补就不能稳启动 SaaS 项目。
### 7.1 产品边界缺口
还没最终拍板:
- SaaS 面向谁
- 单用户还是团队使用
- 是否多租户
- 是否有工作区概念
- 是否要权限系统
- 是否要付费/套餐系统
### 7.2 系统边界缺口
还没最终拍板:
- 素材文件存储在哪里
- 视频生成在什么节点执行
- 是否需要分布式 worker
- 是否有对象存储 / OSS / S3
- 是否支持多设备 / 多节点协同
### 7.3 技术选型缺口
还没确认:
- 前端框架
- 后端框架
- 数据库
- 队列系统
- 任务调度方式
- 鉴权方式
- 部署方式
- 日志/监控方案
### 7.4 工程制度缺口
还没正式落地:
- SaaS 项目目录结构
- API 规范
- 数据模型规范
- 提交流程
- 分支策略
- 测试规范
- 发布规范
- 回滚规范
### 7.5 AI 团队工具缺口
还没确认和落地:
- 编码 AI 用哪些
- UI AI 用哪些
- Review AI 用哪些
- QA AI 用哪些
- 工具怎么购买 / 开通 / 管理
- 哪些人/角色使用哪些工具
---
## 8. 当前最应该保留的东西
### 必须保留
- 业务规则和业务理解
- Gitea 仓库体系
- CI/CD 思路
- 巡检/健康检查能力
- 稳定性治理经验
- 测试意识
- 版本与回滚意识
### 必须放弃
- 继续在旧桌面版上做主线重构
- 临时脚本救火式开发
- 架构半拉子状态
- 一边想边改、一边改一边返工
---
## 9. 下一步盘点顺序
现在不应该直接进入代码开发。
接下来建议按这个顺序盘:
### 第 1 张表:我们现在有什么(当前文档)
已开始。
### 第 2 张表:我们还缺什么
在当前文档中已经列出核心缺口,但还需要进一步定级:
- 必须先有
- 可以后补
- 暂时不做
### 第 3 张表:AI 团队工具清单
需要补充:
- 工具名称
- 用途
- 谁用
- 优先级
- 获取方式
- 是否立即需要
### 第 4 张表:基础设施保留 / 废弃 / 重建清单
需要明确:
- 哪些沿用
- 哪些只保留经验
- 哪些必须全新搭建
---
## 10. 当前阶段的结论
### 我们现在真正拥有的,不只是代码
我们拥有:
- 完整业务样本
- 已知复杂度
- 已知坑点
- 已知异常处理经验
- 已有工程基础设施
- 已有协作方式
### 我们现在真正缺的,也不是代码
我们缺:
- 清晰边界
- 明确规则
- 技术选型
- AI 工具体系
- 正式工程制度
### 所以下一步的重点
不是“赶紧开工写 SaaS 代码”,而是:
- 继续把盘点做全
- 把边界定死
- 把工具准备好
- 把制度落成文档
只有这样,新的 SaaS 项目才能真正轻装上阵,而不是把旧系统的问题带过去。
+577
View File
@@ -0,0 +1,577 @@
# 小虾 SaaS 重建启动方案
## 1. 背景与结论
当前桌面版代码已经不适合作为未来主线继续演进。
核心结论:
- 旧桌面版停止作为未来主线,不再继续做架构性重构。
- 新系统按 **SaaS 版** 重新开始开发。
- 目标不是“在旧系统上修补”,而是按正规软件工程方式做一个长期可维护、可扩展、可迭代的新系统。
- 旧代码只作为 **业务参考样本**、**规则参考**、**流程参考**,不再作为主开发容器。
- 新旧代码必须彻底隔离,避免再次混乱。
## 2. 总目标
做一个全新的 SaaS 系统,业务功能覆盖当前桌面版的核心能力:
- 素材库管理
- 配音库管理
- 素材分类与诊断
- 视频生成
- 批量生成
- 云同步
- 更新/版本机制(未来按 SaaS 方式重新设计)
- 任务进度与结果管理
目标要求:
- 可长期迭代
- 可多人协作开发
- 可上线部署
- 可观察、可回滚、可测试
- 不再回到“哪里报错改哪里”的状态
## 3. 基本原则
### 3.1 先定规则,再开始
在新项目开始编码前,必须先确认:
- 产品边界
- 一期范围
- 核心对象
- 核心流程
- 技术栈
- 架构分层
- 开发规范
- 发布规范
- AI 团队协作方式
### 3.2 新旧彻底隔离
- 旧桌面版代码继续保留,但只作参考
- 新 SaaS 项目必须在全新目录中开始
- 禁止把旧桌面版代码直接混入新系统主线
- 旧系统的安装脚本、Tkinter 逻辑、桌面发布链路不带入新主线
### 3.3 先盘点,再设计,再开发
执行顺序必须固定:
1. 盘点现状
2. 确认边界
3. 明确规则
4. 设计架构
5. 搭项目骨架
6. 分模块开发
7. Review / 测试 / 部署
## 4. 新系统目标架构
采用:
- **Clean Architecture**
- **MVVM / Presenter**
- **Ports / Adapters**
目标分层:
```text
Domain
↑
Application / Use Cases
↑
Ports
↑
Adapters
↑
Presentation (Presenter / ViewModel)
↑
Web UI / API / Worker
```
原则:
- 界面不写业务逻辑
- 业务逻辑不依赖框架
- 外部能力通过接口接入
- 任务、文件、存储、云服务全部可替换
## 5. 我们现在已有的资产
### 5.1 业务资产
- 现有桌面版就是最完整的业务样本
- 已经沉淀出大量业务规则和异常治理经验
- 已经明确的功能模块:
- 素材库
- 配音库
- 批量生成
- 云同步
- 素材分类
- 缺口诊断
- 更新治理
- 稳定性治理
### 5.2 工程资产
- Gitea 代码仓库
- CI / 基础自动化能力
- 巡检机器人
- 项目推进经验
- 既有测试体系雏形
- 架构规约基础文档
### 5.3 人和协作资产
- 你负责产品方向和最终拍板
- 小虾负责技术架构、拆解、规范、收口
- 已经形成连续迭代经验
## 6. 我们明显还缺的东西
### 6.1 产品边界
还需要明确:
- SaaS 面向谁
- 是个人 SaaS 还是团队 SaaS
- 是否多租户
- 是否有角色权限系统
- 是否有套餐/支付
### 6.2 系统边界
还需要明确:
- 素材文件存在哪里
- 视频生成在哪里跑
- 任务如何调度
- 节点/worker 怎么管理
- 哪些功能是一期,哪些延后
### 6.3 技术边界
还需要明确:
- 前端框架
- 后端框架
- 数据库
- 队列系统
- 对象存储
- 鉴权方式
- 部署方式
- 监控和日志方案
## 7. AI 团队工具怎么来
### 7.1 原则
前期 **不自研 AI 平台**。
策略:
- 直接购买/使用现成 AI 工具
- 我们自己做的是协作机制、流程和规则
- 工具是能力来源,制度才是稳定产出的关键
### 7.2 工具来源分类
#### 编码类 AI
用于:
- 写代码
- 重构
- 补测试
- 查仓库
- 做具体模块实现
候选工具:
- Codex / Codex CLI
- Claude Code
- Cursor
- Windsurf
#### 通用大模型类
用于:
- 需求整理
- 架构讨论
- 文档撰写
- 方案对比
- 总结与沟通
候选工具:
- ChatGPT
- Claude
- Gemini
#### UI / 原型类
用于:
- 页面草图
- 交互流程
- 组件原型
- 设计样稿
候选工具:
- Figma
- Figma AI
- v0
- Lovable
#### 协作 / 自动化基础设施
用于:
- 代码托管
- 持续集成
- 告警巡检
- 发布流程
- 项目推进
保留资产:
- Gitea
- CI/CD
- 巡检机器人
- 轻量项目推进机制
### 7.3 工具获取顺序
第一批先具备:
1. ChatGPT / Claude
2. Cursor 或同类编码 AI IDE
3. Figma
4. 保留现有 Gitea / CI / 巡检机器人
第二批按需要补:
1. v0 / Lovable
2. 专门 Review / QA 的 AI 会话体系
3. 更完整的测试辅助和流程自动化工具
### 7.4 工具不是越多越好
关键不是堆模型,而是:
- 谁负责什么
- 谁来拍板
- 谁来 review
- 谁来回归
- 所有人是否围绕同一套真相文档工作
## 8. AI 团队角色分工
### 8.1 人类角色
#### 你(产品负责人)
负责:
- 方向
- 目标
- 优先级
- 体验偏好
- 最终拍板
#### 小虾(技术负责人 / 总协调)
负责:
- 架构方案
- 任务拆解
- 规范制定
- 边界约束
- 技术决策收口
- 最终合并口径
### 8.2 AI 角色
#### UI 设计 AI
负责:
- 页面结构
- 交互流
- 页面草图
- 视觉方案辅助
#### 编码 AI
负责:
- 前端实现
- 后端实现
- 基础模块实现
- 脚手架和具体模块开发
#### Review AI
负责:
- 代码审查
- 风险检查
- 架构偏移检查
- 规范一致性检查
#### QA / Bug AI
负责:
- 复现问题
- 补测试
- 回归验证
- 测试建议
#### 文档 AI
负责:
- PRD 草稿
- 接口文档
- 架构文档
- 发布说明
- 迁移说明
### 8.3 协作铁律
- 一个问题只有一个 owner
- 架构只能由一个统一 owner 维护
- 写代码的人不能代替 Review
- 修 bug 的人不能代替 QA
- 所有 AI 输出必须围绕统一文档体系
## 9. 现有基础设施去留
### 9.1 保留
- 代码仓库(Gitea)
- CI/CD 理念和流程能力
- 巡检机器人
- 项目推进机制(轻量化)
### 9.2 不照搬
- 旧桌面版安装/同步/发布链路
- Tkinter 相关工程结构
- 旧系统临时修补脚本文化
- 为兼容旧桌面版而存在的特殊逻辑
### 9.3 新系统要重建的基础设施
- 新项目仓库结构
- 新 CI/CD 流水线
- 新日志/监控/告警规范
- 新部署流程
- 新测试流程
## 10. 每一步怎么走
### 第 1 步:盘点现状
盘 4 张表:
1. 我们现在已有的功能
2. 我们已有的工程资产
3. 我们已有的工具
4. 我们缺少的能力和基础设施
### 第 2 步:确认产品边界
确认:
- SaaS 给谁用
- 单人还是团队
- 多租户是否需要
- 一期和后续范围怎么划分
### 第 3 步:确认核心对象
确认是否保留和如何设计:
- 用户
- 团队 / 工作区
- 项目
- 素材库
- 配音库
- 生成任务
- 成片
- 任务节点 / worker
- 版本 / 发布对象
### 第 4 步:确认核心流程
要把这些完整走通:
- 上传素材
- 素材分类
- 配音管理
- 生成任务
- 批量生成
- 结果查看
- 下载/发布
- 后台任务监控
### 第 5 步:确认技术栈
需要明确:
- 前端
- 后端
- 数据库
- 队列
- 对象存储
- 鉴权
- 部署方式
- 监控日志
### 第 6 步:确认制度
包括:
- 目录结构
- 代码规范
- 接口规范
- 数据模型规范
- 提交流程
- 发布流程
- AI 协作流程
### 第 7 步:定义 MVP
列出:
- 一期必须做什么
- 可以延后什么
- 明确不做什么
### 第 8 步:再开始搭骨架
包括:
- 新项目目录
- 前后端工程骨架
- 数据库迁移体系
- CI/CD 初版
- 日志与配置体系
### 第 9 步:模块化开发
严格按架构和边界执行。
### 第 10 步:Review / QA / 上线
必须经过:
- Code Review
- 自动化测试
- 回归验证
- 预发布验证
- 正式发布与监控
## 11. 制度怎么落实
### 制度 1:先文档后开发
没有边界、架构、一期范围确认,不开工。
### 制度 2:单一真相源
必须建立统一文档源:
- PRD
- 架构文档
- 数据模型
- API 文档
- 里程碑
### 制度 3:架构 owner 唯一
- 架构由小虾负责收口
- 你负责最终确认
- 其他 AI 不得随意改方向
### 制度 4:AI 不直接上线
AI 产出必须经过:
- Review
- 测试
- CI
- 验收
### 制度 5:禁止临时修补进入主线
- 所有变更必须走既定流程
- 不允许口头规则替代正式规则
### 制度 6:一次只处理一个阶段问题
- 先定需求
- 再定架构
- 再定页面
- 再开发
- 再测试
### 制度 7:发布必须可回滚
- 每次上线必须可回滚
- 必须有监控与告警
### 制度 8:新旧彻底隔离
- 旧项目只参考
- 新项目主线独立开发
## 12. 当前最重要的下一步
不是写 SaaS 代码,而是先盘点:
### 第一张表:我们现在有什么
需要逐项盘清:
- 现有功能
- 现有工具
- 现有基础设施
- 可复用的业务规则
- 缺少的能力
只有这一步做清楚,后面技术选型、架构设计、AI 分工、项目骨架才不会跑偏。
## 13. 当前状态结论
我们现在已经统一了方向:
- 旧桌面版不再作为未来主线
- 新 SaaS 项目全新开始
- 必须正规化开发
- 必须先盘点、先定边界、先定制度
- AI 工具采用现成能力 + 明确分工,不自研底座
- 现有 Gitea / CI / 巡检机器人保留,但未来服务于新系统
下一步进入:
**《SaaS 重建盘点总表》第一阶段:我们现在有什么、还缺什么。**
配套文档:
- `docs/saas-rebuild-inventory.md`
- `docs/ai-team-tooling-checklist.md`
- `docs/infrastructure-keep-rebuild-matrix.md`
- `docs/saas-core-objects.md`
- `docs/saas-core-flows.md`
- `docs/saas-tech-stack-options.md`
- `docs/saas-mvp-scope.md`
- `docs/saas-project-structure-spec.md`
- `docs/saas-development-workflow.md`
+274
View File
@@ -0,0 +1,274 @@
# SaaS 技术栈选型表(第一版)
本文档用于明确新 SaaS 项目应该采用什么技术栈,以及为什么这样选。
当前阶段的原则不是追求“最新最炫”,而是追求:
- 稳定
- 清晰
- 好维护
- 适合两人 + AI 协作
- 便于长期迭代
---
## 1. 选型原则
### 必须满足
- 简单清晰
- 社区成熟
- 适合视频/任务型系统
- 适合长期演进
- 对 AI 编码工具友好
- 前后端边界明确
### 尽量避免
- 过重的企业级复杂框架组合
- 需要大量人力维护的微服务架构
- 前期就引入过多中间件
- 因“先进”而牺牲可维护性
---
## 2. 前端建议
### 推荐方向
- **React + TypeScript + Next.js(管理台 / Web 前端)**
### 原因
- 生态成熟
- AI 工具支持好
- 组件化能力强
- 适合后台系统和 SaaS 管理界面
- 后续做 SSR / 管理台 / 仪表盘都顺手
### UI 组件建议
- `shadcn/ui` 或同类现代组件体系
- 配合 Tailwind CSS
### 结论建议
前端推荐:
- `Next.js + TypeScript + Tailwind + shadcn/ui`
---
## 3. 后端建议
### 推荐方向
- **Python + FastAPI**
### 原因
- 和现有业务逻辑语言认知一致
- 适合 AI 工具协作
- 非常适合 API + 后台任务系统
- 类型标注、文档、异步支持都成熟
- 对视频处理、脚本、任务调度的生态友好
### 不建议前期改成 Node 作为主后端的原因
- 现有业务规则和视频处理经验都更接近 Python 生态
- 后续 worker、ffmpeg、诊断逻辑、脚本集成都更自然
### 结论建议
后端推荐:
- `FastAPI + Python`
---
## 4. 数据库建议
### 推荐方向
- **PostgreSQL**
### 原因
- 稳定成熟
- 非常适合 SaaS 主业务数据
- 对 JSON 字段、索引、事务支持好
- 生态完整
### 结论建议
主数据库:
- `PostgreSQL`
---
## 5. 异步任务 / 队列建议
### 推荐方向
- **Celery + Redis**
### 原因
- 视频生成、素材分类、同步、诊断都属于典型长任务
- 需要 worker、状态、重试、任务分离
- Python 生态成熟
### 可替代方向
- RQ / Dramatiq / 自建轻量任务层
### 当前建议
如果追求成熟和可扩展,优先:
- `Celery + Redis`
---
## 6. 对象存储建议
### 推荐方向
- **S3 兼容对象存储 / OSS**
### 原因
- SaaS 素材和结果文件不适合长期直接绑本地磁盘
- 对象存储天然适合大文件、归档、分发
- 便于后续多 worker / 多环境扩展
### 当前建议
抽象成统一存储接口,底层可接:
- 阿里云 OSS
- MinIO
- S3 兼容存储
---
## 7. 鉴权建议
### 推荐方向
- **JWT + Session/Refresh Token 组合**
### 原因
- 适合 Web SaaS
- 前后端分离支持好
- 后续权限系统扩展方便
### 当前建议
一期先做:
- 登录
- 用户身份校验
- 工作空间边界
高级权限后续再扩。
---
## 8. 部署建议
### 推荐方向
- **Docker Compose 起步,后续再升级**
### 原因
- 我们当前团队规模不适合一开始上 Kubernetes
- Compose 足够支撑首版和测试环境
- 后续如果规模上来,再考虑更复杂调度
### 初期部署对象
- Web 前端
- FastAPI 后端
- PostgreSQL
- Redis
- Worker
- Nginx / 反向代理
---
## 9. 日志与监控建议
### 一期最小配置
- 应用日志
- 任务日志
- 错误追踪
- 健康检查
- 巡检机器人
### 后续建议
- Sentry(错误追踪)
- Prometheus + Grafana(指标)
- Loki / ELK(日志检索,按需要)
---
## 10. 开发规范建议
### 前端
- TypeScript 强制
- ESLint / Prettier
- 组件边界清晰
- 页面逻辑与数据逻辑分离
### 后端
- 类型标注
- Pydantic / schema 明确
- 分层结构清晰
- 用例层和适配器层分开
### 测试
- 单元测试
- API 测试
- 任务流关键路径测试
- CI 自动执行
---
## 11. 当前推荐组合(第一版)
### 推荐首选方案
- **前端**:Next.js + TypeScript + Tailwind + shadcn/ui
- **后端**:FastAPI + Python
- **数据库**:PostgreSQL
- **队列**:Redis + Celery
- **存储**:S3/OSS 兼容对象存储
- **部署**:Docker Compose
- **监控**:巡检机器人 + 基础日志 + 后续接 Sentry
这是我现在最推荐的首版 SaaS 技术栈。
---
## 12. 当前阶段结论
对于我们这种“两个人 + AI 工具”的实际情况,最重要的不是用最潮的栈,而是:
- 选成熟的
- 选 AI 容易协作的
- 选长期不容易后悔的
- 选和视频任务系统天然契合的
当前推荐方向已经足够支撑下一步:
- 正式定义 SaaS 项目目录结构
- 开始搭第一版工程骨架
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONPATH=/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY apps/api /app/apps/api
COPY packages /app/packages
WORKDIR /app/apps/api
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+55
View File
@@ -0,0 +1,55 @@
version: '3.9'
services:
web:
image: node:20
working_dir: /app/apps/web
command: sh -c "npm install && npm run dev"
ports:
- "3000:3000"
volumes:
- ../..:/app
depends_on:
- api
api:
image: xiaoxia-saas-api:dev
build:
context: ../..
dockerfile: infra/docker/api.Dockerfile
env_file:
- ../../.env.example
ports:
- "8000:8000"
depends_on:
- postgres
- redis
worker:
image: xiaoxia-saas-worker:dev
build:
context: ../..
dockerfile: infra/docker/worker.Dockerfile
env_file:
- ../../.env.example
depends_on:
- redis
- postgres
postgres:
image: postgres:16
environment:
POSTGRES_DB: xiaoxia_saas
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
ports:
- "6379:6379"
volumes:
postgres_data:
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONPATH=/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY apps/worker /app/apps/worker
COPY packages /app/packages
WORKDIR /app/apps/worker
CMD ["celery", "-A", "worker_app.celery_app.celery_app", "worker", "--loglevel=info"]
+12
View File
@@ -0,0 +1,12 @@
server {
listen 80;
server_name _;
location /api/ {
proxy_pass http://api:8000/;
}
location / {
return 200 'xiaoxia-saas nginx scaffold';
}
}
+16
View File
@@ -0,0 +1,16 @@
# Bootstrap notes
Current scaffold status:
- `apps/web`: Next.js-style skeleton files created
- `apps/api`: FastAPI package layout created (`app/core`, `app/api`, `app/schemas`)
- `apps/worker`: Celery worker package layout created (`worker_app/core`, `worker_app/tasks`)
- `infra/docker/compose.yml`: local dev compose with web/api/worker/postgres/redis
Next actions:
1. install frontend dependencies
2. add first real domain models in `packages/domain`
3. add first application use cases in `packages/application`
4. connect API routes to use cases
5. add CI workflow
+14
View File
@@ -0,0 +1,14 @@
{
"name": "xiaoxia-saas",
"private": true,
"workspaces": [
"apps/web"
],
"scripts": {
"dev:web": "npm --workspace apps/web run dev",
"build:web": "npm --workspace apps/web run build",
"lint:web": "npm --workspace apps/web run lint",
"lint": "npm run lint:web",
"test": "echo TODO: add frontend/backend tests"
}
}
+1
View File
@@ -0,0 +1 @@
"""Packages root."""
+1
View File
@@ -0,0 +1 @@
"""Adapters package for external implementations."""
+11
View File
@@ -0,0 +1,11 @@
from .asset_library_repository import InMemoryAssetLibraryRepository
from .asset_repository import InMemoryAssetRepository
from .ingest_job_repository import InMemoryIngestJobRepository
from .project_repository import InMemoryProjectRepository
__all__ = [
"InMemoryAssetLibraryRepository",
"InMemoryAssetRepository",
"InMemoryIngestJobRepository",
"InMemoryProjectRepository",
]
@@ -0,0 +1,19 @@
from __future__ import annotations
from packages.domain import AssetLibrary, AssetLibraryKind
class InMemoryAssetLibraryRepository:
def __init__(self):
self._items: dict[str, list[AssetLibrary]] = {}
def list_by_project(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
items = list(self._items.get(project_id, []))
if kind is None:
return items
return [item for item in items if item.kind == kind]
def create(self, library: AssetLibrary) -> AssetLibrary:
items = self._items.setdefault(library.project_id, [])
items.append(library)
return library
@@ -0,0 +1,16 @@
from __future__ import annotations
from packages.domain import Asset
class InMemoryAssetRepository:
def __init__(self):
self._items: dict[str, list[Asset]] = {}
def list_by_library(self, library_id: str) -> list[Asset]:
return list(self._items.get(library_id, []))
def create(self, asset: Asset) -> Asset:
items = self._items.setdefault(asset.library_id, [])
items.append(asset)
return asset
@@ -0,0 +1,19 @@
from __future__ import annotations
from packages.domain import IngestJob
class InMemoryIngestJobRepository:
def __init__(self):
self._items: dict[str, IngestJob] = {}
def create(self, job: IngestJob) -> IngestJob:
self._items[job.id] = job
return job
def get(self, job_id: str) -> IngestJob | None:
return self._items.get(job_id)
def update(self, job: IngestJob) -> IngestJob:
self._items[job.id] = job
return job
@@ -0,0 +1,16 @@
from __future__ import annotations
from packages.domain import Project
class InMemoryProjectRepository:
def __init__(self):
self._items: dict[str, list[Project]] = {}
def list_by_workspace(self, workspace_id: str) -> list[Project]:
return list(self._items.get(workspace_id, []))
def create(self, project: Project) -> Project:
items = self._items.setdefault(project.workspace_id, [])
items.append(project)
return project
+20
View File
@@ -0,0 +1,20 @@
"""Application use cases package."""
from .asset_libraries import CreateAssetLibraryCommand, CreateAssetLibraryUseCase, ListAssetLibrariesUseCase
from .assets import CreateAssetCommand, CreateAssetUseCase, ListAssetsUseCase
from .ingest_jobs import SubmitIngestJobCommand, SubmitIngestJobUseCase
from .projects import CreateProjectCommand, CreateProjectUseCase, ListProjectsUseCase
__all__ = [
"CreateAssetCommand",
"CreateAssetLibraryCommand",
"CreateAssetLibraryUseCase",
"CreateAssetUseCase",
"CreateProjectCommand",
"CreateProjectUseCase",
"ListAssetLibrariesUseCase",
"ListAssetsUseCase",
"ListProjectsUseCase",
"SubmitIngestJobCommand",
"SubmitIngestJobUseCase",
]
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
from dataclasses import dataclass
from packages.domain import AssetLibrary, AssetLibraryKind
from packages.ports.asset_library_repository import AssetLibraryRepository
@dataclass(slots=True)
class CreateAssetLibraryCommand:
workspace_id: str
project_id: str
name: str
kind: AssetLibraryKind
class ListAssetLibrariesUseCase:
def __init__(self, asset_library_repository: AssetLibraryRepository):
self.asset_library_repository = asset_library_repository
def execute(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
if not project_id.strip():
raise ValueError("project_id 不能为空")
return self.asset_library_repository.list_by_project(project_id.strip(), kind=kind)
class CreateAssetLibraryUseCase:
def __init__(self, asset_library_repository: AssetLibraryRepository):
self.asset_library_repository = asset_library_repository
def execute(self, command: CreateAssetLibraryCommand) -> AssetLibrary:
library = AssetLibrary.create(
workspace_id=command.workspace_id,
project_id=command.project_id,
name=command.name,
kind=command.kind,
)
return self.asset_library_repository.create(library)
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
from dataclasses import dataclass
from packages.domain import Asset
from packages.ports.asset_repository import AssetRepository
@dataclass(slots=True)
class CreateAssetCommand:
workspace_id: str
project_id: str
library_id: str
name: str
storage_key: str
mime_type: str
metadata: dict[str, object] | None = None
class ListAssetsUseCase:
def __init__(self, asset_repository: AssetRepository):
self.asset_repository = asset_repository
def execute(self, library_id: str) -> list[Asset]:
if not library_id.strip():
raise ValueError("library_id 不能为空")
return self.asset_repository.list_by_library(library_id.strip())
class CreateAssetUseCase:
def __init__(self, asset_repository: AssetRepository):
self.asset_repository = asset_repository
def execute(self, command: CreateAssetCommand) -> Asset:
asset = Asset.create(
workspace_id=command.workspace_id,
project_id=command.project_id,
library_id=command.library_id,
name=command.name,
storage_key=command.storage_key,
mime_type=command.mime_type,
metadata=command.metadata,
)
return self.asset_repository.create(asset)
+28
View File
@@ -0,0 +1,28 @@
from __future__ import annotations
from dataclasses import dataclass
from packages.domain import IngestJob
from packages.ports.ingest_job_repository import IngestJobRepository
@dataclass(slots=True)
class SubmitIngestJobCommand:
workspace_id: str
project_id: str
library_id: str
storage_key: str
class SubmitIngestJobUseCase:
def __init__(self, ingest_job_repository: IngestJobRepository):
self.ingest_job_repository = ingest_job_repository
def execute(self, command: SubmitIngestJobCommand) -> IngestJob:
job = IngestJob.create(
workspace_id=command.workspace_id,
project_id=command.project_id,
library_id=command.library_id,
storage_key=command.storage_key,
)
return self.ingest_job_repository.create(job)
+36
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
from dataclasses import dataclass
from packages.domain import Project
from packages.ports import ProjectRepository
@dataclass(slots=True)
class CreateProjectCommand:
workspace_id: str
name: str
description: str = ""
class ListProjectsUseCase:
def __init__(self, project_repository: ProjectRepository):
self.project_repository = project_repository
def execute(self, workspace_id: str) -> list[Project]:
if not workspace_id.strip():
raise ValueError("workspace_id 不能为空")
return self.project_repository.list_by_workspace(workspace_id.strip())
class CreateProjectUseCase:
def __init__(self, project_repository: ProjectRepository):
self.project_repository = project_repository
def execute(self, command: CreateProjectCommand) -> Project:
project = Project.create(
workspace_id=command.workspace_id,
name=command.name,
description=command.description,
)
return self.project_repository.create(project)
+14
View File
@@ -0,0 +1,14 @@
"""Domain package for core business entities and rules."""
from .entities import Asset, AssetLibrary, AssetLibraryKind, IngestJob, IngestJobStatus, Project, User, Workspace
__all__ = [
"Asset",
"AssetLibrary",
"AssetLibraryKind",
"IngestJob",
"IngestJobStatus",
"Project",
"User",
"Workspace",
]
+165
View File
@@ -0,0 +1,165 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import StrEnum
from typing import Any
from uuid import uuid4
class AssetLibraryKind(StrEnum):
VIDEO = "video"
VOICE = "voice"
class IngestJobStatus(StrEnum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
@dataclass(slots=True)
class User:
id: str
email: str
display_name: str
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@dataclass(slots=True)
class Workspace:
id: str
name: str
owner_user_id: str
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@dataclass(slots=True)
class Project:
id: str
workspace_id: str
name: str
description: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(cls, workspace_id: str, name: str, description: str = "") -> "Project":
clean_name = name.strip()
if not clean_name:
raise ValueError("项目名称不能为空")
return cls(
id=uuid4().hex,
workspace_id=workspace_id,
name=clean_name,
description=description.strip(),
)
@dataclass(slots=True)
class AssetLibrary:
id: str
workspace_id: str
project_id: str
name: str
kind: AssetLibraryKind
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(
cls,
workspace_id: str,
project_id: str,
name: str,
kind: AssetLibraryKind,
) -> "AssetLibrary":
clean_name = name.strip()
if not clean_name:
raise ValueError("素材库名称不能为空")
return cls(
id=uuid4().hex,
workspace_id=workspace_id,
project_id=project_id,
name=clean_name,
kind=kind,
)
@dataclass(slots=True)
class Asset:
id: str
workspace_id: str
project_id: str
library_id: str
name: str
storage_key: str
mime_type: str
metadata: dict[str, Any] = field(default_factory=dict)
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(
cls,
workspace_id: str,
project_id: str,
library_id: str,
name: str,
storage_key: str,
mime_type: str,
metadata: dict[str, Any] | None = None,
) -> "Asset":
clean_name = name.strip()
if not clean_name:
raise ValueError("素材名称不能为空")
if not storage_key.strip():
raise ValueError("storage_key 不能为空")
if not mime_type.strip():
raise ValueError("mime_type 不能为空")
return cls(
id=uuid4().hex,
workspace_id=workspace_id,
project_id=project_id,
library_id=library_id,
name=clean_name,
storage_key=storage_key.strip(),
mime_type=mime_type.strip(),
metadata=metadata or {},
)
@dataclass(slots=True)
class IngestJob:
id: str
workspace_id: str
project_id: str
library_id: str
storage_key: str
status: IngestJobStatus = IngestJobStatus.PENDING
error_message: str = ""
result_asset_id: str = ""
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@classmethod
def create(
cls,
workspace_id: str,
project_id: str,
library_id: str,
storage_key: str,
) -> "IngestJob":
if not workspace_id.strip():
raise ValueError("workspace_id 不能为空")
if not project_id.strip():
raise ValueError("project_id 不能为空")
if not library_id.strip():
raise ValueError("library_id 不能为空")
if not storage_key.strip():
raise ValueError("storage_key 不能为空")
return cls(
id=uuid4().hex,
workspace_id=workspace_id.strip(),
project_id=project_id.strip(),
library_id=library_id.strip(),
storage_key=storage_key.strip(),
)
+13
View File
@@ -0,0 +1,13 @@
"""Ports package for infrastructure-agnostic interfaces."""
from .asset_library_repository import AssetLibraryRepository
from .asset_repository import AssetRepository
from .ingest_job_repository import IngestJobRepository
from .project_repository import ProjectRepository
__all__ = [
"AssetLibraryRepository",
"AssetRepository",
"IngestJobRepository",
"ProjectRepository",
]
@@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Protocol
from packages.domain import AssetLibrary, AssetLibraryKind
class AssetLibraryRepository(Protocol):
def list_by_project(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
"""List asset libraries for a project."""
def create(self, library: AssetLibrary) -> AssetLibrary:
"""Persist an asset library and return it."""
+13
View File
@@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Protocol
from packages.domain import Asset
class AssetRepository(Protocol):
def list_by_library(self, library_id: str) -> list[Asset]:
"""List assets for a library."""
def create(self, asset: Asset) -> Asset:
"""Persist an asset and return it."""
+16
View File
@@ -0,0 +1,16 @@
from __future__ import annotations
from typing import Protocol
from packages.domain import IngestJob
class IngestJobRepository(Protocol):
def create(self, job: IngestJob) -> IngestJob:
"""Persist an ingest job and return it."""
def get(self, job_id: str) -> IngestJob | None:
"""Retrieve an ingest job by ID."""
def update(self, job: IngestJob) -> IngestJob:
"""Update an ingest job and return it."""
+13
View File
@@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Protocol
from packages.domain import Project
class ProjectRepository(Protocol):
def list_by_workspace(self, workspace_id: str) -> list[Project]:
"""List projects for a workspace."""
def create(self, project: Project) -> Project:
"""Persist a project and return it."""
+1
View File
@@ -0,0 +1 @@
"""Shared package for cross-cutting, non-business-specific utilities."""
+3
View File
@@ -0,0 +1,3 @@
[pytest]
pythonpath = .
testpaths = tests
+10
View File
@@ -0,0 +1,10 @@
# Python runtime dependencies for API and worker will be managed here.
# Initial scaffold only; versions to be pinned after stack confirmation.
fastapi
uvicorn
pydantic
celery
redis
sqlalchemy
alembic
psycopg[binary]
+8
View File
@@ -0,0 +1,8 @@
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
+1
View File
@@ -0,0 +1 @@
# End-to-end tests placeholder
+1
View File
@@ -0,0 +1 @@
# Integration tests placeholder
+102
View File
@@ -0,0 +1,102 @@
from packages.application import (
CreateAssetCommand,
CreateAssetLibraryCommand,
CreateAssetLibraryUseCase,
CreateAssetUseCase,
CreateProjectCommand,
CreateProjectUseCase,
ListAssetLibrariesUseCase,
ListAssetsUseCase,
ListProjectsUseCase,
SubmitIngestJobCommand,
SubmitIngestJobUseCase,
)
from packages.adapters.in_memory import (
InMemoryAssetLibraryRepository,
InMemoryAssetRepository,
InMemoryIngestJobRepository,
InMemoryProjectRepository,
)
from packages.domain import AssetLibraryKind, IngestJobStatus
def test_create_and_list_projects():
repository = InMemoryProjectRepository()
create_use_case = CreateProjectUseCase(repository)
list_use_case = ListProjectsUseCase(repository)
project = create_use_case.execute(CreateProjectCommand(workspace_id="ws-1", name=" Demo Project "))
assert project.name == "Demo Project"
assert project.workspace_id == "ws-1"
items = list_use_case.execute("ws-1")
assert len(items) == 1
assert items[0].id == project.id
def test_create_and_list_asset_libraries():
repository = InMemoryAssetLibraryRepository()
create_use_case = CreateAssetLibraryUseCase(repository)
list_use_case = ListAssetLibrariesUseCase(repository)
library = create_use_case.execute(
CreateAssetLibraryCommand(
workspace_id="ws-1",
project_id="proj-1",
name=" 素材库 A ",
kind=AssetLibraryKind.VIDEO,
)
)
assert library.name == "素材库 A"
assert library.kind == AssetLibraryKind.VIDEO
items = list_use_case.execute("proj-1", kind=AssetLibraryKind.VIDEO)
assert len(items) == 1
assert items[0].id == library.id
def test_create_and_list_assets():
repository = InMemoryAssetRepository()
create_use_case = CreateAssetUseCase(repository)
list_use_case = ListAssetsUseCase(repository)
asset = create_use_case.execute(
CreateAssetCommand(
workspace_id="ws-1",
project_id="proj-1",
library_id="lib-1",
name=" demo.mp4 ",
storage_key="uploads/demo.mp4",
mime_type="video/mp4",
metadata={"duration": 12.5},
)
)
assert asset.name == "demo.mp4"
assert asset.storage_key == "uploads/demo.mp4"
assert asset.metadata["duration"] == 12.5
items = list_use_case.execute("lib-1")
assert len(items) == 1
assert items[0].id == asset.id
def test_submit_ingest_job():
repository = InMemoryIngestJobRepository()
use_case = SubmitIngestJobUseCase(repository)
job = use_case.execute(
SubmitIngestJobCommand(
workspace_id="ws-1",
project_id="proj-1",
library_id="lib-1",
storage_key="uploads/raw-video.mp4",
)
)
assert job.workspace_id == "ws-1"
assert job.storage_key == "uploads/raw-video.mp4"
assert job.status == IngestJobStatus.PENDING
assert job.result_asset_id == ""
retrieved = repository.get(job.id)
assert retrieved is not None
assert retrieved.id == job.id