restart
This commit is contained in:
1 parent
c3c3bbf955
commit
9dd180e4ad
58 files changed
+5584
-1373
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# TODO:
|
||||
|
||||
- [x] add_creation_time_display: 在ProjectCard组件中添加项目创建时间显示,使用dayjs显示相对时间并设置Asia/Shanghai时区 (priority: High)
|
||||
- [ ] fix_thumbnail_generation: 修复ProjectCard组件中的缩略图生成问题,改进错误处理和多路径尝试机制 (priority: High)
|
||||
@@ -0,0 +1,86 @@
|
||||
# AutoClip智能高光切片工具产品需求文档
|
||||
|
||||
## 1. 产品概述
|
||||
|
||||
AutoClip是一个基于AI的智能视频高光切片工具,能够自动分析视频内容,提取精彩片段并生成高质量的短视频合集。
|
||||
- 解决长视频内容筛选效率低的问题,为内容创作者和视频编辑人员提供智能化的视频处理解决方案。
|
||||
- 目标是成为视频内容创作领域的核心生产力工具,提升内容制作效率10倍以上。
|
||||
|
||||
## 2. 核心功能
|
||||
|
||||
### 2.1 用户角色
|
||||
|
||||
| 角色 | 注册方式 | 核心权限 |
|
||||
|------|----------|----------|
|
||||
| 普通用户 | 直接使用,无需注册 | 可上传视频、查看处理进度、下载切片结果 |
|
||||
| 管理员 | 系统预设 | 可管理所有项目、查看系统状态、配置处理参数 |
|
||||
|
||||
### 2.2 功能模块
|
||||
|
||||
我们的AutoClip系统包含以下主要页面:
|
||||
1. **项目管理页面**:项目列表、创建项目、项目状态监控
|
||||
2. **视频上传页面**:文件上传、格式验证、预处理配置
|
||||
3. **处理进度页面**:实时进度显示、任务状态跟踪、错误处理
|
||||
4. **结果展示页面**:切片预览、质量评分、标题生成
|
||||
5. **合集管理页面**:切片组合、合集编辑、导出设置
|
||||
6. **系统设置页面**:处理参数配置、API密钥管理、系统监控
|
||||
|
||||
### 2.3 页面详情
|
||||
|
||||
| 页面名称 | 模块名称 | 功能描述 |
|
||||
|----------|----------|----------|
|
||||
| 项目管理页面 | 项目列表 | 显示所有项目、支持搜索筛选、状态标识、批量操作 |
|
||||
| 项目管理页面 | 项目创建 | 创建新项目、设置项目名称和类型、选择处理模板 |
|
||||
| 视频上传页面 | 文件上传 | 支持拖拽上传、进度显示、格式验证、大小限制 |
|
||||
| 视频上传页面 | 字幕上传 | 可选SRT字幕文件上传、自动时间轴对齐 |
|
||||
| 处理进度页面 | 实时进度 | WebSocket实时更新、6步处理流程显示、当前步骤详情 |
|
||||
| 处理进度页面 | 任务管理 | 任务暂停/恢复、错误重试、日志查看 |
|
||||
| 结果展示页面 | 切片预览 | 视频片段播放、时间轴标注、质量评分显示 |
|
||||
| 结果展示页面 | 内容编辑 | 标题编辑、描述修改、标签管理 |
|
||||
| 合集管理页面 | 切片选择 | 多选切片、排序调整、时长控制 |
|
||||
| 合集管理页面 | 导出设置 | 分辨率选择、格式配置、水印添加 |
|
||||
| 系统设置页面 | 参数配置 | 处理阈值设置、模型选择、并发控制 |
|
||||
| 系统设置页面 | 系统监控 | 任务队列状态、系统资源使用、错误统计 |
|
||||
|
||||
## 3. 核心流程
|
||||
|
||||
### 普通用户流程
|
||||
用户首先在项目管理页面创建新项目并选择视频类型,然后在视频上传页面上传视频文件和可选的字幕文件,系统开始6步智能处理流程(大纲提取→时间定位→内容评分→标题生成→主题聚类→视频切割),用户可在处理进度页面实时查看进度,处理完成后在结果展示页面预览所有切片,最后在合集管理页面选择优质切片组合成合集并导出。
|
||||
|
||||
### 管理员流程
|
||||
管理员可以查看所有用户项目,在系统设置页面配置全局处理参数和监控系统状态,处理异常任务和系统维护。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[项目管理页面] --> B[视频上传页面]
|
||||
B --> C[处理进度页面]
|
||||
C --> D[结果展示页面]
|
||||
D --> E[合集管理页面]
|
||||
A --> F[系统设置页面]
|
||||
C --> C1[实时进度监控]
|
||||
D --> D1[切片预览播放]
|
||||
E --> E1[导出下载]
|
||||
```
|
||||
|
||||
## 4. 用户界面设计
|
||||
|
||||
### 4.1 设计风格
|
||||
- **主色调**:深蓝色(#1890ff)作为主色,浅灰色(#f5f5f5)作为背景色
|
||||
- **按钮样式**:圆角按钮,支持悬停效果和加载状态
|
||||
- **字体**:系统默认字体,标题16px,正文14px,说明文字12px
|
||||
- **布局风格**:卡片式布局,左侧导航栏,顶部面包屑导航
|
||||
- **图标风格**:使用Ant Design图标库,简洁现代的线性图标
|
||||
|
||||
### 4.2 页面设计概览
|
||||
|
||||
| 页面名称 | 模块名称 | UI元素 |
|
||||
|----------|----------|--------|
|
||||
| 项目管理页面 | 项目列表 | 卡片网格布局,每个项目卡片显示缩略图、名称、状态、进度条,支持悬停效果 |
|
||||
| 视频上传页面 | 文件上传区 | 大尺寸拖拽区域,虚线边框,上传图标,支持点击和拖拽两种方式 |
|
||||
| 处理进度页面 | 进度指示器 | 6步骤水平进度条,当前步骤高亮显示,完成步骤显示绿色勾选 |
|
||||
| 结果展示页面 | 视频播放器 | 内嵌视频播放器,时间轴标注,质量评分标签,操作按钮组 |
|
||||
| 合集管理页面 | 切片选择器 | 多选列表,缩略图预览,拖拽排序,时长统计显示 |
|
||||
| 系统设置页面 | 配置表单 | 分组表单布局,实时验证,保存状态提示 |
|
||||
|
||||
### 4.3 响应式设计
|
||||
产品采用桌面优先设计,支持平板和移动端自适应,在移动端优化触摸交互体验,确保核心功能在小屏幕设备上的可用性。
|
||||
@@ -0,0 +1,475 @@
|
||||
# AutoClip智能高光切片工具技术架构文档
|
||||
|
||||
## 1. 架构设计
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用户浏览器] --> B[React前端应用]
|
||||
B --> C[FastAPI后端服务]
|
||||
C --> D[SQLite数据库]
|
||||
C --> E[Celery任务队列]
|
||||
C --> F[WebSocket管理器]
|
||||
E --> G[Redis消息代理]
|
||||
E --> H[AI处理Pipeline]
|
||||
H --> I[LLM API服务]
|
||||
C --> J[文件存储系统]
|
||||
|
||||
subgraph "前端层"
|
||||
B
|
||||
end
|
||||
|
||||
subgraph "后端服务层"
|
||||
C
|
||||
F
|
||||
end
|
||||
|
||||
subgraph "任务处理层"
|
||||
E
|
||||
H
|
||||
end
|
||||
|
||||
subgraph "数据存储层"
|
||||
D
|
||||
G
|
||||
J
|
||||
end
|
||||
|
||||
subgraph "外部服务"
|
||||
I
|
||||
end
|
||||
```
|
||||
|
||||
## 2. 技术描述
|
||||
|
||||
- **前端**: React@18 + TypeScript + Ant Design + Zustand + Vite
|
||||
- **后端**: FastAPI + SQLAlchemy 2.0 + Celery + WebSocket
|
||||
- **数据库**: SQLite (开发) / PostgreSQL (生产)
|
||||
- **消息队列**: Redis + Celery
|
||||
- **AI处理**: 通义千问API + 自研Pipeline
|
||||
|
||||
## 3. 路由定义
|
||||
|
||||
| 路由 | 用途 |
|
||||
|------|------|
|
||||
| / | 首页,显示项目列表和快速操作入口 |
|
||||
| /projects | 项目管理页面,创建和管理所有项目 |
|
||||
| /projects/:id/upload | 视频上传页面,处理文件上传和预处理配置 |
|
||||
| /projects/:id/progress | 处理进度页面,实时显示任务执行状态 |
|
||||
| /projects/:id/results | 结果展示页面,预览和编辑生成的切片 |
|
||||
| /projects/:id/collections | 合集管理页面,组合切片并导出 |
|
||||
| /settings | 系统设置页面,配置处理参数和系统监控 |
|
||||
| /ws | WebSocket连接端点,用于实时进度推送 |
|
||||
|
||||
## 4. API定义
|
||||
|
||||
### 4.1 核心API
|
||||
|
||||
**项目管理相关**
|
||||
```
|
||||
POST /api/v1/projects
|
||||
```
|
||||
|
||||
请求参数:
|
||||
| 参数名 | 参数类型 | 是否必需 | 描述 |
|
||||
|--------|----------|----------|------|
|
||||
| name | string | true | 项目名称 |
|
||||
| description | string | false | 项目描述 |
|
||||
| project_type | string | true | 项目类型(knowledge/business/opinion等) |
|
||||
| processing_config | object | false | 处理配置参数 |
|
||||
|
||||
响应参数:
|
||||
| 参数名 | 参数类型 | 描述 |
|
||||
|--------|----------|------|
|
||||
| id | string | 项目ID |
|
||||
| name | string | 项目名称 |
|
||||
| status | string | 项目状态 |
|
||||
| created_at | string | 创建时间 |
|
||||
|
||||
示例:
|
||||
```json
|
||||
{
|
||||
"name": "知识科普视频处理",
|
||||
"project_type": "knowledge",
|
||||
"processing_config": {
|
||||
"min_score_threshold": 0.7,
|
||||
"max_clips_per_collection": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**文件上传相关**
|
||||
```
|
||||
POST /api/v1/projects/{project_id}/upload
|
||||
```
|
||||
|
||||
请求参数:
|
||||
| 参数名 | 参数类型 | 是否必需 | 描述 |
|
||||
|--------|----------|----------|------|
|
||||
| video_file | file | true | 视频文件 |
|
||||
| srt_file | file | false | 字幕文件 |
|
||||
| file_type | string | true | 文件类型(video/srt) |
|
||||
|
||||
**任务处理相关**
|
||||
```
|
||||
POST /api/v1/projects/{project_id}/process
|
||||
```
|
||||
|
||||
请求参数:
|
||||
| 参数名 | 参数类型 | 是否必需 | 描述 |
|
||||
|--------|----------|----------|------|
|
||||
| processing_steps | array | false | 指定处理步骤 |
|
||||
| priority | integer | false | 任务优先级 |
|
||||
|
||||
**WebSocket消息格式**
|
||||
```
|
||||
WS /api/v1/ws/{project_id}
|
||||
```
|
||||
|
||||
消息格式:
|
||||
```json
|
||||
{
|
||||
"type": "progress_update",
|
||||
"data": {
|
||||
"task_id": "task_123",
|
||||
"progress": 45.5,
|
||||
"current_step": "step2_timeline",
|
||||
"message": "正在进行时间定位...",
|
||||
"estimated_remaining": 120
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 服务器架构图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[客户端请求] --> B[FastAPI路由层]
|
||||
B --> C[服务层]
|
||||
C --> D[仓储层]
|
||||
D --> E[(SQLite数据库)]
|
||||
|
||||
C --> F[任务队列服务]
|
||||
F --> G[Celery Worker]
|
||||
G --> H[AI处理Pipeline]
|
||||
|
||||
C --> I[WebSocket通知服务]
|
||||
I --> J[WebSocket管理器]
|
||||
|
||||
subgraph "FastAPI应用"
|
||||
B
|
||||
C
|
||||
D
|
||||
F
|
||||
I
|
||||
end
|
||||
|
||||
subgraph "异步任务处理"
|
||||
G
|
||||
H
|
||||
end
|
||||
|
||||
subgraph "实时通信"
|
||||
J
|
||||
end
|
||||
```
|
||||
|
||||
## 6. 数据模型
|
||||
|
||||
### 6.1 数据模型定义
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
PROJECT ||--o{ TASK : has
|
||||
PROJECT ||--o{ CLIP : generates
|
||||
PROJECT ||--o{ COLLECTION : creates
|
||||
COLLECTION ||--o{ CLIP : contains
|
||||
|
||||
PROJECT {
|
||||
string id PK
|
||||
string name
|
||||
string description
|
||||
enum status
|
||||
enum project_type
|
||||
string video_path
|
||||
integer video_duration
|
||||
json processing_config
|
||||
json project_metadata
|
||||
datetime created_at
|
||||
datetime updated_at
|
||||
datetime completed_at
|
||||
}
|
||||
|
||||
TASK {
|
||||
string id PK
|
||||
string name
|
||||
string description
|
||||
enum status
|
||||
enum task_type
|
||||
float progress
|
||||
string current_step
|
||||
integer total_steps
|
||||
integer priority
|
||||
datetime started_at
|
||||
datetime completed_at
|
||||
string error_message
|
||||
string celery_task_id
|
||||
json task_config
|
||||
json result_data
|
||||
json task_metadata
|
||||
string project_id FK
|
||||
}
|
||||
|
||||
CLIP {
|
||||
string id PK
|
||||
string title
|
||||
string description
|
||||
float start_time
|
||||
float end_time
|
||||
float duration
|
||||
float score
|
||||
string video_path
|
||||
string thumbnail_path
|
||||
json clip_metadata
|
||||
string project_id FK
|
||||
}
|
||||
|
||||
COLLECTION {
|
||||
string id PK
|
||||
string name
|
||||
string description
|
||||
integer total_clips
|
||||
float total_duration
|
||||
string export_path
|
||||
json export_config
|
||||
json collection_metadata
|
||||
string project_id FK
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 数据定义语言
|
||||
|
||||
**项目表 (projects)**
|
||||
```sql
|
||||
-- 创建项目表
|
||||
CREATE TABLE projects (
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6)))),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed', 'cancelled')),
|
||||
project_type VARCHAR(50) DEFAULT 'default' CHECK (project_type IN ('default', 'knowledge', 'business', 'opinion', 'experience', 'speech', 'content_review', 'entertainment')),
|
||||
video_path VARCHAR(500),
|
||||
video_duration INTEGER,
|
||||
processing_config JSON,
|
||||
project_metadata JSON,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_projects_status ON projects(status);
|
||||
CREATE INDEX idx_projects_type ON projects(project_type);
|
||||
CREATE INDEX idx_projects_created_at ON projects(created_at DESC);
|
||||
```
|
||||
|
||||
**任务表 (tasks)**
|
||||
```sql
|
||||
-- 创建任务表
|
||||
CREATE TABLE tasks (
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6)))),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
|
||||
task_type VARCHAR(50) NOT NULL CHECK (task_type IN ('video_processing', 'clip_generation', 'collection_creation', 'export', 'cleanup')),
|
||||
progress REAL DEFAULT 0.0,
|
||||
current_step VARCHAR(100),
|
||||
total_steps INTEGER DEFAULT 1,
|
||||
priority INTEGER DEFAULT 0,
|
||||
started_at DATETIME,
|
||||
completed_at DATETIME,
|
||||
error_message TEXT,
|
||||
celery_task_id VARCHAR(255),
|
||||
task_config JSON,
|
||||
result_data JSON,
|
||||
task_metadata JSON,
|
||||
project_id VARCHAR(36) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_tasks_project_id ON tasks(project_id);
|
||||
CREATE INDEX idx_tasks_status ON tasks(status);
|
||||
CREATE INDEX idx_tasks_celery_task_id ON tasks(celery_task_id);
|
||||
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);
|
||||
```
|
||||
|
||||
**切片表 (clips)**
|
||||
```sql
|
||||
-- 创建切片表
|
||||
CREATE TABLE clips (
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6)))),
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
start_time REAL NOT NULL,
|
||||
end_time REAL NOT NULL,
|
||||
duration REAL NOT NULL,
|
||||
score REAL DEFAULT 0.0,
|
||||
video_path VARCHAR(500),
|
||||
thumbnail_path VARCHAR(500),
|
||||
clip_metadata JSON,
|
||||
project_id VARCHAR(36) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_clips_project_id ON clips(project_id);
|
||||
CREATE INDEX idx_clips_score ON clips(score DESC);
|
||||
CREATE INDEX idx_clips_duration ON clips(duration);
|
||||
```
|
||||
|
||||
**合集表 (collections)**
|
||||
```sql
|
||||
-- 创建合集表
|
||||
CREATE TABLE collections (
|
||||
id VARCHAR(36) PRIMARY KEY DEFAULT (lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))),2) || '-' || substr('89ab',abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6)))),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
total_clips INTEGER DEFAULT 0,
|
||||
total_duration REAL DEFAULT 0.0,
|
||||
export_path VARCHAR(500),
|
||||
export_config JSON,
|
||||
collection_metadata JSON,
|
||||
project_id VARCHAR(36) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_collections_project_id ON collections(project_id);
|
||||
CREATE INDEX idx_collections_created_at ON collections(created_at DESC);
|
||||
|
||||
-- 初始化数据
|
||||
INSERT INTO projects (name, description, project_type, status) VALUES
|
||||
('示例知识科普项目', '用于演示知识科普类视频处理流程', 'knowledge', 'pending'),
|
||||
('示例商业财经项目', '用于演示商业财经类视频处理流程', 'business', 'pending');
|
||||
```
|
||||
|
||||
## 7. 关键技术实现
|
||||
|
||||
### 7.1 Celery任务队列集成
|
||||
|
||||
**配置文件 (backend/core/celery_app.py)**
|
||||
```python
|
||||
from celery import Celery
|
||||
from backend.core.config import settings
|
||||
|
||||
celery_app = Celery(
|
||||
"autoclip",
|
||||
broker=settings.REDIS_URL,
|
||||
backend=settings.REDIS_URL,
|
||||
include=["backend.tasks.processing", "backend.tasks.video"]
|
||||
)
|
||||
|
||||
celery_app.conf.update(
|
||||
task_serializer="json",
|
||||
accept_content=["json"],
|
||||
result_serializer="json",
|
||||
timezone="UTC",
|
||||
enable_utc=True,
|
||||
task_track_started=True,
|
||||
task_time_limit=30 * 60, # 30分钟超时
|
||||
task_soft_time_limit=25 * 60, # 25分钟软超时
|
||||
worker_prefetch_multiplier=1,
|
||||
task_acks_late=True,
|
||||
worker_max_tasks_per_child=1000
|
||||
)
|
||||
```
|
||||
|
||||
### 7.2 WebSocket实时通信
|
||||
|
||||
**WebSocket管理器 (backend/core/websocket_manager.py)**
|
||||
```python
|
||||
from fastapi import WebSocket
|
||||
from typing import Dict, List
|
||||
import json
|
||||
|
||||
class WebSocketManager:
|
||||
def __init__(self):
|
||||
self.active_connections: Dict[str, List[WebSocket]] = {}
|
||||
|
||||
async def connect(self, websocket: WebSocket, project_id: str):
|
||||
await websocket.accept()
|
||||
if project_id not in self.active_connections:
|
||||
self.active_connections[project_id] = []
|
||||
self.active_connections[project_id].append(websocket)
|
||||
|
||||
async def disconnect(self, websocket: WebSocket, project_id: str):
|
||||
if project_id in self.active_connections:
|
||||
self.active_connections[project_id].remove(websocket)
|
||||
|
||||
async def send_progress_update(self, project_id: str, data: dict):
|
||||
if project_id in self.active_connections:
|
||||
message = json.dumps({"type": "progress_update", "data": data})
|
||||
for connection in self.active_connections[project_id]:
|
||||
try:
|
||||
await connection.send_text(message)
|
||||
except:
|
||||
await self.disconnect(connection, project_id)
|
||||
```
|
||||
|
||||
### 7.3 AI处理Pipeline集成
|
||||
|
||||
**Pipeline适配器 (backend/services/pipeline_adapter.py)**
|
||||
```python
|
||||
from shared.pipeline import step1_outline, step2_timeline, step3_scoring
|
||||
from backend.models import Task, Project
|
||||
from backend.core.websocket_manager import websocket_manager
|
||||
|
||||
class PipelineAdapter:
|
||||
def __init__(self):
|
||||
self.steps = {
|
||||
"step1_outline": step1_outline.OutlineExtractor,
|
||||
"step2_timeline": step2_timeline.TimelineExtractor,
|
||||
"step3_scoring": step3_scoring.ContentScorer,
|
||||
# ... 其他步骤
|
||||
}
|
||||
|
||||
async def execute_pipeline(self, project: Project, task: Task):
|
||||
"""执行完整的AI处理流程"""
|
||||
try:
|
||||
for i, (step_name, step_class) in enumerate(self.steps.items()):
|
||||
# 更新任务进度
|
||||
progress = (i / len(self.steps)) * 100
|
||||
await self.update_task_progress(task, progress, step_name)
|
||||
|
||||
# 执行处理步骤
|
||||
processor = step_class()
|
||||
result = await processor.process(project)
|
||||
|
||||
# 保存中间结果
|
||||
await self.save_step_result(project, step_name, result)
|
||||
|
||||
# 完成任务
|
||||
await self.complete_task(task)
|
||||
|
||||
except Exception as e:
|
||||
await self.fail_task(task, str(e))
|
||||
|
||||
async def update_task_progress(self, task: Task, progress: float, current_step: str):
|
||||
"""更新任务进度并发送WebSocket通知"""
|
||||
task.progress = progress
|
||||
task.current_step = current_step
|
||||
|
||||
# 发送WebSocket通知
|
||||
await websocket_manager.send_progress_update(task.project_id, {
|
||||
"task_id": task.id,
|
||||
"progress": progress,
|
||||
"current_step": current_step,
|
||||
"message": f"正在执行{current_step}..."
|
||||
})
|
||||
```
|
||||
Whitespace-only changes.
Vendored
BIN
Binary file not shown.
+184
-26
@@ -9,6 +9,9 @@ from sqlalchemy.orm import Session
|
||||
from core.database import get_db
|
||||
from services.project_service import ProjectService
|
||||
from services.processing_service import ProcessingService
|
||||
from services.websocket_notification_service import WebSocketNotificationService
|
||||
from tasks.processing import process_video_pipeline
|
||||
from core.websocket_manager import manager as websocket_manager
|
||||
from schemas.project import (
|
||||
ProjectCreate, ProjectUpdate, ProjectResponse, ProjectListResponse, ProjectFilter,
|
||||
ProjectType, ProjectStatus
|
||||
@@ -29,6 +32,11 @@ def get_processing_service(db: Session = Depends(get_db)) -> ProcessingService:
|
||||
return ProcessingService(db)
|
||||
|
||||
|
||||
def get_websocket_service(db: Session = Depends(get_db)) -> WebSocketNotificationService:
|
||||
"""Dependency to get websocket notification service."""
|
||||
return WebSocketNotificationService(websocket_manager)
|
||||
|
||||
|
||||
@router.post("/upload", response_model=ProjectResponse)
|
||||
async def upload_files(
|
||||
video_file: UploadFile = File(...),
|
||||
@@ -234,9 +242,10 @@ async def delete_project(
|
||||
async def start_processing(
|
||||
project_id: str,
|
||||
project_service: ProjectService = Depends(get_project_service),
|
||||
processing_service: ProcessingService = Depends(get_processing_service)
|
||||
processing_service: ProcessingService = Depends(get_processing_service),
|
||||
websocket_service: WebSocketNotificationService = Depends(get_websocket_service)
|
||||
):
|
||||
"""Start processing a project."""
|
||||
"""Start processing a project using Celery task queue."""
|
||||
try:
|
||||
# 获取项目信息
|
||||
project = project_service.get(project_id)
|
||||
@@ -244,42 +253,152 @@ async def start_processing(
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
# 检查项目状态
|
||||
if project.status.value != "pending":
|
||||
raise HTTPException(status_code=400, detail="Project is not in pending status")
|
||||
if project.status.value not in ["pending", "failed"]:
|
||||
raise HTTPException(status_code=400, detail="Project is not in pending or failed status")
|
||||
|
||||
# 获取SRT文件路径
|
||||
srt_path = None
|
||||
if project.processing_config and "srt_file" in project.processing_config:
|
||||
from pathlib import Path
|
||||
# 获取视频和SRT文件路径
|
||||
project_root = Path(__file__).parent.parent.parent / "data" / "projects" / project_id
|
||||
|
||||
video_path = None
|
||||
srt_path = None
|
||||
|
||||
if project.processing_config:
|
||||
if "video_file" in project.processing_config:
|
||||
video_path = project_root / "raw" / project.processing_config["video_file"]
|
||||
if "srt_file" in project.processing_config:
|
||||
srt_path = project_root / "raw" / project.processing_config["srt_file"]
|
||||
|
||||
# 验证文件存在
|
||||
if not video_path or not video_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"Video file not found: {video_path}")
|
||||
|
||||
if not srt_path or not srt_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"SRT file not found: {srt_path}")
|
||||
|
||||
# 调用处理服务
|
||||
result = processing_service.start_processing(project_id, srt_path)
|
||||
# 更新项目状态为处理中
|
||||
project_service.update_project_status(project_id, "processing")
|
||||
|
||||
# 发送WebSocket通知:处理开始
|
||||
await websocket_service.send_processing_started(
|
||||
project_id=int(project_id),
|
||||
message="开始视频处理流程"
|
||||
)
|
||||
|
||||
# 提交Celery任务
|
||||
celery_task = process_video_pipeline.delay(
|
||||
project_id=int(project_id),
|
||||
input_video_path=str(video_path),
|
||||
input_srt_path=str(srt_path)
|
||||
)
|
||||
|
||||
# 创建处理任务记录
|
||||
task_result = processing_service._create_processing_task(
|
||||
project_id=int(project_id),
|
||||
task_type="video_pipeline",
|
||||
celery_task_id=celery_task.id
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Processing started successfully",
|
||||
"project_id": project_id,
|
||||
"task_id": result.get("task_id"),
|
||||
"result": result
|
||||
"task_id": task_result.id,
|
||||
"celery_task_id": celery_task.id,
|
||||
"status": "processing"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
# 发送错误通知
|
||||
try:
|
||||
await websocket_service.send_processing_error(
|
||||
project_id=int(project_id),
|
||||
error=str(e),
|
||||
step="initialization"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{project_id}/retry")
|
||||
async def retry_processing(
|
||||
project_id: str,
|
||||
project_service: ProjectService = Depends(get_project_service)
|
||||
project_service: ProjectService = Depends(get_project_service),
|
||||
processing_service: ProcessingService = Depends(get_processing_service),
|
||||
websocket_service: WebSocketNotificationService = Depends(get_websocket_service)
|
||||
):
|
||||
"""Retry processing a project."""
|
||||
"""Retry processing a project from the beginning."""
|
||||
try:
|
||||
# 这里应该调用处理服务,暂时返回成功
|
||||
return {"message": "Processing retry started successfully"}
|
||||
# 获取项目信息
|
||||
project = project_service.get(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
# 检查项目状态
|
||||
if project.status.value not in ["failed", "completed"]:
|
||||
raise HTTPException(status_code=400, detail="Project is not in failed or completed status")
|
||||
|
||||
# 重置项目状态
|
||||
project_service.update_project_status(project_id, "pending")
|
||||
|
||||
# 发送WebSocket通知
|
||||
await websocket_service.send_processing_started(
|
||||
project_id=int(project_id),
|
||||
message="重新开始处理流程"
|
||||
)
|
||||
|
||||
# 获取文件路径并重新提交任务
|
||||
project_root = Path(__file__).parent.parent.parent / "data" / "projects" / project_id
|
||||
|
||||
video_path = None
|
||||
srt_path = None
|
||||
|
||||
if project.processing_config:
|
||||
if "video_file" in project.processing_config:
|
||||
video_path = project_root / "raw" / project.processing_config["video_file"]
|
||||
if "srt_file" in project.processing_config:
|
||||
srt_path = project_root / "raw" / project.processing_config["srt_file"]
|
||||
|
||||
if not video_path or not video_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"Video file not found: {video_path}")
|
||||
|
||||
if not srt_path or not srt_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"SRT file not found: {srt_path}")
|
||||
|
||||
# 提交Celery任务
|
||||
celery_task = process_video_pipeline.delay(
|
||||
project_id=int(project_id),
|
||||
input_video_path=str(video_path),
|
||||
input_srt_path=str(srt_path)
|
||||
)
|
||||
|
||||
# 创建新的处理任务记录
|
||||
task_result = processing_service._create_processing_task(
|
||||
project_id=int(project_id),
|
||||
task_type="video_pipeline_retry",
|
||||
celery_task_id=celery_task.id
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Processing retry started successfully",
|
||||
"project_id": project_id,
|
||||
"task_id": task_result.id,
|
||||
"celery_task_id": celery_task.id,
|
||||
"status": "processing"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
try:
|
||||
await websocket_service.send_processing_error(
|
||||
project_id=int(project_id),
|
||||
error=str(e),
|
||||
step="retry_initialization"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@@ -288,9 +407,10 @@ async def resume_processing(
|
||||
project_id: str,
|
||||
start_step: str = Form(..., description="Step to resume from (step1_outline, step2_timeline, etc.)"),
|
||||
project_service: ProjectService = Depends(get_project_service),
|
||||
processing_service: ProcessingService = Depends(get_processing_service)
|
||||
processing_service: ProcessingService = Depends(get_processing_service),
|
||||
websocket_service: WebSocketNotificationService = Depends(get_websocket_service)
|
||||
):
|
||||
"""Resume processing from a specific step."""
|
||||
"""Resume processing from a specific step using Celery task queue."""
|
||||
try:
|
||||
# 获取项目信息
|
||||
project = project_service.get(project_id)
|
||||
@@ -301,27 +421,65 @@ async def resume_processing(
|
||||
if project.status.value not in ["failed", "processing", "pending"]:
|
||||
raise HTTPException(status_code=400, detail="Project is not in failed, processing, or pending status")
|
||||
|
||||
# 获取SRT文件路径(如果需要)
|
||||
srt_path = None
|
||||
if start_step == "step1_outline":
|
||||
if project.processing_config and "srt_file" in project.processing_config:
|
||||
from pathlib import Path
|
||||
# 验证步骤名称
|
||||
valid_steps = ["step1_outline", "step2_timeline", "step3_scoring", "step4_filtering", "step5_clustering", "step6_video"]
|
||||
if start_step not in valid_steps:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid step: {start_step}. Valid steps: {valid_steps}")
|
||||
|
||||
# 获取文件路径
|
||||
project_root = Path(__file__).parent.parent.parent / "data" / "projects" / project_id
|
||||
|
||||
video_path = None
|
||||
srt_path = None
|
||||
|
||||
if project.processing_config:
|
||||
if "video_file" in project.processing_config:
|
||||
video_path = project_root / "raw" / project.processing_config["video_file"]
|
||||
if "srt_file" in project.processing_config:
|
||||
srt_path = project_root / "raw" / project.processing_config["srt_file"]
|
||||
|
||||
# 对于从第一步开始的情况,需要验证原始文件
|
||||
if start_step == "step1_outline":
|
||||
if not video_path or not video_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"Video file not found: {video_path}")
|
||||
if not srt_path or not srt_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"SRT file not found: {srt_path}")
|
||||
|
||||
# 调用处理服务恢复执行
|
||||
result = processing_service.resume_processing(project_id, start_step, srt_path)
|
||||
# 更新项目状态为处理中
|
||||
project_service.update_project_status(project_id, "processing")
|
||||
|
||||
# 发送WebSocket通知
|
||||
await websocket_service.send_processing_started(
|
||||
project_id=int(project_id),
|
||||
message=f"从步骤 {start_step} 恢复处理流程"
|
||||
)
|
||||
|
||||
# 使用处理服务恢复执行(这会在内部调用Pipeline适配器)
|
||||
result = processing_service.resume_processing(
|
||||
project_id,
|
||||
start_step,
|
||||
str(srt_path) if srt_path else None
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Processing resumed from {start_step} successfully",
|
||||
"project_id": project_id,
|
||||
"start_step": start_step,
|
||||
"result": result
|
||||
"task_id": result.get("task_id"),
|
||||
"status": "processing"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
try:
|
||||
await websocket_service.send_processing_error(
|
||||
project_id=int(project_id),
|
||||
error=str(e),
|
||||
step=f"resume_{start_step}"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
|
||||
+295
-238
@@ -1,302 +1,359 @@
|
||||
"""
|
||||
任务队列API
|
||||
提供任务提交、状态查询、取消等接口
|
||||
"""任务API路由
|
||||
处理Celery任务相关的操作
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from typing import List, Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any, List, Optional
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from celery.result import AsyncResult
|
||||
|
||||
from core.database import get_db
|
||||
from services.task_queue_service import TaskQueueService
|
||||
from schemas.task import TaskCreate, TaskResponse
|
||||
from schemas.base import SuccessResponse
|
||||
from services.processing_service import ProcessingService
|
||||
from services.websocket_notification_service import WebSocketNotificationService
|
||||
from core.websocket_manager import manager as websocket_manager
|
||||
from core.celery_app import celery_app
|
||||
from models.task import Task
|
||||
from schemas.task import TaskResponse, TaskListResponse, TaskStatus
|
||||
from schemas.base import PaginationParams
|
||||
|
||||
router = APIRouter(tags=["任务队列"])
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/submit/video-processing", response_model=SuccessResponse)
|
||||
async def submit_video_processing_task(
|
||||
project_id: str,
|
||||
srt_path: str,
|
||||
def get_processing_service(db: Session = Depends(get_db)) -> ProcessingService:
|
||||
"""Dependency to get processing service."""
|
||||
return ProcessingService(db)
|
||||
|
||||
|
||||
def get_websocket_service(db: Session = Depends(get_db)) -> WebSocketNotificationService:
|
||||
"""Dependency to get websocket notification service."""
|
||||
return WebSocketNotificationService(websocket_manager)
|
||||
|
||||
|
||||
@router.get("/", response_model=TaskListResponse)
|
||||
async def get_tasks(
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
size: int = Query(20, ge=1, le=100, description="Page size"),
|
||||
project_id: Optional[int] = Query(None, description="Filter by project ID"),
|
||||
status: Optional[str] = Query(None, description="Filter by task status"),
|
||||
task_type: Optional[str] = Query(None, description="Filter by task type"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
提交视频处理任务
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
srt_path: SRT文件路径
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务提交结果
|
||||
"""
|
||||
"""Get paginated tasks with optional filtering."""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.submit_video_processing_task(project_id, srt_path)
|
||||
query = db.query(Task)
|
||||
|
||||
return SuccessResponse(
|
||||
message="视频处理任务已提交",
|
||||
data=result
|
||||
# 应用过滤器
|
||||
if project_id:
|
||||
query = query.filter(Task.project_id == project_id)
|
||||
if status:
|
||||
query = query.filter(Task.status == status)
|
||||
if task_type:
|
||||
query = query.filter(Task.task_type == task_type)
|
||||
|
||||
# 分页
|
||||
total = query.count()
|
||||
offset = (page - 1) * size
|
||||
tasks = query.offset(offset).limit(size).all()
|
||||
|
||||
# 转换为响应格式
|
||||
task_responses = []
|
||||
for task in tasks:
|
||||
task_responses.append(TaskResponse(
|
||||
id=str(task.id),
|
||||
project_id=task.project_id,
|
||||
task_type=task.task_type,
|
||||
status=TaskStatus(task.status),
|
||||
celery_task_id=task.celery_task_id,
|
||||
current_step=task.current_step,
|
||||
total_steps=task.total_steps,
|
||||
progress=task.progress,
|
||||
error_message=task.error_message,
|
||||
result=task.result,
|
||||
created_at=task.created_at,
|
||||
updated_at=task.updated_at,
|
||||
completed_at=task.completed_at
|
||||
))
|
||||
|
||||
return TaskListResponse(
|
||||
items=task_responses,
|
||||
total=total,
|
||||
page=page,
|
||||
size=size,
|
||||
pages=(total + size - 1) // size
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"提交任务失败: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/submit/single-step", response_model=SuccessResponse)
|
||||
async def submit_single_step_task(
|
||||
project_id: str,
|
||||
step_name: str,
|
||||
srt_path: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
提交单个步骤处理任务
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
step_name: 步骤名称
|
||||
srt_path: SRT文件路径(仅Step1需要)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务提交结果
|
||||
"""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.submit_single_step_task(project_id, step_name, srt_path)
|
||||
|
||||
return SuccessResponse(
|
||||
message=f"步骤 {step_name} 处理任务已提交",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"提交任务失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/submit/retry", response_model=SuccessResponse)
|
||||
async def submit_retry_task(
|
||||
project_id: str,
|
||||
@router.get("/{task_id}", response_model=TaskResponse)
|
||||
async def get_task(
|
||||
task_id: str,
|
||||
step_name: str,
|
||||
srt_path: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
提交重试任务
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
task_id: 任务ID
|
||||
step_name: 步骤名称
|
||||
srt_path: SRT文件路径(仅Step1需要)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务提交结果
|
||||
"""
|
||||
"""Get a task by ID."""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.submit_retry_task(project_id, task_id, step_name, srt_path)
|
||||
task = db.query(Task).filter(Task.id == int(task_id)).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
return SuccessResponse(
|
||||
message=f"步骤 {step_name} 重试任务已提交",
|
||||
data=result
|
||||
return TaskResponse(
|
||||
id=str(task.id),
|
||||
project_id=task.project_id,
|
||||
task_type=task.task_type,
|
||||
status=TaskStatus(task.status),
|
||||
celery_task_id=task.celery_task_id,
|
||||
current_step=task.current_step,
|
||||
total_steps=task.total_steps,
|
||||
progress=task.progress,
|
||||
error_message=task.error_message,
|
||||
result=task.result,
|
||||
created_at=task.created_at,
|
||||
updated_at=task.updated_at,
|
||||
completed_at=task.completed_at
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"提交重试任务失败: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/status/{task_id}", response_model=SuccessResponse)
|
||||
@router.get("/{task_id}/status")
|
||||
async def get_task_status(
|
||||
task_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取任务状态
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务状态信息
|
||||
"""
|
||||
"""Get detailed task status including Celery task info."""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.get_task_status(task_id)
|
||||
task = db.query(Task).filter(Task.id == int(task_id)).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
if 'error' in result:
|
||||
raise HTTPException(status_code=404, detail=result['error'])
|
||||
# 获取Celery任务状态
|
||||
celery_result = None
|
||||
celery_status = "UNKNOWN"
|
||||
celery_info = None
|
||||
|
||||
return SuccessResponse(
|
||||
message="获取任务状态成功",
|
||||
data=result
|
||||
)
|
||||
if task.celery_task_id:
|
||||
try:
|
||||
celery_result = AsyncResult(task.celery_task_id, app=celery_app)
|
||||
celery_status = celery_result.status
|
||||
celery_info = celery_result.info
|
||||
except Exception as e:
|
||||
celery_info = {"error": f"Failed to get Celery task info: {str(e)}"}
|
||||
|
||||
return {
|
||||
"task_id": str(task.id),
|
||||
"project_id": task.project_id,
|
||||
"task_type": task.task_type,
|
||||
"status": task.status,
|
||||
"current_step": task.current_step,
|
||||
"total_steps": task.total_steps,
|
||||
"progress": task.progress,
|
||||
"error_message": task.error_message,
|
||||
"celery_task_id": task.celery_task_id,
|
||||
"celery_status": celery_status,
|
||||
"celery_info": celery_info,
|
||||
"created_at": task.created_at,
|
||||
"updated_at": task.updated_at,
|
||||
"completed_at": task.completed_at
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取任务状态失败: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/project/{project_id}", response_model=SuccessResponse)
|
||||
async def get_project_tasks(
|
||||
project_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
获取项目的所有任务
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务列表
|
||||
"""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
tasks = task_service.get_project_tasks(project_id)
|
||||
|
||||
return SuccessResponse(
|
||||
message="获取项目任务列表成功",
|
||||
data={
|
||||
'project_id': project_id,
|
||||
'tasks': tasks,
|
||||
'total': len(tasks)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取项目任务失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/cancel/{task_id}", response_model=SuccessResponse)
|
||||
@router.post("/{task_id}/cancel")
|
||||
async def cancel_task(
|
||||
task_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
websocket_service: WebSocketNotificationService = Depends(get_websocket_service)
|
||||
):
|
||||
"""
|
||||
取消任务
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
取消结果
|
||||
"""
|
||||
"""Cancel a running task."""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.cancel_task(task_id)
|
||||
task = db.query(Task).filter(Task.id == int(task_id)).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
if 'error' in result:
|
||||
raise HTTPException(status_code=404, detail=result['error'])
|
||||
# 检查任务状态
|
||||
if task.status in ["completed", "failed", "cancelled"]:
|
||||
raise HTTPException(status_code=400, detail=f"Cannot cancel task with status: {task.status}")
|
||||
|
||||
return SuccessResponse(
|
||||
message="任务已取消",
|
||||
data=result
|
||||
# 取消Celery任务
|
||||
if task.celery_task_id:
|
||||
try:
|
||||
celery_app.control.revoke(task.celery_task_id, terminate=True)
|
||||
except Exception as e:
|
||||
# 即使Celery取消失败,我们也要更新数据库状态
|
||||
pass
|
||||
|
||||
# 更新任务状态
|
||||
task.status = "cancelled"
|
||||
task.error_message = "Task cancelled by user"
|
||||
db.commit()
|
||||
|
||||
# 发送WebSocket通知
|
||||
await websocket_service.send_task_update(
|
||||
task_id=int(task_id),
|
||||
project_id=task.project_id,
|
||||
status="cancelled",
|
||||
progress=task.progress,
|
||||
message="任务已取消"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Task cancelled successfully",
|
||||
"task_id": task_id,
|
||||
"status": "cancelled"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"取消任务失败: {str(e)}")
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/submit/video-clips", response_model=SuccessResponse)
|
||||
async def submit_video_clips_task(
|
||||
project_id: str,
|
||||
clip_data: List[Dict[str, Any]],
|
||||
db: Session = Depends(get_db)
|
||||
@router.post("/{task_id}/retry")
|
||||
async def retry_task(
|
||||
task_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
processing_service: ProcessingService = Depends(get_processing_service),
|
||||
websocket_service: WebSocketNotificationService = Depends(get_websocket_service)
|
||||
):
|
||||
"""
|
||||
提交视频片段提取任务
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
clip_data: 片段数据列表
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务提交结果
|
||||
"""
|
||||
"""Retry a failed task."""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.submit_video_clips_task(project_id, clip_data)
|
||||
task = db.query(Task).filter(Task.id == int(task_id)).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
return SuccessResponse(
|
||||
message=f"视频片段提取任务已提交,共 {len(clip_data)} 个片段",
|
||||
data=result
|
||||
# 检查任务状态
|
||||
if task.status not in ["failed", "cancelled"]:
|
||||
raise HTTPException(status_code=400, detail=f"Cannot retry task with status: {task.status}")
|
||||
|
||||
# 重置任务状态
|
||||
task.status = "pending"
|
||||
task.current_step = 0
|
||||
task.progress = 0
|
||||
task.error_message = None
|
||||
task.celery_task_id = None
|
||||
db.commit()
|
||||
|
||||
# 发送WebSocket通知
|
||||
await websocket_service.send_task_update(
|
||||
task_id=int(task_id),
|
||||
project_id=task.project_id,
|
||||
status="pending",
|
||||
progress=0,
|
||||
message="任务重试中"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"提交视频片段提取任务失败: {str(e)}")
|
||||
# 根据任务类型重新提交任务
|
||||
if task.task_type == "video_pipeline":
|
||||
# 这里需要重新获取项目信息并提交Celery任务
|
||||
# 为了简化,我们返回成功,实际实现中需要调用相应的处理逻辑
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/submit/collections", response_model=SuccessResponse)
|
||||
async def submit_collection_generation_task(
|
||||
project_id: str,
|
||||
collection_data: List[Dict[str, Any]],
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
提交合集生成任务
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
collection_data: 合集数据列表
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
任务提交结果
|
||||
"""
|
||||
try:
|
||||
task_service = TaskQueueService(db)
|
||||
result = task_service.submit_collection_generation_task(project_id, collection_data)
|
||||
|
||||
return SuccessResponse(
|
||||
message=f"视频合集生成任务已提交,共 {len(collection_data)} 个合集",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"提交合集生成任务失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/queue/status", response_model=SuccessResponse)
|
||||
async def get_queue_status():
|
||||
"""
|
||||
获取队列状态
|
||||
|
||||
Returns:
|
||||
队列状态信息
|
||||
"""
|
||||
try:
|
||||
from core.celery_app import celery_app
|
||||
|
||||
# 获取活跃任务数
|
||||
active_tasks = celery_app.control.inspect().active()
|
||||
reserved_tasks = celery_app.control.inspect().reserved()
|
||||
|
||||
queue_status = {
|
||||
'active_tasks': len(active_tasks.get('celery@localhost', [])) if active_tasks else 0,
|
||||
'reserved_tasks': len(reserved_tasks.get('celery@localhost', [])) if reserved_tasks else 0,
|
||||
'workers': len(celery_app.control.inspect().stats() or {}),
|
||||
'timestamp': datetime.utcnow().isoformat()
|
||||
return {
|
||||
"message": "Task retry initiated successfully",
|
||||
"task_id": task_id,
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
return SuccessResponse(
|
||||
message="获取队列状态成功",
|
||||
data=queue_status
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/project/{project_id}", response_model=TaskListResponse)
|
||||
async def get_project_tasks(
|
||||
project_id: int,
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
size: int = Query(20, ge=1, le=100, description="Page size"),
|
||||
status: Optional[str] = Query(None, description="Filter by task status"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get tasks for a specific project."""
|
||||
try:
|
||||
query = db.query(Task).filter(Task.project_id == project_id)
|
||||
|
||||
if status:
|
||||
query = query.filter(Task.status == status)
|
||||
|
||||
# 按创建时间倒序排列
|
||||
query = query.order_by(Task.created_at.desc())
|
||||
|
||||
# 分页
|
||||
total = query.count()
|
||||
offset = (page - 1) * size
|
||||
tasks = query.offset(offset).limit(size).all()
|
||||
|
||||
# 转换为响应格式
|
||||
task_responses = []
|
||||
for task in tasks:
|
||||
task_responses.append(TaskResponse(
|
||||
id=str(task.id),
|
||||
project_id=task.project_id,
|
||||
task_type=task.task_type,
|
||||
status=TaskStatus(task.status),
|
||||
celery_task_id=task.celery_task_id,
|
||||
current_step=task.current_step,
|
||||
total_steps=task.total_steps,
|
||||
progress=task.progress,
|
||||
error_message=task.error_message,
|
||||
result=task.result,
|
||||
created_at=task.created_at,
|
||||
updated_at=task.updated_at,
|
||||
completed_at=task.completed_at
|
||||
))
|
||||
|
||||
return TaskListResponse(
|
||||
items=task_responses,
|
||||
total=total,
|
||||
page=page,
|
||||
size=size,
|
||||
pages=(total + size - 1) // size
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"获取队列状态失败: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/cleanup")
|
||||
async def cleanup_old_tasks(
|
||||
days: int = Query(7, ge=1, le=365, description="Delete tasks older than this many days"),
|
||||
status: Optional[str] = Query(None, description="Only delete tasks with this status"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Clean up old tasks."""
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
query = db.query(Task).filter(Task.created_at < cutoff_date)
|
||||
|
||||
if status:
|
||||
query = query.filter(Task.status == status)
|
||||
|
||||
# 获取要删除的任务数量
|
||||
count = query.count()
|
||||
|
||||
# 删除任务
|
||||
query.delete()
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Cleaned up {count} old tasks",
|
||||
"deleted_count": count,
|
||||
"cutoff_date": cutoff_date.isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
@@ -0,0 +1,151 @@
|
||||
"""WebSocket API路由
|
||||
处理WebSocket连接和实时通信
|
||||
"""
|
||||
|
||||
import logging
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException
|
||||
from typing import Optional
|
||||
|
||||
from core.websocket_manager import manager
|
||||
from core.database import get_db
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/ws", tags=["websocket"])
|
||||
|
||||
@router.websocket("/{user_id}")
|
||||
async def websocket_endpoint(websocket: WebSocket, user_id: str):
|
||||
"""WebSocket连接端点
|
||||
|
||||
Args:
|
||||
websocket: WebSocket连接对象
|
||||
user_id: 用户ID
|
||||
"""
|
||||
await manager.connect(websocket, user_id)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# 接收客户端消息
|
||||
data = await websocket.receive_json()
|
||||
|
||||
# 处理不同类型的消息
|
||||
message_type = data.get("type")
|
||||
|
||||
if message_type == "subscribe":
|
||||
# 订阅主题
|
||||
topic = data.get("topic")
|
||||
if topic:
|
||||
manager.subscribe_to_topic(user_id, topic)
|
||||
await websocket.send_json({
|
||||
"type": "subscription_confirmed",
|
||||
"topic": topic,
|
||||
"message": f"已订阅主题: {topic}"
|
||||
})
|
||||
|
||||
elif message_type == "unsubscribe":
|
||||
# 取消订阅主题
|
||||
topic = data.get("topic")
|
||||
if topic:
|
||||
manager.unsubscribe_from_topic(user_id, topic)
|
||||
await websocket.send_json({
|
||||
"type": "unsubscription_confirmed",
|
||||
"topic": topic,
|
||||
"message": f"已取消订阅主题: {topic}"
|
||||
})
|
||||
|
||||
elif message_type == "ping":
|
||||
# 心跳检测
|
||||
await websocket.send_json({
|
||||
"type": "pong",
|
||||
"timestamp": data.get("timestamp")
|
||||
})
|
||||
|
||||
elif message_type == "get_status":
|
||||
# 获取连接状态
|
||||
await websocket.send_json({
|
||||
"type": "status",
|
||||
"user_id": user_id,
|
||||
"connection_count": manager.get_connection_count(),
|
||||
"subscribed_topics": list(manager.user_subscriptions.get(user_id, set()))
|
||||
})
|
||||
|
||||
else:
|
||||
logger.warning(f"未知消息类型: {message_type}")
|
||||
await websocket.send_json({
|
||||
"type": "error",
|
||||
"message": f"未知消息类型: {message_type}"
|
||||
})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info(f"用户 {user_id} 断开WebSocket连接")
|
||||
manager.disconnect(user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket连接错误: {str(e)}")
|
||||
manager.disconnect(user_id)
|
||||
|
||||
@router.get("/status")
|
||||
async def get_websocket_status():
|
||||
"""获取WebSocket服务状态"""
|
||||
return {
|
||||
"active_connections": manager.get_connection_count(),
|
||||
"topics": {
|
||||
topic: manager.get_topic_subscriber_count(topic)
|
||||
for topic in manager.topic_subscribers.keys()
|
||||
}
|
||||
}
|
||||
|
||||
@router.post("/broadcast")
|
||||
async def broadcast_message(message: dict):
|
||||
"""广播消息到所有连接
|
||||
|
||||
Args:
|
||||
message: 要广播的消息
|
||||
"""
|
||||
try:
|
||||
await manager.broadcast(message)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "消息已广播",
|
||||
"recipients": manager.get_connection_count()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"广播消息失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"广播失败: {str(e)}")
|
||||
|
||||
@router.post("/broadcast/{topic}")
|
||||
async def broadcast_to_topic(topic: str, message: dict):
|
||||
"""广播消息到特定主题
|
||||
|
||||
Args:
|
||||
topic: 主题名称
|
||||
message: 要广播的消息
|
||||
"""
|
||||
try:
|
||||
await manager.broadcast_to_topic(message, topic)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"消息已广播到主题: {topic}",
|
||||
"recipients": manager.get_topic_subscriber_count(topic)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"广播消息到主题失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"广播失败: {str(e)}")
|
||||
|
||||
@router.post("/send/{user_id}")
|
||||
async def send_personal_message(user_id: str, message: dict):
|
||||
"""发送个人消息
|
||||
|
||||
Args:
|
||||
user_id: 目标用户ID
|
||||
message: 要发送的消息
|
||||
"""
|
||||
try:
|
||||
await manager.send_personal_message(message, user_id)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"消息已发送给用户: {user_id}"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"发送个人消息失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"发送失败: {str(e)}")
|
||||
+46
-32
@@ -20,7 +20,9 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 使用绝对导入
|
||||
from api.v1 import health, projects, clips, collections, tasks, settings
|
||||
from api.v1 import health, projects, clips, collections, tasks as task_routes, settings
|
||||
from api.v1.tasks import router as tasks_router
|
||||
from api.v1 import websocket
|
||||
from core.database import engine
|
||||
from models.base import Base
|
||||
|
||||
@@ -67,11 +69,9 @@ app.include_router(health.router, prefix="/api/v1/health", tags=["health"])
|
||||
app.include_router(projects.router, prefix="/api/v1/projects", tags=["projects"])
|
||||
app.include_router(clips.router, prefix="/api/v1/clips", tags=["clips"])
|
||||
app.include_router(collections.router, prefix="/api/v1/collections", tags=["collections"])
|
||||
app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"])
|
||||
app.include_router(task_routes.router, prefix="/api/v1/tasks", tags=["tasks"])
|
||||
app.include_router(tasks_router, prefix="/api/v1/tasks", tags=["tasks"])
|
||||
app.include_router(settings.router, prefix="/api/v1/settings", tags=["settings"])
|
||||
|
||||
# Include WebSocket routes
|
||||
from api.v1 import websocket
|
||||
app.include_router(websocket.router, prefix="/api/v1", tags=["websocket"])
|
||||
|
||||
# 添加独立的video-categories端点
|
||||
@@ -80,50 +80,64 @@ async def get_video_categories():
|
||||
"""获取视频分类配置."""
|
||||
return {
|
||||
"categories": [
|
||||
{
|
||||
"value": "default",
|
||||
"name": "默认",
|
||||
"description": "通用视频内容处理",
|
||||
"icon": "🎬",
|
||||
"color": "#4facfe"
|
||||
},
|
||||
{
|
||||
"value": "knowledge",
|
||||
"name": "知识科普",
|
||||
"description": "科学、技术、历史、文化等知识类内容",
|
||||
"icon": "book",
|
||||
"color": "#1890ff"
|
||||
},
|
||||
{
|
||||
"value": "entertainment",
|
||||
"name": "娱乐休闲",
|
||||
"description": "游戏、音乐、电影、综艺等娱乐内容",
|
||||
"icon": "play-circle",
|
||||
"icon": "📚",
|
||||
"color": "#52c41a"
|
||||
},
|
||||
{
|
||||
"value": "experience",
|
||||
"name": "生活经验",
|
||||
"description": "生活技巧、美食、旅行、手工等实用内容",
|
||||
"icon": "heart",
|
||||
"color": "#fa8c16"
|
||||
},
|
||||
{
|
||||
"value": "opinion",
|
||||
"name": "观点评论",
|
||||
"description": "时事评论、观点分享、社会话题等",
|
||||
"icon": "message",
|
||||
"color": "#722ed1"
|
||||
},
|
||||
{
|
||||
"value": "business",
|
||||
"name": "商业财经",
|
||||
"description": "商业分析、财经资讯、投资理财等",
|
||||
"icon": "dollar",
|
||||
"icon": "💼",
|
||||
"color": "#faad14"
|
||||
},
|
||||
{
|
||||
"value": "opinion",
|
||||
"name": "观点评论",
|
||||
"description": "时事评论、观点分享、社会话题等",
|
||||
"icon": "💭",
|
||||
"color": "#722ed1"
|
||||
},
|
||||
{
|
||||
"value": "experience",
|
||||
"name": "经验分享",
|
||||
"description": "生活技巧、经验分享、实用内容等",
|
||||
"icon": "🌟",
|
||||
"color": "#13c2c2"
|
||||
},
|
||||
{
|
||||
"value": "speech",
|
||||
"name": "演讲访谈",
|
||||
"description": "演讲、访谈、对话等口语化内容",
|
||||
"icon": "sound",
|
||||
"name": "演讲脱口秀",
|
||||
"description": "演讲、访谈、脱口秀等口语化内容",
|
||||
"icon": "🎤",
|
||||
"color": "#eb2f96"
|
||||
},
|
||||
{
|
||||
"value": "content_review",
|
||||
"name": "内容解说",
|
||||
"description": "影视解说、内容点评等",
|
||||
"icon": "🎭",
|
||||
"color": "#f5222d"
|
||||
},
|
||||
{
|
||||
"value": "entertainment",
|
||||
"name": "娱乐内容",
|
||||
"description": "游戏、音乐、娱乐等轻松内容",
|
||||
"icon": "🎪",
|
||||
"color": "#fa8c16"
|
||||
}
|
||||
],
|
||||
"default_category": "knowledge"
|
||||
"default_category": "default"
|
||||
}
|
||||
|
||||
# Root endpoint
|
||||
|
||||
Binary file not shown.
@@ -2107,3 +2107,23 @@
|
||||
2025-08-03 00:42:23,266 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-03 00:44:52,612 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-03 00:45:00,752 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-06 01:57:45,784 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-06 01:57:56,276 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-06 01:58:17,520 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-06 01:58:17,561 - fastapi - ERROR - Form data requires "python-multipart" to be installed.
|
||||
You can install "python-multipart" with:
|
||||
|
||||
pip install python-multipart
|
||||
|
||||
2025-08-06 01:58:34,095 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-06 01:58:34,168 - app.main - INFO - 启动AutoClip API服务...
|
||||
2025-08-06 01:58:34,171 - app.main - INFO - 数据库表创建完成
|
||||
2025-08-06 01:58:34,172 - app.main - INFO - API密钥已加载到环境变量
|
||||
2025-08-06 02:03:30,178 - app.main - ERROR - 全局异常处理: WebSocketNotificationService() takes no arguments
|
||||
2025-08-06 02:07:15,951 - services.processing_orchestrator - INFO - 流水线模块导入成功
|
||||
2025-08-06 02:07:16,022 - app.main - INFO - 启动AutoClip API服务...
|
||||
2025-08-06 02:07:16,023 - app.main - INFO - 数据库表创建完成
|
||||
2025-08-06 02:07:16,023 - app.main - INFO - API密钥已加载到环境变量
|
||||
2025-08-06 02:10:49,379 - app.main - INFO - 访问根端点
|
||||
2025-08-06 02:11:25,987 - app.main - ERROR - 全局异常处理: WebSocketNotificationService() takes no arguments
|
||||
2025-08-06 02:11:50,877 - app.main - ERROR - 全局异常处理: WebSocketNotificationService() takes no arguments
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from typing import Generator
|
||||
from backend.models.base import Base
|
||||
from models.base import Base
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL = os.getenv(
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
"""进度管理器
|
||||
统一处理任务进度更新和状态同步
|
||||
"""
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, Callable
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.task import Task
|
||||
from models.project import Project
|
||||
from services.websocket_notification_service import WebSocketNotificationService
|
||||
from core.websocket_manager import manager as websocket_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProgressManager:
|
||||
"""进度管理器,负责任务进度更新和状态同步"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.websocket_service = WebSocketNotificationService(websocket_manager)
|
||||
self._callbacks: Dict[str, Callable] = {}
|
||||
|
||||
def register_callback(self, callback_id: str, callback: Callable):
|
||||
"""注册进度回调函数
|
||||
|
||||
Args:
|
||||
callback_id: 回调ID
|
||||
callback: 回调函数
|
||||
"""
|
||||
self._callbacks[callback_id] = callback
|
||||
logger.debug(f"注册进度回调: {callback_id}")
|
||||
|
||||
def unregister_callback(self, callback_id: str):
|
||||
"""注销进度回调函数
|
||||
|
||||
Args:
|
||||
callback_id: 回调ID
|
||||
"""
|
||||
if callback_id in self._callbacks:
|
||||
del self._callbacks[callback_id]
|
||||
logger.debug(f"注销进度回调: {callback_id}")
|
||||
|
||||
async def update_task_progress(
|
||||
self,
|
||||
task_id: int,
|
||||
current_step: int,
|
||||
total_steps: int,
|
||||
step_name: str,
|
||||
progress: float,
|
||||
message: Optional[str] = None,
|
||||
step_result: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""更新任务进度
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
current_step: 当前步骤
|
||||
total_steps: 总步骤数
|
||||
step_name: 步骤名称
|
||||
progress: 进度百分比 (0-100)
|
||||
message: 进度消息
|
||||
step_result: 步骤结果数据
|
||||
"""
|
||||
try:
|
||||
# 更新数据库中的任务状态
|
||||
task = self.db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
logger.error(f"任务 {task_id} 不存在")
|
||||
return
|
||||
|
||||
# 更新任务字段
|
||||
task.current_step = current_step
|
||||
task.total_steps = total_steps
|
||||
task.progress = progress
|
||||
task.updated_at = datetime.utcnow()
|
||||
|
||||
# 如果有步骤结果,更新到任务结果中
|
||||
if step_result:
|
||||
if not task.result:
|
||||
task.result = {}
|
||||
task.result[f"step_{current_step}"] = step_result
|
||||
|
||||
# 如果进度达到100%,标记为完成
|
||||
if progress >= 100:
|
||||
task.status = "completed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
|
||||
# 同时更新项目状态
|
||||
project = self.db.query(Project).filter(Project.id == task.project_id).first()
|
||||
if project:
|
||||
project.status = "completed"
|
||||
project.completed_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# 发送WebSocket通知
|
||||
await self.websocket_service.send_processing_progress(
|
||||
project_id=task.project_id,
|
||||
current_step=current_step,
|
||||
total_steps=total_steps,
|
||||
step_name=step_name,
|
||||
progress=progress,
|
||||
message=message or f"正在执行步骤 {current_step}/{total_steps}: {step_name}"
|
||||
)
|
||||
|
||||
# 发送任务更新通知
|
||||
await self.websocket_service.send_task_update(
|
||||
task_id=task_id,
|
||||
project_id=task.project_id,
|
||||
status=task.status,
|
||||
progress=progress,
|
||||
message=message or f"步骤 {current_step}: {step_name}"
|
||||
)
|
||||
|
||||
# 调用注册的回调函数
|
||||
for callback_id, callback in self._callbacks.items():
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(callback):
|
||||
await callback({
|
||||
'task_id': task_id,
|
||||
'project_id': task.project_id,
|
||||
'current_step': current_step,
|
||||
'total_steps': total_steps,
|
||||
'step_name': step_name,
|
||||
'progress': progress,
|
||||
'message': message,
|
||||
'step_result': step_result
|
||||
})
|
||||
else:
|
||||
callback({
|
||||
'task_id': task_id,
|
||||
'project_id': task.project_id,
|
||||
'current_step': current_step,
|
||||
'total_steps': total_steps,
|
||||
'step_name': step_name,
|
||||
'progress': progress,
|
||||
'message': message,
|
||||
'step_result': step_result
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"执行进度回调 {callback_id} 失败: {str(e)}")
|
||||
|
||||
logger.info(f"任务 {task_id} 进度更新: {current_step}/{total_steps} ({progress}%) - {step_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新任务进度失败: {str(e)}")
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
async def update_task_error(
|
||||
self,
|
||||
task_id: int,
|
||||
error_message: str,
|
||||
current_step: Optional[int] = None,
|
||||
step_name: Optional[str] = None
|
||||
):
|
||||
"""更新任务错误状态
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
error_message: 错误消息
|
||||
current_step: 出错的步骤
|
||||
step_name: 步骤名称
|
||||
"""
|
||||
try:
|
||||
# 更新数据库中的任务状态
|
||||
task = self.db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
logger.error(f"任务 {task_id} 不存在")
|
||||
return
|
||||
|
||||
# 更新任务状态为失败
|
||||
task.status = "failed"
|
||||
task.error_message = error_message
|
||||
task.updated_at = datetime.utcnow()
|
||||
|
||||
if current_step is not None:
|
||||
task.current_step = current_step
|
||||
|
||||
# 同时更新项目状态
|
||||
project = self.db.query(Project).filter(Project.id == task.project_id).first()
|
||||
if project:
|
||||
project.status = "failed"
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# 发送WebSocket错误通知
|
||||
await self.websocket_service.send_processing_error(
|
||||
project_id=task.project_id,
|
||||
error=error_message,
|
||||
step=step_name or f"step_{current_step}" if current_step else "unknown"
|
||||
)
|
||||
|
||||
# 发送任务更新通知
|
||||
await self.websocket_service.send_task_update(
|
||||
task_id=task_id,
|
||||
project_id=task.project_id,
|
||||
status="failed",
|
||||
progress=task.progress,
|
||||
message=f"任务失败: {error_message}"
|
||||
)
|
||||
|
||||
logger.error(f"任务 {task_id} 执行失败: {error_message}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新任务错误状态失败: {str(e)}")
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
async def update_project_status(
|
||||
self,
|
||||
project_id: int,
|
||||
status: str,
|
||||
message: Optional[str] = None
|
||||
):
|
||||
"""更新项目状态
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
status: 项目状态
|
||||
message: 状态消息
|
||||
"""
|
||||
try:
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
logger.error(f"项目 {project_id} 不存在")
|
||||
return
|
||||
|
||||
old_status = project.status
|
||||
project.status = status
|
||||
project.updated_at = datetime.utcnow()
|
||||
|
||||
if status == "completed":
|
||||
project.completed_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# 发送WebSocket项目更新通知
|
||||
await self.websocket_service.send_project_update(
|
||||
project_id=project_id,
|
||||
status=status,
|
||||
message=message or f"项目状态从 {old_status} 更新为 {status}"
|
||||
)
|
||||
|
||||
logger.info(f"项目 {project_id} 状态更新: {old_status} -> {status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新项目状态失败: {str(e)}")
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
def create_progress_callback(self, task_id: int) -> Callable:
|
||||
"""创建进度回调函数
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
进度回调函数
|
||||
"""
|
||||
async def progress_callback(
|
||||
current_step: int,
|
||||
total_steps: int,
|
||||
step_name: str,
|
||||
progress: float,
|
||||
message: Optional[str] = None,
|
||||
step_result: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
await self.update_task_progress(
|
||||
task_id=task_id,
|
||||
current_step=current_step,
|
||||
total_steps=total_steps,
|
||||
step_name=step_name,
|
||||
progress=progress,
|
||||
message=message,
|
||||
step_result=step_result
|
||||
)
|
||||
|
||||
return progress_callback
|
||||
|
||||
def create_error_callback(self, task_id: int) -> Callable:
|
||||
"""创建错误回调函数
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
|
||||
Returns:
|
||||
错误回调函数
|
||||
"""
|
||||
async def error_callback(
|
||||
error_message: str,
|
||||
current_step: Optional[int] = None,
|
||||
step_name: Optional[str] = None
|
||||
):
|
||||
await self.update_task_error(
|
||||
task_id=task_id,
|
||||
error_message=error_message,
|
||||
current_step=current_step,
|
||||
step_name=step_name
|
||||
)
|
||||
|
||||
return error_callback
|
||||
|
||||
async def sync_task_status(self, task_id: int):
|
||||
"""同步任务状态
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
"""
|
||||
try:
|
||||
task = self.db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
logger.error(f"任务 {task_id} 不存在")
|
||||
return
|
||||
|
||||
# 检查Celery任务状态
|
||||
if task.celery_task_id:
|
||||
from celery.result import AsyncResult
|
||||
from core.celery_app import celery_app
|
||||
|
||||
try:
|
||||
celery_result = AsyncResult(task.celery_task_id, app=celery_app)
|
||||
celery_status = celery_result.status
|
||||
|
||||
# 根据Celery状态更新任务状态
|
||||
if celery_status == "SUCCESS" and task.status != "completed":
|
||||
task.status = "completed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
task.progress = 100
|
||||
elif celery_status == "FAILURE" and task.status != "failed":
|
||||
task.status = "failed"
|
||||
task.error_message = str(celery_result.info) if celery_result.info else "Unknown error"
|
||||
elif celery_status == "REVOKED" and task.status != "cancelled":
|
||||
task.status = "cancelled"
|
||||
task.error_message = "Task was cancelled"
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# 发送状态同步通知
|
||||
await self.websocket_service.send_task_update(
|
||||
task_id=task_id,
|
||||
project_id=task.project_id,
|
||||
status=task.status,
|
||||
progress=task.progress,
|
||||
message=f"任务状态已同步: {task.status}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查Celery任务状态失败: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步任务状态失败: {str(e)}")
|
||||
self.db.rollback()
|
||||
|
||||
|
||||
# 全局进度管理器实例
|
||||
_progress_managers: Dict[str, ProgressManager] = {}
|
||||
|
||||
|
||||
def get_progress_manager(db: Session) -> ProgressManager:
|
||||
"""获取进度管理器实例
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
进度管理器实例
|
||||
"""
|
||||
# 使用数据库会话的ID作为键,确保每个会话有独立的管理器
|
||||
session_id = str(id(db))
|
||||
|
||||
if session_id not in _progress_managers:
|
||||
_progress_managers[session_id] = ProgressManager(db)
|
||||
|
||||
return _progress_managers[session_id]
|
||||
|
||||
|
||||
def cleanup_progress_managers():
|
||||
"""清理不再使用的进度管理器"""
|
||||
global _progress_managers
|
||||
_progress_managers.clear()
|
||||
logger.info("已清理所有进度管理器实例")
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
1
|
||||
00:00:00,210 --> 00:00:01,932
|
||||
你们都是什么区块链
|
||||
|
||||
2
|
||||
00:00:03,200 --> 00:00:04,500
|
||||
我说一句话
|
||||
|
||||
3
|
||||
00:00:04,500 --> 00:00:07,888
|
||||
比你外面你自己或你自己瞎操作十年二十年都有用
|
||||
|
||||
4
|
||||
00:00:08,780 --> 00:00:09,548
|
||||
但是你听不懂
|
||||
|
||||
5
|
||||
00:00:10,530 --> 00:00:11,280
|
||||
配三个固态
|
||||
|
||||
6
|
||||
00:00:11,280 --> 00:00:12,320
|
||||
那哥们儿那你出来
|
||||
|
||||
7
|
||||
00:00:12,500 --> 00:00:13,508
|
||||
这不是你他妈又割了
|
||||
|
||||
8
|
||||
00:00:14,050 --> 00:00:15,818
|
||||
割了说因为Z哥说要控制风险
|
||||
|
||||
9
|
||||
00:00:15,830 --> 00:00:16,565
|
||||
所以我他妈割了
|
||||
|
||||
10
|
||||
00:00:16,680 --> 00:00:18,090
|
||||
所以我说傻逼叫你炒股
|
||||
|
||||
11
|
||||
00:00:18,090 --> 00:00:19,034
|
||||
你赚钱跟我没关系
|
||||
|
||||
12
|
||||
00:00:19,040 --> 00:00:20,480
|
||||
你他妈亏钱就是他让我买的
|
||||
|
||||
13
|
||||
00:00:20,700 --> 00:00:21,663
|
||||
我他妈从来没让你买
|
||||
|
||||
14
|
||||
00:00:21,920 --> 00:00:22,390
|
||||
对吗
|
||||
|
||||
15
|
||||
00:00:22,870 --> 00:00:23,686
|
||||
配置还是配置
|
||||
|
||||
16
|
||||
00:00:23,770 --> 00:00:26,058
|
||||
你他妈一个仓配配了三个固态
|
||||
|
||||
17
|
||||
00:00:26,060 --> 00:00:27,360
|
||||
你那赖谁
|
||||
|
||||
18
|
||||
00:00:28,610 --> 00:00:29,780
|
||||
所以这钱你认知不够
|
||||
|
||||
19
|
||||
00:00:29,780 --> 00:00:30,650
|
||||
你就赚不了
|
||||
|
||||
20
|
||||
00:00:30,970 --> 00:00:31,579
|
||||
很正常
|
||||
|
||||
21
|
||||
00:00:31,580 --> 00:00:32,300
|
||||
你就赚不了
|
||||
|
||||
22
|
||||
00:00:34,760 --> 00:00:35,390
|
||||
配三个固态
|
||||
|
||||
23
|
||||
00:00:35,390 --> 00:00:37,040
|
||||
哥们儿前天还发视频总结自己成绩
|
||||
|
||||
24
|
||||
00:00:37,160 --> 00:00:37,680
|
||||
是吗
|
||||
|
||||
25
|
||||
00:00:37,680 --> 00:00:38,728
|
||||
他觉得他行了是吗
|
||||
|
||||
26
|
||||
00:00:39,110 --> 00:00:39,990
|
||||
他不骂我了
|
||||
|
||||
27
|
||||
00:00:40,240 --> 00:00:41,640
|
||||
他他他他不卖门
|
||||
|
||||
28
|
||||
00:00:41,640 --> 00:00:43,134
|
||||
他妈的诋毁我了是吗
|
||||
|
||||
29
|
||||
00:00:44,230 --> 00:00:45,210
|
||||
人性人性
|
||||
|
||||
30
|
||||
00:00:45,350 --> 00:00:47,950
|
||||
我操这人人性人性
|
||||
|
||||
31
|
||||
00:00:48,080 --> 00:00:50,790
|
||||
我操没办法
|
||||
|
||||
32
|
||||
00:00:50,790 --> 00:00:54,320
|
||||
这他妈人性是不是人性
|
||||
|
||||
33
|
||||
00:00:54,940 --> 00:00:58,056
|
||||
我周三我说我说没准你下礼拜就ZGNB了
|
||||
|
||||
34
|
||||
00:00:58,380 --> 00:00:58,880
|
||||
对吗
|
||||
|
||||
35
|
||||
00:01:01,000 --> 00:01:03,272
|
||||
所以我说这个市场永远是有经验人赚
|
||||
|
||||
36
|
||||
00:01:03,280 --> 00:01:05,247
|
||||
没经验钱的地方
|
||||
|
||||
37
|
||||
00:01:06,390 --> 00:01:07,720
|
||||
就是说什么意思
|
||||
|
||||
38
|
||||
00:01:07,720 --> 00:01:08,746
|
||||
我不是吹牛逼
|
||||
|
||||
39
|
||||
00:01:08,750 --> 00:01:11,846
|
||||
咱上礼拜三我给你们举的那五个还是四个
|
||||
|
||||
40
|
||||
00:01:11,860 --> 00:01:13,520
|
||||
四个还是五个例子来着
|
||||
|
||||
41
|
||||
00:01:13,690 --> 00:01:17,630
|
||||
我忘了就那那哥们儿那个短线到后面周四周五接着涨
|
||||
|
||||
42
|
||||
00:01:18,430 --> 00:01:21,334
|
||||
康康又登了10% 10几
|
||||
|
||||
43
|
||||
00:01:21,570 --> 00:01:24,882
|
||||
一根涨停加一个6%还是7
|
||||
|
||||
44
|
||||
00:01:25,140 --> 00:01:25,690
|
||||
是吧
|
||||
|
||||
45
|
||||
00:01:27,390 --> 00:01:30,030
|
||||
某康俩涨停
|
||||
|
||||
46
|
||||
00:01:31,160 --> 00:01:37,348
|
||||
然后那个国某矿直接把周五登进来是吧
|
||||
|
||||
47
|
||||
00:01:37,350 --> 00:01:38,294
|
||||
然后还有什么来着
|
||||
|
||||
48
|
||||
00:01:38,530 --> 00:01:39,610
|
||||
我忘了记不清了
|
||||
|
||||
49
|
||||
00:01:40,570 --> 00:01:43,658
|
||||
然后那陈他没买买进去的又一根长阳
|
||||
|
||||
50
|
||||
00:01:43,670 --> 00:01:44,230
|
||||
对吧
|
||||
|
||||
51
|
||||
00:01:44,890 --> 00:01:45,940
|
||||
这他妈就是水平
|
||||
|
||||
52
|
||||
00:01:46,080 --> 00:01:47,438
|
||||
你别你还别不服
|
||||
|
||||
53
|
||||
00:01:47,440 --> 00:01:53,936
|
||||
我操就有那那你那黑子说听周三的跟大直播没还没比大直播要无聊
|
||||
|
||||
54
|
||||
00:01:53,950 --> 00:01:58,010
|
||||
他说那所以可能他妈是可能玩鞋或者喝喝酒的
|
||||
|
||||
55
|
||||
00:01:58,010 --> 00:01:58,370
|
||||
是Line truncated
|
||||
|
||||
56
|
||||
00:01:58,550 --> 00:01:59,378
|
||||
就是你听不懂
|
||||
|
||||
57
|
||||
00:02:00,230 --> 00:02:00,760
|
||||
所以听不懂
|
||||
|
||||
58
|
||||
00:02:00,760 --> 00:02:01,388
|
||||
认知不够
|
||||
|
||||
59
|
||||
00:02:01,390 --> 00:02:02,086
|
||||
那么就没办法
|
||||
|
||||
60
|
||||
00:02:02,090 --> 00:02:03,962
|
||||
无知者无畏才会说出这种话
|
||||
|
||||
61
|
||||
00:02:05,360 --> 00:02:07,592
|
||||
踏踏实实看两个70%到是吧
|
||||
|
||||
62
|
||||
00:02:07,600 --> 00:02:09,283
|
||||
你这他妈实打实的吗
|
||||
|
||||
63
|
||||
00:02:09,290 --> 00:02:10,958
|
||||
傻逼续费对吗
|
||||
|
||||
64
|
||||
00:02:11,440 --> 00:02:14,116
|
||||
说如果说这哥们儿包括续费
|
||||
|
||||
65
|
||||
00:02:14,120 --> 00:02:17,054
|
||||
现在周三两万多同屏
|
||||
|
||||
66
|
||||
00:02:18,040 --> 00:02:19,370
|
||||
我诚惶诚恐
|
||||
|
||||
67
|
||||
00:02:19,660 --> 00:02:20,050
|
||||
真的
|
||||
|
||||
68
|
||||
00:02:20,050 --> 00:02:22,294
|
||||
咱家当时3000到5000特别自由
|
||||
|
||||
69
|
||||
00:02:23,810 --> 00:02:25,079
|
||||
现在充电视频两万多
|
||||
|
||||
70
|
||||
00:02:25,080 --> 00:02:25,905
|
||||
我诚惶诚恐
|
||||
|
||||
71
|
||||
00:02:25,910 --> 00:02:27,080
|
||||
说实话真的
|
||||
|
||||
72
|
||||
00:02:30,080 --> 00:02:32,852
|
||||
所以我还是这块儿我想告诉大家
|
||||
|
||||
73
|
||||
00:02:33,430 --> 00:02:33,758
|
||||
什么意思
|
||||
|
||||
74
|
||||
00:02:33,760 --> 00:02:39,280
|
||||
就是说咱们一保护咱们直播间不容易
|
||||
|
||||
75
|
||||
00:02:39,590 --> 00:02:42,837
|
||||
在B站能够有这样一个非常开心的地方
|
||||
|
||||
76
|
||||
00:02:42,850 --> 00:02:43,898
|
||||
我觉得非常不容易
|
||||
|
||||
77
|
||||
00:02:44,200 --> 00:02:49,348
|
||||
另外第二就是说也请你们珍惜
|
||||
|
||||
78
|
||||
00:02:49,360 --> 00:02:55,022
|
||||
我还愿意说真的虽然那几个杂毛不超过五个
|
||||
|
||||
79
|
||||
00:02:55,430 --> 00:02:58,269
|
||||
咱家他妈每每礼拜大大直播的十几万人
|
||||
|
||||
80
|
||||
00:02:58,870 --> 00:02:59,927
|
||||
咱们不超过五个
|
||||
|
||||
81
|
||||
00:03:01,490 --> 00:03:06,950
|
||||
但是人性我经历过人性你能明白吗
|
||||
|
||||
82
|
||||
00:03:07,510 --> 00:03:11,386
|
||||
就比如说咱们上礼拜大直播
|
||||
|
||||
83
|
||||
00:03:12,500 --> 00:03:14,580
|
||||
我说反内卷
|
||||
|
||||
84
|
||||
00:03:14,720 --> 00:03:17,814
|
||||
我专门讲了一晚上反内卷对吗
|
||||
|
||||
85
|
||||
00:03:19,620 --> 00:03:20,868
|
||||
周五是不是反内卷
|
||||
|
||||
86
|
||||
00:03:22,800 --> 00:03:25,124
|
||||
然后我之前说我上周说的是反对
|
||||
|
||||
87
|
||||
00:03:25,130 --> 00:03:25,778
|
||||
眼都瞪起来了
|
||||
|
||||
88
|
||||
00:03:25,780 --> 00:03:26,770
|
||||
别着急我等回来
|
||||
|
||||
89
|
||||
00:03:26,770 --> 00:03:27,340
|
||||
等下来
|
||||
|
||||
90
|
||||
00:03:27,720 --> 00:03:28,300
|
||||
等他下来
|
||||
|
||||
91
|
||||
00:03:28,300 --> 00:03:28,789
|
||||
等下来
|
||||
|
||||
92
|
||||
00:03:29,410 --> 00:03:30,280
|
||||
周五是什么
|
||||
|
||||
93
|
||||
00:03:30,400 --> 00:03:32,688
|
||||
煤炭对吧
|
||||
|
||||
94
|
||||
00:03:33,100 --> 00:03:33,410
|
||||
稀土
|
||||
|
||||
95
|
||||
00:03:36,600 --> 00:03:39,336
|
||||
然后水利就乱七八糟
|
||||
|
||||
96
|
||||
00:03:39,340 --> 00:03:41,836
|
||||
这些你们平时就咱们看不到
|
||||
|
||||
97
|
||||
00:03:41,840 --> 00:03:43,465
|
||||
我说我跟他讲的都是机构思维
|
||||
|
||||
98
|
||||
00:03:44,470 --> 00:03:46,630
|
||||
机构思维不是你那种抱着说
|
||||
|
||||
99
|
||||
00:03:47,060 --> 00:03:48,660
|
||||
说完以后马上就涨起来
|
||||
|
||||
100
|
||||
00:03:48,820 --> 00:03:49,288
|
||||
不一定
|
||||
|
||||
101
|
||||
00:03:50,530 --> 00:03:54,273
|
||||
但是概率足够大比你下芝麻轮那些要大很多
|
||||
|
||||
102
|
||||
00:03:55,310 --> 00:03:56,949
|
||||
就是我微信打开我那些群
|
||||
|
||||
103
|
||||
00:03:57,850 --> 00:03:59,390
|
||||
都是什么类的群
|
||||
|
||||
104
|
||||
00:03:59,670 --> 00:04:01,778
|
||||
跟你打开那些他妈小姐姐小妹妹的裙子
|
||||
|
||||
105
|
||||
00:04:01,790 --> 00:04:02,665
|
||||
他妈什么不一样
|
||||
|
||||
106
|
||||
00:04:02,670 --> 00:04:06,058
|
||||
哥们踏踏实实就是很简单
|
||||
|
||||
107
|
||||
00:04:06,060 --> 00:04:08,418
|
||||
你行你上对吗
|
||||
|
||||
108
|
||||
00:04:08,610 --> 00:04:10,338
|
||||
你你你行你上
|
||||
|
||||
109
|
||||
00:04:11,580 --> 00:04:14,375
|
||||
包括下礼拜上周反内卷没涨完
|
||||
|
||||
110
|
||||
00:04:14,670 --> 00:04:16,713
|
||||
没没没休息一礼拜了
|
||||
|
||||
111
|
||||
00:04:16,920 --> 00:04:17,508
|
||||
接着轮
|
||||
|
||||
112
|
||||
00:04:18,230 --> 00:04:19,693
|
||||
就是他妈的没准接着作业
|
||||
|
||||
113
|
||||
00:04:19,700 --> 00:04:22,534
|
||||
接着我就那些还没有轮动到的
|
||||
|
||||
114
|
||||
00:04:22,540 --> 00:04:25,580
|
||||
别着急踏踏实实那踏踏实实
|
||||
|
||||
115
|
||||
00:04:35,320 --> 00:04:38,656
|
||||
然后还有我我我我想到哪儿
|
||||
|
||||
116
|
||||
00:04:38,660 --> 00:04:39,050
|
||||
我说到哪儿
|
||||
|
||||
117
|
||||
00:04:39,170 --> 00:04:41,686
|
||||
因为咱们直播主要就是一个蛋逼直播间
|
||||
|
||||
118
|
||||
00:04:43,750 --> 00:04:48,235
|
||||
有有一些朋友大概我看了有七八个
|
||||
|
||||
119
|
||||
00:04:48,590 --> 00:04:49,910
|
||||
后台给我截图
|
||||
|
||||
120
|
||||
00:04:50,280 --> 00:04:54,620
|
||||
其中还有一个还还有一些是跟的时间比较长的
|
||||
|
||||
121
|
||||
00:04:55,260 --> 00:04:59,385
|
||||
我不知道周三录播你们都看没看啊
|
||||
|
||||
122
|
||||
00:04:59,790 --> 00:05:06,612
|
||||
就是这个我后来后面专门给你们讲的案例
|
||||
|
||||
123
|
||||
00:05:06,780 --> 00:05:09,104
|
||||
我讲第一选出来
|
||||
|
||||
124
|
||||
00:05:09,110 --> 00:05:10,250
|
||||
当天选完之后
|
||||
|
||||
125
|
||||
00:05:10,250 --> 00:05:13,488
|
||||
第二天比如那天比例选了217个
|
||||
|
||||
126
|
||||
00:05:14,050 --> 00:05:16,258
|
||||
第一第二天涨停的只有一个
|
||||
|
||||
127
|
||||
00:05:16,580 --> 00:05:18,637
|
||||
相当于是2百17分之1
|
||||
|
||||
128
|
||||
00:05:19,130 --> 00:05:22,570
|
||||
但是我给你们往上翻三个交易日前的
|
||||
|
||||
129
|
||||
00:05:23,950 --> 00:05:25,358
|
||||
我怀孕了什么意思
|
||||
|
||||
130
|
||||
00:05:26,120 --> 00:05:27,896
|
||||
哥们儿你怀孕了跟我说干嘛
|
||||
|
||||
131
|
||||
00:05:28,310 --> 00:05:29,798
|
||||
他老师该该找谁去
|
||||
|
||||
132
|
||||
00:05:31,860 --> 00:05:32,540
|
||||
说哪儿了
|
||||
|
||||
133
|
||||
00:05:32,540 --> 00:05:35,428
|
||||
对你往三天前一番
|
||||
|
||||
134
|
||||
00:05:36,000 --> 00:05:41,814
|
||||
然后周三的时候一看是单3到5个涨停
|
||||
|
||||
135
|
||||
00:05:42,190 --> 00:05:42,568
|
||||
是不是
|
||||
|
||||
136
|
||||
00:05:42,880 --> 00:05:45,270
|
||||
3到5个涨停什么意思
|
||||
|
||||
137
|
||||
00:05:45,940 --> 00:05:46,930
|
||||
再强调一遍
|
||||
|
||||
138
|
||||
00:05:48,130 --> 00:05:52,882
|
||||
第一叫潜伏好不好
|
||||
|
||||
139
|
||||
00:05:53,930 --> 00:05:57,787
|
||||
B一潜伏不是说B一一进去马上坑就掉下来
|
||||
|
||||
140
|
||||
00:05:58,530 --> 00:06:01,346
|
||||
那概率就是200分之1
|
||||
|
||||
141
|
||||
00:06:03,020 --> 00:06:04,571
|
||||
还是在市场不暴跌的时候
|
||||
|
||||
142
|
||||
00:06:05,100 --> 00:06:08,976
|
||||
B一进去绝大部分时间你要等2到3天
|
||||
|
||||
143
|
||||
00:06:10,060 --> 00:06:12,293
|
||||
甚至个别板块等3到5天
|
||||
|
||||
144
|
||||
00:06:14,130 --> 00:06:15,618
|
||||
这句话能不能明白
|
||||
|
||||
145
|
||||
00:06:16,330 --> 00:06:21,270
|
||||
而B2进去才是已经确认了B一牛逼的情况下
|
||||
|
||||
146
|
||||
00:06:21,410 --> 00:06:22,450
|
||||
B一成立的情况下
|
||||
|
||||
147
|
||||
00:06:22,450 --> 00:06:23,647
|
||||
他可能会快速蹬几根
|
||||
|
||||
148
|
||||
00:06:24,630 --> 00:06:25,894
|
||||
所以先不要管B2
|
||||
|
||||
149
|
||||
00:06:25,900 --> 00:06:27,180
|
||||
先说B1
|
||||
|
||||
150
|
||||
00:06:27,410 --> 00:06:29,060
|
||||
所以说你们第一轮进去的
|
||||
|
||||
151
|
||||
00:06:29,140 --> 00:06:30,860
|
||||
不要抱着不切实际幻想
|
||||
|
||||
152
|
||||
00:06:30,860 --> 00:06:32,948
|
||||
说我今天第一轮完明天就涨
|
||||
|
||||
153
|
||||
00:06:33,300 --> 00:06:34,972
|
||||
说有没有可能有2 1%
|
||||
|
||||
154
|
||||
00:06:34,980 --> 00:06:36,079
|
||||
你就记住这概率
|
||||
|
||||
155
|
||||
00:06:36,080 --> 00:06:37,536
|
||||
市场未来好多1%
|
||||
|
||||
156
|
||||
00:06:38,530 --> 00:06:40,194
|
||||
你100个票第一
|
||||
|
||||
157
|
||||
00:06:40,460 --> 00:06:42,847
|
||||
你虾芝麻轮忙忙忙选进去
|
||||
|
||||
158
|
||||
00:06:43,060 --> 00:06:47,146
|
||||
第二天也是1%的2 1%的涨停的概率
|
||||
|
||||
159
|
||||
00:06:47,950 --> 00:06:51,910
|
||||
5%以上可能是说50分之1或者80分之1就这样了
|
||||
|
||||
160
|
||||
00:06:53,500 --> 00:06:55,130
|
||||
但是你他妈三天后再看
|
||||
|
||||
161
|
||||
00:06:55,630 --> 00:06:57,205
|
||||
或者2到3天后再看
|
||||
|
||||
162
|
||||
00:07:00,030 --> 00:07:01,260
|
||||
所以这块能不能听明白
|
||||
|
||||
163
|
||||
00:07:02,410 --> 00:07:04,160
|
||||
第一叫潜伏
|
||||
|
||||
164
|
||||
00:07:06,070 --> 00:07:09,271
|
||||
B2才是买完之后必须的
|
||||
|
||||
165
|
||||
00:07:10,370 --> 00:07:13,695
|
||||
即使B2也有大概率是那天给你们看的案例
|
||||
|
||||
166
|
||||
00:07:13,710 --> 00:07:15,208
|
||||
一个是国某矿业
|
||||
|
||||
167
|
||||
00:07:15,650 --> 00:07:17,762
|
||||
我调了三天周五才定下来
|
||||
|
||||
168
|
||||
00:07:18,010 --> 00:07:18,540
|
||||
对吗
|
||||
|
||||
169
|
||||
00:07:18,710 --> 00:07:20,355
|
||||
不破止损就拿着
|
||||
|
||||
170
|
||||
00:07:20,360 --> 00:07:21,018
|
||||
但对于很多人讲
|
||||
|
||||
171
|
||||
00:07:21,020 --> 00:07:23,561
|
||||
B2那1根启动的太长了
|
||||
|
||||
172
|
||||
00:07:23,790 --> 00:07:24,498
|
||||
拿不了
|
||||
|
||||
173
|
||||
00:07:24,500 --> 00:07:25,178
|
||||
拿不住
|
||||
|
||||
174
|
||||
00:07:25,470 --> 00:07:26,366
|
||||
B一还没整明白
|
||||
|
||||
175
|
||||
00:07:26,500 --> 00:07:28,489
|
||||
所以先踏踏实实把B一整明白
|
||||
|
||||
176
|
||||
00:07:29,330 --> 00:07:30,930
|
||||
把B一的止损整明白
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
1
|
||||
00:00:00,210 --> 00:00:01,932
|
||||
你们都是什么区块链
|
||||
|
||||
2
|
||||
00:00:03,200 --> 00:00:04,500
|
||||
我说一句话
|
||||
|
||||
3
|
||||
00:00:04,500 --> 00:00:07,888
|
||||
比你外面你自己或你自己瞎操作十年二十年都有用
|
||||
|
||||
4
|
||||
00:00:08,780 --> 00:00:09,548
|
||||
但是你听不懂
|
||||
|
||||
5
|
||||
00:00:10,530 --> 00:00:11,280
|
||||
配三个固态
|
||||
|
||||
6
|
||||
00:00:11,280 --> 00:00:12,320
|
||||
那哥们儿那你出来
|
||||
|
||||
7
|
||||
00:00:12,500 --> 00:00:13,508
|
||||
这不是你他妈又割了
|
||||
|
||||
8
|
||||
00:00:14,050 --> 00:00:15,818
|
||||
割了说因为Z哥说要控制风险
|
||||
|
||||
9
|
||||
00:00:15,830 --> 00:00:16,565
|
||||
所以我他妈割了
|
||||
|
||||
10
|
||||
00:00:16,680 --> 00:00:18,090
|
||||
所以我说傻逼叫你炒股
|
||||
|
||||
11
|
||||
00:00:18,090 --> 00:00:19,034
|
||||
你赚钱跟我没关系
|
||||
|
||||
12
|
||||
00:00:19,040 --> 00:00:20,480
|
||||
你他妈亏钱就是他让我买的
|
||||
|
||||
13
|
||||
00:00:20,700 --> 00:00:21,663
|
||||
我他妈从来没让你买
|
||||
|
||||
14
|
||||
00:00:21,920 --> 00:00:22,390
|
||||
对吗
|
||||
|
||||
15
|
||||
00:00:22,870 --> 00:00:23,686
|
||||
配置还是配置
|
||||
|
||||
16
|
||||
00:00:23,770 --> 00:00:26,058
|
||||
你他妈一个仓配配了三个固态
|
||||
|
||||
17
|
||||
00:00:26,060 --> 00:00:27,360
|
||||
你那赖谁
|
||||
|
||||
18
|
||||
00:00:28,610 --> 00:00:29,780
|
||||
所以这钱你认知不够
|
||||
|
||||
19
|
||||
00:00:29,780 --> 00:00:30,650
|
||||
你就赚不了
|
||||
|
||||
20
|
||||
00:00:30,970 --> 00:00:31,579
|
||||
很正常
|
||||
|
||||
21
|
||||
00:00:31,580 --> 00:00:32,300
|
||||
你就赚不了
|
||||
|
||||
22
|
||||
00:00:34,760 --> 00:00:35,390
|
||||
配三个固态
|
||||
|
||||
23
|
||||
00:00:35,390 --> 00:00:37,040
|
||||
哥们儿前天还发视频总结自己成绩
|
||||
|
||||
24
|
||||
00:00:37,160 --> 00:00:37,680
|
||||
是吗
|
||||
|
||||
25
|
||||
00:00:37,680 --> 00:00:38,728
|
||||
他觉得他行了是吗
|
||||
|
||||
26
|
||||
00:00:39,110 --> 00:00:39,990
|
||||
他不骂我了
|
||||
|
||||
27
|
||||
00:00:40,240 --> 00:00:41,640
|
||||
他他他他不卖门
|
||||
|
||||
28
|
||||
00:00:41,640 --> 00:00:43,134
|
||||
他妈的诋毁我了是吗
|
||||
|
||||
29
|
||||
00:00:44,230 --> 00:00:45,210
|
||||
人性人性
|
||||
|
||||
30
|
||||
00:00:45,350 --> 00:00:47,950
|
||||
我操这人人性人性
|
||||
|
||||
31
|
||||
00:00:48,080 --> 00:00:50,790
|
||||
我操没办法
|
||||
|
||||
32
|
||||
00:00:50,790 --> 00:00:54,320
|
||||
这他妈人性是不是人性
|
||||
|
||||
33
|
||||
00:00:54,940 --> 00:00:58,056
|
||||
我周三我说我说没准你下礼拜就ZGNB了
|
||||
|
||||
34
|
||||
00:00:58,380 --> 00:00:58,880
|
||||
对吗
|
||||
|
||||
35
|
||||
00:01:01,000 --> 00:01:03,272
|
||||
所以我说这个市场永远是有经验人赚
|
||||
|
||||
36
|
||||
00:01:03,280 --> 00:01:05,247
|
||||
没经验钱的地方
|
||||
|
||||
37
|
||||
00:01:06,390 --> 00:01:07,720
|
||||
就是说什么意思
|
||||
|
||||
38
|
||||
00:01:07,720 --> 00:01:08,746
|
||||
我不是吹牛逼
|
||||
|
||||
39
|
||||
00:01:08,750 --> 00:01:11,846
|
||||
咱上礼拜三我给你们举的那五个还是四个
|
||||
|
||||
40
|
||||
00:01:11,860 --> 00:01:13,520
|
||||
四个还是五个例子来着
|
||||
|
||||
41
|
||||
00:01:13,690 --> 00:01:17,630
|
||||
我忘了就那那哥们儿那个短线到后面周四周五接着涨
|
||||
|
||||
42
|
||||
00:01:18,430 --> 00:01:21,334
|
||||
康康又登了10% 10几
|
||||
|
||||
43
|
||||
00:01:21,570 --> 00:01:24,882
|
||||
一根涨停加一个6%还是7
|
||||
|
||||
44
|
||||
00:01:25,140 --> 00:01:25,690
|
||||
是吧
|
||||
|
||||
45
|
||||
00:01:27,390 --> 00:01:30,030
|
||||
某康俩涨停
|
||||
|
||||
46
|
||||
00:01:31,160 --> 00:01:37,348
|
||||
然后那个国某矿直接把周五登进来是吧
|
||||
|
||||
47
|
||||
00:01:37,350 --> 00:01:38,294
|
||||
然后还有什么来着
|
||||
|
||||
48
|
||||
00:01:38,530 --> 00:01:39,610
|
||||
我忘了记不清了
|
||||
|
||||
49
|
||||
00:01:40,570 --> 00:01:43,658
|
||||
然后那陈他没买买进去的又一根长阳
|
||||
|
||||
50
|
||||
00:01:43,670 --> 00:01:44,230
|
||||
对吧
|
||||
|
||||
51
|
||||
00:01:44,890 --> 00:01:45,940
|
||||
这他妈就是水平
|
||||
|
||||
52
|
||||
00:01:46,080 --> 00:01:47,438
|
||||
你别你还别不服
|
||||
|
||||
53
|
||||
00:01:47,440 --> 00:01:53,936
|
||||
我操就有那那你那黑子说听周三的跟大直播没还没比大直播要无聊
|
||||
|
||||
54
|
||||
00:01:53,950 --> 00:01:58,010
|
||||
他说那所以可能他妈是可能玩鞋或者喝喝酒的
|
||||
|
||||
55
|
||||
00:01:58,010 --> 00:01:58,370
|
||||
是Line truncated
|
||||
|
||||
56
|
||||
00:01:58,550 --> 00:01:59,378
|
||||
就是你听不懂
|
||||
|
||||
57
|
||||
00:02:00,230 --> 00:02:00,760
|
||||
所以听不懂
|
||||
|
||||
58
|
||||
00:02:00,760 --> 00:02:01,388
|
||||
认知不够
|
||||
|
||||
59
|
||||
00:02:01,390 --> 00:02:02,086
|
||||
那么就没办法
|
||||
|
||||
60
|
||||
00:02:02,090 --> 00:02:03,962
|
||||
无知者无畏才会说出这种话
|
||||
|
||||
61
|
||||
00:02:05,360 --> 00:02:07,592
|
||||
踏踏实实看两个70%到是吧
|
||||
|
||||
62
|
||||
00:02:07,600 --> 00:02:09,283
|
||||
你这他妈实打实的吗
|
||||
|
||||
63
|
||||
00:02:09,290 --> 00:02:10,958
|
||||
傻逼续费对吗
|
||||
|
||||
64
|
||||
00:02:11,440 --> 00:02:14,116
|
||||
说如果说这哥们儿包括续费
|
||||
|
||||
65
|
||||
00:02:14,120 --> 00:02:17,054
|
||||
现在周三两万多同屏
|
||||
|
||||
66
|
||||
00:02:18,040 --> 00:02:19,370
|
||||
我诚惶诚恐
|
||||
|
||||
67
|
||||
00:02:19,660 --> 00:02:20,050
|
||||
真的
|
||||
|
||||
68
|
||||
00:02:20,050 --> 00:02:22,294
|
||||
咱家当时3000到5000特别自由
|
||||
|
||||
69
|
||||
00:02:23,810 --> 00:02:25,079
|
||||
现在充电视频两万多
|
||||
|
||||
70
|
||||
00:02:25,080 --> 00:02:25,905
|
||||
我诚惶诚恐
|
||||
|
||||
71
|
||||
00:02:25,910 --> 00:02:27,080
|
||||
说实话真的
|
||||
|
||||
72
|
||||
00:02:30,080 --> 00:02:32,852
|
||||
所以我还是这块儿我想告诉大家
|
||||
|
||||
73
|
||||
00:02:33,430 --> 00:02:33,758
|
||||
什么意思
|
||||
|
||||
74
|
||||
00:02:33,760 --> 00:02:39,280
|
||||
就是说咱们一保护咱们直播间不容易
|
||||
|
||||
75
|
||||
00:02:39,590 --> 00:02:42,837
|
||||
在B站能够有这样一个非常开心的地方
|
||||
|
||||
76
|
||||
00:02:42,850 --> 00:02:43,898
|
||||
我觉得非常不容易
|
||||
|
||||
77
|
||||
00:02:44,200 --> 00:02:49,348
|
||||
另外第二就是说也请你们珍惜
|
||||
|
||||
78
|
||||
00:02:49,360 --> 00:02:55,022
|
||||
我还愿意说真的虽然那几个杂毛不超过五个
|
||||
|
||||
79
|
||||
00:02:55,430 --> 00:02:58,269
|
||||
咱家他妈每每礼拜大大直播的十几万人
|
||||
|
||||
80
|
||||
00:02:58,870 --> 00:02:59,927
|
||||
咱们不超过五个
|
||||
|
||||
81
|
||||
00:03:01,490 --> 00:03:06,950
|
||||
但是人性我经历过人性你能明白吗
|
||||
|
||||
82
|
||||
00:03:07,510 --> 00:03:11,386
|
||||
就比如说咱们上礼拜大直播
|
||||
|
||||
83
|
||||
00:03:12,500 --> 00:03:14,580
|
||||
我说反内卷
|
||||
|
||||
84
|
||||
00:03:14,720 --> 00:03:17,814
|
||||
我专门讲了一晚上反内卷对吗
|
||||
|
||||
85
|
||||
00:03:19,620 --> 00:03:20,868
|
||||
周五是不是反内卷
|
||||
|
||||
86
|
||||
00:03:22,800 --> 00:03:25,124
|
||||
然后我之前说我上周说的是反对
|
||||
|
||||
87
|
||||
00:03:25,130 --> 00:03:25,778
|
||||
眼都瞪起来了
|
||||
|
||||
88
|
||||
00:03:25,780 --> 00:03:26,770
|
||||
别着急我等回来
|
||||
|
||||
89
|
||||
00:03:26,770 --> 00:03:27,340
|
||||
等下来
|
||||
|
||||
90
|
||||
00:03:27,720 --> 00:03:28,300
|
||||
等他下来
|
||||
|
||||
91
|
||||
00:03:28,300 --> 00:03:28,789
|
||||
等下来
|
||||
|
||||
92
|
||||
00:03:29,410 --> 00:03:30,280
|
||||
周五是什么
|
||||
|
||||
93
|
||||
00:03:30,400 --> 00:03:32,688
|
||||
煤炭对吧
|
||||
|
||||
94
|
||||
00:03:33,100 --> 00:03:33,410
|
||||
稀土
|
||||
|
||||
95
|
||||
00:03:36,600 --> 00:03:39,336
|
||||
然后水利就乱七八糟
|
||||
|
||||
96
|
||||
00:03:39,340 --> 00:03:41,836
|
||||
这些你们平时就咱们看不到
|
||||
|
||||
97
|
||||
00:03:41,840 --> 00:03:43,465
|
||||
我说我跟他讲的都是机构思维
|
||||
|
||||
98
|
||||
00:03:44,470 --> 00:03:46,630
|
||||
机构思维不是你那种抱着说
|
||||
|
||||
99
|
||||
00:03:47,060 --> 00:03:48,660
|
||||
说完以后马上就涨起来
|
||||
|
||||
100
|
||||
00:03:48,820 --> 00:03:49,288
|
||||
不一定
|
||||
|
||||
101
|
||||
00:03:50,530 --> 00:03:54,273
|
||||
但是概率足够大比你下芝麻轮那些要大很多
|
||||
|
||||
102
|
||||
00:03:55,310 --> 00:03:56,949
|
||||
就是我微信打开我那些群
|
||||
|
||||
103
|
||||
00:03:57,850 --> 00:03:59,390
|
||||
都是什么类的群
|
||||
|
||||
104
|
||||
00:03:59,670 --> 00:04:01,778
|
||||
跟你打开那些他妈小姐姐小妹妹的裙子
|
||||
|
||||
105
|
||||
00:04:01,790 --> 00:04:02,665
|
||||
他妈什么不一样
|
||||
|
||||
106
|
||||
00:04:02,670 --> 00:04:06,058
|
||||
哥们踏踏实实就是很简单
|
||||
|
||||
107
|
||||
00:04:06,060 --> 00:04:08,418
|
||||
你行你上对吗
|
||||
|
||||
108
|
||||
00:04:08,610 --> 00:04:10,338
|
||||
你你你行你上
|
||||
|
||||
109
|
||||
00:04:11,580 --> 00:04:14,375
|
||||
包括下礼拜上周反内卷没涨完
|
||||
|
||||
110
|
||||
00:04:14,670 --> 00:04:16,713
|
||||
没没没休息一礼拜了
|
||||
|
||||
111
|
||||
00:04:16,920 --> 00:04:17,508
|
||||
接着轮
|
||||
|
||||
112
|
||||
00:04:18,230 --> 00:04:19,693
|
||||
就是他妈的没准接着作业
|
||||
|
||||
113
|
||||
00:04:19,700 --> 00:04:22,534
|
||||
接着我就那些还没有轮动到的
|
||||
|
||||
114
|
||||
00:04:22,540 --> 00:04:25,580
|
||||
别着急踏踏实实那踏踏实实
|
||||
|
||||
115
|
||||
00:04:35,320 --> 00:04:38,656
|
||||
然后还有我我我我想到哪儿
|
||||
|
||||
116
|
||||
00:04:38,660 --> 00:04:39,050
|
||||
我说到哪儿
|
||||
|
||||
117
|
||||
00:04:39,170 --> 00:04:41,686
|
||||
因为咱们直播主要就是一个蛋逼直播间
|
||||
|
||||
118
|
||||
00:04:43,750 --> 00:04:48,235
|
||||
有有一些朋友大概我看了有七八个
|
||||
|
||||
119
|
||||
00:04:48,590 --> 00:04:49,910
|
||||
后台给我截图
|
||||
|
||||
120
|
||||
00:04:50,280 --> 00:04:54,620
|
||||
其中还有一个还还有一些是跟的时间比较长的
|
||||
|
||||
121
|
||||
00:04:55,260 --> 00:04:59,385
|
||||
我不知道周三录播你们都看没看啊
|
||||
|
||||
122
|
||||
00:04:59,790 --> 00:05:06,612
|
||||
就是这个我后来后面专门给你们讲的案例
|
||||
|
||||
123
|
||||
00:05:06,780 --> 00:05:09,104
|
||||
我讲第一选出来
|
||||
|
||||
124
|
||||
00:05:09,110 --> 00:05:10,250
|
||||
当天选完之后
|
||||
|
||||
125
|
||||
00:05:10,250 --> 00:05:13,488
|
||||
第二天比如那天比例选了217个
|
||||
|
||||
126
|
||||
00:05:14,050 --> 00:05:16,258
|
||||
第一第二天涨停的只有一个
|
||||
|
||||
127
|
||||
00:05:16,580 --> 00:05:18,637
|
||||
相当于是2百17分之1
|
||||
|
||||
128
|
||||
00:05:19,130 --> 00:05:22,570
|
||||
但是我给你们往上翻三个交易日前的
|
||||
|
||||
129
|
||||
00:05:23,950 --> 00:05:25,358
|
||||
我怀孕了什么意思
|
||||
|
||||
130
|
||||
00:05:26,120 --> 00:05:27,896
|
||||
哥们儿你怀孕了跟我说干嘛
|
||||
|
||||
131
|
||||
00:05:28,310 --> 00:05:29,798
|
||||
他老师该该找谁去
|
||||
|
||||
132
|
||||
00:05:31,860 --> 00:05:32,540
|
||||
说哪儿了
|
||||
|
||||
133
|
||||
00:05:32,540 --> 00:05:35,428
|
||||
对你往三天前一番
|
||||
|
||||
134
|
||||
00:05:36,000 --> 00:05:41,814
|
||||
然后周三的时候一看是单3到5个涨停
|
||||
|
||||
135
|
||||
00:05:42,190 --> 00:05:42,568
|
||||
是不是
|
||||
|
||||
136
|
||||
00:05:42,880 --> 00:05:45,270
|
||||
3到5个涨停什么意思
|
||||
|
||||
137
|
||||
00:05:45,940 --> 00:05:46,930
|
||||
再强调一遍
|
||||
|
||||
138
|
||||
00:05:48,130 --> 00:05:52,882
|
||||
第一叫潜伏好不好
|
||||
|
||||
139
|
||||
00:05:53,930 --> 00:05:57,787
|
||||
B一潜伏不是说B一一进去马上坑就掉下来
|
||||
|
||||
140
|
||||
00:05:58,530 --> 00:06:01,346
|
||||
那概率就是200分之1
|
||||
|
||||
141
|
||||
00:06:03,020 --> 00:06:04,571
|
||||
还是在市场不暴跌的时候
|
||||
|
||||
142
|
||||
00:06:05,100 --> 00:06:08,976
|
||||
B一进去绝大部分时间你要等2到3天
|
||||
|
||||
143
|
||||
00:06:10,060 --> 00:06:12,293
|
||||
甚至个别板块等3到5天
|
||||
|
||||
144
|
||||
00:06:14,130 --> 00:06:15,618
|
||||
这句话能不能明白
|
||||
|
||||
145
|
||||
00:06:16,330 --> 00:06:21,270
|
||||
而B2进去才是已经确认了B一牛逼的情况下
|
||||
|
||||
146
|
||||
00:06:21,410 --> 00:06:22,450
|
||||
B一成立的情况下
|
||||
|
||||
147
|
||||
00:06:22,450 --> 00:06:23,647
|
||||
他可能会快速蹬几根
|
||||
|
||||
148
|
||||
00:06:24,630 --> 00:06:25,894
|
||||
所以先不要管B2
|
||||
|
||||
149
|
||||
00:06:25,900 --> 00:06:27,180
|
||||
先说B1
|
||||
|
||||
150
|
||||
00:06:27,410 --> 00:06:29,060
|
||||
所以说你们第一轮进去的
|
||||
|
||||
151
|
||||
00:06:29,140 --> 00:06:30,860
|
||||
不要抱着不切实际幻想
|
||||
|
||||
152
|
||||
00:06:30,860 --> 00:06:32,948
|
||||
说我今天第一轮完明天就涨
|
||||
|
||||
153
|
||||
00:06:33,300 --> 00:06:34,972
|
||||
说有没有可能有2 1%
|
||||
|
||||
154
|
||||
00:06:34,980 --> 00:06:36,079
|
||||
你就记住这概率
|
||||
|
||||
155
|
||||
00:06:36,080 --> 00:06:37,536
|
||||
市场未来好多1%
|
||||
|
||||
156
|
||||
00:06:38,530 --> 00:06:40,194
|
||||
你100个票第一
|
||||
|
||||
157
|
||||
00:06:40,460 --> 00:06:42,847
|
||||
你虾芝麻轮忙忙忙选进去
|
||||
|
||||
158
|
||||
00:06:43,060 --> 00:06:47,146
|
||||
第二天也是1%的2 1%的涨停的概率
|
||||
|
||||
159
|
||||
00:06:47,950 --> 00:06:51,910
|
||||
5%以上可能是说50分之1或者80分之1就这样了
|
||||
|
||||
160
|
||||
00:06:53,500 --> 00:06:55,130
|
||||
但是你他妈三天后再看
|
||||
|
||||
161
|
||||
00:06:55,630 --> 00:06:57,205
|
||||
或者2到3天后再看
|
||||
|
||||
162
|
||||
00:07:00,030 --> 00:07:01,260
|
||||
所以这块能不能听明白
|
||||
|
||||
163
|
||||
00:07:02,410 --> 00:07:04,160
|
||||
第一叫潜伏
|
||||
|
||||
164
|
||||
00:07:06,070 --> 00:07:09,271
|
||||
B2才是买完之后必须的
|
||||
|
||||
165
|
||||
00:07:10,370 --> 00:07:13,695
|
||||
即使B2也有大概率是那天给你们看的案例
|
||||
|
||||
166
|
||||
00:07:13,710 --> 00:07:15,208
|
||||
一个是国某矿业
|
||||
|
||||
167
|
||||
00:07:15,650 --> 00:07:17,762
|
||||
我调了三天周五才定下来
|
||||
|
||||
168
|
||||
00:07:18,010 --> 00:07:18,540
|
||||
对吗
|
||||
|
||||
169
|
||||
00:07:18,710 --> 00:07:20,355
|
||||
不破止损就拿着
|
||||
|
||||
170
|
||||
00:07:20,360 --> 00:07:21,018
|
||||
但对于很多人讲
|
||||
|
||||
171
|
||||
00:07:21,020 --> 00:07:23,561
|
||||
B2那1根启动的太长了
|
||||
|
||||
172
|
||||
00:07:23,790 --> 00:07:24,498
|
||||
拿不了
|
||||
|
||||
173
|
||||
00:07:24,500 --> 00:07:25,178
|
||||
拿不住
|
||||
|
||||
174
|
||||
00:07:25,470 --> 00:07:26,366
|
||||
B一还没整明白
|
||||
|
||||
175
|
||||
00:07:26,500 --> 00:07:28,489
|
||||
所以先踏踏实实把B一整明白
|
||||
|
||||
176
|
||||
00:07:29,330 --> 00:07:30,930
|
||||
把B一的止损整明白
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
1
|
||||
00:00:00,210 --> 00:00:01,932
|
||||
你们都是什么区块链
|
||||
|
||||
2
|
||||
00:00:03,200 --> 00:00:04,500
|
||||
我说一句话
|
||||
|
||||
3
|
||||
00:00:04,500 --> 00:00:07,888
|
||||
比你外面你自己或你自己瞎操作十年二十年都有用
|
||||
|
||||
4
|
||||
00:00:08,780 --> 00:00:09,548
|
||||
但是你听不懂
|
||||
|
||||
5
|
||||
00:00:10,530 --> 00:00:11,280
|
||||
配三个固态
|
||||
|
||||
6
|
||||
00:00:11,280 --> 00:00:12,320
|
||||
那哥们儿那你出来
|
||||
|
||||
7
|
||||
00:00:12,500 --> 00:00:13,508
|
||||
这不是你他妈又割了
|
||||
|
||||
8
|
||||
00:00:14,050 --> 00:00:15,818
|
||||
割了说因为Z哥说要控制风险
|
||||
|
||||
9
|
||||
00:00:15,830 --> 00:00:16,565
|
||||
所以我他妈割了
|
||||
|
||||
10
|
||||
00:00:16,680 --> 00:00:18,090
|
||||
所以我说傻逼叫你炒股
|
||||
|
||||
11
|
||||
00:00:18,090 --> 00:00:19,034
|
||||
你赚钱跟我没关系
|
||||
|
||||
12
|
||||
00:00:19,040 --> 00:00:20,480
|
||||
你他妈亏钱就是他让我买的
|
||||
|
||||
13
|
||||
00:00:20,700 --> 00:00:21,663
|
||||
我他妈从来没让你买
|
||||
|
||||
14
|
||||
00:00:21,920 --> 00:00:22,390
|
||||
对吗
|
||||
|
||||
15
|
||||
00:00:22,870 --> 00:00:23,686
|
||||
配置还是配置
|
||||
|
||||
16
|
||||
00:00:23,770 --> 00:00:26,058
|
||||
你他妈一个仓配配了三个固态
|
||||
|
||||
17
|
||||
00:00:26,060 --> 00:00:27,360
|
||||
你那赖谁
|
||||
|
||||
18
|
||||
00:00:28,610 --> 00:00:29,780
|
||||
所以这钱你认知不够
|
||||
|
||||
19
|
||||
00:00:29,780 --> 00:00:30,650
|
||||
你就赚不了
|
||||
|
||||
20
|
||||
00:00:30,970 --> 00:00:31,579
|
||||
很正常
|
||||
|
||||
21
|
||||
00:00:31,580 --> 00:00:32,300
|
||||
你就赚不了
|
||||
|
||||
22
|
||||
00:00:34,760 --> 00:00:35,390
|
||||
配三个固态
|
||||
|
||||
23
|
||||
00:00:35,390 --> 00:00:37,040
|
||||
哥们儿前天还发视频总结自己成绩
|
||||
|
||||
24
|
||||
00:00:37,160 --> 00:00:37,680
|
||||
是吗
|
||||
|
||||
25
|
||||
00:00:37,680 --> 00:00:38,728
|
||||
他觉得他行了是吗
|
||||
|
||||
26
|
||||
00:00:39,110 --> 00:00:39,990
|
||||
他不骂我了
|
||||
|
||||
27
|
||||
00:00:40,240 --> 00:00:41,640
|
||||
他他他他不卖门
|
||||
|
||||
28
|
||||
00:00:41,640 --> 00:00:43,134
|
||||
他妈的诋毁我了是吗
|
||||
|
||||
29
|
||||
00:00:44,230 --> 00:00:45,210
|
||||
人性人性
|
||||
|
||||
30
|
||||
00:00:45,350 --> 00:00:47,950
|
||||
我操这人人性人性
|
||||
|
||||
31
|
||||
00:00:48,080 --> 00:00:50,790
|
||||
我操没办法
|
||||
|
||||
32
|
||||
00:00:50,790 --> 00:00:54,320
|
||||
这他妈人性是不是人性
|
||||
|
||||
33
|
||||
00:00:54,940 --> 00:00:58,056
|
||||
我周三我说我说没准你下礼拜就ZGNB了
|
||||
|
||||
34
|
||||
00:00:58,380 --> 00:00:58,880
|
||||
对吗
|
||||
|
||||
35
|
||||
00:01:01,000 --> 00:01:03,272
|
||||
所以我说这个市场永远是有经验人赚
|
||||
|
||||
36
|
||||
00:01:03,280 --> 00:01:05,247
|
||||
没经验钱的地方
|
||||
|
||||
37
|
||||
00:01:06,390 --> 00:01:07,720
|
||||
就是说什么意思
|
||||
|
||||
38
|
||||
00:01:07,720 --> 00:01:08,746
|
||||
我不是吹牛逼
|
||||
|
||||
39
|
||||
00:01:08,750 --> 00:01:11,846
|
||||
咱上礼拜三我给你们举的那五个还是四个
|
||||
|
||||
40
|
||||
00:01:11,860 --> 00:01:13,520
|
||||
四个还是五个例子来着
|
||||
|
||||
41
|
||||
00:01:13,690 --> 00:01:17,630
|
||||
我忘了就那那哥们儿那个短线到后面周四周五接着涨
|
||||
|
||||
42
|
||||
00:01:18,430 --> 00:01:21,334
|
||||
康康又登了10% 10几
|
||||
|
||||
43
|
||||
00:01:21,570 --> 00:01:24,882
|
||||
一根涨停加一个6%还是7
|
||||
|
||||
44
|
||||
00:01:25,140 --> 00:01:25,690
|
||||
是吧
|
||||
|
||||
45
|
||||
00:01:27,390 --> 00:01:30,030
|
||||
某康俩涨停
|
||||
|
||||
46
|
||||
00:01:31,160 --> 00:01:37,348
|
||||
然后那个国某矿直接把周五登进来是吧
|
||||
|
||||
47
|
||||
00:01:37,350 --> 00:01:38,294
|
||||
然后还有什么来着
|
||||
|
||||
48
|
||||
00:01:38,530 --> 00:01:39,610
|
||||
我忘了记不清了
|
||||
|
||||
49
|
||||
00:01:40,570 --> 00:01:43,658
|
||||
然后那陈他没买买进去的又一根长阳
|
||||
|
||||
50
|
||||
00:01:43,670 --> 00:01:44,230
|
||||
对吧
|
||||
|
||||
51
|
||||
00:01:44,890 --> 00:01:45,940
|
||||
这他妈就是水平
|
||||
|
||||
52
|
||||
00:01:46,080 --> 00:01:47,438
|
||||
你别你还别不服
|
||||
|
||||
53
|
||||
00:01:47,440 --> 00:01:53,936
|
||||
我操就有那那你那黑子说听周三的跟大直播没还没比大直播要无聊
|
||||
|
||||
54
|
||||
00:01:53,950 --> 00:01:58,010
|
||||
他说那所以可能他妈是可能玩鞋或者喝喝酒的
|
||||
|
||||
55
|
||||
00:01:58,010 --> 00:01:58,370
|
||||
是Line truncated
|
||||
|
||||
56
|
||||
00:01:58,550 --> 00:01:59,378
|
||||
就是你听不懂
|
||||
|
||||
57
|
||||
00:02:00,230 --> 00:02:00,760
|
||||
所以听不懂
|
||||
|
||||
58
|
||||
00:02:00,760 --> 00:02:01,388
|
||||
认知不够
|
||||
|
||||
59
|
||||
00:02:01,390 --> 00:02:02,086
|
||||
那么就没办法
|
||||
|
||||
60
|
||||
00:02:02,090 --> 00:02:03,962
|
||||
无知者无畏才会说出这种话
|
||||
|
||||
61
|
||||
00:02:05,360 --> 00:02:07,592
|
||||
踏踏实实看两个70%到是吧
|
||||
|
||||
62
|
||||
00:02:07,600 --> 00:02:09,283
|
||||
你这他妈实打实的吗
|
||||
|
||||
63
|
||||
00:02:09,290 --> 00:02:10,958
|
||||
傻逼续费对吗
|
||||
|
||||
64
|
||||
00:02:11,440 --> 00:02:14,116
|
||||
说如果说这哥们儿包括续费
|
||||
|
||||
65
|
||||
00:02:14,120 --> 00:02:17,054
|
||||
现在周三两万多同屏
|
||||
|
||||
66
|
||||
00:02:18,040 --> 00:02:19,370
|
||||
我诚惶诚恐
|
||||
|
||||
67
|
||||
00:02:19,660 --> 00:02:20,050
|
||||
真的
|
||||
|
||||
68
|
||||
00:02:20,050 --> 00:02:22,294
|
||||
咱家当时3000到5000特别自由
|
||||
|
||||
69
|
||||
00:02:23,810 --> 00:02:25,079
|
||||
现在充电视频两万多
|
||||
|
||||
70
|
||||
00:02:25,080 --> 00:02:25,905
|
||||
我诚惶诚恐
|
||||
|
||||
71
|
||||
00:02:25,910 --> 00:02:27,080
|
||||
说实话真的
|
||||
|
||||
72
|
||||
00:02:30,080 --> 00:02:32,852
|
||||
所以我还是这块儿我想告诉大家
|
||||
|
||||
73
|
||||
00:02:33,430 --> 00:02:33,758
|
||||
什么意思
|
||||
|
||||
74
|
||||
00:02:33,760 --> 00:02:39,280
|
||||
就是说咱们一保护咱们直播间不容易
|
||||
|
||||
75
|
||||
00:02:39,590 --> 00:02:42,837
|
||||
在B站能够有这样一个非常开心的地方
|
||||
|
||||
76
|
||||
00:02:42,850 --> 00:02:43,898
|
||||
我觉得非常不容易
|
||||
|
||||
77
|
||||
00:02:44,200 --> 00:02:49,348
|
||||
另外第二就是说也请你们珍惜
|
||||
|
||||
78
|
||||
00:02:49,360 --> 00:02:55,022
|
||||
我还愿意说真的虽然那几个杂毛不超过五个
|
||||
|
||||
79
|
||||
00:02:55,430 --> 00:02:58,269
|
||||
咱家他妈每每礼拜大大直播的十几万人
|
||||
|
||||
80
|
||||
00:02:58,870 --> 00:02:59,927
|
||||
咱们不超过五个
|
||||
|
||||
81
|
||||
00:03:01,490 --> 00:03:06,950
|
||||
但是人性我经历过人性你能明白吗
|
||||
|
||||
82
|
||||
00:03:07,510 --> 00:03:11,386
|
||||
就比如说咱们上礼拜大直播
|
||||
|
||||
83
|
||||
00:03:12,500 --> 00:03:14,580
|
||||
我说反内卷
|
||||
|
||||
84
|
||||
00:03:14,720 --> 00:03:17,814
|
||||
我专门讲了一晚上反内卷对吗
|
||||
|
||||
85
|
||||
00:03:19,620 --> 00:03:20,868
|
||||
周五是不是反内卷
|
||||
|
||||
86
|
||||
00:03:22,800 --> 00:03:25,124
|
||||
然后我之前说我上周说的是反对
|
||||
|
||||
87
|
||||
00:03:25,130 --> 00:03:25,778
|
||||
眼都瞪起来了
|
||||
|
||||
88
|
||||
00:03:25,780 --> 00:03:26,770
|
||||
别着急我等回来
|
||||
|
||||
89
|
||||
00:03:26,770 --> 00:03:27,340
|
||||
等下来
|
||||
|
||||
90
|
||||
00:03:27,720 --> 00:03:28,300
|
||||
等他下来
|
||||
|
||||
91
|
||||
00:03:28,300 --> 00:03:28,789
|
||||
等下来
|
||||
|
||||
92
|
||||
00:03:29,410 --> 00:03:30,280
|
||||
周五是什么
|
||||
|
||||
93
|
||||
00:03:30,400 --> 00:03:32,688
|
||||
煤炭对吧
|
||||
|
||||
94
|
||||
00:03:33,100 --> 00:03:33,410
|
||||
稀土
|
||||
|
||||
95
|
||||
00:03:36,600 --> 00:03:39,336
|
||||
然后水利就乱七八糟
|
||||
|
||||
96
|
||||
00:03:39,340 --> 00:03:41,836
|
||||
这些你们平时就咱们看不到
|
||||
|
||||
97
|
||||
00:03:41,840 --> 00:03:43,465
|
||||
我说我跟他讲的都是机构思维
|
||||
|
||||
98
|
||||
00:03:44,470 --> 00:03:46,630
|
||||
机构思维不是你那种抱着说
|
||||
|
||||
99
|
||||
00:03:47,060 --> 00:03:48,660
|
||||
说完以后马上就涨起来
|
||||
|
||||
100
|
||||
00:03:48,820 --> 00:03:49,288
|
||||
不一定
|
||||
|
||||
101
|
||||
00:03:50,530 --> 00:03:54,273
|
||||
但是概率足够大比你下芝麻轮那些要大很多
|
||||
|
||||
102
|
||||
00:03:55,310 --> 00:03:56,949
|
||||
就是我微信打开我那些群
|
||||
|
||||
103
|
||||
00:03:57,850 --> 00:03:59,390
|
||||
都是什么类的群
|
||||
|
||||
104
|
||||
00:03:59,670 --> 00:04:01,778
|
||||
跟你打开那些他妈小姐姐小妹妹的裙子
|
||||
|
||||
105
|
||||
00:04:01,790 --> 00:04:02,665
|
||||
他妈什么不一样
|
||||
|
||||
106
|
||||
00:04:02,670 --> 00:04:06,058
|
||||
哥们踏踏实实就是很简单
|
||||
|
||||
107
|
||||
00:04:06,060 --> 00:04:08,418
|
||||
你行你上对吗
|
||||
|
||||
108
|
||||
00:04:08,610 --> 00:04:10,338
|
||||
你你你行你上
|
||||
|
||||
109
|
||||
00:04:11,580 --> 00:04:14,375
|
||||
包括下礼拜上周反内卷没涨完
|
||||
|
||||
110
|
||||
00:04:14,670 --> 00:04:16,713
|
||||
没没没休息一礼拜了
|
||||
|
||||
111
|
||||
00:04:16,920 --> 00:04:17,508
|
||||
接着轮
|
||||
|
||||
112
|
||||
00:04:18,230 --> 00:04:19,693
|
||||
就是他妈的没准接着作业
|
||||
|
||||
113
|
||||
00:04:19,700 --> 00:04:22,534
|
||||
接着我就那些还没有轮动到的
|
||||
|
||||
114
|
||||
00:04:22,540 --> 00:04:25,580
|
||||
别着急踏踏实实那踏踏实实
|
||||
|
||||
115
|
||||
00:04:35,320 --> 00:04:38,656
|
||||
然后还有我我我我想到哪儿
|
||||
|
||||
116
|
||||
00:04:38,660 --> 00:04:39,050
|
||||
我说到哪儿
|
||||
|
||||
117
|
||||
00:04:39,170 --> 00:04:41,686
|
||||
因为咱们直播主要就是一个蛋逼直播间
|
||||
|
||||
118
|
||||
00:04:43,750 --> 00:04:48,235
|
||||
有有一些朋友大概我看了有七八个
|
||||
|
||||
119
|
||||
00:04:48,590 --> 00:04:49,910
|
||||
后台给我截图
|
||||
|
||||
120
|
||||
00:04:50,280 --> 00:04:54,620
|
||||
其中还有一个还还有一些是跟的时间比较长的
|
||||
|
||||
121
|
||||
00:04:55,260 --> 00:04:59,385
|
||||
我不知道周三录播你们都看没看啊
|
||||
|
||||
122
|
||||
00:04:59,790 --> 00:05:06,612
|
||||
就是这个我后来后面专门给你们讲的案例
|
||||
|
||||
123
|
||||
00:05:06,780 --> 00:05:09,104
|
||||
我讲第一选出来
|
||||
|
||||
124
|
||||
00:05:09,110 --> 00:05:10,250
|
||||
当天选完之后
|
||||
|
||||
125
|
||||
00:05:10,250 --> 00:05:13,488
|
||||
第二天比如那天比例选了217个
|
||||
|
||||
126
|
||||
00:05:14,050 --> 00:05:16,258
|
||||
第一第二天涨停的只有一个
|
||||
|
||||
127
|
||||
00:05:16,580 --> 00:05:18,637
|
||||
相当于是2百17分之1
|
||||
|
||||
128
|
||||
00:05:19,130 --> 00:05:22,570
|
||||
但是我给你们往上翻三个交易日前的
|
||||
|
||||
129
|
||||
00:05:23,950 --> 00:05:25,358
|
||||
我怀孕了什么意思
|
||||
|
||||
130
|
||||
00:05:26,120 --> 00:05:27,896
|
||||
哥们儿你怀孕了跟我说干嘛
|
||||
|
||||
131
|
||||
00:05:28,310 --> 00:05:29,798
|
||||
他老师该该找谁去
|
||||
|
||||
132
|
||||
00:05:31,860 --> 00:05:32,540
|
||||
说哪儿了
|
||||
|
||||
133
|
||||
00:05:32,540 --> 00:05:35,428
|
||||
对你往三天前一番
|
||||
|
||||
134
|
||||
00:05:36,000 --> 00:05:41,814
|
||||
然后周三的时候一看是单3到5个涨停
|
||||
|
||||
135
|
||||
00:05:42,190 --> 00:05:42,568
|
||||
是不是
|
||||
|
||||
136
|
||||
00:05:42,880 --> 00:05:45,270
|
||||
3到5个涨停什么意思
|
||||
|
||||
137
|
||||
00:05:45,940 --> 00:05:46,930
|
||||
再强调一遍
|
||||
|
||||
138
|
||||
00:05:48,130 --> 00:05:52,882
|
||||
第一叫潜伏好不好
|
||||
|
||||
139
|
||||
00:05:53,930 --> 00:05:57,787
|
||||
B一潜伏不是说B一一进去马上坑就掉下来
|
||||
|
||||
140
|
||||
00:05:58,530 --> 00:06:01,346
|
||||
那概率就是200分之1
|
||||
|
||||
141
|
||||
00:06:03,020 --> 00:06:04,571
|
||||
还是在市场不暴跌的时候
|
||||
|
||||
142
|
||||
00:06:05,100 --> 00:06:08,976
|
||||
B一进去绝大部分时间你要等2到3天
|
||||
|
||||
143
|
||||
00:06:10,060 --> 00:06:12,293
|
||||
甚至个别板块等3到5天
|
||||
|
||||
144
|
||||
00:06:14,130 --> 00:06:15,618
|
||||
这句话能不能明白
|
||||
|
||||
145
|
||||
00:06:16,330 --> 00:06:21,270
|
||||
而B2进去才是已经确认了B一牛逼的情况下
|
||||
|
||||
146
|
||||
00:06:21,410 --> 00:06:22,450
|
||||
B一成立的情况下
|
||||
|
||||
147
|
||||
00:06:22,450 --> 00:06:23,647
|
||||
他可能会快速蹬几根
|
||||
|
||||
148
|
||||
00:06:24,630 --> 00:06:25,894
|
||||
所以先不要管B2
|
||||
|
||||
149
|
||||
00:06:25,900 --> 00:06:27,180
|
||||
先说B1
|
||||
|
||||
150
|
||||
00:06:27,410 --> 00:06:29,060
|
||||
所以说你们第一轮进去的
|
||||
|
||||
151
|
||||
00:06:29,140 --> 00:06:30,860
|
||||
不要抱着不切实际幻想
|
||||
|
||||
152
|
||||
00:06:30,860 --> 00:06:32,948
|
||||
说我今天第一轮完明天就涨
|
||||
|
||||
153
|
||||
00:06:33,300 --> 00:06:34,972
|
||||
说有没有可能有2 1%
|
||||
|
||||
154
|
||||
00:06:34,980 --> 00:06:36,079
|
||||
你就记住这概率
|
||||
|
||||
155
|
||||
00:06:36,080 --> 00:06:37,536
|
||||
市场未来好多1%
|
||||
|
||||
156
|
||||
00:06:38,530 --> 00:06:40,194
|
||||
你100个票第一
|
||||
|
||||
157
|
||||
00:06:40,460 --> 00:06:42,847
|
||||
你虾芝麻轮忙忙忙选进去
|
||||
|
||||
158
|
||||
00:06:43,060 --> 00:06:47,146
|
||||
第二天也是1%的2 1%的涨停的概率
|
||||
|
||||
159
|
||||
00:06:47,950 --> 00:06:51,910
|
||||
5%以上可能是说50分之1或者80分之1就这样了
|
||||
|
||||
160
|
||||
00:06:53,500 --> 00:06:55,130
|
||||
但是你他妈三天后再看
|
||||
|
||||
161
|
||||
00:06:55,630 --> 00:06:57,205
|
||||
或者2到3天后再看
|
||||
|
||||
162
|
||||
00:07:00,030 --> 00:07:01,260
|
||||
所以这块能不能听明白
|
||||
|
||||
163
|
||||
00:07:02,410 --> 00:07:04,160
|
||||
第一叫潜伏
|
||||
|
||||
164
|
||||
00:07:06,070 --> 00:07:09,271
|
||||
B2才是买完之后必须的
|
||||
|
||||
165
|
||||
00:07:10,370 --> 00:07:13,695
|
||||
即使B2也有大概率是那天给你们看的案例
|
||||
|
||||
166
|
||||
00:07:13,710 --> 00:07:15,208
|
||||
一个是国某矿业
|
||||
|
||||
167
|
||||
00:07:15,650 --> 00:07:17,762
|
||||
我调了三天周五才定下来
|
||||
|
||||
168
|
||||
00:07:18,010 --> 00:07:18,540
|
||||
对吗
|
||||
|
||||
169
|
||||
00:07:18,710 --> 00:07:20,355
|
||||
不破止损就拿着
|
||||
|
||||
170
|
||||
00:07:20,360 --> 00:07:21,018
|
||||
但对于很多人讲
|
||||
|
||||
171
|
||||
00:07:21,020 --> 00:07:23,561
|
||||
B2那1根启动的太长了
|
||||
|
||||
172
|
||||
00:07:23,790 --> 00:07:24,498
|
||||
拿不了
|
||||
|
||||
173
|
||||
00:07:24,500 --> 00:07:25,178
|
||||
拿不住
|
||||
|
||||
174
|
||||
00:07:25,470 --> 00:07:26,366
|
||||
B一还没整明白
|
||||
|
||||
175
|
||||
00:07:26,500 --> 00:07:28,489
|
||||
所以先踏踏实实把B一整明白
|
||||
|
||||
176
|
||||
00:07:29,330 --> 00:07:30,930
|
||||
把B一的止损整明白
|
||||
@@ -6,7 +6,7 @@
|
||||
from typing import TypeVar, Generic, Optional, List, Dict, Any, Type
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from backend.models.base import BaseModel
|
||||
from models.base import BaseModel
|
||||
|
||||
# 定义泛型类型
|
||||
ModelType = TypeVar("ModelType", bound=BaseModel)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, asc, func
|
||||
from .base import BaseRepository
|
||||
from backend.models.clip import Clip, ClipStatus
|
||||
from models.clip import Clip, ClipStatus
|
||||
|
||||
class ClipRepository(BaseRepository[Clip]):
|
||||
"""切片Repository类"""
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, asc, func
|
||||
from .base import BaseRepository
|
||||
from backend.models.collection import Collection, CollectionStatus
|
||||
from models.collection import Collection, CollectionStatus
|
||||
|
||||
class CollectionRepository(BaseRepository[Collection]):
|
||||
"""合集Repository类"""
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, asc
|
||||
from .base import BaseRepository
|
||||
from backend.models.project import Project, ProjectStatus, ProjectType
|
||||
from models.project import Project, ProjectStatus, ProjectType
|
||||
|
||||
class ProjectRepository(BaseRepository[Project]):
|
||||
"""项目Repository类"""
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, asc, func
|
||||
from .base import BaseRepository
|
||||
from backend.models.task import Task, TaskStatus, TaskType
|
||||
from models.task import Task, TaskStatus, TaskType
|
||||
|
||||
class TaskRepository(BaseRepository[Task]):
|
||||
"""任务Repository类"""
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
from typing import Generic, TypeVar, Type, Optional, List, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_
|
||||
from backend.repositories.base import BaseRepository, ModelType as RepoModelType
|
||||
from backend.schemas.base import BaseSchema, PaginationParams, PaginationResponse
|
||||
from repositories.base import BaseRepository, ModelType as RepoModelType
|
||||
from schemas.base import BaseSchema, PaginationParams, PaginationResponse
|
||||
|
||||
|
||||
CreateSchemaType = TypeVar("CreateSchemaType")
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
"""
|
||||
合集服务
|
||||
提供合集相关的业务逻辑操作
|
||||
"""合集服务
|
||||
提供合集相关的业务逻辑操作,集成Pipeline处理结果
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from pathlib import Path
|
||||
|
||||
from services.base import BaseService
|
||||
from repositories.collection_repository import CollectionRepository
|
||||
from models.collection import Collection
|
||||
from schemas.collection import CollectionCreate, CollectionUpdate, CollectionResponse, CollectionListResponse, CollectionFilter
|
||||
from schemas.base import PaginationParams, PaginationResponse
|
||||
from models.project import Project
|
||||
from models.clip import Clip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CollectionService(BaseService[Collection, CollectionCreate, CollectionUpdate, CollectionResponse]):
|
||||
"""Collection service with business logic."""
|
||||
"""Collection service with business logic and Pipeline integration."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
repository = CollectionRepository(db)
|
||||
@@ -36,3 +42,230 @@ class CollectionService(BaseService[Collection, CollectionCreate, CollectionUpda
|
||||
def get_collections_by_project(self, project_id: str, skip: int = 0, limit: int = 100) -> List[Collection]:
|
||||
"""Get collections by project ID."""
|
||||
return self.repository.find_by(project_id=project_id)
|
||||
|
||||
def create_collections_from_pipeline_result(self, project_id: int, clustering_result: Dict[str, Any]) -> List[Collection]:
|
||||
"""从Pipeline聚类结果创建合集
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
clustering_result: Pipeline步骤5的聚类结果
|
||||
|
||||
Returns:
|
||||
创建的合集列表
|
||||
"""
|
||||
logger.info(f"从Pipeline结果为项目 {project_id} 创建合集")
|
||||
|
||||
try:
|
||||
collections = []
|
||||
|
||||
# 获取项目信息
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise ValueError(f"项目 {project_id} 不存在")
|
||||
|
||||
# 解析聚类结果
|
||||
if not clustering_result.get('success'):
|
||||
raise ValueError(f"聚类结果无效: {clustering_result.get('error')}")
|
||||
|
||||
clusters_data = clustering_result.get('data', {})
|
||||
clusters = clusters_data.get('clusters', [])
|
||||
|
||||
for i, cluster in enumerate(clusters):
|
||||
# 创建合集
|
||||
collection_data = {
|
||||
'title': cluster.get('title', f'合集 {i+1}'),
|
||||
'description': cluster.get('description', ''),
|
||||
'project_id': project_id,
|
||||
'theme': cluster.get('theme', ''),
|
||||
'clip_count': len(cluster.get('clips', [])),
|
||||
'total_duration': sum(clip.get('duration', 0) for clip in cluster.get('clips', [])),
|
||||
'metadata': {
|
||||
'cluster_id': cluster.get('cluster_id'),
|
||||
'confidence_score': cluster.get('confidence_score', 0.0),
|
||||
'keywords': cluster.get('keywords', []),
|
||||
'created_from_pipeline': True
|
||||
}
|
||||
}
|
||||
|
||||
collection = Collection(**collection_data)
|
||||
self.db.add(collection)
|
||||
self.db.flush() # 获取ID但不提交
|
||||
|
||||
# 关联切片到合集
|
||||
self._associate_clips_to_collection(collection.id, cluster.get('clips', []))
|
||||
|
||||
collections.append(collection)
|
||||
logger.info(f"创建合集: {collection.title} (包含 {collection.clip_count} 个切片)")
|
||||
|
||||
self.db.commit()
|
||||
logger.info(f"成功为项目 {project_id} 创建了 {len(collections)} 个合集")
|
||||
|
||||
return collections
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"从Pipeline结果创建合集失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def _associate_clips_to_collection(self, collection_id: int, clips_data: List[Dict[str, Any]]):
|
||||
"""将切片关联到合集
|
||||
|
||||
Args:
|
||||
collection_id: 合集ID
|
||||
clips_data: 切片数据列表
|
||||
"""
|
||||
for clip_data in clips_data:
|
||||
# 查找或创建切片
|
||||
clip = self._find_or_create_clip(clip_data)
|
||||
if clip:
|
||||
# 更新切片的合集关联
|
||||
clip.collection_id = collection_id
|
||||
self.db.add(clip)
|
||||
|
||||
def _find_or_create_clip(self, clip_data: Dict[str, Any]) -> Optional[Clip]:
|
||||
"""查找或创建切片
|
||||
|
||||
Args:
|
||||
clip_data: 切片数据
|
||||
|
||||
Returns:
|
||||
切片对象
|
||||
"""
|
||||
try:
|
||||
# 根据时间范围查找现有切片
|
||||
start_time = clip_data.get('start_time')
|
||||
end_time = clip_data.get('end_time')
|
||||
project_id = clip_data.get('project_id')
|
||||
|
||||
if start_time is not None and end_time is not None and project_id:
|
||||
existing_clip = self.db.query(Clip).filter(
|
||||
Clip.project_id == project_id,
|
||||
Clip.start_time == start_time,
|
||||
Clip.end_time == end_time
|
||||
).first()
|
||||
|
||||
if existing_clip:
|
||||
return existing_clip
|
||||
|
||||
# 创建新切片
|
||||
clip_data_clean = {
|
||||
'title': clip_data.get('title', ''),
|
||||
'description': clip_data.get('description', ''),
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'duration': clip_data.get('duration', 0),
|
||||
'project_id': project_id,
|
||||
'score': clip_data.get('score', 0.0),
|
||||
'metadata': {
|
||||
'created_from_pipeline': True,
|
||||
'original_text': clip_data.get('text', ''),
|
||||
'keywords': clip_data.get('keywords', []),
|
||||
'confidence': clip_data.get('confidence', 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
clip = Clip(**clip_data_clean)
|
||||
self.db.add(clip)
|
||||
return clip
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建切片失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def generate_collection_video(self, collection_id: int, output_path: Optional[Path] = None) -> Dict[str, Any]:
|
||||
"""生成合集视频
|
||||
|
||||
Args:
|
||||
collection_id: 合集ID
|
||||
output_path: 输出路径
|
||||
|
||||
Returns:
|
||||
生成结果
|
||||
"""
|
||||
logger.info(f"生成合集 {collection_id} 的视频")
|
||||
|
||||
try:
|
||||
collection = self.get(collection_id)
|
||||
if not collection:
|
||||
raise ValueError(f"合集 {collection_id} 不存在")
|
||||
|
||||
# 获取合集中的所有切片
|
||||
clips = self.db.query(Clip).filter(Clip.collection_id == collection_id).all()
|
||||
|
||||
if not clips:
|
||||
raise ValueError(f"合集 {collection_id} 中没有切片")
|
||||
|
||||
# 使用Pipeline的视频生成功能
|
||||
from services.pipeline_adapter import create_pipeline_adapter
|
||||
pipeline_adapter = create_pipeline_adapter(self.db)
|
||||
|
||||
# 准备切片数据
|
||||
clips_data = []
|
||||
for clip in clips:
|
||||
clips_data.append({
|
||||
'start_time': clip.start_time,
|
||||
'end_time': clip.end_time,
|
||||
'title': clip.title,
|
||||
'score': clip.score
|
||||
})
|
||||
|
||||
# 生成合集视频
|
||||
result = pipeline_adapter._step6_video_for_collection(
|
||||
collection.project_id,
|
||||
clips_data,
|
||||
collection.title,
|
||||
output_path
|
||||
)
|
||||
|
||||
# 更新合集的视频路径
|
||||
if result.get('success') and result.get('data', {}).get('video_path'):
|
||||
collection.video_path = result['data']['video_path']
|
||||
collection.status = 'completed'
|
||||
self.db.commit()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成合集视频失败: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def get_collection_statistics(self, project_id: int) -> Dict[str, Any]:
|
||||
"""获取项目的合集统计信息
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
|
||||
Returns:
|
||||
统计信息
|
||||
"""
|
||||
collections = self.get_collections_by_project(str(project_id))
|
||||
|
||||
total_collections = len(collections)
|
||||
total_clips = sum(collection.clip_count for collection in collections)
|
||||
total_duration = sum(collection.total_duration for collection in collections)
|
||||
|
||||
# 按主题分组统计
|
||||
theme_stats = {}
|
||||
for collection in collections:
|
||||
theme = collection.theme or '未分类'
|
||||
if theme not in theme_stats:
|
||||
theme_stats[theme] = {
|
||||
'count': 0,
|
||||
'clips': 0,
|
||||
'duration': 0
|
||||
}
|
||||
theme_stats[theme]['count'] += 1
|
||||
theme_stats[theme]['clips'] += collection.clip_count
|
||||
theme_stats[theme]['duration'] += collection.total_duration
|
||||
|
||||
return {
|
||||
'total_collections': total_collections,
|
||||
'total_clips': total_clips,
|
||||
'total_duration': total_duration,
|
||||
'average_clips_per_collection': total_clips / total_collections if total_collections > 0 else 0,
|
||||
'average_duration_per_collection': total_duration / total_collections if total_collections > 0 else 0,
|
||||
'theme_statistics': theme_stats
|
||||
}
|
||||
@@ -12,8 +12,8 @@ from typing import Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from backend.models.project import ProjectType
|
||||
from backend.core.database import get_db
|
||||
from models.project import ProjectType
|
||||
from core.database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,360 +1,335 @@
|
||||
"""
|
||||
流水线适配器
|
||||
负责文件路径映射和流水线接口适配,让现有流水线逻辑保持原状
|
||||
"""
|
||||
"""Pipeline适配器服务 - 将shared/pipeline中的处理逻辑集成到新架构"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional, Callable
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
from shutil import copy2
|
||||
from datetime import datetime
|
||||
|
||||
from .config_manager import ProjectConfigManager
|
||||
from .path_manager import PathManager
|
||||
# 导入shared/pipeline中的处理步骤
|
||||
import sys
|
||||
shared_path = Path(__file__).parent.parent.parent / "shared"
|
||||
if str(shared_path) not in sys.path:
|
||||
sys.path.insert(0, str(shared_path))
|
||||
|
||||
from pipeline.step1_outline import OutlineExtractor
|
||||
from pipeline.step2_timeline import TimelineExtractor
|
||||
from pipeline.step3_scoring import ClipScorer
|
||||
from pipeline.step4_title import TitleGenerator
|
||||
from pipeline.step5_clustering import ClusteringEngine
|
||||
from pipeline.step6_video import VideoGenerator
|
||||
from utils.project_manager import ProjectManager
|
||||
from config import METADATA_DIR, PROMPT_FILES
|
||||
|
||||
# 导入新架构的模型和服务
|
||||
from models.project import Project, ProjectStatus
|
||||
from models.task import Task, TaskStatus
|
||||
from core.database import get_db
|
||||
from core.progress_manager import get_progress_manager
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PipelineAdapter:
|
||||
"""流水线适配器,负责路径映射和接口适配"""
|
||||
"""Pipeline适配器 - 桥接旧Pipeline和新架构"""
|
||||
|
||||
def __init__(self, project_id: str, debug_mode: bool = False):
|
||||
self.project_id = project_id
|
||||
self.debug_mode = debug_mode
|
||||
self.config_manager = ProjectConfigManager(project_id)
|
||||
self.path_manager = PathManager(project_id)
|
||||
self.project_paths = self.path_manager.get_project_paths()
|
||||
def __init__(self, db: Session, task_id: Optional[int] = None, progress_callback: Optional[Callable[[int, int, str], Any]] = None):
|
||||
self.db = db
|
||||
self.task_id = task_id
|
||||
self.progress_callback = progress_callback
|
||||
self.progress_manager = get_progress_manager(db) if task_id else None
|
||||
|
||||
# 确保项目目录结构存在
|
||||
self.path_manager.ensure_directories()
|
||||
# 初始化各个步骤的处理器
|
||||
self.outline_extractor = None
|
||||
self.timeline_generator = None
|
||||
self.clip_scorer = None
|
||||
self.title_generator = None
|
||||
self.clustering_engine = None
|
||||
self.video_generator = None
|
||||
|
||||
def get_step_input_path(self, step_name: str) -> Path:
|
||||
"""获取步骤输入文件路径"""
|
||||
return self.path_manager.get_step_input_path(step_name)
|
||||
# 步骤配置
|
||||
self.steps = [
|
||||
{"name": "outline", "description": "提取视频大纲", "weight": 15},
|
||||
{"name": "timeline", "description": "生成时间线", "weight": 20},
|
||||
{"name": "scoring", "description": "内容评分", "weight": 20},
|
||||
{"name": "title", "description": "生成标题", "weight": 15},
|
||||
{"name": "clustering", "description": "主题聚类", "weight": 15},
|
||||
{"name": "video", "description": "生成视频", "weight": 15}
|
||||
]
|
||||
|
||||
def get_step_output_path(self, step_name: str) -> Path:
|
||||
"""获取步骤输出文件路径"""
|
||||
return self.path_manager.get_step_output_path(step_name)
|
||||
async def process_project(self, project_id: int, input_video_path: str, input_srt_path: str) -> Dict[str, Any]:
|
||||
"""处理整个项目的Pipeline流程"""
|
||||
try:
|
||||
# 获取项目信息
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
raise ValueError(f"项目 {project_id} 不存在")
|
||||
|
||||
def get_step_intermediate_dir(self, step_name: str) -> Path:
|
||||
"""获取步骤中间文件目录"""
|
||||
return self.path_manager.get_step_intermediate_dir(step_name)
|
||||
# 更新项目状态
|
||||
project.status = ProjectStatus.PROCESSING
|
||||
project.started_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
def prepare_step_environment(self, step_name: str, input_data: Any = None):
|
||||
"""准备步骤执行环境"""
|
||||
# 使用PathManager创建步骤目录
|
||||
self.path_manager.create_step_directories(step_name)
|
||||
# 创建项目工作目录
|
||||
project_dir = Path(METADATA_DIR) / f"project_{project_id}"
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 如果有输入数据,保存到输入文件
|
||||
if input_data is not None:
|
||||
input_path = self.get_step_input_path(step_name)
|
||||
self._save_json(input_path, input_data)
|
||||
# 初始化处理器
|
||||
self._initialize_processors(project_dir)
|
||||
|
||||
def should_skip_step(self, step_name: str) -> bool:
|
||||
"""
|
||||
判断是否应该跳过步骤(debug模式下如果输出已存在则跳过)
|
||||
# 执行6个步骤
|
||||
results = {}
|
||||
total_progress = 0
|
||||
|
||||
Args:
|
||||
step_name: 步骤名称
|
||||
for i, step in enumerate(self.steps):
|
||||
step_name = step["name"]
|
||||
step_description = step["description"]
|
||||
step_weight = step["weight"]
|
||||
|
||||
Returns:
|
||||
是否应该跳过
|
||||
"""
|
||||
if not self.debug_mode:
|
||||
return False
|
||||
logger.info(f"开始执行步骤 {i+1}/6: {step_description}")
|
||||
|
||||
output_path = self.get_step_output_path(step_name)
|
||||
if output_path.exists():
|
||||
logger.info(f"Debug模式:跳过步骤 {step_name},输出文件已存在: {output_path}")
|
||||
return True
|
||||
# 更新进度
|
||||
await self._update_progress(project_id, total_progress, f"正在{step_description}...")
|
||||
|
||||
return False
|
||||
try:
|
||||
# 执行具体步骤
|
||||
if step_name == "outline":
|
||||
results[step_name] = await self._step1_outline(input_srt_path, project_dir)
|
||||
elif step_name == "timeline":
|
||||
results[step_name] = await self._step2_timeline(results["outline"], project_dir)
|
||||
elif step_name == "scoring":
|
||||
results[step_name] = await self._step3_scoring(results["timeline"], project_dir)
|
||||
elif step_name == "title":
|
||||
results[step_name] = await self._step4_title(results["scoring"], project_dir)
|
||||
elif step_name == "clustering":
|
||||
results[step_name] = await self._step5_clustering(results["title"], project_dir)
|
||||
elif step_name == "video":
|
||||
results[step_name] = await self._step6_video(results["clustering"], input_video_path, project_dir)
|
||||
|
||||
def get_step_cache_info(self, step_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取步骤缓存信息
|
||||
total_progress += step_weight
|
||||
await self._update_progress(project_id, total_progress, f"{step_description}完成", results[step_name])
|
||||
|
||||
Args:
|
||||
step_name: 步骤名称
|
||||
except Exception as e:
|
||||
logger.error(f"步骤 {step_name} 执行失败: {str(e)}")
|
||||
await self._update_progress(project_id, total_progress, f"{step_description}失败: {str(e)}")
|
||||
raise
|
||||
|
||||
Returns:
|
||||
缓存信息
|
||||
"""
|
||||
output_path = self.get_step_output_path(step_name)
|
||||
input_path = self.get_step_input_path(step_name)
|
||||
# 更新项目状态为完成
|
||||
project.status = ProjectStatus.COMPLETED
|
||||
project.completed_at = datetime.utcnow()
|
||||
project.result_data = results
|
||||
self.db.commit()
|
||||
|
||||
cache_info = {
|
||||
"step_name": step_name,
|
||||
"output_exists": output_path.exists(),
|
||||
"input_exists": input_path.exists(),
|
||||
"output_path": str(output_path),
|
||||
"input_path": str(input_path),
|
||||
"output_size": output_path.stat().st_size if output_path.exists() else 0,
|
||||
"input_size": input_path.stat().st_size if input_path.exists() else 0,
|
||||
"output_modified": output_path.stat().st_mtime if output_path.exists() else 0,
|
||||
"input_modified": input_path.stat().st_mtime if input_path.exists() else 0
|
||||
}
|
||||
await self._update_progress(project_id, 100, "项目处理完成")
|
||||
|
||||
return cache_info
|
||||
logger.info(f"项目 {project_id} 处理完成")
|
||||
return results
|
||||
|
||||
def _save_json(self, path: Path, data: Any):
|
||||
"""保存JSON数据到文件"""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"数据已保存到: {path}")
|
||||
except Exception as e:
|
||||
# 更新项目状态为失败
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if project:
|
||||
project.status = ProjectStatus.FAILED
|
||||
project.error_message = str(e)
|
||||
self.db.commit()
|
||||
|
||||
def _load_json(self, path: Path) -> Any:
|
||||
"""从文件加载JSON数据"""
|
||||
if not path.exists():
|
||||
return None
|
||||
await self._update_progress(project_id, -1, f"处理失败: {str(e)}")
|
||||
logger.error(f"项目 {project_id} 处理失败: {str(e)}")
|
||||
raise
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
def _initialize_processors(self, project_dir: Path):
|
||||
"""初始化各个处理器"""
|
||||
self.outline_extractor = OutlineExtractor(metadata_dir=project_dir, prompt_files=PROMPT_FILES)
|
||||
self.timeline_extractor = TimelineExtractor(metadata_dir=project_dir, prompt_files=PROMPT_FILES)
|
||||
self.clip_scorer = ClipScorer(prompt_files=PROMPT_FILES)
|
||||
self.title_generator = TitleGenerator(prompt_files=PROMPT_FILES)
|
||||
self.clustering_engine = ClusteringEngine(prompt_files=PROMPT_FILES)
|
||||
self.video_generator = VideoGenerator(metadata_dir=str(project_dir))
|
||||
|
||||
def adapt_step(self, step_name: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
统一的步骤适配方法
|
||||
|
||||
Args:
|
||||
step_name: 步骤名称
|
||||
**kwargs: 步骤特定参数
|
||||
|
||||
Returns:
|
||||
适配后的参数
|
||||
"""
|
||||
logger.info(f"开始适配步骤: {step_name}")
|
||||
logger.debug(f"步骤 {step_name} 输入参数: {list(kwargs.keys())}")
|
||||
|
||||
step_config = {
|
||||
"step1_outline": {
|
||||
"required_inputs": ["srt_path"],
|
||||
"input_mapping": {"srt_path": "srt_path"},
|
||||
"output_path": "step1_outline",
|
||||
"special_handling": "copy_srt_file"
|
||||
},
|
||||
"step2_timeline": {
|
||||
"required_inputs": ["step1_outline"],
|
||||
"input_mapping": {"outline_path": "step1_outline"},
|
||||
"output_path": "step2_timeline"
|
||||
},
|
||||
"step3_scoring": {
|
||||
"required_inputs": ["step2_timeline"],
|
||||
"input_mapping": {"timeline_path": "step2_timeline"},
|
||||
"output_path": "step3_scoring"
|
||||
},
|
||||
"step4_title": {
|
||||
"required_inputs": ["step3_scoring"],
|
||||
"input_mapping": {"high_score_clips_path": "step3_scoring"},
|
||||
"output_path": "step4_title",
|
||||
"metadata_dir_type": "str"
|
||||
},
|
||||
"step5_clustering": {
|
||||
"required_inputs": ["step4_title"],
|
||||
"input_mapping": {"clips_with_titles_path": "step4_title"},
|
||||
"output_path": "step5_clustering",
|
||||
"metadata_dir_type": "str"
|
||||
},
|
||||
"step6_video": {
|
||||
"required_inputs": ["step4_title", "step5_clustering"],
|
||||
"input_mapping": {
|
||||
"clips_with_titles_path": "step4_title",
|
||||
"collections_path": "step5_clustering",
|
||||
"input_video": "input_video"
|
||||
},
|
||||
"output_path": "step6_video",
|
||||
"metadata_dir_type": "str",
|
||||
"output_param_name": "output_dir",
|
||||
"special_handling": "add_input_video"
|
||||
}
|
||||
}
|
||||
|
||||
if step_name not in step_config:
|
||||
logger.error(f"未知的步骤: {step_name}")
|
||||
raise ValueError(f"未知的步骤: {step_name}")
|
||||
|
||||
config = step_config[step_name]
|
||||
logger.debug(f"步骤 {step_name} 配置: {config}")
|
||||
|
||||
# 处理特殊输入(如SRT文件复制)
|
||||
if "special_handling" in config:
|
||||
if config["special_handling"] == "copy_srt_file":
|
||||
srt_path = kwargs.get("srt_path")
|
||||
if not srt_path or not srt_path.exists():
|
||||
logger.error(f"SRT文件不存在: {srt_path}")
|
||||
async def _step1_outline(self, srt_path: str, project_dir: Path) -> List[Dict]:
|
||||
"""步骤1: 提取视频大纲"""
|
||||
srt_file = Path(srt_path)
|
||||
if not srt_file.exists():
|
||||
raise FileNotFoundError(f"SRT文件不存在: {srt_path}")
|
||||
|
||||
# 复制SRT文件到项目目录
|
||||
project_srt_path = self.path_manager.get_srt_path()
|
||||
# 在线程池中执行CPU密集型任务
|
||||
loop = asyncio.get_event_loop()
|
||||
outlines = await loop.run_in_executor(
|
||||
None,
|
||||
self.outline_extractor.extract_outline,
|
||||
srt_file
|
||||
)
|
||||
|
||||
# 检查源路径和目标路径是否相同
|
||||
if srt_path.resolve() == project_srt_path.resolve():
|
||||
logger.info(f"SRT文件已在目标位置,跳过复制: {srt_path}")
|
||||
else:
|
||||
logger.info(f"复制SRT文件: {srt_path} -> {project_srt_path}")
|
||||
copy2(srt_path, project_srt_path)
|
||||
# 保存结果
|
||||
output_path = project_dir / "step1_outline.json"
|
||||
self.outline_extractor.save_outline(outlines, output_path)
|
||||
|
||||
kwargs["srt_path"] = project_srt_path
|
||||
elif config["special_handling"] == "add_input_video":
|
||||
# 为step6_video添加输入视频路径
|
||||
input_video_path = self.path_manager.get_video_path()
|
||||
if not input_video_path.exists():
|
||||
logger.error(f"输入视频文件不存在: {input_video_path}")
|
||||
raise FileNotFoundError(f"输入视频文件不存在: {input_video_path}")
|
||||
return outlines
|
||||
|
||||
kwargs["input_video"] = input_video_path
|
||||
logger.info(f"添加输入视频路径: {input_video_path}")
|
||||
async def _step2_timeline(self, outlines: List[Dict], project_dir: Path) -> List[Dict]:
|
||||
"""步骤2: 生成时间线"""
|
||||
# 加载SRT块数据
|
||||
srt_chunks_dir = project_dir / "step1_srt_chunks"
|
||||
if not srt_chunks_dir.exists():
|
||||
raise FileNotFoundError(f"SRT块目录不存在: {srt_chunks_dir}")
|
||||
|
||||
# 验证输入文件
|
||||
for input_name in config["required_inputs"]:
|
||||
if input_name not in kwargs:
|
||||
# 尝试从前一步的输出获取
|
||||
prev_step_output = self.get_step_output_path(input_name)
|
||||
logger.debug(f"检查输入文件: {input_name} -> {prev_step_output}")
|
||||
if not prev_step_output.exists():
|
||||
logger.error(f"输入文件不存在: {input_name} -> {prev_step_output}")
|
||||
raise FileNotFoundError(f"输入文件不存在: {input_name} -> {prev_step_output}")
|
||||
kwargs[input_name] = prev_step_output
|
||||
logger.debug(f"使用前一步输出作为输入: {input_name}")
|
||||
loop = asyncio.get_event_loop()
|
||||
timeline_data = await loop.run_in_executor(
|
||||
None,
|
||||
self.timeline_extractor.extract_timeline,
|
||||
outlines
|
||||
)
|
||||
|
||||
# 构建输出路径
|
||||
output_path = self.get_step_output_path(config["output_path"])
|
||||
logger.debug(f"步骤 {step_name} 输出路径: {output_path}")
|
||||
# 保存结果
|
||||
output_path = project_dir / "step2_timeline.json"
|
||||
self.timeline_extractor.save_timeline(timeline_data, output_path)
|
||||
|
||||
# 构建基础参数
|
||||
output_param_name = config.get("output_param_name", "output_path")
|
||||
params = {
|
||||
output_param_name: output_path
|
||||
return timeline_data
|
||||
|
||||
async def _step3_scoring(self, timeline_data: List[Dict], project_dir: Path) -> List[Dict]:
|
||||
"""步骤3: 内容评分"""
|
||||
loop = asyncio.get_event_loop()
|
||||
scored_clips = await loop.run_in_executor(
|
||||
None,
|
||||
self.clip_scorer.score_clips,
|
||||
timeline_data
|
||||
)
|
||||
|
||||
# 保存结果
|
||||
output_path = project_dir / "step3_scored_clips.json"
|
||||
self.clip_scorer.save_scores(scored_clips, output_path)
|
||||
|
||||
return scored_clips
|
||||
|
||||
async def _step4_title(self, scored_clips: List[Dict], project_dir: Path) -> List[Dict]:
|
||||
"""步骤4: 生成标题"""
|
||||
loop = asyncio.get_event_loop()
|
||||
clips_with_titles = await loop.run_in_executor(
|
||||
None,
|
||||
self.title_generator.generate_titles,
|
||||
scored_clips
|
||||
)
|
||||
|
||||
# 保存结果
|
||||
output_path = project_dir / "step4_titles.json"
|
||||
self.title_generator.save_titles(clips_with_titles, output_path)
|
||||
|
||||
return clips_with_titles
|
||||
|
||||
async def _step5_clustering(self, clips_with_titles: List[Dict], project_dir: Path) -> List[Dict]:
|
||||
"""步骤5: 主题聚类"""
|
||||
loop = asyncio.get_event_loop()
|
||||
collections = await loop.run_in_executor(
|
||||
None,
|
||||
self.clustering_engine.create_collections,
|
||||
clips_with_titles
|
||||
)
|
||||
|
||||
# 保存结果
|
||||
output_path = project_dir / "step5_collections.json"
|
||||
self.clustering_engine.save_collections(collections, output_path)
|
||||
|
||||
return collections
|
||||
|
||||
async def _step6_video(self, collections: List[Dict], input_video_path: str, project_dir: Path) -> Dict:
|
||||
"""步骤6: 生成视频"""
|
||||
# 从collections中提取clips_with_titles
|
||||
clips_with_titles = []
|
||||
for collection in collections:
|
||||
clips_with_titles.extend(collection.get('clips', []))
|
||||
|
||||
input_video = Path(input_video_path)
|
||||
if not input_video.exists():
|
||||
raise FileNotFoundError(f"输入视频不存在: {input_video_path}")
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# 生成切片视频
|
||||
successful_clips = await loop.run_in_executor(
|
||||
None,
|
||||
self.video_generator.generate_clips,
|
||||
clips_with_titles,
|
||||
input_video
|
||||
)
|
||||
|
||||
# 生成合集视频
|
||||
successful_collections = await loop.run_in_executor(
|
||||
None,
|
||||
self.video_generator.generate_collections,
|
||||
collections
|
||||
)
|
||||
|
||||
# 保存元数据
|
||||
self.video_generator.save_clip_metadata(clips_with_titles)
|
||||
self.video_generator.save_collection_metadata(collections)
|
||||
|
||||
result = {
|
||||
'clips_generated': len(successful_clips),
|
||||
'collections_generated': len(successful_collections),
|
||||
'clip_paths': [str(path) for path in successful_clips],
|
||||
'collection_paths': [str(path) for path in successful_collections]
|
||||
}
|
||||
|
||||
# 只有非step6的步骤才添加prompt_files
|
||||
if step_name != "step6_video":
|
||||
params["prompt_files"] = self.config_manager.get_prompt_files()
|
||||
# 保存结果
|
||||
output_path = project_dir / "step6_video_result.json"
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 处理特殊输出映射(如step6的output_dir)
|
||||
if "special_output_mapping" in config:
|
||||
for param_name, output_param in config["special_output_mapping"].items():
|
||||
params[param_name] = params[output_param]
|
||||
logger.debug(f"特殊输出映射: {param_name} <- {output_param}")
|
||||
|
||||
# 添加输入映射
|
||||
for param_name, input_name in config["input_mapping"].items():
|
||||
if input_name in kwargs:
|
||||
params[param_name] = kwargs[input_name]
|
||||
logger.debug(f"映射参数: {param_name} <- {input_name}")
|
||||
|
||||
# 添加metadata_dir
|
||||
metadata_dir = self.project_paths["metadata_dir"]
|
||||
if config.get("metadata_dir_type") == "str":
|
||||
params["metadata_dir"] = str(metadata_dir)
|
||||
logger.debug(f"metadata_dir类型: str")
|
||||
else:
|
||||
params["metadata_dir"] = metadata_dir
|
||||
logger.debug(f"metadata_dir类型: Path")
|
||||
|
||||
logger.info(f"步骤 {step_name} 适配完成,参数: {list(params.keys())}")
|
||||
return params
|
||||
|
||||
# 保持向后兼容的方法
|
||||
def adapt_step1_outline(self, srt_path: Path) -> Dict[str, Any]:
|
||||
"""适配Step1: 大纲提取"""
|
||||
return self.adapt_step("step1_outline", srt_path=srt_path)
|
||||
|
||||
def adapt_step2_timeline(self) -> Dict[str, Any]:
|
||||
"""适配Step2: 时间线提取"""
|
||||
return self.adapt_step("step2_timeline")
|
||||
|
||||
def adapt_step3_scoring(self) -> Dict[str, Any]:
|
||||
"""适配Step3: 评分"""
|
||||
return self.adapt_step("step3_scoring")
|
||||
|
||||
def adapt_step4_title(self) -> Dict[str, Any]:
|
||||
"""适配Step4: 标题生成"""
|
||||
return self.adapt_step("step4_title")
|
||||
|
||||
def adapt_step5_clustering(self) -> Dict[str, Any]:
|
||||
"""适配Step5: 聚类"""
|
||||
return self.adapt_step("step5_clustering")
|
||||
|
||||
def adapt_step6_video(self) -> Dict[str, Any]:
|
||||
"""适配Step6: 视频生成"""
|
||||
return self.adapt_step("step6_video")
|
||||
|
||||
def get_step_result(self, step_name: str) -> Any:
|
||||
"""获取步骤执行结果"""
|
||||
output_path = self.get_step_output_path(step_name)
|
||||
result = self._load_json(output_path)
|
||||
logger.debug(f"获取步骤 {step_name} 结果: {'成功' if result else '失败'}")
|
||||
return result
|
||||
|
||||
def get_pipeline_cache_info(self) -> Dict[str, Any]:
|
||||
"""获取流水线缓存信息"""
|
||||
steps = [
|
||||
"step1_outline", "step2_timeline", "step3_scoring",
|
||||
"step4_title", "step5_clustering", "step6_video"
|
||||
]
|
||||
async def _update_progress(self, project_id: int, progress: int, message: str, step_result: Optional[Dict[str, Any]] = None):
|
||||
"""更新项目进度"""
|
||||
try:
|
||||
# 更新数据库中的项目进度
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if project:
|
||||
project.progress = progress
|
||||
project.current_step = message
|
||||
self.db.commit()
|
||||
|
||||
cache_info = {}
|
||||
for step in steps:
|
||||
cache_info[step] = self.get_step_cache_info(step)
|
||||
# 使用进度管理器更新进度
|
||||
if self.progress_manager and self.task_id:
|
||||
await self.progress_manager.update_task_progress(
|
||||
task_id=self.task_id,
|
||||
current_step=progress // 17 + 1, # 估算当前步骤
|
||||
total_steps=6,
|
||||
step_name=message,
|
||||
progress=progress,
|
||||
message=message,
|
||||
step_result=step_result
|
||||
)
|
||||
|
||||
return cache_info
|
||||
# 调用进度回调函数(用于WebSocket推送)
|
||||
if self.progress_callback:
|
||||
await self.progress_callback(project_id, progress, message)
|
||||
|
||||
def get_project_size_info(self) -> Dict[str, Any]:
|
||||
"""获取项目大小信息"""
|
||||
return self.path_manager.get_project_size_info()
|
||||
except Exception as e:
|
||||
logger.error(f"更新进度失败: {str(e)}")
|
||||
|
||||
def cleanup_all_intermediate_files(self):
|
||||
"""清理所有步骤的中间文件"""
|
||||
steps = [
|
||||
"step1_outline", "step2_timeline", "step3_scoring",
|
||||
"step4_title", "step5_clustering", "step6_video"
|
||||
]
|
||||
def get_project_status(self, project_id: int) -> Dict[str, Any]:
|
||||
"""获取项目状态"""
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if not project:
|
||||
return {"error": "项目不存在"}
|
||||
|
||||
for step in steps:
|
||||
self.cleanup_intermediate_files(step)
|
||||
|
||||
logger.info("已清理所有步骤的中间文件")
|
||||
|
||||
def cleanup_intermediate_files(self, step_name: str):
|
||||
"""清理步骤的中间文件"""
|
||||
self.path_manager.cleanup_step_files(step_name, keep_output=True)
|
||||
|
||||
def get_pipeline_status(self) -> Dict[str, Any]:
|
||||
"""获取流水线执行状态"""
|
||||
status = {}
|
||||
steps = [
|
||||
"step1_outline", "step2_timeline", "step3_scoring",
|
||||
"step4_title", "step5_clustering", "step6_video"
|
||||
]
|
||||
|
||||
for step in steps:
|
||||
output_path = self.get_step_output_path(step)
|
||||
status[step] = {
|
||||
"completed": output_path.exists(),
|
||||
"output_path": str(output_path),
|
||||
"file_size": output_path.stat().st_size if output_path.exists() else 0
|
||||
return {
|
||||
"id": project.id,
|
||||
"name": project.name,
|
||||
"status": project.status.value,
|
||||
"progress": project.progress or 0,
|
||||
"current_step": project.current_step or "",
|
||||
"started_at": project.started_at.isoformat() if project.started_at else None,
|
||||
"completed_at": project.completed_at.isoformat() if project.completed_at else None,
|
||||
"error_message": project.error_message
|
||||
}
|
||||
|
||||
return status
|
||||
# 工厂函数
|
||||
def create_pipeline_adapter(db: Session, task_id: Optional[int] = None, progress_callback: Optional[Callable[[int, int, str], Any]] = None) -> PipelineAdapter:
|
||||
"""创建Pipeline适配器实例"""
|
||||
return PipelineAdapter(db, task_id, progress_callback)
|
||||
|
||||
def validate_pipeline_prerequisites(self) -> List[str]:
|
||||
"""验证流水线前置条件"""
|
||||
errors = []
|
||||
|
||||
# 检查路径有效性
|
||||
path_errors = self.path_manager.validate_paths()
|
||||
errors.extend(path_errors)
|
||||
|
||||
# 检查SRT文件
|
||||
srt_path = self.path_manager.get_srt_path()
|
||||
if not srt_path.exists():
|
||||
errors.append(f"SRT文件不存在: {srt_path}")
|
||||
|
||||
# 检查LLM配置
|
||||
try:
|
||||
self.config_manager.get_llm_config()
|
||||
except ValueError as e:
|
||||
errors.append(str(e))
|
||||
|
||||
return errors
|
||||
# 同步版本的工厂函数,用于在非异步环境中使用
|
||||
def create_pipeline_adapter_sync(db: Session, task_id: Optional[int] = None) -> PipelineAdapter:
|
||||
"""创建同步版本的Pipeline适配器实例"""
|
||||
return PipelineAdapter(db, task_id, None)
|
||||
@@ -9,10 +9,10 @@ from typing import Dict, Any, List, Optional, Callable
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.models.task import Task, TaskStatus, TaskType
|
||||
from backend.repositories.task_repository import TaskRepository
|
||||
from backend.services.config_manager import ProjectConfigManager, ProcessingStep
|
||||
from backend.services.pipeline_adapter import PipelineAdapter
|
||||
from models.task import Task, TaskStatus, TaskType
|
||||
from repositories.task_repository import TaskRepository
|
||||
from services.config_manager import ProjectConfigManager, ProcessingStep
|
||||
from services.pipeline_adapter import PipelineAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.models.task import Task, TaskStatus, TaskType
|
||||
from backend.repositories.task_repository import TaskRepository
|
||||
from backend.services.config_manager import ProjectConfigManager, ProcessingStep
|
||||
from backend.services.pipeline_adapter import PipelineAdapter
|
||||
from backend.services.processing_orchestrator import ProcessingOrchestrator
|
||||
from backend.services.processing_context import ProcessingContext
|
||||
from backend.services.exceptions import ServiceError, ProcessingError, TaskError, ProjectError, handle_service_error
|
||||
from backend.services.concurrency_manager import with_concurrency_control
|
||||
from models.task import Task, TaskStatus, TaskType
|
||||
from repositories.task_repository import TaskRepository
|
||||
from services.config_manager import ProjectConfigManager, ProcessingStep
|
||||
from services.pipeline_adapter import PipelineAdapter
|
||||
from services.processing_orchestrator import ProcessingOrchestrator
|
||||
from services.processing_context import ProcessingContext
|
||||
from services.exceptions import ServiceError, ProcessingError, TaskError, ProjectError, handle_service_error
|
||||
from services.concurrency_manager import with_concurrency_control
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,7 +61,7 @@ class ProcessingService:
|
||||
|
||||
# 更新项目状态为已完成
|
||||
try:
|
||||
from backend.models.project import Project, ProjectStatus
|
||||
from models.project import Project, ProjectStatus
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if project:
|
||||
project.status = ProjectStatus.COMPLETED
|
||||
@@ -233,7 +233,7 @@ class ProcessingService:
|
||||
|
||||
# 更新项目状态为已完成
|
||||
try:
|
||||
from backend.models.project import Project, ProjectStatus
|
||||
from models.project import Project, ProjectStatus
|
||||
project = self.db.query(Project).filter(Project.id == project_id).first()
|
||||
if project:
|
||||
project.status = ProjectStatus.COMPLETED
|
||||
|
||||
@@ -8,7 +8,7 @@ import asyncio
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from backend.core.websocket_manager import manager, WebSocketMessage
|
||||
from core.websocket_manager import manager, WebSocketMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
from celery import current_task
|
||||
|
||||
from backend.core.celery_app import celery_app
|
||||
from backend.core.database import SessionLocal
|
||||
from backend.models.task import Task, TaskStatus
|
||||
from backend.models.project import Project, ProjectStatus
|
||||
from backend.repositories.task_repository import TaskRepository
|
||||
from backend.repositories.project_repository import ProjectRepository
|
||||
from core.celery_app import celery_app
|
||||
from core.database import SessionLocal
|
||||
from models.task import Task, TaskStatus
|
||||
from models.project import Project, ProjectStatus
|
||||
from repositories.task_repository import TaskRepository
|
||||
from repositories.project_repository import ProjectRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ from typing import Dict, Any, Optional
|
||||
from celery import current_task
|
||||
from datetime import datetime
|
||||
|
||||
from backend.core.celery_app import celery_app
|
||||
from backend.core.database import SessionLocal
|
||||
from backend.models.task import Task, TaskStatus
|
||||
from backend.repositories.task_repository import TaskRepository
|
||||
from core.celery_app import celery_app
|
||||
from core.database import SessionLocal
|
||||
from models.task import Task, TaskStatus
|
||||
from repositories.task_repository import TaskRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
+66
-77
@@ -1,6 +1,5 @@
|
||||
"""
|
||||
视频处理Celery任务
|
||||
包含WebSocket实时通知
|
||||
"""视频处理Celery任务
|
||||
包含WebSocket实时通知和Pipeline适配器集成
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -8,11 +7,16 @@ import logging
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional
|
||||
from celery import current_task
|
||||
from pathlib import Path
|
||||
|
||||
from backend.core.celery_app import celery_app
|
||||
from backend.services.websocket_notification_service import notification_service
|
||||
from backend.services.processing_service import ProcessingService
|
||||
from backend.core.database import SessionLocal
|
||||
from core.celery_app import celery_app
|
||||
from services.websocket_notification_service import notification_service
|
||||
from services.processing_service import ProcessingService
|
||||
from services.pipeline_adapter import create_pipeline_adapter_sync
|
||||
from core.database import SessionLocal
|
||||
from core.progress_manager import get_progress_manager
|
||||
from models.project import Project, ProjectStatus
|
||||
from models.task import Task, TaskStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,13 +31,14 @@ def run_async_notification(coro):
|
||||
return loop.run_until_complete(coro)
|
||||
|
||||
@celery_app.task(bind=True, name='backend.tasks.processing.process_video_pipeline')
|
||||
def process_video_pipeline(self, project_id: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def process_video_pipeline(self, project_id: int, input_video_path: str, input_srt_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
处理视频流水线任务
|
||||
处理视频流水线任务 - 使用Pipeline适配器
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
config: 处理配置
|
||||
input_video_path: 输入视频路径
|
||||
input_srt_path: 输入SRT路径
|
||||
|
||||
Returns:
|
||||
处理结果
|
||||
@@ -42,80 +47,46 @@ def process_video_pipeline(self, project_id: str, config: Dict[str, Any]) -> Dic
|
||||
logger.info(f"开始处理视频流水线: {project_id}, 任务ID: {task_id}")
|
||||
|
||||
try:
|
||||
# 创建数据库会话
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# 创建任务记录
|
||||
task = Task(
|
||||
name=f"视频处理流水线",
|
||||
description=f"处理项目 {project_id} 的完整视频流水线",
|
||||
type="pipeline",
|
||||
project_id=project_id,
|
||||
celery_task_id=task_id,
|
||||
status=TaskStatus.RUNNING,
|
||||
progress=0,
|
||||
current_step="初始化",
|
||||
total_steps=6
|
||||
)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task) # 确保获取到task.id
|
||||
|
||||
# 发送开始通知
|
||||
run_async_notification(
|
||||
notification_service.send_processing_start(project_id, task_id)
|
||||
)
|
||||
|
||||
# 创建数据库会话
|
||||
db = SessionLocal()
|
||||
# 获取进度管理器
|
||||
progress_manager = get_progress_manager(db)
|
||||
|
||||
try:
|
||||
# 创建处理服务
|
||||
processing_service = ProcessingService(db)
|
||||
# 创建Pipeline适配器,传入task.id以启用进度管理
|
||||
pipeline_adapter = create_pipeline_adapter_sync(db, task.id)
|
||||
|
||||
# 步骤1: 大纲生成 (20%)
|
||||
logger.info("步骤1: 生成大纲")
|
||||
run_async_notification(
|
||||
notification_service.send_processing_progress(project_id, task_id, 20, "生成大纲")
|
||||
)
|
||||
# 执行Pipeline处理
|
||||
result = pipeline_adapter.process_project_sync(project_id, input_video_path, input_srt_path)
|
||||
|
||||
outline_result = processing_service.generate_outline(project_id, config)
|
||||
if not outline_result.get("success"):
|
||||
raise Exception(f"大纲生成失败: {outline_result.get('error')}")
|
||||
|
||||
# 步骤2: 时间轴提取 (40%)
|
||||
logger.info("步骤2: 提取时间轴")
|
||||
run_async_notification(
|
||||
notification_service.send_processing_progress(project_id, task_id, 40, "提取时间轴")
|
||||
)
|
||||
|
||||
timeline_result = processing_service.extract_timeline(project_id, config)
|
||||
if not timeline_result.get("success"):
|
||||
raise Exception(f"时间轴提取失败: {timeline_result.get('error')}")
|
||||
|
||||
# 步骤3: 标题生成 (60%)
|
||||
logger.info("步骤3: 生成标题")
|
||||
run_async_notification(
|
||||
notification_service.send_processing_progress(project_id, task_id, 60, "生成标题")
|
||||
)
|
||||
|
||||
title_result = processing_service.generate_titles(project_id, config)
|
||||
if not title_result.get("success"):
|
||||
raise Exception(f"标题生成失败: {title_result.get('error')}")
|
||||
|
||||
# 步骤4: 视频切片 (80%)
|
||||
logger.info("步骤4: 视频切片")
|
||||
run_async_notification(
|
||||
notification_service.send_processing_progress(project_id, task_id, 80, "视频切片")
|
||||
)
|
||||
|
||||
clip_result = processing_service.extract_clips(project_id, config)
|
||||
if not clip_result.get("success"):
|
||||
raise Exception(f"视频切片失败: {clip_result.get('error')}")
|
||||
|
||||
# 步骤5: 合集生成 (100%)
|
||||
logger.info("步骤5: 生成合集")
|
||||
run_async_notification(
|
||||
notification_service.send_processing_progress(project_id, task_id, 100, "生成合集")
|
||||
)
|
||||
|
||||
collection_result = processing_service.generate_collections(project_id, config)
|
||||
if not collection_result.get("success"):
|
||||
raise Exception(f"合集生成失败: {collection_result.get('error')}")
|
||||
|
||||
# 处理完成
|
||||
result = {
|
||||
"success": True,
|
||||
"project_id": project_id,
|
||||
"task_id": task_id,
|
||||
"outline": outline_result,
|
||||
"timeline": timeline_result,
|
||||
"titles": title_result,
|
||||
"clips": clip_result,
|
||||
"collections": collection_result,
|
||||
"message": "视频处理流水线完成"
|
||||
}
|
||||
# 更新任务状态为完成
|
||||
task.status = TaskStatus.COMPLETED
|
||||
task.progress = 100
|
||||
task.current_step = "处理完成"
|
||||
task.result = result
|
||||
db.commit()
|
||||
|
||||
# 发送完成通知
|
||||
run_async_notification(
|
||||
@@ -123,7 +94,13 @@ def process_video_pipeline(self, project_id: str, config: Dict[str, Any]) -> Dic
|
||||
)
|
||||
|
||||
logger.info(f"视频流水线处理完成: {project_id}")
|
||||
return result
|
||||
return {
|
||||
"success": True,
|
||||
"project_id": project_id,
|
||||
"task_id": task_id,
|
||||
"result": result,
|
||||
"message": "视频处理流水线完成"
|
||||
}
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@@ -132,6 +109,18 @@ def process_video_pipeline(self, project_id: str, config: Dict[str, Any]) -> Dic
|
||||
error_msg = f"视频流水线处理失败: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
|
||||
# 更新任务状态为失败
|
||||
try:
|
||||
db = SessionLocal()
|
||||
task = db.query(Task).filter(Task.celery_task_id == task_id).first()
|
||||
if task:
|
||||
task.status = TaskStatus.FAILED
|
||||
task.error_message = error_msg
|
||||
db.commit()
|
||||
db.close()
|
||||
except Exception as db_error:
|
||||
logger.error(f"更新任务状态失败: {str(db_error)}")
|
||||
|
||||
# 发送错误通知
|
||||
run_async_notification(
|
||||
notification_service.send_processing_error(project_id, task_id, error_msg)
|
||||
|
||||
@@ -8,12 +8,12 @@ from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
from celery import current_task
|
||||
|
||||
from backend.core.celery_app import celery_app
|
||||
from backend.core.database import SessionLocal
|
||||
from backend.models.clip import Clip, ClipStatus
|
||||
from backend.models.collection import Collection, CollectionStatus
|
||||
from backend.repositories.clip_repository import ClipRepository
|
||||
from backend.repositories.collection_repository import CollectionRepository
|
||||
from core.celery_app import celery_app
|
||||
from core.database import SessionLocal
|
||||
from models.clip import Clip, ClipStatus
|
||||
from models.collection import Collection, CollectionStatus
|
||||
from repositories.clip_repository import ClipRepository
|
||||
from repositories.collection_repository import CollectionRepository
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
1
|
||||
00:00:01,000 --> 00:00:05,000
|
||||
这是一个测试字幕文件
|
||||
|
||||
2
|
||||
00:00:05,000 --> 00:00:10,000
|
||||
用于测试视频处理功能
|
||||
|
||||
3
|
||||
00:00:10,000 --> 00:00:15,000
|
||||
包含多个字幕片段
|
||||
|
||||
4
|
||||
00:00:15,000 --> 00:00:20,000
|
||||
用于验证处理流程
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Celery任务测试脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from core.celery_app import celery_app
|
||||
from tasks.processing import process_video_pipeline, process_single_step
|
||||
from tasks.maintenance import health_check
|
||||
|
||||
def test_celery_tasks():
|
||||
"""测试Celery任务"""
|
||||
print("🧪 开始测试Celery任务...")
|
||||
|
||||
# 设置测试环境变量
|
||||
os.environ['DASHSCOPE_API_KEY'] = 'test_api_key'
|
||||
|
||||
try:
|
||||
# 测试健康检查任务
|
||||
print("1. 测试健康检查任务...")
|
||||
result = health_check.delay()
|
||||
print(f" 结果: {result.get()}")
|
||||
|
||||
# 测试视频处理任务(使用测试SRT文件)
|
||||
print("2. 测试视频处理任务...")
|
||||
srt_path = Path(__file__).parent / "test.srt"
|
||||
result = process_video_pipeline.delay("test_project", str(srt_path))
|
||||
print(f" 结果: {result.get()}")
|
||||
|
||||
# 测试单个步骤任务
|
||||
print("3. 测试单个步骤任务...")
|
||||
result = process_single_step.delay("test_project", "step1_outline", str(srt_path))
|
||||
print(f" 结果: {result.get()}")
|
||||
|
||||
print("✅ 所有Celery任务测试通过!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Celery任务测试失败: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_celery_tasks()
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
前后端联调测试脚本
|
||||
测试WebSocket连接和消息发送
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from services.websocket_notification_service import notification_service
|
||||
|
||||
async def test_frontend_integration():
|
||||
"""测试前后端联调"""
|
||||
print("🎯 前后端联调测试")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# 1. 发送连接成功通知
|
||||
print("1. 📢 发送连接成功通知...")
|
||||
await notification_service.send_system_notification(
|
||||
"frontend_connected",
|
||||
"前端连接成功",
|
||||
"前端已成功连接到WebSocket服务",
|
||||
"success"
|
||||
)
|
||||
print(" ✅ 连接通知已发送")
|
||||
|
||||
# 2. 模拟任务开始
|
||||
print("\n2. 📋 模拟任务开始...")
|
||||
await notification_service.send_task_update(
|
||||
task_id="test-task-001",
|
||||
status="started",
|
||||
progress=0,
|
||||
message="测试任务已开始"
|
||||
)
|
||||
print(" ✅ 任务开始通知已发送")
|
||||
|
||||
# 3. 模拟任务进度
|
||||
print("\n3. 📈 模拟任务进度...")
|
||||
for i in range(1, 6):
|
||||
progress = i * 20
|
||||
await notification_service.send_task_update(
|
||||
task_id="test-task-001",
|
||||
status="processing",
|
||||
progress=progress,
|
||||
message=f"测试任务进度: {progress}%"
|
||||
)
|
||||
print(f" 📊 进度 {progress}% 已发送")
|
||||
await asyncio.sleep(2) # 等待2秒
|
||||
|
||||
# 4. 模拟任务完成
|
||||
print("\n4. ✅ 模拟任务完成...")
|
||||
await notification_service.send_task_update(
|
||||
task_id="test-task-001",
|
||||
status="completed",
|
||||
progress=100,
|
||||
message="测试任务已完成"
|
||||
)
|
||||
print(" ✅ 任务完成通知已发送")
|
||||
|
||||
# 5. 发送系统通知
|
||||
print("\n5. 📢 发送系统通知...")
|
||||
await notification_service.send_system_notification(
|
||||
"test_complete",
|
||||
"测试完成",
|
||||
"前后端联调测试已完成",
|
||||
"success"
|
||||
)
|
||||
print(" ✅ 系统通知已发送")
|
||||
|
||||
print("\n🎉 前后端联调测试完成!")
|
||||
print("💡 提示:")
|
||||
print(" - 前端应该能看到实时通知")
|
||||
print(" - 任务进度应该实时更新")
|
||||
print(" - WebSocket连接状态应该正常")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试失败: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(test_frontend_integration())
|
||||
exit(0 if success else 1)
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试WebSocket通知的防重复和节流功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import websockets
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def test_notification_throttling():
|
||||
"""测试通知防重复和节流功能"""
|
||||
|
||||
uri = "ws://localhost:8000/api/v1/ws/test-user"
|
||||
|
||||
try:
|
||||
async with websockets.connect(uri) as websocket:
|
||||
logger.info("✅ 已连接到WebSocket服务器")
|
||||
|
||||
# 1. 发送连接成功消息
|
||||
welcome_message = {
|
||||
"type": "system_notification",
|
||||
"notification_type": "connection",
|
||||
"title": "连接成功",
|
||||
"message": "测试用户已连接",
|
||||
"level": "success",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
await websocket.send(json.dumps(welcome_message))
|
||||
logger.info("📢 发送连接成功通知")
|
||||
|
||||
# 2. 快速发送多个相同的任务进度消息(应该被节流)
|
||||
for i in range(5):
|
||||
progress_message = {
|
||||
"type": "task_update",
|
||||
"task_id": "test-task-001",
|
||||
"status": "processing",
|
||||
"progress": 20,
|
||||
"message": "正在处理...",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
await websocket.send(json.dumps(progress_message))
|
||||
logger.info(f"📊 发送任务进度消息 {i+1}/5")
|
||||
await asyncio.sleep(0.5) # 快速发送
|
||||
|
||||
# 3. 发送任务完成消息(应该显示通知)
|
||||
complete_message = {
|
||||
"type": "task_update",
|
||||
"task_id": "test-task-001",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "任务已完成",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
await websocket.send(json.dumps(complete_message))
|
||||
logger.info("✅ 发送任务完成消息")
|
||||
|
||||
# 4. 再次发送相同的完成消息(应该被防重复)
|
||||
await websocket.send(json.dumps(complete_message))
|
||||
logger.info("🔄 重复发送任务完成消息(应该被忽略)")
|
||||
|
||||
# 5. 等待一段时间后发送另一个完成消息(应该被节流)
|
||||
await asyncio.sleep(1)
|
||||
another_complete = {
|
||||
"type": "task_update",
|
||||
"task_id": "test-task-002",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"message": "另一个任务已完成",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
await websocket.send(json.dumps(another_complete))
|
||||
logger.info("✅ 发送另一个任务完成消息")
|
||||
|
||||
# 6. 发送错误消息
|
||||
error_message = {
|
||||
"type": "error_notification",
|
||||
"error_type": "test_error",
|
||||
"error_message": "这是一个测试错误",
|
||||
"details": {"test": True},
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
await websocket.send(json.dumps(error_message))
|
||||
logger.info("❌ 发送错误消息")
|
||||
|
||||
logger.info("🎉 测试完成!")
|
||||
logger.info("💡 前端应该只显示少量重要通知,而不是疯狂弹窗")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"测试失败: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_notification_throttling())
|
||||
@@ -1,244 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WebSocket客户端测试脚本
|
||||
用于测试WebSocket连接和消息接收
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WebSocketClient:
|
||||
"""WebSocket客户端"""
|
||||
|
||||
def __init__(self, user_id: str, uri: str = "ws://localhost:8000/api/v1/ws/"):
|
||||
self.user_id = user_id
|
||||
self.uri = f"{uri}{user_id}"
|
||||
self.websocket = None
|
||||
self.connected = False
|
||||
|
||||
async def connect(self):
|
||||
"""连接到WebSocket服务器"""
|
||||
try:
|
||||
self.websocket = await websockets.connect(self.uri)
|
||||
self.connected = True
|
||||
logger.info(f"用户 {self.user_id} 已连接到WebSocket服务器")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"连接失败: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self):
|
||||
"""断开连接"""
|
||||
if self.websocket:
|
||||
await self.websocket.close()
|
||||
self.connected = False
|
||||
logger.info(f"用户 {self.user_id} 已断开连接")
|
||||
|
||||
async def send_message(self, message: dict):
|
||||
"""发送消息"""
|
||||
if not self.connected:
|
||||
logger.error("未连接到服务器")
|
||||
return False
|
||||
|
||||
try:
|
||||
await self.websocket.send(json.dumps(message))
|
||||
logger.info(f"发送消息: {message}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息失败: {e}")
|
||||
return False
|
||||
|
||||
async def subscribe_to_topic(self, topic: str):
|
||||
"""订阅主题"""
|
||||
message = {
|
||||
"type": "subscribe",
|
||||
"topic": topic
|
||||
}
|
||||
return await self.send_message(message)
|
||||
|
||||
async def unsubscribe_from_topic(self, topic: str):
|
||||
"""取消订阅主题"""
|
||||
message = {
|
||||
"type": "unsubscribe",
|
||||
"topic": topic
|
||||
}
|
||||
return await self.send_message(message)
|
||||
|
||||
async def ping(self):
|
||||
"""发送心跳"""
|
||||
message = {
|
||||
"type": "ping"
|
||||
}
|
||||
return await self.send_message(message)
|
||||
|
||||
async def get_status(self):
|
||||
"""获取连接状态"""
|
||||
message = {
|
||||
"type": "get_status"
|
||||
}
|
||||
return await self.send_message(message)
|
||||
|
||||
async def listen_for_messages(self, duration: int = 30):
|
||||
"""监听消息"""
|
||||
if not self.connected:
|
||||
logger.error("未连接到服务器")
|
||||
return
|
||||
|
||||
logger.info(f"开始监听消息,持续 {duration} 秒...")
|
||||
start_time = datetime.now()
|
||||
|
||||
try:
|
||||
while self.connected:
|
||||
try:
|
||||
# 设置超时
|
||||
message = await asyncio.wait_for(
|
||||
self.websocket.recv(),
|
||||
timeout=1.0
|
||||
)
|
||||
|
||||
# 解析消息
|
||||
data = json.loads(message)
|
||||
await self.handle_message(data)
|
||||
|
||||
# 检查是否超时
|
||||
if (datetime.now() - start_time).seconds >= duration:
|
||||
logger.info("监听时间已到,停止监听")
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# 超时继续监听
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"接收消息失败: {e}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"监听消息时出错: {e}")
|
||||
|
||||
async def handle_message(self, data: dict):
|
||||
"""处理接收到的消息"""
|
||||
message_type = data.get("type", "unknown")
|
||||
timestamp = data.get("timestamp", "")
|
||||
|
||||
print(f"\n📨 收到消息 [{timestamp}]:")
|
||||
print(f" 类型: {message_type}")
|
||||
|
||||
if message_type == "task_update":
|
||||
task_id = data.get("task_id", "")
|
||||
status = data.get("status", "")
|
||||
progress = data.get("progress", 0)
|
||||
message = data.get("message", "")
|
||||
print(f" 📋 任务: {task_id}")
|
||||
print(f" 📈 状态: {status}")
|
||||
print(f" 📊 进度: {progress}%")
|
||||
print(f" 💬 消息: {message}")
|
||||
|
||||
elif message_type == "project_update":
|
||||
project_id = data.get("project_id", "")
|
||||
status = data.get("status", "")
|
||||
progress = data.get("progress", 0)
|
||||
message = data.get("message", "")
|
||||
print(f" 📁 项目: {project_id}")
|
||||
print(f" 📈 状态: {status}")
|
||||
print(f" 📊 进度: {progress}%")
|
||||
print(f" 💬 消息: {message}")
|
||||
|
||||
elif message_type == "system_notification":
|
||||
title = data.get("title", "")
|
||||
message = data.get("message", "")
|
||||
level = data.get("level", "info")
|
||||
print(f" 📢 标题: {title}")
|
||||
print(f" 💬 消息: {message}")
|
||||
print(f" 🎯 级别: {level}")
|
||||
|
||||
elif message_type == "error_notification":
|
||||
error_type = data.get("error_type", "")
|
||||
error_message = data.get("error_message", "")
|
||||
print(f" ⚠️ 错误类型: {error_type}")
|
||||
print(f" 💬 错误消息: {error_message}")
|
||||
|
||||
elif message_type == "pong":
|
||||
print(f" 🏓 心跳响应")
|
||||
|
||||
elif message_type == "status":
|
||||
connected = data.get("connected", False)
|
||||
subscriptions = data.get("subscriptions", [])
|
||||
total_connections = data.get("total_connections", 0)
|
||||
print(f" 🔗 连接状态: {'已连接' if connected else '未连接'}")
|
||||
print(f" 📋 订阅主题: {subscriptions}")
|
||||
print(f" 👥 总连接数: {total_connections}")
|
||||
|
||||
else:
|
||||
print(f" 📄 原始数据: {data}")
|
||||
|
||||
async def demo_websocket_client():
|
||||
"""演示WebSocket客户端功能"""
|
||||
print("🎯 WebSocket客户端演示")
|
||||
print("=" * 50)
|
||||
|
||||
# 创建客户端
|
||||
client = WebSocketClient("demo-user-001")
|
||||
|
||||
try:
|
||||
# 1. 连接到服务器
|
||||
print("1. 🔗 连接到WebSocket服务器...")
|
||||
if not await client.connect():
|
||||
print(" ❌ 连接失败")
|
||||
return False
|
||||
print(" ✅ 连接成功")
|
||||
|
||||
# 2. 获取连接状态
|
||||
print("\n2. 📊 获取连接状态...")
|
||||
await client.get_status()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 3. 订阅主题
|
||||
print("\n3. 📋 订阅主题...")
|
||||
topics = ["task_demo-task-001", "project_demo-project-001", "system"]
|
||||
for topic in topics:
|
||||
await client.subscribe_to_topic(topic)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 4. 发送心跳
|
||||
print("\n4. 🏓 发送心跳...")
|
||||
await client.ping()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 5. 监听消息
|
||||
print("\n5. 📨 开始监听消息...")
|
||||
print(" 💡 提示:现在可以运行 demo_websocket.py 来发送测试消息")
|
||||
print(" ⏰ 监听将持续30秒...")
|
||||
|
||||
# 创建监听任务
|
||||
listen_task = asyncio.create_task(client.listen_for_messages(30))
|
||||
|
||||
# 等待监听完成
|
||||
await listen_task
|
||||
|
||||
# 6. 取消订阅
|
||||
print("\n6. 📋 取消订阅...")
|
||||
for topic in topics:
|
||||
await client.unsubscribe_from_topic(topic)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 7. 断开连接
|
||||
print("\n7. 🔌 断开连接...")
|
||||
await client.disconnect()
|
||||
|
||||
print("\n🎉 WebSocket客户端演示完成!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 演示失败: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = asyncio.run(demo_websocket_client())
|
||||
exit(0 if success else 1)
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
修复backend目录下的导入路径
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
def fix_imports_in_file(file_path):
|
||||
"""修复单个文件中的导入路径"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 替换 from backend. 为相对导入
|
||||
original_content = content
|
||||
content = re.sub(r'from backend\.', 'from ', content)
|
||||
|
||||
if content != original_content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"Fixed: {file_path}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
backend_dir = '/Users/zhoukk/autoclip/backend'
|
||||
fixed_count = 0
|
||||
|
||||
for root, dirs, files in os.walk(backend_dir):
|
||||
for file in files:
|
||||
if file.endswith('.py'):
|
||||
file_path = os.path.join(root, file)
|
||||
if fix_imports_in_file(file_path):
|
||||
fixed_count += 1
|
||||
|
||||
print(f"\nTotal files fixed: {fixed_count}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+2
-2
@@ -835,8 +835,8 @@ import {
|
||||
create,
|
||||
getTwoToneColor,
|
||||
setTwoToneColor
|
||||
} from "./chunk-HWALLW6A.js";
|
||||
import "./chunk-4JNVF2QO.js";
|
||||
} from "./chunk-X7BPSMZA.js";
|
||||
import "./chunk-66ZF2VOI.js";
|
||||
import "./chunk-QJTFJ6OV.js";
|
||||
import "./chunk-V4OQ3NZ2.js";
|
||||
export {
|
||||
|
||||
+47
-35
@@ -2,112 +2,133 @@
|
||||
"hash": "9d37e763",
|
||||
"configHash": "8de57516",
|
||||
"lockfileHash": "0fe605fb",
|
||||
"browserHash": "dc774958",
|
||||
"browserHash": "1cf0d51b",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "e29de45d",
|
||||
"fileHash": "f7ce770a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "df25dae6",
|
||||
"fileHash": "f949a0db",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "1b7b46ff",
|
||||
"fileHash": "c56bee2a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "00d6a67c",
|
||||
"fileHash": "4a65b026",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@ant-design/icons": {
|
||||
"src": "../../@ant-design/icons/es/index.js",
|
||||
"file": "@ant-design_icons.js",
|
||||
"fileHash": "81655c32",
|
||||
"fileHash": "b3cb3c25",
|
||||
"needsInterop": false
|
||||
},
|
||||
"antd": {
|
||||
"src": "../../antd/es/index.js",
|
||||
"file": "antd.js",
|
||||
"fileHash": "93b83763",
|
||||
"fileHash": "df84fd8a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"antd/locale/zh_CN": {
|
||||
"src": "../../antd/locale/zh_CN.js",
|
||||
"file": "antd_locale_zh_CN.js",
|
||||
"fileHash": "b904130e",
|
||||
"fileHash": "418c17f2",
|
||||
"needsInterop": true
|
||||
},
|
||||
"axios": {
|
||||
"src": "../../axios/index.js",
|
||||
"file": "axios.js",
|
||||
"fileHash": "5d2b9067",
|
||||
"fileHash": "31b533d9",
|
||||
"needsInterop": false
|
||||
},
|
||||
"dayjs": {
|
||||
"src": "../../dayjs/dayjs.min.js",
|
||||
"file": "dayjs.js",
|
||||
"fileHash": "77fdcba2",
|
||||
"fileHash": "9b4d2933",
|
||||
"needsInterop": true
|
||||
},
|
||||
"dayjs/locale/zh-cn": {
|
||||
"src": "../../dayjs/locale/zh-cn.js",
|
||||
"file": "dayjs_locale_zh-cn.js",
|
||||
"fileHash": "3cdee437",
|
||||
"fileHash": "a0518c78",
|
||||
"needsInterop": true
|
||||
},
|
||||
"dayjs/plugin/relativeTime": {
|
||||
"src": "../../dayjs/plugin/relativeTime.js",
|
||||
"file": "dayjs_plugin_relativeTime.js",
|
||||
"fileHash": "e9462706",
|
||||
"fileHash": "0b5805c9",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-beautiful-dnd": {
|
||||
"src": "../../react-beautiful-dnd/dist/react-beautiful-dnd.esm.js",
|
||||
"file": "react-beautiful-dnd.js",
|
||||
"fileHash": "b2d5abf1",
|
||||
"fileHash": "9d54abab",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "09ddd4c1",
|
||||
"fileHash": "7a4b6fa8",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dropzone": {
|
||||
"src": "../../react-dropzone/dist/es/index.js",
|
||||
"file": "react-dropzone.js",
|
||||
"fileHash": "cea2e1ae",
|
||||
"fileHash": "2873c972",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-player": {
|
||||
"src": "../../react-player/lib/index.js",
|
||||
"file": "react-player.js",
|
||||
"fileHash": "1a73b58d",
|
||||
"fileHash": "e7b23ed2",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.js",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "71572fe7",
|
||||
"fileHash": "eca96e1d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "a982e874",
|
||||
"fileHash": "e892f14b",
|
||||
"needsInterop": false
|
||||
},
|
||||
"dayjs/plugin/timezone": {
|
||||
"src": "../../dayjs/plugin/timezone.js",
|
||||
"file": "dayjs_plugin_timezone.js",
|
||||
"fileHash": "c573eedd",
|
||||
"needsInterop": true
|
||||
},
|
||||
"dayjs/plugin/utc": {
|
||||
"src": "../../dayjs/plugin/utc.js",
|
||||
"file": "dayjs_plugin_utc.js",
|
||||
"fileHash": "1de96265",
|
||||
"needsInterop": true
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"Preview-TKE3YBNZ": {
|
||||
"file": "Preview-TKE3YBNZ.js"
|
||||
},
|
||||
"Streamable-S7PKL4XG": {
|
||||
"file": "Streamable-S7PKL4XG.js"
|
||||
},
|
||||
"Wistia-Q2EBW457": {
|
||||
"file": "Wistia-Q2EBW457.js"
|
||||
},
|
||||
"Twitch-26ES2YRZ": {
|
||||
"file": "Twitch-26ES2YRZ.js"
|
||||
},
|
||||
@@ -126,9 +147,6 @@
|
||||
"FilePlayer-MNM5SGEG": {
|
||||
"file": "FilePlayer-MNM5SGEG.js"
|
||||
},
|
||||
"Preview-TKE3YBNZ": {
|
||||
"file": "Preview-TKE3YBNZ.js"
|
||||
},
|
||||
"YouTube-SBPGO6SV": {
|
||||
"file": "YouTube-SBPGO6SV.js"
|
||||
},
|
||||
@@ -144,11 +162,11 @@
|
||||
"Facebook-2SVAHVW5": {
|
||||
"file": "Facebook-2SVAHVW5.js"
|
||||
},
|
||||
"Streamable-S7PKL4XG": {
|
||||
"file": "Streamable-S7PKL4XG.js"
|
||||
"chunk-WT22ZCGG": {
|
||||
"file": "chunk-WT22ZCGG.js"
|
||||
},
|
||||
"Wistia-Q2EBW457": {
|
||||
"file": "Wistia-Q2EBW457.js"
|
||||
"chunk-4FTWOKSW": {
|
||||
"file": "chunk-4FTWOKSW.js"
|
||||
},
|
||||
"chunk-XQD3CSF6": {
|
||||
"file": "chunk-XQD3CSF6.js"
|
||||
@@ -159,20 +177,14 @@
|
||||
"chunk-7TYQWO6A": {
|
||||
"file": "chunk-7TYQWO6A.js"
|
||||
},
|
||||
"chunk-4FTWOKSW": {
|
||||
"file": "chunk-4FTWOKSW.js"
|
||||
},
|
||||
"chunk-GKJBSOWT": {
|
||||
"file": "chunk-GKJBSOWT.js"
|
||||
},
|
||||
"chunk-WT22ZCGG": {
|
||||
"file": "chunk-WT22ZCGG.js"
|
||||
"chunk-X7BPSMZA": {
|
||||
"file": "chunk-X7BPSMZA.js"
|
||||
},
|
||||
"chunk-HWALLW6A": {
|
||||
"file": "chunk-HWALLW6A.js"
|
||||
},
|
||||
"chunk-4JNVF2QO": {
|
||||
"file": "chunk-4JNVF2QO.js"
|
||||
"chunk-66ZF2VOI": {
|
||||
"file": "chunk-66ZF2VOI.js"
|
||||
},
|
||||
"chunk-QJTFJ6OV": {
|
||||
"file": "chunk-QJTFJ6OV.js"
|
||||
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
import {
|
||||
require_dayjs_min
|
||||
} from "./chunk-WT22ZCGG.js";
|
||||
import {
|
||||
_setPrototypeOf
|
||||
} from "./chunk-4FTWOKSW.js";
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-GKJBSOWT.js";
|
||||
import {
|
||||
require_dayjs_min
|
||||
} from "./chunk-WT22ZCGG.js";
|
||||
import {
|
||||
BarsOutlined_default,
|
||||
CalendarOutlined_default,
|
||||
@@ -90,14 +90,14 @@ import {
|
||||
useMemo,
|
||||
warning,
|
||||
warning_default
|
||||
} from "./chunk-HWALLW6A.js";
|
||||
} from "./chunk-X7BPSMZA.js";
|
||||
import {
|
||||
_defineProperty,
|
||||
_extends,
|
||||
_objectSpread2,
|
||||
_typeof,
|
||||
toPropertyKey
|
||||
} from "./chunk-4JNVF2QO.js";
|
||||
} from "./chunk-66ZF2VOI.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-QJTFJ6OV.js";
|
||||
|
||||
Generated
Vendored
+14
-14
@@ -47,6 +47,17 @@ function _defineProperty(e, r, t) {
|
||||
}) : e[r] = t, e;
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js
|
||||
function _objectWithoutPropertiesLoose(r, e) {
|
||||
if (null == r) return {};
|
||||
var t = {};
|
||||
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
|
||||
if (-1 !== e.indexOf(n)) continue;
|
||||
t[n] = r[n];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/objectSpread2.js
|
||||
function ownKeys(e, r) {
|
||||
var t = Object.keys(e);
|
||||
@@ -70,23 +81,12 @@ function _objectSpread2(e) {
|
||||
return e;
|
||||
}
|
||||
|
||||
// node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js
|
||||
function _objectWithoutPropertiesLoose(r, e) {
|
||||
if (null == r) return {};
|
||||
var t = {};
|
||||
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
|
||||
if (-1 !== e.indexOf(n)) continue;
|
||||
t[n] = r[n];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
export {
|
||||
_extends,
|
||||
_typeof,
|
||||
toPropertyKey,
|
||||
_defineProperty,
|
||||
_objectSpread2,
|
||||
_objectWithoutPropertiesLoose
|
||||
_objectWithoutPropertiesLoose,
|
||||
_objectSpread2
|
||||
};
|
||||
//# sourceMappingURL=chunk-4JNVF2QO.js.map
|
||||
//# sourceMappingURL=chunk-66ZF2VOI.js.map
|
||||
Generated
Vendored
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../@babel/runtime/helpers/esm/extends.js", "../../@babel/runtime/helpers/esm/typeof.js", "../../@babel/runtime/helpers/esm/toPrimitive.js", "../../@babel/runtime/helpers/esm/toPropertyKey.js", "../../@babel/runtime/helpers/esm/defineProperty.js", "../../@babel/runtime/helpers/esm/objectSpread2.js", "../../@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js"],
|
||||
"sourcesContent": ["function _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\nexport { _extends as default };", "function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };", "import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };", "import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };", "import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };", "import defineProperty from \"./defineProperty.js\";\nfunction ownKeys(e, r) {\n var t = Object.keys(e);\n if (Object.getOwnPropertySymbols) {\n var o = Object.getOwnPropertySymbols(e);\n r && (o = o.filter(function (r) {\n return Object.getOwnPropertyDescriptor(e, r).enumerable;\n })), t.push.apply(t, o);\n }\n return t;\n}\nfunction _objectSpread2(e) {\n for (var r = 1; r < arguments.length; r++) {\n var t = null != arguments[r] ? arguments[r] : {};\n r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {\n defineProperty(e, r, t[r]);\n }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {\n Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));\n });\n }\n return e;\n}\nexport { _objectSpread2 as default };", "function _objectWithoutPropertiesLoose(r, e) {\n if (null == r) return {};\n var t = {};\n for (var n in r) if ({}.hasOwnProperty.call(r, n)) {\n if (-1 !== e.indexOf(n)) continue;\n t[n] = r[n];\n }\n return t;\n}\nexport { _objectWithoutPropertiesLoose as default };"],
|
||||
"mappings": "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",
|
||||
"sources": ["../../@babel/runtime/helpers/esm/extends.js", "../../@babel/runtime/helpers/esm/typeof.js", "../../@babel/runtime/helpers/esm/toPrimitive.js", "../../@babel/runtime/helpers/esm/toPropertyKey.js", "../../@babel/runtime/helpers/esm/defineProperty.js", "../../@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js", "../../@babel/runtime/helpers/esm/objectSpread2.js"],
|
||||
"sourcesContent": ["function _extends() {\n return _extends = Object.assign ? Object.assign.bind() : function (n) {\n for (var e = 1; e < arguments.length; e++) {\n var t = arguments[e];\n for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);\n }\n return n;\n }, _extends.apply(null, arguments);\n}\nexport { _extends as default };", "function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };", "import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };", "import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };", "import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };", "function _objectWithoutPropertiesLoose(r, e) {\n if (null == r) return {};\n var t = {};\n for (var n in r) if ({}.hasOwnProperty.call(r, n)) {\n if (-1 !== e.indexOf(n)) continue;\n t[n] = r[n];\n }\n return t;\n}\nexport { _objectWithoutPropertiesLoose as default };", "import defineProperty from \"./defineProperty.js\";\nfunction ownKeys(e, r) {\n var t = Object.keys(e);\n if (Object.getOwnPropertySymbols) {\n var o = Object.getOwnPropertySymbols(e);\n r && (o = o.filter(function (r) {\n return Object.getOwnPropertyDescriptor(e, r).enumerable;\n })), t.push.apply(t, o);\n }\n return t;\n}\nfunction _objectSpread2(e) {\n for (var r = 1; r < arguments.length; r++) {\n var t = null != arguments[r] ? arguments[r] : {};\n r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {\n defineProperty(e, r, t[r]);\n }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {\n Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));\n });\n }\n return e;\n}\nexport { _objectSpread2 as default };"],
|
||||
"mappings": "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",
|
||||
"names": ["o", "r"]
|
||||
}
|
||||
Generated
Vendored
+50
-50
@@ -4,7 +4,7 @@ import {
|
||||
_objectSpread2,
|
||||
_objectWithoutPropertiesLoose,
|
||||
_typeof
|
||||
} from "./chunk-4JNVF2QO.js";
|
||||
} from "./chunk-66ZF2VOI.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-QJTFJ6OV.js";
|
||||
@@ -18535,68 +18535,30 @@ function create() {
|
||||
var IconProvider = Context_default.Provider;
|
||||
|
||||
export {
|
||||
require_classnames,
|
||||
isFragment,
|
||||
warning,
|
||||
noteOnce,
|
||||
warning_default,
|
||||
useMemo,
|
||||
fillRef,
|
||||
composeRef,
|
||||
useComposeRef,
|
||||
supportRef,
|
||||
supportNodeRef,
|
||||
getNodeRef,
|
||||
Context_default,
|
||||
_arrayWithHoles,
|
||||
_arrayLikeToArray,
|
||||
_unsupportedIterableToArray,
|
||||
_arrayWithHoles,
|
||||
_nonIterableRest,
|
||||
_slicedToArray,
|
||||
canUseDom,
|
||||
contains,
|
||||
removeCSS,
|
||||
updateCSS,
|
||||
_objectWithoutProperties,
|
||||
Context_default,
|
||||
require_classnames,
|
||||
FastColor,
|
||||
generate,
|
||||
presetPrimaryColors,
|
||||
gold,
|
||||
blue,
|
||||
presetPalettes,
|
||||
canUseDom,
|
||||
contains,
|
||||
removeCSS,
|
||||
updateCSS,
|
||||
getShadowRoot,
|
||||
warning,
|
||||
noteOnce,
|
||||
warning_default,
|
||||
setTwoToneColor,
|
||||
getTwoToneColor,
|
||||
CheckCircleFilled_default2 as CheckCircleFilled_default,
|
||||
CloseCircleFilled_default2 as CloseCircleFilled_default,
|
||||
CloseOutlined_default2 as CloseOutlined_default,
|
||||
ExclamationCircleFilled_default2 as ExclamationCircleFilled_default,
|
||||
InfoCircleFilled_default2 as InfoCircleFilled_default,
|
||||
LoadingOutlined_default2 as LoadingOutlined_default,
|
||||
RightOutlined_default2 as RightOutlined_default,
|
||||
CheckOutlined_default2 as CheckOutlined_default,
|
||||
DownOutlined_default2 as DownOutlined_default,
|
||||
SearchOutlined_default2 as SearchOutlined_default,
|
||||
VerticalAlignTopOutlined_default2 as VerticalAlignTopOutlined_default,
|
||||
LeftOutlined_default2 as LeftOutlined_default,
|
||||
BarsOutlined_default2 as BarsOutlined_default,
|
||||
EllipsisOutlined_default2 as EllipsisOutlined_default,
|
||||
PlusOutlined_default2 as PlusOutlined_default,
|
||||
UpOutlined_default2 as UpOutlined_default,
|
||||
CalendarOutlined_default2 as CalendarOutlined_default,
|
||||
ClockCircleOutlined_default2 as ClockCircleOutlined_default,
|
||||
SwapRightOutlined_default2 as SwapRightOutlined_default,
|
||||
FileTextOutlined_default2 as FileTextOutlined_default,
|
||||
QuestionCircleOutlined_default2 as QuestionCircleOutlined_default,
|
||||
EyeOutlined_default2 as EyeOutlined_default,
|
||||
RotateLeftOutlined_default2 as RotateLeftOutlined_default,
|
||||
RotateRightOutlined_default2 as RotateRightOutlined_default,
|
||||
SwapOutlined_default2 as SwapOutlined_default,
|
||||
ZoomInOutlined_default2 as ZoomInOutlined_default,
|
||||
ZoomOutOutlined_default2 as ZoomOutOutlined_default,
|
||||
EyeInvisibleOutlined_default2 as EyeInvisibleOutlined_default,
|
||||
DoubleLeftOutlined_default2 as DoubleLeftOutlined_default,
|
||||
DoubleRightOutlined_default2 as DoubleRightOutlined_default,
|
||||
AccountBookFilled_default2 as AccountBookFilled_default,
|
||||
AccountBookOutlined_default2 as AccountBookOutlined_default,
|
||||
AccountBookTwoTone_default2 as AccountBookTwoTone_default,
|
||||
@@ -18651,6 +18613,7 @@ export {
|
||||
BankTwoTone_default2 as BankTwoTone_default,
|
||||
BarChartOutlined_default2 as BarChartOutlined_default,
|
||||
BarcodeOutlined_default2 as BarcodeOutlined_default,
|
||||
BarsOutlined_default2 as BarsOutlined_default,
|
||||
BehanceCircleFilled_default2 as BehanceCircleFilled_default,
|
||||
BehanceOutlined_default2 as BehanceOutlined_default,
|
||||
BehanceSquareFilled_default2 as BehanceSquareFilled_default,
|
||||
@@ -18693,6 +18656,7 @@ export {
|
||||
CalculatorOutlined_default2 as CalculatorOutlined_default,
|
||||
CalculatorTwoTone_default2 as CalculatorTwoTone_default,
|
||||
CalendarFilled_default2 as CalendarFilled_default,
|
||||
CalendarOutlined_default2 as CalendarOutlined_default,
|
||||
CalendarTwoTone_default2 as CalendarTwoTone_default,
|
||||
CameraFilled_default2 as CameraFilled_default,
|
||||
CameraOutlined_default2 as CameraOutlined_default,
|
||||
@@ -18711,8 +18675,10 @@ export {
|
||||
CarryOutFilled_default2 as CarryOutFilled_default,
|
||||
CarryOutOutlined_default2 as CarryOutOutlined_default,
|
||||
CarryOutTwoTone_default2 as CarryOutTwoTone_default,
|
||||
CheckCircleFilled_default2 as CheckCircleFilled_default,
|
||||
CheckCircleOutlined_default2 as CheckCircleOutlined_default,
|
||||
CheckCircleTwoTone_default2 as CheckCircleTwoTone_default,
|
||||
CheckOutlined_default2 as CheckOutlined_default,
|
||||
CheckSquareFilled_default2 as CheckSquareFilled_default,
|
||||
CheckSquareOutlined_default2 as CheckSquareOutlined_default,
|
||||
CheckSquareTwoTone_default2 as CheckSquareTwoTone_default,
|
||||
@@ -18725,9 +18691,12 @@ export {
|
||||
CiTwoTone_default2 as CiTwoTone_default,
|
||||
ClearOutlined_default2 as ClearOutlined_default,
|
||||
ClockCircleFilled_default2 as ClockCircleFilled_default,
|
||||
ClockCircleOutlined_default2 as ClockCircleOutlined_default,
|
||||
ClockCircleTwoTone_default2 as ClockCircleTwoTone_default,
|
||||
CloseCircleFilled_default2 as CloseCircleFilled_default,
|
||||
CloseCircleOutlined_default2 as CloseCircleOutlined_default,
|
||||
CloseCircleTwoTone_default2 as CloseCircleTwoTone_default,
|
||||
CloseOutlined_default2 as CloseOutlined_default,
|
||||
CloseSquareFilled_default2 as CloseSquareFilled_default,
|
||||
CloseSquareOutlined_default2 as CloseSquareOutlined_default,
|
||||
CloseSquareTwoTone_default2 as CloseSquareTwoTone_default,
|
||||
@@ -18820,9 +18789,12 @@ export {
|
||||
DollarTwoTone_default2 as DollarTwoTone_default,
|
||||
DotChartOutlined_default2 as DotChartOutlined_default,
|
||||
DotNetOutlined_default2 as DotNetOutlined_default,
|
||||
DoubleLeftOutlined_default2 as DoubleLeftOutlined_default,
|
||||
DoubleRightOutlined_default2 as DoubleRightOutlined_default,
|
||||
DownCircleFilled_default2 as DownCircleFilled_default,
|
||||
DownCircleOutlined_default2 as DownCircleOutlined_default,
|
||||
DownCircleTwoTone_default2 as DownCircleTwoTone_default,
|
||||
DownOutlined_default2 as DownOutlined_default,
|
||||
DownSquareFilled_default2 as DownSquareFilled_default,
|
||||
DownSquareOutlined_default2 as DownSquareOutlined_default,
|
||||
DownSquareTwoTone_default2 as DownSquareTwoTone_default,
|
||||
@@ -18838,6 +18810,7 @@ export {
|
||||
EditFilled_default2 as EditFilled_default,
|
||||
EditOutlined_default2 as EditOutlined_default,
|
||||
EditTwoTone_default2 as EditTwoTone_default,
|
||||
EllipsisOutlined_default2 as EllipsisOutlined_default,
|
||||
EnterOutlined_default2 as EnterOutlined_default,
|
||||
EnvironmentFilled_default2 as EnvironmentFilled_default,
|
||||
EnvironmentOutlined_default2 as EnvironmentOutlined_default,
|
||||
@@ -18848,6 +18821,7 @@ export {
|
||||
EuroOutlined_default2 as EuroOutlined_default,
|
||||
EuroTwoTone_default2 as EuroTwoTone_default,
|
||||
ExceptionOutlined_default2 as ExceptionOutlined_default,
|
||||
ExclamationCircleFilled_default2 as ExclamationCircleFilled_default,
|
||||
ExclamationCircleOutlined_default2 as ExclamationCircleOutlined_default,
|
||||
ExclamationCircleTwoTone_default2 as ExclamationCircleTwoTone_default,
|
||||
ExclamationOutlined_default2 as ExclamationOutlined_default,
|
||||
@@ -18859,7 +18833,9 @@ export {
|
||||
ExportOutlined_default2 as ExportOutlined_default,
|
||||
EyeFilled_default2 as EyeFilled_default,
|
||||
EyeInvisibleFilled_default2 as EyeInvisibleFilled_default,
|
||||
EyeInvisibleOutlined_default2 as EyeInvisibleOutlined_default,
|
||||
EyeInvisibleTwoTone_default2 as EyeInvisibleTwoTone_default,
|
||||
EyeOutlined_default2 as EyeOutlined_default,
|
||||
EyeTwoTone_default2 as EyeTwoTone_default,
|
||||
FacebookFilled_default2 as FacebookFilled_default,
|
||||
FacebookOutlined_default2 as FacebookOutlined_default,
|
||||
@@ -18902,6 +18878,7 @@ export {
|
||||
FileSearchOutlined_default2 as FileSearchOutlined_default,
|
||||
FileSyncOutlined_default2 as FileSyncOutlined_default,
|
||||
FileTextFilled_default2 as FileTextFilled_default,
|
||||
FileTextOutlined_default2 as FileTextOutlined_default,
|
||||
FileTextTwoTone_default2 as FileTextTwoTone_default,
|
||||
FileTwoTone_default2 as FileTwoTone_default,
|
||||
FileUnknownFilled_default2 as FileUnknownFilled_default,
|
||||
@@ -19005,6 +18982,7 @@ export {
|
||||
IeSquareFilled_default2 as IeSquareFilled_default,
|
||||
ImportOutlined_default2 as ImportOutlined_default,
|
||||
InboxOutlined_default2 as InboxOutlined_default,
|
||||
InfoCircleFilled_default2 as InfoCircleFilled_default,
|
||||
InfoCircleOutlined_default2 as InfoCircleOutlined_default,
|
||||
InfoCircleTwoTone_default2 as InfoCircleTwoTone_default,
|
||||
InfoOutlined_default2 as InfoOutlined_default,
|
||||
@@ -19033,6 +19011,7 @@ export {
|
||||
LeftCircleFilled_default2 as LeftCircleFilled_default,
|
||||
LeftCircleOutlined_default2 as LeftCircleOutlined_default,
|
||||
LeftCircleTwoTone_default2 as LeftCircleTwoTone_default,
|
||||
LeftOutlined_default2 as LeftOutlined_default,
|
||||
LeftSquareFilled_default2 as LeftSquareFilled_default,
|
||||
LeftSquareOutlined_default2 as LeftSquareOutlined_default,
|
||||
LeftSquareTwoTone_default2 as LeftSquareTwoTone_default,
|
||||
@@ -19047,6 +19026,7 @@ export {
|
||||
LinkedinOutlined_default2 as LinkedinOutlined_default,
|
||||
LinuxOutlined_default2 as LinuxOutlined_default,
|
||||
Loading3QuartersOutlined_default2 as Loading3QuartersOutlined_default,
|
||||
LoadingOutlined_default2 as LoadingOutlined_default,
|
||||
LockFilled_default2 as LockFilled_default,
|
||||
LockOutlined_default2 as LockOutlined_default,
|
||||
LockTwoTone_default2 as LockTwoTone_default,
|
||||
@@ -19139,6 +19119,7 @@ export {
|
||||
PlusCircleFilled_default2 as PlusCircleFilled_default,
|
||||
PlusCircleOutlined_default2 as PlusCircleOutlined_default,
|
||||
PlusCircleTwoTone_default2 as PlusCircleTwoTone_default,
|
||||
PlusOutlined_default2 as PlusOutlined_default,
|
||||
PlusSquareFilled_default2 as PlusSquareFilled_default,
|
||||
PlusSquareOutlined_default2 as PlusSquareOutlined_default,
|
||||
PlusSquareTwoTone_default2 as PlusSquareTwoTone_default,
|
||||
@@ -19171,6 +19152,7 @@ export {
|
||||
QqSquareFilled_default2 as QqSquareFilled_default,
|
||||
QrcodeOutlined_default2 as QrcodeOutlined_default,
|
||||
QuestionCircleFilled_default2 as QuestionCircleFilled_default,
|
||||
QuestionCircleOutlined_default2 as QuestionCircleOutlined_default,
|
||||
QuestionCircleTwoTone_default2 as QuestionCircleTwoTone_default,
|
||||
QuestionOutlined_default2 as QuestionOutlined_default,
|
||||
RadarChartOutlined_default2 as RadarChartOutlined_default,
|
||||
@@ -19199,6 +19181,7 @@ export {
|
||||
RightCircleFilled_default2 as RightCircleFilled_default,
|
||||
RightCircleOutlined_default2 as RightCircleOutlined_default,
|
||||
RightCircleTwoTone_default2 as RightCircleTwoTone_default,
|
||||
RightOutlined_default2 as RightOutlined_default,
|
||||
RightSquareFilled_default2 as RightSquareFilled_default,
|
||||
RightSquareOutlined_default2 as RightSquareOutlined_default,
|
||||
RightSquareTwoTone_default2 as RightSquareTwoTone_default,
|
||||
@@ -19209,6 +19192,8 @@ export {
|
||||
RocketOutlined_default2 as RocketOutlined_default,
|
||||
RocketTwoTone_default2 as RocketTwoTone_default,
|
||||
RollbackOutlined_default2 as RollbackOutlined_default,
|
||||
RotateLeftOutlined_default2 as RotateLeftOutlined_default,
|
||||
RotateRightOutlined_default2 as RotateRightOutlined_default,
|
||||
RubyOutlined_default2 as RubyOutlined_default,
|
||||
SafetyCertificateFilled_default2 as SafetyCertificateFilled_default,
|
||||
SafetyCertificateOutlined_default2 as SafetyCertificateOutlined_default,
|
||||
@@ -19222,6 +19207,7 @@ export {
|
||||
ScheduleOutlined_default2 as ScheduleOutlined_default,
|
||||
ScheduleTwoTone_default2 as ScheduleTwoTone_default,
|
||||
ScissorOutlined_default2 as ScissorOutlined_default,
|
||||
SearchOutlined_default2 as SearchOutlined_default,
|
||||
SecurityScanFilled_default2 as SecurityScanFilled_default,
|
||||
SecurityScanOutlined_default2 as SecurityScanOutlined_default,
|
||||
SecurityScanTwoTone_default2 as SecurityScanTwoTone_default,
|
||||
@@ -19291,6 +19277,8 @@ export {
|
||||
SunFilled_default2 as SunFilled_default,
|
||||
SunOutlined_default2 as SunOutlined_default,
|
||||
SwapLeftOutlined_default2 as SwapLeftOutlined_default,
|
||||
SwapOutlined_default2 as SwapOutlined_default,
|
||||
SwapRightOutlined_default2 as SwapRightOutlined_default,
|
||||
SwitcherFilled_default2 as SwitcherFilled_default,
|
||||
SwitcherOutlined_default2 as SwitcherOutlined_default,
|
||||
SwitcherTwoTone_default2 as SwitcherTwoTone_default,
|
||||
@@ -19345,6 +19333,7 @@ export {
|
||||
UpCircleFilled_default2 as UpCircleFilled_default,
|
||||
UpCircleOutlined_default2 as UpCircleOutlined_default,
|
||||
UpCircleTwoTone_default2 as UpCircleTwoTone_default,
|
||||
UpOutlined_default2 as UpOutlined_default,
|
||||
UpSquareFilled_default2 as UpSquareFilled_default,
|
||||
UpSquareOutlined_default2 as UpSquareOutlined_default,
|
||||
UpSquareTwoTone_default2 as UpSquareTwoTone_default,
|
||||
@@ -19361,6 +19350,7 @@ export {
|
||||
VerifiedOutlined_default2 as VerifiedOutlined_default,
|
||||
VerticalAlignBottomOutlined_default2 as VerticalAlignBottomOutlined_default,
|
||||
VerticalAlignMiddleOutlined_default2 as VerticalAlignMiddleOutlined_default,
|
||||
VerticalAlignTopOutlined_default2 as VerticalAlignTopOutlined_default,
|
||||
VerticalLeftOutlined_default2 as VerticalLeftOutlined_default,
|
||||
VerticalRightOutlined_default2 as VerticalRightOutlined_default,
|
||||
VideoCameraAddOutlined_default2 as VideoCameraAddOutlined_default,
|
||||
@@ -19398,6 +19388,16 @@ export {
|
||||
ZhihuCircleFilled_default2 as ZhihuCircleFilled_default,
|
||||
ZhihuOutlined_default2 as ZhihuOutlined_default,
|
||||
ZhihuSquareFilled_default2 as ZhihuSquareFilled_default,
|
||||
ZoomInOutlined_default2 as ZoomInOutlined_default,
|
||||
ZoomOutOutlined_default2 as ZoomOutOutlined_default,
|
||||
useMemo,
|
||||
isFragment,
|
||||
fillRef,
|
||||
composeRef,
|
||||
useComposeRef,
|
||||
supportRef,
|
||||
supportNodeRef,
|
||||
getNodeRef,
|
||||
Icon_default,
|
||||
create,
|
||||
IconProvider
|
||||
@@ -19422,4 +19422,4 @@ react-is/cjs/react-is.development.js:
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
*/
|
||||
//# sourceMappingURL=chunk-HWALLW6A.js.map
|
||||
//# sourceMappingURL=chunk-X7BPSMZA.js.map
|
||||
Generated
Vendored
File renamed without changes.
+70
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-V4OQ3NZ2.js";
|
||||
|
||||
// node_modules/dayjs/plugin/timezone.js
|
||||
var require_timezone = __commonJS({
|
||||
"node_modules/dayjs/plugin/timezone.js"(exports, module) {
|
||||
!function(t, e) {
|
||||
"object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs_plugin_timezone = e();
|
||||
}(exports, function() {
|
||||
"use strict";
|
||||
var t = { year: 0, month: 1, day: 2, hour: 3, minute: 4, second: 5 }, e = {};
|
||||
return function(n, i, o) {
|
||||
var r, a = function(t2, n2, i2) {
|
||||
void 0 === i2 && (i2 = {});
|
||||
var o2 = new Date(t2), r2 = function(t3, n3) {
|
||||
void 0 === n3 && (n3 = {});
|
||||
var i3 = n3.timeZoneName || "short", o3 = t3 + "|" + i3, r3 = e[o3];
|
||||
return r3 || (r3 = new Intl.DateTimeFormat("en-US", { hour12: false, timeZone: t3, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZoneName: i3 }), e[o3] = r3), r3;
|
||||
}(n2, i2);
|
||||
return r2.formatToParts(o2);
|
||||
}, u = function(e2, n2) {
|
||||
for (var i2 = a(e2, n2), r2 = [], u2 = 0; u2 < i2.length; u2 += 1) {
|
||||
var f2 = i2[u2], s2 = f2.type, m = f2.value, c = t[s2];
|
||||
c >= 0 && (r2[c] = parseInt(m, 10));
|
||||
}
|
||||
var d = r2[3], l = 24 === d ? 0 : d, h = r2[0] + "-" + r2[1] + "-" + r2[2] + " " + l + ":" + r2[4] + ":" + r2[5] + ":000", v = +e2;
|
||||
return (o.utc(h).valueOf() - (v -= v % 1e3)) / 6e4;
|
||||
}, f = i.prototype;
|
||||
f.tz = function(t2, e2) {
|
||||
void 0 === t2 && (t2 = r);
|
||||
var n2, i2 = this.utcOffset(), a2 = this.toDate(), u2 = a2.toLocaleString("en-US", { timeZone: t2 }), f2 = Math.round((a2 - new Date(u2)) / 1e3 / 60), s2 = 15 * -Math.round(a2.getTimezoneOffset() / 15) - f2;
|
||||
if (!Number(s2)) n2 = this.utcOffset(0, e2);
|
||||
else if (n2 = o(u2, { locale: this.$L }).$set("millisecond", this.$ms).utcOffset(s2, true), e2) {
|
||||
var m = n2.utcOffset();
|
||||
n2 = n2.add(i2 - m, "minute");
|
||||
}
|
||||
return n2.$x.$timezone = t2, n2;
|
||||
}, f.offsetName = function(t2) {
|
||||
var e2 = this.$x.$timezone || o.tz.guess(), n2 = a(this.valueOf(), e2, { timeZoneName: t2 }).find(function(t3) {
|
||||
return "timezonename" === t3.type.toLowerCase();
|
||||
});
|
||||
return n2 && n2.value;
|
||||
};
|
||||
var s = f.startOf;
|
||||
f.startOf = function(t2, e2) {
|
||||
if (!this.$x || !this.$x.$timezone) return s.call(this, t2, e2);
|
||||
var n2 = o(this.format("YYYY-MM-DD HH:mm:ss:SSS"), { locale: this.$L });
|
||||
return s.call(n2, t2, e2).tz(this.$x.$timezone, true);
|
||||
}, o.tz = function(t2, e2, n2) {
|
||||
var i2 = n2 && e2, a2 = n2 || e2 || r, f2 = u(+o(), a2);
|
||||
if ("string" != typeof t2) return o(t2).tz(a2);
|
||||
var s2 = function(t3, e3, n3) {
|
||||
var i3 = t3 - 60 * e3 * 1e3, o2 = u(i3, n3);
|
||||
if (e3 === o2) return [i3, e3];
|
||||
var r2 = u(i3 -= 60 * (o2 - e3) * 1e3, n3);
|
||||
return o2 === r2 ? [i3, o2] : [t3 - 60 * Math.min(o2, r2) * 1e3, Math.max(o2, r2)];
|
||||
}(o.utc(t2, i2).valueOf(), f2, a2), m = s2[0], c = s2[1], d = o(m).utcOffset(c);
|
||||
return d.$x.$timezone = a2, d;
|
||||
}, o.tz.guess = function() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}, o.tz.setDefault = function(t2) {
|
||||
r = t2;
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
export default require_timezone();
|
||||
//# sourceMappingURL=dayjs_plugin_timezone.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../dayjs/plugin/timezone.js"],
|
||||
"sourcesContent": ["!function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e():\"function\"==typeof define&&define.amd?define(e):(t=\"undefined\"!=typeof globalThis?globalThis:t||self).dayjs_plugin_timezone=e()}(this,(function(){\"use strict\";var t={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(n,i,o){var r,a=function(t,n,i){void 0===i&&(i={});var o=new Date(t),r=function(t,n){void 0===n&&(n={});var i=n.timeZoneName||\"short\",o=t+\"|\"+i,r=e[o];return r||(r=new Intl.DateTimeFormat(\"en-US\",{hour12:!1,timeZone:t,year:\"numeric\",month:\"2-digit\",day:\"2-digit\",hour:\"2-digit\",minute:\"2-digit\",second:\"2-digit\",timeZoneName:i}),e[o]=r),r}(n,i);return r.formatToParts(o)},u=function(e,n){for(var i=a(e,n),r=[],u=0;u<i.length;u+=1){var f=i[u],s=f.type,m=f.value,c=t[s];c>=0&&(r[c]=parseInt(m,10))}var d=r[3],l=24===d?0:d,h=r[0]+\"-\"+r[1]+\"-\"+r[2]+\" \"+l+\":\"+r[4]+\":\"+r[5]+\":000\",v=+e;return(o.utc(h).valueOf()-(v-=v%1e3))/6e4},f=i.prototype;f.tz=function(t,e){void 0===t&&(t=r);var n,i=this.utcOffset(),a=this.toDate(),u=a.toLocaleString(\"en-US\",{timeZone:t}),f=Math.round((a-new Date(u))/1e3/60),s=15*-Math.round(a.getTimezoneOffset()/15)-f;if(!Number(s))n=this.utcOffset(0,e);else if(n=o(u,{locale:this.$L}).$set(\"millisecond\",this.$ms).utcOffset(s,!0),e){var m=n.utcOffset();n=n.add(i-m,\"minute\")}return n.$x.$timezone=t,n},f.offsetName=function(t){var e=this.$x.$timezone||o.tz.guess(),n=a(this.valueOf(),e,{timeZoneName:t}).find((function(t){return\"timezonename\"===t.type.toLowerCase()}));return n&&n.value};var s=f.startOf;f.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return s.call(this,t,e);var n=o(this.format(\"YYYY-MM-DD HH:mm:ss:SSS\"),{locale:this.$L});return s.call(n,t,e).tz(this.$x.$timezone,!0)},o.tz=function(t,e,n){var i=n&&e,a=n||e||r,f=u(+o(),a);if(\"string\"!=typeof t)return o(t).tz(a);var s=function(t,e,n){var i=t-60*e*1e3,o=u(i,n);if(e===o)return[i,e];var r=u(i-=60*(o-e)*1e3,n);return o===r?[i,o]:[t-60*Math.min(o,r)*1e3,Math.max(o,r)]}(o.utc(t,i).valueOf(),f,a),m=s[0],c=s[1],d=o(m).utcOffset(c);return d.$x.$timezone=a,d},o.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},o.tz.setDefault=function(t){r=t}}}));"],
|
||||
"mappings": "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",
|
||||
"names": ["t", "n", "i", "o", "r", "e", "u", "f", "s", "a"]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-V4OQ3NZ2.js";
|
||||
|
||||
// node_modules/dayjs/plugin/utc.js
|
||||
var require_utc = __commonJS({
|
||||
"node_modules/dayjs/plugin/utc.js"(exports, module) {
|
||||
!function(t, i) {
|
||||
"object" == typeof exports && "undefined" != typeof module ? module.exports = i() : "function" == typeof define && define.amd ? define(i) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs_plugin_utc = i();
|
||||
}(exports, function() {
|
||||
"use strict";
|
||||
var t = "minute", i = /[+-]\d\d(?::?\d\d)?/g, e = /([+-]|\d\d)/g;
|
||||
return function(s, f, n) {
|
||||
var u = f.prototype;
|
||||
n.utc = function(t2) {
|
||||
var i2 = { date: t2, utc: true, args: arguments };
|
||||
return new f(i2);
|
||||
}, u.utc = function(i2) {
|
||||
var e2 = n(this.toDate(), { locale: this.$L, utc: true });
|
||||
return i2 ? e2.add(this.utcOffset(), t) : e2;
|
||||
}, u.local = function() {
|
||||
return n(this.toDate(), { locale: this.$L, utc: false });
|
||||
};
|
||||
var o = u.parse;
|
||||
u.parse = function(t2) {
|
||||
t2.utc && (this.$u = true), this.$utils().u(t2.$offset) || (this.$offset = t2.$offset), o.call(this, t2);
|
||||
};
|
||||
var r = u.init;
|
||||
u.init = function() {
|
||||
if (this.$u) {
|
||||
var t2 = this.$d;
|
||||
this.$y = t2.getUTCFullYear(), this.$M = t2.getUTCMonth(), this.$D = t2.getUTCDate(), this.$W = t2.getUTCDay(), this.$H = t2.getUTCHours(), this.$m = t2.getUTCMinutes(), this.$s = t2.getUTCSeconds(), this.$ms = t2.getUTCMilliseconds();
|
||||
} else r.call(this);
|
||||
};
|
||||
var a = u.utcOffset;
|
||||
u.utcOffset = function(s2, f2) {
|
||||
var n2 = this.$utils().u;
|
||||
if (n2(s2)) return this.$u ? 0 : n2(this.$offset) ? a.call(this) : this.$offset;
|
||||
if ("string" == typeof s2 && (s2 = function(t2) {
|
||||
void 0 === t2 && (t2 = "");
|
||||
var s3 = t2.match(i);
|
||||
if (!s3) return null;
|
||||
var f3 = ("" + s3[0]).match(e) || ["-", 0, 0], n3 = f3[0], u3 = 60 * +f3[1] + +f3[2];
|
||||
return 0 === u3 ? 0 : "+" === n3 ? u3 : -u3;
|
||||
}(s2), null === s2)) return this;
|
||||
var u2 = Math.abs(s2) <= 16 ? 60 * s2 : s2, o2 = this;
|
||||
if (f2) return o2.$offset = u2, o2.$u = 0 === s2, o2;
|
||||
if (0 !== s2) {
|
||||
var r2 = this.$u ? this.toDate().getTimezoneOffset() : -1 * this.utcOffset();
|
||||
(o2 = this.local().add(u2 + r2, t)).$offset = u2, o2.$x.$localOffset = r2;
|
||||
} else o2 = this.utc();
|
||||
return o2;
|
||||
};
|
||||
var h = u.format;
|
||||
u.format = function(t2) {
|
||||
var i2 = t2 || (this.$u ? "YYYY-MM-DDTHH:mm:ss[Z]" : "");
|
||||
return h.call(this, i2);
|
||||
}, u.valueOf = function() {
|
||||
var t2 = this.$utils().u(this.$offset) ? 0 : this.$offset + (this.$x.$localOffset || this.$d.getTimezoneOffset());
|
||||
return this.$d.valueOf() - 6e4 * t2;
|
||||
}, u.isUTC = function() {
|
||||
return !!this.$u;
|
||||
}, u.toISOString = function() {
|
||||
return this.toDate().toISOString();
|
||||
}, u.toString = function() {
|
||||
return this.toDate().toUTCString();
|
||||
};
|
||||
var l = u.toDate;
|
||||
u.toDate = function(t2) {
|
||||
return "s" === t2 && this.$offset ? n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate() : l.call(this);
|
||||
};
|
||||
var c = u.diff;
|
||||
u.diff = function(t2, i2, e2) {
|
||||
if (t2 && this.$u === t2.$u) return c.call(this, t2, i2, e2);
|
||||
var s2 = this.local(), f2 = n(t2).local();
|
||||
return c.call(s2, f2, i2, e2);
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
export default require_utc();
|
||||
//# sourceMappingURL=dayjs_plugin_utc.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../dayjs/plugin/utc.js"],
|
||||
"sourcesContent": ["!function(t,i){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=i():\"function\"==typeof define&&define.amd?define(i):(t=\"undefined\"!=typeof globalThis?globalThis:t||self).dayjs_plugin_utc=i()}(this,(function(){\"use strict\";var t=\"minute\",i=/[+-]\\d\\d(?::?\\d\\d)?/g,e=/([+-]|\\d\\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var o=u.parse;u.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),o.call(this,t)};var r=u.init;u.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds()}else r.call(this)};var a=u.utcOffset;u.utcOffset=function(s,f){var n=this.$utils().u;if(n(s))return this.$u?0:n(this.$offset)?a.call(this):this.$offset;if(\"string\"==typeof s&&(s=function(t){void 0===t&&(t=\"\");var s=t.match(i);if(!s)return null;var f=(\"\"+s[0]).match(e)||[\"-\",0,0],n=f[0],u=60*+f[1]+ +f[2];return 0===u?0:\"+\"===n?u:-u}(s),null===s))return this;var u=Math.abs(s)<=16?60*s:s,o=this;if(f)return o.$offset=u,o.$u=0===s,o;if(0!==s){var r=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();(o=this.local().add(u+r,t)).$offset=u,o.$x.$localOffset=r}else o=this.utc();return o};var h=u.format;u.format=function(t){var i=t||(this.$u?\"YYYY-MM-DDTHH:mm:ss[Z]\":\"\");return h.call(this,i)},u.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*t},u.isUTC=function(){return!!this.$u},u.toISOString=function(){return this.toDate().toISOString()},u.toString=function(){return this.toDate().toUTCString()};var l=u.toDate;u.toDate=function(t){return\"s\"===t&&this.$offset?n(this.format(\"YYYY-MM-DD HH:mm:ss:SSS\")).toDate():l.call(this)};var c=u.diff;u.diff=function(t,i,e){if(t&&this.$u===t.$u)return c.call(this,t,i,e);var s=this.local(),f=n(t).local();return c.call(s,f,i,e)}}}));"],
|
||||
"mappings": "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",
|
||||
"names": ["t", "i", "e", "s", "f", "n", "u", "o", "r"]
|
||||
}
|
||||
+4
-4
@@ -1,3 +1,6 @@
|
||||
import {
|
||||
_setPrototypeOf
|
||||
} from "./chunk-4FTWOKSW.js";
|
||||
import {
|
||||
init_memoize_one_esm,
|
||||
memoize_one_esm_default
|
||||
@@ -5,9 +8,6 @@ import {
|
||||
import {
|
||||
require_prop_types
|
||||
} from "./chunk-7TYQWO6A.js";
|
||||
import {
|
||||
_setPrototypeOf
|
||||
} from "./chunk-4FTWOKSW.js";
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-GKJBSOWT.js";
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
_extends,
|
||||
_objectSpread2,
|
||||
_objectWithoutPropertiesLoose
|
||||
} from "./chunk-4JNVF2QO.js";
|
||||
} from "./chunk-66ZF2VOI.js";
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-QJTFJ6OV.js";
|
||||
|
||||
+1
-25
@@ -1,12 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
import { Layout, Tabs } from 'antd'
|
||||
import { Layout } from 'antd'
|
||||
import HomePage from './pages/HomePage'
|
||||
import ProjectDetailPage from './pages/ProjectDetailPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
import Header from './components/Header'
|
||||
import { RealTimeStatus } from './components/RealTimeStatus'
|
||||
import { SimpleTest } from './components/SimpleTest'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
@@ -17,33 +15,11 @@ function App() {
|
||||
<Layout>
|
||||
<Header />
|
||||
<Content>
|
||||
<Tabs
|
||||
defaultActiveKey="main"
|
||||
onChange={(key) => console.log('🔄 切换到标签页:', key)}
|
||||
items={[
|
||||
{
|
||||
key: 'main',
|
||||
label: '主界面',
|
||||
children: (
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/project/:id" element={<ProjectDetailPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'realtime',
|
||||
label: '实时状态',
|
||||
children: <RealTimeStatus userId="frontend-user" />
|
||||
},
|
||||
{
|
||||
key: 'test',
|
||||
label: '简单测试',
|
||||
children: <SimpleTest />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
@@ -6,9 +6,14 @@ import { Project } from '../store/useProjectStore'
|
||||
import { projectApi } from '../services/api'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(utc)
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const { Meta } = Card
|
||||
@@ -35,6 +40,13 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [currentLogIndex, setCurrentLogIndex] = useState(0)
|
||||
|
||||
// 格式化创建时间,确保正确的时区处理
|
||||
const formatCreatedTime = (createdAt: string) => {
|
||||
const now = dayjs().tz('Asia/Shanghai')
|
||||
const created = dayjs(createdAt).tz('Asia/Shanghai')
|
||||
return created.from(now)
|
||||
}
|
||||
|
||||
// 获取分类信息
|
||||
const getCategoryInfo = (category?: string) => {
|
||||
const categoryMap: Record<string, { name: string; icon: string; color: string }> = {
|
||||
@@ -56,7 +68,10 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
|
||||
// 生成项目视频缩略图(带缓存)
|
||||
useEffect(() => {
|
||||
const generateThumbnail = async () => {
|
||||
if (!project.video_path) return
|
||||
if (!project.video_path) {
|
||||
console.log('项目没有视频路径:', project.id)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
const cachedThumbnail = localStorage.getItem(thumbnailCacheKey)
|
||||
@@ -71,18 +86,45 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
|
||||
const video = document.createElement('video')
|
||||
video.crossOrigin = 'anonymous'
|
||||
video.muted = true
|
||||
video.preload = 'metadata'
|
||||
|
||||
const videoUrl = projectApi.getProjectFileUrl(project.id, 'input/input.mp4')
|
||||
// 尝试多个可能的视频文件路径
|
||||
const possiblePaths = [
|
||||
'input/input.mp4',
|
||||
'input.mp4',
|
||||
project.video_path,
|
||||
`${project.video_path}/input.mp4`
|
||||
].filter(Boolean)
|
||||
|
||||
let videoLoaded = false
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
if (videoLoaded) break
|
||||
|
||||
try {
|
||||
const videoUrl = projectApi.getProjectFileUrl(project.id, path)
|
||||
console.log('尝试加载视频:', videoUrl)
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error('视频加载超时'))
|
||||
}, 10000) // 10秒超时
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
clearTimeout(timeoutId)
|
||||
console.log('视频元数据加载成功:', videoUrl)
|
||||
video.currentTime = Math.min(5, video.duration / 4) // 取视频1/4处或5秒处的帧
|
||||
}
|
||||
|
||||
video.onseeked = () => {
|
||||
clearTimeout(timeoutId)
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
if (!ctx) {
|
||||
reject(new Error('无法获取canvas上下文'))
|
||||
return
|
||||
}
|
||||
|
||||
// 设置合适的缩略图尺寸
|
||||
const maxWidth = 320
|
||||
@@ -116,21 +158,36 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
|
||||
localStorage.setItem(thumbnailCacheKey, thumbnail)
|
||||
}
|
||||
}
|
||||
|
||||
videoLoaded = true
|
||||
resolve(thumbnail)
|
||||
} catch (error) {
|
||||
console.error('生成缩略图失败:', error)
|
||||
} finally {
|
||||
setThumbnailLoading(false)
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
video.onerror = (error) => {
|
||||
console.error('视频加载失败:', error)
|
||||
setThumbnailLoading(false)
|
||||
clearTimeout(timeoutId)
|
||||
console.error('视频加载失败:', videoUrl, error)
|
||||
reject(error)
|
||||
}
|
||||
|
||||
video.src = videoUrl
|
||||
})
|
||||
|
||||
break // 如果成功加载,跳出循环
|
||||
} catch (error) {
|
||||
console.warn(`路径 ${path} 加载失败:`, error)
|
||||
continue // 尝试下一个路径
|
||||
}
|
||||
}
|
||||
|
||||
if (!videoLoaded) {
|
||||
console.error('所有视频路径都加载失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成缩略图时发生错误:', error)
|
||||
} finally {
|
||||
setThumbnailLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -366,7 +423,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project, onDelete, onRetry, o
|
||||
height: '28px'
|
||||
}}>
|
||||
<Text style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
{dayjs(project.updated_at).fromNow()}
|
||||
{formatCreatedTime(project.created_at)}
|
||||
</Text>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
|
||||
+10
-1
@@ -5,11 +5,20 @@ import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
// 设置dayjs中文
|
||||
// 配置dayjs插件
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(utc)
|
||||
|
||||
// 设置dayjs中文和时区
|
||||
dayjs.locale('zh-cn')
|
||||
dayjs.tz.setDefault('Asia/Shanghai')
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<ConfigProvider locale={zhCN}>
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* WebSocket连接提供者组件
|
||||
* 管理WebSocket连接生命周期和自动重连
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useWebSocketStore, TaskProgressMessage } from '../stores/websocketStore'
|
||||
import { toast } from 'sonner'
|
||||
import { CheckCircle, AlertCircle, Activity } from 'lucide-react'
|
||||
|
||||
interface WebSocketProviderProps {
|
||||
children: React.ReactNode
|
||||
userId?: string
|
||||
autoConnect?: boolean
|
||||
}
|
||||
|
||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
|
||||
children,
|
||||
userId = 'user', // 默认用户ID,实际应用中应该从认证状态获取
|
||||
autoConnect = true
|
||||
}) => {
|
||||
const {
|
||||
connectionStatus,
|
||||
connect,
|
||||
disconnect,
|
||||
notifications,
|
||||
removeNotification
|
||||
} = useWebSocketStore()
|
||||
|
||||
const hasConnected = useRef(false)
|
||||
const notificationTimeouts = useRef<Map<number, NodeJS.Timeout>>(new Map())
|
||||
|
||||
// 自动连接
|
||||
useEffect(() => {
|
||||
if (autoConnect && !hasConnected.current && connectionStatus === 'disconnected') {
|
||||
connect(userId)
|
||||
hasConnected.current = true
|
||||
}
|
||||
|
||||
// 组件卸载时断开连接
|
||||
return () => {
|
||||
if (hasConnected.current) {
|
||||
disconnect()
|
||||
hasConnected.current = false
|
||||
}
|
||||
}
|
||||
}, [autoConnect, userId, connectionStatus, connect, disconnect])
|
||||
|
||||
// 显示连接状态变化的提示
|
||||
useEffect(() => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected':
|
||||
toast.success('WebSocket连接已建立')
|
||||
break
|
||||
case 'disconnected':
|
||||
if (hasConnected.current) {
|
||||
toast.error('WebSocket连接已断开')
|
||||
}
|
||||
break
|
||||
case 'reconnecting':
|
||||
toast.loading('正在重新连接...')
|
||||
break
|
||||
case 'error':
|
||||
toast.error('WebSocket连接失败')
|
||||
break
|
||||
}
|
||||
}, [connectionStatus])
|
||||
|
||||
// 处理WebSocket消息并显示通知
|
||||
useEffect(() => {
|
||||
const handleMessage = (message: any) => {
|
||||
// 根据消息类型显示不同的通知
|
||||
switch (message.type) {
|
||||
case 'task_update':
|
||||
if (message.status === 'completed') {
|
||||
toast.success(`任务完成: ${message.message || '处理完成'}`, {
|
||||
icon: <CheckCircle className="w-4 h-4" />
|
||||
})
|
||||
} else if (message.status === 'failed') {
|
||||
toast.error(`任务失败: ${message.error || '处理失败'}`, {
|
||||
icon: <AlertCircle className="w-4 h-4" />
|
||||
})
|
||||
} else if (message.status === 'started') {
|
||||
toast.info(`任务开始: ${message.message || '开始处理'}`, {
|
||||
icon: <Activity className="w-4 h-4" />
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'task_progress':
|
||||
// 对于进度更新,只在特定里程碑显示通知
|
||||
const progressMsg = message as TaskProgressMessage
|
||||
if (progressMsg.current_step === 1 || progressMsg.progress >= 100 || progressMsg.progress % 25 === 0) {
|
||||
toast.info(`${progressMsg.step_name}: ${Math.round(progressMsg.progress)}%`, {
|
||||
icon: <Activity className="w-4 h-4" />,
|
||||
description: `步骤 ${progressMsg.current_step}/${progressMsg.total_steps}`
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'project_update':
|
||||
if (message.status === 'completed') {
|
||||
toast.success(`项目完成: ${message.message || '项目处理完成'}`, {
|
||||
icon: <CheckCircle className="w-4 h-4" />
|
||||
})
|
||||
} else if (message.status === 'failed') {
|
||||
toast.error(`项目失败: ${message.message || '项目处理失败'}`, {
|
||||
icon: <AlertCircle className="w-4 h-4" />
|
||||
})
|
||||
}
|
||||
break
|
||||
|
||||
case 'system_notification':
|
||||
const level = message.level || 'info'
|
||||
const toastFn = level === 'error' ? toast.error :
|
||||
level === 'warning' ? toast.warning :
|
||||
level === 'success' ? toast.success : toast.info
|
||||
|
||||
toastFn(message.message, {
|
||||
description: message.title
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 监听新消息
|
||||
const unsubscribe = useWebSocketStore.subscribe(
|
||||
(state) => state.messages,
|
||||
(messages) => {
|
||||
if (messages.length > 0) {
|
||||
const latestMessage = messages[messages.length - 1]
|
||||
handleMessage(latestMessage)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return unsubscribe
|
||||
}, [])
|
||||
|
||||
// 显示系统通知
|
||||
useEffect(() => {
|
||||
notifications.forEach((notification, index) => {
|
||||
// 避免重复显示同一个通知
|
||||
if (!notificationTimeouts.current.has(index)) {
|
||||
const { title, message, level } = notification
|
||||
|
||||
// 根据级别显示不同类型的toast
|
||||
switch (level) {
|
||||
case 'success':
|
||||
toast.success(title, { description: message })
|
||||
break
|
||||
case 'error':
|
||||
toast.error(title, { description: message })
|
||||
break
|
||||
case 'warning':
|
||||
toast.warning(title, { description: message })
|
||||
break
|
||||
default:
|
||||
toast.info(title, { description: message })
|
||||
}
|
||||
|
||||
// 设置自动移除通知的定时器
|
||||
const timeout = setTimeout(() => {
|
||||
removeNotification(index)
|
||||
notificationTimeouts.current.delete(index)
|
||||
}, 5000) // 5秒后自动移除
|
||||
|
||||
notificationTimeouts.current.set(index, timeout)
|
||||
}
|
||||
})
|
||||
|
||||
// 清理过期的定时器
|
||||
return () => {
|
||||
notificationTimeouts.current.forEach(timeout => clearTimeout(timeout))
|
||||
notificationTimeouts.current.clear()
|
||||
}
|
||||
}, [notifications, removeNotification])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket连接状态指示器组件
|
||||
*/
|
||||
export const WebSocketStatusIndicator: React.FC = () => {
|
||||
const { connectionStatus, reconnect } = useWebSocketStore()
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected':
|
||||
return 'text-green-500'
|
||||
case 'connecting':
|
||||
case 'reconnecting':
|
||||
return 'text-yellow-500'
|
||||
case 'disconnected':
|
||||
case 'error':
|
||||
return 'text-red-500'
|
||||
default:
|
||||
return 'text-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = () => {
|
||||
switch (connectionStatus) {
|
||||
case 'connected':
|
||||
return '已连接'
|
||||
case 'connecting':
|
||||
return '连接中'
|
||||
case 'reconnecting':
|
||||
return '重连中'
|
||||
case 'disconnected':
|
||||
return '已断开'
|
||||
case 'error':
|
||||
return '连接错误'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<div className={`w-2 h-2 rounded-full ${getStatusColor().replace('text-', 'bg-')}`} />
|
||||
<span className={getStatusColor()}>{getStatusText()}</span>
|
||||
{(connectionStatus === 'disconnected' || connectionStatus === 'error') && (
|
||||
<button
|
||||
onClick={reconnect}
|
||||
className="text-blue-500 hover:text-blue-700 underline"
|
||||
>
|
||||
重连
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务进度监控组件
|
||||
*/
|
||||
interface TaskProgressMonitorProps {
|
||||
taskId: string
|
||||
onComplete?: (result: any) => void
|
||||
onError?: (error: string) => void
|
||||
}
|
||||
|
||||
export const TaskProgressMonitor: React.FC<TaskProgressMonitorProps> = ({
|
||||
taskId,
|
||||
onComplete,
|
||||
onError
|
||||
}) => {
|
||||
const { subscribeToTopic, unsubscribeFromTopic, getTaskStatus } = useWebSocketStore()
|
||||
|
||||
const taskStatus = getTaskStatus(taskId)
|
||||
|
||||
useEffect(() => {
|
||||
// 订阅任务主题
|
||||
const topic = `task_${taskId}`
|
||||
subscribeToTopic(topic)
|
||||
|
||||
return () => {
|
||||
unsubscribeFromTopic(topic)
|
||||
}
|
||||
}, [taskId, subscribeToTopic, unsubscribeFromTopic])
|
||||
|
||||
useEffect(() => {
|
||||
if (taskStatus) {
|
||||
if (taskStatus.status === 'completed' && onComplete) {
|
||||
onComplete(taskStatus)
|
||||
} else if (taskStatus.status === 'failed' && onError) {
|
||||
onError(taskStatus.error || '任务执行失败')
|
||||
}
|
||||
}
|
||||
}, [taskStatus, onComplete, onError])
|
||||
|
||||
if (!taskStatus) {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
|
||||
<span className="text-gray-500">等待任务状态...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const extendedTask = taskStatus as any
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
{taskStatus.status === 'processing' ? (
|
||||
<Activity className="w-4 h-4 animate-spin text-blue-500" />
|
||||
) : taskStatus.status === 'completed' ? (
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
) : taskStatus.status === 'failed' ? (
|
||||
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||
) : (
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
|
||||
)}
|
||||
<span className="font-medium">
|
||||
{taskStatus.message || '处理中...'}
|
||||
</span>
|
||||
{taskStatus.progress !== undefined && (
|
||||
<span className="text-blue-600 font-medium">
|
||||
{Math.round(taskStatus.progress)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 显示步骤信息 */}
|
||||
{extendedTask.current_step && extendedTask.total_steps && (
|
||||
<div className="text-xs text-gray-500">
|
||||
步骤 {extendedTask.current_step}/{extendedTask.total_steps}: {extendedTask.step_name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 进度条 */}
|
||||
{taskStatus.progress !== undefined && (
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(100, Math.max(0, taskStatus.progress))}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{taskStatus.status === 'failed' && taskStatus.error && (
|
||||
<div className="text-red-500 text-sm">
|
||||
错误: {taskStatus.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目进度监控组件
|
||||
*/
|
||||
interface ProjectProgressMonitorProps {
|
||||
projectId: string
|
||||
onComplete?: (result: any) => void
|
||||
onError?: (error: string) => void
|
||||
}
|
||||
|
||||
export const ProjectProgressMonitor: React.FC<ProjectProgressMonitorProps> = ({
|
||||
projectId,
|
||||
onComplete,
|
||||
onError
|
||||
}) => {
|
||||
const { subscribeToTopic, unsubscribeFromTopic, getProjectStatus } = useWebSocketStore()
|
||||
|
||||
const projectStatus = getProjectStatus(projectId)
|
||||
|
||||
useEffect(() => {
|
||||
// 订阅项目主题
|
||||
const topic = `project_${projectId}`
|
||||
subscribeToTopic(topic)
|
||||
|
||||
return () => {
|
||||
unsubscribeFromTopic(topic)
|
||||
}
|
||||
}, [projectId, subscribeToTopic, unsubscribeFromTopic])
|
||||
|
||||
useEffect(() => {
|
||||
if (projectStatus) {
|
||||
if (projectStatus.status === 'completed' && onComplete) {
|
||||
onComplete(projectStatus)
|
||||
} else if (projectStatus.status === 'failed' && onError) {
|
||||
onError(projectStatus.message || '项目处理失败')
|
||||
}
|
||||
}
|
||||
}, [projectStatus, onComplete, onError])
|
||||
|
||||
if (!projectStatus) {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
|
||||
<span className="text-gray-500">等待项目状态...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
{projectStatus.message || '处理中...'}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{projectStatus.progress || 0}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${projectStatus.progress || 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{projectStatus.status === 'failed' && (
|
||||
<div className="text-red-500 text-sm">
|
||||
错误: {projectStatus.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* WebSocket状态管理Store
|
||||
* 使用Zustand管理WebSocket连接、消息和状态
|
||||
*/
|
||||
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
// WebSocket消息类型定义
|
||||
export interface WebSocketMessage {
|
||||
type: 'task_update' | 'task_progress' | 'project_update' | 'system_notification' | 'error_notification'
|
||||
timestamp: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 任务更新消息
|
||||
export interface TaskUpdateMessage extends WebSocketMessage {
|
||||
type: 'task_update'
|
||||
task_id: string
|
||||
status: 'started' | 'processing' | 'completed' | 'failed'
|
||||
progress?: number
|
||||
message?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 任务进度消息
|
||||
export interface TaskProgressMessage extends WebSocketMessage {
|
||||
type: 'task_progress'
|
||||
task_id: string
|
||||
current_step: number
|
||||
total_steps: number
|
||||
step_name: string
|
||||
progress: number
|
||||
message?: string
|
||||
step_result?: Record<string, any>
|
||||
}
|
||||
|
||||
// 项目更新消息
|
||||
export interface ProjectUpdateMessage extends WebSocketMessage {
|
||||
type: 'project_update'
|
||||
project_id: string
|
||||
status: 'processing' | 'completed' | 'failed'
|
||||
progress?: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
// 系统通知消息
|
||||
export interface SystemNotificationMessage extends WebSocketMessage {
|
||||
type: 'system_notification'
|
||||
notification_type: string
|
||||
title: string
|
||||
message: string
|
||||
level: 'info' | 'success' | 'warning' | 'error'
|
||||
}
|
||||
|
||||
// 错误通知消息
|
||||
export interface ErrorNotificationMessage extends WebSocketMessage {
|
||||
type: 'error_notification'
|
||||
error_type: string
|
||||
error_message: string
|
||||
details?: Record<string, any>
|
||||
}
|
||||
|
||||
// WebSocket连接状态
|
||||
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'
|
||||
|
||||
// Store状态接口
|
||||
interface WebSocketState {
|
||||
// 连接状态
|
||||
connectionStatus: ConnectionStatus
|
||||
socket: WebSocket | null
|
||||
reconnectAttempts: number
|
||||
maxReconnectAttempts: number
|
||||
reconnectInterval: number
|
||||
|
||||
// 消息管理
|
||||
messages: WebSocketMessage[]
|
||||
unreadCount: number
|
||||
|
||||
// 任务状态
|
||||
tasks: Record<string, TaskUpdateMessage>
|
||||
|
||||
// 项目状态
|
||||
projects: Record<string, ProjectUpdateMessage>
|
||||
|
||||
// 系统通知
|
||||
notifications: SystemNotificationMessage[]
|
||||
|
||||
// 订阅主题
|
||||
subscribedTopics: Set<string>
|
||||
|
||||
// Actions
|
||||
connect: (userId: string) => void
|
||||
disconnect: () => void
|
||||
reconnect: () => void
|
||||
sendMessage: (message: any) => void
|
||||
subscribeToTopic: (topic: string) => void
|
||||
unsubscribeFromTopic: (topic: string) => void
|
||||
clearMessages: () => void
|
||||
markAsRead: () => void
|
||||
removeNotification: (index: number) => void
|
||||
getTaskStatus: (taskId: string) => TaskUpdateMessage | null
|
||||
getProjectStatus: (projectId: string) => ProjectUpdateMessage | null
|
||||
}
|
||||
|
||||
// WebSocket URL配置
|
||||
const getWebSocketUrl = () => {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
return `${protocol}//${host}/api/ws`
|
||||
}
|
||||
|
||||
export const useWebSocketStore = create<WebSocketState>()(devtools(
|
||||
(set, get) => ({
|
||||
// 初始状态
|
||||
connectionStatus: 'disconnected',
|
||||
socket: null,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 5,
|
||||
reconnectInterval: 3000,
|
||||
|
||||
messages: [],
|
||||
unreadCount: 0,
|
||||
|
||||
tasks: {},
|
||||
projects: {},
|
||||
notifications: [],
|
||||
|
||||
subscribedTopics: new Set(),
|
||||
|
||||
// 连接WebSocket
|
||||
connect: (userId: string) => {
|
||||
const state = get()
|
||||
|
||||
if (state.socket && state.connectionStatus === 'connected') {
|
||||
console.log('WebSocket已连接,跳过重复连接')
|
||||
return
|
||||
}
|
||||
|
||||
set({ connectionStatus: 'connecting' })
|
||||
|
||||
try {
|
||||
const wsUrl = `${getWebSocketUrl()}/${userId}`
|
||||
const socket = new WebSocket(wsUrl)
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket连接已建立')
|
||||
set({
|
||||
connectionStatus: 'connected',
|
||||
socket,
|
||||
reconnectAttempts: 0
|
||||
})
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data)
|
||||
|
||||
set((state) => {
|
||||
const newState = {
|
||||
messages: [...state.messages, message],
|
||||
unreadCount: state.unreadCount + 1
|
||||
}
|
||||
|
||||
// 根据消息类型更新相应状态
|
||||
switch (message.type) {
|
||||
case 'task_update':
|
||||
const taskMsg = message as TaskUpdateMessage
|
||||
newState.tasks = {
|
||||
...state.tasks,
|
||||
[taskMsg.task_id]: taskMsg
|
||||
}
|
||||
break
|
||||
|
||||
case 'task_progress':
|
||||
const progressMsg = message as TaskProgressMessage
|
||||
// 更新任务进度信息
|
||||
const existingTask = state.tasks[progressMsg.task_id]
|
||||
newState.tasks = {
|
||||
...state.tasks,
|
||||
[progressMsg.task_id]: {
|
||||
...existingTask,
|
||||
type: 'task_update',
|
||||
task_id: progressMsg.task_id,
|
||||
status: 'processing',
|
||||
progress: progressMsg.progress,
|
||||
message: progressMsg.message || progressMsg.step_name,
|
||||
timestamp: progressMsg.timestamp,
|
||||
current_step: progressMsg.current_step,
|
||||
total_steps: progressMsg.total_steps,
|
||||
step_name: progressMsg.step_name,
|
||||
step_result: progressMsg.step_result
|
||||
} as TaskUpdateMessage & {
|
||||
current_step: number
|
||||
total_steps: number
|
||||
step_name: string
|
||||
step_result?: Record<string, any>
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'project_update':
|
||||
const projectMsg = message as ProjectUpdateMessage
|
||||
newState.projects = {
|
||||
...state.projects,
|
||||
[projectMsg.project_id]: projectMsg
|
||||
}
|
||||
break
|
||||
|
||||
case 'system_notification':
|
||||
const notificationMsg = message as SystemNotificationMessage
|
||||
newState.notifications = [...state.notifications, notificationMsg]
|
||||
break
|
||||
|
||||
case 'error_notification':
|
||||
const errorMsg = message as ErrorNotificationMessage
|
||||
newState.notifications = [
|
||||
...state.notifications,
|
||||
{
|
||||
type: 'system_notification',
|
||||
notification_type: 'error',
|
||||
title: '错误',
|
||||
message: errorMsg.error_message,
|
||||
level: 'error',
|
||||
timestamp: errorMsg.timestamp
|
||||
} as SystemNotificationMessage
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
return newState
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('解析WebSocket消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
socket.onclose = (event) => {
|
||||
console.log('WebSocket连接已关闭:', event.code, event.reason)
|
||||
set({ connectionStatus: 'disconnected', socket: null })
|
||||
|
||||
// 自动重连
|
||||
const state = get()
|
||||
if (state.reconnectAttempts < state.maxReconnectAttempts) {
|
||||
setTimeout(() => {
|
||||
set((state) => ({
|
||||
connectionStatus: 'reconnecting',
|
||||
reconnectAttempts: state.reconnectAttempts + 1
|
||||
}))
|
||||
get().connect(userId)
|
||||
}, state.reconnectInterval)
|
||||
} else {
|
||||
set({ connectionStatus: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error)
|
||||
set({ connectionStatus: 'error' })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建WebSocket连接失败:', error)
|
||||
set({ connectionStatus: 'error' })
|
||||
}
|
||||
},
|
||||
|
||||
// 断开连接
|
||||
disconnect: () => {
|
||||
const state = get()
|
||||
if (state.socket) {
|
||||
state.socket.close()
|
||||
}
|
||||
set({
|
||||
connectionStatus: 'disconnected',
|
||||
socket: null,
|
||||
reconnectAttempts: 0
|
||||
})
|
||||
},
|
||||
|
||||
// 重新连接
|
||||
reconnect: () => {
|
||||
const state = get()
|
||||
state.disconnect()
|
||||
// 这里需要用户ID,实际使用时应该从认证状态获取
|
||||
setTimeout(() => state.connect('user'), 1000)
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage: (message: any) => {
|
||||
const state = get()
|
||||
if (state.socket && state.connectionStatus === 'connected') {
|
||||
state.socket.send(JSON.stringify(message))
|
||||
} else {
|
||||
console.warn('WebSocket未连接,无法发送消息')
|
||||
}
|
||||
},
|
||||
|
||||
// 订阅主题
|
||||
subscribeToTopic: (topic: string) => {
|
||||
const state = get()
|
||||
state.sendMessage({
|
||||
type: 'subscribe',
|
||||
topic
|
||||
})
|
||||
|
||||
set((state) => ({
|
||||
subscribedTopics: new Set([...state.subscribedTopics, topic])
|
||||
}))
|
||||
},
|
||||
|
||||
// 取消订阅主题
|
||||
unsubscribeFromTopic: (topic: string) => {
|
||||
const state = get()
|
||||
state.sendMessage({
|
||||
type: 'unsubscribe',
|
||||
topic
|
||||
})
|
||||
|
||||
set((state) => {
|
||||
const newTopics = new Set(state.subscribedTopics)
|
||||
newTopics.delete(topic)
|
||||
return { subscribedTopics: newTopics }
|
||||
})
|
||||
},
|
||||
|
||||
// 清空消息
|
||||
clearMessages: () => {
|
||||
set({ messages: [], unreadCount: 0 })
|
||||
},
|
||||
|
||||
// 标记为已读
|
||||
markAsRead: () => {
|
||||
set({ unreadCount: 0 })
|
||||
},
|
||||
|
||||
// 移除通知
|
||||
removeNotification: (index: number) => {
|
||||
set((state) => ({
|
||||
notifications: state.notifications.filter((_, i) => i !== index)
|
||||
}))
|
||||
},
|
||||
|
||||
// 获取任务状态
|
||||
getTaskStatus: (taskId: string) => {
|
||||
return get().tasks[taskId] || null
|
||||
},
|
||||
|
||||
// 获取项目状态
|
||||
getProjectStatus: (projectId: string) => {
|
||||
return get().projects[projectId] || null
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'websocket-store',
|
||||
partialize: (state) => ({
|
||||
// 只持久化部分状态,不包括连接相关的状态
|
||||
notifications: state.notifications,
|
||||
subscribedTopics: Array.from(state.subscribedTopics)
|
||||
})
|
||||
}
|
||||
))
|
||||
|
||||
// 导出便捷的hooks
|
||||
export const useWebSocketConnection = () => {
|
||||
const { connectionStatus, connect, disconnect, reconnect } = useWebSocketStore()
|
||||
return { connectionStatus, connect, disconnect, reconnect }
|
||||
}
|
||||
|
||||
export const useWebSocketMessages = () => {
|
||||
const { messages, unreadCount, clearMessages, markAsRead } = useWebSocketStore()
|
||||
return { messages, unreadCount, clearMessages, markAsRead }
|
||||
}
|
||||
|
||||
export const useTaskStatus = (taskId: string) => {
|
||||
return useWebSocketStore((state) => state.getTaskStatus(taskId))
|
||||
}
|
||||
|
||||
export const useProjectStatus = (projectId: string) => {
|
||||
return useWebSocketStore((state) => state.getProjectStatus(projectId))
|
||||
}
|
||||
|
||||
export const useNotifications = () => {
|
||||
const { notifications, removeNotification } = useWebSocketStore()
|
||||
return { notifications, removeNotification }
|
||||
}
|
||||
Reference in new issue
Block a user