2026-03-31 04:31:47 +00:00
|
|
|
|
import logging
|
|
|
|
|
|
import os
|
|
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
|
from sqlmodel import Session
|
|
|
|
|
|
|
|
|
|
|
|
from core.docker_instance import docker_manager
|
2026-04-04 16:29:37 +00:00
|
|
|
|
from core.utils import _is_video_attachment_path, _is_visual_attachment_path
|
2026-03-31 04:31:47 +00:00
|
|
|
|
from models.bot import BotInstance
|
2026-04-04 16:29:37 +00:00
|
|
|
|
from services.bot_service import read_bot_runtime_snapshot
|
2026-04-14 02:04:12 +00:00
|
|
|
|
from services.platform_activity_service import record_activity_event
|
|
|
|
|
|
from services.platform_usage_service import create_usage_request, fail_latest_usage
|
2026-04-04 16:29:37 +00:00
|
|
|
|
from services.runtime_service import broadcast_runtime_packet, persist_runtime_packet
|
|
|
|
|
|
from services.workspace_service import resolve_workspace_path
|
2026-03-31 04:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("dashboard.backend")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_message_media_item(value: Any) -> str:
|
|
|
|
|
|
return str(value or "").strip().replace("\\", "/").lstrip("/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_message_media_list(raw: Any) -> List[str]:
|
|
|
|
|
|
if not isinstance(raw, list):
|
|
|
|
|
|
return []
|
|
|
|
|
|
rows: List[str] = []
|
|
|
|
|
|
for value in raw:
|
|
|
|
|
|
normalized = _normalize_message_media_item(value)
|
|
|
|
|
|
if normalized:
|
|
|
|
|
|
rows.append(normalized)
|
|
|
|
|
|
return rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_delivery_command(command: str, checked_attachments: List[str]) -> str:
|
|
|
|
|
|
if not checked_attachments:
|
|
|
|
|
|
return command
|
|
|
|
|
|
|
|
|
|
|
|
attachment_block = "\n".join(f"- {path}" for path in checked_attachments)
|
|
|
|
|
|
if all(_is_visual_attachment_path(path) for path in checked_attachments):
|
|
|
|
|
|
has_video = any(_is_video_attachment_path(path) for path in checked_attachments)
|
|
|
|
|
|
media_label = "图片/视频" if has_video else "图片"
|
|
|
|
|
|
capability_hint = (
|
|
|
|
|
|
"1) 附件已随请求附带;图片在可用时可直接作为多模态输入理解,视频请按附件路径处理。\n"
|
|
|
|
|
|
if has_video
|
|
|
|
|
|
else "1) 附件中的图片已作为多模态输入提供,优先直接理解并回答。\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
if command:
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"{command}\n\n"
|
|
|
|
|
|
"[Attached files]\n"
|
|
|
|
|
|
f"{attachment_block}\n\n"
|
|
|
|
|
|
"【附件处理要求】\n"
|
|
|
|
|
|
f"{capability_hint}"
|
|
|
|
|
|
"2) 若当前模型或接口不支持直接理解该附件,请明确说明后再调用工具解析。\n"
|
|
|
|
|
|
"3) 除非用户明确要求,不要先调用工具读取附件文件。\n"
|
|
|
|
|
|
"4) 回复语言必须遵循 USER.md;若未指定,则与用户当前输入语言保持一致。\n"
|
|
|
|
|
|
"5) 仅基于可见内容回答;看不清或无法确认的部分请明确说明,不要猜测。"
|
|
|
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
|
|
|
"请先处理已附带的附件列表:\n"
|
|
|
|
|
|
f"{attachment_block}\n\n"
|
|
|
|
|
|
f"请直接分析已附带的{media_label}并总结关键信息。\n"
|
|
|
|
|
|
f"{'图片在可用时可直接作为多模态输入理解,视频请按附件路径处理。' if has_video else ''}\n"
|
|
|
|
|
|
"若当前模型或接口不支持直接理解该附件,请明确说明后再调用工具解析。\n"
|
|
|
|
|
|
"回复语言必须遵循 USER.md;若未指定,则与用户当前输入语言保持一致。\n"
|
|
|
|
|
|
"仅基于可见内容回答;看不清或无法确认的部分请明确说明,不要猜测。"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
command_has_paths = all(path in command for path in checked_attachments) if command else False
|
|
|
|
|
|
if command and not command_has_paths:
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"{command}\n\n"
|
|
|
|
|
|
"[Attached files]\n"
|
|
|
|
|
|
f"{attachment_block}\n\n"
|
|
|
|
|
|
"Please process the attached file(s) listed above when answering this request.\n"
|
|
|
|
|
|
"Reply language must follow USER.md. If not specified, use the same language as the user input."
|
|
|
|
|
|
)
|
|
|
|
|
|
if not command:
|
|
|
|
|
|
return (
|
|
|
|
|
|
"Please process the uploaded file(s) listed below:\n"
|
|
|
|
|
|
f"{attachment_block}\n\n"
|
|
|
|
|
|
"Reply language must follow USER.md. If not specified, use the same language as the user input."
|
|
|
|
|
|
)
|
|
|
|
|
|
return command
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_bot_command(session: Session, bot_id: str, command: str, attachments: Any) -> Dict[str, Any]:
|
|
|
|
|
|
request_id = ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
bot = session.get(BotInstance, bot_id)
|
|
|
|
|
|
if not bot:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="Bot not found")
|
2026-04-04 16:29:37 +00:00
|
|
|
|
runtime_snapshot = read_bot_runtime_snapshot(bot)
|
2026-03-31 04:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
normalized_attachments = _normalize_message_media_list(attachments)
|
|
|
|
|
|
text_command = str(command or "").strip()
|
|
|
|
|
|
if not text_command and not normalized_attachments:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="Command or attachments is required")
|
|
|
|
|
|
|
|
|
|
|
|
checked_attachments: List[str] = []
|
|
|
|
|
|
for rel_path in normalized_attachments:
|
2026-04-04 16:29:37 +00:00
|
|
|
|
_, target = resolve_workspace_path(bot_id, rel_path)
|
2026-03-31 04:31:47 +00:00
|
|
|
|
if not os.path.isfile(target):
|
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"attachment not found: {rel_path}")
|
|
|
|
|
|
checked_attachments.append(rel_path)
|
|
|
|
|
|
delivery_media = [f"/root/.nanobot/workspace/{path.lstrip('/')}" for path in checked_attachments]
|
|
|
|
|
|
|
|
|
|
|
|
display_command = text_command if text_command else "[attachment message]"
|
|
|
|
|
|
delivery_command = _build_delivery_command(text_command, checked_attachments)
|
|
|
|
|
|
|
|
|
|
|
|
request_id = create_usage_request(
|
|
|
|
|
|
session,
|
|
|
|
|
|
bot_id,
|
|
|
|
|
|
display_command,
|
|
|
|
|
|
attachments=checked_attachments,
|
|
|
|
|
|
channel="dashboard",
|
|
|
|
|
|
metadata={"attachment_count": len(checked_attachments)},
|
|
|
|
|
|
provider=str(runtime_snapshot.get("llm_provider") or "").strip() or None,
|
|
|
|
|
|
model=str(runtime_snapshot.get("llm_model") or "").strip() or None,
|
|
|
|
|
|
)
|
|
|
|
|
|
record_activity_event(
|
|
|
|
|
|
session,
|
|
|
|
|
|
bot_id,
|
|
|
|
|
|
"command_submitted",
|
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
|
channel="dashboard",
|
|
|
|
|
|
detail="command submitted",
|
|
|
|
|
|
metadata={"attachment_count": len(checked_attachments), "has_text": bool(text_command)},
|
|
|
|
|
|
)
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
outbound_user_packet: Dict[str, Any] | None = None
|
|
|
|
|
|
if display_command or checked_attachments:
|
|
|
|
|
|
outbound_user_packet = {
|
|
|
|
|
|
"type": "USER_COMMAND",
|
|
|
|
|
|
"channel": "dashboard",
|
|
|
|
|
|
"text": display_command,
|
|
|
|
|
|
"media": checked_attachments,
|
|
|
|
|
|
"request_id": request_id,
|
|
|
|
|
|
}
|
2026-04-04 16:29:37 +00:00
|
|
|
|
persist_runtime_packet(bot_id, outbound_user_packet)
|
2026-03-31 04:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
if outbound_user_packet:
|
2026-04-04 16:29:37 +00:00
|
|
|
|
broadcast_runtime_packet(bot_id, outbound_user_packet)
|
2026-03-31 04:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
success = docker_manager.send_command(bot_id, delivery_command, media=delivery_media)
|
|
|
|
|
|
if success:
|
|
|
|
|
|
return {"success": True}
|
|
|
|
|
|
|
|
|
|
|
|
detail = docker_manager.get_last_delivery_error(bot_id)
|
|
|
|
|
|
fail_latest_usage(session, bot_id, detail or "command delivery failed")
|
|
|
|
|
|
record_activity_event(
|
|
|
|
|
|
session,
|
|
|
|
|
|
bot_id,
|
|
|
|
|
|
"command_failed",
|
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
|
channel="dashboard",
|
|
|
|
|
|
detail=(detail or "command delivery failed")[:400],
|
|
|
|
|
|
)
|
|
|
|
|
|
session.commit()
|
2026-04-04 16:29:37 +00:00
|
|
|
|
broadcast_runtime_packet(
|
2026-03-31 04:31:47 +00:00
|
|
|
|
bot_id,
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "AGENT_STATE",
|
|
|
|
|
|
"channel": "dashboard",
|
|
|
|
|
|
"payload": {
|
|
|
|
|
|
"state": "ERROR",
|
|
|
|
|
|
"action_msg": detail or "command delivery failed",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=502,
|
|
|
|
|
|
detail=f"Failed to deliver command to bot dashboard channel{': ' + detail if detail else ''}",
|
|
|
|
|
|
)
|
|
|
|
|
|
except HTTPException:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
logger.exception("send_bot_command failed for bot_id=%s", bot_id)
|
|
|
|
|
|
try:
|
|
|
|
|
|
session.rollback()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
if request_id:
|
|
|
|
|
|
try:
|
|
|
|
|
|
fail_latest_usage(session, bot_id, str(exc))
|
|
|
|
|
|
record_activity_event(
|
|
|
|
|
|
session,
|
|
|
|
|
|
bot_id,
|
|
|
|
|
|
"command_failed",
|
|
|
|
|
|
request_id=request_id,
|
|
|
|
|
|
channel="dashboard",
|
|
|
|
|
|
detail=str(exc)[:400],
|
|
|
|
|
|
)
|
|
|
|
|
|
session.commit()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
try:
|
|
|
|
|
|
session.rollback()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Failed to process bot command: {exc}") from exc
|