Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion api/v1/endpoints/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
import json
import logging
import re
import threading
from datetime import datetime
from typing import Optional, Union, Dict, Any

from fastapi import APIRouter, HTTPException, Depends, Query
from fastapi import APIRouter, HTTPException, Depends, Query, BackgroundTasks
from fastapi.responses import JSONResponse, StreamingResponse

from api.deps import get_config_dep
Expand All @@ -38,6 +39,8 @@
TaskInfo,
TaskListResponse,
DuplicateTaskErrorResponse,
MarketReviewRequest,
MarketReviewAccepted,
)
from api.v1.schemas.common import ErrorResponse
from api.v1.schemas.history import (
Expand Down Expand Up @@ -69,6 +72,24 @@
router = APIRouter()

_SUPPORTED_FREE_TEXT_RE = re.compile(r"^[A-Za-z0-9.*\-+\u3400-\u9fff\s]+$")
_market_review_lock = threading.Lock()
_market_review_running = False


def _run_market_review_background(send_notification: bool) -> None:
"""Run market review after the API response has been accepted."""
global _market_review_running
try:
from src.core.market_review import run_market_review
from src.notification import NotificationService

notifier = NotificationService()
run_market_review(notifier, send_notification=send_notification)
except Exception as exc:
logger.error("大盘复盘后台任务失败: %s", exc, exc_info=True)
finally:
with _market_review_lock:
_market_review_running = False


def _invalid_analysis_input_error() -> HTTPException:
Expand Down Expand Up @@ -392,6 +413,47 @@ def _handle_sync_analysis(
)


# ============================================================
# POST /market-review - 触发大盘复盘
# ============================================================

@router.post(
"/market-review",
response_model=MarketReviewAccepted,
status_code=202,
responses={
202: {"description": "大盘复盘任务已接受", "model": MarketReviewAccepted},
409: {"description": "大盘复盘正在执行", "model": ErrorResponse},
500: {"description": "提交失败", "model": ErrorResponse},
},
summary="触发大盘复盘",
description="提交一个后台大盘复盘任务,复用 CLI 的大盘复盘链路并保存报告。",
)
def trigger_market_review(
request: MarketReviewRequest,
background_tasks: BackgroundTasks,
) -> MarketReviewAccepted:
"""Trigger market review from Web/API without blocking the request."""
global _market_review_running
with _market_review_lock:
if _market_review_running:
raise HTTPException(
status_code=409,
detail={
"error": "duplicate_market_review",
"message": "大盘复盘正在执行中,请稍后再试",
},
)
_market_review_running = True

background_tasks.add_task(_run_market_review_background, request.send_notification)
return MarketReviewAccepted(
status="accepted",
message="大盘复盘任务已提交,完成后会保存报告并按配置推送通知",
send_notification=request.send_notification,
)


# ============================================================
# GET /tasks - 获取任务列表
# ============================================================
Expand Down
17 changes: 17 additions & 0 deletions api/v1/schemas/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ class Config:
}


class MarketReviewRequest(BaseModel):
"""Market review trigger parameters."""

send_notification: bool = Field(
True,
description="是否在大盘复盘完成后发送推送通知",
)


class MarketReviewAccepted(BaseModel):
"""Market review background task accepted response."""

status: str = Field("accepted", description="提交状态")
message: str = Field(..., description="提示信息")
send_notification: bool = Field(..., description="是否发送通知")


class AnalysisResultResponse(BaseModel):
"""分析结果响应模型"""

Expand Down
33 changes: 32 additions & 1 deletion apps/dsa-web/src/api/__tests__/systemConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { systemConfigApi } from '../systemConfig';

const get = vi.hoisted(() => vi.fn());
const post = vi.hoisted(() => vi.fn());

vi.mock('../index', () => ({
default: {
get: vi.fn(),
get,
post,
put: vi.fn(),
},
}));

describe('systemConfigApi', () => {
beforeEach(() => {
get.mockReset();
post.mockReset();
post.mockResolvedValue({
data: {
Expand Down Expand Up @@ -111,4 +113,33 @@ describe('systemConfigApi', () => {
expect(result.attempts[0].errorCode).toBeNull();
expect(result.attempts[0].httpStatus).toBe(200);
});

it('loads first-run setup status with camelCase fields', async () => {
get.mockResolvedValueOnce({
data: {
is_complete: false,
ready_for_smoke: false,
required_missing_keys: ['llm_primary'],
next_step_key: 'llm_primary',
checks: [
{
key: 'llm_primary',
title: 'LLM 主渠道',
category: 'ai_model',
required: true,
status: 'needs_action',
message: '缺少主模型配置',
next_step: '打开系统设置',
},
],
},
});

const result = await systemConfigApi.getSetupStatus();

expect(get).toHaveBeenCalledWith('/api/v1/system/config/setup/status');
expect(result.isComplete).toBe(false);
expect(result.nextStepKey).toBe('llm_primary');
expect(result.checks[0].nextStep).toBe('打开系统设置');
});
});
27 changes: 27 additions & 0 deletions apps/dsa-web/src/api/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
AnalyzeResponse,
AnalyzeAsyncResponse,
AnalysisReport,
MarketReviewAccepted,
MarketReviewRequest,
TaskStatus,
TaskListResponse,
} from '../types/analysis';
Expand Down Expand Up @@ -87,6 +89,31 @@ export const analysisApi = {
return toCamelCase<AnalyzeAsyncResponse>(response.data);
},

/**
* Trigger market review in background mode.
*/
triggerMarketReview: async (data: MarketReviewRequest = {}): Promise<MarketReviewAccepted> => {
const response = await apiClient.post<Record<string, unknown>>(
'/api/v1/analysis/market-review',
{
send_notification: data.sendNotification ?? true,
},
{
validateStatus: (status) => status === 202 || status === 409,
}
);

if (response.status === 409) {
const detail = response.data?.detail;
const message = detail && typeof detail === 'object' && 'message' in detail
? String((detail as { message?: unknown }).message || '')
: String(response.data?.message || '');
throw new Error(message || '大盘复盘正在执行中,请稍后再试');
}

return toCamelCase<MarketReviewAccepted>(response.data);
},

/**
* Get async task status.
* @param taskId Task ID
Expand Down
6 changes: 6 additions & 0 deletions apps/dsa-web/src/api/systemConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
DiscoverLLMChannelModelsResponse,
ExportSystemConfigResponse,
ImportSystemConfigRequest,
SetupStatusResponse,
SystemConfigConflictResponse,
SystemConfigResponse,
SystemConfigSchemaResponse,
Expand Down Expand Up @@ -144,6 +145,11 @@ export const systemConfigApi = {
return toCamelCase<SystemConfigSchemaResponse>(response.data);
},

async getSetupStatus(): Promise<SetupStatusResponse> {
const response = await apiClient.get<Record<string, unknown>>('/api/v1/system/config/setup/status');
return toCamelCase<SetupStatusResponse>(response.data);
},

async validate(payload: ValidateSystemConfigRequest): Promise<ValidateSystemConfigResponse> {
const response = await apiClient.post<Record<string, unknown>>(
'/api/v1/system/config/validate',
Expand Down
Loading
Loading