积分冻结释放
This commit is contained in:
@@ -11,6 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.enums.common import ModuleEventTypeEnum, ModuleProjectStatusEnum, ModulePromptTypeEnum, ModuleStepStatusEnum
|
||||
from app.enums.credit_record import CreditRecordBillingScene, CreditRecordChargeKind, CreditRecordOwnerType
|
||||
from app.enums.llm_billing import LlmBillingConfigKey
|
||||
from app.enums.shot_replicate import ShotReplicateGenerationModeEnum, ShotReplicateStepCodeEnum, ModuleCodeEnum
|
||||
from app.models.chat_generation_task import ChatGenerationTask
|
||||
from app.models.module_generation_project import ModuleGenerationProject
|
||||
@@ -41,7 +43,6 @@ from app.services.generation.ai.engine_service import (
|
||||
get_video_engine,
|
||||
parse_json_list,
|
||||
)
|
||||
from app.services.generation.billing_service import charge_module_prompt_usage
|
||||
from app.services.generation.refund_service import mark_chat_generation_task_failed_and_refund_once
|
||||
from app.services.generation.pipeline.db_lock_service import (
|
||||
DatabaseRowLockBusy,
|
||||
@@ -56,6 +57,16 @@ from app.services.hot_opening_video_prompt_service import (
|
||||
)
|
||||
from app.services.module_generation_log_service import log_module_error, log_module_event_file, log_module_prompt_event
|
||||
from app.services.llm import optimize_prompt
|
||||
from app.services.llm_billing import (
|
||||
LlmBillingContext,
|
||||
ensure_hold_exists,
|
||||
log_provider_failure,
|
||||
log_provider_start,
|
||||
log_provider_success,
|
||||
release_on_failure,
|
||||
settle_success,
|
||||
start_hold,
|
||||
)
|
||||
from app.services.module_generation_flow_base_service import (
|
||||
assert_project_has_no_active_chat_tasks as _base_assert_project_has_no_active_chat_tasks,
|
||||
chat_tasks_by_id as _base_chat_tasks_by_id,
|
||||
@@ -820,6 +831,25 @@ async def submit_image_prompt_optimize(
|
||||
project.status = ModuleProjectStatusEnum.PROCESSING.value
|
||||
project.current_step_code = ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
|
||||
project.error_message = None
|
||||
await start_hold(
|
||||
db,
|
||||
LlmBillingContext(
|
||||
user_id=str(project.user_id),
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=str(step.id),
|
||||
attempt_no=int(step.version or 1),
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=CreditRecordBillingScene.SHOT_IMAGE_PROMPT_OPTIMIZE.value,
|
||||
source_module=MODULE,
|
||||
source_project_id=str(project.id),
|
||||
source_step_id=str(step.id),
|
||||
source_step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||
related_id=str(step.id),
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value,
|
||||
description_prefix="拆镜复刻图片AI提词优化",
|
||||
trace_id=f"llm-submit-hold:{step.id}",
|
||||
),
|
||||
)
|
||||
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_SUBMITTED.value, message="图片 AI 提词任务已提交")
|
||||
return project, step
|
||||
|
||||
@@ -902,8 +932,37 @@ async def run_image_prompt_optimize(
|
||||
module_value = str(project.module)
|
||||
expected_step_version = int(step.version or 1)
|
||||
expected_input_json = json.dumps(step.input_json, ensure_ascii=False, sort_keys=True, default=str)
|
||||
llm_billing_context = LlmBillingContext(
|
||||
user_id=user_id_value,
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=step_id_value,
|
||||
attempt_no=expected_step_version,
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=CreditRecordBillingScene.SHOT_IMAGE_PROMPT_OPTIMIZE.value,
|
||||
source_module=module_value,
|
||||
source_project_id=project_id_value,
|
||||
source_step_id=step_id_value,
|
||||
source_step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||
related_id=step_id_value,
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value,
|
||||
description_prefix="拆镜复刻图片AI提词优化",
|
||||
trace_id=f"shot-image-prompt:{step_id_value}",
|
||||
)
|
||||
hold_validation = await ensure_hold_exists(db, llm_billing_context)
|
||||
if not hold_validation.can_execute:
|
||||
step.status = ModuleStepStatusEnum.FAILED.value
|
||||
step.error_message = f"LLM账务状态异常({hold_validation.state.value}),已终止任务"
|
||||
step.completed_at = _now()
|
||||
project.status = ModuleProjectStatusEnum.FAILED.value
|
||||
project.error_message = step.error_message
|
||||
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.CHAT_TASK_FAILED.value, message=step.error_message)
|
||||
await db.commit()
|
||||
return step
|
||||
await db.commit()
|
||||
|
||||
provider_succeeded = False
|
||||
token_usage: dict[str, Any] = {}
|
||||
log_provider_start(llm_billing_context, detail={"prompt_type": "image"})
|
||||
try:
|
||||
request_log = {"original_prompt": prompt_text, "references": references, "gen_type": "image"}
|
||||
log_module_prompt_event(
|
||||
@@ -929,6 +988,8 @@ async def run_image_prompt_optimize(
|
||||
log_owner_id=step_id_value,
|
||||
generation_attempt_no=expected_step_version,
|
||||
)
|
||||
provider_succeeded = True
|
||||
log_provider_success(llm_billing_context, usage=token_usage)
|
||||
if execution_guard is not None:
|
||||
await execution_guard()
|
||||
project, step = await _reload_prompt_context_for_update(
|
||||
@@ -942,19 +1003,25 @@ async def run_image_prompt_optimize(
|
||||
expected_version=expected_step_version,
|
||||
expected_input_json=expected_input_json,
|
||||
):
|
||||
await db.rollback()
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-图片AI提词优化(失效结果结算)",
|
||||
)
|
||||
await db.commit()
|
||||
return None
|
||||
billing = await charge_module_prompt_usage(
|
||||
billing = await settle_success(
|
||||
db,
|
||||
user_id=project.user_id,
|
||||
step_id=step.id,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-图片AI提词优化",
|
||||
)
|
||||
actual_billing_item = next((item for item in billing.items if item.charge_key == CreditRecordChargeKind.TEXT_PROMPT.value and item.charged), None)
|
||||
usage = dict(token_usage or {})
|
||||
usage.update({
|
||||
"text_credits_cost": (billing.items[0].amount if billing.items else billing.total_charged),
|
||||
"credit_biz_key": billing.items[0].biz_key if billing.items else None,
|
||||
"text_credits_cost": billing.get_amount(CreditRecordChargeKind.TEXT_PROMPT.value),
|
||||
"credit_biz_key": actual_billing_item.biz_key if actual_billing_item else None,
|
||||
})
|
||||
step.status = ModuleStepStatusEnum.COMPLETED.value
|
||||
step.completed_at = _now()
|
||||
@@ -991,9 +1058,22 @@ async def run_image_prompt_optimize(
|
||||
await db.commit()
|
||||
except DatabaseRowLockBusy:
|
||||
await db.rollback()
|
||||
if provider_succeeded:
|
||||
# Provider 已完成后不再重复调用模型;先按真实 usage 结算,本次结果因本地行锁冲突丢弃。
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-图片AI提词优化(行锁失败结算)",
|
||||
)
|
||||
await db.commit()
|
||||
return None
|
||||
# Provider 尚未成功才允许同一 attempt 做系统自动重试。
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
if not provider_succeeded:
|
||||
log_provider_failure(llm_billing_context, error=str(exc))
|
||||
if execution_guard is not None:
|
||||
await execution_guard()
|
||||
project, step = await _reload_prompt_context_for_update(
|
||||
@@ -1008,6 +1088,16 @@ async def run_image_prompt_optimize(
|
||||
expected_input_json=expected_input_json,
|
||||
):
|
||||
await db.rollback()
|
||||
if provider_succeeded:
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-图片AI提词优化(异常失效结算)",
|
||||
)
|
||||
else:
|
||||
await release_on_failure(db, llm_billing_context, error="当前步骤已失效,释放LLM预扣积分")
|
||||
await db.commit()
|
||||
return None
|
||||
step.status = ModuleStepStatusEnum.FAILED.value
|
||||
step.error_message = str(exc)
|
||||
@@ -1026,6 +1116,15 @@ async def run_image_prompt_optimize(
|
||||
)
|
||||
_log_project_error(project=project, step=step, event_type="IMAGE_PROMPT_FAILED", message=project.error_message, exc=exc)
|
||||
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.IMAGE_PROMPT_FAILED.value, message=project.error_message)
|
||||
if provider_succeeded:
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-图片AI提词优化(本地失败结算)",
|
||||
)
|
||||
else:
|
||||
await release_on_failure(db, llm_billing_context, error=str(exc))
|
||||
await db.commit()
|
||||
return step
|
||||
|
||||
@@ -1200,6 +1299,25 @@ async def submit_video_prompt_optimize(
|
||||
project.status = ModuleProjectStatusEnum.PROCESSING.value
|
||||
project.current_step_code = ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value
|
||||
project.error_message = None
|
||||
await start_hold(
|
||||
db,
|
||||
LlmBillingContext(
|
||||
user_id=str(project.user_id),
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=str(step.id),
|
||||
attempt_no=int(step.version or 1),
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value,
|
||||
source_module=MODULE,
|
||||
source_project_id=str(project.id),
|
||||
source_step_id=str(step.id),
|
||||
source_step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||
related_id=str(step.id),
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value,
|
||||
description_prefix="拆镜复刻视频AI提词优化",
|
||||
trace_id=f"llm-submit-hold:{step.id}",
|
||||
),
|
||||
)
|
||||
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_SUBMITTED.value, message="视频 AI 提词任务已提交")
|
||||
return project, step
|
||||
|
||||
@@ -1280,8 +1398,37 @@ async def run_video_prompt_optimize(
|
||||
module_value = str(project.module)
|
||||
expected_step_version = int(step.version or 1)
|
||||
expected_input_json = json.dumps(step.input_json, ensure_ascii=False, sort_keys=True, default=str)
|
||||
llm_billing_context = LlmBillingContext(
|
||||
user_id=user_id_value,
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=step_id_value,
|
||||
attempt_no=expected_step_version,
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value,
|
||||
source_module=module_value,
|
||||
source_project_id=project_id_value,
|
||||
source_step_id=step_id_value,
|
||||
source_step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||
related_id=step_id_value,
|
||||
hold_config_key=LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value,
|
||||
description_prefix="拆镜复刻视频AI提词优化",
|
||||
trace_id=f"shot-video-prompt:{step_id_value}",
|
||||
)
|
||||
hold_validation = await ensure_hold_exists(db, llm_billing_context)
|
||||
if not hold_validation.can_execute:
|
||||
step.status = ModuleStepStatusEnum.FAILED.value
|
||||
step.error_message = f"LLM账务状态异常({hold_validation.state.value}),已终止任务"
|
||||
step.completed_at = _now()
|
||||
project.status = ModuleProjectStatusEnum.FAILED.value
|
||||
project.error_message = step.error_message
|
||||
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.CHAT_TASK_FAILED.value, message=step.error_message)
|
||||
await db.commit()
|
||||
return step
|
||||
await db.commit()
|
||||
|
||||
provider_succeeded = False
|
||||
token_usage: dict[str, Any] = {}
|
||||
log_provider_start(llm_billing_context, detail={"prompt_type": "video"})
|
||||
try:
|
||||
request_log = {
|
||||
"source_project_name": material.get("source_project_name") or "无",
|
||||
@@ -1319,6 +1466,8 @@ async def run_video_prompt_optimize(
|
||||
step_id=step_id_value,
|
||||
trace_id=f"shot-video-prompt:{step_id_value}",
|
||||
)
|
||||
provider_succeeded = True
|
||||
log_provider_success(llm_billing_context, usage=token_usage)
|
||||
if execution_guard is not None:
|
||||
await execution_guard()
|
||||
project, step = await _reload_prompt_context_for_update(
|
||||
@@ -1332,19 +1481,25 @@ async def run_video_prompt_optimize(
|
||||
expected_version=expected_step_version,
|
||||
expected_input_json=expected_input_json,
|
||||
):
|
||||
await db.rollback()
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-视频AI提词优化(失效结果结算)",
|
||||
)
|
||||
await db.commit()
|
||||
return None
|
||||
billing = await charge_module_prompt_usage(
|
||||
billing = await settle_success(
|
||||
db,
|
||||
user_id=project.user_id,
|
||||
step_id=step.id,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-视频AI提词优化",
|
||||
)
|
||||
actual_billing_item = next((item for item in billing.items if item.charge_key == CreditRecordChargeKind.TEXT_PROMPT.value and item.charged), None)
|
||||
usage = dict(token_usage or {})
|
||||
usage.update({
|
||||
"text_credits_cost": (billing.items[0].amount if billing.items else billing.total_charged),
|
||||
"credit_biz_key": billing.items[0].biz_key if billing.items else None,
|
||||
"text_credits_cost": billing.get_amount(CreditRecordChargeKind.TEXT_PROMPT.value),
|
||||
"credit_biz_key": actual_billing_item.biz_key if actual_billing_item else None,
|
||||
})
|
||||
step.status = ModuleStepStatusEnum.COMPLETED.value
|
||||
step.completed_at = _now()
|
||||
@@ -1384,9 +1539,22 @@ async def run_video_prompt_optimize(
|
||||
await db.commit()
|
||||
except DatabaseRowLockBusy:
|
||||
await db.rollback()
|
||||
if provider_succeeded:
|
||||
# Provider 已完成后不再重复调用模型;先按真实 usage 结算,本次结果因本地行锁冲突丢弃。
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-视频AI提词优化(行锁失败结算)",
|
||||
)
|
||||
await db.commit()
|
||||
return None
|
||||
# Provider 尚未成功才允许同一 attempt 做系统自动重试。
|
||||
raise
|
||||
except Exception as exc:
|
||||
await db.rollback()
|
||||
if not provider_succeeded:
|
||||
log_provider_failure(llm_billing_context, error=str(exc))
|
||||
if execution_guard is not None:
|
||||
await execution_guard()
|
||||
project, step = await _reload_prompt_context_for_update(
|
||||
@@ -1401,6 +1569,16 @@ async def run_video_prompt_optimize(
|
||||
expected_input_json=expected_input_json,
|
||||
):
|
||||
await db.rollback()
|
||||
if provider_succeeded:
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-视频AI提词优化(异常失效结算)",
|
||||
)
|
||||
else:
|
||||
await release_on_failure(db, llm_billing_context, error="当前步骤已失效,释放LLM预扣积分")
|
||||
await db.commit()
|
||||
return None
|
||||
step.status = ModuleStepStatusEnum.FAILED.value
|
||||
step.error_message = str(exc)
|
||||
@@ -1419,6 +1597,15 @@ async def run_video_prompt_optimize(
|
||||
)
|
||||
_log_project_error(project=project, step=step, event_type="VIDEO_PROMPT_FAILED", message=project.error_message, exc=exc)
|
||||
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.VIDEO_PROMPT_FAILED.value, message=project.error_message)
|
||||
if provider_succeeded:
|
||||
await settle_success(
|
||||
db,
|
||||
llm_billing_context,
|
||||
usage=token_usage,
|
||||
description="拆镜复刻-视频AI提词优化(本地失败结算)",
|
||||
)
|
||||
else:
|
||||
await release_on_failure(db, llm_billing_context, error=str(exc))
|
||||
await db.commit()
|
||||
return step
|
||||
|
||||
@@ -1680,7 +1867,20 @@ async def _assert_project_has_no_active_chat_tasks_for_delete(
|
||||
*,
|
||||
project: ModuleGenerationProject,
|
||||
) -> None:
|
||||
"""用户主动删除项目/切片时不退款;如仍有异步生成任务进行中,直接拦截。"""
|
||||
"""用户主动删除项目/切片时不退款;如仍有异步任务进行中,直接拦截。"""
|
||||
processing_result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(ModuleGenerationStep)
|
||||
.where(
|
||||
ModuleGenerationStep.project_id == project.id,
|
||||
ModuleGenerationStep.module == MODULE,
|
||||
ModuleGenerationStep.deleted_at.is_(None),
|
||||
ModuleGenerationStep.is_current == True,
|
||||
ModuleGenerationStep.status == ModuleStepStatusEnum.PROCESSING.value,
|
||||
)
|
||||
)
|
||||
if int(processing_result.scalar() or 0) > 0:
|
||||
raise HTTPException(status_code=409, detail="当前拆镜复刻项目仍有 AI 任务处理中,暂不能删除")
|
||||
await _base_assert_project_has_no_active_chat_tasks(
|
||||
db,
|
||||
project=project,
|
||||
@@ -1726,6 +1926,39 @@ async def mark_shot_replicate_step_dispatch_failed(
|
||||
step.completed_at = _now()
|
||||
project.status = ModuleProjectStatusEnum.FAILED.value
|
||||
project.error_message = error_message
|
||||
if step.step_code in (ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value, ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value):
|
||||
await release_on_failure(
|
||||
db,
|
||||
LlmBillingContext(
|
||||
user_id=str(project.user_id),
|
||||
owner_type=CreditRecordOwnerType.MODULE_GENERATION_STEP.value,
|
||||
owner_id=str(step.id),
|
||||
attempt_no=int(step.version or 1),
|
||||
charge_kind=CreditRecordChargeKind.TEXT_PROMPT.value,
|
||||
billing_scene=(
|
||||
CreditRecordBillingScene.SHOT_IMAGE_PROMPT_OPTIMIZE.value
|
||||
if step.step_code == ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
|
||||
else CreditRecordBillingScene.SHOT_VIDEO_PROMPT_OPTIMIZE.value
|
||||
),
|
||||
source_module=MODULE,
|
||||
source_project_id=str(project.id),
|
||||
source_step_id=str(step.id),
|
||||
source_step_code=str(step.step_code),
|
||||
related_id=str(step.id),
|
||||
hold_config_key=(
|
||||
LlmBillingConfigKey.HOLD_MODULE_IMAGE_PROMPT.value
|
||||
if step.step_code == ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
|
||||
else LlmBillingConfigKey.HOLD_MODULE_VIDEO_PROMPT.value
|
||||
),
|
||||
description_prefix=(
|
||||
"拆镜复刻图片AI提词优化"
|
||||
if step.step_code == ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value
|
||||
else "拆镜复刻视频AI提词优化"
|
||||
),
|
||||
trace_id=f"shot-replicate-dispatch-failed:{step.id}",
|
||||
),
|
||||
error=error_message,
|
||||
)
|
||||
log_module_error(
|
||||
module=project.module,
|
||||
event_type="CELERY_DISPATCH_FAILED",
|
||||
|
||||
Reference in New Issue
Block a user