fix: schedule timezone bug + missing notifications + celery beat startup
- Timezone: compute_next_run now correctly interprets cron in the schedule's configured timezone (e.g., "0 8 * * *" with Asia/Shanghai = 8AM Beijing, not UTC) - Notifications: agent_tasks now reuses pre-created execution records and calls notify_schedule_result on completion, so non-workflow agent schedules get DB notifications + Feishu webhook + Feishu app messages - Duplicate execution: execute_agent_task accepts optional execution_id to reuse the record created by schedule_service instead of creating a second one - Celery Beat: added to restart_backend_celery.ps1, stop_aiagent.ps1, and docker-compose.dev.yml; fixed repo-root path resolution in all PS1 scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -95,7 +95,7 @@ async def create_schedule(
|
|||||||
|
|
||||||
# 验证 cron 表达式
|
# 验证 cron 表达式
|
||||||
try:
|
try:
|
||||||
next_run = compute_next_run(data.cron_expression)
|
next_run = compute_next_run(data.cron_expression, tz=data.timezone or "Asia/Shanghai")
|
||||||
except (ValueError, KeyError) as e:
|
except (ValueError, KeyError) as e:
|
||||||
raise HTTPException(status_code=400, detail=f"cron 表达式无效: {e}")
|
raise HTTPException(status_code=400, detail=f"cron 表达式无效: {e}")
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ async def update_schedule(
|
|||||||
schedule.name = data.name
|
schedule.name = data.name
|
||||||
if data.cron_expression is not None:
|
if data.cron_expression is not None:
|
||||||
try:
|
try:
|
||||||
schedule.next_run_at = compute_next_run(data.cron_expression, after=datetime.utcnow())
|
schedule.next_run_at = compute_next_run(data.cron_expression, after=datetime.utcnow(), tz=data.timezone or schedule.timezone or "Asia/Shanghai")
|
||||||
schedule.cron_expression = data.cron_expression
|
schedule.cron_expression = data.cron_expression
|
||||||
except (ValueError, KeyError) as e:
|
except (ValueError, KeyError) as e:
|
||||||
raise HTTPException(status_code=400, detail=f"cron 表达式无效: {e}")
|
raise HTTPException(status_code=400, detail=f"cron 表达式无效: {e}")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import pytz
|
||||||
from croniter import croniter
|
from croniter import croniter
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -12,22 +13,33 @@ from app.core.database import SessionLocal
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def compute_next_run(cron_expression: str, after: Optional[datetime] = None) -> datetime:
|
def compute_next_run(cron_expression: str, after: Optional[datetime] = None, tz: str = "UTC") -> datetime:
|
||||||
"""根据 cron 表达式计算下一次执行时间。
|
"""根据 cron 表达式计算下一次执行时间。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cron_expression: 标准 5 位 cron 表达式,如 "0 9 * * *"
|
cron_expression: 标准 5 位 cron 表达式,如 "0 9 * * *"
|
||||||
after: 从哪个时间点开始计算,默认为当前 UTC 时间
|
after: 从哪个时间点开始计算,默认为当前 UTC 时间
|
||||||
|
tz: 时区名称,如 "Asia/Shanghai",cron 表达式在该时区下解释
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
下次执行的 UTC datetime
|
下次执行的 UTC datetime(naive,存储时与 datetime.utcnow() 对齐)
|
||||||
"""
|
"""
|
||||||
base = after or datetime.now(timezone.utc)
|
base = after or datetime.now(timezone.utc)
|
||||||
# croniter 需要以 datetime 对象作为基准
|
# 将 UTC 时间转换为目标时区,cron 表达式在目标时区下解释
|
||||||
base_naive = base.replace(tzinfo=None) if base.tzinfo else base
|
try:
|
||||||
|
target_tz = pytz.timezone(tz)
|
||||||
|
except pytz.UnknownTimeZoneError:
|
||||||
|
target_tz = pytz.UTC
|
||||||
|
base_in_tz = base.astimezone(target_tz)
|
||||||
|
# croniter 需要 naive datetime,在目标时区下计算
|
||||||
|
base_naive = base_in_tz.replace(tzinfo=None)
|
||||||
cron = croniter(cron_expression, base_naive)
|
cron = croniter(cron_expression, base_naive)
|
||||||
next_dt = cron.get_next(datetime)
|
next_naive = cron.get_next(datetime)
|
||||||
return next_dt
|
# 将结果 localize 回目标时区,再转 UTC
|
||||||
|
next_in_tz = target_tz.localize(next_naive)
|
||||||
|
next_utc = next_in_tz.astimezone(pytz.UTC)
|
||||||
|
# 返回 naive UTC(与 datetime.utcnow() 对齐)
|
||||||
|
return next_utc.replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
def create_execution_for_schedule(db: Session, schedule) -> Optional[str]:
|
def create_execution_for_schedule(db: Session, schedule) -> Optional[str]:
|
||||||
@@ -69,11 +81,12 @@ def create_execution_for_schedule(db: Session, schedule) -> Optional[str]:
|
|||||||
{"message": schedule.input_message},
|
{"message": schedule.input_message},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 无工作流配置:走简单 Agent 异步执行
|
# 无工作流配置:走简单 Agent 异步执行(传入已创建的 execution_id)
|
||||||
from app.tasks.agent_tasks import execute_agent_task
|
from app.tasks.agent_tasks import execute_agent_task
|
||||||
task = execute_agent_task.delay(
|
task = execute_agent_task.delay(
|
||||||
str(schedule.agent_id),
|
str(schedule.agent_id),
|
||||||
{"message": schedule.input_message},
|
{"message": schedule.input_message},
|
||||||
|
execution_id=str(execution.id),
|
||||||
)
|
)
|
||||||
execution.task_id = task.id
|
execution.task_id = task.id
|
||||||
execution.status = "running"
|
execution.status = "running"
|
||||||
@@ -124,8 +137,8 @@ def check_and_run_due_schedules() -> int:
|
|||||||
# 更新定时任务状态
|
# 更新定时任务状态
|
||||||
sched.last_run_at = now
|
sched.last_run_at = now
|
||||||
sched.last_run_status = "success"
|
sched.last_run_status = "success"
|
||||||
# 计算下次执行时间
|
# 计算下次执行时间(使用定时任务配置的时区)
|
||||||
sched.next_run_at = compute_next_run(sched.cron_expression, after=now)
|
sched.next_run_at = compute_next_run(sched.cron_expression, after=now, tz=sched.timezone or "UTC")
|
||||||
db.commit()
|
db.commit()
|
||||||
triggered += 1
|
triggered += 1
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -145,3 +158,91 @@ def check_and_run_due_schedules() -> int:
|
|||||||
finally:
|
finally:
|
||||||
if db:
|
if db:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def notify_schedule_result(db: Session, execution, status: str, error_message: Optional[str] = None) -> None:
|
||||||
|
"""如果 execution 关联了定时任务,创建通知并推送飞书消息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库会话
|
||||||
|
execution: Execution ORM 对象
|
||||||
|
status: "completed" 或 "failed"
|
||||||
|
error_message: 失败时的错误信息
|
||||||
|
"""
|
||||||
|
if not execution or not execution.schedule_id:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from app.models.agent_schedule import AgentSchedule
|
||||||
|
from app.services.notification_service import create_notification
|
||||||
|
|
||||||
|
schedule = db.query(AgentSchedule).filter(AgentSchedule.id == execution.schedule_id).first()
|
||||||
|
if not schedule:
|
||||||
|
return
|
||||||
|
|
||||||
|
if status == "completed":
|
||||||
|
title = f"定时任务「{schedule.name}」执行成功"
|
||||||
|
content = f"Agent 已按计划执行完成。"
|
||||||
|
else:
|
||||||
|
title = f"定时任务「{schedule.name}」执行失败"
|
||||||
|
content = f"错误信息: {error_message or '未知错误'}"
|
||||||
|
|
||||||
|
create_notification(
|
||||||
|
db,
|
||||||
|
user_id=schedule.user_id,
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
category="schedule",
|
||||||
|
ref_type="execution",
|
||||||
|
ref_id=str(execution.id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 如果配置了飞书 webhook,发送飞书通知(非阻塞,失败不影响主流程)
|
||||||
|
if schedule.webhook_url:
|
||||||
|
try:
|
||||||
|
from app.services.feishu_notifier import send_feishu_card
|
||||||
|
|
||||||
|
detail_link = None
|
||||||
|
try:
|
||||||
|
from app.core.config import settings
|
||||||
|
if settings.EXTERNAL_URL:
|
||||||
|
detail_link = f"{settings.EXTERNAL_URL}/executions/{execution.id}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
send_feishu_card(
|
||||||
|
webhook_url=schedule.webhook_url,
|
||||||
|
title=title,
|
||||||
|
body=content,
|
||||||
|
status=status,
|
||||||
|
detail_link=detail_link,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("飞书 webhook 通知发送失败: %s", e)
|
||||||
|
|
||||||
|
# 如果用户绑定了飞书账号,通过飞书应用发送通知
|
||||||
|
try:
|
||||||
|
from app.models.user import User
|
||||||
|
from app.services.feishu_app_service import send_message_to_user
|
||||||
|
|
||||||
|
schedule_user = db.query(User).filter(User.id == schedule.user_id).first()
|
||||||
|
if schedule_user and schedule_user.feishu_open_id:
|
||||||
|
detail_link = None
|
||||||
|
try:
|
||||||
|
from app.core.config import settings
|
||||||
|
if settings.EXTERNAL_URL:
|
||||||
|
detail_link = f"{settings.EXTERNAL_URL}/executions/{execution.id}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
send_message_to_user(
|
||||||
|
open_id=schedule_user.feishu_open_id,
|
||||||
|
title=title,
|
||||||
|
content=content,
|
||||||
|
status=status,
|
||||||
|
detail_link=detail_link,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("飞书应用通知发送失败: %s", e)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("创建定时任务通知失败: %s", e)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from app.models.execution import Execution
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -77,16 +78,20 @@ def _build_agent_config_from_db(agent: Agent) -> AgentConfig:
|
|||||||
|
|
||||||
|
|
||||||
@celery_app.task(bind=True)
|
@celery_app.task(bind=True)
|
||||||
def execute_agent_task(self, agent_id: str, input_data: dict):
|
def execute_agent_task(self, agent_id: str, input_data: dict, execution_id: Optional[str] = None):
|
||||||
"""异步执行 Agent 任务。
|
"""异步执行 Agent 任务。
|
||||||
|
|
||||||
由定时调度 (check_agent_schedules_task) 或手动 API 触发。
|
由定时调度 (check_agent_schedules_task) 或手动 API 触发。
|
||||||
创建 Execution 记录,运行 Agent,更新结果。
|
如果传入 execution_id(由 schedule_service 预创建),则复用该记录;
|
||||||
|
否则创建新的 Execution 记录。运行 Agent 后更新结果并发送通知。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent_id: Agent ID
|
agent_id: Agent ID
|
||||||
input_data: 输入数据,至少包含 "message" 字段
|
input_data: 输入数据,至少包含 "message" 字段
|
||||||
|
execution_id: 可选,预创建的 execution 记录 ID(定时任务场景)
|
||||||
"""
|
"""
|
||||||
|
from app.services.agent_schedule_service import notify_schedule_result
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
@@ -99,14 +104,21 @@ def execute_agent_task(self, agent_id: str, input_data: dict):
|
|||||||
if not user_message:
|
if not user_message:
|
||||||
return {"status": "error", "detail": "缺少 message 输入"}
|
return {"status": "error", "detail": "缺少 message 输入"}
|
||||||
|
|
||||||
# 创建执行记录
|
# 复用预创建的 execution 记录,或新建
|
||||||
execution = Execution(
|
if execution_id:
|
||||||
agent_id=agent_id,
|
execution = db.query(Execution).filter(Execution.id == execution_id).first()
|
||||||
input_data=input_data,
|
if not execution:
|
||||||
status="running",
|
return {"status": "error", "detail": f"Execution {execution_id} 不存在"}
|
||||||
)
|
execution.status = "running"
|
||||||
db.add(execution)
|
db.commit()
|
||||||
db.flush()
|
else:
|
||||||
|
execution = Execution(
|
||||||
|
agent_id=agent_id,
|
||||||
|
input_data=input_data,
|
||||||
|
status="running",
|
||||||
|
)
|
||||||
|
db.add(execution)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
# 更新 Celery 任务状态
|
# 更新 Celery 任务状态
|
||||||
self.update_state(
|
self.update_state(
|
||||||
@@ -137,6 +149,9 @@ def execute_agent_task(self, agent_id: str, input_data: dict):
|
|||||||
execution.execution_time = execution_time
|
execution.execution_time = execution_time
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# 定时任务结果通知
|
||||||
|
notify_schedule_result(db, execution, "completed")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Agent 异步执行完成: agent=%s execution=%s time=%dms",
|
"Agent 异步执行完成: agent=%s execution=%s time=%dms",
|
||||||
agent_id, execution.id, execution_time,
|
agent_id, execution.id, execution_time,
|
||||||
@@ -154,6 +169,10 @@ def execute_agent_task(self, agent_id: str, input_data: dict):
|
|||||||
execution.error_message = result.error or "Agent 执行返回失败"
|
execution.error_message = result.error or "Agent 执行返回失败"
|
||||||
execution.execution_time = int((time.time() - start_time) * 1000)
|
execution.execution_time = int((time.time() - start_time) * 1000)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# 定时任务失败通知
|
||||||
|
notify_schedule_result(db, execution, "failed", error_message=execution.error_message)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "failed",
|
"status": "failed",
|
||||||
"execution_id": str(execution.id),
|
"execution_id": str(execution.id),
|
||||||
@@ -165,6 +184,10 @@ def execute_agent_task(self, agent_id: str, input_data: dict):
|
|||||||
execution.error_message = f"Agent 执行异常: {run_e!s}"
|
execution.error_message = f"Agent 执行异常: {run_e!s}"
|
||||||
execution.execution_time = execution_time
|
execution.execution_time = execution_time
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# 定时任务失败通知
|
||||||
|
notify_schedule_result(db, execution, "failed", error_message=execution.error_message)
|
||||||
|
|
||||||
logger.error("Agent 异步执行异常: agent=%s error=%s", agent_id, run_e)
|
logger.error("Agent 异步执行异常: agent=%s error=%s", agent_id, run_e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from app.models.agent import Agent
|
|||||||
from app.models.workflow import Workflow
|
from app.models.workflow import Workflow
|
||||||
from app.services.execution_budget import merge_budget_for_execution
|
from app.services.execution_budget import merge_budget_for_execution
|
||||||
from app.services.agent_workspace_chat_log import try_append_agent_dialogue_after_success
|
from app.services.agent_workspace_chat_log import try_append_agent_dialogue_after_success
|
||||||
from app.services.notification_service import create_notification
|
from app.services.agent_schedule_service import notify_schedule_result
|
||||||
from app.websocket.manager import websocket_manager
|
from app.websocket.manager import websocket_manager
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
@@ -80,92 +80,6 @@ def _trusted_user_for_execution(db, execution: Optional[Execution]) -> Optional[
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _notify_schedule_result(
|
|
||||||
db,
|
|
||||||
execution,
|
|
||||||
status: str,
|
|
||||||
error_message: Optional[str] = None,
|
|
||||||
):
|
|
||||||
"""如果 execution 关联了定时任务,创建通知推送结果给用户。"""
|
|
||||||
if not execution or not execution.schedule_id:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
from app.models.agent_schedule import AgentSchedule
|
|
||||||
|
|
||||||
schedule = db.query(AgentSchedule).filter(AgentSchedule.id == execution.schedule_id).first()
|
|
||||||
if not schedule:
|
|
||||||
return
|
|
||||||
|
|
||||||
if status == "completed":
|
|
||||||
title = f"定时任务「{schedule.name}」执行成功"
|
|
||||||
content = f"Agent 已按计划执行完成。"
|
|
||||||
else:
|
|
||||||
title = f"定时任务「{schedule.name}」执行失败"
|
|
||||||
content = f"错误信息: {error_message or '未知错误'}"
|
|
||||||
|
|
||||||
create_notification(
|
|
||||||
db,
|
|
||||||
user_id=schedule.user_id,
|
|
||||||
title=title,
|
|
||||||
content=content,
|
|
||||||
category="schedule",
|
|
||||||
ref_type="execution",
|
|
||||||
ref_id=str(execution.id),
|
|
||||||
)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
# 如果配置了飞书 webhook,发送飞书通知(非阻塞,失败不影响主流程)
|
|
||||||
if schedule.webhook_url:
|
|
||||||
try:
|
|
||||||
from app.services.feishu_notifier import send_feishu_card
|
|
||||||
|
|
||||||
detail_link = None
|
|
||||||
# 如果系统配置了外部访问地址,拼接 execution 详情链接
|
|
||||||
try:
|
|
||||||
from app.core.config import settings
|
|
||||||
if settings.EXTERNAL_URL:
|
|
||||||
detail_link = f"{settings.EXTERNAL_URL}/executions/{execution.id}"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
send_feishu_card(
|
|
||||||
webhook_url=schedule.webhook_url,
|
|
||||||
title=title,
|
|
||||||
body=content,
|
|
||||||
status=status,
|
|
||||||
detail_link=detail_link,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("飞书 webhook 通知发送失败: %s", e)
|
|
||||||
|
|
||||||
# 如果用户绑定了飞书账号,通过飞书应用发送通知
|
|
||||||
try:
|
|
||||||
from app.models.user import User
|
|
||||||
from app.services.feishu_app_service import send_message_to_user
|
|
||||||
|
|
||||||
schedule_user = db.query(User).filter(User.id == schedule.user_id).first()
|
|
||||||
if schedule_user and schedule_user.feishu_open_id:
|
|
||||||
detail_link = None
|
|
||||||
try:
|
|
||||||
from app.core.config import settings
|
|
||||||
if settings.EXTERNAL_URL:
|
|
||||||
detail_link = f"{settings.EXTERNAL_URL}/executions/{execution.id}"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
send_message_to_user(
|
|
||||||
open_id=schedule_user.feishu_open_id,
|
|
||||||
title=title,
|
|
||||||
content=content,
|
|
||||||
status=status,
|
|
||||||
detail_link=detail_link,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("飞书应用通知发送失败: %s", e)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning("创建定时任务通知失败: %s", e)
|
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(bind=True)
|
@celery_app.task(bind=True)
|
||||||
def execute_workflow_task(
|
def execute_workflow_task(
|
||||||
self,
|
self,
|
||||||
@@ -287,7 +201,7 @@ def execute_workflow_task(
|
|||||||
execution_logger.warn(f"告警检测失败: {str(e)}")
|
execution_logger.warn(f"告警检测失败: {str(e)}")
|
||||||
|
|
||||||
# 定时任务结果通知
|
# 定时任务结果通知
|
||||||
_notify_schedule_result(db, execution, "completed")
|
notify_schedule_result(db, execution, "completed")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'completed',
|
'status': 'completed',
|
||||||
@@ -331,7 +245,7 @@ def execute_workflow_task(
|
|||||||
execution_logger.warn(f"告警检测失败: {str(e2)}")
|
execution_logger.warn(f"告警检测失败: {str(e2)}")
|
||||||
|
|
||||||
# 定时任务失败通知
|
# 定时任务失败通知
|
||||||
_notify_schedule_result(db, execution, "failed", error_message=err_text)
|
notify_schedule_result(db, execution, "failed", error_message=err_text)
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
89
docs/startup-deploy/docker-compose.dev.yml
Normal file
89
docs/startup-deploy/docker-compose.dev.yml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "8038:3000"
|
||||||
|
volumes:
|
||||||
|
- ./frontend:/app
|
||||||
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=http://101.43.95.130:8037
|
||||||
|
# 注意:Vite环境变量需要在构建时设置,运行时修改需要重启容器
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- aiagent-network
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
ports:
|
||||||
|
- "8037:8000"
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=mysql+pymysql://root:!Rjb12191@gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936/agent_db?charset=utf8mb4
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- SECRET_KEY=dev-secret-key-change-in-production
|
||||||
|
- CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:8038,http://101.43.95.130:8038
|
||||||
|
- DEEPSEEK_API_KEY=sk-fdf7cc1c73504e628ec0119b7e11b8cc
|
||||||
|
- DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- aiagent-network
|
||||||
|
|
||||||
|
celery:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
command: celery -A app.core.celery_app worker --loglevel=info
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=mysql+pymysql://root:!Rjb12191@gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936/agent_db?charset=utf8mb4
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- DEEPSEEK_API_KEY=sk-fdf7cc1c73504e628ec0119b7e11b8cc
|
||||||
|
- DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- aiagent-network
|
||||||
|
|
||||||
|
celery-beat:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
command: celery -A app.core.celery_app beat --loglevel=info
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=mysql+pymysql://root:!Rjb12191@gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com:24936/agent_db?charset=utf8mb4
|
||||||
|
- REDIS_URL=redis://redis:6379/0
|
||||||
|
- DEEPSEEK_API_KEY=sk-fdf7cc1c73504e628ec0119b7e11b8cc
|
||||||
|
- DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- aiagent-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6380:6379" # 主机 6380 映射到容器 6379,避免与宿主机 6379 冲突
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
- aiagent-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
aiagent-network:
|
||||||
|
driver: bridge
|
||||||
73
scripts/startup/restart_backend_celery.ps1
Normal file
73
scripts/startup/restart_backend_celery.ps1
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Restart backend API (uvicorn) + Celery worker + Celery Beat (scheduler)
|
||||||
|
# Does not stop frontend/Redis.
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
# Repo root = two levels up from scripts/startup/
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$RepoRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||||
|
$Backend = Join-Path $RepoRoot "backend"
|
||||||
|
$ApiPort = 8037
|
||||||
|
|
||||||
|
Write-Host "== Stop API + Celery worker + Celery Beat ==" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
function Stop-ByCommandLine([string]$pattern, [string]$name) {
|
||||||
|
$targets = Get-CimInstance Win32_Process | Where-Object {
|
||||||
|
$_.CommandLine -and $_.CommandLine -match $pattern
|
||||||
|
}
|
||||||
|
if (-not $targets) {
|
||||||
|
Write-Host "[SKIP] ${name}: no matching process" -ForegroundColor DarkGray
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foreach ($p in $targets) {
|
||||||
|
try {
|
||||||
|
Stop-Process -Id $p.ProcessId -Force
|
||||||
|
Write-Host "[OK] stopped ${name} PID=$($p.ProcessId)" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "[WARN] failed to stop ${name} PID=$($p.ProcessId)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stop-ByCommandLine "uvicorn\s+app\.main:app" "backend-api"
|
||||||
|
Stop-ByCommandLine "celery\s+-A\s+app\.core\.celery_app\s+worker" "celery-worker"
|
||||||
|
Stop-ByCommandLine "celery\s+-A\s+app\.core\.celery_app\s+beat" "celery-beat"
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
|
||||||
|
Write-Host "== Start API on $ApiPort + Celery worker + Celery Beat ==" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 1. API (uvicorn)
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $ApiPort"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Celery worker
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Celery Beat (scheduler — 每分钟检查到期的定时任务)
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app beat --loglevel=info"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[DONE] 已在新窗口启动 API + Celery Worker + Celery Beat" -ForegroundColor Green
|
||||||
|
Write-Host "API: http://127.0.0.1:$ApiPort/docs" -ForegroundColor Cyan
|
||||||
|
Write-Host "Celery Beat 负责每分钟检查定时任务,如果定时推送没触发,请确认 Beat 窗口正在运行" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Port $ApiPort :" -ForegroundColor Cyan
|
||||||
|
netstat -ano | findstr ":$ApiPort"
|
||||||
91
scripts/startup/start_aiagent.ps1
Normal file
91
scripts/startup/start_aiagent.ps1
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
param(
|
||||||
|
[int]$ApiPort = 8037,
|
||||||
|
[int]$FallbackApiPort = 8041,
|
||||||
|
[int]$FrontendPort = 3001
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$RepoRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||||
|
$Backend = Join-Path $RepoRoot "backend"
|
||||||
|
$Frontend = Join-Path $RepoRoot "frontend"
|
||||||
|
$RedisDir = Join-Path $Backend "redis"
|
||||||
|
$RedisExe = Join-Path $RedisDir "redis-server.exe"
|
||||||
|
$RedisCli = Join-Path $RedisDir "redis-cli.exe"
|
||||||
|
|
||||||
|
function Test-PortListening([int]$Port) {
|
||||||
|
$line = netstat -ano | Select-String ":$Port\s+.*LISTENING" | Select-Object -First 1
|
||||||
|
return [bool]$line
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Redis {
|
||||||
|
if (Test-PortListening 6379) {
|
||||||
|
Write-Host '[OK] Redis already listening on 6379' -ForegroundColor Green
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (-not (Test-Path $RedisExe)) {
|
||||||
|
throw "Redis executable not found: $RedisExe"
|
||||||
|
}
|
||||||
|
Write-Host '[RUN] Starting Redis on 6379 ...' -ForegroundColor Yellow
|
||||||
|
Start-Process -FilePath $RedisExe -ArgumentList "--port 6379" -WorkingDirectory $RedisDir | Out-Null
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
if (-not (Test-PortListening 6379)) {
|
||||||
|
throw "Redis failed: port 6379 not listening"
|
||||||
|
}
|
||||||
|
if (Test-Path $RedisCli) {
|
||||||
|
& $RedisCli -p 6379 ping | Out-Null
|
||||||
|
}
|
||||||
|
Write-Host '[OK] Redis started' -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-ApiPort {
|
||||||
|
if (-not (Test-PortListening $ApiPort)) {
|
||||||
|
return $ApiPort
|
||||||
|
}
|
||||||
|
Write-Host ('[WARN] Port {0} is occupied, switching to {1}' -f $ApiPort, $FallbackApiPort) -ForegroundColor Yellow
|
||||||
|
if (Test-PortListening $FallbackApiPort) {
|
||||||
|
throw "Ports $ApiPort and $FallbackApiPort are in use; free one first"
|
||||||
|
}
|
||||||
|
return $FallbackApiPort
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host '== AIAgent one-click start ==' -ForegroundColor Cyan
|
||||||
|
Write-Host "Repo: $RepoRoot"
|
||||||
|
|
||||||
|
Ensure-Redis
|
||||||
|
$RealApiPort = Resolve-ApiPort
|
||||||
|
$ApiBase = "http://127.0.0.1:$RealApiPort"
|
||||||
|
|
||||||
|
Write-Host ('[RUN] Starting backend API on {0} ...' -f $RealApiPort) -ForegroundColor Yellow
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-Command",
|
||||||
|
"cd '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $RealApiPort"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host '[RUN] Starting Celery worker ...' -ForegroundColor Yellow
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-Command",
|
||||||
|
"cd '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host '[RUN] Starting Celery Beat (scheduler) ...' -ForegroundColor Yellow
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-Command",
|
||||||
|
"cd '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app beat --loglevel=info"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host ('[RUN] Starting frontend on {0} (proxy -> {1}) ...' -f $FrontendPort, $ApiBase) -ForegroundColor Yellow
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit",
|
||||||
|
"-Command",
|
||||||
|
"`$env:AIAGENT_API_PROXY='$ApiBase'; cd '$Frontend'; pnpm dev --port $FrontendPort"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host '[DONE] Start commands issued (check new PowerShell windows)' -ForegroundColor Green
|
||||||
|
Write-Host "Frontend: http://localhost:$FrontendPort" -ForegroundColor Cyan
|
||||||
|
Write-Host "API docs: $ApiBase/docs" -ForegroundColor Cyan
|
||||||
|
Write-Host "Redis: 127.0.0.1:6379" -ForegroundColor Cyan
|
||||||
112
scripts/startup/start_aiagent_background.ps1
Normal file
112
scripts/startup/start_aiagent_background.ps1
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# 静默后台启动脚本(无弹窗,日志写文件)
|
||||||
|
# 用于 Windows 任务计划程序开机自启
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$RepoRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||||
|
$Backend = Join-Path $RepoRoot "backend"
|
||||||
|
$Frontend = Join-Path $RepoRoot "frontend"
|
||||||
|
$RedisDir = Join-Path $Backend "redis"
|
||||||
|
$RedisExe = Join-Path $RedisDir "redis-server.exe"
|
||||||
|
$RedisCli = Join-Path $RedisDir "redis-cli.exe"
|
||||||
|
$LogDir = Join-Path $RepoRoot "logs"
|
||||||
|
$LogFile = Join-Path $LogDir "autostart_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
|
||||||
|
|
||||||
|
if (-not (Test-Path $LogDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Log($msg) {
|
||||||
|
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||||
|
"$ts $msg" | Out-File -Append -FilePath $LogFile -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "========== AIAgent background start =========="
|
||||||
|
Write-Log "Repo: $RepoRoot"
|
||||||
|
|
||||||
|
# ── 1) Redis ──────────────────────────────────
|
||||||
|
$redisPort = 6379
|
||||||
|
$redisListening = netstat -ano | Select-String ":$redisPort\s+.*LISTENING"
|
||||||
|
if ($redisListening) {
|
||||||
|
Write-Log "Redis already listening on $redisPort"
|
||||||
|
} else {
|
||||||
|
if (-not (Test-Path $RedisExe)) {
|
||||||
|
Write-Log "ERROR: Redis not found at $RedisExe"
|
||||||
|
} else {
|
||||||
|
Write-Log "Starting Redis on $redisPort ..."
|
||||||
|
$redisProc = Start-Process -FilePath $RedisExe `
|
||||||
|
-ArgumentList "--port $redisPort" `
|
||||||
|
-WorkingDirectory $RedisDir `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-PassThru
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
if (Test-Path $RedisCli) {
|
||||||
|
& $RedisCli -p $redisPort ping 2>&1 | Out-File -Append $LogFile -Encoding UTF8
|
||||||
|
}
|
||||||
|
Write-Log "Redis PID=$($redisProc.Id) started"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2) Backend API ────────────────────────────
|
||||||
|
$apiPort = 8037
|
||||||
|
$apiListening = netstat -ano | Select-String ":$apiPort\s+.*LISTENING"
|
||||||
|
if ($apiListening) {
|
||||||
|
Write-Log "Backend API already listening on $apiPort"
|
||||||
|
} else {
|
||||||
|
Write-Log "Starting backend API on $apiPort ..."
|
||||||
|
$apiLog = Join-Path $LogDir "api.log"
|
||||||
|
Start-Process powershell `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-ArgumentList @(
|
||||||
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $apiPort 2>&1 | Out-File -Append '$apiLog' -Encoding UTF8"
|
||||||
|
)
|
||||||
|
Write-Log "Backend API starting (log: $apiLog)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3) Celery Worker ──────────────────────────
|
||||||
|
Write-Log "Starting Celery worker ..."
|
||||||
|
$celeryLog = Join-Path $LogDir "celery.log"
|
||||||
|
Start-Process powershell `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-ArgumentList @(
|
||||||
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8 2>&1 | Out-File -Append '$celeryLog' -Encoding UTF8"
|
||||||
|
)
|
||||||
|
Write-Log "Celery worker starting (log: $celeryLog)"
|
||||||
|
|
||||||
|
# ── 3.5) Celery Beat (scheduler) ──────────────
|
||||||
|
Write-Log "Starting Celery Beat ..."
|
||||||
|
$beatLog = Join-Path $LogDir "celery_beat.log"
|
||||||
|
Start-Process powershell `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-ArgumentList @(
|
||||||
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app beat --loglevel=info 2>&1 | Out-File -Append '$beatLog' -Encoding UTF8"
|
||||||
|
)
|
||||||
|
Write-Log "Celery Beat starting (log: $beatLog)"
|
||||||
|
|
||||||
|
# ── 4) Frontend (pnpm dev) ────────────────────
|
||||||
|
$frontendPort = 3001
|
||||||
|
$frontendListening = netstat -ano | Select-String ":$frontendPort\s+.*LISTENING"
|
||||||
|
if ($frontendListening) {
|
||||||
|
Write-Log "Frontend already listening on $frontendPort"
|
||||||
|
} else {
|
||||||
|
Write-Log "Starting frontend on $frontendPort ..."
|
||||||
|
$frontendLog = Join-Path $LogDir "frontend.log"
|
||||||
|
Start-Process powershell `
|
||||||
|
-WindowStyle Hidden `
|
||||||
|
-ArgumentList @(
|
||||||
|
"-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||||
|
"-Command",
|
||||||
|
"`$env:AIAGENT_API_PROXY='http://127.0.0.1:$apiPort'; Set-Location '$Frontend'; pnpm dev --port $frontendPort 2>&1 | Out-File -Append '$frontendLog' -Encoding UTF8"
|
||||||
|
)
|
||||||
|
Write-Log "Frontend starting (log: $frontendLog)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "========== All services issued =========="
|
||||||
|
Write-Log "Frontend: http://localhost:$frontendPort"
|
||||||
|
Write-Log "API docs: http://127.0.0.1:$apiPort/docs"
|
||||||
87
scripts/startup/stop_aiagent.ps1
Normal file
87
scripts/startup/stop_aiagent.ps1
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
$ErrorActionPreference = "SilentlyContinue"
|
||||||
|
|
||||||
|
Write-Host "== AIAgent stop ==" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
function Get-PidsListeningOnPort([int]$Port) {
|
||||||
|
$pids = New-Object System.Collections.Generic.HashSet[int]
|
||||||
|
try {
|
||||||
|
netstat -ano | ForEach-Object {
|
||||||
|
$ln = $_.Trim()
|
||||||
|
if ($ln -notmatch "LISTENING") { return }
|
||||||
|
# 匹配 :8037 或 :3001 等端口后的 LISTENING 行末 PID
|
||||||
|
if ($ln -match ":$Port\s+.*LISTENING\s+(\d+)\s*$") {
|
||||||
|
[void]$pids.Add([int]$Matches[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return @($pids)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stop-OnPorts([int[]]$ports, [string]$name) {
|
||||||
|
$all = New-Object System.Collections.Generic.HashSet[int]
|
||||||
|
foreach ($p in $ports) {
|
||||||
|
foreach ($pid in (Get-PidsListeningOnPort $p)) {
|
||||||
|
[void]$all.Add($pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($all.Count -eq 0) {
|
||||||
|
Write-Host "[SKIP] ${name}: no listener on ports $($ports -join ',')" -ForegroundColor DarkGray
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foreach ($pid in $all) {
|
||||||
|
if ($pid -le 4) { continue }
|
||||||
|
try {
|
||||||
|
Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "[OK] stopped ${name} PID=$pid" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "[WARN] failed to stop ${name} PID=$pid" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 后端 API(8037 / 8041 备用)
|
||||||
|
Stop-OnPorts @(8037, 8041) "backend-api"
|
||||||
|
|
||||||
|
# 前端 Vite(3001)
|
||||||
|
Stop-OnPorts @(3001) "frontend-dev"
|
||||||
|
|
||||||
|
# Redis(6379,仅当监听在本地开发端口时结束;若与其它项目共用请谨慎)
|
||||||
|
Stop-OnPorts @(6379) "redis"
|
||||||
|
|
||||||
|
# Celery Worker / Beat(不监听端口,需要按命令行匹配)
|
||||||
|
$celeryPatterns = @(
|
||||||
|
@{Pattern='celery\s+-A\s+app\.core\.celery_app\s+worker'; Name='celery-worker'},
|
||||||
|
@{Pattern='celery\s+-A\s+app\.core\.celery_app\s+beat'; Name='celery-beat'}
|
||||||
|
)
|
||||||
|
foreach ($cp in $celeryPatterns) {
|
||||||
|
$targets = Get-CimInstance Win32_Process | Where-Object {
|
||||||
|
$_.CommandLine -and $_.CommandLine -match $cp.Pattern
|
||||||
|
}
|
||||||
|
if (-not $targets) {
|
||||||
|
Write-Host "[SKIP] $($cp.Name): no matching process" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
foreach ($p in $targets) {
|
||||||
|
try {
|
||||||
|
Stop-Process -Id $p.ProcessId -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "[OK] stopped $($cp.Name) PID=$($p.ProcessId)" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "[WARN] failed to stop $($cp.Name) PID=$($p.ProcessId)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 600
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Port check:" -ForegroundColor Cyan
|
||||||
|
foreach ($port in 3001, 8037, 8041, 6379) {
|
||||||
|
$line = netstat -ano | Select-String ":$port\s+.*LISTENING" | Select-Object -First 1
|
||||||
|
if ($line) {
|
||||||
|
Write-Host " - ${port}: LISTEN" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " - ${port}: free" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "DONE: stop script finished" -ForegroundColor Green
|
||||||
Reference in New Issue
Block a user