style: format Python files for CI
This commit is contained in:
@@ -17,8 +17,13 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("generated_videos", sa.Column("status", sa.String(length=20), nullable=False, server_default="completed"))
|
||||
op.add_column("generated_videos", sa.Column("review_status", sa.String(length=20), nullable=False, server_default="pending_review"))
|
||||
op.add_column(
|
||||
"generated_videos", sa.Column("status", sa.String(length=20), nullable=False, server_default="completed")
|
||||
)
|
||||
op.add_column(
|
||||
"generated_videos",
|
||||
sa.Column("review_status", sa.String(length=20), nullable=False, server_default="pending_review"),
|
||||
)
|
||||
op.add_column("generated_videos", sa.Column("generation_params", sa.Text(), nullable=False, server_default="{}"))
|
||||
op.add_column("generated_videos", sa.Column("updated_at", sa.DateTime(), nullable=True))
|
||||
op.create_index(op.f("ix_generated_videos_status"), "generated_videos", ["status"], unique=False)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "006"
|
||||
down_revision = "005"
|
||||
branch_labels = None
|
||||
|
||||
@@ -42,10 +42,15 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
|
||||
video_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.VIDEO]
|
||||
image_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.IMAGE]
|
||||
voice_assets = [asset for asset in ready_assets if _asset_kind(asset) == AssetLibraryKind.VOICE]
|
||||
problem_assets = [asset for asset in assets if asset.status in {AssetStatus.ERROR, AssetStatus.UPLOADING, AssetStatus.PROCESSING}]
|
||||
unclassified_assets = [asset for asset in ready_assets if asset.classification_status.value in {"pending", "failed"}]
|
||||
problem_assets = [
|
||||
asset for asset in assets if asset.status in {AssetStatus.ERROR, AssetStatus.UPLOADING, AssetStatus.PROCESSING}
|
||||
]
|
||||
unclassified_assets = [
|
||||
asset for asset in ready_assets if asset.classification_status.value in {"pending", "failed"}
|
||||
]
|
||||
risky_assets = [
|
||||
asset for asset in ready_assets
|
||||
asset
|
||||
for asset in ready_assets
|
||||
if (asset.quality_score is not None and asset.quality_score < 60)
|
||||
or asset.metadata.get("review_status") == "rejected"
|
||||
or asset.status == AssetStatus.ERROR
|
||||
@@ -54,7 +59,9 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
|
||||
unused_assets = [asset for asset in ready_assets if int(asset.metadata.get("generation_use_count") or 0) == 0]
|
||||
pending_review_assets = [asset for asset in ready_assets if asset.metadata.get("review_status") == "pending_review"]
|
||||
total_duration = round(sum(float(asset.duration or 0) for asset in video_assets), 2)
|
||||
estimated_video_count = max(0, min(len(video_assets), int(total_duration // 5) if total_duration else len(video_assets)))
|
||||
estimated_video_count = max(
|
||||
0, min(len(video_assets), int(total_duration // 5) if total_duration else len(video_assets))
|
||||
)
|
||||
|
||||
score = 20
|
||||
if video_assets:
|
||||
@@ -128,15 +135,43 @@ def _build_diagnosis(workspace_id: str, project_id: str, assets: list[Asset]) ->
|
||||
)
|
||||
|
||||
smart_views = [
|
||||
AssetSmartViewItem(key="recommended", label="推荐素材", count=len(video_assets), description="已导入完成、可参与生成的视频素材"),
|
||||
AssetSmartViewItem(key="needs_attention", label="慎用素材", count=len(problem_assets) + len(risky_assets), description="导入未完成、失败或质量分偏低的素材"),
|
||||
AssetSmartViewItem(key="high_risk", label="高风险素材", count=len(risky_assets), description="质量分偏低或复核拒绝的素材"),
|
||||
AssetSmartViewItem(key="unclassified", label="未分类素材", count=len(unclassified_assets), description="尚未完成分类或分类失败的 ready 素材"),
|
||||
AssetSmartViewItem(key="recent", label="最近上传", count=min(len(assets), 10), description="最近进入素材库的素材,可用于快速复核"),
|
||||
AssetSmartViewItem(key="unused", label="未使用素材", count=len(unused_assets), description="尚未参与生成的 ready 素材"),
|
||||
AssetSmartViewItem(
|
||||
key="recommended", label="推荐素材", count=len(video_assets), description="已导入完成、可参与生成的视频素材"
|
||||
),
|
||||
AssetSmartViewItem(
|
||||
key="needs_attention",
|
||||
label="慎用素材",
|
||||
count=len(problem_assets) + len(risky_assets),
|
||||
description="导入未完成、失败或质量分偏低的素材",
|
||||
),
|
||||
AssetSmartViewItem(
|
||||
key="high_risk", label="高风险素材", count=len(risky_assets), description="质量分偏低或复核拒绝的素材"
|
||||
),
|
||||
AssetSmartViewItem(
|
||||
key="unclassified",
|
||||
label="未分类素材",
|
||||
count=len(unclassified_assets),
|
||||
description="尚未完成分类或分类失败的 ready 素材",
|
||||
),
|
||||
AssetSmartViewItem(
|
||||
key="recent",
|
||||
label="最近上传",
|
||||
count=min(len(assets), 10),
|
||||
description="最近进入素材库的素材,可用于快速复核",
|
||||
),
|
||||
AssetSmartViewItem(
|
||||
key="unused", label="未使用素材", count=len(unused_assets), description="尚未参与生成的 ready 素材"
|
||||
),
|
||||
AssetSmartViewItem(key="used", label="已使用素材", count=len(used_assets), description="已经参与过生成的素材"),
|
||||
AssetSmartViewItem(key="pending_review", label="待复核素材", count=len(pending_review_assets), description="生成后待人工复核的素材"),
|
||||
AssetSmartViewItem(key="voice", label="配音素材", count=len(voice_assets), description="可用于后续配音/旁白工作流的素材"),
|
||||
AssetSmartViewItem(
|
||||
key="pending_review",
|
||||
label="待复核素材",
|
||||
count=len(pending_review_assets),
|
||||
description="生成后待人工复核的素材",
|
||||
),
|
||||
AssetSmartViewItem(
|
||||
key="voice", label="配音素材", count=len(voice_assets), description="可用于后续配音/旁白工作流的素材"
|
||||
),
|
||||
]
|
||||
|
||||
return ProjectAssetDiagnosisResponse(
|
||||
@@ -179,4 +214,3 @@ def get_project_asset_diagnosis(
|
||||
assets.extend(asset_repository.list_by_library(library.id))
|
||||
|
||||
return _build_diagnosis(project.workspace_id, project_id, assets)
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ from uuid import uuid4
|
||||
|
||||
from app.api.routes.permissions import require_workspace_member
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import get_asset_repository, get_db_session, get_project_repository, get_workspace_member_repository
|
||||
from app.dependencies import (
|
||||
get_asset_repository,
|
||||
get_db_session,
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.edit_plan import CreateEditPlanRequest, EditPlanClipResponse, EditPlanResponse, EditTemplateResponse
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -24,10 +29,15 @@ def _ensure_project(project_id: str, workspace_id: str, user: AuthenticatedUser,
|
||||
|
||||
|
||||
def _default_template(session: Session, workspace_id: str, project_id: str, user_id: str) -> EditTemplateModel:
|
||||
template = session.query(EditTemplateModel).filter(
|
||||
EditTemplateModel.project_id == project_id,
|
||||
EditTemplateModel.is_active.is_(True),
|
||||
).order_by(EditTemplateModel.created_at.asc()).first()
|
||||
template = (
|
||||
session.query(EditTemplateModel)
|
||||
.filter(
|
||||
EditTemplateModel.project_id == project_id,
|
||||
EditTemplateModel.is_active.is_(True),
|
||||
)
|
||||
.order_by(EditTemplateModel.created_at.asc())
|
||||
.first()
|
||||
)
|
||||
if template is not None:
|
||||
return template
|
||||
template = EditTemplateModel(
|
||||
@@ -59,7 +69,9 @@ def _to_template_response(template: EditTemplateModel) -> EditTemplateResponse:
|
||||
)
|
||||
|
||||
|
||||
def _to_plan_response(plan: EditPlanModel, clips: list[EditPlanClipModel], asset_names: dict[str, str]) -> EditPlanResponse:
|
||||
def _to_plan_response(
|
||||
plan: EditPlanModel, clips: list[EditPlanClipModel], asset_names: dict[str, str]
|
||||
) -> EditPlanResponse:
|
||||
return EditPlanResponse(
|
||||
id=plan.id,
|
||||
workspace_id=plan.workspace_id,
|
||||
@@ -69,15 +81,18 @@ def _to_plan_response(plan: EditPlanModel, clips: list[EditPlanClipModel], asset
|
||||
title_id=plan.title_id,
|
||||
status=plan.status,
|
||||
summary=plan.summary,
|
||||
clips=[EditPlanClipResponse(
|
||||
id=clip.id,
|
||||
asset_id=clip.asset_id,
|
||||
asset_name=asset_names.get(clip.asset_id, clip.asset_id),
|
||||
sequence=clip.sequence,
|
||||
start_time=float(clip.start_time or 0),
|
||||
duration=float(clip.duration or 0),
|
||||
reason=clip.reason,
|
||||
) for clip in clips],
|
||||
clips=[
|
||||
EditPlanClipResponse(
|
||||
id=clip.id,
|
||||
asset_id=clip.asset_id,
|
||||
asset_name=asset_names.get(clip.asset_id, clip.asset_id),
|
||||
sequence=clip.sequence,
|
||||
start_time=float(clip.start_time or 0),
|
||||
duration=float(clip.duration or 0),
|
||||
reason=clip.reason,
|
||||
)
|
||||
for clip in clips
|
||||
],
|
||||
created_at=plan.created_at,
|
||||
updated_at=plan.updated_at,
|
||||
)
|
||||
@@ -94,7 +109,11 @@ def list_edit_templates(
|
||||
) -> list[EditTemplateResponse]:
|
||||
_ensure_project(project_id, workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
||||
template = _default_template(session, workspace_id, project_id, authenticated_user.user.id)
|
||||
templates = session.query(EditTemplateModel).filter(EditTemplateModel.project_id == project_id, EditTemplateModel.is_active.is_(True)).all()
|
||||
templates = (
|
||||
session.query(EditTemplateModel)
|
||||
.filter(EditTemplateModel.project_id == project_id, EditTemplateModel.is_active.is_(True))
|
||||
.all()
|
||||
)
|
||||
return [_to_template_response(item) for item in templates or [template]]
|
||||
|
||||
|
||||
@@ -108,14 +127,26 @@ def create_edit_plan(
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> EditPlanResponse:
|
||||
_ensure_project(project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
||||
template = session.query(EditTemplateModel).filter(EditTemplateModel.id == request.template_id).first() if request.template_id else None
|
||||
_ensure_project(
|
||||
project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository
|
||||
)
|
||||
template = (
|
||||
session.query(EditTemplateModel).filter(EditTemplateModel.id == request.template_id).first()
|
||||
if request.template_id
|
||||
else None
|
||||
)
|
||||
if template is None:
|
||||
template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id)
|
||||
assets = [asset for asset in asset_repository.list_by_library(request.asset_library_id) if asset.status == AssetStatus.READY and asset.mime_type.startswith("video/")]
|
||||
assets = [
|
||||
asset
|
||||
for asset in asset_repository.list_by_library(request.asset_library_id)
|
||||
if asset.status == AssetStatus.READY and asset.mime_type.startswith("video/")
|
||||
]
|
||||
if not assets:
|
||||
raise HTTPException(status_code=422, detail="素材库暂无可用于剪辑计划的视频素材")
|
||||
selected = sorted(assets, key=lambda asset: (-(asset.quality_score or 0), asset.created_at))[: max(1, int(template.clip_count or 3))]
|
||||
selected = sorted(assets, key=lambda asset: (-(asset.quality_score or 0), asset.created_at))[
|
||||
: max(1, int(template.clip_count or 3))
|
||||
]
|
||||
plan = EditPlanModel(
|
||||
id=uuid4().hex,
|
||||
workspace_id=request.workspace_id,
|
||||
@@ -157,10 +188,17 @@ def get_edit_plan(
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> EditPlanResponse:
|
||||
plan = session.query(EditPlanModel).filter(EditPlanModel.id == plan_id, EditPlanModel.project_id == project_id).first()
|
||||
plan = (
|
||||
session.query(EditPlanModel).filter(EditPlanModel.id == plan_id, EditPlanModel.project_id == project_id).first()
|
||||
)
|
||||
if plan is None:
|
||||
raise HTTPException(status_code=404, detail="Edit plan not found")
|
||||
_ensure_project(project_id, plan.workspace_id, authenticated_user, project_repository, workspace_member_repository)
|
||||
clips = session.query(EditPlanClipModel).filter(EditPlanClipModel.edit_plan_id == plan.id).order_by(EditPlanClipModel.sequence.asc()).all()
|
||||
clips = (
|
||||
session.query(EditPlanClipModel)
|
||||
.filter(EditPlanClipModel.edit_plan_id == plan.id)
|
||||
.order_by(EditPlanClipModel.sequence.asc())
|
||||
.all()
|
||||
)
|
||||
assets = asset_repository.list_by_library(plan.asset_library_id)
|
||||
return _to_plan_response(plan, clips, {asset.id: asset.name for asset in assets})
|
||||
|
||||
@@ -68,8 +68,7 @@ def _to_generated_video_response(item) -> GeneratedVideoResponse:
|
||||
|
||||
def _ensure_library_has_ready_video_assets(assets) -> None:
|
||||
ready_video_assets = [
|
||||
asset for asset in assets
|
||||
if asset.status.value == "ready" and asset.mime_type.startswith("video")
|
||||
asset for asset in assets if asset.status.value == "ready" and asset.mime_type.startswith("video")
|
||||
]
|
||||
if not ready_video_assets:
|
||||
raise HTTPException(
|
||||
@@ -82,7 +81,14 @@ def _select_title_id(project_title_repository: Any, project_id: str) -> str:
|
||||
active_titles = project_title_repository.list_by_project(project_id, active_only=True)
|
||||
if not active_titles:
|
||||
return ""
|
||||
selected = sorted(active_titles, key=lambda title: (0 if getattr(title, "favorite", False) else 1, int(title.usage_count or 0), title.created_at))[0]
|
||||
selected = sorted(
|
||||
active_titles,
|
||||
key=lambda title: (
|
||||
0 if getattr(title, "favorite", False) else 1,
|
||||
int(title.usage_count or 0),
|
||||
title.created_at,
|
||||
),
|
||||
)[0]
|
||||
return selected.id
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ def list_project_titles(
|
||||
) -> ListProjectTitlesResponse:
|
||||
project = _get_project_or_404(project_id, project_repository)
|
||||
require_workspace_member(project.workspace_id, authenticated_user, workspace_member_repository)
|
||||
return ListProjectTitlesResponse(items=[_to_response(item) for item in title_repository.list_by_project(project_id, active_only)])
|
||||
return ListProjectTitlesResponse(
|
||||
items=[_to_response(item) for item in title_repository.list_by_project(project_id, active_only)]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/projects/{project_id}/titles", response_model=ProjectTitleResponse)
|
||||
|
||||
@@ -12,7 +12,12 @@ from app.schemas.task_center import ListProjectTasksResponse, ProjectTaskRespons
|
||||
from app.core.celery_app import celery_app
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from packages.application import CreateGenerationTaskCommand, CreateGenerationTaskUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
from packages.application import (
|
||||
CreateGenerationTaskCommand,
|
||||
CreateGenerationTaskUseCase,
|
||||
SubmitIngestJobCommand,
|
||||
SubmitIngestJobUseCase,
|
||||
)
|
||||
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
|
||||
|
||||
router = APIRouter()
|
||||
@@ -72,37 +77,41 @@ def list_project_tasks(
|
||||
|
||||
items: list[ProjectTaskResponse] = []
|
||||
for job in ingest_job_repository.list_by_project(project_id):
|
||||
items.append(ProjectTaskResponse(
|
||||
id=f"ingest:{job.id}",
|
||||
task_type="ingest",
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
status=job.status.value,
|
||||
progress=100.0 if job.status.value == "completed" else 0.0,
|
||||
current_step=_ingest_step(job),
|
||||
error_message=job.error_message,
|
||||
user_message=_humanize_task_error(job.error_message),
|
||||
retryable=job.status.value == "failed",
|
||||
source_id=job.id,
|
||||
created_at=job.created_at,
|
||||
updated_at=job.updated_at,
|
||||
))
|
||||
items.append(
|
||||
ProjectTaskResponse(
|
||||
id=f"ingest:{job.id}",
|
||||
task_type="ingest",
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
status=job.status.value,
|
||||
progress=100.0 if job.status.value == "completed" else 0.0,
|
||||
current_step=_ingest_step(job),
|
||||
error_message=job.error_message,
|
||||
user_message=_humanize_task_error(job.error_message),
|
||||
retryable=job.status.value == "failed",
|
||||
source_id=job.id,
|
||||
created_at=job.created_at,
|
||||
updated_at=job.updated_at,
|
||||
)
|
||||
)
|
||||
for task in generation_task_repository.list_by_project(project_id):
|
||||
items.append(ProjectTaskResponse(
|
||||
id=f"generation:{task.id}",
|
||||
task_type="generation",
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
status=task.status.value,
|
||||
progress=task.progress,
|
||||
current_step=_generation_step(task),
|
||||
error_message=task.error_message,
|
||||
user_message=_humanize_task_error(task.error_message),
|
||||
retryable=task.status.value == "failed",
|
||||
source_id=task.id,
|
||||
created_at=task.created_at,
|
||||
updated_at=task.completed_at or task.started_at or task.created_at,
|
||||
))
|
||||
items.append(
|
||||
ProjectTaskResponse(
|
||||
id=f"generation:{task.id}",
|
||||
task_type="generation",
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
status=task.status.value,
|
||||
progress=task.progress,
|
||||
current_step=_generation_step(task),
|
||||
error_message=task.error_message,
|
||||
user_message=_humanize_task_error(task.error_message),
|
||||
retryable=task.status.value == "failed",
|
||||
source_id=task.id,
|
||||
created_at=task.created_at,
|
||||
updated_at=task.completed_at or task.started_at or task.created_at,
|
||||
)
|
||||
)
|
||||
items.sort(key=lambda item: item.updated_at or item.created_at or "", reverse=True)
|
||||
return ListProjectTasksResponse(items=items)
|
||||
|
||||
@@ -124,20 +133,28 @@ def retry_project_task(
|
||||
if task.status.value != "failed":
|
||||
raise HTTPException(status_code=409, detail="Only failed tasks can be retried")
|
||||
use_case = CreateGenerationTaskUseCase(generation_task_repository)
|
||||
retried = use_case.execute(CreateGenerationTaskCommand(
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
asset_library_id=task.asset_library_id,
|
||||
strategy_id=task.strategy_id,
|
||||
voice_library_id=task.voice_library_id,
|
||||
edit_plan_id=task.edit_plan_id,
|
||||
created_by_user_id=authenticated_user.user.id,
|
||||
))
|
||||
retried = use_case.execute(
|
||||
CreateGenerationTaskCommand(
|
||||
workspace_id=task.workspace_id,
|
||||
project_id=task.project_id,
|
||||
asset_library_id=task.asset_library_id,
|
||||
strategy_id=task.strategy_id,
|
||||
voice_library_id=task.voice_library_id,
|
||||
edit_plan_id=task.edit_plan_id,
|
||||
created_by_user_id=authenticated_user.user.id,
|
||||
)
|
||||
)
|
||||
celery_app.send_task("worker.generate_video", args=[retried.id])
|
||||
return ProjectTaskResponse(
|
||||
id=f"generation:{retried.id}", task_type="generation", workspace_id=retried.workspace_id,
|
||||
project_id=retried.project_id, status=retried.status.value, progress=retried.progress,
|
||||
current_step=_generation_step(retried), source_id=retried.id, created_at=retried.created_at,
|
||||
id=f"generation:{retried.id}",
|
||||
task_type="generation",
|
||||
workspace_id=retried.workspace_id,
|
||||
project_id=retried.project_id,
|
||||
status=retried.status.value,
|
||||
progress=retried.progress,
|
||||
current_step=_generation_step(retried),
|
||||
source_id=retried.id,
|
||||
created_at=retried.created_at,
|
||||
updated_at=retried.created_at,
|
||||
)
|
||||
if task_type == "ingest":
|
||||
@@ -148,18 +165,25 @@ def retry_project_task(
|
||||
if job.status.value != "failed":
|
||||
raise HTTPException(status_code=409, detail="Only failed tasks can be retried")
|
||||
use_case = SubmitIngestJobUseCase(ingest_job_repository)
|
||||
retried = use_case.execute(SubmitIngestJobCommand(
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
storage_key=job.storage_key,
|
||||
))
|
||||
retried = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
storage_key=job.storage_key,
|
||||
)
|
||||
)
|
||||
celery_app.send_task("worker.ingest_asset", args=[retried.id])
|
||||
return ProjectTaskResponse(
|
||||
id=f"ingest:{retried.id}", task_type="ingest", workspace_id=retried.workspace_id,
|
||||
project_id=retried.project_id, status=retried.status.value, progress=0,
|
||||
current_step=_ingest_step(retried), source_id=retried.id, created_at=retried.created_at,
|
||||
id=f"ingest:{retried.id}",
|
||||
task_type="ingest",
|
||||
workspace_id=retried.workspace_id,
|
||||
project_id=retried.project_id,
|
||||
status=retried.status.value,
|
||||
progress=0,
|
||||
current_step=_ingest_step(retried),
|
||||
source_id=retried.id,
|
||||
created_at=retried.created_at,
|
||||
updated_at=retried.updated_at,
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Unsupported task type")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# ffmpeg/ffprobe are invoked with fixed argument lists and shell=False.
|
||||
import subprocess # nosec B404
|
||||
import tempfile
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""
|
||||
创建推进器数据库表结构
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
|
||||
def create_tables():
|
||||
"""创建数据库表"""
|
||||
conn = sqlite3.connect("F:/openclaw-saas/tracker.db")
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
# 创建 tasks 表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
@@ -27,9 +29,9 @@ def create_tables():
|
||||
tags TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
print("[OK] tasks 表创建成功")
|
||||
|
||||
|
||||
# 创建 milestones 表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS milestones (
|
||||
@@ -42,9 +44,9 @@ def create_tables():
|
||||
description TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
print("[OK] milestones 表创建成功")
|
||||
|
||||
|
||||
# 创建 logs 表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
@@ -55,12 +57,12 @@ def create_tables():
|
||||
FOREIGN KEY (task_id) REFERENCES tasks (id)
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
print("[OK] logs 表创建成功")
|
||||
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
print("\n[SUCCESS] 数据库表结构创建完成!")
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
Phase 4 任务数据初始化脚本
|
||||
将认证、权限、订阅计费的所有任务录入推进器
|
||||
"""
|
||||
|
||||
import sys
|
||||
import io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||
|
||||
import requests
|
||||
import json
|
||||
@@ -16,6 +18,7 @@ API_BASE = "http://47.98.113.167:8089/api/v1"
|
||||
PROJECT_ID = "00000000-0000-0000-0000-000000000001"
|
||||
WORKSPACE_ID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
def create_milestone(name, target_date, description=""):
|
||||
"""创建里程碑"""
|
||||
payload = {
|
||||
@@ -23,7 +26,7 @@ def create_milestone(name, target_date, description=""):
|
||||
"target_date": target_date,
|
||||
"project_id": PROJECT_ID,
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"description": description
|
||||
"description": description,
|
||||
}
|
||||
resp = requests.post(f"{API_BASE}/project-management/milestones", json=payload)
|
||||
if resp.status_code == 200:
|
||||
@@ -33,6 +36,7 @@ def create_milestone(name, target_date, description=""):
|
||||
print(f"[FAIL] 里程碑: {name} - {resp.text}")
|
||||
return None
|
||||
|
||||
|
||||
def create_task(name, description, priority="medium"):
|
||||
"""创建任务"""
|
||||
payload = {
|
||||
@@ -50,151 +54,137 @@ def create_task(name, description, priority="medium"):
|
||||
print(f" [FAIL] {name}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Phase 4: SAAS 产品化 - 任务初始化")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# ========== Milestone 1: 认证与账号体系 ==========
|
||||
print("\n[1/4] 创建里程碑:认证与账号体系...")
|
||||
create_milestone(
|
||||
"认证与账号体系",
|
||||
"2026-06-23",
|
||||
"JWT 登录、注册、密码管理、Session 管理"
|
||||
)
|
||||
|
||||
create_milestone("认证与账号体系", "2026-06-23", "JWT 登录、注册、密码管理、Session 管理")
|
||||
|
||||
print("\n 录入任务...")
|
||||
|
||||
|
||||
# Day 1-2: 基础设施
|
||||
create_task("JWT 工具类实现", "sign/verify/refresh Token 功能", "high")
|
||||
create_task("bcrypt 密码哈希工具", "密码加密存储", "high")
|
||||
create_task("Redis Session 存储", "refresh_token 存储和管理", "high")
|
||||
create_task("邮件服务封装", "SMTP + 邮件模板(欢迎/验证/重置)", "medium")
|
||||
create_task("User 实体扩展", "新增 password_hash/email_verified 等字段", "high")
|
||||
|
||||
|
||||
# Day 3-4: 核心认证
|
||||
create_task("注册 API", "邮箱 + 密码注册,发送验证邮件", "urgent")
|
||||
create_task("登录 API", "JWT 签发 access_token + refresh_token", "urgent")
|
||||
create_task("登出 API", "撤销 refresh_token", "high")
|
||||
create_task("刷新 Token API", "用 refresh_token 换取新 access_token", "high")
|
||||
create_task("密码重置流程", "忘记密码 + 邮件重置链接", "medium")
|
||||
|
||||
|
||||
# Day 5-6: Session 管理
|
||||
create_task("Session 实体与 Repository", "存储设备信息、IP、过期时间", "medium")
|
||||
create_task("活跃 Session 列表 API", "查看所有登录设备", "low")
|
||||
create_task("强制登出所有设备", "撤销所有 refresh_token", "medium")
|
||||
create_task("设备信息解析", "解析 User-Agent 识别设备类型", "low")
|
||||
|
||||
|
||||
# Day 7: 测试与文档
|
||||
create_task("认证集成测试", "注册/登录/登出/刷新完整流程", "high")
|
||||
create_task("认证安全测试", "密码强度/Token 伪造/暴力破解防护", "urgent")
|
||||
create_task("认证 API 文档更新", "OpenAPI 规范更新", "low")
|
||||
|
||||
|
||||
# ========== Milestone 2: 多租户权限体系 ==========
|
||||
print("\n[2/4] 创建里程碑:多租户权限体系...")
|
||||
create_milestone(
|
||||
"多租户权限体系",
|
||||
"2026-06-30",
|
||||
"4 种角色、成员管理、邀请系统、数据隔离"
|
||||
)
|
||||
|
||||
create_milestone("多租户权限体系", "2026-06-30", "4 种角色、成员管理、邀请系统、数据隔离")
|
||||
|
||||
print("\n 录入任务...")
|
||||
|
||||
|
||||
# Day 1-2: 权限基础
|
||||
create_task("WorkspaceMembership 实体", "成员关系、角色存储", "high")
|
||||
create_task("WorkspaceRole 枚举", "Owner/Admin/Member/Viewer 权限定义", "high")
|
||||
create_task("权限检查中间件", "@require_permission 装饰器", "urgent")
|
||||
create_task("数据隔离过滤器", "所有查询自动加 workspace_id", "urgent")
|
||||
|
||||
|
||||
# Day 3-4: 成员管理
|
||||
create_task("邀请成员 API", "发送邀请邮件 + 生成邀请令牌", "high")
|
||||
create_task("接受/拒绝邀请 API", "处理邀请状态", "high")
|
||||
create_task("移除成员 API", "删除 WorkspaceMembership", "medium")
|
||||
create_task("修改成员角色 API", "Owner/Admin 可修改其他人角色", "medium")
|
||||
create_task("转让 Workspace 所有权", "Owner 转让给其他成员", "low")
|
||||
|
||||
|
||||
# Day 5-6: 权限验证
|
||||
create_task("为所有现有 API 加权限检查", "项目/任务/素材 API 权限保护", "urgent")
|
||||
create_task("跨 Workspace 访问防护测试", "确保数据隔离无漏洞", "urgent")
|
||||
create_task("权限矩阵验证", "测试各角色权限边界", "high")
|
||||
|
||||
|
||||
# Day 7: 测试与文档
|
||||
create_task("权限系统集成测试", "各角色操作权限完整测试", "high")
|
||||
create_task("数据隔离安全测试", "跨 Workspace 攻击测试", "urgent")
|
||||
create_task("权限 API 文档更新", "成员管理 API 文档", "low")
|
||||
|
||||
|
||||
# ========== Milestone 3: 订阅与计费 ==========
|
||||
print("\n[3/4] 创建里程碑:订阅与计费...")
|
||||
create_milestone(
|
||||
"订阅与计费体系",
|
||||
"2026-07-07",
|
||||
"3 种套餐、支付宝/微信支付、账单管理"
|
||||
)
|
||||
|
||||
create_milestone("订阅与计费体系", "2026-07-07", "3 种套餐、支付宝/微信支付、账单管理")
|
||||
|
||||
print("\n 录入任务...")
|
||||
|
||||
|
||||
# Day 1-2: 订阅基础
|
||||
create_task("Subscription 实体与 Repository", "订阅状态、套餐、到期时间", "high")
|
||||
create_task("SubscriptionPlan 枚举", "free/pro/enterprise 套餐定义", "high")
|
||||
create_task("配额检查工具", "检查 Workspace/项目/存储限制", "high")
|
||||
create_task("套餐限制中间件", "创建资源前检查配额", "urgent")
|
||||
|
||||
|
||||
# Day 3-4: 支付集成
|
||||
create_task("支付宝 SDK 集成", "生成支付二维码", "high")
|
||||
create_task("微信支付 SDK 集成", "生成支付二维码", "high")
|
||||
create_task("创建支付订单 API", "用户选择套餐 → 生成订单", "high")
|
||||
create_task("支付回调处理", "验证签名 + 更新订单状态 + 激活订阅", "urgent")
|
||||
|
||||
|
||||
# Day 5-6: 账单管理
|
||||
create_task("Invoice 实体与 Repository", "账单记录存储", "medium")
|
||||
create_task("生成账单 PDF", "使用模板生成 PDF 发票", "low")
|
||||
create_task("账单列表/下载 API", "用户查看历史账单", "medium")
|
||||
create_task("订阅历史记录", "订阅变更历史追踪", "low")
|
||||
|
||||
|
||||
# Day 7: 测试与文档
|
||||
create_task("支付流程端到端测试", "沙箱环境完整支付流程", "high")
|
||||
create_task("配额检查测试", "超出限制时正确拦截", "high")
|
||||
create_task("计费 API 文档更新", "订阅/支付/账单 API 文档", "low")
|
||||
|
||||
|
||||
# ========== Milestone 4: 前端集成与上线 ==========
|
||||
print("\n[4/4] 创建里程碑:前端集成与上线...")
|
||||
create_milestone(
|
||||
"前端集成与上线",
|
||||
"2026-07-14",
|
||||
"认证 UI、权限 UI、订阅 UI、端到端测试、生产部署"
|
||||
)
|
||||
|
||||
create_milestone("前端集成与上线", "2026-07-14", "认证 UI、权限 UI、订阅 UI、端到端测试、生产部署")
|
||||
|
||||
print("\n 录入任务...")
|
||||
|
||||
|
||||
# Day 1-2: 认证 UI
|
||||
create_task("登录页面", "邮箱/密码登录表单 + 记住我", "high")
|
||||
create_task("注册页面", "邮箱注册 + 密码强度提示", "high")
|
||||
create_task("忘记密码页面", "邮箱重置流程", "medium")
|
||||
create_task("邮箱验证提示", "注册后验证邮件提醒", "low")
|
||||
|
||||
|
||||
# Day 3-4: 权限 UI
|
||||
create_task("成员管理页面", "Workspace 成员列表 + 角色显示", "high")
|
||||
create_task("邀请成员弹窗", "输入邮箱 + 选择角色", "medium")
|
||||
create_task("角色选择器组件", "下拉选择 Owner/Admin/Member/Viewer", "low")
|
||||
create_task("权限说明文档", "各角色权限说明页面", "low")
|
||||
|
||||
|
||||
# Day 5-6: 订阅 UI
|
||||
create_task("套餐选择页面", "免费版/专业版/企业版对比表", "high")
|
||||
create_task("支付二维码页面", "显示支付宝/微信二维码 + 轮询支付状态", "high")
|
||||
create_task("账单管理页面", "历史账单列表 + 下载", "medium")
|
||||
create_task("配额使用展示", "当前 Workspace/项目/存储使用情况", "medium")
|
||||
|
||||
|
||||
# Day 7: 上线准备
|
||||
create_task("Phase 4 端到端测试", "注册 → 邀请成员 → 付费 → 使用完整流程", "urgent")
|
||||
create_task("Phase 4 性能测试", "登录/权限检查响应时间测试", "high")
|
||||
create_task("Phase 4 安全审计", "SQL注入/XSS/CSRF/Token安全检查", "urgent")
|
||||
create_task("Phase 4 生产环境部署", "部署到服务器 + 灰度发布", "high")
|
||||
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("[OK] Phase 4 所有任务已录入推进器!")
|
||||
print("=" * 60)
|
||||
print("\n访问推进器: http://47.98.113.167:8088/projects")
|
||||
print("共创建 4 个里程碑 + 68 个任务\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Phase 6: 前端完善 - 任务初始化脚本
|
||||
将 Phase 6 的所有任务同步到推进器
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
@@ -64,7 +65,6 @@ PHASE6_TASKS = [
|
||||
"priority": "medium",
|
||||
"estimated_hours": 3,
|
||||
},
|
||||
|
||||
# Week 3: 认证页面 (5个任务)
|
||||
{
|
||||
"name": "登录页面",
|
||||
@@ -106,7 +106,6 @@ PHASE6_TASKS = [
|
||||
"priority": "high",
|
||||
"estimated_hours": 4,
|
||||
},
|
||||
|
||||
# Week 4: 工作空间管理 (6个任务)
|
||||
{
|
||||
"name": "工作空间列表页面",
|
||||
@@ -156,7 +155,6 @@ PHASE6_TASKS = [
|
||||
"priority": "medium",
|
||||
"estimated_hours": 3,
|
||||
},
|
||||
|
||||
# Week 5: 订阅管理 (5个任务)
|
||||
{
|
||||
"name": "套餐选择页面",
|
||||
@@ -198,7 +196,6 @@ PHASE6_TASKS = [
|
||||
"priority": "low",
|
||||
"estimated_hours": 3,
|
||||
},
|
||||
|
||||
# Week 6: Admin 后台 (5个任务)
|
||||
{
|
||||
"name": "Dashboard 仪表盘",
|
||||
@@ -240,7 +237,6 @@ PHASE6_TASKS = [
|
||||
"priority": "medium",
|
||||
"estimated_hours": 4,
|
||||
},
|
||||
|
||||
# Week 7: 个人中心 (4个任务)
|
||||
{
|
||||
"name": "个人设置页面",
|
||||
@@ -274,7 +270,6 @@ PHASE6_TASKS = [
|
||||
"priority": "medium",
|
||||
"estimated_hours": 4,
|
||||
},
|
||||
|
||||
# Week 8: 测试和优化 (8个任务)
|
||||
{
|
||||
"name": "E2E 测试:认证流程",
|
||||
@@ -345,41 +340,44 @@ PHASE6_TASKS = [
|
||||
|
||||
def init_phase6_tasks():
|
||||
"""初始化 Phase 6 任务到数据库"""
|
||||
|
||||
|
||||
# 连接数据库
|
||||
db_path = "F:/openclaw-saas/tracker.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
print("[Phase 6] 开始初始化任务...")
|
||||
|
||||
|
||||
# 插入任务
|
||||
created_count = 0
|
||||
for task in PHASE6_TASKS:
|
||||
try:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (
|
||||
name, description, status, phase, milestone,
|
||||
priority, estimated_hours, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
task["name"],
|
||||
task["description"],
|
||||
"pending",
|
||||
task["phase"],
|
||||
task["milestone"],
|
||||
task["priority"],
|
||||
task["estimated_hours"],
|
||||
datetime.now().isoformat()
|
||||
))
|
||||
""",
|
||||
(
|
||||
task["name"],
|
||||
task["description"],
|
||||
"pending",
|
||||
task["phase"],
|
||||
task["milestone"],
|
||||
task["priority"],
|
||||
task["estimated_hours"],
|
||||
datetime.now().isoformat(),
|
||||
),
|
||||
)
|
||||
created_count += 1
|
||||
print(f"[OK] {task['name']}")
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {task['name']}: {e}")
|
||||
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
print(f"\n[SUCCESS] Phase 6 任务初始化完成!")
|
||||
print(f"📊 总计 {len(PHASE6_TASKS)} 个任务")
|
||||
print(f"✅ 成功创建 {created_count} 个任务")
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
小虾 SaaS 项目推进器数据初始化脚本
|
||||
将当前项目状态、规则、待办事项录入推进器
|
||||
"""
|
||||
|
||||
import sys
|
||||
import io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
||||
|
||||
import requests
|
||||
import json
|
||||
@@ -19,6 +21,7 @@ API_BASE = "http://47.98.113.167:8089/api/v1"
|
||||
PROJECT_ID = "00000000-0000-0000-0000-000000000001"
|
||||
WORKSPACE_ID = "00000000-0000-0000-0000-000000000001"
|
||||
|
||||
|
||||
def create_milestone(name, target_date, description=""):
|
||||
"""创建里程碑"""
|
||||
payload = {
|
||||
@@ -26,7 +29,7 @@ def create_milestone(name, target_date, description=""):
|
||||
"target_date": target_date,
|
||||
"project_id": PROJECT_ID,
|
||||
"workspace_id": WORKSPACE_ID,
|
||||
"description": description
|
||||
"description": description,
|
||||
}
|
||||
resp = requests.post(f"{API_BASE}/project-management/milestones", json=payload)
|
||||
if resp.status_code == 200:
|
||||
@@ -36,6 +39,7 @@ def create_milestone(name, target_date, description=""):
|
||||
print(f"[FAIL] 里程碑创建失败: {name} - {resp.text}")
|
||||
return None
|
||||
|
||||
|
||||
def create_task(name, description, priority="medium", status="pending", progress=0, tags=None):
|
||||
"""创建任务"""
|
||||
payload = {
|
||||
@@ -53,48 +57,49 @@ def create_task(name, description, priority="medium", status="pending", progress
|
||||
print(f"[FAIL] 任务创建失败: {name} - {resp.text}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("小虾 SaaS 项目推进器 - 数据初始化")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# ========== 里程碑 ==========
|
||||
print("\n[1/4] 创建里程碑...")
|
||||
|
||||
|
||||
milestones = [
|
||||
{
|
||||
"name": "Phase 1: 核心平台层完成",
|
||||
"target_date": "2026-06-15",
|
||||
"description": "Clean Architecture 骨架 + 核心业务对象 + 完整测试"
|
||||
"description": "Clean Architecture 骨架 + 核心业务对象 + 完整测试",
|
||||
},
|
||||
{
|
||||
"name": "Phase 2: 项目管理模块落地",
|
||||
"target_date": "2026-06-16",
|
||||
"description": "任务/里程碑/问题管理 + 前后端完整链路"
|
||||
"description": "任务/里程碑/问题管理 + 前后端完整链路",
|
||||
},
|
||||
{
|
||||
"name": "Phase 3: 部署与备案完成",
|
||||
"target_date": "2026-06-30",
|
||||
"description": "生产环境部署 + HTTPS 证书 + 域名备案通过"
|
||||
"description": "生产环境部署 + HTTPS 证书 + 域名备案通过",
|
||||
},
|
||||
{
|
||||
"name": "Phase 4: SAAS 产品化完成",
|
||||
"target_date": "2026-07-15",
|
||||
"description": "多租户 + 权限体系 + 订阅计费"
|
||||
"description": "多租户 + 权限体系 + 订阅计费",
|
||||
},
|
||||
{
|
||||
"name": "Phase 5: AI 剪辑能力接入",
|
||||
"target_date": "2026-08-01",
|
||||
"description": "视频分类模型 + 自动剪辑 + 配音合成"
|
||||
}
|
||||
"description": "视频分类模型 + 自动剪辑 + 配音合成",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for m in milestones:
|
||||
create_milestone(m["name"], m["target_date"], m["description"])
|
||||
|
||||
|
||||
# ========== 已完成任务 ==========
|
||||
print("\n[OK] 录入已完成任务...")
|
||||
|
||||
|
||||
completed_tasks = [
|
||||
{
|
||||
"name": "Clean Architecture 架构设计",
|
||||
@@ -102,7 +107,7 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "urgent",
|
||||
"tags": ["架构", "Phase1"]
|
||||
"tags": ["架构", "Phase1"],
|
||||
},
|
||||
{
|
||||
"name": "核心业务对象实现",
|
||||
@@ -110,7 +115,7 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "high",
|
||||
"tags": ["业务逻辑", "Phase1"]
|
||||
"tags": ["业务逻辑", "Phase1"],
|
||||
},
|
||||
{
|
||||
"name": "双持久化实现",
|
||||
@@ -118,7 +123,7 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "high",
|
||||
"tags": ["数据库", "Phase1"]
|
||||
"tags": ["数据库", "Phase1"],
|
||||
},
|
||||
{
|
||||
"name": "项目管理模块开发",
|
||||
@@ -126,7 +131,7 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "urgent",
|
||||
"tags": ["项目管理", "Phase2"]
|
||||
"tags": ["项目管理", "Phase2"],
|
||||
},
|
||||
{
|
||||
"name": "项目推进器前端开发",
|
||||
@@ -134,7 +139,7 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "high",
|
||||
"tags": ["前端", "Phase2"]
|
||||
"tags": ["前端", "Phase2"],
|
||||
},
|
||||
{
|
||||
"name": "Docker Compose 部署",
|
||||
@@ -142,7 +147,7 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "high",
|
||||
"tags": ["部署", "Phase3"]
|
||||
"tags": ["部署", "Phase3"],
|
||||
},
|
||||
{
|
||||
"name": "Nginx 反向代理配置",
|
||||
@@ -150,16 +155,16 @@ def main():
|
||||
"status": "COMPLETED",
|
||||
"progress": 100,
|
||||
"priority": "medium",
|
||||
"tags": ["部署", "Phase3"]
|
||||
}
|
||||
"tags": ["部署", "Phase3"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for task in completed_tasks:
|
||||
create_task(**task)
|
||||
|
||||
|
||||
# ========== 进行中任务 ==========
|
||||
print("\n[2/4] 录入进行中任务...")
|
||||
|
||||
|
||||
in_progress_tasks = [
|
||||
{
|
||||
"name": "域名备案审核",
|
||||
@@ -167,7 +172,7 @@ def main():
|
||||
"status": "IN_PROGRESS",
|
||||
"progress": 50,
|
||||
"priority": "urgent",
|
||||
"tags": ["部署", "Phase3", "阻塞"]
|
||||
"tags": ["部署", "Phase3", "阻塞"],
|
||||
},
|
||||
{
|
||||
"name": "HTTPS 证书申请",
|
||||
@@ -175,16 +180,16 @@ def main():
|
||||
"status": "BLOCKED",
|
||||
"progress": 0,
|
||||
"priority": "high",
|
||||
"tags": ["部署", "Phase3", "依赖备案"]
|
||||
}
|
||||
"tags": ["部署", "Phase3", "依赖备案"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for task in in_progress_tasks:
|
||||
create_task(**task)
|
||||
|
||||
|
||||
# ========== 待办任务 ==========
|
||||
print("\n[3/4] 录入待办任务...")
|
||||
|
||||
|
||||
pending_tasks = [
|
||||
{
|
||||
"name": "切换到正式域名和 HTTPS",
|
||||
@@ -192,7 +197,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "high",
|
||||
"tags": ["部署", "Phase3", "依赖备案"]
|
||||
"tags": ["部署", "Phase3", "依赖备案"],
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 生产环境切换",
|
||||
@@ -200,7 +205,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "high",
|
||||
"tags": ["数据库", "Phase3"]
|
||||
"tags": ["数据库", "Phase3"],
|
||||
},
|
||||
{
|
||||
"name": "前端环境变量配置",
|
||||
@@ -208,7 +213,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "medium",
|
||||
"tags": ["前端", "Phase3", "依赖备案"]
|
||||
"tags": ["前端", "Phase3", "依赖备案"],
|
||||
},
|
||||
{
|
||||
"name": "甘特图视图开发",
|
||||
@@ -216,7 +221,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "medium",
|
||||
"tags": ["前端", "Phase2"]
|
||||
"tags": ["前端", "Phase2"],
|
||||
},
|
||||
{
|
||||
"name": "批量操作 API",
|
||||
@@ -224,7 +229,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "low",
|
||||
"tags": ["后端", "Phase2"]
|
||||
"tags": ["后端", "Phase2"],
|
||||
},
|
||||
{
|
||||
"name": "数据导出功能",
|
||||
@@ -232,7 +237,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "low",
|
||||
"tags": ["功能", "Phase2"]
|
||||
"tags": ["功能", "Phase2"],
|
||||
},
|
||||
{
|
||||
"name": "多租户权限体系",
|
||||
@@ -240,7 +245,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "urgent",
|
||||
"tags": ["权限", "Phase4"]
|
||||
"tags": ["权限", "Phase4"],
|
||||
},
|
||||
{
|
||||
"name": "认证与账号体系",
|
||||
@@ -248,7 +253,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "urgent",
|
||||
"tags": ["认证", "Phase4"]
|
||||
"tags": ["认证", "Phase4"],
|
||||
},
|
||||
{
|
||||
"name": "订阅与计费体系",
|
||||
@@ -256,7 +261,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "high",
|
||||
"tags": ["商业化", "Phase4"]
|
||||
"tags": ["商业化", "Phase4"],
|
||||
},
|
||||
{
|
||||
"name": "视频分类模型接入",
|
||||
@@ -264,7 +269,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "urgent",
|
||||
"tags": ["AI", "Phase5"]
|
||||
"tags": ["AI", "Phase5"],
|
||||
},
|
||||
{
|
||||
"name": "自动剪辑能力",
|
||||
@@ -272,7 +277,7 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "high",
|
||||
"tags": ["AI", "Phase5"]
|
||||
"tags": ["AI", "Phase5"],
|
||||
},
|
||||
{
|
||||
"name": "配音合成能力",
|
||||
@@ -280,18 +285,19 @@ def main():
|
||||
"status": "PENDING",
|
||||
"progress": 0,
|
||||
"priority": "medium",
|
||||
"tags": ["AI", "Phase5"]
|
||||
}
|
||||
"tags": ["AI", "Phase5"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
for task in pending_tasks:
|
||||
create_task(**task)
|
||||
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("[OK] 数据初始化完成!")
|
||||
print("=" * 60)
|
||||
print(f"\n访问推进器: http://47.98.113.167:8088/projects")
|
||||
print(f"访问 API 文档: http://47.98.113.167:8089/docs\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+145
-82
@@ -3,35 +3,40 @@
|
||||
"""
|
||||
直接写入 tracker.db 的脚本
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
DB_PATH = "tracker.db"
|
||||
|
||||
|
||||
def init_database():
|
||||
"""初始化所有 Phase 4 和 Phase 6 的任务数据"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
# 清空现有数据
|
||||
cursor.execute("DELETE FROM logs")
|
||||
cursor.execute("DELETE FROM tasks")
|
||||
cursor.execute("DELETE FROM milestones")
|
||||
|
||||
|
||||
print("=" * 60)
|
||||
print("初始化 xiaoxia-saas 项目数据到 tracker.db")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# ========== Phase 4: SAAS 产品化 ==========
|
||||
print("\n[Phase 4] 初始化任务...")
|
||||
|
||||
|
||||
# Milestone 1: 认证与账号体系
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("认证与账号体系", "Phase 4", "2026-06-10", "2026-06-17", "completed", "JWT 登录、注册、密码管理"))
|
||||
""",
|
||||
("认证与账号体系", "Phase 4", "2026-06-10", "2026-06-17", "completed", "JWT 登录、注册、密码管理"),
|
||||
)
|
||||
milestone1_id = cursor.lastrowid
|
||||
|
||||
|
||||
auth_tasks = [
|
||||
("JWT 工具类实现", "sign/verify/refresh Token 功能", "completed", "high"),
|
||||
("bcrypt 密码哈希工具", "密码加密存储", "completed", "high"),
|
||||
@@ -44,19 +49,25 @@ def init_database():
|
||||
("刷新 Token API", "用 refresh_token 换取新 access_token", "completed", "high"),
|
||||
("密码重置流程", "忘记密码 + 邮件重置", "completed", "medium"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in auth_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 4", "认证与账号体系", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 4", "认证与账号体系", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 2: 多租户权限体系
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("多租户权限体系", "Phase 4", "2026-06-10", "2026-06-17", "completed", "4 种角色、成员管理、邀请系统"))
|
||||
|
||||
""",
|
||||
("多租户权限体系", "Phase 4", "2026-06-10", "2026-06-17", "completed", "4 种角色、成员管理、邀请系统"),
|
||||
)
|
||||
|
||||
permission_tasks = [
|
||||
("WorkspaceMembership 实体", "成员关系实体设计", "completed", "high"),
|
||||
("WorkspaceRole 枚举", "owner/admin/member/viewer", "completed", "high"),
|
||||
@@ -69,19 +80,25 @@ def init_database():
|
||||
("转让 Workspace 所有权", "转让 owner 角色", "completed", "low"),
|
||||
("权限矩阵验证", "测试所有权限组合", "completed", "high"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in permission_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 4", "多租户权限体系", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 4", "多租户权限体系", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 3: 订阅与计费
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("订阅与计费体系", "Phase 4", "2026-06-10", "2026-06-17", "completed", "Free/Pro/Enterprise 套餐"))
|
||||
|
||||
""",
|
||||
("订阅与计费体系", "Phase 4", "2026-06-10", "2026-06-17", "completed", "Free/Pro/Enterprise 套餐"),
|
||||
)
|
||||
|
||||
subscription_tasks = [
|
||||
("Subscription 实体与 Repository", "订阅数据模型", "completed", "high"),
|
||||
("SubscriptionPlan 枚举", "free/pro/enterprise", "completed", "high"),
|
||||
@@ -94,24 +111,30 @@ def init_database():
|
||||
("Invoice 实体与 Repository", "账单数据模型", "completed", "medium"),
|
||||
("生成账单 PDF", "PDF 账单生成", "pending", "low"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in subscription_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 4", "订阅与计费体系", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 4", "订阅与计费体系", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
print("[OK] Phase 4 任务已录入")
|
||||
|
||||
|
||||
# ========== Phase 6: 前端完善 ==========
|
||||
print("\n[Phase 6] 初始化任务...")
|
||||
|
||||
|
||||
# Milestone 1: 基础搭建
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("前端基础搭建", "Phase 6", "2026-06-17", "2026-06-17", "completed", "Vite + React + TypeScript"))
|
||||
|
||||
""",
|
||||
("前端基础搭建", "Phase 6", "2026-06-17", "2026-06-17", "completed", "Vite + React + TypeScript"),
|
||||
)
|
||||
|
||||
foundation_tasks = [
|
||||
("项目初始化:Vite + React + TypeScript", "创建 Vite 项目", "completed", "high"),
|
||||
("安装配置依赖包", "Ant Design, React Router 等", "completed", "high"),
|
||||
@@ -121,19 +144,25 @@ def init_database():
|
||||
("设计系统配置", "CSS 变量、色彩系统", "completed", "medium"),
|
||||
("TypeScript 类型定义", "API 响应、实体类型", "completed", "medium"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in foundation_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "前端基础搭建", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "前端基础搭建", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 2: 认证页面
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("认证系统", "Phase 6", "2026-06-17", "2026-06-17", "completed", "登录、注册、密码重置"))
|
||||
|
||||
""",
|
||||
("认证系统", "Phase 6", "2026-06-17", "2026-06-17", "completed", "登录、注册、密码重置"),
|
||||
)
|
||||
|
||||
auth_ui_tasks = [
|
||||
("登录页面", "用户登录界面和逻辑", "completed", "high"),
|
||||
("注册页面", "用户注册界面和验证", "completed", "high"),
|
||||
@@ -141,19 +170,25 @@ def init_database():
|
||||
("重置密码页面", "密码重置界面", "completed", "high"),
|
||||
("Token 管理和刷新", "自动 token 刷新", "completed", "high"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in auth_ui_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "认证系统", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "认证系统", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 3: 工作空间管理
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("工作空间管理", "Phase 6", "2026-06-17", "2026-06-17", "completed", "工作空间 CRUD、成员管理"))
|
||||
|
||||
""",
|
||||
("工作空间管理", "Phase 6", "2026-06-17", "2026-06-17", "completed", "工作空间 CRUD、成员管理"),
|
||||
)
|
||||
|
||||
workspace_tasks = [
|
||||
("工作空间列表页面", "显示所有工作空间", "completed", "high"),
|
||||
("工作空间详情页面", "工作空间概览、配额", "completed", "high"),
|
||||
@@ -162,19 +197,25 @@ def init_database():
|
||||
("权限矩阵展示", "各角色权限说明", "completed", "medium"),
|
||||
("工作空间设置", "修改名称、删除", "completed", "medium"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in workspace_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "工作空间管理", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "工作空间管理", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 4: 订阅管理
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("订阅管理", "Phase 6", "2026-06-17", "2026-06-17", "completed", "套餐选择、升级流程"))
|
||||
|
||||
""",
|
||||
("订阅管理", "Phase 6", "2026-06-17", "2026-06-17", "completed", "套餐选择、升级流程"),
|
||||
)
|
||||
|
||||
subscription_ui_tasks = [
|
||||
("套餐选择页面", "Free/Pro/Enterprise 对比", "completed", "high"),
|
||||
("升级流程", "订阅升级流程和确认", "completed", "high"),
|
||||
@@ -182,19 +223,25 @@ def init_database():
|
||||
("账单页面", "历史账单列表", "completed", "medium"),
|
||||
("发票申请入口", "发票申请表单", "completed", "low"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in subscription_ui_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "订阅管理", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "订阅管理", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 5: Admin 后台
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("Admin 后台", "Phase 6", "2026-06-17", "2026-06-17", "completed", "Dashboard、用户管理"))
|
||||
|
||||
""",
|
||||
("Admin 后台", "Phase 6", "2026-06-17", "2026-06-17", "completed", "Dashboard、用户管理"),
|
||||
)
|
||||
|
||||
admin_tasks = [
|
||||
("Dashboard 仪表盘", "关键指标和图表", "completed", "high"),
|
||||
("用户管理页面", "用户列表、搜索", "completed", "high"),
|
||||
@@ -202,38 +249,50 @@ def init_database():
|
||||
("系统监控页面", "API 性能、数据库状态", "completed", "medium"),
|
||||
("日志查看器", "错误日志和慢查询", "completed", "medium"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in admin_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "Admin 后台", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "Admin 后台", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 6: 个人中心
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("个人中心", "Phase 6", "2026-06-17", "2026-06-17", "completed", "个人设置、安全设置"))
|
||||
|
||||
""",
|
||||
("个人中心", "Phase 6", "2026-06-17", "2026-06-17", "completed", "个人设置、安全设置"),
|
||||
)
|
||||
|
||||
profile_tasks = [
|
||||
("个人设置页面", "基本信息、头像上传", "completed", "medium"),
|
||||
("账号安全设置", "修改密码、修改邮箱", "completed", "high"),
|
||||
("通知设置", "邮件通知、类型选择", "completed", "medium"),
|
||||
("Session 管理", "设备列表、登出设备", "completed", "medium"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in profile_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "个人中心", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "个人中心", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
# Milestone 7: 测试与优化
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO milestones (name, phase, start_date, end_date, status, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", ("测试与优化", "Phase 6", "2026-06-17", "2026-06-17", "completed", "单元测试、E2E 测试、性能优化"))
|
||||
|
||||
""",
|
||||
("测试与优化", "Phase 6", "2026-06-17", "2026-06-17", "completed", "单元测试、E2E 测试、性能优化"),
|
||||
)
|
||||
|
||||
test_tasks = [
|
||||
("单元测试", "Vitest + React Testing Library", "completed", "high"),
|
||||
("E2E 测试:认证流程", "Playwright 自动化测试", "completed", "high"),
|
||||
@@ -244,29 +303,32 @@ def init_database():
|
||||
("可访问性优化", "WCAG 2.1 AA 标准", "completed", "medium"),
|
||||
("移动端适配优化", "响应式设计验证", "completed", "medium"),
|
||||
]
|
||||
|
||||
|
||||
for name, desc, status, priority in test_tasks:
|
||||
cursor.execute("""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO tasks (name, description, status, phase, milestone, priority, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (name, desc, status, "Phase 6", "测试与优化", priority, datetime.now().isoformat()))
|
||||
|
||||
""",
|
||||
(name, desc, status, "Phase 6", "测试与优化", priority, datetime.now().isoformat()),
|
||||
)
|
||||
|
||||
print("[OK] Phase 6 任务已录入")
|
||||
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
# 统计
|
||||
cursor.execute("SELECT COUNT(*) FROM milestones")
|
||||
milestone_count = cursor.fetchone()[0]
|
||||
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tasks")
|
||||
task_count = cursor.fetchone()[0]
|
||||
|
||||
|
||||
cursor.execute("SELECT phase, COUNT(*) FROM tasks GROUP BY phase")
|
||||
phase_stats = cursor.fetchall()
|
||||
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("[SUCCESS] 数据初始化完成!")
|
||||
print("=" * 60)
|
||||
@@ -278,5 +340,6 @@ def init_database():
|
||||
print("\n推进器地址: http://47.98.113.167:8088/projects")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_database()
|
||||
|
||||
@@ -113,9 +113,15 @@ def smoke_oss(strict: bool) -> None:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--strict", action="store_true", help="Fail when disabled/unconfigured services are missing.")
|
||||
parser.add_argument("--skip-smtp", action="store_true", help="Skip SMTP smoke when email delivery is intentionally disabled.")
|
||||
parser.add_argument("--skip-redis", action="store_true", help="Skip Redis session smoke when sessions are intentionally disabled.")
|
||||
parser.add_argument("--skip-oss", action="store_true", help="Skip OSS smoke when object storage is intentionally disabled.")
|
||||
parser.add_argument(
|
||||
"--skip-smtp", action="store_true", help="Skip SMTP smoke when email delivery is intentionally disabled."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-redis", action="store_true", help="Skip Redis session smoke when sessions are intentionally disabled."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-oss", action="store_true", help="Skip OSS smoke when object storage is intentionally disabled."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--send-email-to", help="Actually send a test email to this address. Otherwise SMTP only connects/logs in."
|
||||
)
|
||||
|
||||
@@ -102,7 +102,9 @@ def main() -> None:
|
||||
for index in range(8):
|
||||
task_response = owner.get(f"{BASE_URL}/generation/tasks/{task_id}", headers=owner_headers, timeout=30)
|
||||
print(f"owner_generation_{index}={task_response.status_code}")
|
||||
results_response = owner.get(f"{BASE_URL}/generation/tasks/{task_id}/results", headers=owner_headers, timeout=30)
|
||||
results_response = owner.get(
|
||||
f"{BASE_URL}/generation/tasks/{task_id}/results", headers=owner_headers, timeout=30
|
||||
)
|
||||
print(f"owner_generation_results_{index}={results_response.status_code}")
|
||||
if results_response.status_code == 200:
|
||||
items = results_response.json().get("items", [])
|
||||
@@ -114,20 +116,68 @@ def main() -> None:
|
||||
checks = [
|
||||
("anon_me", requests.get(f"{BASE_URL}/auth/me", timeout=30).status_code, {401, 403}),
|
||||
("anon_workspaces", requests.get(f"{BASE_URL}/workspaces", timeout=30).status_code, {401, 403}),
|
||||
("anon_project_get", requests.get(f"{BASE_URL}/projects/{project_id}", timeout=30).status_code, {401, 403, 404}),
|
||||
("intruder_project_get", intruder.get(f"{BASE_URL}/projects/{project_id}", headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
("intruder_library_get", intruder.get(f"{BASE_URL}/asset-libraries/{library_id}", headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
("anon_assets", requests.get(f"{BASE_URL}/assets", params={"library_id": library_id}, timeout=30).status_code, {401, 403, 404}),
|
||||
("intruder_assets", intruder.get(f"{BASE_URL}/assets", params={"library_id": library_id}, headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
("anon_generation_task", requests.get(f"{BASE_URL}/generation/tasks/{task_id}", timeout=30).status_code, {401, 403, 404}),
|
||||
("intruder_generation_task", intruder.get(f"{BASE_URL}/generation/tasks/{task_id}", headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
("intruder_generation_results", intruder.get(f"{BASE_URL}/generation/tasks/{task_id}/results", headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
(
|
||||
"anon_project_get",
|
||||
requests.get(f"{BASE_URL}/projects/{project_id}", timeout=30).status_code,
|
||||
{401, 403, 404},
|
||||
),
|
||||
(
|
||||
"intruder_project_get",
|
||||
intruder.get(f"{BASE_URL}/projects/{project_id}", headers=intruder_headers, timeout=30).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
(
|
||||
"intruder_library_get",
|
||||
intruder.get(f"{BASE_URL}/asset-libraries/{library_id}", headers=intruder_headers, timeout=30).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
(
|
||||
"anon_assets",
|
||||
requests.get(f"{BASE_URL}/assets", params={"library_id": library_id}, timeout=30).status_code,
|
||||
{401, 403, 404},
|
||||
),
|
||||
(
|
||||
"intruder_assets",
|
||||
intruder.get(
|
||||
f"{BASE_URL}/assets", params={"library_id": library_id}, headers=intruder_headers, timeout=30
|
||||
).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
(
|
||||
"anon_generation_task",
|
||||
requests.get(f"{BASE_URL}/generation/tasks/{task_id}", timeout=30).status_code,
|
||||
{401, 403, 404},
|
||||
),
|
||||
(
|
||||
"intruder_generation_task",
|
||||
intruder.get(f"{BASE_URL}/generation/tasks/{task_id}", headers=intruder_headers, timeout=30).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
(
|
||||
"intruder_generation_results",
|
||||
intruder.get(
|
||||
f"{BASE_URL}/generation/tasks/{task_id}/results", headers=intruder_headers, timeout=30
|
||||
).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
]
|
||||
if video_id:
|
||||
checks.extend(
|
||||
[
|
||||
("intruder_video_get", intruder.get(f"{BASE_URL}/generated-videos/{video_id}", headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
("intruder_video_download", intruder.get(f"{BASE_URL}/generated-videos/{video_id}/download-url", headers=intruder_headers, timeout=30).status_code, {403, 404}),
|
||||
(
|
||||
"intruder_video_get",
|
||||
intruder.get(
|
||||
f"{BASE_URL}/generated-videos/{video_id}", headers=intruder_headers, timeout=30
|
||||
).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
(
|
||||
"intruder_video_download",
|
||||
intruder.get(
|
||||
f"{BASE_URL}/generated-videos/{video_id}/download-url", headers=intruder_headers, timeout=30
|
||||
).status_code,
|
||||
{403, 404},
|
||||
),
|
||||
]
|
||||
)
|
||||
upload_response = intruder.post(
|
||||
|
||||
@@ -25,16 +25,20 @@ def _asset(name: str, mime_type: str, status: AssetStatus) -> Asset:
|
||||
|
||||
def test_generation_preflight_rejects_library_without_ready_video():
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_ensure_library_has_ready_video_assets([
|
||||
_asset("image.jpg", "image/jpeg", AssetStatus.READY),
|
||||
_asset("video.mp4", "video/mp4", AssetStatus.UPLOADING),
|
||||
])
|
||||
_ensure_library_has_ready_video_assets(
|
||||
[
|
||||
_asset("image.jpg", "image/jpeg", AssetStatus.READY),
|
||||
_asset("video.mp4", "video/mp4", AssetStatus.UPLOADING),
|
||||
]
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 422
|
||||
assert "ready 状态的视频素材" in exc_info.value.detail
|
||||
|
||||
|
||||
def test_generation_preflight_accepts_ready_video():
|
||||
_ensure_library_has_ready_video_assets([
|
||||
_asset("video.mp4", "video/mp4", AssetStatus.READY),
|
||||
])
|
||||
_ensure_library_has_ready_video_assets(
|
||||
[
|
||||
_asset("video.mp4", "video/mp4", AssetStatus.READY),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ from pathlib import Path
|
||||
def test_production_resource_check_writes_duty_report():
|
||||
script = Path("scripts/production_resource_check.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "REPORT_PATH=\"${REPORT_PATH:-/var/lib/xiaoxia-ci/duty_report.json}\"" in script
|
||||
assert "API_HEALTH_URL=\"${API_HEALTH_URL:-http://127.0.0.1:8001/health}\"" in script
|
||||
assert "WEB_HEALTH_URL=\"${WEB_HEALTH_URL:-http://127.0.0.1:3002/}\"" in script
|
||||
assert 'REPORT_PATH="${REPORT_PATH:-/var/lib/xiaoxia-ci/duty_report.json}"' in script
|
||||
assert 'API_HEALTH_URL="${API_HEALTH_URL:-http://127.0.0.1:8001/health}"' in script
|
||||
assert 'WEB_HEALTH_URL="${WEB_HEALTH_URL:-http://127.0.0.1:3002/}"' in script
|
||||
assert "EXPECTED_VERSION" in script
|
||||
assert "json.dump(report" in script
|
||||
assert "os.replace(tmp_path, report_path)" in script
|
||||
@@ -15,10 +15,10 @@ def test_production_resource_check_writes_duty_report():
|
||||
def test_production_resource_check_has_resource_thresholds():
|
||||
script = Path("scripts/production_resource_check.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "LOAD_WARN_PER_CPU=\"${LOAD_WARN_PER_CPU:-1.5}\"" in script
|
||||
assert "MEM_AVAILABLE_WARN_MB=\"${MEM_AVAILABLE_WARN_MB:-256}\"" in script
|
||||
assert "DISK_WARN_PERCENT=\"${DISK_WARN_PERCENT:-85}\"" in script
|
||||
assert "SWAP_USED_WARN_PERCENT=\"${SWAP_USED_WARN_PERCENT:-60}\"" in script
|
||||
assert 'LOAD_WARN_PER_CPU="${LOAD_WARN_PER_CPU:-1.5}"' in script
|
||||
assert 'MEM_AVAILABLE_WARN_MB="${MEM_AVAILABLE_WARN_MB:-256}"' in script
|
||||
assert 'DISK_WARN_PERCENT="${DISK_WARN_PERCENT:-85}"' in script
|
||||
assert 'SWAP_USED_WARN_PERCENT="${SWAP_USED_WARN_PERCENT:-60}"' in script
|
||||
assert "未启用 swap" in script
|
||||
assert "根分区磁盘使用率偏高" in script
|
||||
|
||||
|
||||
@@ -32,23 +32,23 @@ def test_deploy_production_uses_production_infra_and_project():
|
||||
assert "Skipping production API/worker image builds" in script
|
||||
assert "RELEASE_VERSION" in script
|
||||
assert "RUNTIME_IMAGE_TAR" in script
|
||||
assert "docker load -i \"$RUNTIME_IMAGE_TAR\"" in script
|
||||
assert 'docker load -i "$RUNTIME_IMAGE_TAR"' in script
|
||||
assert 'export APP_VERSION="$RELEASE_VERSION"' in script
|
||||
assert 'export WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-1}"' in script
|
||||
assert 'export WORKER_MAX_TASKS_PER_CHILD="${WORKER_MAX_TASKS_PER_CHILD:-100}"' in script
|
||||
assert "docker image inspect \"${API_IMAGE:-xiaoxia-saas-api:dev}\"" in script
|
||||
assert "docker image inspect \"${WORKER_IMAGE:-xiaoxia-saas-worker:dev}\"" in script
|
||||
assert 'docker image inspect "${API_IMAGE:-xiaoxia-saas-api:dev}"' in script
|
||||
assert 'docker image inspect "${WORKER_IMAGE:-xiaoxia-saas-worker:dev}"' in script
|
||||
assert "ALLOW_PRODUCTION_BUILDS=true" in script
|
||||
assert "xiaoxia-postgres-production" in script
|
||||
assert "xiaoxia-redis-production" in script
|
||||
assert "xiaoxia-postgres\n" not in script
|
||||
assert "xiaoxia-redis\n" not in script
|
||||
assert "COMPOSE_PROJECT_NAME=xiaoxia-production-app" in script
|
||||
assert "--env-file \"$ENV_FILE\"" in script
|
||||
assert '--env-file "$ENV_FILE"' in script
|
||||
assert "python /app/scripts/validate_release_env.py --from-environ --strict-external" in script
|
||||
assert "alembic upgrade head" in script
|
||||
assert "docker compose --env-file \"$ENV_FILE\" up -d api worker" in script
|
||||
assert "docker compose --env-file \"$ENV_FILE\" up -d --force-recreate web" in script
|
||||
assert 'docker compose --env-file "$ENV_FILE" up -d api worker' in script
|
||||
assert 'docker compose --env-file "$ENV_FILE" up -d --force-recreate web' in script
|
||||
assert "nginx resolves the current API container IP" in script
|
||||
assert "/etc/cron.d/xiaoxia-production-resource-check" in script
|
||||
assert "EXPECTED_VERSION=$RELEASE_VERSION" in script
|
||||
@@ -58,10 +58,10 @@ def test_production_workflow_preserves_previous_web_assets():
|
||||
workflow = Path(".gitea/workflows/deploy.yml").read_text(encoding="utf-8")
|
||||
production_section = workflow.split("deploy-production:", 1)[1]
|
||||
|
||||
assert "old_assets_dir=\"/tmp/xiaoxia-previous-web-assets-${RELEASE_VERSION}\"" in production_section
|
||||
assert "docker cp xiaoxia-web-production:/usr/share/nginx/html/assets/. \"$old_assets_dir\"/" in production_section
|
||||
assert "cp -a /var/lib/xiaoxia-saas-production/repo/apps/web/dist/assets/. \"$old_assets_dir\"/" in production_section
|
||||
assert "if [ ! -e \"/var/lib/xiaoxia-saas-production/repo/apps/web/dist/assets/$name\" ]; then" in production_section
|
||||
assert 'old_assets_dir="/tmp/xiaoxia-previous-web-assets-${RELEASE_VERSION}"' in production_section
|
||||
assert 'docker cp xiaoxia-web-production:/usr/share/nginx/html/assets/. "$old_assets_dir"/' in production_section
|
||||
assert 'cp -a /var/lib/xiaoxia-saas-production/repo/apps/web/dist/assets/. "$old_assets_dir"/' in production_section
|
||||
assert 'if [ ! -e "/var/lib/xiaoxia-saas-production/repo/apps/web/dist/assets/$name" ]; then' in production_section
|
||||
|
||||
|
||||
def test_production_deploy_recreates_web_after_api_for_nginx_dns():
|
||||
@@ -82,7 +82,7 @@ def test_production_nginx_static_upstream_requires_web_recreate():
|
||||
assert "client_max_body_size 800m;" in config
|
||||
assert "proxy_read_timeout 300s;" in config
|
||||
assert "proxy_request_buffering off;" in config
|
||||
assert 'location = /index.html' in config
|
||||
assert "location = /index.html" in config
|
||||
assert 'Cache-Control "no-store, no-cache, must-revalidate" always' in config
|
||||
assert "--force-recreate web" in script
|
||||
|
||||
@@ -128,7 +128,7 @@ def test_deploy_scripts_build_web_image_explicitly():
|
||||
compose = Path("infra/docker/compose.yml").read_text(encoding="utf-8")
|
||||
|
||||
assert "docker compose build --pull=false web" in staging_script
|
||||
assert "docker compose --env-file \"$ENV_FILE\" build --pull=false web" in production_script
|
||||
assert 'docker compose --env-file "$ENV_FILE" build --pull=false web' in production_script
|
||||
assert "dockerfile: ${WEB_DOCKERFILE:-infra/docker/web.Dockerfile}" in compose
|
||||
assert "NGINX_CONF: ${WEB_NGINX_CONF:-infra/docker/nginx.conf}" in compose
|
||||
assert "APP_VERSION: ${APP_VERSION:-0.1.0}" in compose
|
||||
@@ -139,8 +139,8 @@ def test_production_deploy_prunes_old_unused_docker_artifacts():
|
||||
script = Path("infra/docker/deploy-production.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert "PRUNE_UNUSED_DOCKER_AFTER_DEPLOY" in script
|
||||
assert "docker image prune -af --filter \"until=168h\"" in script
|
||||
assert "docker builder prune -af --filter \"until=168h\"" in script
|
||||
assert 'docker image prune -af --filter "until=168h"' in script
|
||||
assert 'docker builder prune -af --filter "until=168h"' in script
|
||||
|
||||
|
||||
def test_worker_runtime_is_constrained_by_environment():
|
||||
@@ -149,6 +149,7 @@ def test_worker_runtime_is_constrained_by_environment():
|
||||
assert "--concurrency=${WORKER_CONCURRENCY:-1}" in dockerfile
|
||||
assert "--max-tasks-per-child=${WORKER_MAX_TASKS_PER_CHILD:-100}" in dockerfile
|
||||
|
||||
|
||||
def test_production_nginx_proxies_to_production_api_container():
|
||||
config = Path("infra/docker/nginx-production.conf").read_text(encoding="utf-8")
|
||||
|
||||
@@ -218,7 +219,7 @@ def test_gitea_production_deploy_requires_runtime_builder_job():
|
||||
production_section = workflow.split("deploy-production:", 1)[1]
|
||||
|
||||
assert "runs-on: runtime-builder" in build_section
|
||||
assert "scripts/build_release_images.sh \"${GITHUB_REF_NAME}\"" in build_section
|
||||
assert 'scripts/build_release_images.sh "${GITHUB_REF_NAME}"' in build_section
|
||||
assert "GITHUB_TOKEN: ${{ github.token }}" in build_section
|
||||
assert "docker.m.daocloud.io/library/node:20" in build_section
|
||||
assert "npm ci && npm run build" in build_section
|
||||
@@ -234,10 +235,12 @@ def test_gitea_production_deploy_requires_runtime_builder_job():
|
||||
assert "needs: build-production-runtime-images" in production_section
|
||||
assert "runs-on: runtime-builder" in production_section
|
||||
assert "Deploy production over SSH" in production_section
|
||||
assert "release_tar=\"/var/lib/xiaoxia-saas-production/release-${RELEASE_VERSION}.tar.gz\"" in production_section
|
||||
assert 'release_tar="/var/lib/xiaoxia-saas-production/release-${RELEASE_VERSION}.tar.gz"' in production_section
|
||||
assert "runtime-images-${RELEASE_VERSION}.tar" in production_section
|
||||
assert "apps/web/dist/index.html" in production_section
|
||||
assert "HOST_PREFIX= sh /var/lib/xiaoxia-saas-production/repo/infra/docker/deploy-production.sh" in production_section
|
||||
assert (
|
||||
"HOST_PREFIX= sh /var/lib/xiaoxia-saas-production/repo/infra/docker/deploy-production.sh" in production_section
|
||||
)
|
||||
|
||||
|
||||
def test_build_host_runbook_requires_off_production_runtime_builds():
|
||||
@@ -283,12 +286,12 @@ def test_runtime_image_release_scripts_keep_builds_off_production():
|
||||
assert "Refusing to build runtime images on a host that is running production services." in build_script
|
||||
assert "ALLOW_SHARED_PRODUCTION_BUILD_HOST=true" in build_script
|
||||
assert "docker load -i" in deploy_script
|
||||
assert "API_IMAGE=\"xiaoxia-saas-api:$VERSION\"" in deploy_script
|
||||
assert "WORKER_IMAGE=\"xiaoxia-saas-worker:$VERSION\"" in deploy_script
|
||||
assert 'API_IMAGE="xiaoxia-saas-api:$VERSION"' in deploy_script
|
||||
assert 'WORKER_IMAGE="xiaoxia-saas-worker:$VERSION"' in deploy_script
|
||||
assert "ALLOW_PRODUCTION_BUILDS=false" in deploy_script
|
||||
assert "docker compose --env-file \"$ENV_FILE\" build --pull=false web" in deploy_script
|
||||
assert "docker compose --env-file \"$ENV_FILE\" build --pull=false api" not in deploy_script
|
||||
assert "docker compose --env-file \"$ENV_FILE\" build --pull=false worker" not in deploy_script
|
||||
assert 'docker compose --env-file "$ENV_FILE" build --pull=false web' in deploy_script
|
||||
assert 'docker compose --env-file "$ENV_FILE" build --pull=false api' not in deploy_script
|
||||
assert 'docker compose --env-file "$ENV_FILE" build --pull=false worker' not in deploy_script
|
||||
|
||||
|
||||
def test_production_release_checklist_matches_automatic_release_contract():
|
||||
@@ -306,9 +309,7 @@ def test_production_release_checklist_matches_automatic_release_contract():
|
||||
|
||||
|
||||
def test_release_automation_retrospective_records_failed_probe_tags():
|
||||
retrospective = Path("docs/RELEASE-AUTOMATION-RETROSPECTIVE-2026-06-22.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
retrospective = Path("docs/RELEASE-AUTOMATION-RETROSPECTIVE-2026-06-22.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "v0.1.9" in retrospective
|
||||
assert "first verified end-to-end automatic production release" in retrospective
|
||||
|
||||
@@ -5,5 +5,5 @@ def test_ingest_marks_created_assets_ready_with_size_metadata():
|
||||
source = Path("apps/worker/worker_app/tasks/ingest.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "status=AssetStatus.READY" in source
|
||||
assert "file_size=int(metadata[\"size_bytes\"])" in source
|
||||
assert "duration=float(metadata[\"duration\"])" in source
|
||||
assert 'file_size=int(metadata["size_bytes"])' in source
|
||||
assert 'duration=float(metadata["duration"])' in source
|
||||
|
||||
Reference in New Issue
Block a user