v0.1.4-p5
parent
ca1f941e4c
commit
ae34bfc6a0
|
|
@ -16,9 +16,9 @@ from services.bot_management_service import (
|
||||||
create_bot_record,
|
create_bot_record,
|
||||||
get_bot_detail_cached,
|
get_bot_detail_cached,
|
||||||
list_bots_with_cache,
|
list_bots_with_cache,
|
||||||
test_provider_connection,
|
|
||||||
update_bot_record,
|
update_bot_record,
|
||||||
)
|
)
|
||||||
|
from services.provider_service import test_provider_connection
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from core.cache import cache
|
||||||
|
from core.database import engine
|
||||||
|
from core.settings import DATABASE_ENGINE, REDIS_ENABLED, REDIS_PREFIX, REDIS_URL
|
||||||
|
from models.bot import BotInstance
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/health")
|
||||||
|
def get_health():
|
||||||
|
try:
|
||||||
|
with Session(engine) as session:
|
||||||
|
session.exec(select(BotInstance).limit(1)).first()
|
||||||
|
return {"status": "ok", "database": DATABASE_ENGINE}
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=503, detail=f"database check failed: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/health/cache")
|
||||||
|
def get_cache_health():
|
||||||
|
redis_url = str(REDIS_URL or "").strip()
|
||||||
|
configured = bool(REDIS_ENABLED and redis_url)
|
||||||
|
client_enabled = bool(getattr(cache, "enabled", False))
|
||||||
|
reachable = bool(cache.ping()) if client_enabled else False
|
||||||
|
status = "ok"
|
||||||
|
if configured and not reachable:
|
||||||
|
status = "degraded"
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"cache": {
|
||||||
|
"configured": configured,
|
||||||
|
"enabled": client_enabled,
|
||||||
|
"reachable": reachable,
|
||||||
|
"prefix": REDIS_PREFIX,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,121 +1,31 @@
|
||||||
from typing import Any, Dict, List
|
from typing import Dict
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session
|
||||||
|
|
||||||
from core.cache import cache
|
|
||||||
from core.database import get_session
|
from core.database import get_session
|
||||||
from core.docker_instance import docker_manager
|
from services.image_service import (
|
||||||
from models.bot import BotInstance, NanobotImage
|
delete_registered_image,
|
||||||
from services.cache_service import _cache_key_images, _invalidate_images_cache
|
list_docker_images_by_repository,
|
||||||
|
list_registered_images,
|
||||||
|
register_image as register_image_record,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _serialize_image(row: NanobotImage) -> Dict[str, Any]:
|
|
||||||
created_at = row.created_at.isoformat() + "Z" if row.created_at else None
|
|
||||||
return {
|
|
||||||
"tag": row.tag,
|
|
||||||
"image_id": row.image_id,
|
|
||||||
"version": row.version,
|
|
||||||
"status": row.status,
|
|
||||||
"source_dir": row.source_dir,
|
|
||||||
"created_at": created_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _reconcile_registered_images(session: Session) -> None:
|
|
||||||
rows = session.exec(select(NanobotImage)).all()
|
|
||||||
dirty = False
|
|
||||||
for row in rows:
|
|
||||||
docker_exists = docker_manager.has_image(row.tag)
|
|
||||||
next_status = "READY" if docker_exists else "ERROR"
|
|
||||||
next_image_id = row.image_id
|
|
||||||
if docker_exists and docker_manager.client:
|
|
||||||
try:
|
|
||||||
next_image_id = docker_manager.client.images.get(row.tag).id
|
|
||||||
except Exception:
|
|
||||||
next_image_id = row.image_id
|
|
||||||
if row.status != next_status or row.image_id != next_image_id:
|
|
||||||
row.status = next_status
|
|
||||||
row.image_id = next_image_id
|
|
||||||
session.add(row)
|
|
||||||
dirty = True
|
|
||||||
if dirty:
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/images")
|
@router.get("/api/images")
|
||||||
def list_images(session: Session = Depends(get_session)):
|
def list_images(session: Session = Depends(get_session)):
|
||||||
cached = cache.get_json(_cache_key_images())
|
return list_registered_images(session)
|
||||||
if isinstance(cached, list) and all(isinstance(row, dict) for row in cached):
|
|
||||||
return cached
|
|
||||||
if isinstance(cached, list):
|
|
||||||
_invalidate_images_cache()
|
|
||||||
try:
|
|
||||||
_reconcile_registered_images(session)
|
|
||||||
except Exception as exc:
|
|
||||||
# Docker status probing should not break image management in dev mode.
|
|
||||||
print(f"[image_router] reconcile images skipped: {exc}")
|
|
||||||
rows = session.exec(select(NanobotImage).order_by(NanobotImage.created_at.desc())).all()
|
|
||||||
payload = [_serialize_image(row) for row in rows]
|
|
||||||
cache.set_json(_cache_key_images(), payload, ttl=60)
|
|
||||||
return payload
|
|
||||||
|
|
||||||
@router.delete("/api/images/{tag:path}")
|
@router.delete("/api/images/{tag:path}")
|
||||||
def delete_image(tag: str, session: Session = Depends(get_session)):
|
def delete_image(tag: str, session: Session = Depends(get_session)):
|
||||||
image = session.get(NanobotImage, tag)
|
return delete_registered_image(session, tag=tag)
|
||||||
if not image:
|
|
||||||
raise HTTPException(status_code=404, detail="Image not found")
|
|
||||||
|
|
||||||
# 检查是否有机器人正在使用此镜像
|
|
||||||
bots_using = session.exec(select(BotInstance).where(BotInstance.image_tag == tag)).all()
|
|
||||||
if bots_using:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Cannot delete image: {len(bots_using)} bots are using it.")
|
|
||||||
|
|
||||||
session.delete(image)
|
|
||||||
session.commit()
|
|
||||||
_invalidate_images_cache()
|
|
||||||
return {"status": "deleted"}
|
|
||||||
|
|
||||||
@router.get("/api/docker-images")
|
@router.get("/api/docker-images")
|
||||||
def list_docker_images(repository: str = "nanobot-base"):
|
def list_docker_images(repository: str = "nanobot-base"):
|
||||||
rows = docker_manager.list_images_by_repo(repository)
|
return list_docker_images_by_repository(repository)
|
||||||
return rows
|
|
||||||
|
|
||||||
@router.post("/api/images/register")
|
@router.post("/api/images/register")
|
||||||
def register_image(payload: dict, session: Session = Depends(get_session)):
|
def register_image(payload: dict, session: Session = Depends(get_session)):
|
||||||
tag = (payload.get("tag") or "").strip()
|
return register_image_record(session, payload)
|
||||||
source_dir = (payload.get("source_dir") or "manual").strip() or "manual"
|
|
||||||
if not tag:
|
|
||||||
raise HTTPException(status_code=400, detail="tag is required")
|
|
||||||
|
|
||||||
if not docker_manager.has_image(tag):
|
|
||||||
raise HTTPException(status_code=404, detail=f"Docker image not found: {tag}")
|
|
||||||
|
|
||||||
version = tag.split(":")[-1].removeprefix("v") if ":" in tag else tag
|
|
||||||
try:
|
|
||||||
docker_img = docker_manager.client.images.get(tag) if docker_manager.client else None
|
|
||||||
image_id = docker_img.id if docker_img else None
|
|
||||||
except Exception:
|
|
||||||
image_id = None
|
|
||||||
|
|
||||||
row = session.get(NanobotImage, tag)
|
|
||||||
if not row:
|
|
||||||
row = NanobotImage(
|
|
||||||
tag=tag,
|
|
||||||
version=version,
|
|
||||||
status="READY",
|
|
||||||
source_dir=source_dir,
|
|
||||||
image_id=image_id,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
row.version = version
|
|
||||||
row.status = "READY"
|
|
||||||
row.source_dir = source_dir
|
|
||||||
row.image_id = image_id
|
|
||||||
session.add(row)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(row)
|
|
||||||
_invalidate_images_cache()
|
|
||||||
return _serialize_image(row)
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from core.database import get_session
|
||||||
|
from core.settings import PANEL_ACCESS_PASSWORD
|
||||||
|
from schemas.system import PanelLoginRequest
|
||||||
|
from services.platform_auth_service import (
|
||||||
|
clear_panel_token_cookie,
|
||||||
|
create_panel_token,
|
||||||
|
resolve_panel_request_auth,
|
||||||
|
revoke_panel_token,
|
||||||
|
set_panel_token_cookie,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/panel/auth/status")
|
||||||
|
def get_panel_auth_status(request: Request, session: Session = Depends(get_session)):
|
||||||
|
configured = str(PANEL_ACCESS_PASSWORD or "").strip()
|
||||||
|
principal = resolve_panel_request_auth(session, request)
|
||||||
|
return {
|
||||||
|
"enabled": bool(configured),
|
||||||
|
"authenticated": bool(principal.authenticated),
|
||||||
|
"auth_source": principal.auth_source if principal.authenticated else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/panel/auth/login")
|
||||||
|
def panel_login(
|
||||||
|
payload: PanelLoginRequest,
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
configured = str(PANEL_ACCESS_PASSWORD or "").strip()
|
||||||
|
if not configured:
|
||||||
|
clear_panel_token_cookie(response)
|
||||||
|
return {"success": True, "enabled": False}
|
||||||
|
supplied = str(payload.password or "").strip()
|
||||||
|
if supplied != configured:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid panel access password")
|
||||||
|
try:
|
||||||
|
raw_token = create_panel_token(session, request)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||||
|
set_panel_token_cookie(response, request, raw_token, session)
|
||||||
|
return {"success": True, "enabled": True, "authenticated": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/panel/auth/logout")
|
||||||
|
def panel_logout(request: Request, response: Response, session: Session = Depends(get_session)):
|
||||||
|
revoke_panel_token(session, request)
|
||||||
|
clear_panel_token_cookie(response)
|
||||||
|
return {"success": True}
|
||||||
|
|
@ -3,6 +3,7 @@ from typing import Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from bootstrap.app_runtime import reload_platform_runtime
|
||||||
from core.cache import cache
|
from core.cache import cache
|
||||||
from core.database import get_session
|
from core.database import get_session
|
||||||
from schemas.platform import PlatformSettingsPayload, SystemSettingPayload
|
from schemas.platform import PlatformSettingsPayload, SystemSettingPayload
|
||||||
|
|
@ -22,13 +23,6 @@ from services.platform_service import (
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _apply_platform_runtime_changes(request: Request) -> None:
|
|
||||||
cache.delete_prefix("")
|
|
||||||
speech_service = getattr(request.app.state, "speech_service", None)
|
|
||||||
if speech_service is not None and hasattr(speech_service, "reset_runtime"):
|
|
||||||
speech_service.reset_runtime()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/platform/overview")
|
@router.get("/api/platform/overview")
|
||||||
def get_platform_overview(request: Request, session: Session = Depends(get_session)):
|
def get_platform_overview(request: Request, session: Session = Depends(get_session)):
|
||||||
docker_manager = getattr(request.app.state, "docker_manager", None)
|
docker_manager = getattr(request.app.state, "docker_manager", None)
|
||||||
|
|
@ -43,7 +37,7 @@ def get_platform_settings_api(session: Session = Depends(get_session)):
|
||||||
@router.put("/api/platform/settings")
|
@router.put("/api/platform/settings")
|
||||||
def update_platform_settings_api(payload: PlatformSettingsPayload, request: Request, session: Session = Depends(get_session)):
|
def update_platform_settings_api(payload: PlatformSettingsPayload, request: Request, session: Session = Depends(get_session)):
|
||||||
result = save_platform_settings(session, payload).model_dump()
|
result = save_platform_settings(session, payload).model_dump()
|
||||||
_apply_platform_runtime_changes(request)
|
reload_platform_runtime(request.app)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,8 +48,8 @@ def clear_platform_cache():
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/platform/reload")
|
@router.post("/api/platform/reload")
|
||||||
def reload_platform_runtime(request: Request):
|
def reload_platform_runtime_api(request: Request):
|
||||||
_apply_platform_runtime_changes(request)
|
reload_platform_runtime(request.app)
|
||||||
return {"status": "reloaded"}
|
return {"status": "reloaded"}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -107,7 +101,7 @@ def get_system_settings(search: str = "", session: Session = Depends(get_session
|
||||||
def create_system_setting(payload: SystemSettingPayload, request: Request, session: Session = Depends(get_session)):
|
def create_system_setting(payload: SystemSettingPayload, request: Request, session: Session = Depends(get_session)):
|
||||||
try:
|
try:
|
||||||
result = create_or_update_system_setting(session, payload)
|
result = create_or_update_system_setting(session, payload)
|
||||||
_apply_platform_runtime_changes(request)
|
reload_platform_runtime(request.app)
|
||||||
return result
|
return result
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
@ -117,7 +111,7 @@ def create_system_setting(payload: SystemSettingPayload, request: Request, sessi
|
||||||
def update_system_setting(key: str, payload: SystemSettingPayload, request: Request, session: Session = Depends(get_session)):
|
def update_system_setting(key: str, payload: SystemSettingPayload, request: Request, session: Session = Depends(get_session)):
|
||||||
try:
|
try:
|
||||||
result = create_or_update_system_setting(session, payload.model_copy(update={"key": key}))
|
result = create_or_update_system_setting(session, payload.model_copy(update={"key": key}))
|
||||||
_apply_platform_runtime_changes(request)
|
reload_platform_runtime(request.app)
|
||||||
return result
|
return result
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
@ -127,7 +121,7 @@ def update_system_setting(key: str, payload: SystemSettingPayload, request: Requ
|
||||||
def remove_system_setting(key: str, request: Request, session: Session = Depends(get_session)):
|
def remove_system_setting(key: str, request: Request, session: Session = Depends(get_session)):
|
||||||
try:
|
try:
|
||||||
delete_system_setting(session, key)
|
delete_system_setting(session, key)
|
||||||
_apply_platform_runtime_changes(request)
|
reload_platform_runtime(request.app)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
return {"status": "deleted", "key": key}
|
return {"status": "deleted", "key": key}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,7 @@
|
||||||
from typing import Any, Dict
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
||||||
from sqlmodel import Session, select
|
|
||||||
|
|
||||||
from core.database import engine, get_session
|
|
||||||
from core.settings import DATABASE_ENGINE, PANEL_ACCESS_PASSWORD, REDIS_ENABLED, REDIS_PREFIX, REDIS_URL
|
|
||||||
from core.utils import _get_default_system_timezone
|
from core.utils import _get_default_system_timezone
|
||||||
from models.bot import BotInstance
|
from schemas.system import SystemTemplatesUpdateRequest
|
||||||
from schemas.system import PanelLoginRequest, SystemTemplatesUpdateRequest
|
|
||||||
from core.cache import cache
|
|
||||||
from services.platform_auth_service import (
|
|
||||||
clear_panel_token_cookie,
|
|
||||||
create_panel_token,
|
|
||||||
resolve_panel_request_auth,
|
|
||||||
revoke_panel_token,
|
|
||||||
set_panel_token_cookie,
|
|
||||||
)
|
|
||||||
from services.platform_service import get_platform_settings_snapshot, get_speech_runtime_settings
|
from services.platform_service import get_platform_settings_snapshot, get_speech_runtime_settings
|
||||||
from services.template_service import (
|
from services.template_service import (
|
||||||
get_agent_md_templates,
|
get_agent_md_templates,
|
||||||
|
|
@ -26,40 +12,6 @@ from services.template_service import (
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/panel/auth/status")
|
|
||||||
def get_panel_auth_status(request: Request, session: Session = Depends(get_session)):
|
|
||||||
configured = str(PANEL_ACCESS_PASSWORD or "").strip()
|
|
||||||
principal = resolve_panel_request_auth(session, request)
|
|
||||||
return {
|
|
||||||
"enabled": bool(configured),
|
|
||||||
"authenticated": bool(principal.authenticated),
|
|
||||||
"auth_source": principal.auth_source if principal.authenticated else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
@router.post("/api/panel/auth/login")
|
|
||||||
def panel_login(payload: PanelLoginRequest, request: Request, response: Response, session: Session = Depends(get_session)):
|
|
||||||
configured = str(PANEL_ACCESS_PASSWORD or "").strip()
|
|
||||||
if not configured:
|
|
||||||
clear_panel_token_cookie(response)
|
|
||||||
return {"success": True, "enabled": False}
|
|
||||||
supplied = str(payload.password or "").strip()
|
|
||||||
if supplied != configured:
|
|
||||||
raise HTTPException(status_code=401, detail="Invalid panel access password")
|
|
||||||
try:
|
|
||||||
raw_token = create_panel_token(session, request)
|
|
||||||
except RuntimeError as exc:
|
|
||||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
||||||
set_panel_token_cookie(response, request, raw_token, session)
|
|
||||||
return {"success": True, "enabled": True, "authenticated": True}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/panel/auth/logout")
|
|
||||||
def panel_logout(request: Request, response: Response, session: Session = Depends(get_session)):
|
|
||||||
revoke_panel_token(session, request)
|
|
||||||
clear_panel_token_cookie(response)
|
|
||||||
return {"success": True}
|
|
||||||
|
|
||||||
@router.get("/api/system/defaults")
|
@router.get("/api/system/defaults")
|
||||||
def get_system_defaults():
|
def get_system_defaults():
|
||||||
md_templates = get_agent_md_templates()
|
md_templates = get_agent_md_templates()
|
||||||
|
|
@ -115,31 +67,3 @@ def update_system_templates(payload: SystemTemplatesUpdateRequest):
|
||||||
"agent_md_templates": get_agent_md_templates(),
|
"agent_md_templates": get_agent_md_templates(),
|
||||||
"topic_presets": get_topic_presets(),
|
"topic_presets": get_topic_presets(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@router.get("/api/health")
|
|
||||||
def get_health():
|
|
||||||
try:
|
|
||||||
with Session(engine) as session:
|
|
||||||
session.exec(select(BotInstance).limit(1)).first()
|
|
||||||
return {"status": "ok", "database": DATABASE_ENGINE}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=503, detail=f"database check failed: {e}")
|
|
||||||
|
|
||||||
@router.get("/api/health/cache")
|
|
||||||
def get_cache_health():
|
|
||||||
redis_url = str(REDIS_URL or "").strip()
|
|
||||||
configured = bool(REDIS_ENABLED and redis_url)
|
|
||||||
client_enabled = bool(getattr(cache, "enabled", False))
|
|
||||||
reachable = bool(cache.ping()) if client_enabled else False
|
|
||||||
status = "ok"
|
|
||||||
if configured and not reachable:
|
|
||||||
status = "degraded"
|
|
||||||
return {
|
|
||||||
"status": status,
|
|
||||||
"cache": {
|
|
||||||
"configured": configured,
|
|
||||||
"enabled": client_enabled,
|
|
||||||
"reachable": reachable,
|
|
||||||
"prefix": REDIS_PREFIX,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,24 @@
|
||||||
import json
|
from typing import Any, Dict, Optional
|
||||||
from datetime import datetime
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import func
|
from sqlmodel import Session
|
||||||
from sqlmodel import Session, select
|
|
||||||
|
|
||||||
from core.database import get_session
|
from core.database import get_session
|
||||||
from models.bot import BotInstance
|
|
||||||
from models.topic import TopicItem, TopicTopic
|
|
||||||
from services.topic_service import (
|
from services.topic_service import (
|
||||||
_TOPIC_KEY_RE,
|
create_topic,
|
||||||
_list_topics,
|
delete_topic,
|
||||||
_normalize_topic_key,
|
delete_topic_item,
|
||||||
_topic_item_to_dict,
|
get_topic_item_stats,
|
||||||
_topic_to_dict,
|
list_topic_items,
|
||||||
|
list_topics,
|
||||||
|
mark_topic_item_read,
|
||||||
|
update_topic,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _count_topic_items(
|
|
||||||
session: Session,
|
|
||||||
bot_id: str,
|
|
||||||
topic_key: Optional[str] = None,
|
|
||||||
unread_only: bool = False,
|
|
||||||
) -> int:
|
|
||||||
stmt = select(func.count()).select_from(TopicItem).where(TopicItem.bot_id == bot_id)
|
|
||||||
normalized_topic_key = _normalize_topic_key(topic_key or "")
|
|
||||||
if normalized_topic_key:
|
|
||||||
stmt = stmt.where(TopicItem.topic_key == normalized_topic_key)
|
|
||||||
if unread_only:
|
|
||||||
stmt = stmt.where(TopicItem.is_read == False) # noqa: E712
|
|
||||||
value = session.exec(stmt).one()
|
|
||||||
return int(value or 0)
|
|
||||||
|
|
||||||
|
|
||||||
class TopicCreateRequest(BaseModel):
|
class TopicCreateRequest(BaseModel):
|
||||||
topic_key: str
|
topic_key: str
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
|
@ -56,112 +38,31 @@ class TopicUpdateRequest(BaseModel):
|
||||||
|
|
||||||
@router.get("/api/bots/{bot_id}/topics")
|
@router.get("/api/bots/{bot_id}/topics")
|
||||||
def list_bot_topics(bot_id: str, session: Session = Depends(get_session)):
|
def list_bot_topics(bot_id: str, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return list_topics(session, bot_id)
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
return _list_topics(session, bot_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/bots/{bot_id}/topics")
|
@router.post("/api/bots/{bot_id}/topics")
|
||||||
def create_bot_topic(bot_id: str, payload: TopicCreateRequest, session: Session = Depends(get_session)):
|
def create_bot_topic(bot_id: str, payload: TopicCreateRequest, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return create_topic(
|
||||||
if not bot:
|
session,
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
topic_key = _normalize_topic_key(payload.topic_key)
|
|
||||||
if not topic_key:
|
|
||||||
raise HTTPException(status_code=400, detail="topic_key is required")
|
|
||||||
if not _TOPIC_KEY_RE.fullmatch(topic_key):
|
|
||||||
raise HTTPException(status_code=400, detail="invalid topic_key")
|
|
||||||
exists = session.exec(
|
|
||||||
select(TopicTopic)
|
|
||||||
.where(TopicTopic.bot_id == bot_id)
|
|
||||||
.where(TopicTopic.topic_key == topic_key)
|
|
||||||
.limit(1)
|
|
||||||
).first()
|
|
||||||
if exists:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Topic already exists: {topic_key}")
|
|
||||||
|
|
||||||
now = datetime.utcnow()
|
|
||||||
row = TopicTopic(
|
|
||||||
bot_id=bot_id,
|
bot_id=bot_id,
|
||||||
topic_key=topic_key,
|
topic_key=payload.topic_key,
|
||||||
name=str(payload.name or topic_key).strip() or topic_key,
|
name=payload.name,
|
||||||
description=str(payload.description or "").strip(),
|
description=payload.description,
|
||||||
is_active=bool(payload.is_active),
|
is_active=payload.is_active,
|
||||||
is_default_fallback=False,
|
routing=payload.routing,
|
||||||
routing_json=json.dumps(payload.routing or {}, ensure_ascii=False),
|
view_schema=payload.view_schema,
|
||||||
view_schema_json=json.dumps(payload.view_schema or {}, ensure_ascii=False),
|
|
||||||
created_at=now,
|
|
||||||
updated_at=now,
|
|
||||||
)
|
)
|
||||||
session.add(row)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(row)
|
|
||||||
return _topic_to_dict(row)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/api/bots/{bot_id}/topics/{topic_key}")
|
@router.put("/api/bots/{bot_id}/topics/{topic_key}")
|
||||||
def update_bot_topic(bot_id: str, topic_key: str, payload: TopicUpdateRequest, session: Session = Depends(get_session)):
|
def update_bot_topic(bot_id: str, topic_key: str, payload: TopicUpdateRequest, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return update_topic(session, bot_id=bot_id, topic_key=topic_key, updates=payload.model_dump(exclude_unset=True))
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
normalized_key = _normalize_topic_key(topic_key)
|
|
||||||
if not normalized_key:
|
|
||||||
raise HTTPException(status_code=400, detail="topic_key is required")
|
|
||||||
row = session.exec(
|
|
||||||
select(TopicTopic)
|
|
||||||
.where(TopicTopic.bot_id == bot_id)
|
|
||||||
.where(TopicTopic.topic_key == normalized_key)
|
|
||||||
.limit(1)
|
|
||||||
).first()
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Topic not found")
|
|
||||||
|
|
||||||
update_data = payload.model_dump(exclude_unset=True)
|
|
||||||
if "name" in update_data:
|
|
||||||
row.name = str(update_data.get("name") or "").strip() or row.topic_key
|
|
||||||
if "description" in update_data:
|
|
||||||
row.description = str(update_data.get("description") or "").strip()
|
|
||||||
if "is_active" in update_data:
|
|
||||||
row.is_active = bool(update_data.get("is_active"))
|
|
||||||
if "routing" in update_data:
|
|
||||||
row.routing_json = json.dumps(update_data.get("routing") or {}, ensure_ascii=False)
|
|
||||||
if "view_schema" in update_data:
|
|
||||||
row.view_schema_json = json.dumps(update_data.get("view_schema") or {}, ensure_ascii=False)
|
|
||||||
row.is_default_fallback = False
|
|
||||||
row.updated_at = datetime.utcnow()
|
|
||||||
session.add(row)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(row)
|
|
||||||
return _topic_to_dict(row)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/api/bots/{bot_id}/topics/{topic_key}")
|
@router.delete("/api/bots/{bot_id}/topics/{topic_key}")
|
||||||
def delete_bot_topic(bot_id: str, topic_key: str, session: Session = Depends(get_session)):
|
def delete_bot_topic(bot_id: str, topic_key: str, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return delete_topic(session, bot_id=bot_id, topic_key=topic_key)
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
normalized_key = _normalize_topic_key(topic_key)
|
|
||||||
if not normalized_key:
|
|
||||||
raise HTTPException(status_code=400, detail="topic_key is required")
|
|
||||||
row = session.exec(
|
|
||||||
select(TopicTopic)
|
|
||||||
.where(TopicTopic.bot_id == bot_id)
|
|
||||||
.where(TopicTopic.topic_key == normalized_key)
|
|
||||||
.limit(1)
|
|
||||||
).first()
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Topic not found")
|
|
||||||
items = session.exec(
|
|
||||||
select(TopicItem)
|
|
||||||
.where(TopicItem.bot_id == bot_id)
|
|
||||||
.where(TopicItem.topic_key == normalized_key)
|
|
||||||
).all()
|
|
||||||
for item in items:
|
|
||||||
session.delete(item)
|
|
||||||
session.delete(row)
|
|
||||||
session.commit()
|
|
||||||
return {"status": "deleted", "bot_id": bot_id, "topic_key": normalized_key}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/bots/{bot_id}/topic-items")
|
@router.get("/api/bots/{bot_id}/topic-items")
|
||||||
|
|
@ -172,97 +73,19 @@ def list_bot_topic_items(
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
):
|
):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return list_topic_items(session, bot_id=bot_id, topic_key=topic_key, cursor=cursor, limit=limit)
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
normalized_limit = max(1, min(int(limit or 50), 100))
|
|
||||||
stmt = select(TopicItem).where(TopicItem.bot_id == bot_id)
|
|
||||||
normalized_topic_key = _normalize_topic_key(topic_key or "")
|
|
||||||
if normalized_topic_key:
|
|
||||||
stmt = stmt.where(TopicItem.topic_key == normalized_topic_key)
|
|
||||||
if cursor is not None:
|
|
||||||
normalized_cursor = int(cursor)
|
|
||||||
if normalized_cursor > 0:
|
|
||||||
stmt = stmt.where(TopicItem.id < normalized_cursor)
|
|
||||||
rows = session.exec(
|
|
||||||
stmt.order_by(TopicItem.id.desc()).limit(normalized_limit + 1)
|
|
||||||
).all()
|
|
||||||
next_cursor: Optional[int] = None
|
|
||||||
if len(rows) > normalized_limit:
|
|
||||||
next_cursor = rows[-1].id
|
|
||||||
rows = rows[:normalized_limit]
|
|
||||||
return {
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"topic_key": normalized_topic_key or None,
|
|
||||||
"items": [_topic_item_to_dict(row) for row in rows],
|
|
||||||
"next_cursor": next_cursor,
|
|
||||||
"unread_count": _count_topic_items(session, bot_id, normalized_topic_key, unread_only=True),
|
|
||||||
"total_unread_count": _count_topic_items(session, bot_id, unread_only=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/bots/{bot_id}/topic-items/stats")
|
@router.get("/api/bots/{bot_id}/topic-items/stats")
|
||||||
def get_bot_topic_item_stats(bot_id: str, session: Session = Depends(get_session)):
|
def get_bot_topic_item_stats(bot_id: str, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return get_topic_item_stats(session, bot_id=bot_id)
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
latest_item = session.exec(
|
|
||||||
select(TopicItem)
|
|
||||||
.where(TopicItem.bot_id == bot_id)
|
|
||||||
.order_by(TopicItem.id.desc())
|
|
||||||
.limit(1)
|
|
||||||
).first()
|
|
||||||
return {
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"total_count": _count_topic_items(session, bot_id),
|
|
||||||
"unread_count": _count_topic_items(session, bot_id, unread_only=True),
|
|
||||||
"latest_item_id": int(latest_item.id or 0) if latest_item and latest_item.id else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/bots/{bot_id}/topic-items/{item_id}/read")
|
@router.post("/api/bots/{bot_id}/topic-items/{item_id}/read")
|
||||||
def mark_bot_topic_item_read(bot_id: str, item_id: int, session: Session = Depends(get_session)):
|
def mark_bot_topic_item_read(bot_id: str, item_id: int, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return mark_topic_item_read(session, bot_id=bot_id, item_id=item_id)
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
row = session.exec(
|
|
||||||
select(TopicItem)
|
|
||||||
.where(TopicItem.bot_id == bot_id)
|
|
||||||
.where(TopicItem.id == item_id)
|
|
||||||
.limit(1)
|
|
||||||
).first()
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Topic item not found")
|
|
||||||
if not bool(row.is_read):
|
|
||||||
row.is_read = True
|
|
||||||
session.add(row)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(row)
|
|
||||||
return {
|
|
||||||
"status": "updated",
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"item": _topic_item_to_dict(row),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/api/bots/{bot_id}/topic-items/{item_id}")
|
@router.delete("/api/bots/{bot_id}/topic-items/{item_id}")
|
||||||
def delete_bot_topic_item(bot_id: str, item_id: int, session: Session = Depends(get_session)):
|
def delete_bot_topic_item(bot_id: str, item_id: int, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
return delete_topic_item(session, bot_id=bot_id, item_id=item_id)
|
||||||
if not bot:
|
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
|
||||||
row = session.exec(
|
|
||||||
select(TopicItem)
|
|
||||||
.where(TopicItem.bot_id == bot_id)
|
|
||||||
.where(TopicItem.id == item_id)
|
|
||||||
.limit(1)
|
|
||||||
).first()
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Topic item not found")
|
|
||||||
payload = _topic_item_to_dict(row)
|
|
||||||
session.delete(row)
|
|
||||||
session.commit()
|
|
||||||
return {
|
|
||||||
"status": "deleted",
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"item": payload,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,16 @@ from api.bot_runtime_router import router as bot_runtime_router
|
||||||
from api.bot_speech_router import router as bot_speech_router
|
from api.bot_speech_router import router as bot_speech_router
|
||||||
from api.chat_history_router import router as chat_history_router
|
from api.chat_history_router import router as chat_history_router
|
||||||
from api.chat_router import router as chat_router
|
from api.chat_router import router as chat_router
|
||||||
|
from api.health_router import router as health_router
|
||||||
from api.image_router import router as image_router
|
from api.image_router import router as image_router
|
||||||
|
from api.panel_auth_router import router as panel_auth_router
|
||||||
from api.platform_router import router as platform_router
|
from api.platform_router import router as platform_router
|
||||||
from api.skill_router import router as skill_router
|
from api.skill_router import router as skill_router
|
||||||
from api.system_router import router as system_router
|
from api.system_router import router as system_router
|
||||||
from api.topic_router import router as topic_router
|
from api.topic_router import router as topic_router
|
||||||
from api.workspace_router import router as workspace_router
|
from api.workspace_router import router as workspace_router
|
||||||
from bootstrap.app_runtime import register_app_runtime
|
from bootstrap.app_runtime import register_app_runtime
|
||||||
from core.auth_middleware import PasswordProtectionMiddleware
|
from core.auth_middleware import AuthAccessMiddleware
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
from core.settings import BOTS_WORKSPACE_ROOT, CORS_ALLOWED_ORIGINS, DATA_ROOT
|
from core.settings import BOTS_WORKSPACE_ROOT, CORS_ALLOWED_ORIGINS, DATA_ROOT
|
||||||
from core.speech_service import WhisperSpeechService
|
from core.speech_service import WhisperSpeechService
|
||||||
|
|
@ -30,7 +32,7 @@ def create_app() -> FastAPI:
|
||||||
app.state.docker_manager = docker_manager
|
app.state.docker_manager = docker_manager
|
||||||
app.state.speech_service = speech_service
|
app.state.speech_service = speech_service
|
||||||
|
|
||||||
app.add_middleware(PasswordProtectionMiddleware)
|
app.add_middleware(AuthAccessMiddleware)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=list(CORS_ALLOWED_ORIGINS),
|
allow_origins=list(CORS_ALLOWED_ORIGINS),
|
||||||
|
|
@ -39,6 +41,8 @@ def create_app() -> FastAPI:
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.include_router(panel_auth_router)
|
||||||
|
app.include_router(health_router)
|
||||||
app.include_router(platform_router)
|
app.include_router(platform_router)
|
||||||
app.include_router(topic_router)
|
app.include_router(topic_router)
|
||||||
app.include_router(system_router)
|
app.include_router(system_router)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import asyncio
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from core.cache import cache
|
||||||
from core.database import engine, init_database
|
from core.database import engine, init_database
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
from core.settings import DATABASE_URL_DISPLAY, REDIS_ENABLED
|
from core.settings import DATABASE_URL_DISPLAY, REDIS_ENABLED
|
||||||
|
|
@ -12,6 +13,13 @@ from services.platform_service import prune_expired_activity_events
|
||||||
from services.runtime_service import docker_callback, set_main_loop
|
from services.runtime_service import docker_callback, set_main_loop
|
||||||
|
|
||||||
|
|
||||||
|
def reload_platform_runtime(app: FastAPI) -> None:
|
||||||
|
cache.delete_prefix("")
|
||||||
|
speech_service = getattr(app.state, "speech_service", None)
|
||||||
|
if speech_service is not None and hasattr(speech_service, "reset_runtime"):
|
||||||
|
speech_service.reset_runtime()
|
||||||
|
|
||||||
|
|
||||||
def register_app_runtime(app: FastAPI) -> None:
|
def register_app_runtime(app: FastAPI) -> None:
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def _on_startup() -> None:
|
async def _on_startup() -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -12,46 +11,84 @@ class RouteAccessMode(str, Enum):
|
||||||
PUBLIC_BOT_OR_PANEL = "public_bot_or_panel"
|
PUBLIC_BOT_OR_PANEL = "public_bot_or_panel"
|
||||||
|
|
||||||
|
|
||||||
_BOT_ID_API_RE = re.compile(r"^/api/bots/([^/]+)(?:/.*)?$")
|
_PUBLIC_EXACT_PATHS = {
|
||||||
_BOT_ID_PUBLIC_RE = re.compile(r"^/public/bots/([^/]+)(?:/.*)?$")
|
|
||||||
_BOT_PANEL_ONLY_ROUTE_METHODS = [
|
|
||||||
(re.compile(r"^/api/bots/[^/]+$"), {"DELETE"}),
|
|
||||||
(re.compile(r"^/api/bots/[^/]+/(?:enable|disable|deactivate)$"), {"POST"}),
|
|
||||||
]
|
|
||||||
|
|
||||||
_PUBLIC_PATHS = {
|
|
||||||
"/api/panel/auth/status",
|
|
||||||
"/api/panel/auth/login",
|
|
||||||
"/api/panel/auth/logout",
|
|
||||||
"/api/health",
|
"/api/health",
|
||||||
"/api/health/cache",
|
"/api/health/cache",
|
||||||
"/api/system/defaults",
|
"/api/system/defaults",
|
||||||
}
|
}
|
||||||
|
|
||||||
_BOT_PUBLIC_AUTH_RE = re.compile(r"^/api/bots/[^/]+/auth/(?:login|logout|status)$")
|
_PANEL_AUTH_SEGMENTS = ("api", "panel", "auth")
|
||||||
|
_BOT_PUBLIC_SEGMENTS = ("public", "bots")
|
||||||
|
_BOT_API_SEGMENTS = ("api", "bots")
|
||||||
|
_BOT_AUTH_SEGMENT = "auth"
|
||||||
|
_BOT_PANEL_ONLY_ACTIONS = {"enable", "disable", "deactivate"}
|
||||||
|
_BOT_PUBLIC_AUTH_ACTIONS = {"login", "logout", "status"}
|
||||||
|
|
||||||
|
|
||||||
|
def _path_segments(path: str) -> list[str]:
|
||||||
|
raw = str(path or "").strip().strip("/")
|
||||||
|
if not raw:
|
||||||
|
return []
|
||||||
|
return [segment for segment in raw.split("/") if segment]
|
||||||
|
|
||||||
|
|
||||||
def extract_bot_id(path: str) -> Optional[str]:
|
def extract_bot_id(path: str) -> Optional[str]:
|
||||||
raw = str(path or "").strip()
|
segments = _path_segments(path)
|
||||||
match = _BOT_ID_API_RE.match(raw) or _BOT_ID_PUBLIC_RE.match(raw)
|
if len(segments) < 3:
|
||||||
if not match or not match.group(1):
|
|
||||||
return None
|
return None
|
||||||
return match.group(1).strip() or None
|
if tuple(segments[:2]) not in {_BOT_API_SEGMENTS, _BOT_PUBLIC_SEGMENTS}:
|
||||||
|
return None
|
||||||
|
bot_id = str(segments[2] or "").strip()
|
||||||
|
return bot_id or None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_panel_auth_route(segments: list[str]) -> bool:
|
||||||
|
return tuple(segments[:3]) == _PANEL_AUTH_SEGMENTS
|
||||||
|
|
||||||
|
|
||||||
|
def _is_public_bot_route(segments: list[str]) -> bool:
|
||||||
|
return tuple(segments[:2]) == _BOT_PUBLIC_SEGMENTS and len(segments) >= 3
|
||||||
|
|
||||||
|
|
||||||
|
def _is_bot_auth_route(segments: list[str]) -> bool:
|
||||||
|
return (
|
||||||
|
tuple(segments[:2]) == _BOT_API_SEGMENTS
|
||||||
|
and len(segments) >= 5
|
||||||
|
and segments[3] == _BOT_AUTH_SEGMENT
|
||||||
|
and segments[4] in _BOT_PUBLIC_AUTH_ACTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_panel_only_bot_action(segments: list[str], method: str) -> bool:
|
||||||
|
if tuple(segments[:2]) != _BOT_API_SEGMENTS or len(segments) < 3:
|
||||||
|
return False
|
||||||
|
if len(segments) == 3 and method == "DELETE":
|
||||||
|
return True
|
||||||
|
return len(segments) >= 4 and method == "POST" and segments[3] in _BOT_PANEL_ONLY_ACTIONS
|
||||||
|
|
||||||
|
|
||||||
|
def _is_bot_scoped_api_route(segments: list[str]) -> bool:
|
||||||
|
return tuple(segments[:2]) == _BOT_API_SEGMENTS and len(segments) >= 3
|
||||||
|
|
||||||
|
|
||||||
def resolve_route_access_mode(path: str, method: str) -> RouteAccessMode:
|
def resolve_route_access_mode(path: str, method: str) -> RouteAccessMode:
|
||||||
raw_path = str(path or "").strip()
|
raw_path = str(path or "").strip()
|
||||||
verb = str(method or "GET").strip().upper()
|
verb = str(method or "GET").strip().upper()
|
||||||
|
segments = _path_segments(raw_path)
|
||||||
|
|
||||||
if raw_path in _PUBLIC_PATHS or _BOT_PUBLIC_AUTH_RE.fullmatch(raw_path):
|
if raw_path in _PUBLIC_EXACT_PATHS:
|
||||||
return RouteAccessMode.PUBLIC
|
return RouteAccessMode.PUBLIC
|
||||||
|
|
||||||
if raw_path.startswith("/public/bots/"):
|
if _is_panel_auth_route(segments) or _is_bot_auth_route(segments):
|
||||||
|
return RouteAccessMode.PUBLIC
|
||||||
|
|
||||||
|
if _is_public_bot_route(segments):
|
||||||
return RouteAccessMode.PUBLIC_BOT_OR_PANEL
|
return RouteAccessMode.PUBLIC_BOT_OR_PANEL
|
||||||
|
|
||||||
if _BOT_ID_API_RE.fullmatch(raw_path):
|
if _is_panel_only_bot_action(segments, verb):
|
||||||
if any(pattern.fullmatch(raw_path) and verb in methods for pattern, methods in _BOT_PANEL_ONLY_ROUTE_METHODS):
|
|
||||||
return RouteAccessMode.PANEL_ONLY
|
return RouteAccessMode.PANEL_ONLY
|
||||||
|
|
||||||
|
if _is_bot_scoped_api_route(segments):
|
||||||
return RouteAccessMode.BOT_OR_PANEL
|
return RouteAccessMode.BOT_OR_PANEL
|
||||||
|
|
||||||
if raw_path.startswith("/api/"):
|
if raw_path.startswith("/api/"):
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ def _unauthorized(detail: str) -> JSONResponse:
|
||||||
return JSONResponse(status_code=401, content={"detail": detail})
|
return JSONResponse(status_code=401, content={"detail": detail})
|
||||||
|
|
||||||
|
|
||||||
class PasswordProtectionMiddleware(BaseHTTPMiddleware):
|
class AuthAccessMiddleware(BaseHTTPMiddleware):
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
if request.method.upper() == "OPTIONS":
|
if request.method.upper() == "OPTIONS":
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
|
||||||
|
|
@ -1,366 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
from sqlmodel import Session
|
|
||||||
|
|
||||||
from core.config_manager import BotConfigManager
|
|
||||||
from core.settings import BOTS_WORKSPACE_ROOT
|
|
||||||
from models.bot import BotInstance
|
|
||||||
from schemas.bot import ChannelConfigRequest
|
|
||||||
from services.bot_storage_service import (
|
|
||||||
_normalize_resource_limits,
|
|
||||||
_read_bot_config,
|
|
||||||
_write_bot_resources,
|
|
||||||
)
|
|
||||||
from services.template_service import get_agent_md_templates
|
|
||||||
|
|
||||||
config_manager = BotConfigManager(host_data_root=BOTS_WORKSPACE_ROOT)
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_channel_extra(raw: Any) -> Dict[str, Any]:
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return {}
|
|
||||||
return raw
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_allow_from(raw: Any) -> List[str]:
|
|
||||||
rows: List[str] = []
|
|
||||||
if isinstance(raw, list):
|
|
||||||
for item in raw:
|
|
||||||
text = str(item or "").strip()
|
|
||||||
if text and text not in rows:
|
|
||||||
rows.append(text)
|
|
||||||
return rows or ["*"]
|
|
||||||
|
|
||||||
|
|
||||||
def _read_global_delivery_flags(channels_cfg: Any) -> tuple[bool, bool]:
|
|
||||||
if not isinstance(channels_cfg, dict):
|
|
||||||
return False, False
|
|
||||||
send_progress = channels_cfg.get("sendProgress")
|
|
||||||
send_tool_hints = channels_cfg.get("sendToolHints")
|
|
||||||
dashboard_cfg = channels_cfg.get("dashboard")
|
|
||||||
if isinstance(dashboard_cfg, dict):
|
|
||||||
if send_progress is None and "sendProgress" in dashboard_cfg:
|
|
||||||
send_progress = dashboard_cfg.get("sendProgress")
|
|
||||||
if send_tool_hints is None and "sendToolHints" in dashboard_cfg:
|
|
||||||
send_tool_hints = dashboard_cfg.get("sendToolHints")
|
|
||||||
return bool(send_progress), bool(send_tool_hints)
|
|
||||||
|
|
||||||
|
|
||||||
def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
ctype = str(ctype or "").strip().lower()
|
|
||||||
enabled = bool(cfg.get("enabled", True))
|
|
||||||
port = max(1, min(int(cfg.get("port", 8080) or 8080), 65535))
|
|
||||||
extra: Dict[str, Any] = {}
|
|
||||||
external_app_id = ""
|
|
||||||
app_secret = ""
|
|
||||||
|
|
||||||
if ctype == "feishu":
|
|
||||||
external_app_id = str(cfg.get("appId") or "")
|
|
||||||
app_secret = str(cfg.get("appSecret") or "")
|
|
||||||
extra = {
|
|
||||||
"encryptKey": cfg.get("encryptKey", ""),
|
|
||||||
"verificationToken": cfg.get("verificationToken", ""),
|
|
||||||
"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
elif ctype == "dingtalk":
|
|
||||||
external_app_id = str(cfg.get("clientId") or "")
|
|
||||||
app_secret = str(cfg.get("clientSecret") or "")
|
|
||||||
extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))}
|
|
||||||
elif ctype == "telegram":
|
|
||||||
app_secret = str(cfg.get("token") or "")
|
|
||||||
extra = {
|
|
||||||
"proxy": cfg.get("proxy", ""),
|
|
||||||
"replyToMessage": bool(cfg.get("replyToMessage", False)),
|
|
||||||
"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
elif ctype == "slack":
|
|
||||||
external_app_id = str(cfg.get("botToken") or "")
|
|
||||||
app_secret = str(cfg.get("appToken") or "")
|
|
||||||
extra = {
|
|
||||||
"mode": cfg.get("mode", "socket"),
|
|
||||||
"replyInThread": bool(cfg.get("replyInThread", True)),
|
|
||||||
"groupPolicy": cfg.get("groupPolicy", "mention"),
|
|
||||||
"groupAllowFrom": cfg.get("groupAllowFrom", []),
|
|
||||||
"reactEmoji": cfg.get("reactEmoji", "eyes"),
|
|
||||||
}
|
|
||||||
elif ctype == "qq":
|
|
||||||
external_app_id = str(cfg.get("appId") or "")
|
|
||||||
app_secret = str(cfg.get("secret") or "")
|
|
||||||
extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))}
|
|
||||||
elif ctype == "weixin":
|
|
||||||
app_secret = ""
|
|
||||||
extra = {
|
|
||||||
"hasSavedState": (Path(BOTS_WORKSPACE_ROOT) / bot_id / ".nanobot" / "weixin" / "account.json").is_file(),
|
|
||||||
}
|
|
||||||
elif ctype == "email":
|
|
||||||
extra = {
|
|
||||||
"consentGranted": bool(cfg.get("consentGranted", False)),
|
|
||||||
"imapHost": str(cfg.get("imapHost") or ""),
|
|
||||||
"imapPort": int(cfg.get("imapPort") or 993),
|
|
||||||
"imapUsername": str(cfg.get("imapUsername") or ""),
|
|
||||||
"imapPassword": str(cfg.get("imapPassword") or ""),
|
|
||||||
"imapMailbox": str(cfg.get("imapMailbox") or "INBOX"),
|
|
||||||
"imapUseSsl": bool(cfg.get("imapUseSsl", True)),
|
|
||||||
"smtpHost": str(cfg.get("smtpHost") or ""),
|
|
||||||
"smtpPort": int(cfg.get("smtpPort") or 587),
|
|
||||||
"smtpUsername": str(cfg.get("smtpUsername") or ""),
|
|
||||||
"smtpPassword": str(cfg.get("smtpPassword") or ""),
|
|
||||||
"smtpUseTls": bool(cfg.get("smtpUseTls", True)),
|
|
||||||
"smtpUseSsl": bool(cfg.get("smtpUseSsl", False)),
|
|
||||||
"fromAddress": str(cfg.get("fromAddress") or ""),
|
|
||||||
"autoReplyEnabled": bool(cfg.get("autoReplyEnabled", True)),
|
|
||||||
"pollIntervalSeconds": int(cfg.get("pollIntervalSeconds") or 30),
|
|
||||||
"markSeen": bool(cfg.get("markSeen", True)),
|
|
||||||
"maxBodyChars": int(cfg.get("maxBodyChars") or 12000),
|
|
||||||
"subjectPrefix": str(cfg.get("subjectPrefix") or "Re: "),
|
|
||||||
"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
external_app_id = str(cfg.get("appId") or cfg.get("clientId") or cfg.get("botToken") or cfg.get("externalAppId") or "")
|
|
||||||
app_secret = str(cfg.get("appSecret") or cfg.get("clientSecret") or cfg.get("secret") or cfg.get("token") or cfg.get("appToken") or "")
|
|
||||||
extra = {
|
|
||||||
key: value
|
|
||||||
for key, value in cfg.items()
|
|
||||||
if key not in {"enabled", "port", "appId", "clientId", "botToken", "externalAppId", "appSecret", "clientSecret", "secret", "token", "appToken"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"id": ctype,
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"channel_type": ctype,
|
|
||||||
"external_app_id": external_app_id,
|
|
||||||
"app_secret": app_secret,
|
|
||||||
"internal_port": port,
|
|
||||||
"is_active": enabled,
|
|
||||||
"extra_config": extra,
|
|
||||||
"locked": ctype == "dashboard",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _channel_api_to_cfg(row: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
ctype = str(row.get("channel_type") or "").strip().lower()
|
|
||||||
enabled = bool(row.get("is_active", True))
|
|
||||||
extra = _normalize_channel_extra(row.get("extra_config"))
|
|
||||||
external_app_id = str(row.get("external_app_id") or "")
|
|
||||||
app_secret = str(row.get("app_secret") or "")
|
|
||||||
port = max(1, min(int(row.get("internal_port") or 8080), 65535))
|
|
||||||
|
|
||||||
if ctype == "feishu":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"appId": external_app_id,
|
|
||||||
"appSecret": app_secret,
|
|
||||||
"encryptKey": extra.get("encryptKey", ""),
|
|
||||||
"verificationToken": extra.get("verificationToken", ""),
|
|
||||||
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
if ctype == "dingtalk":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"clientId": external_app_id,
|
|
||||||
"clientSecret": app_secret,
|
|
||||||
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
if ctype == "telegram":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"token": app_secret,
|
|
||||||
"proxy": extra.get("proxy", ""),
|
|
||||||
"replyToMessage": bool(extra.get("replyToMessage", False)),
|
|
||||||
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
if ctype == "slack":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"mode": extra.get("mode", "socket"),
|
|
||||||
"botToken": external_app_id,
|
|
||||||
"appToken": app_secret,
|
|
||||||
"replyInThread": bool(extra.get("replyInThread", True)),
|
|
||||||
"groupPolicy": extra.get("groupPolicy", "mention"),
|
|
||||||
"groupAllowFrom": extra.get("groupAllowFrom", []),
|
|
||||||
"reactEmoji": extra.get("reactEmoji", "eyes"),
|
|
||||||
}
|
|
||||||
if ctype == "qq":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"appId": external_app_id,
|
|
||||||
"secret": app_secret,
|
|
||||||
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
if ctype == "weixin":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"token": app_secret,
|
|
||||||
}
|
|
||||||
if ctype == "email":
|
|
||||||
return {
|
|
||||||
"enabled": enabled,
|
|
||||||
"consentGranted": bool(extra.get("consentGranted", False)),
|
|
||||||
"imapHost": str(extra.get("imapHost") or ""),
|
|
||||||
"imapPort": max(1, min(int(extra.get("imapPort") or 993), 65535)),
|
|
||||||
"imapUsername": str(extra.get("imapUsername") or ""),
|
|
||||||
"imapPassword": str(extra.get("imapPassword") or ""),
|
|
||||||
"imapMailbox": str(extra.get("imapMailbox") or "INBOX"),
|
|
||||||
"imapUseSsl": bool(extra.get("imapUseSsl", True)),
|
|
||||||
"smtpHost": str(extra.get("smtpHost") or ""),
|
|
||||||
"smtpPort": max(1, min(int(extra.get("smtpPort") or 587), 65535)),
|
|
||||||
"smtpUsername": str(extra.get("smtpUsername") or ""),
|
|
||||||
"smtpPassword": str(extra.get("smtpPassword") or ""),
|
|
||||||
"smtpUseTls": bool(extra.get("smtpUseTls", True)),
|
|
||||||
"smtpUseSsl": bool(extra.get("smtpUseSsl", False)),
|
|
||||||
"fromAddress": str(extra.get("fromAddress") or ""),
|
|
||||||
"autoReplyEnabled": bool(extra.get("autoReplyEnabled", True)),
|
|
||||||
"pollIntervalSeconds": max(5, int(extra.get("pollIntervalSeconds") or 30)),
|
|
||||||
"markSeen": bool(extra.get("markSeen", True)),
|
|
||||||
"maxBodyChars": max(1, int(extra.get("maxBodyChars") or 12000)),
|
|
||||||
"subjectPrefix": str(extra.get("subjectPrefix") or "Re: "),
|
|
||||||
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
|
||||||
}
|
|
||||||
merged = dict(extra)
|
|
||||||
merged.update(
|
|
||||||
{
|
|
||||||
"enabled": enabled,
|
|
||||||
"appId": external_app_id,
|
|
||||||
"appSecret": app_secret,
|
|
||||||
"port": port,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return merged
|
|
||||||
|
|
||||||
|
|
||||||
def _get_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]:
|
|
||||||
config_data = _read_bot_config(bot.id)
|
|
||||||
channels_cfg = config_data.get("channels")
|
|
||||||
if not isinstance(channels_cfg, dict):
|
|
||||||
channels_cfg = {}
|
|
||||||
send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
|
||||||
rows: List[Dict[str, Any]] = [
|
|
||||||
{
|
|
||||||
"id": "dashboard",
|
|
||||||
"bot_id": bot.id,
|
|
||||||
"channel_type": "dashboard",
|
|
||||||
"external_app_id": f"dashboard-{bot.id}",
|
|
||||||
"app_secret": "",
|
|
||||||
"internal_port": 9000,
|
|
||||||
"is_active": True,
|
|
||||||
"extra_config": {
|
|
||||||
"sendProgress": send_progress,
|
|
||||||
"sendToolHints": send_tool_hints,
|
|
||||||
},
|
|
||||||
"locked": True,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
for ctype, cfg in channels_cfg.items():
|
|
||||||
if ctype in {"sendProgress", "sendToolHints", "dashboard"} or not isinstance(cfg, dict):
|
|
||||||
continue
|
|
||||||
rows.append(_channel_cfg_to_api_dict(bot.id, ctype, cfg))
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_initial_channels(bot_id: str, channels: Optional[List[ChannelConfigRequest]]) -> List[Dict[str, Any]]:
|
|
||||||
rows: List[Dict[str, Any]] = []
|
|
||||||
seen_types: set[str] = set()
|
|
||||||
for channel in channels or []:
|
|
||||||
ctype = (channel.channel_type or "").strip().lower()
|
|
||||||
if not ctype or ctype == "dashboard" or ctype in seen_types:
|
|
||||||
continue
|
|
||||||
seen_types.add(ctype)
|
|
||||||
rows.append(
|
|
||||||
{
|
|
||||||
"id": ctype,
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"channel_type": ctype,
|
|
||||||
"external_app_id": (channel.external_app_id or "").strip() or f"{ctype}-{bot_id}",
|
|
||||||
"app_secret": (channel.app_secret or "").strip(),
|
|
||||||
"internal_port": max(1, min(int(channel.internal_port or 8080), 65535)),
|
|
||||||
"is_active": bool(channel.is_active),
|
|
||||||
"extra_config": _normalize_channel_extra(channel.extra_config),
|
|
||||||
"locked": False,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def _sync_workspace_channels_impl(
|
|
||||||
session: Session,
|
|
||||||
bot_id: str,
|
|
||||||
snapshot: Dict[str, Any],
|
|
||||||
*,
|
|
||||||
channels_override: Optional[List[Dict[str, Any]]] = None,
|
|
||||||
global_delivery_override: Optional[Dict[str, Any]] = None,
|
|
||||||
runtime_overrides: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> None:
|
|
||||||
bot = session.get(BotInstance, bot_id)
|
|
||||||
if not bot:
|
|
||||||
return
|
|
||||||
template_defaults = get_agent_md_templates()
|
|
||||||
bot_data: Dict[str, Any] = {
|
|
||||||
"name": bot.name,
|
|
||||||
"system_prompt": snapshot.get("system_prompt") or template_defaults.get("soul_md", ""),
|
|
||||||
"soul_md": snapshot.get("soul_md") or template_defaults.get("soul_md", ""),
|
|
||||||
"agents_md": snapshot.get("agents_md") or template_defaults.get("agents_md", ""),
|
|
||||||
"user_md": snapshot.get("user_md") or template_defaults.get("user_md", ""),
|
|
||||||
"tools_md": snapshot.get("tools_md") or template_defaults.get("tools_md", ""),
|
|
||||||
"identity_md": snapshot.get("identity_md") or template_defaults.get("identity_md", ""),
|
|
||||||
"llm_provider": snapshot.get("llm_provider") or "",
|
|
||||||
"llm_model": snapshot.get("llm_model") or "",
|
|
||||||
"api_key": snapshot.get("api_key") or "",
|
|
||||||
"api_base": snapshot.get("api_base") or "",
|
|
||||||
"temperature": snapshot.get("temperature"),
|
|
||||||
"top_p": snapshot.get("top_p"),
|
|
||||||
"max_tokens": snapshot.get("max_tokens"),
|
|
||||||
"cpu_cores": snapshot.get("cpu_cores"),
|
|
||||||
"memory_mb": snapshot.get("memory_mb"),
|
|
||||||
"storage_gb": snapshot.get("storage_gb"),
|
|
||||||
"send_progress": bool(snapshot.get("send_progress")),
|
|
||||||
"send_tool_hints": bool(snapshot.get("send_tool_hints")),
|
|
||||||
}
|
|
||||||
if isinstance(runtime_overrides, dict):
|
|
||||||
for key, value in runtime_overrides.items():
|
|
||||||
if key in {"api_key", "llm_provider", "llm_model"}:
|
|
||||||
text = str(value or "").strip()
|
|
||||||
if not text:
|
|
||||||
continue
|
|
||||||
bot_data[key] = text
|
|
||||||
continue
|
|
||||||
if key == "api_base":
|
|
||||||
bot_data[key] = str(value or "").strip()
|
|
||||||
continue
|
|
||||||
bot_data[key] = value
|
|
||||||
|
|
||||||
resources = _normalize_resource_limits(
|
|
||||||
bot_data.get("cpu_cores"),
|
|
||||||
bot_data.get("memory_mb"),
|
|
||||||
bot_data.get("storage_gb"),
|
|
||||||
)
|
|
||||||
bot_data.update(resources)
|
|
||||||
send_progress = bool(bot_data.get("send_progress", False))
|
|
||||||
send_tool_hints = bool(bot_data.get("send_tool_hints", False))
|
|
||||||
if isinstance(global_delivery_override, dict):
|
|
||||||
if "sendProgress" in global_delivery_override:
|
|
||||||
send_progress = bool(global_delivery_override.get("sendProgress"))
|
|
||||||
if "sendToolHints" in global_delivery_override:
|
|
||||||
send_tool_hints = bool(global_delivery_override.get("sendToolHints"))
|
|
||||||
|
|
||||||
channels_data = channels_override if channels_override is not None else _get_bot_channels_from_config(bot)
|
|
||||||
bot_data["send_progress"] = send_progress
|
|
||||||
bot_data["send_tool_hints"] = send_tool_hints
|
|
||||||
normalized_channels: List[Dict[str, Any]] = []
|
|
||||||
for row in channels_data:
|
|
||||||
ctype = str(row.get("channel_type") or "").strip().lower()
|
|
||||||
if not ctype or ctype == "dashboard":
|
|
||||||
continue
|
|
||||||
normalized_channels.append(
|
|
||||||
{
|
|
||||||
"channel_type": ctype,
|
|
||||||
"external_app_id": str(row.get("external_app_id") or ""),
|
|
||||||
"app_secret": str(row.get("app_secret") or ""),
|
|
||||||
"internal_port": max(1, min(int(row.get("internal_port") or 8080), 65535)),
|
|
||||||
"is_active": bool(row.get("is_active", True)),
|
|
||||||
"extra_config": _normalize_channel_extra(row.get("extra_config")),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
config_manager.update_workspace(bot_id=bot_id, bot_data=bot_data, channels=normalized_channels)
|
|
||||||
_write_bot_resources(bot_id, bot_data.get("cpu_cores"), bot_data.get("memory_mb"), bot_data.get("storage_gb"))
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
from core.utils import _calc_dir_size_bytes
|
from core.settings import BOTS_WORKSPACE_ROOT
|
||||||
from models.bot import BotInstance
|
from models.bot import BotInstance
|
||||||
from schemas.bot import (
|
from schemas.bot import (
|
||||||
BotEnvParamsUpdateRequest,
|
BotEnvParamsUpdateRequest,
|
||||||
|
|
@ -13,28 +14,30 @@ from schemas.bot import (
|
||||||
ChannelConfigRequest,
|
ChannelConfigRequest,
|
||||||
ChannelConfigUpdateRequest,
|
ChannelConfigUpdateRequest,
|
||||||
)
|
)
|
||||||
from services.bot_channel_service import (
|
from services.bot_service import (
|
||||||
_channel_api_to_cfg,
|
channel_api_to_config,
|
||||||
_get_bot_channels_from_config,
|
list_bot_channels_from_config,
|
||||||
_normalize_channel_extra,
|
normalize_channel_extra,
|
||||||
_read_global_delivery_flags,
|
read_global_delivery_flags,
|
||||||
|
sync_bot_workspace_channels,
|
||||||
)
|
)
|
||||||
from services.bot_service import _sync_workspace_channels
|
|
||||||
from services.bot_mcp_service import (
|
from services.bot_mcp_service import (
|
||||||
_merge_mcp_servers_preserving_extras,
|
_merge_mcp_servers_preserving_extras,
|
||||||
_normalize_mcp_servers,
|
_normalize_mcp_servers,
|
||||||
)
|
)
|
||||||
from services.bot_storage_service import (
|
from services.bot_storage_service import (
|
||||||
_normalize_env_params,
|
get_bot_resource_limits,
|
||||||
_read_bot_config,
|
get_bot_workspace_snapshot,
|
||||||
_read_bot_resources,
|
normalize_bot_env_params,
|
||||||
_read_env_store,
|
read_bot_config_data,
|
||||||
_workspace_root,
|
read_bot_env_params,
|
||||||
_write_bot_config,
|
write_bot_config_data,
|
||||||
_write_env_store,
|
write_bot_env_params,
|
||||||
)
|
)
|
||||||
from services.cache_service import _invalidate_bot_detail_cache
|
from services.cache_service import _invalidate_bot_detail_cache
|
||||||
|
|
||||||
|
MANAGED_WORKSPACE_FILENAMES = ("AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md")
|
||||||
|
|
||||||
|
|
||||||
def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
|
def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
|
@ -43,14 +46,103 @@ def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
|
|
||||||
|
def _read_bot_config_object(bot_id: str) -> Dict[str, Any]:
|
||||||
|
config_data = read_bot_config_data(bot_id)
|
||||||
|
return config_data if isinstance(config_data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_bot_tools_cfg(bot_id: str) -> tuple[Dict[str, Any], Dict[str, Any]]:
|
||||||
|
config_data = _read_bot_config_object(bot_id)
|
||||||
|
tools_cfg = config_data.get("tools")
|
||||||
|
if not isinstance(tools_cfg, dict):
|
||||||
|
tools_cfg = {}
|
||||||
|
config_data["tools"] = tools_cfg
|
||||||
|
return config_data, tools_cfg
|
||||||
|
|
||||||
|
|
||||||
|
def _read_bot_channels_cfg(bot_id: str) -> tuple[Dict[str, Any], Dict[str, Any]]:
|
||||||
|
config_data = _read_bot_config_object(bot_id)
|
||||||
|
channels_cfg = config_data.get("channels")
|
||||||
|
if not isinstance(channels_cfg, dict):
|
||||||
|
channels_cfg = {}
|
||||||
|
config_data["channels"] = channels_cfg
|
||||||
|
return config_data, channels_cfg
|
||||||
|
|
||||||
|
|
||||||
|
def _managed_bot_file_paths(bot_id: str) -> Dict[str, str]:
|
||||||
|
bot_root = os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot")
|
||||||
|
workspace_root = os.path.join(bot_root, "workspace")
|
||||||
|
paths = {
|
||||||
|
"config": os.path.join(bot_root, "config.json"),
|
||||||
|
"resources": os.path.join(bot_root, "resources.json"),
|
||||||
|
}
|
||||||
|
for filename in MANAGED_WORKSPACE_FILENAMES:
|
||||||
|
paths[f"workspace:{filename}"] = os.path.join(workspace_root, filename)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_managed_bot_files(bot_id: str) -> Dict[str, Optional[bytes]]:
|
||||||
|
snapshot: Dict[str, Optional[bytes]] = {}
|
||||||
|
for key, path in _managed_bot_file_paths(bot_id).items():
|
||||||
|
if os.path.isfile(path):
|
||||||
|
with open(path, "rb") as file:
|
||||||
|
snapshot[key] = file.read()
|
||||||
|
else:
|
||||||
|
snapshot[key] = None
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def _restore_managed_bot_files(bot_id: str, snapshot: Dict[str, Optional[bytes]]) -> None:
|
||||||
|
for key, path in _managed_bot_file_paths(bot_id).items():
|
||||||
|
payload = snapshot.get(key)
|
||||||
|
if payload is None:
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.remove(path)
|
||||||
|
continue
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
tmp_path = f"{path}.tmp"
|
||||||
|
with open(tmp_path, "wb") as file:
|
||||||
|
file.write(payload)
|
||||||
|
os.replace(tmp_path, path)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_bot_config_state(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
bot_id: str,
|
||||||
|
config_data: Dict[str, Any],
|
||||||
|
sync_workspace: bool = False,
|
||||||
|
) -> None:
|
||||||
|
managed_file_snapshot = _snapshot_managed_bot_files(bot_id) if sync_workspace else None
|
||||||
|
try:
|
||||||
|
write_bot_config_data(bot_id, config_data)
|
||||||
|
if sync_workspace:
|
||||||
|
sync_bot_workspace_channels(session, bot_id)
|
||||||
|
except Exception:
|
||||||
|
if managed_file_snapshot is not None:
|
||||||
|
_restore_managed_bot_files(bot_id, managed_file_snapshot)
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_channel_row(rows: list[Dict[str, Any]], channel_id: str) -> Dict[str, Any]:
|
||||||
|
channel_key = str(channel_id or "").strip().lower()
|
||||||
|
row = next((item for item in rows if str(item.get("id") or "").lower() == channel_key), None)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Channel not found")
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
def get_bot_resources_snapshot(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
def get_bot_resources_snapshot(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
bot = _get_bot_or_404(session, bot_id)
|
bot = _get_bot_or_404(session, bot_id)
|
||||||
|
|
||||||
configured = _read_bot_resources(bot_id)
|
configured = get_bot_resource_limits(bot_id)
|
||||||
runtime = docker_manager.get_bot_resource_snapshot(bot_id)
|
runtime = docker_manager.get_bot_resource_snapshot(bot_id)
|
||||||
workspace_root = _workspace_root(bot_id)
|
workspace = get_bot_workspace_snapshot(bot_id)
|
||||||
workspace_bytes = _calc_dir_size_bytes(workspace_root)
|
workspace_root = str(workspace.get("path") or "")
|
||||||
configured_storage_bytes = int(configured.get("storage_gb", 0) or 0) * 1024 * 1024 * 1024
|
workspace_bytes = int(workspace.get("usage_bytes") or 0)
|
||||||
|
configured_storage_bytes = int(workspace.get("configured_limit_bytes") or 0)
|
||||||
workspace_percent = 0.0
|
workspace_percent = 0.0
|
||||||
if configured_storage_bytes > 0:
|
if configured_storage_bytes > 0:
|
||||||
workspace_percent = (workspace_bytes / configured_storage_bytes) * 100.0
|
workspace_percent = (workspace_bytes / configured_storage_bytes) * 100.0
|
||||||
|
|
@ -86,7 +178,7 @@ def get_bot_resources_snapshot(session: Session, *, bot_id: str) -> Dict[str, An
|
||||||
|
|
||||||
def list_bot_channels_config(session: Session, *, bot_id: str):
|
def list_bot_channels_config(session: Session, *, bot_id: str):
|
||||||
bot = _get_bot_or_404(session, bot_id)
|
bot = _get_bot_or_404(session, bot_id)
|
||||||
return _get_bot_channels_from_config(bot)
|
return list_bot_channels_from_config(bot)
|
||||||
|
|
||||||
|
|
||||||
def get_bot_tools_config_state(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
def get_bot_tools_config_state(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
|
|
@ -114,10 +206,7 @@ def reject_bot_tools_config_update(
|
||||||
|
|
||||||
def get_bot_mcp_config_state(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
def get_bot_mcp_config_state(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
_get_bot_or_404(session, bot_id)
|
_get_bot_or_404(session, bot_id)
|
||||||
config_data = _read_bot_config(bot_id)
|
_config_data, tools_cfg = _read_bot_tools_cfg(bot_id)
|
||||||
tools_cfg = config_data.get("tools") if isinstance(config_data, dict) else {}
|
|
||||||
if not isinstance(tools_cfg, dict):
|
|
||||||
tools_cfg = {}
|
|
||||||
mcp_servers = _normalize_mcp_servers(tools_cfg.get("mcpServers"))
|
mcp_servers = _normalize_mcp_servers(tools_cfg.get("mcpServers"))
|
||||||
return {
|
return {
|
||||||
"bot_id": bot_id,
|
"bot_id": bot_id,
|
||||||
|
|
@ -134,20 +223,13 @@ def update_bot_mcp_config_state(
|
||||||
payload: BotMcpConfigUpdateRequest,
|
payload: BotMcpConfigUpdateRequest,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
_get_bot_or_404(session, bot_id)
|
_get_bot_or_404(session, bot_id)
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data, tools_cfg = _read_bot_tools_cfg(bot_id)
|
||||||
if not isinstance(config_data, dict):
|
|
||||||
config_data = {}
|
|
||||||
tools_cfg = config_data.get("tools")
|
|
||||||
if not isinstance(tools_cfg, dict):
|
|
||||||
tools_cfg = {}
|
|
||||||
normalized_mcp_servers = _normalize_mcp_servers(payload.mcp_servers or {})
|
normalized_mcp_servers = _normalize_mcp_servers(payload.mcp_servers or {})
|
||||||
current_mcp_servers = tools_cfg.get("mcpServers")
|
current_mcp_servers = tools_cfg.get("mcpServers")
|
||||||
merged_mcp_servers = _merge_mcp_servers_preserving_extras(current_mcp_servers, normalized_mcp_servers)
|
merged_mcp_servers = _merge_mcp_servers_preserving_extras(current_mcp_servers, normalized_mcp_servers)
|
||||||
tools_cfg["mcpServers"] = merged_mcp_servers
|
tools_cfg["mcpServers"] = merged_mcp_servers
|
||||||
config_data["tools"] = tools_cfg
|
|
||||||
sanitized_after_save = _normalize_mcp_servers(tools_cfg.get("mcpServers"))
|
sanitized_after_save = _normalize_mcp_servers(tools_cfg.get("mcpServers"))
|
||||||
_write_bot_config(bot_id, config_data)
|
_write_bot_config_state(session, bot_id=bot_id, config_data=config_data)
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
|
||||||
return {
|
return {
|
||||||
"status": "updated",
|
"status": "updated",
|
||||||
"bot_id": bot_id,
|
"bot_id": bot_id,
|
||||||
|
|
@ -161,7 +243,7 @@ def get_bot_env_params_state(session: Session, *, bot_id: str) -> Dict[str, Any]
|
||||||
_get_bot_or_404(session, bot_id)
|
_get_bot_or_404(session, bot_id)
|
||||||
return {
|
return {
|
||||||
"bot_id": bot_id,
|
"bot_id": bot_id,
|
||||||
"env_params": _read_env_store(bot_id),
|
"env_params": read_bot_env_params(bot_id),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -172,8 +254,8 @@ def update_bot_env_params_state(
|
||||||
payload: BotEnvParamsUpdateRequest,
|
payload: BotEnvParamsUpdateRequest,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
_get_bot_or_404(session, bot_id)
|
_get_bot_or_404(session, bot_id)
|
||||||
normalized = _normalize_env_params(payload.env_params)
|
normalized = normalize_bot_env_params(payload.env_params)
|
||||||
_write_env_store(bot_id, normalized)
|
write_bot_env_params(bot_id, normalized)
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
return {
|
return {
|
||||||
"status": "updated",
|
"status": "updated",
|
||||||
|
|
@ -196,7 +278,7 @@ def create_bot_channel_config(
|
||||||
raise HTTPException(status_code=400, detail="channel_type is required")
|
raise HTTPException(status_code=400, detail="channel_type is required")
|
||||||
if ctype == "dashboard":
|
if ctype == "dashboard":
|
||||||
raise HTTPException(status_code=400, detail="dashboard channel is built-in and cannot be created manually")
|
raise HTTPException(status_code=400, detail="dashboard channel is built-in and cannot be created manually")
|
||||||
current_rows = _get_bot_channels_from_config(bot)
|
current_rows = list_bot_channels_from_config(bot)
|
||||||
if any(str(row.get("channel_type") or "").lower() == ctype for row in current_rows):
|
if any(str(row.get("channel_type") or "").lower() == ctype for row in current_rows):
|
||||||
raise HTTPException(status_code=400, detail=f"Channel already exists: {ctype}")
|
raise HTTPException(status_code=400, detail=f"Channel already exists: {ctype}")
|
||||||
|
|
||||||
|
|
@ -208,19 +290,13 @@ def create_bot_channel_config(
|
||||||
"app_secret": (payload.app_secret or "").strip(),
|
"app_secret": (payload.app_secret or "").strip(),
|
||||||
"internal_port": max(1, min(int(payload.internal_port or 8080), 65535)),
|
"internal_port": max(1, min(int(payload.internal_port or 8080), 65535)),
|
||||||
"is_active": bool(payload.is_active),
|
"is_active": bool(payload.is_active),
|
||||||
"extra_config": _normalize_channel_extra(payload.extra_config),
|
"extra_config": normalize_channel_extra(payload.extra_config),
|
||||||
"locked": False,
|
"locked": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data, channels_cfg = _read_bot_channels_cfg(bot_id)
|
||||||
channels_cfg = config_data.get("channels")
|
channels_cfg[ctype] = channel_api_to_config(new_row)
|
||||||
if not isinstance(channels_cfg, dict):
|
_write_bot_config_state(session, bot_id=bot_id, config_data=config_data, sync_workspace=True)
|
||||||
channels_cfg = {}
|
|
||||||
config_data["channels"] = channels_cfg
|
|
||||||
channels_cfg[ctype] = _channel_api_to_cfg(new_row)
|
|
||||||
_write_bot_config(bot_id, config_data)
|
|
||||||
_sync_workspace_channels(session, bot_id)
|
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
|
||||||
return new_row
|
return new_row
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -233,11 +309,8 @@ def update_bot_channel_config(
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
bot = _get_bot_or_404(session, bot_id)
|
bot = _get_bot_or_404(session, bot_id)
|
||||||
|
|
||||||
channel_key = str(channel_id or "").strip().lower()
|
rows = list_bot_channels_from_config(bot)
|
||||||
rows = _get_bot_channels_from_config(bot)
|
row = _find_channel_row(rows, channel_id)
|
||||||
row = next((r for r in rows if str(r.get("id") or "").lower() == channel_key), None)
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Channel not found")
|
|
||||||
if str(row.get("channel_type") or "").strip().lower() == "dashboard" or bool(row.get("locked")):
|
if str(row.get("channel_type") or "").strip().lower() == "dashboard" or bool(row.get("locked")):
|
||||||
raise HTTPException(status_code=400, detail="dashboard channel is built-in and cannot be modified")
|
raise HTTPException(status_code=400, detail="dashboard channel is built-in and cannot be modified")
|
||||||
|
|
||||||
|
|
@ -265,19 +338,15 @@ def update_bot_channel_config(
|
||||||
raise HTTPException(status_code=400, detail="dashboard channel must remain enabled")
|
raise HTTPException(status_code=400, detail="dashboard channel must remain enabled")
|
||||||
row["is_active"] = next_active
|
row["is_active"] = next_active
|
||||||
if "extra_config" in update_data:
|
if "extra_config" in update_data:
|
||||||
row["extra_config"] = _normalize_channel_extra(update_data.get("extra_config"))
|
row["extra_config"] = normalize_channel_extra(update_data.get("extra_config"))
|
||||||
row["channel_type"] = new_type
|
row["channel_type"] = new_type
|
||||||
row["id"] = new_type
|
row["id"] = new_type
|
||||||
row["locked"] = new_type == "dashboard"
|
row["locked"] = new_type == "dashboard"
|
||||||
|
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data, channels_cfg = _read_bot_channels_cfg(bot_id)
|
||||||
channels_cfg = config_data.get("channels")
|
current_send_progress, current_send_tool_hints = read_global_delivery_flags(channels_cfg)
|
||||||
if not isinstance(channels_cfg, dict):
|
|
||||||
channels_cfg = {}
|
|
||||||
config_data["channels"] = channels_cfg
|
|
||||||
current_send_progress, current_send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
|
||||||
if new_type == "dashboard":
|
if new_type == "dashboard":
|
||||||
extra = _normalize_channel_extra(row.get("extra_config"))
|
extra = normalize_channel_extra(row.get("extra_config"))
|
||||||
channels_cfg["sendProgress"] = bool(extra.get("sendProgress", current_send_progress))
|
channels_cfg["sendProgress"] = bool(extra.get("sendProgress", current_send_progress))
|
||||||
channels_cfg["sendToolHints"] = bool(extra.get("sendToolHints", current_send_tool_hints))
|
channels_cfg["sendToolHints"] = bool(extra.get("sendToolHints", current_send_tool_hints))
|
||||||
else:
|
else:
|
||||||
|
|
@ -287,11 +356,8 @@ def update_bot_channel_config(
|
||||||
if existing_type != "dashboard" and existing_type in channels_cfg and existing_type != new_type:
|
if existing_type != "dashboard" and existing_type in channels_cfg and existing_type != new_type:
|
||||||
channels_cfg.pop(existing_type, None)
|
channels_cfg.pop(existing_type, None)
|
||||||
if new_type != "dashboard":
|
if new_type != "dashboard":
|
||||||
channels_cfg[new_type] = _channel_api_to_cfg(row)
|
channels_cfg[new_type] = channel_api_to_config(row)
|
||||||
_write_bot_config(bot_id, config_data)
|
_write_bot_config_state(session, bot_id=bot_id, config_data=config_data, sync_workspace=True)
|
||||||
session.commit()
|
|
||||||
_sync_workspace_channels(session, bot_id)
|
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -303,22 +369,12 @@ def delete_bot_channel_config(
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
bot = _get_bot_or_404(session, bot_id)
|
bot = _get_bot_or_404(session, bot_id)
|
||||||
|
|
||||||
channel_key = str(channel_id or "").strip().lower()
|
rows = list_bot_channels_from_config(bot)
|
||||||
rows = _get_bot_channels_from_config(bot)
|
row = _find_channel_row(rows, channel_id)
|
||||||
row = next((r for r in rows if str(r.get("id") or "").lower() == channel_key), None)
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(status_code=404, detail="Channel not found")
|
|
||||||
if str(row.get("channel_type") or "").lower() == "dashboard":
|
if str(row.get("channel_type") or "").lower() == "dashboard":
|
||||||
raise HTTPException(status_code=400, detail="dashboard channel cannot be deleted")
|
raise HTTPException(status_code=400, detail="dashboard channel cannot be deleted")
|
||||||
|
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data, channels_cfg = _read_bot_channels_cfg(bot_id)
|
||||||
channels_cfg = config_data.get("channels")
|
|
||||||
if not isinstance(channels_cfg, dict):
|
|
||||||
channels_cfg = {}
|
|
||||||
config_data["channels"] = channels_cfg
|
|
||||||
channels_cfg.pop(str(row.get("channel_type") or "").lower(), None)
|
channels_cfg.pop(str(row.get("channel_type") or "").lower(), None)
|
||||||
_write_bot_config(bot_id, config_data)
|
_write_bot_config_state(session, bot_id=bot_id, config_data=config_data, sync_workspace=True)
|
||||||
session.commit()
|
|
||||||
_sync_workspace_channels(session, bot_id)
|
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
|
||||||
return {"status": "deleted"}
|
return {"status": "deleted"}
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ from models.platform import BotActivityEvent, BotRequestUsage
|
||||||
from models.skill import BotSkillInstall
|
from models.skill import BotSkillInstall
|
||||||
from models.topic import TopicItem, TopicTopic
|
from models.topic import TopicItem, TopicTopic
|
||||||
from services.bot_service import (
|
from services.bot_service import (
|
||||||
_read_bot_runtime_snapshot,
|
|
||||||
_resolve_bot_env_params,
|
|
||||||
_safe_float,
|
_safe_float,
|
||||||
_safe_int,
|
_safe_int,
|
||||||
_sync_workspace_channels,
|
read_bot_runtime_snapshot,
|
||||||
|
resolve_bot_runtime_env_params,
|
||||||
|
sync_bot_workspace_channels,
|
||||||
)
|
)
|
||||||
from services.bot_storage_service import _write_env_store
|
from services.bot_storage_service import write_bot_env_params
|
||||||
from services.cache_service import _invalidate_bot_detail_cache, _invalidate_bot_messages_cache
|
from services.cache_service import _invalidate_bot_detail_cache, _invalidate_bot_messages_cache
|
||||||
from services.platform_service import record_activity_event
|
from services.platform_service import record_activity_event
|
||||||
from services.runtime_service import _record_agent_loop_ready_warning, docker_callback
|
from services.runtime_service import docker_callback, record_agent_loop_ready_warning
|
||||||
|
|
||||||
|
|
||||||
def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
|
def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
|
||||||
|
|
@ -36,10 +36,10 @@ async def start_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]:
|
||||||
if not bool(getattr(bot, "enabled", True)):
|
if not bool(getattr(bot, "enabled", True)):
|
||||||
raise PermissionError("Bot is disabled. Enable it first.")
|
raise PermissionError("Bot is disabled. Enable it first.")
|
||||||
|
|
||||||
_sync_workspace_channels(session, bot_id)
|
sync_bot_workspace_channels(session, bot_id)
|
||||||
runtime_snapshot = _read_bot_runtime_snapshot(bot)
|
runtime_snapshot = read_bot_runtime_snapshot(bot)
|
||||||
env_params = _resolve_bot_env_params(bot_id)
|
env_params = resolve_bot_runtime_env_params(bot_id)
|
||||||
_write_env_store(bot_id, env_params)
|
write_bot_env_params(bot_id, env_params)
|
||||||
success = docker_manager.start_bot(
|
success = docker_manager.start_bot(
|
||||||
bot_id,
|
bot_id,
|
||||||
image_tag=bot.image_tag,
|
image_tag=bot.image_tag,
|
||||||
|
|
@ -63,7 +63,7 @@ async def start_bot_instance(session: Session, bot_id: str) -> Dict[str, Any]:
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
raise RuntimeError("Bot container failed shortly after startup. Check bot logs/config.")
|
raise RuntimeError("Bot container failed shortly after startup. Check bot logs/config.")
|
||||||
|
|
||||||
asyncio.create_task(_record_agent_loop_ready_warning(bot_id))
|
asyncio.create_task(record_agent_loop_ready_warning(bot_id))
|
||||||
session.add(bot)
|
session.add(bot)
|
||||||
record_activity_event(session, bot_id, "bot_started", channel="system", detail=f"Container started for {bot_id}")
|
record_activity_event(session, bot_id, "bot_started", channel="system", detail=f"Container started for {bot_id}")
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import re
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import httpx
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
|
@ -13,19 +12,21 @@ from core.settings import BOTS_WORKSPACE_ROOT
|
||||||
from models.bot import BotInstance, NanobotImage
|
from models.bot import BotInstance, NanobotImage
|
||||||
from schemas.bot import BotCreateRequest, BotUpdateRequest
|
from schemas.bot import BotCreateRequest, BotUpdateRequest
|
||||||
from services.bot_service import (
|
from services.bot_service import (
|
||||||
_normalize_env_params,
|
normalize_initial_bot_channels,
|
||||||
_normalize_initial_channels,
|
normalize_bot_system_timezone,
|
||||||
_normalize_resource_limits,
|
resolve_bot_runtime_env_params,
|
||||||
_normalize_system_timezone,
|
serialize_bot_detail,
|
||||||
_provider_defaults,
|
serialize_bot_list_entry,
|
||||||
_resolve_bot_env_params,
|
sync_bot_workspace_channels,
|
||||||
_serialize_bot,
|
)
|
||||||
_serialize_bot_list_item,
|
from services.bot_storage_service import (
|
||||||
_sync_workspace_channels,
|
normalize_bot_env_params,
|
||||||
|
normalize_bot_resource_limits,
|
||||||
|
write_bot_env_params,
|
||||||
)
|
)
|
||||||
from services.bot_storage_service import _write_env_store
|
|
||||||
from services.cache_service import _cache_key_bot_detail, _cache_key_bots_list, _invalidate_bot_detail_cache
|
from services.cache_service import _cache_key_bot_detail, _cache_key_bots_list, _invalidate_bot_detail_cache
|
||||||
from services.platform_service import record_activity_event
|
from services.platform_service import record_activity_event
|
||||||
|
from services.provider_service import get_provider_defaults
|
||||||
from services.template_service import get_agent_md_templates
|
from services.template_service import get_agent_md_templates
|
||||||
|
|
||||||
BOT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_]+$")
|
BOT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_]+$")
|
||||||
|
|
@ -76,60 +77,6 @@ def _cleanup_bot_workspace_root(bot_id: str) -> None:
|
||||||
shutil.rmtree(bot_root, ignore_errors=True)
|
shutil.rmtree(bot_root, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
async def test_provider_connection(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
provider = (payload.get("provider") or "").strip()
|
|
||||||
api_key = (payload.get("api_key") or "").strip()
|
|
||||||
model = (payload.get("model") or "").strip()
|
|
||||||
api_base = (payload.get("api_base") or "").strip()
|
|
||||||
|
|
||||||
if not provider or not api_key:
|
|
||||||
raise HTTPException(status_code=400, detail="provider and api_key are required")
|
|
||||||
|
|
||||||
normalized_provider, default_base = _provider_defaults(provider)
|
|
||||||
base = (api_base or default_base).rstrip("/")
|
|
||||||
if normalized_provider not in {"openrouter", "dashscope", "kimi", "minimax", "openai", "deepseek"}:
|
|
||||||
raise HTTPException(status_code=400, detail=f"provider not supported for test: {provider}")
|
|
||||||
if not base:
|
|
||||||
raise HTTPException(status_code=400, detail=f"api_base is required for provider: {provider}")
|
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {api_key}"}
|
|
||||||
timeout = httpx.Timeout(20.0, connect=10.0)
|
|
||||||
url = f"{base}/models"
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
||||||
response = await client.get(url, headers=headers)
|
|
||||||
if response.status_code >= 400:
|
|
||||||
return {
|
|
||||||
"ok": False,
|
|
||||||
"provider": normalized_provider,
|
|
||||||
"status_code": response.status_code,
|
|
||||||
"detail": response.text[:500],
|
|
||||||
}
|
|
||||||
data = response.json()
|
|
||||||
models_raw = data.get("data", []) if isinstance(data, dict) else []
|
|
||||||
model_ids: List[str] = [
|
|
||||||
str(item["id"]) for item in models_raw[:20] if isinstance(item, dict) and item.get("id")
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"provider": normalized_provider,
|
|
||||||
"endpoint": url,
|
|
||||||
"models_preview": model_ids[:8],
|
|
||||||
"model_hint": (
|
|
||||||
"model_found"
|
|
||||||
if model and any(model in item for item in model_ids)
|
|
||||||
else ("model_not_listed" if model else "")
|
|
||||||
),
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {
|
|
||||||
"ok": False,
|
|
||||||
"provider": normalized_provider,
|
|
||||||
"endpoint": url,
|
|
||||||
"detail": str(exc),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[str, Any]:
|
def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[str, Any]:
|
||||||
normalized_bot_id = str(payload.id or "").strip()
|
normalized_bot_id = str(payload.id or "").strip()
|
||||||
if not normalized_bot_id:
|
if not normalized_bot_id:
|
||||||
|
|
@ -147,9 +94,9 @@ def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[st
|
||||||
if not docker_manager.has_image(payload.image_tag):
|
if not docker_manager.has_image(payload.image_tag):
|
||||||
raise HTTPException(status_code=400, detail=f"Docker image not found locally: {payload.image_tag}")
|
raise HTTPException(status_code=400, detail=f"Docker image not found locally: {payload.image_tag}")
|
||||||
|
|
||||||
normalized_env_params = _normalize_env_params(payload.env_params)
|
normalized_env_params = normalize_bot_env_params(payload.env_params)
|
||||||
try:
|
try:
|
||||||
normalized_env_params["TZ"] = _normalize_system_timezone(payload.system_timezone)
|
normalized_env_params["TZ"] = normalize_bot_system_timezone(payload.system_timezone)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
@ -162,15 +109,15 @@ def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[st
|
||||||
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, normalized_bot_id),
|
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, normalized_bot_id),
|
||||||
)
|
)
|
||||||
template_defaults = get_agent_md_templates()
|
template_defaults = get_agent_md_templates()
|
||||||
resource_limits = _normalize_resource_limits(payload.cpu_cores, payload.memory_mb, payload.storage_gb)
|
resource_limits = normalize_bot_resource_limits(payload.cpu_cores, payload.memory_mb, payload.storage_gb)
|
||||||
try:
|
try:
|
||||||
session.add(bot)
|
session.add(bot)
|
||||||
session.flush()
|
session.flush()
|
||||||
_write_env_store(normalized_bot_id, normalized_env_params)
|
write_bot_env_params(normalized_bot_id, normalized_env_params)
|
||||||
_sync_workspace_channels(
|
sync_bot_workspace_channels(
|
||||||
session,
|
session,
|
||||||
normalized_bot_id,
|
normalized_bot_id,
|
||||||
channels_override=_normalize_initial_channels(normalized_bot_id, payload.channels),
|
channels_override=normalize_initial_bot_channels(normalized_bot_id, payload.channels),
|
||||||
global_delivery_override={
|
global_delivery_override={
|
||||||
"sendProgress": bool(payload.send_progress) if payload.send_progress is not None else False,
|
"sendProgress": bool(payload.send_progress) if payload.send_progress is not None else False,
|
||||||
"sendToolHints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False,
|
"sendToolHints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False,
|
||||||
|
|
@ -211,7 +158,7 @@ def create_bot_record(session: Session, *, payload: BotCreateRequest) -> Dict[st
|
||||||
_cleanup_bot_workspace_root(normalized_bot_id)
|
_cleanup_bot_workspace_root(normalized_bot_id)
|
||||||
raise
|
raise
|
||||||
_invalidate_bot_detail_cache(normalized_bot_id)
|
_invalidate_bot_detail_cache(normalized_bot_id)
|
||||||
return _serialize_bot(bot)
|
return serialize_bot_detail(bot)
|
||||||
|
|
||||||
|
|
||||||
def list_bots_with_cache(session: Session) -> List[Dict[str, Any]]:
|
def list_bots_with_cache(session: Session) -> List[Dict[str, Any]]:
|
||||||
|
|
@ -234,7 +181,7 @@ def list_bots_with_cache(session: Session) -> List[Dict[str, Any]]:
|
||||||
session.commit()
|
session.commit()
|
||||||
for bot in bots:
|
for bot in bots:
|
||||||
session.refresh(bot)
|
session.refresh(bot)
|
||||||
rows = [_serialize_bot_list_item(bot) for bot in bots]
|
rows = [serialize_bot_list_entry(bot) for bot in bots]
|
||||||
cache.set_json(_cache_key_bots_list(), rows, ttl=30)
|
cache.set_json(_cache_key_bots_list(), rows, ttl=30)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
@ -246,7 +193,7 @@ def get_bot_detail_cached(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
row = _serialize_bot(bot)
|
row = serialize_bot_detail(bot)
|
||||||
cache.set_json(_cache_key_bot_detail(bot_id), row, ttl=30)
|
cache.set_json(_cache_key_bot_detail(bot_id), row, ttl=30)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
@ -290,7 +237,7 @@ def update_bot_record(session: Session, *, bot_id: str, payload: BotUpdateReques
|
||||||
normalized_system_timezone: Optional[str] = None
|
normalized_system_timezone: Optional[str] = None
|
||||||
if system_timezone is not None:
|
if system_timezone is not None:
|
||||||
try:
|
try:
|
||||||
normalized_system_timezone = _normalize_system_timezone(system_timezone)
|
normalized_system_timezone = normalize_bot_system_timezone(system_timezone)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
@ -335,7 +282,7 @@ def update_bot_record(session: Session, *, bot_id: str, payload: BotUpdateReques
|
||||||
runtime_overrides["system_prompt"] = runtime_overrides["soul_md"]
|
runtime_overrides["system_prompt"] = runtime_overrides["soul_md"]
|
||||||
if {"cpu_cores", "memory_mb", "storage_gb"} & set(runtime_overrides.keys()):
|
if {"cpu_cores", "memory_mb", "storage_gb"} & set(runtime_overrides.keys()):
|
||||||
runtime_overrides.update(
|
runtime_overrides.update(
|
||||||
_normalize_resource_limits(
|
normalize_bot_resource_limits(
|
||||||
runtime_overrides.get("cpu_cores"),
|
runtime_overrides.get("cpu_cores"),
|
||||||
runtime_overrides.get("memory_mb"),
|
runtime_overrides.get("memory_mb"),
|
||||||
runtime_overrides.get("storage_gb"),
|
runtime_overrides.get("storage_gb"),
|
||||||
|
|
@ -350,12 +297,12 @@ def update_bot_record(session: Session, *, bot_id: str, payload: BotUpdateReques
|
||||||
session.flush()
|
session.flush()
|
||||||
|
|
||||||
if env_params is not None or normalized_system_timezone is not None:
|
if env_params is not None or normalized_system_timezone is not None:
|
||||||
next_env_params = _resolve_bot_env_params(bot_id)
|
next_env_params = resolve_bot_runtime_env_params(bot_id)
|
||||||
if env_params is not None:
|
if env_params is not None:
|
||||||
next_env_params = _normalize_env_params(env_params)
|
next_env_params = normalize_bot_env_params(env_params)
|
||||||
if normalized_system_timezone is not None:
|
if normalized_system_timezone is not None:
|
||||||
next_env_params["TZ"] = normalized_system_timezone
|
next_env_params["TZ"] = normalized_system_timezone
|
||||||
_write_env_store(bot_id, next_env_params)
|
write_bot_env_params(bot_id, next_env_params)
|
||||||
|
|
||||||
global_delivery_override: Optional[Dict[str, Any]] = None
|
global_delivery_override: Optional[Dict[str, Any]] = None
|
||||||
if "send_progress" in runtime_overrides or "send_tool_hints" in runtime_overrides:
|
if "send_progress" in runtime_overrides or "send_tool_hints" in runtime_overrides:
|
||||||
|
|
@ -365,7 +312,7 @@ def update_bot_record(session: Session, *, bot_id: str, payload: BotUpdateReques
|
||||||
if "send_tool_hints" in runtime_overrides:
|
if "send_tool_hints" in runtime_overrides:
|
||||||
global_delivery_override["sendToolHints"] = bool(runtime_overrides.get("send_tool_hints"))
|
global_delivery_override["sendToolHints"] = bool(runtime_overrides.get("send_tool_hints"))
|
||||||
|
|
||||||
_sync_workspace_channels(
|
sync_bot_workspace_channels(
|
||||||
session,
|
session,
|
||||||
bot_id,
|
bot_id,
|
||||||
runtime_overrides=runtime_overrides if runtime_overrides else None,
|
runtime_overrides=runtime_overrides if runtime_overrides else None,
|
||||||
|
|
@ -382,4 +329,4 @@ def update_bot_record(session: Session, *, bot_id: str, payload: BotUpdateReques
|
||||||
bot = refreshed_bot
|
bot = refreshed_bot
|
||||||
raise
|
raise
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
return _serialize_bot(bot)
|
return serialize_bot_detail(bot)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,14 @@ from sqlmodel import Session
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
from core.settings import BOTS_WORKSPACE_ROOT
|
from core.settings import BOTS_WORKSPACE_ROOT
|
||||||
from models.bot import BotInstance
|
from models.bot import BotInstance
|
||||||
from services.bot_channel_service import _get_bot_channels_from_config
|
|
||||||
from services.bot_lifecycle_service import start_bot_instance, stop_bot_instance
|
from services.bot_lifecycle_service import start_bot_instance, stop_bot_instance
|
||||||
from services.bot_storage_service import _read_bot_config, _read_cron_store, _write_bot_config, _write_cron_store
|
from services.bot_service import list_bot_channels_from_config
|
||||||
|
from services.bot_storage_service import (
|
||||||
|
read_bot_config_data,
|
||||||
|
read_bot_cron_jobs_store,
|
||||||
|
write_bot_config_data,
|
||||||
|
write_bot_cron_jobs_store,
|
||||||
|
)
|
||||||
from services.platform_auth_service import resolve_bot_websocket_auth, resolve_panel_websocket_auth
|
from services.platform_auth_service import resolve_bot_websocket_auth, resolve_panel_websocket_auth
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +95,7 @@ async def relogin_weixin(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
weixin_channel = next(
|
weixin_channel = next(
|
||||||
(
|
(
|
||||||
row
|
row
|
||||||
for row in _get_bot_channels_from_config(bot)
|
for row in list_bot_channels_from_config(bot)
|
||||||
if str(row.get("channel_type") or "").strip().lower() == "weixin"
|
if str(row.get("channel_type") or "").strip().lower() == "weixin"
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
|
|
@ -107,12 +112,12 @@ async def relogin_weixin(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise RuntimeError(f"Failed to remove weixin state: {exc}") from exc
|
raise RuntimeError(f"Failed to remove weixin state: {exc}") from exc
|
||||||
|
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data = read_bot_config_data(bot_id)
|
||||||
channels_cfg = config_data.get("channels") if isinstance(config_data, dict) else {}
|
channels_cfg = config_data.get("channels") if isinstance(config_data, dict) else {}
|
||||||
weixin_cfg = channels_cfg.get("weixin") if isinstance(channels_cfg, dict) else None
|
weixin_cfg = channels_cfg.get("weixin") if isinstance(channels_cfg, dict) else None
|
||||||
if isinstance(weixin_cfg, dict) and "token" in weixin_cfg:
|
if isinstance(weixin_cfg, dict) and "token" in weixin_cfg:
|
||||||
weixin_cfg.pop("token", None)
|
weixin_cfg.pop("token", None)
|
||||||
_write_bot_config(bot_id, config_data)
|
write_bot_config_data(bot_id, config_data)
|
||||||
|
|
||||||
restarted = False
|
restarted = False
|
||||||
if str(bot.docker_status or "").upper() == "RUNNING":
|
if str(bot.docker_status or "").upper() == "RUNNING":
|
||||||
|
|
@ -130,7 +135,7 @@ async def relogin_weixin(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
|
|
||||||
def list_cron_jobs(session: Session, *, bot_id: str, include_disabled: bool = True) -> Dict[str, Any]:
|
def list_cron_jobs(session: Session, *, bot_id: str, include_disabled: bool = True) -> Dict[str, Any]:
|
||||||
_get_bot_or_raise(session, bot_id)
|
_get_bot_or_raise(session, bot_id)
|
||||||
store = _read_cron_store(bot_id)
|
store = read_bot_cron_jobs_store(bot_id)
|
||||||
rows = []
|
rows = []
|
||||||
for row in store.get("jobs", []):
|
for row in store.get("jobs", []):
|
||||||
if not isinstance(row, dict):
|
if not isinstance(row, dict):
|
||||||
|
|
@ -145,7 +150,7 @@ def list_cron_jobs(session: Session, *, bot_id: str, include_disabled: bool = Tr
|
||||||
|
|
||||||
def stop_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, Any]:
|
def stop_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, Any]:
|
||||||
_get_bot_or_raise(session, bot_id)
|
_get_bot_or_raise(session, bot_id)
|
||||||
store = _read_cron_store(bot_id)
|
store = read_bot_cron_jobs_store(bot_id)
|
||||||
jobs = store.get("jobs", [])
|
jobs = store.get("jobs", [])
|
||||||
if not isinstance(jobs, list):
|
if not isinstance(jobs, list):
|
||||||
jobs = []
|
jobs = []
|
||||||
|
|
@ -159,13 +164,13 @@ def stop_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, An
|
||||||
state = {}
|
state = {}
|
||||||
found["state"] = state
|
found["state"] = state
|
||||||
state["nextRunAtMs"] = None
|
state["nextRunAtMs"] = None
|
||||||
_write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs})
|
write_bot_cron_jobs_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs})
|
||||||
return {"status": "stopped", "job_id": job_id}
|
return {"status": "stopped", "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
def start_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, Any]:
|
def start_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, Any]:
|
||||||
_get_bot_or_raise(session, bot_id)
|
_get_bot_or_raise(session, bot_id)
|
||||||
store = _read_cron_store(bot_id)
|
store = read_bot_cron_jobs_store(bot_id)
|
||||||
jobs = store.get("jobs", [])
|
jobs = store.get("jobs", [])
|
||||||
if not isinstance(jobs, list):
|
if not isinstance(jobs, list):
|
||||||
jobs = []
|
jobs = []
|
||||||
|
|
@ -180,20 +185,20 @@ def start_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, A
|
||||||
found["state"] = state
|
found["state"] = state
|
||||||
schedule = found.get("schedule")
|
schedule = found.get("schedule")
|
||||||
state["nextRunAtMs"] = _compute_cron_next_run(schedule if isinstance(schedule, dict) else {})
|
state["nextRunAtMs"] = _compute_cron_next_run(schedule if isinstance(schedule, dict) else {})
|
||||||
_write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs})
|
write_bot_cron_jobs_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": jobs})
|
||||||
return {"status": "started", "job_id": job_id}
|
return {"status": "started", "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
def delete_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, Any]:
|
def delete_cron_job(session: Session, *, bot_id: str, job_id: str) -> Dict[str, Any]:
|
||||||
_get_bot_or_raise(session, bot_id)
|
_get_bot_or_raise(session, bot_id)
|
||||||
store = _read_cron_store(bot_id)
|
store = read_bot_cron_jobs_store(bot_id)
|
||||||
jobs = store.get("jobs", [])
|
jobs = store.get("jobs", [])
|
||||||
if not isinstance(jobs, list):
|
if not isinstance(jobs, list):
|
||||||
jobs = []
|
jobs = []
|
||||||
kept = [row for row in jobs if not (isinstance(row, dict) and str(row.get("id")) == job_id)]
|
kept = [row for row in jobs if not (isinstance(row, dict) and str(row.get("id")) == job_id)]
|
||||||
if len(kept) == len(jobs):
|
if len(kept) == len(jobs):
|
||||||
raise LookupError("Cron job not found")
|
raise LookupError("Cron job not found")
|
||||||
_write_cron_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": kept})
|
write_bot_cron_jobs_store(bot_id, {"version": int(store.get("version", 1) or 1), "jobs": kept})
|
||||||
return {"status": "deleted", "job_id": job_id}
|
return {"status": "deleted", "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,32 @@
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from datetime import datetime, timezone
|
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from core.settings import DEFAULT_BOT_SYSTEM_TIMEZONE
|
from core.config_manager import BotConfigManager
|
||||||
|
from core.settings import BOTS_WORKSPACE_ROOT, DEFAULT_BOT_SYSTEM_TIMEZONE
|
||||||
from models.bot import BotInstance
|
from models.bot import BotInstance
|
||||||
|
from schemas.bot import ChannelConfigRequest
|
||||||
from services.bot_storage_service import (
|
from services.bot_storage_service import (
|
||||||
_bot_data_root,
|
|
||||||
_clear_bot_dashboard_direct_session,
|
|
||||||
_clear_bot_sessions,
|
|
||||||
_migrate_bot_resources_store,
|
|
||||||
_normalize_env_params,
|
_normalize_env_params,
|
||||||
_normalize_resource_limits,
|
|
||||||
_read_bot_config,
|
_read_bot_config,
|
||||||
_read_bot_resources,
|
_read_bot_resources,
|
||||||
_read_cron_store,
|
|
||||||
_read_env_store,
|
_read_env_store,
|
||||||
_safe_float,
|
_safe_float,
|
||||||
_safe_int,
|
_safe_int,
|
||||||
_workspace_root,
|
_workspace_root,
|
||||||
_write_bot_config,
|
normalize_bot_resource_limits,
|
||||||
_write_bot_resources,
|
write_bot_resource_limits,
|
||||||
_write_cron_store,
|
|
||||||
_write_env_store,
|
|
||||||
)
|
|
||||||
from services.bot_channel_service import (
|
|
||||||
_channel_api_to_cfg,
|
|
||||||
_get_bot_channels_from_config,
|
|
||||||
_normalize_channel_extra,
|
|
||||||
_normalize_initial_channels,
|
|
||||||
_read_global_delivery_flags,
|
|
||||||
_sync_workspace_channels_impl,
|
|
||||||
)
|
|
||||||
from services.bot_mcp_service import (
|
|
||||||
_merge_mcp_servers_preserving_extras,
|
|
||||||
_normalize_mcp_servers,
|
|
||||||
_sanitize_mcp_servers_in_config_data,
|
|
||||||
)
|
)
|
||||||
from services.template_service import get_agent_md_templates
|
from services.template_service import get_agent_md_templates
|
||||||
|
|
||||||
__all__ = [
|
config_manager = BotConfigManager(host_data_root=BOTS_WORKSPACE_ROOT)
|
||||||
"_bot_data_root",
|
|
||||||
"_channel_api_to_cfg",
|
|
||||||
"_clear_bot_dashboard_direct_session",
|
def get_default_bot_system_timezone() -> str:
|
||||||
"_clear_bot_sessions",
|
|
||||||
"_get_bot_channels_from_config",
|
|
||||||
"_migrate_bot_resources_store",
|
|
||||||
"_normalize_channel_extra",
|
|
||||||
"_normalize_env_params",
|
|
||||||
"_normalize_initial_channels",
|
|
||||||
"_normalize_mcp_servers",
|
|
||||||
"_normalize_resource_limits",
|
|
||||||
"_normalize_system_timezone",
|
|
||||||
"_provider_defaults",
|
|
||||||
"_read_bot_config",
|
|
||||||
"_read_bot_resources",
|
|
||||||
"_read_bot_runtime_snapshot",
|
|
||||||
"_read_cron_store",
|
|
||||||
"_read_env_store",
|
|
||||||
"_read_global_delivery_flags",
|
|
||||||
"_resolve_bot_env_params",
|
|
||||||
"_safe_float",
|
|
||||||
"_safe_int",
|
|
||||||
"_sanitize_mcp_servers_in_config_data",
|
|
||||||
"_serialize_bot",
|
|
||||||
"_serialize_bot_list_item",
|
|
||||||
"_sync_workspace_channels",
|
|
||||||
"_workspace_root",
|
|
||||||
"_write_bot_config",
|
|
||||||
"_write_bot_resources",
|
|
||||||
"_write_cron_store",
|
|
||||||
"_write_env_store",
|
|
||||||
"_merge_mcp_servers_preserving_extras",
|
|
||||||
]
|
|
||||||
def _get_default_system_timezone() -> str:
|
|
||||||
value = str(DEFAULT_BOT_SYSTEM_TIMEZONE or "").strip() or "Asia/Shanghai"
|
value = str(DEFAULT_BOT_SYSTEM_TIMEZONE or "").strip() or "Asia/Shanghai"
|
||||||
try:
|
try:
|
||||||
ZoneInfo(value)
|
ZoneInfo(value)
|
||||||
|
|
@ -84,10 +35,10 @@ def _get_default_system_timezone() -> str:
|
||||||
return "Asia/Shanghai"
|
return "Asia/Shanghai"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_system_timezone(raw: Any) -> str:
|
def normalize_bot_system_timezone(raw: Any) -> str:
|
||||||
value = str(raw or "").strip()
|
value = str(raw or "").strip()
|
||||||
if not value:
|
if not value:
|
||||||
return _get_default_system_timezone()
|
return get_default_bot_system_timezone()
|
||||||
try:
|
try:
|
||||||
ZoneInfo(value)
|
ZoneInfo(value)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -95,47 +46,316 @@ def _normalize_system_timezone(raw: Any) -> str:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _resolve_bot_env_params(bot_id: str, raw: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
def resolve_bot_runtime_env_params(bot_id: str, raw: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
||||||
env_params = _normalize_env_params(raw if isinstance(raw, dict) else _read_env_store(bot_id))
|
env_params = _normalize_env_params(raw if isinstance(raw, dict) else _read_env_store(bot_id))
|
||||||
try:
|
try:
|
||||||
env_params["TZ"] = _normalize_system_timezone(env_params.get("TZ"))
|
env_params["TZ"] = normalize_bot_system_timezone(env_params.get("TZ"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
env_params["TZ"] = _get_default_system_timezone()
|
env_params["TZ"] = get_default_bot_system_timezone()
|
||||||
return env_params
|
return env_params
|
||||||
|
|
||||||
|
|
||||||
def _provider_defaults(provider: str) -> tuple[str, str]:
|
def normalize_channel_extra(raw: Any) -> Dict[str, Any]:
|
||||||
normalized = provider.lower().strip()
|
if not isinstance(raw, dict):
|
||||||
if normalized in {"openai"}:
|
return {}
|
||||||
return "openai", "https://api.openai.com/v1"
|
return raw
|
||||||
if normalized in {"openrouter"}:
|
|
||||||
return "openrouter", "https://openrouter.ai/api/v1"
|
|
||||||
if normalized in {"dashscope", "aliyun", "qwen", "aliyun-qwen"}:
|
def _normalize_allow_from(raw: Any) -> List[str]:
|
||||||
return "dashscope", "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
rows: List[str] = []
|
||||||
if normalized in {"deepseek"}:
|
if isinstance(raw, list):
|
||||||
return "deepseek", "https://api.deepseek.com/v1"
|
for item in raw:
|
||||||
if normalized in {"xunfei", "iflytek", "xfyun"}:
|
text = str(item or "").strip()
|
||||||
return "openai", "https://spark-api-open.xf-yun.com/v1"
|
if text and text not in rows:
|
||||||
if normalized in {"vllm"}:
|
rows.append(text)
|
||||||
return "openai", ""
|
return rows or ["*"]
|
||||||
if normalized in {"kimi", "moonshot"}:
|
|
||||||
return "kimi", "https://api.moonshot.cn/v1"
|
|
||||||
if normalized in {"minimax"}:
|
def read_global_delivery_flags(channels_cfg: Any) -> tuple[bool, bool]:
|
||||||
return "minimax", "https://api.minimax.chat/v1"
|
if not isinstance(channels_cfg, dict):
|
||||||
return normalized, ""
|
return False, False
|
||||||
|
send_progress = channels_cfg.get("sendProgress")
|
||||||
|
send_tool_hints = channels_cfg.get("sendToolHints")
|
||||||
|
dashboard_cfg = channels_cfg.get("dashboard")
|
||||||
|
if isinstance(dashboard_cfg, dict):
|
||||||
|
if send_progress is None and "sendProgress" in dashboard_cfg:
|
||||||
|
send_progress = dashboard_cfg.get("sendProgress")
|
||||||
|
if send_tool_hints is None and "sendToolHints" in dashboard_cfg:
|
||||||
|
send_tool_hints = dashboard_cfg.get("sendToolHints")
|
||||||
|
return bool(send_progress), bool(send_tool_hints)
|
||||||
|
|
||||||
|
|
||||||
|
def channel_config_to_api(bot_id: str, channel_type: str, cfg: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
ctype = str(channel_type or "").strip().lower()
|
||||||
|
enabled = bool(cfg.get("enabled", True))
|
||||||
|
port = max(1, min(int(cfg.get("port", 8080) or 8080), 65535))
|
||||||
|
extra: Dict[str, Any] = {}
|
||||||
|
external_app_id = ""
|
||||||
|
app_secret = ""
|
||||||
|
|
||||||
|
if ctype == "feishu":
|
||||||
|
external_app_id = str(cfg.get("appId") or "")
|
||||||
|
app_secret = str(cfg.get("appSecret") or "")
|
||||||
|
extra = {
|
||||||
|
"encryptKey": cfg.get("encryptKey", ""),
|
||||||
|
"verificationToken": cfg.get("verificationToken", ""),
|
||||||
|
"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
elif ctype == "dingtalk":
|
||||||
|
external_app_id = str(cfg.get("clientId") or "")
|
||||||
|
app_secret = str(cfg.get("clientSecret") or "")
|
||||||
|
extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))}
|
||||||
|
elif ctype == "telegram":
|
||||||
|
app_secret = str(cfg.get("token") or "")
|
||||||
|
extra = {
|
||||||
|
"proxy": cfg.get("proxy", ""),
|
||||||
|
"replyToMessage": bool(cfg.get("replyToMessage", False)),
|
||||||
|
"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
elif ctype == "slack":
|
||||||
|
external_app_id = str(cfg.get("botToken") or "")
|
||||||
|
app_secret = str(cfg.get("appToken") or "")
|
||||||
|
extra = {
|
||||||
|
"mode": cfg.get("mode", "socket"),
|
||||||
|
"replyInThread": bool(cfg.get("replyInThread", True)),
|
||||||
|
"groupPolicy": cfg.get("groupPolicy", "mention"),
|
||||||
|
"groupAllowFrom": cfg.get("groupAllowFrom", []),
|
||||||
|
"reactEmoji": cfg.get("reactEmoji", "eyes"),
|
||||||
|
}
|
||||||
|
elif ctype == "qq":
|
||||||
|
external_app_id = str(cfg.get("appId") or "")
|
||||||
|
app_secret = str(cfg.get("secret") or "")
|
||||||
|
extra = {"allowFrom": _normalize_allow_from(cfg.get("allowFrom", []))}
|
||||||
|
elif ctype == "weixin":
|
||||||
|
app_secret = ""
|
||||||
|
extra = {
|
||||||
|
"hasSavedState": (Path(BOTS_WORKSPACE_ROOT) / bot_id / ".nanobot" / "weixin" / "account.json").is_file(),
|
||||||
|
}
|
||||||
|
elif ctype == "email":
|
||||||
|
extra = {
|
||||||
|
"consentGranted": bool(cfg.get("consentGranted", False)),
|
||||||
|
"imapHost": str(cfg.get("imapHost") or ""),
|
||||||
|
"imapPort": int(cfg.get("imapPort") or 993),
|
||||||
|
"imapUsername": str(cfg.get("imapUsername") or ""),
|
||||||
|
"imapPassword": str(cfg.get("imapPassword") or ""),
|
||||||
|
"imapMailbox": str(cfg.get("imapMailbox") or "INBOX"),
|
||||||
|
"imapUseSsl": bool(cfg.get("imapUseSsl", True)),
|
||||||
|
"smtpHost": str(cfg.get("smtpHost") or ""),
|
||||||
|
"smtpPort": int(cfg.get("smtpPort") or 587),
|
||||||
|
"smtpUsername": str(cfg.get("smtpUsername") or ""),
|
||||||
|
"smtpPassword": str(cfg.get("smtpPassword") or ""),
|
||||||
|
"smtpUseTls": bool(cfg.get("smtpUseTls", True)),
|
||||||
|
"smtpUseSsl": bool(cfg.get("smtpUseSsl", False)),
|
||||||
|
"fromAddress": str(cfg.get("fromAddress") or ""),
|
||||||
|
"autoReplyEnabled": bool(cfg.get("autoReplyEnabled", True)),
|
||||||
|
"pollIntervalSeconds": int(cfg.get("pollIntervalSeconds") or 30),
|
||||||
|
"markSeen": bool(cfg.get("markSeen", True)),
|
||||||
|
"maxBodyChars": int(cfg.get("maxBodyChars") or 12000),
|
||||||
|
"subjectPrefix": str(cfg.get("subjectPrefix") or "Re: "),
|
||||||
|
"allowFrom": _normalize_allow_from(cfg.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
external_app_id = str(
|
||||||
|
cfg.get("appId") or cfg.get("clientId") or cfg.get("botToken") or cfg.get("externalAppId") or ""
|
||||||
|
)
|
||||||
|
app_secret = str(
|
||||||
|
cfg.get("appSecret")
|
||||||
|
or cfg.get("clientSecret")
|
||||||
|
or cfg.get("secret")
|
||||||
|
or cfg.get("token")
|
||||||
|
or cfg.get("appToken")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
extra = {
|
||||||
|
key: value
|
||||||
|
for key, value in cfg.items()
|
||||||
|
if key
|
||||||
|
not in {
|
||||||
|
"enabled",
|
||||||
|
"port",
|
||||||
|
"appId",
|
||||||
|
"clientId",
|
||||||
|
"botToken",
|
||||||
|
"externalAppId",
|
||||||
|
"appSecret",
|
||||||
|
"clientSecret",
|
||||||
|
"secret",
|
||||||
|
"token",
|
||||||
|
"appToken",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": ctype,
|
||||||
|
"bot_id": bot_id,
|
||||||
|
"channel_type": ctype,
|
||||||
|
"external_app_id": external_app_id,
|
||||||
|
"app_secret": app_secret,
|
||||||
|
"internal_port": port,
|
||||||
|
"is_active": enabled,
|
||||||
|
"extra_config": extra,
|
||||||
|
"locked": ctype == "dashboard",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def channel_api_to_config(row: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
ctype = str(row.get("channel_type") or "").strip().lower()
|
||||||
|
enabled = bool(row.get("is_active", True))
|
||||||
|
extra = normalize_channel_extra(row.get("extra_config"))
|
||||||
|
external_app_id = str(row.get("external_app_id") or "")
|
||||||
|
app_secret = str(row.get("app_secret") or "")
|
||||||
|
port = max(1, min(int(row.get("internal_port") or 8080), 65535))
|
||||||
|
|
||||||
|
if ctype == "feishu":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"appId": external_app_id,
|
||||||
|
"appSecret": app_secret,
|
||||||
|
"encryptKey": extra.get("encryptKey", ""),
|
||||||
|
"verificationToken": extra.get("verificationToken", ""),
|
||||||
|
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
if ctype == "dingtalk":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"clientId": external_app_id,
|
||||||
|
"clientSecret": app_secret,
|
||||||
|
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
if ctype == "telegram":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"token": app_secret,
|
||||||
|
"proxy": extra.get("proxy", ""),
|
||||||
|
"replyToMessage": bool(extra.get("replyToMessage", False)),
|
||||||
|
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
if ctype == "slack":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"mode": extra.get("mode", "socket"),
|
||||||
|
"botToken": external_app_id,
|
||||||
|
"appToken": app_secret,
|
||||||
|
"replyInThread": bool(extra.get("replyInThread", True)),
|
||||||
|
"groupPolicy": extra.get("groupPolicy", "mention"),
|
||||||
|
"groupAllowFrom": extra.get("groupAllowFrom", []),
|
||||||
|
"reactEmoji": extra.get("reactEmoji", "eyes"),
|
||||||
|
}
|
||||||
|
if ctype == "qq":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"appId": external_app_id,
|
||||||
|
"secret": app_secret,
|
||||||
|
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
if ctype == "weixin":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"token": app_secret,
|
||||||
|
}
|
||||||
|
if ctype == "email":
|
||||||
|
return {
|
||||||
|
"enabled": enabled,
|
||||||
|
"consentGranted": bool(extra.get("consentGranted", False)),
|
||||||
|
"imapHost": str(extra.get("imapHost") or ""),
|
||||||
|
"imapPort": max(1, min(int(extra.get("imapPort") or 993), 65535)),
|
||||||
|
"imapUsername": str(extra.get("imapUsername") or ""),
|
||||||
|
"imapPassword": str(extra.get("imapPassword") or ""),
|
||||||
|
"imapMailbox": str(extra.get("imapMailbox") or "INBOX"),
|
||||||
|
"imapUseSsl": bool(extra.get("imapUseSsl", True)),
|
||||||
|
"smtpHost": str(extra.get("smtpHost") or ""),
|
||||||
|
"smtpPort": max(1, min(int(extra.get("smtpPort") or 587), 65535)),
|
||||||
|
"smtpUsername": str(extra.get("smtpUsername") or ""),
|
||||||
|
"smtpPassword": str(extra.get("smtpPassword") or ""),
|
||||||
|
"smtpUseTls": bool(extra.get("smtpUseTls", True)),
|
||||||
|
"smtpUseSsl": bool(extra.get("smtpUseSsl", False)),
|
||||||
|
"fromAddress": str(extra.get("fromAddress") or ""),
|
||||||
|
"autoReplyEnabled": bool(extra.get("autoReplyEnabled", True)),
|
||||||
|
"pollIntervalSeconds": max(5, int(extra.get("pollIntervalSeconds") or 30)),
|
||||||
|
"markSeen": bool(extra.get("markSeen", True)),
|
||||||
|
"maxBodyChars": max(1, int(extra.get("maxBodyChars") or 12000)),
|
||||||
|
"subjectPrefix": str(extra.get("subjectPrefix") or "Re: "),
|
||||||
|
"allowFrom": _normalize_allow_from(extra.get("allowFrom", [])),
|
||||||
|
}
|
||||||
|
merged = dict(extra)
|
||||||
|
merged.update(
|
||||||
|
{
|
||||||
|
"enabled": enabled,
|
||||||
|
"appId": external_app_id,
|
||||||
|
"appSecret": app_secret,
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def list_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]:
|
||||||
|
config_data = _read_bot_config(bot.id)
|
||||||
|
channels_cfg = config_data.get("channels")
|
||||||
|
if not isinstance(channels_cfg, dict):
|
||||||
|
channels_cfg = {}
|
||||||
|
send_progress, send_tool_hints = read_global_delivery_flags(channels_cfg)
|
||||||
|
rows: List[Dict[str, Any]] = [
|
||||||
|
{
|
||||||
|
"id": "dashboard",
|
||||||
|
"bot_id": bot.id,
|
||||||
|
"channel_type": "dashboard",
|
||||||
|
"external_app_id": f"dashboard-{bot.id}",
|
||||||
|
"app_secret": "",
|
||||||
|
"internal_port": 9000,
|
||||||
|
"is_active": True,
|
||||||
|
"extra_config": {
|
||||||
|
"sendProgress": send_progress,
|
||||||
|
"sendToolHints": send_tool_hints,
|
||||||
|
},
|
||||||
|
"locked": True,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for ctype, cfg in channels_cfg.items():
|
||||||
|
if ctype in {"sendProgress", "sendToolHints", "dashboard"} or not isinstance(cfg, dict):
|
||||||
|
continue
|
||||||
|
rows.append(channel_config_to_api(bot.id, ctype, cfg))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_initial_bot_channels(bot_id: str, channels: Optional[List[ChannelConfigRequest]]) -> List[Dict[str, Any]]:
|
||||||
|
rows: List[Dict[str, Any]] = []
|
||||||
|
seen_types: set[str] = set()
|
||||||
|
for channel in channels or []:
|
||||||
|
ctype = (channel.channel_type or "").strip().lower()
|
||||||
|
if not ctype or ctype == "dashboard" or ctype in seen_types:
|
||||||
|
continue
|
||||||
|
seen_types.add(ctype)
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"id": ctype,
|
||||||
|
"bot_id": bot_id,
|
||||||
|
"channel_type": ctype,
|
||||||
|
"external_app_id": (channel.external_app_id or "").strip() or f"{ctype}-{bot_id}",
|
||||||
|
"app_secret": (channel.app_secret or "").strip(),
|
||||||
|
"internal_port": max(1, min(int(channel.internal_port or 8080), 65535)),
|
||||||
|
"is_active": bool(channel.is_active),
|
||||||
|
"extra_config": normalize_channel_extra(channel.extra_config),
|
||||||
|
"locked": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _read_workspace_md(bot_id: str, filename: str, default_value: str) -> str:
|
def _read_workspace_md(bot_id: str, filename: str, default_value: str) -> str:
|
||||||
path = os.path.join(_workspace_root(bot_id), filename)
|
path = os.path.join(_workspace_root(bot_id), filename)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
return default_value
|
return default_value
|
||||||
try:
|
try:
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as file:
|
||||||
return f.read().strip()
|
return file.read().strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
return default_value
|
return default_value
|
||||||
|
|
||||||
def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
|
||||||
|
def read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
config_data = _read_bot_config(bot.id)
|
config_data = _read_bot_config(bot.id)
|
||||||
env_params = _resolve_bot_env_params(bot.id)
|
env_params = resolve_bot_runtime_env_params(bot.id)
|
||||||
template_defaults = get_agent_md_templates()
|
template_defaults = get_agent_md_templates()
|
||||||
|
|
||||||
provider_name = ""
|
provider_name = ""
|
||||||
|
|
@ -156,7 +376,7 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
agents_defaults = defaults
|
agents_defaults = defaults
|
||||||
|
|
||||||
channels_cfg = config_data.get("channels")
|
channels_cfg = config_data.get("channels")
|
||||||
send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
send_progress, send_tool_hints = read_global_delivery_flags(channels_cfg)
|
||||||
|
|
||||||
llm_provider = provider_name or ""
|
llm_provider = provider_name or ""
|
||||||
llm_model = str(agents_defaults.get("model") or "")
|
llm_model = str(agents_defaults.get("model") or "")
|
||||||
|
|
@ -182,7 +402,7 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"cpu_cores": resources["cpu_cores"],
|
"cpu_cores": resources["cpu_cores"],
|
||||||
"memory_mb": resources["memory_mb"],
|
"memory_mb": resources["memory_mb"],
|
||||||
"storage_gb": resources["storage_gb"],
|
"storage_gb": resources["storage_gb"],
|
||||||
"system_timezone": env_params.get("TZ") or _get_default_system_timezone(),
|
"system_timezone": env_params.get("TZ") or get_default_bot_system_timezone(),
|
||||||
"send_progress": send_progress,
|
"send_progress": send_progress,
|
||||||
"send_tool_hints": send_tool_hints,
|
"send_tool_hints": send_tool_hints,
|
||||||
"soul_md": soul_md,
|
"soul_md": soul_md,
|
||||||
|
|
@ -193,8 +413,9 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"system_prompt": soul_md,
|
"system_prompt": soul_md,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
|
||||||
runtime = _read_bot_runtime_snapshot(bot)
|
def serialize_bot_detail(bot: BotInstance) -> Dict[str, Any]:
|
||||||
|
runtime = read_bot_runtime_snapshot(bot)
|
||||||
created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None
|
created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None
|
||||||
updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None
|
updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None
|
||||||
return {
|
return {
|
||||||
|
|
@ -216,7 +437,7 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"cpu_cores": _safe_float(runtime.get("cpu_cores"), 1.0),
|
"cpu_cores": _safe_float(runtime.get("cpu_cores"), 1.0),
|
||||||
"memory_mb": _safe_int(runtime.get("memory_mb"), 1024),
|
"memory_mb": _safe_int(runtime.get("memory_mb"), 1024),
|
||||||
"storage_gb": _safe_int(runtime.get("storage_gb"), 10),
|
"storage_gb": _safe_int(runtime.get("storage_gb"), 10),
|
||||||
"system_timezone": str(runtime.get("system_timezone") or _get_default_system_timezone()),
|
"system_timezone": str(runtime.get("system_timezone") or get_default_bot_system_timezone()),
|
||||||
"send_progress": bool(runtime.get("send_progress")),
|
"send_progress": bool(runtime.get("send_progress")),
|
||||||
"send_tool_hints": bool(runtime.get("send_tool_hints")),
|
"send_tool_hints": bool(runtime.get("send_tool_hints")),
|
||||||
"soul_md": runtime.get("soul_md") or "",
|
"soul_md": runtime.get("soul_md") or "",
|
||||||
|
|
@ -232,7 +453,8 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
|
|
||||||
|
def serialize_bot_list_entry(bot: BotInstance) -> Dict[str, Any]:
|
||||||
created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None
|
created_at = bot.created_at.isoformat() + "Z" if bot.created_at else None
|
||||||
updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None
|
updated_at = bot.updated_at.isoformat() + "Z" if bot.updated_at else None
|
||||||
return {
|
return {
|
||||||
|
|
@ -248,7 +470,8 @@ def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
|
||||||
"updated_at": updated_at,
|
"updated_at": updated_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _sync_workspace_channels(
|
|
||||||
|
def sync_bot_workspace_channels(
|
||||||
session: Session,
|
session: Session,
|
||||||
bot_id: str,
|
bot_id: str,
|
||||||
channels_override: Optional[List[Dict[str, Any]]] = None,
|
channels_override: Optional[List[Dict[str, Any]]] = None,
|
||||||
|
|
@ -258,12 +481,75 @@ def _sync_workspace_channels(
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
return
|
return
|
||||||
snapshot = _read_bot_runtime_snapshot(bot)
|
|
||||||
_sync_workspace_channels_impl(
|
snapshot = read_bot_runtime_snapshot(bot)
|
||||||
session,
|
template_defaults = get_agent_md_templates()
|
||||||
bot_id,
|
bot_data: Dict[str, Any] = {
|
||||||
snapshot,
|
"name": bot.name,
|
||||||
channels_override=channels_override,
|
"system_prompt": snapshot.get("system_prompt") or template_defaults.get("soul_md", ""),
|
||||||
global_delivery_override=global_delivery_override,
|
"soul_md": snapshot.get("soul_md") or template_defaults.get("soul_md", ""),
|
||||||
runtime_overrides=runtime_overrides,
|
"agents_md": snapshot.get("agents_md") or template_defaults.get("agents_md", ""),
|
||||||
|
"user_md": snapshot.get("user_md") or template_defaults.get("user_md", ""),
|
||||||
|
"tools_md": snapshot.get("tools_md") or template_defaults.get("tools_md", ""),
|
||||||
|
"identity_md": snapshot.get("identity_md") or template_defaults.get("identity_md", ""),
|
||||||
|
"llm_provider": snapshot.get("llm_provider") or "",
|
||||||
|
"llm_model": snapshot.get("llm_model") or "",
|
||||||
|
"api_key": snapshot.get("api_key") or "",
|
||||||
|
"api_base": snapshot.get("api_base") or "",
|
||||||
|
"temperature": snapshot.get("temperature"),
|
||||||
|
"top_p": snapshot.get("top_p"),
|
||||||
|
"max_tokens": snapshot.get("max_tokens"),
|
||||||
|
"cpu_cores": snapshot.get("cpu_cores"),
|
||||||
|
"memory_mb": snapshot.get("memory_mb"),
|
||||||
|
"storage_gb": snapshot.get("storage_gb"),
|
||||||
|
"send_progress": bool(snapshot.get("send_progress")),
|
||||||
|
"send_tool_hints": bool(snapshot.get("send_tool_hints")),
|
||||||
|
}
|
||||||
|
if isinstance(runtime_overrides, dict):
|
||||||
|
for key, value in runtime_overrides.items():
|
||||||
|
if key in {"api_key", "llm_provider", "llm_model"}:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
bot_data[key] = text
|
||||||
|
continue
|
||||||
|
if key == "api_base":
|
||||||
|
bot_data[key] = str(value or "").strip()
|
||||||
|
continue
|
||||||
|
bot_data[key] = value
|
||||||
|
|
||||||
|
resources = normalize_bot_resource_limits(
|
||||||
|
bot_data.get("cpu_cores"),
|
||||||
|
bot_data.get("memory_mb"),
|
||||||
|
bot_data.get("storage_gb"),
|
||||||
)
|
)
|
||||||
|
bot_data.update(resources)
|
||||||
|
send_progress = bool(bot_data.get("send_progress", False))
|
||||||
|
send_tool_hints = bool(bot_data.get("send_tool_hints", False))
|
||||||
|
if isinstance(global_delivery_override, dict):
|
||||||
|
if "sendProgress" in global_delivery_override:
|
||||||
|
send_progress = bool(global_delivery_override.get("sendProgress"))
|
||||||
|
if "sendToolHints" in global_delivery_override:
|
||||||
|
send_tool_hints = bool(global_delivery_override.get("sendToolHints"))
|
||||||
|
|
||||||
|
channels_data = channels_override if channels_override is not None else list_bot_channels_from_config(bot)
|
||||||
|
bot_data["send_progress"] = send_progress
|
||||||
|
bot_data["send_tool_hints"] = send_tool_hints
|
||||||
|
normalized_channels: List[Dict[str, Any]] = []
|
||||||
|
for row in channels_data:
|
||||||
|
ctype = str(row.get("channel_type") or "").strip().lower()
|
||||||
|
if not ctype or ctype == "dashboard":
|
||||||
|
continue
|
||||||
|
normalized_channels.append(
|
||||||
|
{
|
||||||
|
"channel_type": ctype,
|
||||||
|
"external_app_id": str(row.get("external_app_id") or ""),
|
||||||
|
"app_secret": str(row.get("app_secret") or ""),
|
||||||
|
"internal_port": max(1, min(int(row.get("internal_port") or 8080), 65535)),
|
||||||
|
"is_active": bool(row.get("is_active", True)),
|
||||||
|
"extra_config": normalize_channel_extra(row.get("extra_config")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
config_manager.update_workspace(bot_id=bot_id, bot_data=bot_data, channels=normalized_channels)
|
||||||
|
write_bot_resource_limits(bot_id, bot_data.get("cpu_cores"), bot_data.get("memory_mb"), bot_data.get("storage_gb"))
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,27 @@ import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from core.utils import _calc_dir_size_bytes
|
||||||
from core.settings import BOTS_WORKSPACE_ROOT
|
from core.settings import BOTS_WORKSPACE_ROOT
|
||||||
|
|
||||||
_ENV_KEY_RE = re.compile(r"^[A-Z_][A-Z0-9_]{0,127}$")
|
_ENV_KEY_RE = re.compile(r"^[A-Z_][A-Z0-9_]{0,127}$")
|
||||||
|
_BYTES_PER_GB = 1024 * 1024 * 1024
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"get_bot_data_root",
|
||||||
|
"normalize_bot_env_params",
|
||||||
|
"normalize_bot_resource_limits",
|
||||||
|
"read_bot_config_data",
|
||||||
|
"read_bot_cron_jobs_store",
|
||||||
|
"read_bot_env_params",
|
||||||
|
"get_bot_resource_limits",
|
||||||
|
"get_bot_workspace_root",
|
||||||
|
"get_bot_workspace_snapshot",
|
||||||
|
"get_bot_workspace_usage_bytes",
|
||||||
|
"write_bot_config_data",
|
||||||
|
"write_bot_cron_jobs_store",
|
||||||
|
"write_bot_env_params",
|
||||||
|
"write_bot_resource_limits",
|
||||||
"_bot_data_root",
|
"_bot_data_root",
|
||||||
"_clear_bot_dashboard_direct_session",
|
"_clear_bot_dashboard_direct_session",
|
||||||
"_clear_bot_sessions",
|
"_clear_bot_sessions",
|
||||||
|
|
@ -30,10 +46,18 @@ __all__ = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_workspace_root(bot_id: str) -> str:
|
||||||
|
return _workspace_root(bot_id)
|
||||||
|
|
||||||
|
|
||||||
def _workspace_root(bot_id: str) -> str:
|
def _workspace_root(bot_id: str) -> str:
|
||||||
return os.path.abspath(os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot", "workspace"))
|
return os.path.abspath(os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot", "workspace"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_data_root(bot_id: str) -> str:
|
||||||
|
return _bot_data_root(bot_id)
|
||||||
|
|
||||||
|
|
||||||
def _bot_data_root(bot_id: str) -> str:
|
def _bot_data_root(bot_id: str) -> str:
|
||||||
return os.path.abspath(os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot"))
|
return os.path.abspath(os.path.join(BOTS_WORKSPACE_ROOT, bot_id, ".nanobot"))
|
||||||
|
|
||||||
|
|
@ -72,6 +96,10 @@ def _normalize_resource_limits(cpu_cores: Any, memory_mb: Any, storage_gb: Any)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_bot_resource_limits(cpu_cores: Any, memory_mb: Any, storage_gb: Any) -> Dict[str, Any]:
|
||||||
|
return _normalize_resource_limits(cpu_cores, memory_mb, storage_gb)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_env_params(raw: Any) -> Dict[str, str]:
|
def _normalize_env_params(raw: Any) -> Dict[str, str]:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -84,6 +112,10 @@ def _normalize_env_params(raw: Any) -> Dict[str, str]:
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_bot_env_params(raw: Any) -> Dict[str, str]:
|
||||||
|
return _normalize_env_params(raw)
|
||||||
|
|
||||||
|
|
||||||
def _read_json_object(path: str) -> Dict[str, Any]:
|
def _read_json_object(path: str) -> Dict[str, Any]:
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -121,10 +153,18 @@ def _read_bot_config(bot_id: str) -> Dict[str, Any]:
|
||||||
return _read_json_object(_config_json_path(bot_id))
|
return _read_json_object(_config_json_path(bot_id))
|
||||||
|
|
||||||
|
|
||||||
|
def read_bot_config_data(bot_id: str) -> Dict[str, Any]:
|
||||||
|
return _read_bot_config(bot_id)
|
||||||
|
|
||||||
|
|
||||||
def _write_bot_config(bot_id: str, config_data: Dict[str, Any]) -> None:
|
def _write_bot_config(bot_id: str, config_data: Dict[str, Any]) -> None:
|
||||||
_write_json_atomic(_config_json_path(bot_id), config_data)
|
_write_json_atomic(_config_json_path(bot_id), config_data)
|
||||||
|
|
||||||
|
|
||||||
|
def write_bot_config_data(bot_id: str, config_data: Dict[str, Any]) -> None:
|
||||||
|
_write_bot_config(bot_id, config_data)
|
||||||
|
|
||||||
|
|
||||||
def _resources_json_path(bot_id: str) -> str:
|
def _resources_json_path(bot_id: str) -> str:
|
||||||
return os.path.join(_bot_data_root(bot_id), "resources.json")
|
return os.path.join(_bot_data_root(bot_id), "resources.json")
|
||||||
|
|
||||||
|
|
@ -141,6 +181,10 @@ def _write_bot_resources(bot_id: str, cpu_cores: Any, memory_mb: Any, storage_gb
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_bot_resource_limits(bot_id: str, cpu_cores: Any, memory_mb: Any, storage_gb: Any) -> None:
|
||||||
|
_write_bot_resources(bot_id, cpu_cores, memory_mb, storage_gb)
|
||||||
|
|
||||||
|
|
||||||
def _read_bot_resources(bot_id: str, config_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
def _read_bot_resources(bot_id: str, config_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
cpu_raw: Any = None
|
cpu_raw: Any = None
|
||||||
memory_raw: Any = None
|
memory_raw: Any = None
|
||||||
|
|
@ -168,6 +212,24 @@ def _read_bot_resources(bot_id: str, config_data: Optional[Dict[str, Any]] = Non
|
||||||
return _normalize_resource_limits(cpu_raw, memory_raw, storage_raw)
|
return _normalize_resource_limits(cpu_raw, memory_raw, storage_raw)
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_resource_limits(bot_id: str, config_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
return _read_bot_resources(bot_id, config_data=config_data)
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_workspace_usage_bytes(bot_id: str) -> int:
|
||||||
|
return _calc_dir_size_bytes(_workspace_root(bot_id))
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_workspace_snapshot(bot_id: str, config_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
resources = get_bot_resource_limits(bot_id, config_data=config_data)
|
||||||
|
configured_limit_bytes = int(resources.get("storage_gb") or 0) * _BYTES_PER_GB
|
||||||
|
return {
|
||||||
|
"path": get_bot_workspace_root(bot_id),
|
||||||
|
"usage_bytes": get_bot_workspace_usage_bytes(bot_id),
|
||||||
|
"configured_limit_bytes": configured_limit_bytes if configured_limit_bytes > 0 else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _migrate_bot_resources_store(bot_id: str) -> None:
|
def _migrate_bot_resources_store(bot_id: str) -> None:
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data = _read_bot_config(bot_id)
|
||||||
runtime_cfg = config_data.get("runtime")
|
runtime_cfg = config_data.get("runtime")
|
||||||
|
|
@ -201,10 +263,18 @@ def _read_env_store(bot_id: str) -> Dict[str, str]:
|
||||||
return _normalize_env_params(_read_json_object(_env_store_path(bot_id)))
|
return _normalize_env_params(_read_json_object(_env_store_path(bot_id)))
|
||||||
|
|
||||||
|
|
||||||
|
def read_bot_env_params(bot_id: str) -> Dict[str, str]:
|
||||||
|
return _read_env_store(bot_id)
|
||||||
|
|
||||||
|
|
||||||
def _write_env_store(bot_id: str, env_params: Dict[str, str]) -> None:
|
def _write_env_store(bot_id: str, env_params: Dict[str, str]) -> None:
|
||||||
_write_json_atomic(_env_store_path(bot_id), _normalize_env_params(env_params))
|
_write_json_atomic(_env_store_path(bot_id), _normalize_env_params(env_params))
|
||||||
|
|
||||||
|
|
||||||
|
def write_bot_env_params(bot_id: str, env_params: Dict[str, str]) -> None:
|
||||||
|
_write_env_store(bot_id, env_params)
|
||||||
|
|
||||||
|
|
||||||
def _cron_store_path(bot_id: str) -> str:
|
def _cron_store_path(bot_id: str) -> str:
|
||||||
return os.path.join(_workspace_root(bot_id), "cron", "jobs.json")
|
return os.path.join(_workspace_root(bot_id), "cron", "jobs.json")
|
||||||
|
|
||||||
|
|
@ -229,11 +299,19 @@ def _read_cron_store(bot_id: str) -> Dict[str, Any]:
|
||||||
return _normalize_cron_store_payload(_read_json_value(_cron_store_path(bot_id)))
|
return _normalize_cron_store_payload(_read_json_value(_cron_store_path(bot_id)))
|
||||||
|
|
||||||
|
|
||||||
|
def read_bot_cron_jobs_store(bot_id: str) -> Dict[str, Any]:
|
||||||
|
return _read_cron_store(bot_id)
|
||||||
|
|
||||||
|
|
||||||
def _write_cron_store(bot_id: str, store: Dict[str, Any]) -> None:
|
def _write_cron_store(bot_id: str, store: Dict[str, Any]) -> None:
|
||||||
normalized = _normalize_cron_store_payload(store)
|
normalized = _normalize_cron_store_payload(store)
|
||||||
_write_json_atomic(_cron_store_path(bot_id), normalized)
|
_write_json_atomic(_cron_store_path(bot_id), normalized)
|
||||||
|
|
||||||
|
|
||||||
|
def write_bot_cron_jobs_store(bot_id: str, store: Dict[str, Any]) -> None:
|
||||||
|
_write_cron_store(bot_id, store)
|
||||||
|
|
||||||
|
|
||||||
def _sessions_root(bot_id: str) -> str:
|
def _sessions_root(bot_id: str) -> str:
|
||||||
return os.path.join(_workspace_root(bot_id), "sessions")
|
return os.path.join(_workspace_root(bot_id), "sessions")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,16 @@ from fastapi import HTTPException
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
|
from core.utils import _is_video_attachment_path, _is_visual_attachment_path
|
||||||
from models.bot import BotInstance
|
from models.bot import BotInstance
|
||||||
from services.bot_service import _read_bot_runtime_snapshot
|
from services.bot_service import read_bot_runtime_snapshot
|
||||||
from services.platform_service import (
|
from services.platform_service import (
|
||||||
create_usage_request,
|
create_usage_request,
|
||||||
fail_latest_usage,
|
fail_latest_usage,
|
||||||
record_activity_event,
|
record_activity_event,
|
||||||
)
|
)
|
||||||
from services.runtime_service import _persist_runtime_packet, _queue_runtime_broadcast
|
from services.runtime_service import broadcast_runtime_packet, persist_runtime_packet
|
||||||
from services.workspace_service import _resolve_workspace_path
|
from services.workspace_service import resolve_workspace_path
|
||||||
from core.utils import _is_video_attachment_path, _is_visual_attachment_path
|
|
||||||
|
|
||||||
logger = logging.getLogger("dashboard.backend")
|
logger = logging.getLogger("dashboard.backend")
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ def send_bot_command(session: Session, bot_id: str, command: str, attachments: A
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
runtime_snapshot = _read_bot_runtime_snapshot(bot)
|
runtime_snapshot = read_bot_runtime_snapshot(bot)
|
||||||
|
|
||||||
normalized_attachments = _normalize_message_media_list(attachments)
|
normalized_attachments = _normalize_message_media_list(attachments)
|
||||||
text_command = str(command or "").strip()
|
text_command = str(command or "").strip()
|
||||||
|
|
@ -103,7 +103,7 @@ def send_bot_command(session: Session, bot_id: str, command: str, attachments: A
|
||||||
|
|
||||||
checked_attachments: List[str] = []
|
checked_attachments: List[str] = []
|
||||||
for rel_path in normalized_attachments:
|
for rel_path in normalized_attachments:
|
||||||
_, target = _resolve_workspace_path(bot_id, rel_path)
|
_, target = resolve_workspace_path(bot_id, rel_path)
|
||||||
if not os.path.isfile(target):
|
if not os.path.isfile(target):
|
||||||
raise HTTPException(status_code=400, detail=f"attachment not found: {rel_path}")
|
raise HTTPException(status_code=400, detail=f"attachment not found: {rel_path}")
|
||||||
checked_attachments.append(rel_path)
|
checked_attachments.append(rel_path)
|
||||||
|
|
@ -142,10 +142,10 @@ def send_bot_command(session: Session, bot_id: str, command: str, attachments: A
|
||||||
"media": checked_attachments,
|
"media": checked_attachments,
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
}
|
}
|
||||||
_persist_runtime_packet(bot_id, outbound_user_packet)
|
persist_runtime_packet(bot_id, outbound_user_packet)
|
||||||
|
|
||||||
if outbound_user_packet:
|
if outbound_user_packet:
|
||||||
_queue_runtime_broadcast(bot_id, outbound_user_packet)
|
broadcast_runtime_packet(bot_id, outbound_user_packet)
|
||||||
|
|
||||||
success = docker_manager.send_command(bot_id, delivery_command, media=delivery_media)
|
success = docker_manager.send_command(bot_id, delivery_command, media=delivery_media)
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -162,7 +162,7 @@ def send_bot_command(session: Session, bot_id: str, command: str, attachments: A
|
||||||
detail=(detail or "command delivery failed")[:400],
|
detail=(detail or "command delivery failed")[:400],
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
_queue_runtime_broadcast(
|
broadcast_runtime_packet(
|
||||||
bot_id,
|
bot_id,
|
||||||
{
|
{
|
||||||
"type": "AGENT_STATE",
|
"type": "AGENT_STATE",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ from core.cache import cache
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
from core.utils import _resolve_local_day_range
|
from core.utils import _resolve_local_day_range
|
||||||
from models.bot import BotInstance, BotMessage
|
from models.bot import BotInstance, BotMessage
|
||||||
from services.bot_storage_service import _clear_bot_dashboard_direct_session, _clear_bot_sessions, _workspace_root
|
from services.bot_storage_service import (
|
||||||
|
_clear_bot_dashboard_direct_session,
|
||||||
|
_clear_bot_sessions,
|
||||||
|
get_bot_workspace_root,
|
||||||
|
)
|
||||||
from services.cache_service import (
|
from services.cache_service import (
|
||||||
_cache_key_bot_messages,
|
_cache_key_bot_messages,
|
||||||
_cache_key_bot_messages_page,
|
_cache_key_bot_messages_page,
|
||||||
|
|
@ -33,7 +37,7 @@ def _normalize_message_media_item(bot_id: str, value: Any) -> str:
|
||||||
return ""
|
return ""
|
||||||
if raw.startswith("/root/.nanobot/workspace/"):
|
if raw.startswith("/root/.nanobot/workspace/"):
|
||||||
return raw[len("/root/.nanobot/workspace/") :].lstrip("/")
|
return raw[len("/root/.nanobot/workspace/") :].lstrip("/")
|
||||||
root = _workspace_root(bot_id)
|
root = get_bot_workspace_root(bot_id)
|
||||||
if os.path.isabs(raw):
|
if os.path.isabs(raw):
|
||||||
try:
|
try:
|
||||||
if os.path.commonpath([root, raw]) == root:
|
if os.path.commonpath([root, raw]) == root:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from core.cache import cache
|
||||||
|
from core.docker_instance import docker_manager
|
||||||
|
from models.bot import BotInstance, NanobotImage
|
||||||
|
from services.cache_service import _cache_key_images, _invalidate_images_cache
|
||||||
|
|
||||||
|
logger = logging.getLogger("dashboard.backend")
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_image(row: NanobotImage) -> Dict[str, Any]:
|
||||||
|
created_at = row.created_at.isoformat() + "Z" if row.created_at else None
|
||||||
|
return {
|
||||||
|
"tag": row.tag,
|
||||||
|
"image_id": row.image_id,
|
||||||
|
"version": row.version,
|
||||||
|
"status": row.status,
|
||||||
|
"source_dir": row.source_dir,
|
||||||
|
"created_at": created_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _reconcile_registered_images(session: Session) -> None:
|
||||||
|
rows = session.exec(select(NanobotImage)).all()
|
||||||
|
dirty = False
|
||||||
|
for row in rows:
|
||||||
|
docker_exists = docker_manager.has_image(row.tag)
|
||||||
|
next_status = "READY" if docker_exists else "ERROR"
|
||||||
|
next_image_id = row.image_id
|
||||||
|
if docker_exists and docker_manager.client:
|
||||||
|
try:
|
||||||
|
next_image_id = docker_manager.client.images.get(row.tag).id
|
||||||
|
except Exception:
|
||||||
|
next_image_id = row.image_id
|
||||||
|
if row.status != next_status or row.image_id != next_image_id:
|
||||||
|
row.status = next_status
|
||||||
|
row.image_id = next_image_id
|
||||||
|
session.add(row)
|
||||||
|
dirty = True
|
||||||
|
if dirty:
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def list_registered_images(session: Session) -> List[Dict[str, Any]]:
|
||||||
|
cached = cache.get_json(_cache_key_images())
|
||||||
|
if isinstance(cached, list) and all(isinstance(row, dict) for row in cached):
|
||||||
|
return cached
|
||||||
|
if isinstance(cached, list):
|
||||||
|
_invalidate_images_cache()
|
||||||
|
try:
|
||||||
|
_reconcile_registered_images(session)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("image reconcile skipped: %s", exc)
|
||||||
|
rows = session.exec(select(NanobotImage).order_by(NanobotImage.created_at.desc())).all()
|
||||||
|
payload = [_serialize_image(row) for row in rows]
|
||||||
|
cache.set_json(_cache_key_images(), payload, ttl=60)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def delete_registered_image(session: Session, *, tag: str) -> Dict[str, Any]:
|
||||||
|
image = session.get(NanobotImage, tag)
|
||||||
|
if not image:
|
||||||
|
raise HTTPException(status_code=404, detail="Image not found")
|
||||||
|
|
||||||
|
bots_using = session.exec(select(BotInstance).where(BotInstance.image_tag == tag)).all()
|
||||||
|
if bots_using:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Cannot delete image: {len(bots_using)} bots are using it.")
|
||||||
|
|
||||||
|
session.delete(image)
|
||||||
|
session.commit()
|
||||||
|
_invalidate_images_cache()
|
||||||
|
return {"status": "deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
def list_docker_images_by_repository(repository: str = "nanobot-base") -> List[Dict[str, Any]]:
|
||||||
|
return docker_manager.list_images_by_repo(repository)
|
||||||
|
|
||||||
|
|
||||||
|
def register_image(session: Session, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
tag = str(payload.get("tag") or "").strip()
|
||||||
|
source_dir = str(payload.get("source_dir") or "manual").strip() or "manual"
|
||||||
|
if not tag:
|
||||||
|
raise HTTPException(status_code=400, detail="tag is required")
|
||||||
|
if not docker_manager.has_image(tag):
|
||||||
|
raise HTTPException(status_code=404, detail=f"Docker image not found: {tag}")
|
||||||
|
|
||||||
|
version = tag.split(":")[-1].removeprefix("v") if ":" in tag else tag
|
||||||
|
try:
|
||||||
|
docker_img = docker_manager.client.images.get(tag) if docker_manager.client else None
|
||||||
|
image_id = docker_img.id if docker_img else None
|
||||||
|
except Exception:
|
||||||
|
image_id = None
|
||||||
|
|
||||||
|
row = session.get(NanobotImage, tag)
|
||||||
|
if not row:
|
||||||
|
row = NanobotImage(
|
||||||
|
tag=tag,
|
||||||
|
version=version,
|
||||||
|
status="READY",
|
||||||
|
source_dir=source_dir,
|
||||||
|
image_id=image_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
row.version = version
|
||||||
|
row.status = "READY"
|
||||||
|
row.source_dir = source_dir
|
||||||
|
row.image_id = image_id
|
||||||
|
session.add(row)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(row)
|
||||||
|
_invalidate_images_cache()
|
||||||
|
return _serialize_image(row)
|
||||||
|
|
@ -2,9 +2,8 @@ from typing import Any, Dict
|
||||||
|
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from core.utils import _calc_dir_size_bytes
|
|
||||||
from models.bot import BotInstance, NanobotImage
|
from models.bot import BotInstance, NanobotImage
|
||||||
from services.bot_storage_service import _read_bot_resources, _workspace_root
|
from services.bot_storage_service import get_bot_resource_limits, get_bot_workspace_snapshot
|
||||||
from services.platform_activity_service import (
|
from services.platform_activity_service import (
|
||||||
get_bot_activity_stats,
|
get_bot_activity_stats,
|
||||||
list_activity_events,
|
list_activity_events,
|
||||||
|
|
@ -39,15 +38,15 @@ def build_platform_overview(session: Session, docker_manager: Any) -> Dict[str,
|
||||||
for bot in bots:
|
for bot in bots:
|
||||||
enabled = bool(getattr(bot, "enabled", True))
|
enabled = bool(getattr(bot, "enabled", True))
|
||||||
runtime_status = docker_manager.get_bot_status(bot.id) if docker_manager else str(bot.docker_status or "STOPPED")
|
runtime_status = docker_manager.get_bot_status(bot.id) if docker_manager else str(bot.docker_status or "STOPPED")
|
||||||
resources = _read_bot_resources(bot.id)
|
resources = get_bot_resource_limits(bot.id)
|
||||||
runtime = (
|
runtime = (
|
||||||
docker_manager.get_bot_resource_snapshot(bot.id)
|
docker_manager.get_bot_resource_snapshot(bot.id)
|
||||||
if docker_manager
|
if docker_manager
|
||||||
else {"usage": {}, "limits": {}, "docker_status": runtime_status}
|
else {"usage": {}, "limits": {}, "docker_status": runtime_status}
|
||||||
)
|
)
|
||||||
workspace_root = _workspace_root(bot.id)
|
workspace = get_bot_workspace_snapshot(bot.id, config_data=None)
|
||||||
workspace_used = _calc_dir_size_bytes(workspace_root)
|
workspace_used = int(workspace.get("usage_bytes") or 0)
|
||||||
workspace_limit = int(resources["storage_gb"] or 0) * 1024 * 1024 * 1024
|
workspace_limit = int(workspace.get("configured_limit_bytes") or 0)
|
||||||
|
|
||||||
configured_cpu_total += float(resources["cpu_cores"] or 0)
|
configured_cpu_total += float(resources["cpu_cores"] or 0)
|
||||||
configured_memory_total += int(resources["memory_mb"] or 0) * 1024 * 1024
|
configured_memory_total += int(resources["memory_mb"] or 0) * 1024 * 1024
|
||||||
|
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
from typing import Any, Dict, List
|
|
||||||
|
|
||||||
from sqlmodel import Session, select
|
|
||||||
|
|
||||||
from core.database import engine
|
|
||||||
from core.settings import (
|
|
||||||
DEFAULT_STT_AUDIO_FILTER,
|
|
||||||
DEFAULT_STT_AUDIO_PREPROCESS,
|
|
||||||
DEFAULT_STT_DEFAULT_LANGUAGE,
|
|
||||||
DEFAULT_STT_FORCE_SIMPLIFIED,
|
|
||||||
DEFAULT_STT_INITIAL_PROMPT,
|
|
||||||
DEFAULT_STT_MAX_AUDIO_SECONDS,
|
|
||||||
STT_DEVICE,
|
|
||||||
STT_MODEL,
|
|
||||||
)
|
|
||||||
from models.platform import PlatformSetting
|
|
||||||
from schemas.platform import PlatformSettingsPayload
|
|
||||||
from services.platform_settings_core import (
|
|
||||||
SETTING_KEYS,
|
|
||||||
SYSTEM_SETTING_DEFINITIONS,
|
|
||||||
_bootstrap_platform_setting_values,
|
|
||||||
_normalize_extension_list,
|
|
||||||
_read_setting_value,
|
|
||||||
_upsert_setting_row,
|
|
||||||
)
|
|
||||||
from services.platform_system_settings_service import ensure_default_system_settings
|
|
||||||
|
|
||||||
|
|
||||||
def default_platform_settings() -> PlatformSettingsPayload:
|
|
||||||
bootstrap = _bootstrap_platform_setting_values()
|
|
||||||
return PlatformSettingsPayload(
|
|
||||||
page_size=int(bootstrap["page_size"]),
|
|
||||||
chat_pull_page_size=int(bootstrap["chat_pull_page_size"]),
|
|
||||||
command_auto_unlock_seconds=int(bootstrap["command_auto_unlock_seconds"]),
|
|
||||||
auth_token_ttl_hours=int(bootstrap["auth_token_ttl_hours"]),
|
|
||||||
auth_token_max_active=int(bootstrap["auth_token_max_active"]),
|
|
||||||
upload_max_mb=int(bootstrap["upload_max_mb"]),
|
|
||||||
allowed_attachment_extensions=list(bootstrap["allowed_attachment_extensions"]),
|
|
||||||
workspace_download_extensions=list(bootstrap["workspace_download_extensions"]),
|
|
||||||
speech_enabled=bool(bootstrap["speech_enabled"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_platform_settings(session: Session) -> PlatformSettingsPayload:
|
|
||||||
defaults = default_platform_settings()
|
|
||||||
ensure_default_system_settings(session)
|
|
||||||
rows = session.exec(select(PlatformSetting).where(PlatformSetting.key.in_(SETTING_KEYS))).all()
|
|
||||||
data: Dict[str, Any] = {row.key: _read_setting_value(row) for row in rows}
|
|
||||||
|
|
||||||
merged = defaults.model_dump()
|
|
||||||
merged["page_size"] = max(1, min(100, int(data.get("page_size") or merged["page_size"])))
|
|
||||||
merged["chat_pull_page_size"] = max(10, min(500, int(data.get("chat_pull_page_size") or merged["chat_pull_page_size"])))
|
|
||||||
merged["command_auto_unlock_seconds"] = max(
|
|
||||||
1,
|
|
||||||
min(600, int(data.get("command_auto_unlock_seconds") or merged["command_auto_unlock_seconds"])),
|
|
||||||
)
|
|
||||||
merged["auth_token_ttl_hours"] = max(
|
|
||||||
1,
|
|
||||||
min(720, int(data.get("auth_token_ttl_hours") or merged["auth_token_ttl_hours"])),
|
|
||||||
)
|
|
||||||
merged["auth_token_max_active"] = max(
|
|
||||||
1,
|
|
||||||
min(20, int(data.get("auth_token_max_active") or merged["auth_token_max_active"])),
|
|
||||||
)
|
|
||||||
merged["upload_max_mb"] = int(data.get("upload_max_mb") or merged["upload_max_mb"])
|
|
||||||
merged["allowed_attachment_extensions"] = _normalize_extension_list(
|
|
||||||
data.get("allowed_attachment_extensions", merged["allowed_attachment_extensions"])
|
|
||||||
)
|
|
||||||
merged["workspace_download_extensions"] = _normalize_extension_list(
|
|
||||||
data.get("workspace_download_extensions", merged["workspace_download_extensions"])
|
|
||||||
)
|
|
||||||
merged["speech_enabled"] = bool(data.get("speech_enabled", merged["speech_enabled"]))
|
|
||||||
return PlatformSettingsPayload.model_validate(merged)
|
|
||||||
|
|
||||||
|
|
||||||
def save_platform_settings(session: Session, payload: PlatformSettingsPayload) -> PlatformSettingsPayload:
|
|
||||||
normalized = PlatformSettingsPayload(
|
|
||||||
page_size=max(1, min(100, int(payload.page_size))),
|
|
||||||
chat_pull_page_size=max(10, min(500, int(payload.chat_pull_page_size))),
|
|
||||||
command_auto_unlock_seconds=max(1, min(600, int(payload.command_auto_unlock_seconds))),
|
|
||||||
auth_token_ttl_hours=max(1, min(720, int(payload.auth_token_ttl_hours))),
|
|
||||||
auth_token_max_active=max(1, min(20, int(payload.auth_token_max_active))),
|
|
||||||
upload_max_mb=payload.upload_max_mb,
|
|
||||||
allowed_attachment_extensions=_normalize_extension_list(payload.allowed_attachment_extensions),
|
|
||||||
workspace_download_extensions=_normalize_extension_list(payload.workspace_download_extensions),
|
|
||||||
speech_enabled=bool(payload.speech_enabled),
|
|
||||||
)
|
|
||||||
payload_by_key = normalized.model_dump()
|
|
||||||
for key in SETTING_KEYS:
|
|
||||||
definition = SYSTEM_SETTING_DEFINITIONS[key]
|
|
||||||
_upsert_setting_row(
|
|
||||||
session,
|
|
||||||
key,
|
|
||||||
name=str(definition["name"]),
|
|
||||||
category=str(definition["category"]),
|
|
||||||
description=str(definition["description"]),
|
|
||||||
value_type=str(definition["value_type"]),
|
|
||||||
value=payload_by_key[key],
|
|
||||||
is_public=bool(definition["is_public"]),
|
|
||||||
sort_order=int(definition["sort_order"]),
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def get_platform_settings_snapshot() -> PlatformSettingsPayload:
|
|
||||||
with Session(engine) as session:
|
|
||||||
return get_platform_settings(session)
|
|
||||||
|
|
||||||
|
|
||||||
def get_upload_max_mb() -> int:
|
|
||||||
return get_platform_settings_snapshot().upload_max_mb
|
|
||||||
|
|
||||||
|
|
||||||
def get_allowed_attachment_extensions() -> List[str]:
|
|
||||||
return get_platform_settings_snapshot().allowed_attachment_extensions
|
|
||||||
|
|
||||||
|
|
||||||
def get_workspace_download_extensions() -> List[str]:
|
|
||||||
return get_platform_settings_snapshot().workspace_download_extensions
|
|
||||||
|
|
||||||
|
|
||||||
def get_page_size() -> int:
|
|
||||||
return get_platform_settings_snapshot().page_size
|
|
||||||
|
|
||||||
|
|
||||||
def get_chat_pull_page_size() -> int:
|
|
||||||
return get_platform_settings_snapshot().chat_pull_page_size
|
|
||||||
|
|
||||||
|
|
||||||
def get_auth_token_ttl_hours(session: Session) -> int:
|
|
||||||
return get_platform_settings(session).auth_token_ttl_hours
|
|
||||||
|
|
||||||
|
|
||||||
def get_auth_token_max_active(session: Session) -> int:
|
|
||||||
return get_platform_settings(session).auth_token_max_active
|
|
||||||
|
|
||||||
|
|
||||||
def get_speech_runtime_settings() -> Dict[str, Any]:
|
|
||||||
settings = get_platform_settings_snapshot()
|
|
||||||
return {
|
|
||||||
"enabled": bool(settings.speech_enabled),
|
|
||||||
"max_audio_seconds": int(DEFAULT_STT_MAX_AUDIO_SECONDS),
|
|
||||||
"default_language": str(DEFAULT_STT_DEFAULT_LANGUAGE or "zh").strip().lower() or "zh",
|
|
||||||
"force_simplified": bool(DEFAULT_STT_FORCE_SIMPLIFIED),
|
|
||||||
"audio_preprocess": bool(DEFAULT_STT_AUDIO_PREPROCESS),
|
|
||||||
"audio_filter": str(DEFAULT_STT_AUDIO_FILTER or "").strip(),
|
|
||||||
"initial_prompt": str(DEFAULT_STT_INITIAL_PROMPT or "").strip(),
|
|
||||||
"model": STT_MODEL,
|
|
||||||
"device": STT_DEVICE,
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
from services.platform_runtime_settings_service import (
|
from typing import Any, Dict, List
|
||||||
get_auth_token_max_active,
|
|
||||||
get_auth_token_ttl_hours,
|
from sqlmodel import Session, select
|
||||||
default_platform_settings,
|
|
||||||
get_allowed_attachment_extensions,
|
from core.database import engine
|
||||||
get_chat_pull_page_size,
|
from core.settings import (
|
||||||
get_page_size,
|
DEFAULT_STT_AUDIO_FILTER,
|
||||||
get_platform_settings,
|
DEFAULT_STT_AUDIO_PREPROCESS,
|
||||||
get_platform_settings_snapshot,
|
DEFAULT_STT_DEFAULT_LANGUAGE,
|
||||||
get_speech_runtime_settings,
|
DEFAULT_STT_FORCE_SIMPLIFIED,
|
||||||
get_upload_max_mb,
|
DEFAULT_STT_INITIAL_PROMPT,
|
||||||
get_workspace_download_extensions,
|
DEFAULT_STT_MAX_AUDIO_SECONDS,
|
||||||
save_platform_settings,
|
STT_DEVICE,
|
||||||
|
STT_MODEL,
|
||||||
)
|
)
|
||||||
|
from models.platform import PlatformSetting
|
||||||
|
from schemas.platform import PlatformSettingsPayload
|
||||||
from services.platform_settings_core import (
|
from services.platform_settings_core import (
|
||||||
ACTIVITY_EVENT_RETENTION_SETTING_KEY,
|
ACTIVITY_EVENT_RETENTION_SETTING_KEY,
|
||||||
DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS,
|
DEFAULT_ACTIVITY_EVENT_RETENTION_DAYS,
|
||||||
DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS,
|
DEFAULT_ALLOWED_ATTACHMENT_EXTENSIONS,
|
||||||
SETTING_KEYS,
|
SETTING_KEYS,
|
||||||
SYSTEM_SETTING_DEFINITIONS,
|
SYSTEM_SETTING_DEFINITIONS,
|
||||||
|
_bootstrap_platform_setting_values,
|
||||||
|
_normalize_extension_list,
|
||||||
|
_read_setting_value,
|
||||||
|
_upsert_setting_row,
|
||||||
)
|
)
|
||||||
from services.platform_system_settings_service import (
|
from services.platform_system_settings_service import (
|
||||||
create_or_update_system_setting,
|
create_or_update_system_setting,
|
||||||
|
|
@ -26,3 +33,128 @@ from services.platform_system_settings_service import (
|
||||||
get_activity_event_retention_days,
|
get_activity_event_retention_days,
|
||||||
list_system_settings,
|
list_system_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def default_platform_settings() -> PlatformSettingsPayload:
|
||||||
|
bootstrap = _bootstrap_platform_setting_values()
|
||||||
|
return PlatformSettingsPayload(
|
||||||
|
page_size=int(bootstrap["page_size"]),
|
||||||
|
chat_pull_page_size=int(bootstrap["chat_pull_page_size"]),
|
||||||
|
command_auto_unlock_seconds=int(bootstrap["command_auto_unlock_seconds"]),
|
||||||
|
auth_token_ttl_hours=int(bootstrap["auth_token_ttl_hours"]),
|
||||||
|
auth_token_max_active=int(bootstrap["auth_token_max_active"]),
|
||||||
|
upload_max_mb=int(bootstrap["upload_max_mb"]),
|
||||||
|
allowed_attachment_extensions=list(bootstrap["allowed_attachment_extensions"]),
|
||||||
|
workspace_download_extensions=list(bootstrap["workspace_download_extensions"]),
|
||||||
|
speech_enabled=bool(bootstrap["speech_enabled"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform_settings(session: Session) -> PlatformSettingsPayload:
|
||||||
|
defaults = default_platform_settings()
|
||||||
|
ensure_default_system_settings(session)
|
||||||
|
rows = session.exec(select(PlatformSetting).where(PlatformSetting.key.in_(SETTING_KEYS))).all()
|
||||||
|
data: Dict[str, Any] = {row.key: _read_setting_value(row) for row in rows}
|
||||||
|
|
||||||
|
merged = defaults.model_dump()
|
||||||
|
merged["page_size"] = max(1, min(100, int(data.get("page_size") or merged["page_size"])))
|
||||||
|
merged["chat_pull_page_size"] = max(10, min(500, int(data.get("chat_pull_page_size") or merged["chat_pull_page_size"])))
|
||||||
|
merged["command_auto_unlock_seconds"] = max(
|
||||||
|
1,
|
||||||
|
min(600, int(data.get("command_auto_unlock_seconds") or merged["command_auto_unlock_seconds"])),
|
||||||
|
)
|
||||||
|
merged["auth_token_ttl_hours"] = max(
|
||||||
|
1,
|
||||||
|
min(720, int(data.get("auth_token_ttl_hours") or merged["auth_token_ttl_hours"])),
|
||||||
|
)
|
||||||
|
merged["auth_token_max_active"] = max(
|
||||||
|
1,
|
||||||
|
min(20, int(data.get("auth_token_max_active") or merged["auth_token_max_active"])),
|
||||||
|
)
|
||||||
|
merged["upload_max_mb"] = int(data.get("upload_max_mb") or merged["upload_max_mb"])
|
||||||
|
merged["allowed_attachment_extensions"] = _normalize_extension_list(
|
||||||
|
data.get("allowed_attachment_extensions", merged["allowed_attachment_extensions"])
|
||||||
|
)
|
||||||
|
merged["workspace_download_extensions"] = _normalize_extension_list(
|
||||||
|
data.get("workspace_download_extensions", merged["workspace_download_extensions"])
|
||||||
|
)
|
||||||
|
merged["speech_enabled"] = bool(data.get("speech_enabled", merged["speech_enabled"]))
|
||||||
|
return PlatformSettingsPayload.model_validate(merged)
|
||||||
|
|
||||||
|
|
||||||
|
def save_platform_settings(session: Session, payload: PlatformSettingsPayload) -> PlatformSettingsPayload:
|
||||||
|
normalized = PlatformSettingsPayload(
|
||||||
|
page_size=max(1, min(100, int(payload.page_size))),
|
||||||
|
chat_pull_page_size=max(10, min(500, int(payload.chat_pull_page_size))),
|
||||||
|
command_auto_unlock_seconds=max(1, min(600, int(payload.command_auto_unlock_seconds))),
|
||||||
|
auth_token_ttl_hours=max(1, min(720, int(payload.auth_token_ttl_hours))),
|
||||||
|
auth_token_max_active=max(1, min(20, int(payload.auth_token_max_active))),
|
||||||
|
upload_max_mb=payload.upload_max_mb,
|
||||||
|
allowed_attachment_extensions=_normalize_extension_list(payload.allowed_attachment_extensions),
|
||||||
|
workspace_download_extensions=_normalize_extension_list(payload.workspace_download_extensions),
|
||||||
|
speech_enabled=bool(payload.speech_enabled),
|
||||||
|
)
|
||||||
|
payload_by_key = normalized.model_dump()
|
||||||
|
for key in SETTING_KEYS:
|
||||||
|
definition = SYSTEM_SETTING_DEFINITIONS[key]
|
||||||
|
_upsert_setting_row(
|
||||||
|
session,
|
||||||
|
key,
|
||||||
|
name=str(definition["name"]),
|
||||||
|
category=str(definition["category"]),
|
||||||
|
description=str(definition["description"]),
|
||||||
|
value_type=str(definition["value_type"]),
|
||||||
|
value=payload_by_key[key],
|
||||||
|
is_public=bool(definition["is_public"]),
|
||||||
|
sort_order=int(definition["sort_order"]),
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def get_platform_settings_snapshot() -> PlatformSettingsPayload:
|
||||||
|
with Session(engine) as session:
|
||||||
|
return get_platform_settings(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_upload_max_mb() -> int:
|
||||||
|
return get_platform_settings_snapshot().upload_max_mb
|
||||||
|
|
||||||
|
|
||||||
|
def get_allowed_attachment_extensions() -> List[str]:
|
||||||
|
return get_platform_settings_snapshot().allowed_attachment_extensions
|
||||||
|
|
||||||
|
|
||||||
|
def get_workspace_download_extensions() -> List[str]:
|
||||||
|
return get_platform_settings_snapshot().workspace_download_extensions
|
||||||
|
|
||||||
|
|
||||||
|
def get_page_size() -> int:
|
||||||
|
return get_platform_settings_snapshot().page_size
|
||||||
|
|
||||||
|
|
||||||
|
def get_chat_pull_page_size() -> int:
|
||||||
|
return get_platform_settings_snapshot().chat_pull_page_size
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_token_ttl_hours(session: Session) -> int:
|
||||||
|
return get_platform_settings(session).auth_token_ttl_hours
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_token_max_active(session: Session) -> int:
|
||||||
|
return get_platform_settings(session).auth_token_max_active
|
||||||
|
|
||||||
|
|
||||||
|
def get_speech_runtime_settings() -> Dict[str, Any]:
|
||||||
|
settings = get_platform_settings_snapshot()
|
||||||
|
return {
|
||||||
|
"enabled": bool(settings.speech_enabled),
|
||||||
|
"max_audio_seconds": int(DEFAULT_STT_MAX_AUDIO_SECONDS),
|
||||||
|
"default_language": str(DEFAULT_STT_DEFAULT_LANGUAGE or "zh").strip().lower() or "zh",
|
||||||
|
"force_simplified": bool(DEFAULT_STT_FORCE_SIMPLIFIED),
|
||||||
|
"audio_preprocess": bool(DEFAULT_STT_AUDIO_PREPROCESS),
|
||||||
|
"audio_filter": str(DEFAULT_STT_AUDIO_FILTER or "").strip(),
|
||||||
|
"initial_prompt": str(DEFAULT_STT_INITIAL_PROMPT or "").strip(),
|
||||||
|
"model": STT_MODEL,
|
||||||
|
"device": STT_DEVICE,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
def get_provider_defaults(provider: str) -> tuple[str, str]:
|
||||||
|
normalized = str(provider or "").lower().strip()
|
||||||
|
if normalized in {"openai"}:
|
||||||
|
return "openai", "https://api.openai.com/v1"
|
||||||
|
if normalized in {"openrouter"}:
|
||||||
|
return "openrouter", "https://openrouter.ai/api/v1"
|
||||||
|
if normalized in {"dashscope", "aliyun", "qwen", "aliyun-qwen"}:
|
||||||
|
return "dashscope", "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||||
|
if normalized in {"deepseek"}:
|
||||||
|
return "deepseek", "https://api.deepseek.com/v1"
|
||||||
|
if normalized in {"xunfei", "iflytek", "xfyun"}:
|
||||||
|
return "openai", "https://spark-api-open.xf-yun.com/v1"
|
||||||
|
if normalized in {"vllm"}:
|
||||||
|
return "openai", ""
|
||||||
|
if normalized in {"kimi", "moonshot"}:
|
||||||
|
return "kimi", "https://api.moonshot.cn/v1"
|
||||||
|
if normalized in {"minimax"}:
|
||||||
|
return "minimax", "https://api.minimax.chat/v1"
|
||||||
|
return normalized, ""
|
||||||
|
|
||||||
|
|
||||||
|
async def test_provider_connection(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
provider = str(payload.get("provider") or "").strip()
|
||||||
|
api_key = str(payload.get("api_key") or "").strip()
|
||||||
|
model = str(payload.get("model") or "").strip()
|
||||||
|
api_base = str(payload.get("api_base") or "").strip()
|
||||||
|
|
||||||
|
if not provider or not api_key:
|
||||||
|
raise HTTPException(status_code=400, detail="provider and api_key are required")
|
||||||
|
|
||||||
|
normalized_provider, default_base = get_provider_defaults(provider)
|
||||||
|
base = (api_base or default_base).rstrip("/")
|
||||||
|
if normalized_provider not in {"openrouter", "dashscope", "kimi", "minimax", "openai", "deepseek"}:
|
||||||
|
raise HTTPException(status_code=400, detail=f"provider not supported for test: {provider}")
|
||||||
|
if not base:
|
||||||
|
raise HTTPException(status_code=400, detail=f"api_base is required for provider: {provider}")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
|
timeout = httpx.Timeout(20.0, connect=10.0)
|
||||||
|
url = f"{base}/models"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
|
response = await client.get(url, headers=headers)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"provider": normalized_provider,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"detail": response.text[:500],
|
||||||
|
}
|
||||||
|
data = response.json()
|
||||||
|
models_raw = data.get("data", []) if isinstance(data, dict) else []
|
||||||
|
model_ids: List[str] = [
|
||||||
|
str(item["id"]) for item in models_raw[:20] if isinstance(item, dict) and item.get("id")
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"provider": normalized_provider,
|
||||||
|
"endpoint": url,
|
||||||
|
"models_preview": model_ids[:8],
|
||||||
|
"model_hint": (
|
||||||
|
"model_found"
|
||||||
|
if model and any(model in item for item in model_ids)
|
||||||
|
else ("model_not_listed" if model else "")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"provider": normalized_provider,
|
||||||
|
"endpoint": url,
|
||||||
|
"detail": str(exc),
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ from core.database import engine
|
||||||
from core.docker_instance import docker_manager
|
from core.docker_instance import docker_manager
|
||||||
from core.websocket_manager import manager
|
from core.websocket_manager import manager
|
||||||
from models.bot import BotInstance, BotMessage
|
from models.bot import BotInstance, BotMessage
|
||||||
from services.bot_service import _workspace_root
|
from services.bot_storage_service import get_bot_workspace_root
|
||||||
from services.cache_service import _invalidate_bot_detail_cache, _invalidate_bot_messages_cache
|
from services.cache_service import _invalidate_bot_detail_cache, _invalidate_bot_messages_cache
|
||||||
from services.platform_service import bind_usage_message, finalize_usage_from_packet, record_activity_event
|
from services.platform_service import bind_usage_message, finalize_usage_from_packet, record_activity_event
|
||||||
from services.topic_runtime import publish_runtime_topic_packet
|
from services.topic_runtime import publish_runtime_topic_packet
|
||||||
|
|
@ -41,6 +41,10 @@ def _queue_runtime_broadcast(bot_id: str, packet: Dict[str, Any]) -> None:
|
||||||
asyncio.run_coroutine_threadsafe(manager.broadcast(bot_id, packet), loop)
|
asyncio.run_coroutine_threadsafe(manager.broadcast(bot_id, packet), loop)
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_runtime_packet(bot_id: str, packet: Dict[str, Any]) -> None:
|
||||||
|
_queue_runtime_broadcast(bot_id, packet)
|
||||||
|
|
||||||
|
|
||||||
def _normalize_packet_channel(packet: Dict[str, Any]) -> str:
|
def _normalize_packet_channel(packet: Dict[str, Any]) -> str:
|
||||||
raw = str(packet.get("channel") or packet.get("source") or "").strip().lower()
|
raw = str(packet.get("channel") or packet.get("source") or "").strip().lower()
|
||||||
if raw in {"dashboard", "dashboard_channel", "dashboard-channel"}:
|
if raw in {"dashboard", "dashboard_channel", "dashboard-channel"}:
|
||||||
|
|
@ -54,7 +58,7 @@ def _normalize_media_item(bot_id: str, value: Any) -> str:
|
||||||
return ""
|
return ""
|
||||||
if raw.startswith("/root/.nanobot/workspace/"):
|
if raw.startswith("/root/.nanobot/workspace/"):
|
||||||
return raw[len("/root/.nanobot/workspace/") :].lstrip("/")
|
return raw[len("/root/.nanobot/workspace/") :].lstrip("/")
|
||||||
root = _workspace_root(bot_id)
|
root = get_bot_workspace_root(bot_id)
|
||||||
if os.path.isabs(raw):
|
if os.path.isabs(raw):
|
||||||
try:
|
try:
|
||||||
if os.path.commonpath([root, raw]) == root:
|
if os.path.commonpath([root, raw]) == root:
|
||||||
|
|
@ -205,6 +209,10 @@ def _persist_runtime_packet(bot_id: str, packet: Dict[str, Any]) -> Optional[int
|
||||||
return persisted_message_id
|
return persisted_message_id
|
||||||
|
|
||||||
|
|
||||||
|
def persist_runtime_packet(bot_id: str, packet: Dict[str, Any]) -> Optional[int]:
|
||||||
|
return _persist_runtime_packet(bot_id, packet)
|
||||||
|
|
||||||
|
|
||||||
def docker_callback(bot_id: str, packet: Dict[str, Any]) -> None:
|
def docker_callback(bot_id: str, packet: Dict[str, Any]) -> None:
|
||||||
packet_type = str(packet.get("type", "")).upper()
|
packet_type = str(packet.get("type", "")).upper()
|
||||||
if packet_type == "RAW_LOG":
|
if packet_type == "RAW_LOG":
|
||||||
|
|
@ -272,3 +280,15 @@ async def _record_agent_loop_ready_warning(
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to record agent loop readiness warning for bot_id=%s", bot_id)
|
logger.exception("Failed to record agent loop readiness warning for bot_id=%s", bot_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def record_agent_loop_ready_warning(
|
||||||
|
bot_id: str,
|
||||||
|
timeout_seconds: float = 12.0,
|
||||||
|
poll_interval_seconds: float = 0.5,
|
||||||
|
) -> None:
|
||||||
|
await _record_agent_loop_ready_warning(
|
||||||
|
bot_id,
|
||||||
|
timeout_seconds=timeout_seconds,
|
||||||
|
poll_interval_seconds=poll_interval_seconds,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ from core.utils import (
|
||||||
)
|
)
|
||||||
from models.skill import BotSkillInstall, SkillMarketItem
|
from models.skill import BotSkillInstall, SkillMarketItem
|
||||||
from services.platform_service import get_platform_settings_snapshot
|
from services.platform_service import get_platform_settings_snapshot
|
||||||
from services.skill_service import _install_skill_zip_into_workspace, _skills_root
|
from services.skill_service import get_bot_skills_root, install_skill_zip_into_workspace
|
||||||
|
|
||||||
|
|
||||||
def _skill_market_root() -> str:
|
def _skill_market_root() -> str:
|
||||||
|
|
@ -341,7 +341,7 @@ def list_bot_skill_market_items(session: Session, *, bot_id: str) -> List[Dict[s
|
||||||
else (
|
else (
|
||||||
install_lookup[int(item.id or 0)].status == "INSTALLED"
|
install_lookup[int(item.id or 0)].status == "INSTALLED"
|
||||||
and all(
|
and all(
|
||||||
os.path.exists(os.path.join(_skills_root(bot_id), name))
|
os.path.exists(os.path.join(get_bot_skills_root(bot_id), name))
|
||||||
for name in _parse_json_string_list(install_lookup[int(item.id or 0)].installed_entries_json)
|
for name in _parse_json_string_list(install_lookup[int(item.id or 0)].installed_entries_json)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -378,7 +378,7 @@ def install_skill_market_item_for_bot(
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
install_result = _install_skill_zip_into_workspace(bot_id, zip_path)
|
install_result = install_skill_zip_into_workspace(bot_id, zip_path)
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
if not install_row:
|
if not install_row:
|
||||||
install_row = BotSkillInstall(
|
install_row = BotSkillInstall(
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,16 @@ from core.utils import (
|
||||||
_is_ignored_skill_zip_top_level,
|
_is_ignored_skill_zip_top_level,
|
||||||
_is_valid_top_level_skill_name,
|
_is_valid_top_level_skill_name,
|
||||||
)
|
)
|
||||||
from services.bot_storage_service import _workspace_root
|
from services.bot_storage_service import get_bot_workspace_root
|
||||||
from services.platform_service import get_platform_settings_snapshot
|
from services.platform_service import get_platform_settings_snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot_skills_root(bot_id: str) -> str:
|
||||||
|
return _skills_root(bot_id)
|
||||||
|
|
||||||
|
|
||||||
def _skills_root(bot_id: str) -> str:
|
def _skills_root(bot_id: str) -> str:
|
||||||
return os.path.join(_workspace_root(bot_id), "skills")
|
return os.path.join(get_bot_workspace_root(bot_id), "skills")
|
||||||
|
|
||||||
def _read_skill_description(entry_path: str) -> str:
|
def _read_skill_description(entry_path: str) -> str:
|
||||||
candidates: List[str] = []
|
candidates: List[str] = []
|
||||||
|
|
@ -139,6 +143,10 @@ def _install_skill_zip_into_workspace(bot_id: str, zip_path: str) -> Dict[str, A
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def install_skill_zip_into_workspace(bot_id: str, zip_path: str) -> Dict[str, Any]:
|
||||||
|
return _install_skill_zip_into_workspace(bot_id, zip_path)
|
||||||
|
|
||||||
|
|
||||||
def list_bot_skills(bot_id: str) -> List[Dict[str, Any]]:
|
def list_bot_skills(bot_id: str) -> List[Dict[str, Any]]:
|
||||||
return _list_workspace_skills(bot_id)
|
return _list_workspace_skills(bot_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
|
|
||||||
from services.topic_service import _topic_publish_internal
|
from services.topic_service import publish_topic_item
|
||||||
|
|
||||||
from .publisher import build_topic_publish_payload
|
from .publisher import build_topic_publish_payload
|
||||||
|
|
||||||
|
|
@ -30,6 +30,6 @@ def publish_runtime_topic_packet(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
_topic_publish_internal(session, bot_id, topic_payload)
|
publish_topic_item(session, bot_id, topic_payload)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("topic auto publish failed for bot %s packet %s", bot_id, packet_type)
|
logger.exception("topic auto publish failed for bot %s packet %s", bot_id, packet_type)
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
from models.bot import BotInstance
|
||||||
from models.topic import TopicItem, TopicTopic
|
from models.topic import TopicItem, TopicTopic
|
||||||
|
|
||||||
TOPIC_DEDUPE_WINDOW_SECONDS = 10 * 60
|
TOPIC_DEDUPE_WINDOW_SECONDS = 10 * 60
|
||||||
TOPIC_LEVEL_SET = {"info", "warn", "error", "success"}
|
TOPIC_LEVEL_SET = {"info", "warn", "error", "success"}
|
||||||
_TOPIC_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,63}$")
|
_TOPIC_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,63}$")
|
||||||
|
TOPIC_KEY_RE = _TOPIC_KEY_RE
|
||||||
|
|
||||||
|
|
||||||
def _as_bool(value: Any) -> bool:
|
def _as_bool(value: Any) -> bool:
|
||||||
|
|
@ -101,6 +104,13 @@ def _topic_get_row(session: Session, bot_id: str, topic_key: str) -> Optional[To
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bot_or_404(session: Session, bot_id: str) -> BotInstance:
|
||||||
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
return bot
|
||||||
|
|
||||||
|
|
||||||
def _normalize_topic_keywords(raw: Any) -> List[str]:
|
def _normalize_topic_keywords(raw: Any) -> List[str]:
|
||||||
rows: List[str] = []
|
rows: List[str] = []
|
||||||
if isinstance(raw, list):
|
if isinstance(raw, list):
|
||||||
|
|
@ -338,3 +348,217 @@ def _topic_publish_internal(session: Session, bot_id: str, payload: Dict[str, An
|
||||||
"item": _topic_item_to_dict(item),
|
"item": _topic_item_to_dict(item),
|
||||||
"route": route_result,
|
"route": route_result,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_topic_key(raw: Any) -> str:
|
||||||
|
return _normalize_topic_key(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def list_topics(session: Session, bot_id: str) -> List[Dict[str, Any]]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
return _list_topics(session, bot_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_topic(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
bot_id: str,
|
||||||
|
topic_key: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
is_active: bool = True,
|
||||||
|
routing: Optional[Dict[str, Any]] = None,
|
||||||
|
view_schema: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
normalized_key = _normalize_topic_key(topic_key)
|
||||||
|
if not normalized_key:
|
||||||
|
raise HTTPException(status_code=400, detail="topic_key is required")
|
||||||
|
if not TOPIC_KEY_RE.fullmatch(normalized_key):
|
||||||
|
raise HTTPException(status_code=400, detail="invalid topic_key")
|
||||||
|
exists = _topic_get_row(session, bot_id, normalized_key)
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Topic already exists: {normalized_key}")
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
row = TopicTopic(
|
||||||
|
bot_id=bot_id,
|
||||||
|
topic_key=normalized_key,
|
||||||
|
name=str(name or normalized_key).strip() or normalized_key,
|
||||||
|
description=str(description or "").strip(),
|
||||||
|
is_active=bool(is_active),
|
||||||
|
is_default_fallback=False,
|
||||||
|
routing_json=json.dumps(routing or {}, ensure_ascii=False),
|
||||||
|
view_schema_json=json.dumps(view_schema or {}, ensure_ascii=False),
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
)
|
||||||
|
session.add(row)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(row)
|
||||||
|
return _topic_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def update_topic(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
bot_id: str,
|
||||||
|
topic_key: str,
|
||||||
|
updates: Dict[str, Any],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
normalized_key = _normalize_topic_key(topic_key)
|
||||||
|
if not normalized_key:
|
||||||
|
raise HTTPException(status_code=400, detail="topic_key is required")
|
||||||
|
row = _topic_get_row(session, bot_id, normalized_key)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Topic not found")
|
||||||
|
|
||||||
|
if "name" in updates:
|
||||||
|
row.name = str(updates.get("name") or "").strip() or row.topic_key
|
||||||
|
if "description" in updates:
|
||||||
|
row.description = str(updates.get("description") or "").strip()
|
||||||
|
if "is_active" in updates:
|
||||||
|
row.is_active = bool(updates.get("is_active"))
|
||||||
|
if "routing" in updates:
|
||||||
|
row.routing_json = json.dumps(updates.get("routing") or {}, ensure_ascii=False)
|
||||||
|
if "view_schema" in updates:
|
||||||
|
row.view_schema_json = json.dumps(updates.get("view_schema") or {}, ensure_ascii=False)
|
||||||
|
row.is_default_fallback = False
|
||||||
|
row.updated_at = datetime.utcnow()
|
||||||
|
session.add(row)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(row)
|
||||||
|
return _topic_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_topic(session: Session, *, bot_id: str, topic_key: str) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
normalized_key = _normalize_topic_key(topic_key)
|
||||||
|
if not normalized_key:
|
||||||
|
raise HTTPException(status_code=400, detail="topic_key is required")
|
||||||
|
row = _topic_get_row(session, bot_id, normalized_key)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Topic not found")
|
||||||
|
items = session.exec(
|
||||||
|
select(TopicItem)
|
||||||
|
.where(TopicItem.bot_id == bot_id)
|
||||||
|
.where(TopicItem.topic_key == normalized_key)
|
||||||
|
).all()
|
||||||
|
for item in items:
|
||||||
|
session.delete(item)
|
||||||
|
session.delete(row)
|
||||||
|
session.commit()
|
||||||
|
return {"status": "deleted", "bot_id": bot_id, "topic_key": normalized_key}
|
||||||
|
|
||||||
|
|
||||||
|
def _count_topic_items(
|
||||||
|
session: Session,
|
||||||
|
bot_id: str,
|
||||||
|
topic_key: Optional[str] = None,
|
||||||
|
unread_only: bool = False,
|
||||||
|
) -> int:
|
||||||
|
stmt = select(TopicItem).where(TopicItem.bot_id == bot_id)
|
||||||
|
normalized_topic_key = _normalize_topic_key(topic_key or "")
|
||||||
|
if normalized_topic_key:
|
||||||
|
stmt = stmt.where(TopicItem.topic_key == normalized_topic_key)
|
||||||
|
rows = session.exec(stmt).all()
|
||||||
|
if unread_only:
|
||||||
|
return sum(1 for row in rows if not bool(row.is_read))
|
||||||
|
return len(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def list_topic_items(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
bot_id: str,
|
||||||
|
topic_key: Optional[str] = None,
|
||||||
|
cursor: Optional[int] = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
normalized_limit = max(1, min(int(limit or 50), 100))
|
||||||
|
stmt = select(TopicItem).where(TopicItem.bot_id == bot_id)
|
||||||
|
normalized_topic_key = _normalize_topic_key(topic_key or "")
|
||||||
|
if normalized_topic_key:
|
||||||
|
stmt = stmt.where(TopicItem.topic_key == normalized_topic_key)
|
||||||
|
if cursor is not None:
|
||||||
|
normalized_cursor = int(cursor)
|
||||||
|
if normalized_cursor > 0:
|
||||||
|
stmt = stmt.where(TopicItem.id < normalized_cursor)
|
||||||
|
rows = session.exec(stmt.order_by(TopicItem.id.desc()).limit(normalized_limit + 1)).all()
|
||||||
|
next_cursor: Optional[int] = None
|
||||||
|
if len(rows) > normalized_limit:
|
||||||
|
next_cursor = rows[-1].id
|
||||||
|
rows = rows[:normalized_limit]
|
||||||
|
return {
|
||||||
|
"bot_id": bot_id,
|
||||||
|
"topic_key": normalized_topic_key or None,
|
||||||
|
"items": [_topic_item_to_dict(row) for row in rows],
|
||||||
|
"next_cursor": next_cursor,
|
||||||
|
"unread_count": _count_topic_items(session, bot_id, normalized_topic_key, unread_only=True),
|
||||||
|
"total_unread_count": _count_topic_items(session, bot_id, unread_only=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_topic_item_stats(session: Session, *, bot_id: str) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
latest_item = session.exec(
|
||||||
|
select(TopicItem)
|
||||||
|
.where(TopicItem.bot_id == bot_id)
|
||||||
|
.order_by(TopicItem.id.desc())
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
return {
|
||||||
|
"bot_id": bot_id,
|
||||||
|
"total_count": _count_topic_items(session, bot_id),
|
||||||
|
"unread_count": _count_topic_items(session, bot_id, unread_only=True),
|
||||||
|
"latest_item_id": int(latest_item.id or 0) if latest_item and latest_item.id else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def mark_topic_item_read(session: Session, *, bot_id: str, item_id: int) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
row = session.exec(
|
||||||
|
select(TopicItem)
|
||||||
|
.where(TopicItem.bot_id == bot_id)
|
||||||
|
.where(TopicItem.id == item_id)
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Topic item not found")
|
||||||
|
if not bool(row.is_read):
|
||||||
|
row.is_read = True
|
||||||
|
session.add(row)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(row)
|
||||||
|
return {
|
||||||
|
"status": "updated",
|
||||||
|
"bot_id": bot_id,
|
||||||
|
"item": _topic_item_to_dict(row),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def delete_topic_item(session: Session, *, bot_id: str, item_id: int) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
row = session.exec(
|
||||||
|
select(TopicItem)
|
||||||
|
.where(TopicItem.bot_id == bot_id)
|
||||||
|
.where(TopicItem.id == item_id)
|
||||||
|
.limit(1)
|
||||||
|
).first()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Topic item not found")
|
||||||
|
payload = _topic_item_to_dict(row)
|
||||||
|
session.delete(row)
|
||||||
|
session.commit()
|
||||||
|
return {
|
||||||
|
"status": "deleted",
|
||||||
|
"bot_id": bot_id,
|
||||||
|
"item": payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def publish_topic_item(session: Session, bot_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
_get_bot_or_404(session, bot_id)
|
||||||
|
return _topic_publish_internal(session, bot_id, payload)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from fastapi import HTTPException, Request, UploadFile
|
||||||
from fastapi.responses import FileResponse, RedirectResponse, Response, StreamingResponse
|
from fastapi.responses import FileResponse, RedirectResponse, Response, StreamingResponse
|
||||||
|
|
||||||
from core.utils import _workspace_stat_ctime_iso
|
from core.utils import _workspace_stat_ctime_iso
|
||||||
from services.bot_storage_service import _workspace_root
|
from services.bot_storage_service import get_bot_workspace_root
|
||||||
from services.platform_service import get_platform_settings_snapshot
|
from services.platform_service import get_platform_settings_snapshot
|
||||||
|
|
||||||
TEXT_PREVIEW_EXTENSIONS = {
|
TEXT_PREVIEW_EXTENSIONS = {
|
||||||
|
|
@ -32,7 +32,7 @@ TEXT_PREVIEW_EXTENSIONS = {
|
||||||
MARKDOWN_EXTENSIONS = {".md", ".markdown"}
|
MARKDOWN_EXTENSIONS = {".md", ".markdown"}
|
||||||
|
|
||||||
def _resolve_workspace_path(bot_id: str, rel_path: Optional[str] = None) -> tuple[str, str]:
|
def _resolve_workspace_path(bot_id: str, rel_path: Optional[str] = None) -> tuple[str, str]:
|
||||||
root = _workspace_root(bot_id)
|
root = get_bot_workspace_root(bot_id)
|
||||||
rel = (rel_path or "").strip().replace("\\", "/")
|
rel = (rel_path or "").strip().replace("\\", "/")
|
||||||
target = os.path.abspath(os.path.join(root, rel))
|
target = os.path.abspath(os.path.join(root, rel))
|
||||||
if os.path.commonpath([root, target]) != root:
|
if os.path.commonpath([root, target]) != root:
|
||||||
|
|
@ -40,6 +40,10 @@ def _resolve_workspace_path(bot_id: str, rel_path: Optional[str] = None) -> tupl
|
||||||
return root, target
|
return root, target
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_workspace_path(bot_id: str, rel_path: Optional[str] = None) -> tuple[str, str]:
|
||||||
|
return _resolve_workspace_path(bot_id, rel_path)
|
||||||
|
|
||||||
|
|
||||||
def _write_text_atomic(target: str, content: str) -> None:
|
def _write_text_atomic(target: str, content: str) -> None:
|
||||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||||
tmp = f"{target}.tmp"
|
tmp = f"{target}.tmp"
|
||||||
|
|
@ -249,7 +253,7 @@ def get_workspace_tree_data(
|
||||||
path: Optional[str] = None,
|
path: Optional[str] = None,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
root = _workspace_root(bot_id)
|
root = get_bot_workspace_root(bot_id)
|
||||||
if not os.path.isdir(root):
|
if not os.path.isdir(root):
|
||||||
return {"bot_id": bot_id, "root": root, "cwd": "", "parent": None, "entries": []}
|
return {"bot_id": bot_id, "root": root, "cwd": "", "parent": None, "entries": []}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
|
|
||||||
### 2.6 前端禁止事项
|
### 2.6 前端禁止事项
|
||||||
|
|
||||||
- 禁止再次把页面做成“一个文件管状态、接口、弹层、列表、详情、搜索、分页”
|
- 禁止把页面做成“一个文件管状态、接口、弹层、列表、详情、搜索、分页”
|
||||||
- 禁止把样式、业务逻辑、视图结构三者重新耦合回单文件
|
- 禁止把样式、业务逻辑、视图结构三者重新耦合回单文件
|
||||||
- 禁止创建无明确职责的超通用组件
|
- 禁止创建无明确职责的超通用组件
|
||||||
- 禁止为减少行数而做不可读的过度抽象
|
- 禁止为减少行数而做不可读的过度抽象
|
||||||
|
|
@ -226,12 +226,6 @@ Router 不允许承担:
|
||||||
- 数据库表间拼装
|
- 数据库表间拼装
|
||||||
- 本地文件系统读写细节
|
- 本地文件系统读写细节
|
||||||
|
|
||||||
Router 文件体量规则:
|
|
||||||
|
|
||||||
- 目标:`< 300` 行
|
|
||||||
- 可接受上限:`400` 行
|
|
||||||
- 超过 `400` 行必须拆成子 router,并由装配层统一 `include_router`
|
|
||||||
|
|
||||||
### 3.4 Service 规范
|
### 3.4 Service 规范
|
||||||
|
|
||||||
Service 必须按业务域内聚组织,而不是为了压缩行数而机械切碎。
|
Service 必须按业务域内聚组织,而不是为了压缩行数而机械切碎。
|
||||||
|
|
|
||||||
|
|
@ -68,26 +68,18 @@ function AppShell() {
|
||||||
const showNavRail = route.kind !== 'bot' && !compactMode;
|
const showNavRail = route.kind !== 'bot' && !compactMode;
|
||||||
const showAppNavDrawerEntry = route.kind !== 'bot' && compactMode;
|
const showAppNavDrawerEntry = route.kind !== 'bot' && compactMode;
|
||||||
const showBotPanelDrawerEntry = route.kind === 'bot' && compactMode;
|
const showBotPanelDrawerEntry = route.kind === 'bot' && compactMode;
|
||||||
|
const appNavDrawerVisible = showAppNavDrawerEntry && appNavDrawerOpen;
|
||||||
|
const botPanelDrawerVisible = showBotPanelDrawerEntry && botPanelDrawerOpen;
|
||||||
|
const activeCompactPanelTab = showBotPanelDrawerEntry ? botCompactPanelTab : 'chat';
|
||||||
const useCompactSimpleHeader = showBotPanelDrawerEntry || showAppNavDrawerEntry;
|
const useCompactSimpleHeader = showBotPanelDrawerEntry || showAppNavDrawerEntry;
|
||||||
const headerTitle = showBotPanelDrawerEntry
|
const headerTitle = showBotPanelDrawerEntry
|
||||||
? (botCompactPanelTab === 'runtime' ? t.botPanels.runtime : t.botPanels.chat)
|
? (activeCompactPanelTab === 'runtime' ? t.botPanels.runtime : t.botPanels.chat)
|
||||||
: routeMeta.title;
|
: routeMeta.title;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${t.title} - ${route.kind === 'bot' ? botDocumentTitle : routeMeta.title}`;
|
document.title = `${t.title} - ${route.kind === 'bot' ? botDocumentTitle : routeMeta.title}`;
|
||||||
}, [botDocumentTitle, route.kind, routeMeta.title, t.title]);
|
}, [botDocumentTitle, route.kind, routeMeta.title, t.title]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showBotPanelDrawerEntry) {
|
|
||||||
setBotPanelDrawerOpen(false);
|
|
||||||
setBotCompactPanelTab('chat');
|
|
||||||
}
|
|
||||||
}, [forcedBotId, showBotPanelDrawerEntry]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showAppNavDrawerEntry) setAppNavDrawerOpen(false);
|
|
||||||
}, [route.kind, showAppNavDrawerEntry]);
|
|
||||||
|
|
||||||
const botPanelLabels = t.botPanels;
|
const botPanelLabels = t.botPanels;
|
||||||
const drawerBotName = String(forcedBot?.name || '').trim() || defaultLoadingTitle;
|
const drawerBotName = String(forcedBot?.name || '').trim() || defaultLoadingTitle;
|
||||||
const drawerBotId = String(forcedBotId || '').trim() || '-';
|
const drawerBotId = String(forcedBotId || '').trim() || '-';
|
||||||
|
|
@ -152,7 +144,7 @@ function AppShell() {
|
||||||
<LazyBotHomePage
|
<LazyBotHomePage
|
||||||
botId={forcedBotId}
|
botId={forcedBotId}
|
||||||
compactMode={compactMode}
|
compactMode={compactMode}
|
||||||
compactPanelTab={botCompactPanelTab}
|
compactPanelTab={activeCompactPanelTab}
|
||||||
onCompactPanelTabChange={setBotCompactPanelTab}
|
onCompactPanelTabChange={setBotCompactPanelTab}
|
||||||
/>
|
/>
|
||||||
</BotRouteAccessGate>
|
</BotRouteAccessGate>
|
||||||
|
|
@ -299,7 +291,7 @@ function AppShell() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showAppNavDrawerEntry && appNavDrawerOpen ? (
|
{appNavDrawerVisible ? (
|
||||||
<div className="app-bot-panel-drawer-mask" onClick={() => setAppNavDrawerOpen(false)}>
|
<div className="app-bot-panel-drawer-mask" onClick={() => setAppNavDrawerOpen(false)}>
|
||||||
<aside
|
<aside
|
||||||
className="app-bot-panel-drawer app-nav-drawer"
|
className="app-bot-panel-drawer app-nav-drawer"
|
||||||
|
|
@ -354,7 +346,7 @@ function AppShell() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showBotPanelDrawerEntry && botPanelDrawerOpen ? (
|
{botPanelDrawerVisible ? (
|
||||||
<div className="app-bot-panel-drawer-mask" onClick={() => setBotPanelDrawerOpen(false)}>
|
<div className="app-bot-panel-drawer-mask" onClick={() => setBotPanelDrawerOpen(false)}>
|
||||||
<aside
|
<aside
|
||||||
className="app-bot-panel-drawer"
|
className="app-bot-panel-drawer"
|
||||||
|
|
@ -380,26 +372,26 @@ function AppShell() {
|
||||||
<div className="app-bot-panel-drawer-list" role="tablist" aria-label={botPanelLabels.title}>
|
<div className="app-bot-panel-drawer-list" role="tablist" aria-label={botPanelLabels.title}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`app-bot-panel-drawer-item ${botCompactPanelTab === 'chat' ? 'is-active' : ''}`}
|
className={`app-bot-panel-drawer-item ${activeCompactPanelTab === 'chat' ? 'is-active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBotCompactPanelTab('chat');
|
setBotCompactPanelTab('chat');
|
||||||
setBotPanelDrawerOpen(false);
|
setBotPanelDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={botCompactPanelTab === 'chat'}
|
aria-selected={activeCompactPanelTab === 'chat'}
|
||||||
>
|
>
|
||||||
<MessageSquareText size={16} />
|
<MessageSquareText size={16} />
|
||||||
<span>{botPanelLabels.chat}</span>
|
<span>{botPanelLabels.chat}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`app-bot-panel-drawer-item ${botCompactPanelTab === 'runtime' ? 'is-active' : ''}`}
|
className={`app-bot-panel-drawer-item ${activeCompactPanelTab === 'runtime' ? 'is-active' : ''}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBotCompactPanelTab('runtime');
|
setBotCompactPanelTab('runtime');
|
||||||
setBotPanelDrawerOpen(false);
|
setBotPanelDrawerOpen(false);
|
||||||
}}
|
}}
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={botCompactPanelTab === 'runtime'}
|
aria-selected={activeCompactPanelTab === 'runtime'}
|
||||||
>
|
>
|
||||||
<Activity size={16} />
|
<Activity size={16} />
|
||||||
<span>{botPanelLabels.runtime}</span>
|
<span>{botPanelLabels.runtime}</span>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,6 @@ import { useAppStore } from '../store/appStore';
|
||||||
import type { BotState } from '../types/bot';
|
import type { BotState } from '../types/bot';
|
||||||
import {
|
import {
|
||||||
BOT_AUTH_INVALID_EVENT,
|
BOT_AUTH_INVALID_EVENT,
|
||||||
clearBotAccessPassword,
|
|
||||||
getBotAccessPassword,
|
|
||||||
setBotAccessPassword,
|
|
||||||
} from '../utils/botAccess';
|
} from '../utils/botAccess';
|
||||||
|
|
||||||
interface BotRouteAccessGateProps {
|
interface BotRouteAccessGateProps {
|
||||||
|
|
@ -133,32 +130,6 @@ export function BotRouteAccessGate({
|
||||||
return () => window.removeEventListener(BOT_AUTH_INVALID_EVENT, handleBotAuthInvalid as EventListener);
|
return () => window.removeEventListener(BOT_AUTH_INVALID_EVENT, handleBotAuthInvalid as EventListener);
|
||||||
}, [copy.errorExpired, normalizedBotId, passwordEnabled]);
|
}, [copy.errorExpired, normalizedBotId, passwordEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!normalizedBotId || !passwordEnabled || unlocked) return;
|
|
||||||
const stored = getBotAccessPassword(normalizedBotId);
|
|
||||||
if (!stored) return;
|
|
||||||
let alive = true;
|
|
||||||
const boot = async () => {
|
|
||||||
try {
|
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(normalizedBotId)}/auth/login`, { password: stored });
|
|
||||||
if (!alive) return;
|
|
||||||
setBotAccessPassword(normalizedBotId, stored);
|
|
||||||
setUnlocked(true);
|
|
||||||
setPassword('');
|
|
||||||
setPasswordError('');
|
|
||||||
await refreshBotDetail();
|
|
||||||
} catch {
|
|
||||||
clearBotAccessPassword(normalizedBotId);
|
|
||||||
if (!alive) return;
|
|
||||||
setPasswordError(copy.errorInvalid);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
void boot();
|
|
||||||
return () => {
|
|
||||||
alive = false;
|
|
||||||
};
|
|
||||||
}, [copy.errorInvalid, normalizedBotId, passwordEnabled, refreshBotDetail, unlocked]);
|
|
||||||
|
|
||||||
const unlockBot = async () => {
|
const unlockBot = async () => {
|
||||||
const entered = String(password || '').trim();
|
const entered = String(password || '').trim();
|
||||||
if (!entered || !normalizedBotId) {
|
if (!entered || !normalizedBotId) {
|
||||||
|
|
@ -168,13 +139,11 @@ export function BotRouteAccessGate({
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(normalizedBotId)}/auth/login`, { password: entered });
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(normalizedBotId)}/auth/login`, { password: entered });
|
||||||
setBotAccessPassword(normalizedBotId, entered);
|
|
||||||
setPasswordError('');
|
setPasswordError('');
|
||||||
setUnlocked(true);
|
setUnlocked(true);
|
||||||
setPassword('');
|
setPassword('');
|
||||||
await refreshBotDetail();
|
await refreshBotDetail();
|
||||||
} catch {
|
} catch {
|
||||||
clearBotAccessPassword(normalizedBotId);
|
|
||||||
setPasswordError(copy.errorInvalid);
|
setPasswordError(copy.errorInvalid);
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
@ -184,10 +153,13 @@ export function BotRouteAccessGate({
|
||||||
const shouldPromptPassword = Boolean(
|
const shouldPromptPassword = Boolean(
|
||||||
normalizedBotId && passwordEnabled && !authChecking && !unlocked,
|
normalizedBotId && passwordEnabled && !authChecking && !unlocked,
|
||||||
);
|
);
|
||||||
|
const canRenderChildren = normalizedBotId
|
||||||
|
? (!authChecking && (unlocked || !passwordEnabled))
|
||||||
|
: true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children}
|
{canRenderChildren ? children : null}
|
||||||
{shouldPromptPassword ? (
|
{shouldPromptPassword ? (
|
||||||
<div className="modal-mask app-modal-mask">
|
<div className="modal-mask app-modal-mask">
|
||||||
<div className="app-login-card" onClick={(event) => event.stopPropagation()}>
|
<div className="app-login-card" onClick={(event) => event.stopPropagation()}>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,6 @@ import { pickLocale } from '../i18n';
|
||||||
import { useAppStore } from '../store/appStore';
|
import { useAppStore } from '../store/appStore';
|
||||||
import {
|
import {
|
||||||
PANEL_AUTH_INVALID_EVENT,
|
PANEL_AUTH_INVALID_EVENT,
|
||||||
clearPanelAccessPassword,
|
|
||||||
getPanelAccessPassword,
|
|
||||||
setPanelAccessPassword,
|
|
||||||
} from '../utils/panelAccess';
|
} from '../utils/panelAccess';
|
||||||
|
|
||||||
interface PanelLoginGateProps {
|
interface PanelLoginGateProps {
|
||||||
|
|
@ -46,9 +43,10 @@ export function PanelLoginGate({
|
||||||
let alive = true;
|
let alive = true;
|
||||||
const boot = async () => {
|
const boot = async () => {
|
||||||
try {
|
try {
|
||||||
const status = await axios.get<{ enabled: boolean }>(`${APP_ENDPOINTS.apiBase}/panel/auth/status`);
|
const status = await axios.get<{ enabled?: boolean; authenticated?: boolean }>(`${APP_ENDPOINTS.apiBase}/panel/auth/status`);
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
const enabled = Boolean(status.data?.enabled);
|
const enabled = Boolean(status.data?.enabled);
|
||||||
|
const authenticated = Boolean(status.data?.authenticated);
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
setRequired(false);
|
setRequired(false);
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
|
|
@ -56,25 +54,15 @@ export function PanelLoginGate({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setRequired(true);
|
setRequired(true);
|
||||||
const stored = getPanelAccessPassword();
|
if (!authenticated) {
|
||||||
if (!stored) {
|
setAuthenticated(false);
|
||||||
setChecking(false);
|
setChecking(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/panel/auth/login`, { password: stored });
|
|
||||||
if (!alive) return;
|
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
} catch {
|
setChecking(false);
|
||||||
clearPanelAccessPassword();
|
|
||||||
if (!alive) return;
|
|
||||||
setError(isZh ? '面板访问密码错误,请重新输入。' : 'Invalid panel access password. Please try again.');
|
|
||||||
} finally {
|
|
||||||
if (alive) setChecking(false);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
clearPanelAccessPassword();
|
|
||||||
setRequired(true);
|
setRequired(true);
|
||||||
setAuthenticated(false);
|
setAuthenticated(false);
|
||||||
setError(
|
setError(
|
||||||
|
|
@ -118,12 +106,13 @@ export function PanelLoginGate({
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/panel/auth/login`, { password: next });
|
await axios.post(`${APP_ENDPOINTS.apiBase}/panel/auth/login`, { password: next });
|
||||||
setPanelAccessPassword(next);
|
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
clearPanelAccessPassword();
|
const resolvedError = (error && typeof error === 'object'
|
||||||
|
? error
|
||||||
|
: {}) as { response?: { data?: { detail?: string } } };
|
||||||
setError(
|
setError(
|
||||||
error?.response?.data?.detail
|
resolvedError.response?.data?.detail
|
||||||
|| (isZh ? '面板访问密码错误。' : 'Invalid panel access password.'),
|
|| (isZh ? '面板访问密码错误。' : 'Invalid panel access password.'),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,11 @@ export function DrawerShell({
|
||||||
bodyClassName,
|
bodyClassName,
|
||||||
}: DrawerShellProps) {
|
}: DrawerShellProps) {
|
||||||
const [mounted, setMounted] = useState(open);
|
const [mounted, setMounted] = useState(open);
|
||||||
const [visible, setVisible] = useState(open);
|
const visible = open;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return undefined;
|
if (!open) return undefined;
|
||||||
|
if (!mounted) return undefined;
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
|
|
@ -55,27 +56,27 @@ export function DrawerShell({
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [onClose, open]);
|
}, [mounted, onClose, open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setMounted(true);
|
if (mounted) return undefined;
|
||||||
const frameId = window.requestAnimationFrame(() => {
|
const frameId = window.requestAnimationFrame(() => {
|
||||||
setVisible(true);
|
setMounted(true);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
window.cancelAnimationFrame(frameId);
|
window.cancelAnimationFrame(frameId);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setVisible(false);
|
if (!mounted) return undefined;
|
||||||
const timerId = window.setTimeout(() => {
|
const timerId = window.setTimeout(() => {
|
||||||
setMounted(false);
|
setMounted(false);
|
||||||
}, DRAWER_ANIMATION_MS);
|
}, DRAWER_ANIMATION_MS);
|
||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timerId);
|
window.clearTimeout(timerId);
|
||||||
};
|
};
|
||||||
}, [open]);
|
}, [mounted, open]);
|
||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
.ops-searchbar {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-searchbar-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-autofill-trap {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-search-input {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-search-input-with-icon {
|
||||||
|
padding-right: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-search-inline-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--icon);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-search-inline-btn:hover {
|
||||||
|
border-color: color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
||||||
|
background: color-mix(in oklab, var(--brand-soft) 42%, var(--panel-soft) 58%);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { useEffect, useRef, useState, type FormEvent } from 'react';
|
import { useEffect, useRef, useState, type FormEvent } from 'react';
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import './ProtectedSearchInput.css';
|
||||||
|
|
||||||
interface ProtectedSearchInputProps {
|
interface ProtectedSearchInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
|
@ -47,16 +49,18 @@ export function ProtectedSearchInput({
|
||||||
const hasValue = currentValue.trim().length > 0;
|
const hasValue = currentValue.trim().length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debounceMs <= 0) return;
|
if (debounceMs <= 0) {
|
||||||
if (value === latestExternalValueRef.current) return;
|
|
||||||
latestExternalValueRef.current = value;
|
latestExternalValueRef.current = value;
|
||||||
setDraftValue(value);
|
return undefined;
|
||||||
}, [debounceMs, value]);
|
}
|
||||||
|
if (value === latestExternalValueRef.current) return undefined;
|
||||||
useEffect(() => {
|
|
||||||
if (debounceMs > 0) return;
|
|
||||||
setDraftValue(value);
|
|
||||||
latestExternalValueRef.current = value;
|
latestExternalValueRef.current = value;
|
||||||
|
const frameId = window.requestAnimationFrame(() => {
|
||||||
|
setDraftValue(value);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frameId);
|
||||||
|
};
|
||||||
}, [debounceMs, value]);
|
}, [debounceMs, value]);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,52 @@ import { botsSyncZhCn } from '../i18n/bots-sync.zh-cn';
|
||||||
import { botsSyncEn } from '../i18n/bots-sync.en';
|
import { botsSyncEn } from '../i18n/bots-sync.en';
|
||||||
import { buildMonitorWsUrl, notifyBotAuthInvalid } from '../utils/botAccess';
|
import { buildMonitorWsUrl, notifyBotAuthInvalid } from '../utils/botAccess';
|
||||||
|
|
||||||
|
interface BotMessageRow {
|
||||||
|
id?: unknown;
|
||||||
|
role?: unknown;
|
||||||
|
text?: unknown;
|
||||||
|
media?: unknown;
|
||||||
|
ts?: unknown;
|
||||||
|
feedback?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BotMessagesPageResponse {
|
||||||
|
items?: BotMessageRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestErrorShape {
|
||||||
|
response?: {
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonitorEventPayload {
|
||||||
|
state?: unknown;
|
||||||
|
action_msg?: unknown;
|
||||||
|
msg?: unknown;
|
||||||
|
text?: unknown;
|
||||||
|
content?: unknown;
|
||||||
|
media?: unknown;
|
||||||
|
message_id?: unknown;
|
||||||
|
command?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MonitorWsMessage {
|
||||||
|
type?: unknown;
|
||||||
|
channel?: unknown;
|
||||||
|
source?: unknown;
|
||||||
|
payload?: MonitorEventPayload | null;
|
||||||
|
state?: unknown;
|
||||||
|
action_msg?: unknown;
|
||||||
|
msg?: unknown;
|
||||||
|
text?: unknown;
|
||||||
|
media?: unknown;
|
||||||
|
message_id?: unknown;
|
||||||
|
content?: unknown;
|
||||||
|
is_progress?: unknown;
|
||||||
|
is_tool?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeState(v: string): 'THINKING' | 'TOOL_CALL' | 'SUCCESS' | 'ERROR' | 'INFO' {
|
function normalizeState(v: string): 'THINKING' | 'TOOL_CALL' | 'SUCCESS' | 'ERROR' | 'INFO' {
|
||||||
const s = (v || '').toUpperCase();
|
const s = (v || '').toUpperCase();
|
||||||
if (s === 'THINKING' || s === 'TOOL_CALL' || s === 'SUCCESS' || s === 'ERROR') return s;
|
if (s === 'THINKING' || s === 'TOOL_CALL' || s === 'SUCCESS' || s === 'ERROR') return s;
|
||||||
|
|
@ -98,7 +144,7 @@ export function useBotsSync(forcedBotId?: string) {
|
||||||
const target = String(botId || '').trim();
|
const target = String(botId || '').trim();
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<{ items?: any[] }>(`${APP_ENDPOINTS.apiBase}/bots/${target}/messages/page`);
|
const res = await axios.get<BotMessagesPageResponse>(`${APP_ENDPOINTS.apiBase}/bots/${target}/messages/page`);
|
||||||
const rows = Array.isArray(res.data?.items) ? res.data.items : [];
|
const rows = Array.isArray(res.data?.items) ? res.data.items : [];
|
||||||
const latestPage: ChatMessage[] = rows
|
const latestPage: ChatMessage[] = rows
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
|
|
@ -152,8 +198,9 @@ export function useBotsSync(forcedBotId?: string) {
|
||||||
}
|
}
|
||||||
const res = await axios.get<BotState[]>(`${APP_ENDPOINTS.apiBase}/bots`);
|
const res = await axios.get<BotState[]>(`${APP_ENDPOINTS.apiBase}/bots`);
|
||||||
setBots(res.data);
|
setBots(res.data);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
const status = Number(error?.response?.status || 0);
|
const resolvedError = (error && typeof error === 'object' ? error : {}) as RequestErrorShape;
|
||||||
|
const status = Number(resolvedError.response?.status || 0);
|
||||||
if (forced && status === 401) {
|
if (forced && status === 401) {
|
||||||
setBots([]);
|
setBots([]);
|
||||||
return;
|
return;
|
||||||
|
|
@ -240,9 +287,9 @@ export function useBotsSync(forcedBotId?: string) {
|
||||||
void syncBotMessages(bot.id);
|
void syncBotMessages(bot.id);
|
||||||
};
|
};
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
let data: any;
|
let data: MonitorWsMessage;
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(event.data);
|
data = JSON.parse(event.data) as MonitorWsMessage;
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +421,7 @@ export function useBotsSync(forcedBotId?: string) {
|
||||||
return () => {
|
return () => {
|
||||||
// no-op: clean in unmount effect below
|
// no-op: clean in unmount effect below
|
||||||
};
|
};
|
||||||
}, [activeBots, addBotEvent, addBotLog, addBotMessage, isZh, syncBotMessages, t.progress, t.replied, t.stateUpdated, updateBotState]);
|
}, [activeBots, addBotEvent, addBotLog, addBotMessage, forced, isZh, syncBotMessages, t.progress, t.replied, t.stateUpdated, updateBotState]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
import { BotDashboardView } from './components/BotDashboardView';
|
import { BotDashboardView } from './components/BotDashboardView';
|
||||||
import { useBotDashboardModule } from './hooks/useBotDashboardModule';
|
import { useBotDashboardModule } from './hooks/useBotDashboardModule';
|
||||||
import type { BotDashboardModuleProps } from './types';
|
import type { BotDashboardModuleProps } from './types';
|
||||||
import {
|
import { useBotDashboardViewProps } from './useBotDashboardViewProps';
|
||||||
formatBytes,
|
|
||||||
formatWorkspaceTime,
|
|
||||||
} from './utils';
|
|
||||||
import './BotDashboardModule.css';
|
import './BotDashboardModule.css';
|
||||||
import './components/DashboardShared.css';
|
import './components/DashboardShared.css';
|
||||||
|
|
||||||
|
|
@ -21,487 +18,11 @@ export function BotDashboardModule({
|
||||||
compactPanelTab: compactPanelTabProp,
|
compactPanelTab: compactPanelTabProp,
|
||||||
onCompactPanelTabChange,
|
onCompactPanelTabChange,
|
||||||
});
|
});
|
||||||
const runtimeMoreLabel = dashboard.isZh ? '更多' : 'More';
|
|
||||||
|
|
||||||
const botListPanelProps = {
|
const viewProps = useBotDashboardViewProps({
|
||||||
bots: dashboard.bots,
|
dashboard,
|
||||||
filteredBots: dashboard.filteredBots,
|
|
||||||
pagedBots: dashboard.pagedBots,
|
|
||||||
selectedBotId: dashboard.selectedBotId,
|
|
||||||
normalizedBotListQuery: dashboard.normalizedBotListQuery,
|
|
||||||
botListQuery: dashboard.botListQuery,
|
|
||||||
botListPageSizeReady: dashboard.botListPageSizeReady,
|
|
||||||
botListPage: dashboard.botListPage,
|
|
||||||
botListTotalPages: dashboard.botListTotalPages,
|
|
||||||
botListMenuOpen: dashboard.botListMenuOpen,
|
|
||||||
controlStateByBot: dashboard.controlStateByBot,
|
|
||||||
operatingBotId: dashboard.operatingBotId,
|
|
||||||
compactMode: dashboard.compactMode,
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
isLoadingTemplates: dashboard.isLoadingTemplates,
|
|
||||||
isBatchOperating: dashboard.isBatchOperating,
|
|
||||||
labels: {
|
|
||||||
batchStart: dashboard.t.batchStart,
|
|
||||||
batchStop: dashboard.t.batchStop,
|
|
||||||
botSearchNoResult: dashboard.t.botSearchNoResult,
|
|
||||||
botSearchPlaceholder: dashboard.t.botSearchPlaceholder,
|
|
||||||
clearSearch: dashboard.t.clearSearch,
|
|
||||||
delete: dashboard.t.delete,
|
|
||||||
disable: dashboard.t.disable,
|
|
||||||
disabled: dashboard.t.disabled,
|
|
||||||
enable: dashboard.t.enable,
|
|
||||||
extensions: dashboard.t.extensions,
|
|
||||||
image: dashboard.t.image,
|
|
||||||
manageImages: dashboard.t.manageImages,
|
|
||||||
newBot: dashboard.t.newBot,
|
|
||||||
paginationNext: dashboard.t.paginationNext,
|
|
||||||
paginationPage: dashboard.t.paginationPage,
|
|
||||||
paginationPrev: dashboard.t.paginationPrev,
|
|
||||||
searchAction: dashboard.t.searchAction,
|
|
||||||
start: dashboard.t.start,
|
|
||||||
stop: dashboard.t.stop,
|
|
||||||
syncingPageSize: dashboard.t.syncingPageSize,
|
|
||||||
templateManager: dashboard.t.templateManager,
|
|
||||||
titleBots: dashboard.t.titleBots,
|
|
||||||
},
|
|
||||||
botSearchInputName: dashboard.botSearchInputName,
|
|
||||||
botListMenuRef: dashboard.botListMenuRef,
|
|
||||||
onOpenCreateWizard: () => dashboard.setShowCreateBotModal(true),
|
|
||||||
onOpenImageFactory,
|
onOpenImageFactory,
|
||||||
onToggleMenu: () => dashboard.setBotListMenuOpen((prev) => !prev),
|
});
|
||||||
onCloseMenu: () => dashboard.setBotListMenuOpen(false),
|
|
||||||
onOpenTemplateManager: dashboard.openTemplateManager,
|
|
||||||
onBatchStartBots: dashboard.batchStartBots,
|
|
||||||
onBatchStopBots: dashboard.batchStopBots,
|
|
||||||
onBotListQueryChange: dashboard.setBotListQuery,
|
|
||||||
onBotListPageChange: dashboard.setBotListPage,
|
|
||||||
onSelectBot: dashboard.setSelectedBotId,
|
|
||||||
onSetCompactPanelTab: (tab: 'chat' | 'runtime') => dashboard.setCompactPanelTab(tab),
|
|
||||||
onSetBotEnabled: dashboard.setBotEnabled,
|
|
||||||
onStartBot: dashboard.startBot,
|
|
||||||
onStopBot: dashboard.stopBot,
|
|
||||||
onOpenResourceMonitor: dashboard.openResourceMonitor,
|
|
||||||
onRemoveBot: dashboard.removeBot,
|
|
||||||
};
|
|
||||||
|
|
||||||
const topicFeedPanelProps = {
|
return <BotDashboardView {...viewProps} />;
|
||||||
isZh: dashboard.isZh,
|
|
||||||
topicKey: dashboard.topicFeedTopicKey,
|
|
||||||
topicOptions: dashboard.activeTopicOptions,
|
|
||||||
topicState: dashboard.topicPanelState,
|
|
||||||
items: dashboard.topicFeedItems,
|
|
||||||
loading: dashboard.topicFeedLoading,
|
|
||||||
loadingMore: dashboard.topicFeedLoadingMore,
|
|
||||||
nextCursor: dashboard.topicFeedNextCursor,
|
|
||||||
error: dashboard.topicFeedError,
|
|
||||||
readSavingById: dashboard.topicFeedReadSavingById,
|
|
||||||
deleteSavingById: dashboard.topicFeedDeleteSavingById,
|
|
||||||
onTopicChange: dashboard.setTopicFeedTopicKey,
|
|
||||||
onRefresh: () => void dashboard.loadTopicFeed({ append: false, topicKey: dashboard.topicFeedTopicKey }),
|
|
||||||
onMarkRead: (itemId: number) => void dashboard.markTopicFeedItemRead(itemId),
|
|
||||||
onDeleteItem: (item: (typeof dashboard.topicFeedItems)[number]) => void dashboard.deleteTopicFeedItem(item),
|
|
||||||
onLoadMore: () => void dashboard.loadTopicFeed({ append: true, cursor: dashboard.topicFeedNextCursor, topicKey: dashboard.topicFeedTopicKey }),
|
|
||||||
onOpenWorkspacePath: (path: string) => void dashboard.openWorkspacePathFromChat(path),
|
|
||||||
resolveWorkspaceMediaSrc: dashboard.resolveWorkspaceMediaSrc,
|
|
||||||
onOpenTopicSettings: dashboard.openTopicConfigModal,
|
|
||||||
onDetailOpenChange: dashboard.setTopicDetailOpen,
|
|
||||||
layout: 'panel' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dashboardChatPanelProps = {
|
|
||||||
conversation: dashboard.conversation,
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
labels: {
|
|
||||||
badReply: dashboard.t.badReply,
|
|
||||||
botDisabledHint: dashboard.t.botDisabledHint,
|
|
||||||
botStarting: dashboard.t.botStarting,
|
|
||||||
botStopping: dashboard.t.botStopping,
|
|
||||||
chatDisabled: dashboard.t.chatDisabled,
|
|
||||||
controlCommandsHide: dashboard.t.controlCommandsHide,
|
|
||||||
controlCommandsShow: dashboard.t.controlCommandsShow,
|
|
||||||
copyPrompt: dashboard.t.copyPrompt,
|
|
||||||
copyReply: dashboard.t.copyReply,
|
|
||||||
deleteMessage: dashboard.t.deleteMessage,
|
|
||||||
disabledPlaceholder: dashboard.t.disabledPlaceholder,
|
|
||||||
download: dashboard.t.download,
|
|
||||||
editPrompt: dashboard.t.editPrompt,
|
|
||||||
fileNotPreviewable: dashboard.t.fileNotPreviewable,
|
|
||||||
goodReply: dashboard.t.goodReply,
|
|
||||||
inputPlaceholder: dashboard.t.inputPlaceholder,
|
|
||||||
interrupt: dashboard.t.interrupt,
|
|
||||||
noConversation: dashboard.t.noConversation,
|
|
||||||
previewTitle: dashboard.t.previewTitle,
|
|
||||||
stagedSubmissionAttachmentCount: dashboard.t.stagedSubmissionAttachmentCount,
|
|
||||||
stagedSubmissionEmpty: dashboard.t.stagedSubmissionEmpty,
|
|
||||||
stagedSubmissionRestore: dashboard.t.stagedSubmissionRestore,
|
|
||||||
stagedSubmissionRemove: dashboard.t.stagedSubmissionRemove,
|
|
||||||
quoteReply: dashboard.t.quoteReply,
|
|
||||||
quotedReplyLabel: dashboard.t.quotedReplyLabel,
|
|
||||||
send: dashboard.t.send,
|
|
||||||
thinking: dashboard.t.thinking,
|
|
||||||
uploadFile: dashboard.t.uploadFile,
|
|
||||||
uploadingFile: dashboard.t.uploadingFile,
|
|
||||||
user: dashboard.t.user,
|
|
||||||
voiceStart: dashboard.t.voiceStart,
|
|
||||||
voiceStop: dashboard.t.voiceStop,
|
|
||||||
voiceTranscribing: dashboard.t.voiceTranscribing,
|
|
||||||
you: dashboard.t.you,
|
|
||||||
},
|
|
||||||
chatScrollRef: dashboard.chatScrollRef,
|
|
||||||
onChatScroll: dashboard.onChatScroll,
|
|
||||||
expandedProgressByKey: dashboard.expandedProgressByKey,
|
|
||||||
expandedUserByKey: dashboard.expandedUserByKey,
|
|
||||||
deletingMessageIdMap: dashboard.deletingMessageIdMap,
|
|
||||||
feedbackSavingByMessageId: dashboard.feedbackSavingByMessageId,
|
|
||||||
markdownComponents: dashboard.markdownComponents,
|
|
||||||
workspaceDownloadExtensionSet: dashboard.workspaceDownloadExtensionSet,
|
|
||||||
onToggleProgressExpand: dashboard.toggleProgressExpanded,
|
|
||||||
onToggleUserExpand: dashboard.toggleUserExpanded,
|
|
||||||
onEditUserPrompt: dashboard.editUserPrompt,
|
|
||||||
onCopyUserPrompt: dashboard.copyUserPrompt,
|
|
||||||
onDeleteConversationMessage: dashboard.deleteConversationMessage,
|
|
||||||
onOpenWorkspacePath: dashboard.openWorkspacePathFromChat,
|
|
||||||
onSubmitAssistantFeedback: dashboard.submitAssistantFeedback,
|
|
||||||
onQuoteAssistantReply: dashboard.quoteAssistantReply,
|
|
||||||
onCopyAssistantReply: dashboard.copyAssistantReply,
|
|
||||||
isThinking: dashboard.isThinking,
|
|
||||||
canChat: dashboard.canChat,
|
|
||||||
isChatEnabled: dashboard.isChatEnabled,
|
|
||||||
speechEnabled: dashboard.speechEnabled,
|
|
||||||
selectedBotEnabled: dashboard.selectedBotEnabled,
|
|
||||||
selectedBotControlState: dashboard.selectedBotControlState,
|
|
||||||
quotedReply: dashboard.quotedReply,
|
|
||||||
onClearQuotedReply: () => dashboard.setQuotedReply(null),
|
|
||||||
stagedSubmissions: dashboard.selectedBotStagedSubmissions,
|
|
||||||
onRestoreStagedSubmission: dashboard.restoreStagedSubmission,
|
|
||||||
onRemoveStagedSubmission: dashboard.removeStagedSubmission,
|
|
||||||
pendingAttachments: dashboard.pendingAttachments,
|
|
||||||
onRemovePendingAttachment: (path: string) =>
|
|
||||||
dashboard.setPendingAttachments((prev) => prev.filter((value) => value !== path)),
|
|
||||||
attachmentUploadPercent: dashboard.attachmentUploadPercent,
|
|
||||||
isUploadingAttachments: dashboard.isUploadingAttachments,
|
|
||||||
filePickerRef: dashboard.filePickerRef,
|
|
||||||
allowedAttachmentExtensions: dashboard.allowedAttachmentExtensions,
|
|
||||||
onPickAttachments: dashboard.onPickAttachments,
|
|
||||||
controlCommandPanelOpen: dashboard.controlCommandPanelOpen,
|
|
||||||
controlCommandPanelRef: dashboard.controlCommandPanelRef,
|
|
||||||
onToggleControlCommandPanel: () => {
|
|
||||||
dashboard.setChatDatePickerOpen(false);
|
|
||||||
dashboard.setControlCommandPanelOpen((prev) => !prev);
|
|
||||||
},
|
|
||||||
activeControlCommand: dashboard.activeControlCommand,
|
|
||||||
canSendControlCommand: dashboard.canSendControlCommand,
|
|
||||||
isInterrupting: dashboard.isInterrupting,
|
|
||||||
onSendControlCommand: dashboard.sendControlCommand,
|
|
||||||
onInterruptExecution: dashboard.interruptExecution,
|
|
||||||
chatDateTriggerRef: dashboard.chatDateTriggerRef,
|
|
||||||
hasSelectedBot: Boolean(dashboard.selectedBotId),
|
|
||||||
chatDateJumping: dashboard.chatDateJumping,
|
|
||||||
onToggleChatDatePicker: dashboard.toggleChatDatePicker,
|
|
||||||
chatDatePickerOpen: dashboard.chatDatePickerOpen,
|
|
||||||
chatDatePanelPosition: dashboard.chatDatePanelPosition,
|
|
||||||
chatDateValue: dashboard.chatDateValue,
|
|
||||||
onChatDateValueChange: dashboard.setChatDateValue,
|
|
||||||
onCloseChatDatePicker: () => dashboard.setChatDatePickerOpen(false),
|
|
||||||
onJumpConversationToDate: dashboard.jumpConversationToDate,
|
|
||||||
command: dashboard.command,
|
|
||||||
onCommandChange: dashboard.setCommand,
|
|
||||||
composerTextareaRef: dashboard.composerTextareaRef,
|
|
||||||
onComposerKeyDown: dashboard.onComposerKeyDown,
|
|
||||||
isVoiceRecording: dashboard.isVoiceRecording,
|
|
||||||
isVoiceTranscribing: dashboard.isVoiceTranscribing,
|
|
||||||
isCompactMobile: dashboard.isCompactMobile,
|
|
||||||
voiceCountdown: dashboard.voiceCountdown,
|
|
||||||
onVoiceInput: dashboard.onVoiceInput,
|
|
||||||
onTriggerPickAttachments: dashboard.triggerPickAttachments,
|
|
||||||
submitActionMode: dashboard.submitActionMode,
|
|
||||||
onSubmitAction: dashboard.handlePrimarySubmitAction,
|
|
||||||
};
|
|
||||||
|
|
||||||
const runtimePanelProps = {
|
|
||||||
selectedBot: dashboard.selectedBot,
|
|
||||||
selectedBotEnabled: dashboard.selectedBotEnabled,
|
|
||||||
operatingBotId: dashboard.operatingBotId,
|
|
||||||
runtimeMenuOpen: dashboard.runtimeMenuOpen,
|
|
||||||
runtimeMenuRef: dashboard.runtimeMenuRef,
|
|
||||||
displayState: dashboard.displayState,
|
|
||||||
workspaceError: dashboard.workspaceError,
|
|
||||||
workspacePathDisplay: dashboard.workspacePathDisplay,
|
|
||||||
workspaceLoading: dashboard.workspaceLoading,
|
|
||||||
workspaceQuery: dashboard.workspaceQuery,
|
|
||||||
workspaceSearchInputName: dashboard.workspaceSearchInputName,
|
|
||||||
workspaceSearchLoading: dashboard.workspaceSearchLoading,
|
|
||||||
filteredWorkspaceEntries: dashboard.filteredWorkspaceEntries,
|
|
||||||
workspaceParentPath: dashboard.workspaceParentPath,
|
|
||||||
workspaceFileLoading: dashboard.workspaceFileLoading,
|
|
||||||
workspaceDownloadExtensionSet: dashboard.workspaceDownloadExtensionSet,
|
|
||||||
workspaceAutoRefresh: dashboard.workspaceAutoRefresh,
|
|
||||||
isCompactHidden: dashboard.compactMode && (dashboard.isCompactListPage || dashboard.compactPanelTab !== 'runtime'),
|
|
||||||
showCompactSurface: dashboard.showCompactBotPageClose,
|
|
||||||
emptyStateText: dashboard.forcedBotMissing ? `${dashboard.t.noTelemetry}: ${String(dashboard.forcedBotId).trim()}` : dashboard.t.noTelemetry,
|
|
||||||
labels: {
|
|
||||||
agent: dashboard.t.agent,
|
|
||||||
autoRefresh: dashboard.lc.autoRefresh,
|
|
||||||
base: dashboard.t.base,
|
|
||||||
channels: dashboard.t.channels,
|
|
||||||
clearHistory: dashboard.t.clearHistory,
|
|
||||||
clearSearch: dashboard.t.clearSearch,
|
|
||||||
cronViewer: dashboard.t.cronViewer,
|
|
||||||
download: dashboard.t.download,
|
|
||||||
emptyDir: dashboard.t.emptyDir,
|
|
||||||
envParams: dashboard.t.envParams,
|
|
||||||
exportHistory: dashboard.t.exportHistory,
|
|
||||||
fileNotPreviewable: dashboard.t.fileNotPreviewable,
|
|
||||||
folder: dashboard.t.folder,
|
|
||||||
goUp: dashboard.t.goUp,
|
|
||||||
goUpTitle: dashboard.t.goUpTitle,
|
|
||||||
loadingDir: dashboard.t.loadingDir,
|
|
||||||
mcp: dashboard.t.mcp,
|
|
||||||
more: runtimeMoreLabel,
|
|
||||||
noPreviewFile: dashboard.t.noPreviewFile,
|
|
||||||
openingPreview: dashboard.t.openingPreview,
|
|
||||||
openFolderTitle: dashboard.t.openFolderTitle,
|
|
||||||
params: dashboard.t.params,
|
|
||||||
previewTitle: dashboard.t.previewTitle,
|
|
||||||
refreshHint: dashboard.lc.refreshHint,
|
|
||||||
restart: dashboard.t.restart,
|
|
||||||
runtime: dashboard.t.runtime,
|
|
||||||
searchAction: dashboard.t.searchAction,
|
|
||||||
skills: dashboard.t.skills,
|
|
||||||
topic: dashboard.t.topic,
|
|
||||||
workspaceHint: dashboard.t.workspaceHint,
|
|
||||||
workspaceOutputs: dashboard.t.workspaceOutputs,
|
|
||||||
workspaceSearchNoResult: dashboard.t.workspaceSearchNoResult,
|
|
||||||
workspaceSearchPlaceholder: dashboard.t.workspaceSearchPlaceholder,
|
|
||||||
},
|
|
||||||
onRestartBot: dashboard.restartBot,
|
|
||||||
onToggleRuntimeMenu: () => dashboard.setRuntimeMenuOpen((prev) => !prev),
|
|
||||||
onOpenBaseConfig: dashboard.openBaseConfigModal,
|
|
||||||
onOpenParamConfig: dashboard.openParamConfigModal,
|
|
||||||
onOpenChannelConfig: dashboard.openChannelConfigModal,
|
|
||||||
onOpenTopicConfig: dashboard.openTopicConfigModal,
|
|
||||||
onOpenEnvParams: dashboard.openEnvParamsConfigModal,
|
|
||||||
onOpenSkills: dashboard.openSkillsConfigModal,
|
|
||||||
onOpenMcpConfig: dashboard.openMcpConfigModal,
|
|
||||||
onOpenCronJobs: dashboard.openCronJobsModal,
|
|
||||||
onOpenAgentFiles: dashboard.openAgentFilesModal,
|
|
||||||
onExportHistory: () => {
|
|
||||||
dashboard.setRuntimeMenuOpen(false);
|
|
||||||
dashboard.exportConversationJson();
|
|
||||||
},
|
|
||||||
onClearHistory: async () => {
|
|
||||||
dashboard.setRuntimeMenuOpen(false);
|
|
||||||
await dashboard.clearConversationHistory();
|
|
||||||
},
|
|
||||||
onRefreshWorkspace: () => dashboard.selectedBot ? dashboard.loadWorkspaceTree(dashboard.selectedBot.id, dashboard.workspaceCurrentPath) : undefined,
|
|
||||||
onWorkspaceQueryChange: dashboard.setWorkspaceQuery,
|
|
||||||
onWorkspaceQueryClear: () => dashboard.setWorkspaceQuery(''),
|
|
||||||
onWorkspaceQuerySearch: () => dashboard.setWorkspaceQuery((value) => value.trim()),
|
|
||||||
onToggleWorkspaceAutoRefresh: () => dashboard.setWorkspaceAutoRefresh((prev) => !prev),
|
|
||||||
onLoadWorkspaceTree: dashboard.loadWorkspaceTree,
|
|
||||||
onOpenWorkspaceFilePreview: dashboard.openWorkspaceFilePreview,
|
|
||||||
onShowWorkspaceHoverCard: dashboard.showWorkspaceHoverCard,
|
|
||||||
onHideWorkspaceHoverCard: dashboard.hideWorkspaceHoverCard,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dashboardModalStackProps = {
|
|
||||||
resourceMonitorModal: {
|
|
||||||
open: dashboard.showResourceModal,
|
|
||||||
botId: dashboard.resourceBotId,
|
|
||||||
resourceBot: dashboard.resourceBot,
|
|
||||||
resourceSnapshot: dashboard.resourceSnapshot,
|
|
||||||
resourceLoading: dashboard.resourceLoading,
|
|
||||||
resourceError: dashboard.resourceError,
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
closeLabel: dashboard.t.close,
|
|
||||||
onClose: () => dashboard.setShowResourceModal(false),
|
|
||||||
onRefresh: dashboard.loadResourceSnapshot,
|
|
||||||
},
|
|
||||||
baseConfigModal: {
|
|
||||||
open: dashboard.showBaseModal,
|
|
||||||
selectedBotId: dashboard.selectedBot?.id || '',
|
|
||||||
editForm: dashboard.editForm,
|
|
||||||
paramDraft: dashboard.paramDraft,
|
|
||||||
baseImageOptions: dashboard.baseImageOptions,
|
|
||||||
systemTimezoneOptions: dashboard.systemTimezoneOptions,
|
|
||||||
defaultSystemTimezone: dashboard.defaultSystemTimezone,
|
|
||||||
passwordToggleLabels: dashboard.passwordToggleLabels,
|
|
||||||
isSaving: dashboard.isSaving,
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
labels: {
|
|
||||||
accessPassword: dashboard.t.accessPassword,
|
|
||||||
accessPasswordPlaceholder: dashboard.t.accessPasswordPlaceholder,
|
|
||||||
baseConfig: dashboard.t.baseConfig,
|
|
||||||
baseImageReadonly: dashboard.t.baseImageReadonly,
|
|
||||||
botIdReadonly: dashboard.t.botIdReadonly,
|
|
||||||
botName: dashboard.t.botName,
|
|
||||||
botNamePlaceholder: dashboard.t.botNamePlaceholder,
|
|
||||||
cancel: dashboard.t.cancel,
|
|
||||||
close: dashboard.t.close,
|
|
||||||
save: dashboard.t.save,
|
|
||||||
},
|
|
||||||
onClose: () => dashboard.setShowBaseModal(false),
|
|
||||||
onEditFormChange: dashboard.updateEditForm,
|
|
||||||
onParamDraftChange: dashboard.updateParamDraft,
|
|
||||||
onSave: () => dashboard.saveBot('base'),
|
|
||||||
},
|
|
||||||
paramConfigModal: {
|
|
||||||
open: dashboard.showParamModal,
|
|
||||||
editForm: dashboard.editForm,
|
|
||||||
paramDraft: dashboard.paramDraft,
|
|
||||||
passwordToggleLabels: dashboard.passwordToggleLabels,
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
isTestingProvider: dashboard.isTestingProvider,
|
|
||||||
providerTestResult: dashboard.providerTestResult,
|
|
||||||
isSaving: dashboard.isSaving,
|
|
||||||
labels: {
|
|
||||||
cancel: dashboard.t.cancel,
|
|
||||||
close: dashboard.t.close,
|
|
||||||
modelName: dashboard.t.modelName,
|
|
||||||
modelNamePlaceholder: dashboard.t.modelNamePlaceholder,
|
|
||||||
modelParams: dashboard.t.modelParams,
|
|
||||||
newApiKey: dashboard.t.newApiKey,
|
|
||||||
newApiKeyPlaceholder: dashboard.t.newApiKeyPlaceholder,
|
|
||||||
saveParams: dashboard.t.saveParams,
|
|
||||||
testModelConnection: dashboard.t.testModelConnection,
|
|
||||||
testing: dashboard.t.testing,
|
|
||||||
},
|
|
||||||
onClose: () => dashboard.setShowParamModal(false),
|
|
||||||
onEditFormChange: dashboard.updateEditForm,
|
|
||||||
onParamDraftChange: dashboard.updateParamDraft,
|
|
||||||
onProviderChange: dashboard.onBaseProviderChange,
|
|
||||||
onTestProviderConnection: dashboard.testProviderConnection,
|
|
||||||
onSave: () => dashboard.saveBot('params'),
|
|
||||||
},
|
|
||||||
channelConfigModal: dashboard.channelConfigModalProps,
|
|
||||||
topicConfigModal: dashboard.topicConfigModalProps,
|
|
||||||
skillsModal: dashboard.skillsModalProps,
|
|
||||||
skillMarketInstallModal: dashboard.skillMarketInstallModalProps,
|
|
||||||
mcpConfigModal: dashboard.mcpConfigModalProps,
|
|
||||||
envParamsModal: dashboard.envParamsModalProps,
|
|
||||||
cronJobsModal: dashboard.cronJobsModalProps,
|
|
||||||
templateManagerModal: {
|
|
||||||
open: dashboard.showTemplateModal,
|
|
||||||
templateTab: dashboard.templateTab,
|
|
||||||
templateAgentCount: dashboard.templateAgentCount,
|
|
||||||
templateTopicCount: dashboard.templateTopicCount,
|
|
||||||
templateAgentText: dashboard.templateAgentText,
|
|
||||||
templateTopicText: dashboard.templateTopicText,
|
|
||||||
isSavingTemplates: dashboard.isSavingTemplates,
|
|
||||||
labels: {
|
|
||||||
cancel: dashboard.t.cancel,
|
|
||||||
close: dashboard.t.close,
|
|
||||||
processing: dashboard.t.processing,
|
|
||||||
save: dashboard.t.save,
|
|
||||||
templateManagerTitle: dashboard.t.templateManagerTitle,
|
|
||||||
templateTabAgent: dashboard.t.templateTabAgent,
|
|
||||||
templateTabTopic: dashboard.t.templateTabTopic,
|
|
||||||
},
|
|
||||||
onClose: () => dashboard.setShowTemplateModal(false),
|
|
||||||
onTemplateTabChange: dashboard.setTemplateTab,
|
|
||||||
onTemplateAgentTextChange: dashboard.setTemplateAgentText,
|
|
||||||
onTemplateTopicTextChange: dashboard.setTemplateTopicText,
|
|
||||||
onSave: dashboard.saveTemplateManager,
|
|
||||||
},
|
|
||||||
agentFilesModal: {
|
|
||||||
open: dashboard.showAgentModal,
|
|
||||||
agentTab: dashboard.agentTab,
|
|
||||||
tabValue: String(dashboard.editForm[dashboard.agentFieldByTab[dashboard.agentTab]]),
|
|
||||||
isSaving: dashboard.isSaving,
|
|
||||||
labels: {
|
|
||||||
agentFiles: dashboard.t.agentFiles,
|
|
||||||
cancel: dashboard.t.cancel,
|
|
||||||
close: dashboard.t.close,
|
|
||||||
saveFiles: dashboard.t.saveFiles,
|
|
||||||
},
|
|
||||||
onClose: () => dashboard.setShowAgentModal(false),
|
|
||||||
onAgentTabChange: dashboard.setAgentTab,
|
|
||||||
onTabValueChange: (nextValue: string) => dashboard.updateAgentTabValue(dashboard.agentTab, nextValue),
|
|
||||||
onSave: () => dashboard.saveBot('agent'),
|
|
||||||
},
|
|
||||||
runtimeActionModal: {
|
|
||||||
open: dashboard.showRuntimeActionModal,
|
|
||||||
runtimeAction: dashboard.runtimeAction,
|
|
||||||
labels: {
|
|
||||||
close: dashboard.t.close,
|
|
||||||
lastAction: dashboard.t.lastAction,
|
|
||||||
},
|
|
||||||
onClose: () => dashboard.setShowRuntimeActionModal(false),
|
|
||||||
},
|
|
||||||
workspacePreviewModal: {
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
labels: {
|
|
||||||
cancel: dashboard.t.cancel,
|
|
||||||
close: dashboard.t.close,
|
|
||||||
copyAddress: dashboard.t.copyAddress,
|
|
||||||
download: dashboard.t.download,
|
|
||||||
editFile: dashboard.t.editFile,
|
|
||||||
filePreview: dashboard.t.filePreview,
|
|
||||||
fileTruncated: dashboard.t.fileTruncated,
|
|
||||||
save: dashboard.t.save,
|
|
||||||
},
|
|
||||||
preview: dashboard.workspacePreview,
|
|
||||||
previewFullscreen: dashboard.workspacePreviewFullscreen,
|
|
||||||
previewEditorEnabled: dashboard.workspacePreviewEditorEnabled,
|
|
||||||
previewCanEdit: dashboard.workspacePreviewCanEdit,
|
|
||||||
previewDraft: dashboard.workspacePreviewDraft,
|
|
||||||
previewSaving: dashboard.workspacePreviewSaving,
|
|
||||||
markdownComponents: dashboard.workspacePreviewMarkdownComponents,
|
|
||||||
onClose: dashboard.closeWorkspacePreview,
|
|
||||||
onToggleFullscreen: () => dashboard.setWorkspacePreviewFullscreen((prev) => !prev),
|
|
||||||
onCopyPreviewPath: dashboard.copyWorkspacePreviewPath,
|
|
||||||
onCopyPreviewUrl: dashboard.copyWorkspacePreviewUrl,
|
|
||||||
onPreviewDraftChange: dashboard.setWorkspacePreviewDraft,
|
|
||||||
onSavePreviewMarkdown: dashboard.saveWorkspacePreviewMarkdown,
|
|
||||||
onEnterEditMode: () => dashboard.setWorkspacePreviewMode('edit'),
|
|
||||||
onExitEditMode: () => {
|
|
||||||
dashboard.setWorkspacePreviewDraft(dashboard.workspacePreview?.content || '');
|
|
||||||
dashboard.setWorkspacePreviewMode('preview');
|
|
||||||
},
|
|
||||||
getWorkspaceDownloadHref: dashboard.getWorkspaceDownloadHref,
|
|
||||||
getWorkspaceRawHref: dashboard.getWorkspaceRawHref,
|
|
||||||
},
|
|
||||||
workspaceHoverCard: {
|
|
||||||
state: dashboard.workspaceHoverCard,
|
|
||||||
isZh: dashboard.isZh,
|
|
||||||
formatWorkspaceTime,
|
|
||||||
formatBytes,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const createBotModalProps = {
|
|
||||||
open: dashboard.showCreateBotModal,
|
|
||||||
onClose: () => dashboard.setShowCreateBotModal(false),
|
|
||||||
onCreated: () => {
|
|
||||||
void dashboard.refresh();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BotDashboardView
|
|
||||||
compactMode={dashboard.compactMode}
|
|
||||||
hasForcedBot={dashboard.hasForcedBot}
|
|
||||||
showBotListPanel={dashboard.showBotListPanel}
|
|
||||||
botListPanelProps={botListPanelProps}
|
|
||||||
hasSelectedBot={Boolean(dashboard.selectedBot)}
|
|
||||||
isCompactListPage={dashboard.isCompactListPage}
|
|
||||||
compactPanelTab={dashboard.compactPanelTab}
|
|
||||||
showCompactBotPageClose={dashboard.showCompactBotPageClose}
|
|
||||||
forcedBotId={dashboard.forcedBotId}
|
|
||||||
selectBotText={dashboard.forcedBotMissing ? `${dashboard.t.selectBot}: ${String(dashboard.forcedBotId).trim()}` : dashboard.t.selectBot}
|
|
||||||
isZh={dashboard.isZh}
|
|
||||||
runtimeViewMode={dashboard.runtimeViewMode}
|
|
||||||
hasTopicUnread={dashboard.hasTopicUnread}
|
|
||||||
onRuntimeViewModeChange={dashboard.setRuntimeViewMode}
|
|
||||||
topicFeedPanelProps={topicFeedPanelProps}
|
|
||||||
dashboardChatPanelProps={dashboardChatPanelProps}
|
|
||||||
runtimePanelProps={runtimePanelProps}
|
|
||||||
onCompactClose={() => {
|
|
||||||
dashboard.setSelectedBotId('');
|
|
||||||
dashboard.setCompactPanelTab('chat');
|
|
||||||
}}
|
|
||||||
dashboardModalStackProps={dashboardModalStackProps}
|
|
||||||
createBotModalProps={createBotModalProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ import { MessageCircle, MessageSquareText, X } from 'lucide-react';
|
||||||
|
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
import type { CompactPanelTab, RuntimeViewMode } from '../types';
|
import type { CompactPanelTab, RuntimeViewMode } from '../types';
|
||||||
import { BotListPanel } from './BotListPanel';
|
|
||||||
import { DashboardChatPanel } from './DashboardChatPanel';
|
import { DashboardChatPanel } from './DashboardChatPanel';
|
||||||
import { RuntimePanel } from './RuntimePanel';
|
|
||||||
|
|
||||||
|
const LazyBotListPanel = lazy(() =>
|
||||||
|
import('./BotListPanel').then((module) => ({ default: module.BotListPanel })),
|
||||||
|
);
|
||||||
|
const LazyRuntimePanel = lazy(() =>
|
||||||
|
import('./RuntimePanel').then((module) => ({ default: module.RuntimePanel })),
|
||||||
|
);
|
||||||
const LazyCreateBotWizardModal = lazy(() =>
|
const LazyCreateBotWizardModal = lazy(() =>
|
||||||
import('../../onboarding/CreateBotWizardModal').then((module) => ({ default: module.CreateBotWizardModal })),
|
import('../../onboarding/CreateBotWizardModal').then((module) => ({ default: module.CreateBotWizardModal })),
|
||||||
);
|
);
|
||||||
|
|
@ -25,7 +29,7 @@ export interface BotDashboardViewProps {
|
||||||
compactMode: boolean;
|
compactMode: boolean;
|
||||||
hasForcedBot: boolean;
|
hasForcedBot: boolean;
|
||||||
showBotListPanel: boolean;
|
showBotListPanel: boolean;
|
||||||
botListPanelProps: ComponentProps<typeof BotListPanel>;
|
botListPanelProps: Parameters<typeof import('./BotListPanel').BotListPanel>[0];
|
||||||
hasSelectedBot: boolean;
|
hasSelectedBot: boolean;
|
||||||
isCompactListPage: boolean;
|
isCompactListPage: boolean;
|
||||||
compactPanelTab: CompactPanelTab;
|
compactPanelTab: CompactPanelTab;
|
||||||
|
|
@ -38,7 +42,7 @@ export interface BotDashboardViewProps {
|
||||||
onRuntimeViewModeChange: (mode: RuntimeViewMode) => void;
|
onRuntimeViewModeChange: (mode: RuntimeViewMode) => void;
|
||||||
topicFeedPanelProps: TopicFeedPanelProps;
|
topicFeedPanelProps: TopicFeedPanelProps;
|
||||||
dashboardChatPanelProps: ComponentProps<typeof DashboardChatPanel>;
|
dashboardChatPanelProps: ComponentProps<typeof DashboardChatPanel>;
|
||||||
runtimePanelProps: ComponentProps<typeof RuntimePanel>;
|
runtimePanelProps: Parameters<typeof import('./RuntimePanel').RuntimePanel>[0];
|
||||||
onCompactClose: () => void;
|
onCompactClose: () => void;
|
||||||
dashboardModalStackProps: DashboardModalStackProps;
|
dashboardModalStackProps: DashboardModalStackProps;
|
||||||
createBotModalProps: CreateBotWizardModalProps;
|
createBotModalProps: CreateBotWizardModalProps;
|
||||||
|
|
@ -86,7 +90,11 @@ export function BotDashboardView({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`grid-ops ${compactMode ? 'grid-ops-compact' : ''} ${hasForcedBot && !compactMode ? 'grid-ops-forced' : ''}`}>
|
<div className={`grid-ops ${compactMode ? 'grid-ops-compact' : ''} ${hasForcedBot && !compactMode ? 'grid-ops-forced' : ''}`}>
|
||||||
{showBotListPanel ? <BotListPanel {...botListPanelProps} /> : null}
|
{showBotListPanel ? (
|
||||||
|
<Suspense fallback={<section className="panel stack ops-bot-list"><div className="ops-bot-list-empty">{isZh ? '读取 Bot 列表中...' : 'Loading bots...'}</div></section>}>
|
||||||
|
<LazyBotListPanel {...botListPanelProps} />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<section className={`panel ops-chat-panel ${compactMode && (isCompactListPage || compactPanelTab !== 'chat') ? 'ops-compact-hidden' : ''} ${showCompactBotPageClose ? 'ops-compact-bot-surface' : ''}`}>
|
<section className={`panel ops-chat-panel ${compactMode && (isCompactListPage || compactPanelTab !== 'chat') ? 'ops-compact-hidden' : ''} ${showCompactBotPageClose ? 'ops-compact-bot-surface' : ''}`}>
|
||||||
{hasSelectedBot ? (
|
{hasSelectedBot ? (
|
||||||
|
|
@ -137,7 +145,9 @@ export function BotDashboardView({
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<RuntimePanel {...runtimePanelProps} />
|
<Suspense fallback={<section className={`panel stack ops-runtime-panel ${runtimePanelProps.isCompactHidden ? 'ops-compact-hidden' : ''} ${runtimePanelProps.showCompactSurface ? 'ops-compact-bot-surface' : ''}`}><div className="ops-panel-empty-copy">{isZh ? '读取运行面板中...' : 'Loading runtime...'}</div></section>}>
|
||||||
|
<LazyRuntimePanel {...runtimePanelProps} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCompactBotPageClose ? (
|
{showCompactBotPageClose ? (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Boxes, ChevronLeft, ChevronRight, EllipsisVertical, ExternalLink, FileText, Gauge, Lock, Plus, Power, Square, Trash2 } from 'lucide-react';
|
import { Boxes, ChevronLeft, ChevronRight, EllipsisVertical, ExternalLink, FileText, Gauge, Lock, Plus, Power, Square, Trash2 } from 'lucide-react';
|
||||||
import type { RefObject } from 'react';
|
import { memo, type RefObject } from 'react';
|
||||||
|
|
||||||
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
|
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
|
|
@ -70,7 +70,7 @@ interface BotListPanelProps {
|
||||||
onRemoveBot: (botId: string) => Promise<void> | void;
|
onRemoveBot: (botId: string) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BotListPanel({
|
export const BotListPanel = memo(function BotListPanel({
|
||||||
bots,
|
bots,
|
||||||
filteredBots,
|
filteredBots,
|
||||||
pagedBots,
|
pagedBots,
|
||||||
|
|
@ -360,4 +360,4 @@ export function BotListPanel({
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import { workspaceFileAction } from '../../../shared/workspace/utils';
|
||||||
import { formatDateInputValue } from '../chat/chatUtils';
|
import { formatDateInputValue } from '../chat/chatUtils';
|
||||||
import type { DashboardChatPanelLabels } from './DashboardChatPanel.types';
|
import type { DashboardChatPanelLabels } from './DashboardChatPanel.types';
|
||||||
|
|
||||||
|
const TODAY_DATE_INPUT_MAX = formatDateInputValue(Date.now());
|
||||||
|
|
||||||
interface DashboardChatComposerProps {
|
interface DashboardChatComposerProps {
|
||||||
isZh: boolean;
|
isZh: boolean;
|
||||||
labels: DashboardChatPanelLabels;
|
labels: DashboardChatPanelLabels;
|
||||||
|
|
@ -276,7 +278,7 @@ export function DashboardChatComposer({
|
||||||
className="input ops-control-date-input"
|
className="input ops-control-date-input"
|
||||||
type="date"
|
type="date"
|
||||||
value={chatDateValue}
|
value={chatDateValue}
|
||||||
max={formatDateInputValue(Date.now())}
|
max={TODAY_DATE_INPUT_MAX}
|
||||||
onChange={(event) => onChatDateValueChange(event.target.value)}
|
onChange={(event) => onChatDateValueChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
import type { Components } from 'react-markdown';
|
import type { Components } from 'react-markdown';
|
||||||
import { memo, type ChangeEventHandler, type KeyboardEventHandler, type RefObject } from 'react';
|
import { Suspense, lazy, memo, useMemo, type ChangeEventHandler, type KeyboardEventHandler, type RefObject } from 'react';
|
||||||
|
|
||||||
import nanobotLogo from '../../../assets/nanobot-logo.png';
|
import nanobotLogo from '../../../assets/nanobot-logo.png';
|
||||||
import type { ChatMessage } from '../../../types/bot';
|
import type { ChatMessage } from '../../../types/bot';
|
||||||
import type { StagedSubmissionDraft } from '../types';
|
import type { StagedSubmissionDraft } from '../types';
|
||||||
import { DashboardChatComposer } from './DashboardChatComposer';
|
import { DashboardChatComposer } from './DashboardChatComposer';
|
||||||
import type { DashboardChatPanelLabels } from './DashboardChatPanel.types';
|
import type { DashboardChatPanelLabels } from './DashboardChatPanel.types';
|
||||||
import { DashboardConversationMessages } from './DashboardConversationMessages';
|
|
||||||
import { DashboardStagedSubmissionQueue } from './DashboardStagedSubmissionQueue';
|
import { DashboardStagedSubmissionQueue } from './DashboardStagedSubmissionQueue';
|
||||||
import './DashboardChatPanel.css';
|
import './DashboardChatPanel.css';
|
||||||
|
|
||||||
|
const LazyDashboardConversationMessages = lazy(() =>
|
||||||
|
import('./DashboardConversationMessages').then((module) => ({ default: module.DashboardConversationMessages })),
|
||||||
|
);
|
||||||
|
|
||||||
interface DashboardChatPanelProps {
|
interface DashboardChatPanelProps {
|
||||||
conversation: ChatMessage[];
|
conversation: ChatMessage[];
|
||||||
isZh: boolean;
|
isZh: boolean;
|
||||||
|
|
@ -84,7 +87,7 @@ interface DashboardChatPanelProps {
|
||||||
interface DashboardChatTranscriptProps {
|
interface DashboardChatTranscriptProps {
|
||||||
conversation: ChatMessage[];
|
conversation: ChatMessage[];
|
||||||
isZh: boolean;
|
isZh: boolean;
|
||||||
labels: DashboardChatPanelLabels;
|
labels: DashboardConversationTranscriptLabels;
|
||||||
chatScrollRef: RefObject<HTMLDivElement | null>;
|
chatScrollRef: RefObject<HTMLDivElement | null>;
|
||||||
onChatScroll: () => void;
|
onChatScroll: () => void;
|
||||||
expandedProgressByKey: Record<string, boolean>;
|
expandedProgressByKey: Record<string, boolean>;
|
||||||
|
|
@ -105,6 +108,24 @@ interface DashboardChatTranscriptProps {
|
||||||
isThinking: boolean;
|
isThinking: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DashboardConversationTranscriptLabels {
|
||||||
|
badReply: string;
|
||||||
|
copyPrompt: string;
|
||||||
|
copyReply: string;
|
||||||
|
deleteMessage: string;
|
||||||
|
download: string;
|
||||||
|
editPrompt: string;
|
||||||
|
fileNotPreviewable: string;
|
||||||
|
goodReply: string;
|
||||||
|
noConversation: string;
|
||||||
|
previewTitle: string;
|
||||||
|
quoteReply: string;
|
||||||
|
quotedReplyLabel: string;
|
||||||
|
thinking: string;
|
||||||
|
user: string;
|
||||||
|
you: string;
|
||||||
|
}
|
||||||
|
|
||||||
const MemoizedChatTranscript = memo(function MemoizedChatTranscript({
|
const MemoizedChatTranscript = memo(function MemoizedChatTranscript({
|
||||||
conversation,
|
conversation,
|
||||||
isZh,
|
isZh,
|
||||||
|
|
@ -135,24 +156,11 @@ const MemoizedChatTranscript = memo(function MemoizedChatTranscript({
|
||||||
{labels.noConversation}
|
{labels.noConversation}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<DashboardConversationMessages
|
<Suspense fallback={<div className="ops-empty-inline">{isZh ? '读取对话内容中...' : 'Loading conversation...'}</div>}>
|
||||||
|
<LazyDashboardConversationMessages
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
isZh={isZh}
|
isZh={isZh}
|
||||||
labels={{
|
labels={labels}
|
||||||
badReply: labels.badReply,
|
|
||||||
copyPrompt: labels.copyPrompt,
|
|
||||||
copyReply: labels.copyReply,
|
|
||||||
deleteMessage: labels.deleteMessage,
|
|
||||||
download: labels.download,
|
|
||||||
editPrompt: labels.editPrompt,
|
|
||||||
fileNotPreviewable: labels.fileNotPreviewable,
|
|
||||||
goodReply: labels.goodReply,
|
|
||||||
previewTitle: labels.previewTitle,
|
|
||||||
quoteReply: labels.quoteReply,
|
|
||||||
quotedReplyLabel: labels.quotedReplyLabel,
|
|
||||||
user: labels.user,
|
|
||||||
you: labels.you,
|
|
||||||
}}
|
|
||||||
expandedProgressByKey={expandedProgressByKey}
|
expandedProgressByKey={expandedProgressByKey}
|
||||||
expandedUserByKey={expandedUserByKey}
|
expandedUserByKey={expandedUserByKey}
|
||||||
deletingMessageIdMap={deletingMessageIdMap}
|
deletingMessageIdMap={deletingMessageIdMap}
|
||||||
|
|
@ -169,6 +177,7 @@ const MemoizedChatTranscript = memo(function MemoizedChatTranscript({
|
||||||
onQuoteAssistantReply={onQuoteAssistantReply}
|
onQuoteAssistantReply={onQuoteAssistantReply}
|
||||||
onCopyAssistantReply={onCopyAssistantReply}
|
onCopyAssistantReply={onCopyAssistantReply}
|
||||||
/>
|
/>
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isThinking ? (
|
{isThinking ? (
|
||||||
|
|
@ -192,33 +201,7 @@ const MemoizedChatTranscript = memo(function MemoizedChatTranscript({
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, (prev, next) => (
|
});
|
||||||
prev.conversation === next.conversation
|
|
||||||
&& prev.isZh === next.isZh
|
|
||||||
&& prev.isThinking === next.isThinking
|
|
||||||
&& prev.chatScrollRef === next.chatScrollRef
|
|
||||||
&& prev.expandedProgressByKey === next.expandedProgressByKey
|
|
||||||
&& prev.expandedUserByKey === next.expandedUserByKey
|
|
||||||
&& prev.deletingMessageIdMap === next.deletingMessageIdMap
|
|
||||||
&& prev.feedbackSavingByMessageId === next.feedbackSavingByMessageId
|
|
||||||
&& prev.markdownComponents === next.markdownComponents
|
|
||||||
&& prev.workspaceDownloadExtensionSet === next.workspaceDownloadExtensionSet
|
|
||||||
&& prev.labels.badReply === next.labels.badReply
|
|
||||||
&& prev.labels.copyPrompt === next.labels.copyPrompt
|
|
||||||
&& prev.labels.copyReply === next.labels.copyReply
|
|
||||||
&& prev.labels.deleteMessage === next.labels.deleteMessage
|
|
||||||
&& prev.labels.download === next.labels.download
|
|
||||||
&& prev.labels.editPrompt === next.labels.editPrompt
|
|
||||||
&& prev.labels.fileNotPreviewable === next.labels.fileNotPreviewable
|
|
||||||
&& prev.labels.goodReply === next.labels.goodReply
|
|
||||||
&& prev.labels.noConversation === next.labels.noConversation
|
|
||||||
&& prev.labels.previewTitle === next.labels.previewTitle
|
|
||||||
&& prev.labels.quoteReply === next.labels.quoteReply
|
|
||||||
&& prev.labels.quotedReplyLabel === next.labels.quotedReplyLabel
|
|
||||||
&& prev.labels.thinking === next.labels.thinking
|
|
||||||
&& prev.labels.user === next.labels.user
|
|
||||||
&& prev.labels.you === next.labels.you
|
|
||||||
));
|
|
||||||
|
|
||||||
export function DashboardChatPanel({
|
export function DashboardChatPanel({
|
||||||
conversation,
|
conversation,
|
||||||
|
|
@ -290,12 +273,60 @@ export function DashboardChatPanel({
|
||||||
submitActionMode,
|
submitActionMode,
|
||||||
onSubmitAction,
|
onSubmitAction,
|
||||||
}: DashboardChatPanelProps) {
|
}: DashboardChatPanelProps) {
|
||||||
|
const transcriptLabels = useMemo<DashboardConversationTranscriptLabels>(() => ({
|
||||||
|
badReply: labels.badReply,
|
||||||
|
copyPrompt: labels.copyPrompt,
|
||||||
|
copyReply: labels.copyReply,
|
||||||
|
deleteMessage: labels.deleteMessage,
|
||||||
|
download: labels.download,
|
||||||
|
editPrompt: labels.editPrompt,
|
||||||
|
fileNotPreviewable: labels.fileNotPreviewable,
|
||||||
|
goodReply: labels.goodReply,
|
||||||
|
noConversation: labels.noConversation,
|
||||||
|
previewTitle: labels.previewTitle,
|
||||||
|
quoteReply: labels.quoteReply,
|
||||||
|
quotedReplyLabel: labels.quotedReplyLabel,
|
||||||
|
thinking: labels.thinking,
|
||||||
|
user: labels.user,
|
||||||
|
you: labels.you,
|
||||||
|
}), [
|
||||||
|
labels.badReply,
|
||||||
|
labels.copyPrompt,
|
||||||
|
labels.copyReply,
|
||||||
|
labels.deleteMessage,
|
||||||
|
labels.download,
|
||||||
|
labels.editPrompt,
|
||||||
|
labels.fileNotPreviewable,
|
||||||
|
labels.goodReply,
|
||||||
|
labels.noConversation,
|
||||||
|
labels.previewTitle,
|
||||||
|
labels.quoteReply,
|
||||||
|
labels.quotedReplyLabel,
|
||||||
|
labels.thinking,
|
||||||
|
labels.user,
|
||||||
|
labels.you,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const stagedSubmissionLabels = useMemo(() => ({
|
||||||
|
quotedReplyLabel: labels.quotedReplyLabel,
|
||||||
|
stagedSubmissionAttachmentCount: labels.stagedSubmissionAttachmentCount,
|
||||||
|
stagedSubmissionEmpty: labels.stagedSubmissionEmpty,
|
||||||
|
stagedSubmissionRestore: labels.stagedSubmissionRestore,
|
||||||
|
stagedSubmissionRemove: labels.stagedSubmissionRemove,
|
||||||
|
}), [
|
||||||
|
labels.quotedReplyLabel,
|
||||||
|
labels.stagedSubmissionAttachmentCount,
|
||||||
|
labels.stagedSubmissionEmpty,
|
||||||
|
labels.stagedSubmissionRestore,
|
||||||
|
labels.stagedSubmissionRemove,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`ops-chat-frame ${isChatEnabled ? '' : 'is-disabled'}`}>
|
<div className={`ops-chat-frame ${isChatEnabled ? '' : 'is-disabled'}`}>
|
||||||
<MemoizedChatTranscript
|
<MemoizedChatTranscript
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
isZh={isZh}
|
isZh={isZh}
|
||||||
labels={labels}
|
labels={transcriptLabels}
|
||||||
chatScrollRef={chatScrollRef}
|
chatScrollRef={chatScrollRef}
|
||||||
onChatScroll={onChatScroll}
|
onChatScroll={onChatScroll}
|
||||||
expandedProgressByKey={expandedProgressByKey}
|
expandedProgressByKey={expandedProgressByKey}
|
||||||
|
|
@ -318,7 +349,7 @@ export function DashboardChatPanel({
|
||||||
|
|
||||||
<div className="ops-chat-dock">
|
<div className="ops-chat-dock">
|
||||||
<DashboardStagedSubmissionQueue
|
<DashboardStagedSubmissionQueue
|
||||||
labels={labels}
|
labels={stagedSubmissionLabels}
|
||||||
stagedSubmissions={stagedSubmissions}
|
stagedSubmissions={stagedSubmissions}
|
||||||
onRestoreStagedSubmission={onRestoreStagedSubmission}
|
onRestoreStagedSubmission={onRestoreStagedSubmission}
|
||||||
onRemoveStagedSubmission={onRemoveStagedSubmission}
|
onRemoveStagedSubmission={onRemoveStagedSubmission}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { ChevronDown, ChevronUp, Copy, Download, Eye, FileText, Pencil, Reply, ThumbsDown, ThumbsUp, Trash2, UserRound } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Copy, Download, Eye, FileText, Pencil, Reply, ThumbsDown, ThumbsUp, Trash2, UserRound } from 'lucide-react';
|
||||||
import { memo } from 'react';
|
import { Suspense, lazy, memo } from 'react';
|
||||||
import ReactMarkdown, { type Components } from 'react-markdown';
|
import type { Components } from 'react-markdown';
|
||||||
import rehypeRaw from 'rehype-raw';
|
|
||||||
import rehypeSanitize from 'rehype-sanitize';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
|
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
import nanobotLogo from '../../../assets/nanobot-logo.png';
|
import nanobotLogo from '../../../assets/nanobot-logo.png';
|
||||||
import { normalizeAssistantMessageText, normalizeUserMessageText, summarizeProgressText } from '../../../shared/text/messageText';
|
import { normalizeAssistantMessageText, normalizeUserMessageText, summarizeProgressText } from '../../../shared/text/messageText';
|
||||||
import { MARKDOWN_SANITIZE_SCHEMA } from '../../../shared/workspace/constants';
|
|
||||||
import { decorateWorkspacePathsForMarkdown, normalizeDashboardAttachmentPath } from '../../../shared/workspace/workspaceMarkdown';
|
import { decorateWorkspacePathsForMarkdown, normalizeDashboardAttachmentPath } from '../../../shared/workspace/workspaceMarkdown';
|
||||||
import { workspaceFileAction } from '../../../shared/workspace/utils';
|
import { workspaceFileAction } from '../../../shared/workspace/utils';
|
||||||
import type { ChatMessage } from '../../../types/bot';
|
import type { ChatMessage } from '../../../types/bot';
|
||||||
import { formatClock, formatConversationDate } from '../chat/chatUtils';
|
import { formatClock, formatConversationDate } from '../chat/chatUtils';
|
||||||
import './DashboardConversationMessages.css';
|
import './DashboardConversationMessages.css';
|
||||||
|
|
||||||
|
const LazyMarkdownRenderer = lazy(() =>
|
||||||
|
import('../../../shared/markdown/MarkdownRenderer').then((module) => ({ default: module.MarkdownRenderer })),
|
||||||
|
);
|
||||||
|
|
||||||
interface DashboardConversationLabels {
|
interface DashboardConversationLabels {
|
||||||
badReply: string;
|
badReply: string;
|
||||||
copyPrompt: string;
|
copyPrompt: string;
|
||||||
|
|
@ -215,13 +215,14 @@ const DashboardConversationMessageRow = memo(function DashboardConversationMessa
|
||||||
<div className="whitespace-pre-wrap">{normalizeUserMessageText(displayText)}</div>
|
<div className="whitespace-pre-wrap">{normalizeUserMessageText(displayText)}</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ReactMarkdown
|
<Suspense
|
||||||
remarkPlugins={[remarkGfm]}
|
fallback={<div className="whitespace-pre-wrap">{normalizeAssistantMessageText(displayText)}</div>}
|
||||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
|
||||||
components={markdownComponents}
|
|
||||||
>
|
>
|
||||||
{decorateWorkspacePathsForMarkdown(displayText)}
|
<LazyMarkdownRenderer
|
||||||
</ReactMarkdown>
|
components={markdownComponents}
|
||||||
|
content={decorateWorkspacePathsForMarkdown(displayText)}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
{(item.attachments || []).length > 0 ? (
|
{(item.attachments || []).length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Plus, Save, Trash2, X } from 'lucide-react';
|
import { Plus, Save, Trash2, X } from 'lucide-react';
|
||||||
|
|
||||||
import { DrawerShell } from '../../../components/DrawerShell';
|
import { DrawerShell } from '../../../components/DrawerShell';
|
||||||
|
|
@ -57,26 +57,28 @@ export function EnvParamsModal({
|
||||||
const [createPanelOpen, setCreatePanelOpen] = useState(false);
|
const [createPanelOpen, setCreatePanelOpen] = useState(false);
|
||||||
const [envEditDrafts, setEnvEditDrafts] = useState<Record<string, { key: string; value: string }>>({});
|
const [envEditDrafts, setEnvEditDrafts] = useState<Record<string, { key: string; value: string }>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
const resetLocalState = () => {
|
||||||
if (open) return;
|
|
||||||
setCreatePanelOpen(false);
|
setCreatePanelOpen(false);
|
||||||
}, [open]);
|
setEnvEditDrafts({});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const mergedEnvDrafts = useMemo(() => {
|
||||||
if (!open) return;
|
|
||||||
const nextDrafts: Record<string, { key: string; value: string }> = {};
|
const nextDrafts: Record<string, { key: string; value: string }> = {};
|
||||||
envEntries.forEach(([key, value]) => {
|
envEntries.forEach(([key, value]) => {
|
||||||
nextDrafts[key] = { key, value };
|
nextDrafts[key] = envEditDrafts[key] || { key, value };
|
||||||
});
|
});
|
||||||
setEnvEditDrafts(nextDrafts);
|
return nextDrafts;
|
||||||
}, [envEntries, open]);
|
}, [envEditDrafts, envEntries]);
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerShell
|
<DrawerShell
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={() => {
|
||||||
|
resetLocalState();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
title={labels.envParams}
|
title={labels.envParams}
|
||||||
size="standard"
|
size="standard"
|
||||||
bodyClassName="ops-config-drawer-body"
|
bodyClassName="ops-config-drawer-body"
|
||||||
|
|
@ -99,7 +101,7 @@ export function EnvParamsModal({
|
||||||
<div className="ops-empty-inline">{labels.noEnvParams}</div>
|
<div className="ops-empty-inline">{labels.noEnvParams}</div>
|
||||||
) : (
|
) : (
|
||||||
envEntries.map(([key, value]) => {
|
envEntries.map(([key, value]) => {
|
||||||
const draft = envEditDrafts[key] || { key, value };
|
const draft = mergedEnvDrafts[key] || { key, value };
|
||||||
return (
|
return (
|
||||||
<div key={key} className="card wizard-channel-card wizard-channel-compact">
|
<div key={key} className="card wizard-channel-card wizard-channel-compact">
|
||||||
<div className="ops-config-card-header">
|
<div className="ops-config-card-header">
|
||||||
|
|
@ -110,7 +112,16 @@ export function EnvParamsModal({
|
||||||
<div className="ops-config-card-actions">
|
<div className="ops-config-card-actions">
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-danger btn-sm wizard-icon-btn"
|
className="btn btn-danger btn-sm wizard-icon-btn"
|
||||||
onClick={() => void onDeleteEnvParam(key)}
|
onClick={async () => {
|
||||||
|
const deleted = await onDeleteEnvParam(key);
|
||||||
|
if (!deleted) return;
|
||||||
|
setEnvEditDrafts((prev) => {
|
||||||
|
if (!(key in prev)) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
tooltip={labels.removeEnvParam}
|
tooltip={labels.removeEnvParam}
|
||||||
aria-label={labels.removeEnvParam}
|
aria-label={labels.removeEnvParam}
|
||||||
>
|
>
|
||||||
|
|
@ -165,7 +176,19 @@ export function EnvParamsModal({
|
||||||
</div>
|
</div>
|
||||||
<div className="row-between ops-config-footer">
|
<div className="row-between ops-config-footer">
|
||||||
<span className="field-label">{labels.envParamsHint}</span>
|
<span className="field-label">{labels.envParamsHint}</span>
|
||||||
<button className="btn btn-primary btn-sm" onClick={() => void onSaveEnvParam(key, draft.key, draft.value)}>
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={async () => {
|
||||||
|
const saved = await onSaveEnvParam(key, draft.key, draft.value);
|
||||||
|
if (!saved) return;
|
||||||
|
setEnvEditDrafts((prev) => {
|
||||||
|
if (!(key in prev)) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[key];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Save size={14} />
|
<Save size={14} />
|
||||||
<span style={{ marginLeft: 6 }}>{labels.save}</span>
|
<span style={{ marginLeft: 6 }}>{labels.save}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -186,7 +209,7 @@ export function EnvParamsModal({
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="ops-plain-icon-btn"
|
className="ops-plain-icon-btn"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCreatePanelOpen(false);
|
resetLocalState();
|
||||||
onEnvDraftKeyChange('');
|
onEnvDraftKeyChange('');
|
||||||
onEnvDraftValueChange('');
|
onEnvDraftValueChange('');
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,25 @@
|
||||||
import { Pencil, Trash2 } from 'lucide-react';
|
import { Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
import { normalizeUserMessageText } from '../../../shared/text/messageText';
|
import { normalizeUserMessageText } from '../../../shared/text/messageText';
|
||||||
import type { StagedSubmissionDraft } from '../types';
|
import type { StagedSubmissionDraft } from '../types';
|
||||||
import type { DashboardChatPanelLabels } from './DashboardChatPanel.types';
|
|
||||||
|
interface DashboardStagedSubmissionQueueLabels {
|
||||||
|
quotedReplyLabel: string;
|
||||||
|
stagedSubmissionAttachmentCount: (count: number) => string;
|
||||||
|
stagedSubmissionEmpty: string;
|
||||||
|
stagedSubmissionRestore: string;
|
||||||
|
stagedSubmissionRemove: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface DashboardStagedSubmissionQueueProps {
|
interface DashboardStagedSubmissionQueueProps {
|
||||||
labels: DashboardChatPanelLabels;
|
labels: DashboardStagedSubmissionQueueLabels;
|
||||||
stagedSubmissions: StagedSubmissionDraft[];
|
stagedSubmissions: StagedSubmissionDraft[];
|
||||||
onRestoreStagedSubmission: (stagedSubmissionId: string) => void;
|
onRestoreStagedSubmission: (stagedSubmissionId: string) => void;
|
||||||
onRemoveStagedSubmission: (stagedSubmissionId: string) => void;
|
onRemoveStagedSubmission: (stagedSubmissionId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DashboardStagedSubmissionQueue({
|
export const DashboardStagedSubmissionQueue = memo(function DashboardStagedSubmissionQueue({
|
||||||
labels,
|
labels,
|
||||||
stagedSubmissions,
|
stagedSubmissions,
|
||||||
onRestoreStagedSubmission,
|
onRestoreStagedSubmission,
|
||||||
|
|
@ -67,4 +75,4 @@ export function DashboardStagedSubmissionQueue({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Boxes, Check, Clock3, EllipsisVertical, FileText, Hammer, MessageSquareText, RefreshCw, RotateCcw, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, Waypoints } from 'lucide-react';
|
import { Boxes, Check, Clock3, EllipsisVertical, FileText, Hammer, MessageSquareText, RefreshCw, RotateCcw, Save, Settings2, SlidersHorizontal, TriangleAlert, Trash2, Waypoints } from 'lucide-react';
|
||||||
import type { RefObject } from 'react';
|
import { memo, type RefObject } from 'react';
|
||||||
|
|
||||||
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
|
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
|
|
@ -92,7 +92,7 @@ interface RuntimePanelProps {
|
||||||
onHideWorkspaceHoverCard: () => void;
|
onHideWorkspaceHoverCard: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RuntimePanel({
|
export const RuntimePanel = memo(function RuntimePanel({
|
||||||
selectedBot,
|
selectedBot,
|
||||||
selectedBotEnabled,
|
selectedBotEnabled,
|
||||||
operatingBotId,
|
operatingBotId,
|
||||||
|
|
@ -336,4 +336,4 @@ export function RuntimePanel({
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useId, useState } from 'react';
|
||||||
|
|
||||||
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
||||||
import { pickLocale } from '../../../i18n';
|
import { pickLocale } from '../../../i18n';
|
||||||
|
|
@ -44,14 +44,8 @@ export function useBotDashboardModule({
|
||||||
const [botListPageSize, setBotListPageSize] = useState(10);
|
const [botListPageSize, setBotListPageSize] = useState(10);
|
||||||
const [chatPullPageSize, setChatPullPageSize] = useState(60);
|
const [chatPullPageSize, setChatPullPageSize] = useState(60);
|
||||||
const [commandAutoUnlockSeconds, setCommandAutoUnlockSeconds] = useState(10);
|
const [commandAutoUnlockSeconds, setCommandAutoUnlockSeconds] = useState(10);
|
||||||
const botSearchInputName = useMemo(
|
const botSearchInputName = `nbot-search-${useId().replace(/:/g, '-')}`;
|
||||||
() => `nbot-search-${Math.random().toString(36).slice(2, 10)}`,
|
const workspaceSearchInputName = `nbot-workspace-search-${useId().replace(/:/g, '-')}`;
|
||||||
[],
|
|
||||||
);
|
|
||||||
const workspaceSearchInputName = useMemo(
|
|
||||||
() => `nbot-workspace-search-${Math.random().toString(36).slice(2, 10)}`,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const {
|
const {
|
||||||
allowedAttachmentExtensions,
|
allowedAttachmentExtensions,
|
||||||
botListPageSizeReady,
|
botListPageSizeReady,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,498 @@
|
||||||
|
import type { BotDashboardModuleProps } from './types';
|
||||||
|
import { formatBytes, formatWorkspaceTime } from './utils';
|
||||||
|
import type { BotDashboardViewProps } from './components/BotDashboardView';
|
||||||
|
import type { useBotDashboardModule } from './hooks/useBotDashboardModule';
|
||||||
|
|
||||||
|
type BotDashboardState = ReturnType<typeof useBotDashboardModule>;
|
||||||
|
|
||||||
|
interface UseBotDashboardViewPropsOptions {
|
||||||
|
dashboard: BotDashboardState;
|
||||||
|
onOpenImageFactory?: BotDashboardModuleProps['onOpenImageFactory'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBotDashboardViewProps({
|
||||||
|
dashboard,
|
||||||
|
onOpenImageFactory,
|
||||||
|
}: UseBotDashboardViewPropsOptions): BotDashboardViewProps {
|
||||||
|
const runtimeMoreLabel = dashboard.isZh ? '更多' : 'More';
|
||||||
|
|
||||||
|
const botListPanelProps = {
|
||||||
|
bots: dashboard.bots,
|
||||||
|
filteredBots: dashboard.filteredBots,
|
||||||
|
pagedBots: dashboard.pagedBots,
|
||||||
|
selectedBotId: dashboard.selectedBotId,
|
||||||
|
normalizedBotListQuery: dashboard.normalizedBotListQuery,
|
||||||
|
botListQuery: dashboard.botListQuery,
|
||||||
|
botListPageSizeReady: dashboard.botListPageSizeReady,
|
||||||
|
botListPage: dashboard.botListPage,
|
||||||
|
botListTotalPages: dashboard.botListTotalPages,
|
||||||
|
botListMenuOpen: dashboard.botListMenuOpen,
|
||||||
|
controlStateByBot: dashboard.controlStateByBot,
|
||||||
|
operatingBotId: dashboard.operatingBotId,
|
||||||
|
compactMode: dashboard.compactMode,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
isLoadingTemplates: dashboard.isLoadingTemplates,
|
||||||
|
isBatchOperating: dashboard.isBatchOperating,
|
||||||
|
labels: {
|
||||||
|
batchStart: dashboard.t.batchStart,
|
||||||
|
batchStop: dashboard.t.batchStop,
|
||||||
|
botSearchNoResult: dashboard.t.botSearchNoResult,
|
||||||
|
botSearchPlaceholder: dashboard.t.botSearchPlaceholder,
|
||||||
|
clearSearch: dashboard.t.clearSearch,
|
||||||
|
delete: dashboard.t.delete,
|
||||||
|
disable: dashboard.t.disable,
|
||||||
|
disabled: dashboard.t.disabled,
|
||||||
|
enable: dashboard.t.enable,
|
||||||
|
extensions: dashboard.t.extensions,
|
||||||
|
image: dashboard.t.image,
|
||||||
|
manageImages: dashboard.t.manageImages,
|
||||||
|
newBot: dashboard.t.newBot,
|
||||||
|
paginationNext: dashboard.t.paginationNext,
|
||||||
|
paginationPage: dashboard.t.paginationPage,
|
||||||
|
paginationPrev: dashboard.t.paginationPrev,
|
||||||
|
searchAction: dashboard.t.searchAction,
|
||||||
|
start: dashboard.t.start,
|
||||||
|
stop: dashboard.t.stop,
|
||||||
|
syncingPageSize: dashboard.t.syncingPageSize,
|
||||||
|
templateManager: dashboard.t.templateManager,
|
||||||
|
titleBots: dashboard.t.titleBots,
|
||||||
|
},
|
||||||
|
botSearchInputName: dashboard.botSearchInputName,
|
||||||
|
botListMenuRef: dashboard.botListMenuRef,
|
||||||
|
onOpenCreateWizard: () => dashboard.setShowCreateBotModal(true),
|
||||||
|
onOpenImageFactory,
|
||||||
|
onToggleMenu: () => dashboard.setBotListMenuOpen((prev) => !prev),
|
||||||
|
onCloseMenu: () => dashboard.setBotListMenuOpen(false),
|
||||||
|
onOpenTemplateManager: dashboard.openTemplateManager,
|
||||||
|
onBatchStartBots: dashboard.batchStartBots,
|
||||||
|
onBatchStopBots: dashboard.batchStopBots,
|
||||||
|
onBotListQueryChange: dashboard.setBotListQuery,
|
||||||
|
onBotListPageChange: dashboard.setBotListPage,
|
||||||
|
onSelectBot: dashboard.setSelectedBotId,
|
||||||
|
onSetCompactPanelTab: (tab: 'chat' | 'runtime') => dashboard.setCompactPanelTab(tab),
|
||||||
|
onSetBotEnabled: dashboard.setBotEnabled,
|
||||||
|
onStartBot: dashboard.startBot,
|
||||||
|
onStopBot: dashboard.stopBot,
|
||||||
|
onOpenResourceMonitor: dashboard.openResourceMonitor,
|
||||||
|
onRemoveBot: dashboard.removeBot,
|
||||||
|
};
|
||||||
|
|
||||||
|
const topicFeedPanelProps = {
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
topicKey: dashboard.topicFeedTopicKey,
|
||||||
|
topicOptions: dashboard.activeTopicOptions,
|
||||||
|
topicState: dashboard.topicPanelState,
|
||||||
|
items: dashboard.topicFeedItems,
|
||||||
|
loading: dashboard.topicFeedLoading,
|
||||||
|
loadingMore: dashboard.topicFeedLoadingMore,
|
||||||
|
nextCursor: dashboard.topicFeedNextCursor,
|
||||||
|
error: dashboard.topicFeedError,
|
||||||
|
readSavingById: dashboard.topicFeedReadSavingById,
|
||||||
|
deleteSavingById: dashboard.topicFeedDeleteSavingById,
|
||||||
|
onTopicChange: dashboard.setTopicFeedTopicKey,
|
||||||
|
onRefresh: () => void dashboard.loadTopicFeed({ append: false, topicKey: dashboard.topicFeedTopicKey }),
|
||||||
|
onMarkRead: (itemId: number) => void dashboard.markTopicFeedItemRead(itemId),
|
||||||
|
onDeleteItem: (item: (typeof dashboard.topicFeedItems)[number]) => void dashboard.deleteTopicFeedItem(item),
|
||||||
|
onLoadMore: () => void dashboard.loadTopicFeed({ append: true, cursor: dashboard.topicFeedNextCursor, topicKey: dashboard.topicFeedTopicKey }),
|
||||||
|
onOpenWorkspacePath: (path: string) => void dashboard.openWorkspacePathFromChat(path),
|
||||||
|
resolveWorkspaceMediaSrc: dashboard.resolveWorkspaceMediaSrc,
|
||||||
|
onOpenTopicSettings: dashboard.openTopicConfigModal,
|
||||||
|
onDetailOpenChange: dashboard.setTopicDetailOpen,
|
||||||
|
layout: 'panel' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dashboardChatPanelProps = {
|
||||||
|
conversation: dashboard.conversation,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
labels: {
|
||||||
|
badReply: dashboard.t.badReply,
|
||||||
|
botDisabledHint: dashboard.t.botDisabledHint,
|
||||||
|
botStarting: dashboard.t.botStarting,
|
||||||
|
botStopping: dashboard.t.botStopping,
|
||||||
|
chatDisabled: dashboard.t.chatDisabled,
|
||||||
|
controlCommandsHide: dashboard.t.controlCommandsHide,
|
||||||
|
controlCommandsShow: dashboard.t.controlCommandsShow,
|
||||||
|
copyPrompt: dashboard.t.copyPrompt,
|
||||||
|
copyReply: dashboard.t.copyReply,
|
||||||
|
deleteMessage: dashboard.t.deleteMessage,
|
||||||
|
disabledPlaceholder: dashboard.t.disabledPlaceholder,
|
||||||
|
download: dashboard.t.download,
|
||||||
|
editPrompt: dashboard.t.editPrompt,
|
||||||
|
fileNotPreviewable: dashboard.t.fileNotPreviewable,
|
||||||
|
goodReply: dashboard.t.goodReply,
|
||||||
|
inputPlaceholder: dashboard.t.inputPlaceholder,
|
||||||
|
interrupt: dashboard.t.interrupt,
|
||||||
|
noConversation: dashboard.t.noConversation,
|
||||||
|
previewTitle: dashboard.t.previewTitle,
|
||||||
|
stagedSubmissionAttachmentCount: dashboard.t.stagedSubmissionAttachmentCount,
|
||||||
|
stagedSubmissionEmpty: dashboard.t.stagedSubmissionEmpty,
|
||||||
|
stagedSubmissionRestore: dashboard.t.stagedSubmissionRestore,
|
||||||
|
stagedSubmissionRemove: dashboard.t.stagedSubmissionRemove,
|
||||||
|
quoteReply: dashboard.t.quoteReply,
|
||||||
|
quotedReplyLabel: dashboard.t.quotedReplyLabel,
|
||||||
|
send: dashboard.t.send,
|
||||||
|
thinking: dashboard.t.thinking,
|
||||||
|
uploadFile: dashboard.t.uploadFile,
|
||||||
|
uploadingFile: dashboard.t.uploadingFile,
|
||||||
|
user: dashboard.t.user,
|
||||||
|
voiceStart: dashboard.t.voiceStart,
|
||||||
|
voiceStop: dashboard.t.voiceStop,
|
||||||
|
voiceTranscribing: dashboard.t.voiceTranscribing,
|
||||||
|
you: dashboard.t.you,
|
||||||
|
},
|
||||||
|
chatScrollRef: dashboard.chatScrollRef,
|
||||||
|
onChatScroll: dashboard.onChatScroll,
|
||||||
|
expandedProgressByKey: dashboard.expandedProgressByKey,
|
||||||
|
expandedUserByKey: dashboard.expandedUserByKey,
|
||||||
|
deletingMessageIdMap: dashboard.deletingMessageIdMap,
|
||||||
|
feedbackSavingByMessageId: dashboard.feedbackSavingByMessageId,
|
||||||
|
markdownComponents: dashboard.markdownComponents,
|
||||||
|
workspaceDownloadExtensionSet: dashboard.workspaceDownloadExtensionSet,
|
||||||
|
onToggleProgressExpand: dashboard.toggleProgressExpanded,
|
||||||
|
onToggleUserExpand: dashboard.toggleUserExpanded,
|
||||||
|
onEditUserPrompt: dashboard.editUserPrompt,
|
||||||
|
onCopyUserPrompt: dashboard.copyUserPrompt,
|
||||||
|
onDeleteConversationMessage: dashboard.deleteConversationMessage,
|
||||||
|
onOpenWorkspacePath: dashboard.openWorkspacePathFromChat,
|
||||||
|
onSubmitAssistantFeedback: dashboard.submitAssistantFeedback,
|
||||||
|
onQuoteAssistantReply: dashboard.quoteAssistantReply,
|
||||||
|
onCopyAssistantReply: dashboard.copyAssistantReply,
|
||||||
|
isThinking: dashboard.isThinking,
|
||||||
|
canChat: dashboard.canChat,
|
||||||
|
isChatEnabled: dashboard.isChatEnabled,
|
||||||
|
speechEnabled: dashboard.speechEnabled,
|
||||||
|
selectedBotEnabled: dashboard.selectedBotEnabled,
|
||||||
|
selectedBotControlState: dashboard.selectedBotControlState,
|
||||||
|
quotedReply: dashboard.quotedReply,
|
||||||
|
onClearQuotedReply: () => dashboard.setQuotedReply(null),
|
||||||
|
stagedSubmissions: dashboard.selectedBotStagedSubmissions,
|
||||||
|
onRestoreStagedSubmission: dashboard.restoreStagedSubmission,
|
||||||
|
onRemoveStagedSubmission: dashboard.removeStagedSubmission,
|
||||||
|
pendingAttachments: dashboard.pendingAttachments,
|
||||||
|
onRemovePendingAttachment: (path: string) =>
|
||||||
|
dashboard.setPendingAttachments((prev) => prev.filter((value) => value !== path)),
|
||||||
|
attachmentUploadPercent: dashboard.attachmentUploadPercent,
|
||||||
|
isUploadingAttachments: dashboard.isUploadingAttachments,
|
||||||
|
filePickerRef: dashboard.filePickerRef,
|
||||||
|
allowedAttachmentExtensions: dashboard.allowedAttachmentExtensions,
|
||||||
|
onPickAttachments: dashboard.onPickAttachments,
|
||||||
|
controlCommandPanelOpen: dashboard.controlCommandPanelOpen,
|
||||||
|
controlCommandPanelRef: dashboard.controlCommandPanelRef,
|
||||||
|
onToggleControlCommandPanel: () => {
|
||||||
|
dashboard.setChatDatePickerOpen(false);
|
||||||
|
dashboard.setControlCommandPanelOpen((prev) => !prev);
|
||||||
|
},
|
||||||
|
activeControlCommand: dashboard.activeControlCommand,
|
||||||
|
canSendControlCommand: dashboard.canSendControlCommand,
|
||||||
|
isInterrupting: dashboard.isInterrupting,
|
||||||
|
onSendControlCommand: dashboard.sendControlCommand,
|
||||||
|
onInterruptExecution: dashboard.interruptExecution,
|
||||||
|
chatDateTriggerRef: dashboard.chatDateTriggerRef,
|
||||||
|
hasSelectedBot: Boolean(dashboard.selectedBotId),
|
||||||
|
chatDateJumping: dashboard.chatDateJumping,
|
||||||
|
onToggleChatDatePicker: dashboard.toggleChatDatePicker,
|
||||||
|
chatDatePickerOpen: dashboard.chatDatePickerOpen,
|
||||||
|
chatDatePanelPosition: dashboard.chatDatePanelPosition,
|
||||||
|
chatDateValue: dashboard.chatDateValue,
|
||||||
|
onChatDateValueChange: dashboard.setChatDateValue,
|
||||||
|
onCloseChatDatePicker: () => dashboard.setChatDatePickerOpen(false),
|
||||||
|
onJumpConversationToDate: dashboard.jumpConversationToDate,
|
||||||
|
command: dashboard.command,
|
||||||
|
onCommandChange: dashboard.setCommand,
|
||||||
|
composerTextareaRef: dashboard.composerTextareaRef,
|
||||||
|
onComposerKeyDown: dashboard.onComposerKeyDown,
|
||||||
|
isVoiceRecording: dashboard.isVoiceRecording,
|
||||||
|
isVoiceTranscribing: dashboard.isVoiceTranscribing,
|
||||||
|
isCompactMobile: dashboard.isCompactMobile,
|
||||||
|
voiceCountdown: dashboard.voiceCountdown,
|
||||||
|
onVoiceInput: dashboard.onVoiceInput,
|
||||||
|
onTriggerPickAttachments: dashboard.triggerPickAttachments,
|
||||||
|
submitActionMode: dashboard.submitActionMode,
|
||||||
|
onSubmitAction: dashboard.handlePrimarySubmitAction,
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtimePanelProps = {
|
||||||
|
selectedBot: dashboard.selectedBot,
|
||||||
|
selectedBotEnabled: dashboard.selectedBotEnabled,
|
||||||
|
operatingBotId: dashboard.operatingBotId,
|
||||||
|
runtimeMenuOpen: dashboard.runtimeMenuOpen,
|
||||||
|
runtimeMenuRef: dashboard.runtimeMenuRef,
|
||||||
|
displayState: dashboard.displayState,
|
||||||
|
workspaceError: dashboard.workspaceError,
|
||||||
|
workspacePathDisplay: dashboard.workspacePathDisplay,
|
||||||
|
workspaceLoading: dashboard.workspaceLoading,
|
||||||
|
workspaceQuery: dashboard.workspaceQuery,
|
||||||
|
workspaceSearchInputName: dashboard.workspaceSearchInputName,
|
||||||
|
workspaceSearchLoading: dashboard.workspaceSearchLoading,
|
||||||
|
filteredWorkspaceEntries: dashboard.filteredWorkspaceEntries,
|
||||||
|
workspaceParentPath: dashboard.workspaceParentPath,
|
||||||
|
workspaceFileLoading: dashboard.workspaceFileLoading,
|
||||||
|
workspaceDownloadExtensionSet: dashboard.workspaceDownloadExtensionSet,
|
||||||
|
workspaceAutoRefresh: dashboard.workspaceAutoRefresh,
|
||||||
|
isCompactHidden: dashboard.compactMode && (dashboard.isCompactListPage || dashboard.compactPanelTab !== 'runtime'),
|
||||||
|
showCompactSurface: dashboard.showCompactBotPageClose,
|
||||||
|
emptyStateText: dashboard.forcedBotMissing ? `${dashboard.t.noTelemetry}: ${String(dashboard.forcedBotId).trim()}` : dashboard.t.noTelemetry,
|
||||||
|
labels: {
|
||||||
|
agent: dashboard.t.agent,
|
||||||
|
autoRefresh: dashboard.lc.autoRefresh,
|
||||||
|
base: dashboard.t.base,
|
||||||
|
channels: dashboard.t.channels,
|
||||||
|
clearHistory: dashboard.t.clearHistory,
|
||||||
|
clearSearch: dashboard.t.clearSearch,
|
||||||
|
cronViewer: dashboard.t.cronViewer,
|
||||||
|
download: dashboard.t.download,
|
||||||
|
emptyDir: dashboard.t.emptyDir,
|
||||||
|
envParams: dashboard.t.envParams,
|
||||||
|
exportHistory: dashboard.t.exportHistory,
|
||||||
|
fileNotPreviewable: dashboard.t.fileNotPreviewable,
|
||||||
|
folder: dashboard.t.folder,
|
||||||
|
goUp: dashboard.t.goUp,
|
||||||
|
goUpTitle: dashboard.t.goUpTitle,
|
||||||
|
loadingDir: dashboard.t.loadingDir,
|
||||||
|
mcp: dashboard.t.mcp,
|
||||||
|
more: runtimeMoreLabel,
|
||||||
|
noPreviewFile: dashboard.t.noPreviewFile,
|
||||||
|
openingPreview: dashboard.t.openingPreview,
|
||||||
|
openFolderTitle: dashboard.t.openFolderTitle,
|
||||||
|
params: dashboard.t.params,
|
||||||
|
previewTitle: dashboard.t.previewTitle,
|
||||||
|
refreshHint: dashboard.lc.refreshHint,
|
||||||
|
restart: dashboard.t.restart,
|
||||||
|
runtime: dashboard.t.runtime,
|
||||||
|
searchAction: dashboard.t.searchAction,
|
||||||
|
skills: dashboard.t.skills,
|
||||||
|
topic: dashboard.t.topic,
|
||||||
|
workspaceHint: dashboard.t.workspaceHint,
|
||||||
|
workspaceOutputs: dashboard.t.workspaceOutputs,
|
||||||
|
workspaceSearchNoResult: dashboard.t.workspaceSearchNoResult,
|
||||||
|
workspaceSearchPlaceholder: dashboard.t.workspaceSearchPlaceholder,
|
||||||
|
},
|
||||||
|
onRestartBot: dashboard.restartBot,
|
||||||
|
onToggleRuntimeMenu: () => dashboard.setRuntimeMenuOpen((prev) => !prev),
|
||||||
|
onOpenBaseConfig: dashboard.openBaseConfigModal,
|
||||||
|
onOpenParamConfig: dashboard.openParamConfigModal,
|
||||||
|
onOpenChannelConfig: dashboard.openChannelConfigModal,
|
||||||
|
onOpenTopicConfig: dashboard.openTopicConfigModal,
|
||||||
|
onOpenEnvParams: dashboard.openEnvParamsConfigModal,
|
||||||
|
onOpenSkills: dashboard.openSkillsConfigModal,
|
||||||
|
onOpenMcpConfig: dashboard.openMcpConfigModal,
|
||||||
|
onOpenCronJobs: dashboard.openCronJobsModal,
|
||||||
|
onOpenAgentFiles: dashboard.openAgentFilesModal,
|
||||||
|
onExportHistory: () => {
|
||||||
|
dashboard.setRuntimeMenuOpen(false);
|
||||||
|
dashboard.exportConversationJson();
|
||||||
|
},
|
||||||
|
onClearHistory: async () => {
|
||||||
|
dashboard.setRuntimeMenuOpen(false);
|
||||||
|
await dashboard.clearConversationHistory();
|
||||||
|
},
|
||||||
|
onRefreshWorkspace: () => dashboard.selectedBot ? dashboard.loadWorkspaceTree(dashboard.selectedBot.id, dashboard.workspaceCurrentPath) : undefined,
|
||||||
|
onWorkspaceQueryChange: dashboard.setWorkspaceQuery,
|
||||||
|
onWorkspaceQueryClear: () => dashboard.setWorkspaceQuery(''),
|
||||||
|
onWorkspaceQuerySearch: () => dashboard.setWorkspaceQuery((value) => value.trim()),
|
||||||
|
onToggleWorkspaceAutoRefresh: () => dashboard.setWorkspaceAutoRefresh((prev) => !prev),
|
||||||
|
onLoadWorkspaceTree: dashboard.loadWorkspaceTree,
|
||||||
|
onOpenWorkspaceFilePreview: dashboard.openWorkspaceFilePreview,
|
||||||
|
onShowWorkspaceHoverCard: dashboard.showWorkspaceHoverCard,
|
||||||
|
onHideWorkspaceHoverCard: dashboard.hideWorkspaceHoverCard,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dashboardModalStackProps = {
|
||||||
|
resourceMonitorModal: {
|
||||||
|
open: dashboard.showResourceModal,
|
||||||
|
botId: dashboard.resourceBotId,
|
||||||
|
resourceBot: dashboard.resourceBot,
|
||||||
|
resourceSnapshot: dashboard.resourceSnapshot,
|
||||||
|
resourceLoading: dashboard.resourceLoading,
|
||||||
|
resourceError: dashboard.resourceError,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
closeLabel: dashboard.t.close,
|
||||||
|
onClose: () => dashboard.setShowResourceModal(false),
|
||||||
|
onRefresh: dashboard.loadResourceSnapshot,
|
||||||
|
},
|
||||||
|
baseConfigModal: {
|
||||||
|
open: dashboard.showBaseModal,
|
||||||
|
selectedBotId: dashboard.selectedBot?.id || '',
|
||||||
|
editForm: dashboard.editForm,
|
||||||
|
paramDraft: dashboard.paramDraft,
|
||||||
|
baseImageOptions: dashboard.baseImageOptions,
|
||||||
|
systemTimezoneOptions: dashboard.systemTimezoneOptions,
|
||||||
|
defaultSystemTimezone: dashboard.defaultSystemTimezone,
|
||||||
|
passwordToggleLabels: dashboard.passwordToggleLabels,
|
||||||
|
isSaving: dashboard.isSaving,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
labels: {
|
||||||
|
accessPassword: dashboard.t.accessPassword,
|
||||||
|
accessPasswordPlaceholder: dashboard.t.accessPasswordPlaceholder,
|
||||||
|
baseConfig: dashboard.t.baseConfig,
|
||||||
|
baseImageReadonly: dashboard.t.baseImageReadonly,
|
||||||
|
botIdReadonly: dashboard.t.botIdReadonly,
|
||||||
|
botName: dashboard.t.botName,
|
||||||
|
botNamePlaceholder: dashboard.t.botNamePlaceholder,
|
||||||
|
cancel: dashboard.t.cancel,
|
||||||
|
close: dashboard.t.close,
|
||||||
|
save: dashboard.t.save,
|
||||||
|
},
|
||||||
|
onClose: () => dashboard.setShowBaseModal(false),
|
||||||
|
onEditFormChange: dashboard.updateEditForm,
|
||||||
|
onParamDraftChange: dashboard.updateParamDraft,
|
||||||
|
onSave: () => dashboard.saveBot('base'),
|
||||||
|
},
|
||||||
|
paramConfigModal: {
|
||||||
|
open: dashboard.showParamModal,
|
||||||
|
editForm: dashboard.editForm,
|
||||||
|
paramDraft: dashboard.paramDraft,
|
||||||
|
passwordToggleLabels: dashboard.passwordToggleLabels,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
isTestingProvider: dashboard.isTestingProvider,
|
||||||
|
providerTestResult: dashboard.providerTestResult,
|
||||||
|
isSaving: dashboard.isSaving,
|
||||||
|
labels: {
|
||||||
|
cancel: dashboard.t.cancel,
|
||||||
|
close: dashboard.t.close,
|
||||||
|
modelName: dashboard.t.modelName,
|
||||||
|
modelNamePlaceholder: dashboard.t.modelNamePlaceholder,
|
||||||
|
modelParams: dashboard.t.modelParams,
|
||||||
|
newApiKey: dashboard.t.newApiKey,
|
||||||
|
newApiKeyPlaceholder: dashboard.t.newApiKeyPlaceholder,
|
||||||
|
saveParams: dashboard.t.saveParams,
|
||||||
|
testModelConnection: dashboard.t.testModelConnection,
|
||||||
|
testing: dashboard.t.testing,
|
||||||
|
},
|
||||||
|
onClose: () => dashboard.setShowParamModal(false),
|
||||||
|
onEditFormChange: dashboard.updateEditForm,
|
||||||
|
onParamDraftChange: dashboard.updateParamDraft,
|
||||||
|
onProviderChange: dashboard.onBaseProviderChange,
|
||||||
|
onTestProviderConnection: dashboard.testProviderConnection,
|
||||||
|
onSave: () => dashboard.saveBot('params'),
|
||||||
|
},
|
||||||
|
channelConfigModal: dashboard.channelConfigModalProps,
|
||||||
|
topicConfigModal: dashboard.topicConfigModalProps,
|
||||||
|
skillsModal: dashboard.skillsModalProps,
|
||||||
|
skillMarketInstallModal: dashboard.skillMarketInstallModalProps,
|
||||||
|
mcpConfigModal: dashboard.mcpConfigModalProps,
|
||||||
|
envParamsModal: dashboard.envParamsModalProps,
|
||||||
|
cronJobsModal: dashboard.cronJobsModalProps,
|
||||||
|
templateManagerModal: {
|
||||||
|
open: dashboard.showTemplateModal,
|
||||||
|
templateTab: dashboard.templateTab,
|
||||||
|
templateAgentCount: dashboard.templateAgentCount,
|
||||||
|
templateTopicCount: dashboard.templateTopicCount,
|
||||||
|
templateAgentText: dashboard.templateAgentText,
|
||||||
|
templateTopicText: dashboard.templateTopicText,
|
||||||
|
isSavingTemplates: dashboard.isSavingTemplates,
|
||||||
|
labels: {
|
||||||
|
cancel: dashboard.t.cancel,
|
||||||
|
close: dashboard.t.close,
|
||||||
|
processing: dashboard.t.processing,
|
||||||
|
save: dashboard.t.save,
|
||||||
|
templateManagerTitle: dashboard.t.templateManagerTitle,
|
||||||
|
templateTabAgent: dashboard.t.templateTabAgent,
|
||||||
|
templateTabTopic: dashboard.t.templateTabTopic,
|
||||||
|
},
|
||||||
|
onClose: () => dashboard.setShowTemplateModal(false),
|
||||||
|
onTemplateTabChange: dashboard.setTemplateTab,
|
||||||
|
onTemplateAgentTextChange: dashboard.setTemplateAgentText,
|
||||||
|
onTemplateTopicTextChange: dashboard.setTemplateTopicText,
|
||||||
|
onSave: dashboard.saveTemplateManager,
|
||||||
|
},
|
||||||
|
agentFilesModal: {
|
||||||
|
open: dashboard.showAgentModal,
|
||||||
|
agentTab: dashboard.agentTab,
|
||||||
|
tabValue: String(dashboard.editForm[dashboard.agentFieldByTab[dashboard.agentTab]]),
|
||||||
|
isSaving: dashboard.isSaving,
|
||||||
|
labels: {
|
||||||
|
agentFiles: dashboard.t.agentFiles,
|
||||||
|
cancel: dashboard.t.cancel,
|
||||||
|
close: dashboard.t.close,
|
||||||
|
saveFiles: dashboard.t.saveFiles,
|
||||||
|
},
|
||||||
|
onClose: () => dashboard.setShowAgentModal(false),
|
||||||
|
onAgentTabChange: dashboard.setAgentTab,
|
||||||
|
onTabValueChange: (nextValue: string) => dashboard.updateAgentTabValue(dashboard.agentTab, nextValue),
|
||||||
|
onSave: () => dashboard.saveBot('agent'),
|
||||||
|
},
|
||||||
|
runtimeActionModal: {
|
||||||
|
open: dashboard.showRuntimeActionModal,
|
||||||
|
runtimeAction: dashboard.runtimeAction,
|
||||||
|
labels: {
|
||||||
|
close: dashboard.t.close,
|
||||||
|
lastAction: dashboard.t.lastAction,
|
||||||
|
},
|
||||||
|
onClose: () => dashboard.setShowRuntimeActionModal(false),
|
||||||
|
},
|
||||||
|
workspacePreviewModal: {
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
labels: {
|
||||||
|
cancel: dashboard.t.cancel,
|
||||||
|
close: dashboard.t.close,
|
||||||
|
copyAddress: dashboard.t.copyAddress,
|
||||||
|
download: dashboard.t.download,
|
||||||
|
editFile: dashboard.t.editFile,
|
||||||
|
filePreview: dashboard.t.filePreview,
|
||||||
|
fileTruncated: dashboard.t.fileTruncated,
|
||||||
|
save: dashboard.t.save,
|
||||||
|
},
|
||||||
|
preview: dashboard.workspacePreview,
|
||||||
|
previewFullscreen: dashboard.workspacePreviewFullscreen,
|
||||||
|
previewEditorEnabled: dashboard.workspacePreviewEditorEnabled,
|
||||||
|
previewCanEdit: dashboard.workspacePreviewCanEdit,
|
||||||
|
previewDraft: dashboard.workspacePreviewDraft,
|
||||||
|
previewSaving: dashboard.workspacePreviewSaving,
|
||||||
|
markdownComponents: dashboard.workspacePreviewMarkdownComponents,
|
||||||
|
onClose: dashboard.closeWorkspacePreview,
|
||||||
|
onToggleFullscreen: () => dashboard.setWorkspacePreviewFullscreen((prev) => !prev),
|
||||||
|
onCopyPreviewPath: dashboard.copyWorkspacePreviewPath,
|
||||||
|
onCopyPreviewUrl: dashboard.copyWorkspacePreviewUrl,
|
||||||
|
onPreviewDraftChange: dashboard.setWorkspacePreviewDraft,
|
||||||
|
onSavePreviewMarkdown: dashboard.saveWorkspacePreviewMarkdown,
|
||||||
|
onEnterEditMode: () => dashboard.setWorkspacePreviewMode('edit'),
|
||||||
|
onExitEditMode: () => {
|
||||||
|
dashboard.setWorkspacePreviewDraft(dashboard.workspacePreview?.content || '');
|
||||||
|
dashboard.setWorkspacePreviewMode('preview');
|
||||||
|
},
|
||||||
|
getWorkspaceDownloadHref: dashboard.getWorkspaceDownloadHref,
|
||||||
|
getWorkspaceRawHref: dashboard.getWorkspaceRawHref,
|
||||||
|
},
|
||||||
|
workspaceHoverCard: {
|
||||||
|
state: dashboard.workspaceHoverCard,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
formatWorkspaceTime,
|
||||||
|
formatBytes,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBotModalProps = {
|
||||||
|
open: dashboard.showCreateBotModal,
|
||||||
|
onClose: () => dashboard.setShowCreateBotModal(false),
|
||||||
|
onCreated: () => {
|
||||||
|
void dashboard.refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
compactMode: dashboard.compactMode,
|
||||||
|
hasForcedBot: dashboard.hasForcedBot,
|
||||||
|
showBotListPanel: dashboard.showBotListPanel,
|
||||||
|
botListPanelProps,
|
||||||
|
hasSelectedBot: Boolean(dashboard.selectedBot),
|
||||||
|
isCompactListPage: dashboard.isCompactListPage,
|
||||||
|
compactPanelTab: dashboard.compactPanelTab,
|
||||||
|
showCompactBotPageClose: dashboard.showCompactBotPageClose,
|
||||||
|
forcedBotId: dashboard.forcedBotId,
|
||||||
|
selectBotText: dashboard.forcedBotMissing ? `${dashboard.t.selectBot}: ${String(dashboard.forcedBotId).trim()}` : dashboard.t.selectBot,
|
||||||
|
isZh: dashboard.isZh,
|
||||||
|
runtimeViewMode: dashboard.runtimeViewMode,
|
||||||
|
hasTopicUnread: dashboard.hasTopicUnread,
|
||||||
|
onRuntimeViewModeChange: dashboard.setRuntimeViewMode,
|
||||||
|
topicFeedPanelProps,
|
||||||
|
dashboardChatPanelProps,
|
||||||
|
runtimePanelProps,
|
||||||
|
onCompactClose: () => {
|
||||||
|
dashboard.setSelectedBotId('');
|
||||||
|
dashboard.setCompactPanelTab('chat');
|
||||||
|
},
|
||||||
|
dashboardModalStackProps,
|
||||||
|
createBotModalProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,10 @@ export function PlatformBotManagementPage({ compactMode }: PlatformBotManagement
|
||||||
isZh={dashboard.isZh}
|
isZh={dashboard.isZh}
|
||||||
lastActionPreview={dashboard.lastActionPreview}
|
lastActionPreview={dashboard.lastActionPreview}
|
||||||
operatingBotId={dashboard.operatingBotId}
|
operatingBotId={dashboard.operatingBotId}
|
||||||
|
selectedBotEnabledChannels={dashboard.selectedBotEnabledChannels}
|
||||||
selectedBotInfo={dashboard.selectedBotInfo}
|
selectedBotInfo={dashboard.selectedBotInfo}
|
||||||
|
selectedBotMcpCount={dashboard.selectedBotMcpCount}
|
||||||
|
selectedBotSkillCount={dashboard.selectedBotSkillCount}
|
||||||
selectedBotUsageSummary={dashboard.selectedBotUsageSummary}
|
selectedBotUsageSummary={dashboard.selectedBotUsageSummary}
|
||||||
onClearDashboardDirectSession={dashboard.clearDashboardDirectSession}
|
onClearDashboardDirectSession={dashboard.clearDashboardDirectSession}
|
||||||
onOpenBotPanel={dashboard.openBotPanel}
|
onOpenBotPanel={dashboard.openBotPanel}
|
||||||
|
|
|
||||||
|
|
@ -624,8 +624,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-selected-bot-name {
|
.platform-selected-bot-name {
|
||||||
font-size: 26px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
line-height: 1.3;
|
||||||
|
font-weight: 700;
|
||||||
color: var(--title);
|
color: var(--title);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -643,14 +644,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-selected-bot-name-id {
|
.platform-selected-bot-name-id {
|
||||||
font-size: 0.78em;
|
font-size: 0.74em;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-selected-bot-grid {
|
.platform-selected-bot-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 12px 14px;
|
gap: 12px 14px;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
@ -668,15 +669,19 @@
|
||||||
|
|
||||||
.platform-selected-bot-info-label {
|
.platform-selected-bot-info-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-selected-bot-info-value {
|
.platform-selected-bot-info-value {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 600;
|
||||||
color: var(--title);
|
color: var(--title);
|
||||||
overflow: hidden;
|
white-space: normal;
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
white-space: nowrap;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-selected-bot-last-row {
|
.platform-selected-bot-last-row {
|
||||||
|
|
@ -702,6 +707,8 @@
|
||||||
|
|
||||||
.platform-selected-bot-last-preview {
|
.platform-selected-bot-last-preview {
|
||||||
display: block;
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
@ -1264,6 +1271,22 @@
|
||||||
max-width: 460px;
|
max-width: 460px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.platform-login-log-toolbar {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-login-log-search {
|
||||||
|
flex: 1 1 420px;
|
||||||
|
min-width: min(420px, 100%);
|
||||||
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-login-log-select {
|
||||||
|
flex: 0 0 180px;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.platform-settings-table-wrap {
|
.platform-settings-table-wrap {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
@ -1308,6 +1331,19 @@
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.platform-login-log-search {
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-login-log-select {
|
||||||
|
flex: 1 1 180px;
|
||||||
|
width: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.platform-template-shell {
|
.platform-template-shell {
|
||||||
max-width: min(1400px, 96vw);
|
max-width: min(1400px, 96vw);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,9 @@ export function PlatformLoginLogPage({ isZh }: PlatformLoginLogPageProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="platform-settings-toolbar">
|
<div className="platform-settings-toolbar platform-login-log-toolbar">
|
||||||
<ProtectedSearchInput
|
<ProtectedSearchInput
|
||||||
className="platform-searchbar platform-settings-search"
|
className="platform-searchbar platform-settings-search platform-login-log-search"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
onClear={() => setSearch('')}
|
onClear={() => setSearch('')}
|
||||||
|
|
@ -128,12 +128,20 @@ export function PlatformLoginLogPage({ isZh }: PlatformLoginLogPageProps) {
|
||||||
clearTitle={isZh ? '清除搜索' : 'Clear search'}
|
clearTitle={isZh ? '清除搜索' : 'Clear search'}
|
||||||
searchTitle={isZh ? '搜索' : 'Search'}
|
searchTitle={isZh ? '搜索' : 'Search'}
|
||||||
/>
|
/>
|
||||||
<LucentSelect value={authType} onChange={(event) => setAuthType(event.target.value as 'all' | 'panel' | 'bot')}>
|
<LucentSelect
|
||||||
|
wrapperClassName="platform-login-log-select"
|
||||||
|
value={authType}
|
||||||
|
onChange={(event) => setAuthType(event.target.value as 'all' | 'panel' | 'bot')}
|
||||||
|
>
|
||||||
<option value="all">{isZh ? '全部类型' : 'All Types'}</option>
|
<option value="all">{isZh ? '全部类型' : 'All Types'}</option>
|
||||||
<option value="panel">{isZh ? 'Panel' : 'Panel'}</option>
|
<option value="panel">{isZh ? 'Panel' : 'Panel'}</option>
|
||||||
<option value="bot">{isZh ? 'Bot' : 'Bot'}</option>
|
<option value="bot">{isZh ? 'Bot' : 'Bot'}</option>
|
||||||
</LucentSelect>
|
</LucentSelect>
|
||||||
<LucentSelect value={status} onChange={(event) => setStatus(event.target.value as 'all' | 'active' | 'revoked')}>
|
<LucentSelect
|
||||||
|
wrapperClassName="platform-login-log-select"
|
||||||
|
value={status}
|
||||||
|
onChange={(event) => setStatus(event.target.value as 'all' | 'active' | 'revoked')}
|
||||||
|
>
|
||||||
<option value="all">{isZh ? '全部状态' : 'All Statuses'}</option>
|
<option value="all">{isZh ? '全部状态' : 'All Statuses'}</option>
|
||||||
<option value="active">{isZh ? '活跃中' : 'Active'}</option>
|
<option value="active">{isZh ? '活跃中' : 'Active'}</option>
|
||||||
<option value="revoked">{isZh ? '已失效' : 'Revoked'}</option>
|
<option value="revoked">{isZh ? '已失效' : 'Revoked'}</option>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ interface PlatformBotOverviewSectionProps {
|
||||||
isZh: boolean;
|
isZh: boolean;
|
||||||
lastActionPreview: string;
|
lastActionPreview: string;
|
||||||
operatingBotId: string;
|
operatingBotId: string;
|
||||||
|
selectedBotEnabledChannels: string[];
|
||||||
selectedBotInfo?: BotState;
|
selectedBotInfo?: BotState;
|
||||||
|
selectedBotMcpCount: number;
|
||||||
|
selectedBotSkillCount: number;
|
||||||
selectedBotUsageSummary: PlatformUsageResponse['summary'] | null;
|
selectedBotUsageSummary: PlatformUsageResponse['summary'] | null;
|
||||||
onClearDashboardDirectSession: (bot: BotState) => Promise<void> | void;
|
onClearDashboardDirectSession: (bot: BotState) => Promise<void> | void;
|
||||||
onOpenBotPanel: (botId: string) => void;
|
onOpenBotPanel: (botId: string) => void;
|
||||||
|
|
@ -18,12 +21,21 @@ interface PlatformBotOverviewSectionProps {
|
||||||
onRemoveBot: (bot: BotState) => Promise<void> | void;
|
onRemoveBot: (bot: BotState) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatChannelTypeLabel(channelType: string) {
|
||||||
|
const normalized = String(channelType || '').trim().toLowerCase();
|
||||||
|
if (!normalized) return '-';
|
||||||
|
return normalized.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
export function PlatformBotOverviewSection({
|
export function PlatformBotOverviewSection({
|
||||||
compactSheet = false,
|
compactSheet = false,
|
||||||
isZh,
|
isZh,
|
||||||
lastActionPreview,
|
lastActionPreview,
|
||||||
operatingBotId,
|
operatingBotId,
|
||||||
|
selectedBotEnabledChannels,
|
||||||
selectedBotInfo,
|
selectedBotInfo,
|
||||||
|
selectedBotMcpCount,
|
||||||
|
selectedBotSkillCount,
|
||||||
selectedBotUsageSummary,
|
selectedBotUsageSummary,
|
||||||
onClearDashboardDirectSession,
|
onClearDashboardDirectSession,
|
||||||
onOpenBotPanel,
|
onOpenBotPanel,
|
||||||
|
|
@ -31,6 +43,14 @@ export function PlatformBotOverviewSection({
|
||||||
onOpenResourceMonitor,
|
onOpenResourceMonitor,
|
||||||
onRemoveBot,
|
onRemoveBot,
|
||||||
}: PlatformBotOverviewSectionProps) {
|
}: PlatformBotOverviewSectionProps) {
|
||||||
|
const enabledChannelSummary = selectedBotEnabledChannels.length > 0
|
||||||
|
? selectedBotEnabledChannels.map(formatChannelTypeLabel).join(', ')
|
||||||
|
: (isZh ? '未启用外部渠道' : 'No external channels enabled');
|
||||||
|
|
||||||
|
const skillMcpSummary = isZh
|
||||||
|
? `技能 ${selectedBotSkillCount} / MCP ${selectedBotMcpCount}`
|
||||||
|
: `Skills ${selectedBotSkillCount} / MCP ${selectedBotMcpCount}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${compactSheet ? 'platform-compact-overview' : 'panel stack'}`}>
|
<section className={`${compactSheet ? 'platform-compact-overview' : 'panel stack'}`}>
|
||||||
<div className={compactSheet ? 'platform-compact-overview-head' : undefined}>
|
<div className={compactSheet ? 'platform-compact-overview-head' : undefined}>
|
||||||
|
|
@ -108,11 +128,16 @@ export function PlatformBotOverviewSection({
|
||||||
<div className="platform-selected-bot-grid">
|
<div className="platform-selected-bot-grid">
|
||||||
<div className="platform-selected-bot-info">
|
<div className="platform-selected-bot-info">
|
||||||
<span className="platform-selected-bot-info-label">{isZh ? '镜像' : 'Image'}</span>
|
<span className="platform-selected-bot-info-label">{isZh ? '镜像' : 'Image'}</span>
|
||||||
<span className="mono platform-selected-bot-info-value">{selectedBotInfo.image_tag || '-'}</span>
|
<span className="mono platform-selected-bot-info-value" title={selectedBotInfo.image_tag || '-'}>
|
||||||
|
{selectedBotInfo.image_tag || '-'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="platform-selected-bot-info">
|
<div className="platform-selected-bot-info">
|
||||||
<span className="platform-selected-bot-info-label">{isZh ? 'Provider / 模型' : 'Provider / Model'}</span>
|
<span className="platform-selected-bot-info-label">{isZh ? 'Provider / 模型' : 'Provider / Model'}</span>
|
||||||
<span className="mono platform-selected-bot-info-value">
|
<span
|
||||||
|
className="mono platform-selected-bot-info-value"
|
||||||
|
title={`${selectedBotInfo.llm_provider || '-'} / ${selectedBotInfo.llm_model || '-'}`}
|
||||||
|
>
|
||||||
{selectedBotInfo.llm_provider || '-'} / {selectedBotInfo.llm_model || '-'}
|
{selectedBotInfo.llm_provider || '-'} / {selectedBotInfo.llm_model || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,9 +145,24 @@ export function PlatformBotOverviewSection({
|
||||||
<span className="platform-selected-bot-info-label">{isZh ? 'Bot 状态' : 'Bot State'}</span>
|
<span className="platform-selected-bot-info-label">{isZh ? 'Bot 状态' : 'Bot State'}</span>
|
||||||
<span className="platform-selected-bot-info-value">{selectedBotInfo.current_state || 'IDLE'}</span>
|
<span className="platform-selected-bot-info-value">{selectedBotInfo.current_state || 'IDLE'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="platform-selected-bot-info">
|
||||||
|
<span className="platform-selected-bot-info-label">{isZh ? '已启用渠道' : 'Enabled Channels'}</span>
|
||||||
|
<span className="platform-selected-bot-info-value" title={enabledChannelSummary}>
|
||||||
|
{enabledChannelSummary}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="platform-selected-bot-info">
|
||||||
|
<span className="platform-selected-bot-info-label">{isZh ? '已启用技能 / MCP' : 'Enabled Skills / MCP'}</span>
|
||||||
|
<span className="platform-selected-bot-info-value">{skillMcpSummary}</span>
|
||||||
|
</div>
|
||||||
<div className="platform-selected-bot-info">
|
<div className="platform-selected-bot-info">
|
||||||
<span className="platform-selected-bot-info-label">{isZh ? 'Token 用量合计' : 'Token Usage Summary'}</span>
|
<span className="platform-selected-bot-info-label">{isZh ? 'Token 用量合计' : 'Token Usage Summary'}</span>
|
||||||
<span className="platform-selected-bot-info-value">
|
<span
|
||||||
|
className="platform-selected-bot-info-value"
|
||||||
|
title={isZh
|
||||||
|
? `请求 ${selectedBotUsageSummary?.request_count || 0} / 输入 ${selectedBotUsageSummary?.input_tokens || 0} / 输出 ${selectedBotUsageSummary?.output_tokens || 0} / 总计 ${selectedBotUsageSummary?.total_tokens || 0}`
|
||||||
|
: `Req ${selectedBotUsageSummary?.request_count || 0} / In ${selectedBotUsageSummary?.input_tokens || 0} / Out ${selectedBotUsageSummary?.output_tokens || 0} / Total ${selectedBotUsageSummary?.total_tokens || 0}`}
|
||||||
|
>
|
||||||
{isZh
|
{isZh
|
||||||
? `请求 ${selectedBotUsageSummary?.request_count || 0} / 输入 ${selectedBotUsageSummary?.input_tokens || 0} / 输出 ${selectedBotUsageSummary?.output_tokens || 0} / 总计 ${selectedBotUsageSummary?.total_tokens || 0}`
|
? `请求 ${selectedBotUsageSummary?.request_count || 0} / 输入 ${selectedBotUsageSummary?.input_tokens || 0} / 输出 ${selectedBotUsageSummary?.output_tokens || 0} / 总计 ${selectedBotUsageSummary?.total_tokens || 0}`
|
||||||
: `Req ${selectedBotUsageSummary?.request_count || 0} / In ${selectedBotUsageSummary?.input_tokens || 0} / Out ${selectedBotUsageSummary?.output_tokens || 0} / Total ${selectedBotUsageSummary?.total_tokens || 0}`}
|
: `Req ${selectedBotUsageSummary?.request_count || 0} / In ${selectedBotUsageSummary?.input_tokens || 0} / Out ${selectedBotUsageSummary?.output_tokens || 0} / Total ${selectedBotUsageSummary?.total_tokens || 0}`}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
||||||
|
|
@ -6,317 +6,75 @@ import { APP_ENDPOINTS } from '../../../config/env';
|
||||||
import { sortBotsByCreatedAtDesc } from '../../../shared/bot/sortBots';
|
import { sortBotsByCreatedAtDesc } from '../../../shared/bot/sortBots';
|
||||||
import { useAppStore } from '../../../store/appStore';
|
import { useAppStore } from '../../../store/appStore';
|
||||||
import type { BotState } from '../../../types/bot';
|
import type { BotState } from '../../../types/bot';
|
||||||
import {
|
import { buildBotPanelHref } from '../utils';
|
||||||
normalizePlatformPageSize,
|
import { usePlatformManagementState } from './usePlatformManagementState';
|
||||||
readCachedPlatformPageSize,
|
import { usePlatformOverviewState } from './usePlatformOverviewState';
|
||||||
writeCachedPlatformPageSize,
|
|
||||||
} from '../../../utils/platformPageSize';
|
|
||||||
import type {
|
|
||||||
BotActivityStatsItem,
|
|
||||||
PlatformBotResourceSnapshot,
|
|
||||||
PlatformOverviewResponse,
|
|
||||||
PlatformUsageAnalyticsSeriesItem,
|
|
||||||
PlatformUsageResponse,
|
|
||||||
} from '../types';
|
|
||||||
import {
|
|
||||||
buildBotPanelHref,
|
|
||||||
buildPlatformUsageAnalyticsSeries,
|
|
||||||
buildPlatformUsageAnalyticsTicks,
|
|
||||||
getPlatformChartCeiling,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
interface UsePlatformDashboardOptions {
|
interface UsePlatformDashboardOptions {
|
||||||
compactMode: boolean;
|
compactMode: boolean;
|
||||||
mode?: 'admin' | 'management';
|
mode?: 'admin' | 'management';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RequestErrorShape {
|
||||||
|
response?: {
|
||||||
|
data?: {
|
||||||
|
detail?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequestErrorDetail(error: unknown): string {
|
||||||
|
const resolvedError = (error && typeof error === 'object' ? error : {}) as RequestErrorShape;
|
||||||
|
return String(resolvedError.response?.data?.detail || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function usePlatformDashboard({ compactMode, mode = 'management' }: UsePlatformDashboardOptions) {
|
export function usePlatformDashboard({ compactMode, mode = 'management' }: UsePlatformDashboardOptions) {
|
||||||
const { activeBots, setBots, updateBotStatus, locale } = useAppStore();
|
const { activeBots, setBots, updateBotStatus, locale } = useAppStore();
|
||||||
const { notify, confirm } = useLucentPrompt();
|
const { notify, confirm } = useLucentPrompt();
|
||||||
const isZh = locale === 'zh';
|
const isZh = locale === 'zh';
|
||||||
const isAdminMode = mode === 'admin';
|
const isAdminMode = mode === 'admin';
|
||||||
const isManagementMode = mode === 'management';
|
const isManagementMode = mode === 'management';
|
||||||
const [overview, setOverview] = useState<PlatformOverviewResponse | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [selectedBotId, setSelectedBotId] = useState('');
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [operatingBotId, setOperatingBotId] = useState('');
|
const [operatingBotId, setOperatingBotId] = useState('');
|
||||||
const [showBotLastActionModal, setShowBotLastActionModal] = useState(false);
|
|
||||||
const [showResourceModal, setShowResourceModal] = useState(false);
|
|
||||||
const [selectedBotDetail, setSelectedBotDetail] = useState<BotState | null>(null);
|
|
||||||
const [selectedBotUsageSummary, setSelectedBotUsageSummary] = useState<PlatformUsageResponse['summary'] | null>(null);
|
|
||||||
const [resourceBotId, setResourceBotId] = useState('');
|
|
||||||
const [resourceSnapshot, setResourceSnapshot] = useState<PlatformBotResourceSnapshot | null>(null);
|
|
||||||
const [resourceLoading, setResourceLoading] = useState(false);
|
|
||||||
const [resourceError, setResourceError] = useState('');
|
|
||||||
const [usageData, setUsageData] = useState<PlatformUsageResponse | null>(null);
|
|
||||||
const [usageLoading, setUsageLoading] = useState(false);
|
|
||||||
const [activityStatsData, setActivityStatsData] = useState<BotActivityStatsItem[] | null>(null);
|
|
||||||
const [activityLoading, setActivityLoading] = useState(false);
|
|
||||||
const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10));
|
|
||||||
const [botListPage, setBotListPage] = useState(1);
|
|
||||||
const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10));
|
|
||||||
const [showCompactBotSheet, setShowCompactBotSheet] = useState(false);
|
|
||||||
const [compactSheetClosing, setCompactSheetClosing] = useState(false);
|
|
||||||
const [compactSheetMounted, setCompactSheetMounted] = useState(false);
|
|
||||||
const compactSheetTimerRef = useRef<number | null>(null);
|
|
||||||
|
|
||||||
const botList = useMemo(() => {
|
const overviewState = usePlatformOverviewState({
|
||||||
return sortBotsByCreatedAtDesc(Object.values(activeBots)) as BotState[];
|
isAdminMode,
|
||||||
}, [activeBots]);
|
isZh,
|
||||||
|
notify,
|
||||||
|
});
|
||||||
|
|
||||||
const filteredBots = useMemo(() => {
|
const botList = useMemo(
|
||||||
const keyword = search.trim().toLowerCase();
|
() => sortBotsByCreatedAtDesc(Object.values(activeBots)) as BotState[],
|
||||||
if (!keyword) return botList;
|
[activeBots],
|
||||||
return botList.filter((bot) => `${bot.name} ${bot.id}`.toLowerCase().includes(keyword));
|
|
||||||
}, [botList, search]);
|
|
||||||
|
|
||||||
const botListPageCount = useMemo(
|
|
||||||
() => Math.max(1, Math.ceil(filteredBots.length / botListPageSize)),
|
|
||||||
[filteredBots.length, botListPageSize],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const pagedBots = useMemo(() => {
|
const management = usePlatformManagementState({
|
||||||
const page = Math.min(Math.max(1, botListPage), botListPageCount);
|
botList,
|
||||||
const start = (page - 1) * botListPageSize;
|
compactMode,
|
||||||
return filteredBots.slice(start, start + botListPageSize);
|
isManagementMode,
|
||||||
}, [filteredBots, botListPage, botListPageCount, botListPageSize]);
|
isZh,
|
||||||
|
platformPageSize: overviewState.platformPageSize,
|
||||||
const selectedBot = useMemo(
|
});
|
||||||
() => (selectedBotId ? botList.find((bot) => bot.id === selectedBotId) : undefined),
|
|
||||||
[botList, selectedBotId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadBots = useCallback(async () => {
|
const loadBots = useCallback(async () => {
|
||||||
const res = await axios.get<BotState[]>(`${APP_ENDPOINTS.apiBase}/bots`);
|
const res = await axios.get<BotState[]>(`${APP_ENDPOINTS.apiBase}/bots`);
|
||||||
setBots(res.data);
|
setBots(res.data);
|
||||||
}, [setBots]);
|
}, [setBots]);
|
||||||
|
|
||||||
const loadOverview = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await axios.get<PlatformOverviewResponse>(`${APP_ENDPOINTS.apiBase}/platform/overview`);
|
|
||||||
setOverview(res.data);
|
|
||||||
const normalizedPageSize = normalizePlatformPageSize(
|
|
||||||
res.data?.settings?.page_size,
|
|
||||||
readCachedPlatformPageSize(10),
|
|
||||||
);
|
|
||||||
writeCachedPlatformPageSize(normalizedPageSize);
|
|
||||||
setUsagePageSize(normalizedPageSize);
|
|
||||||
setBotListPageSize(normalizedPageSize);
|
|
||||||
} catch (error: any) {
|
|
||||||
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [isZh, notify]);
|
|
||||||
|
|
||||||
const loadUsage = useCallback(async (page = 1) => {
|
|
||||||
setUsageLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await axios.get<PlatformUsageResponse>(`${APP_ENDPOINTS.apiBase}/platform/usage`, {
|
|
||||||
params: {
|
|
||||||
limit: usagePageSize,
|
|
||||||
offset: Math.max(0, page - 1) * usagePageSize,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setUsageData(res.data);
|
|
||||||
} catch (error: any) {
|
|
||||||
notify(error?.response?.data?.detail || (isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' });
|
|
||||||
} finally {
|
|
||||||
setUsageLoading(false);
|
|
||||||
}
|
|
||||||
}, [isZh, notify, usagePageSize]);
|
|
||||||
|
|
||||||
const loadActivityStats = useCallback(async () => {
|
|
||||||
setActivityLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await axios.get<{ items: BotActivityStatsItem[] }>(`${APP_ENDPOINTS.apiBase}/platform/activity-stats`);
|
|
||||||
setActivityStatsData(Array.isArray(res.data?.items) ? res.data.items : []);
|
|
||||||
} catch (error: any) {
|
|
||||||
notify(error?.response?.data?.detail || (isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' });
|
|
||||||
} finally {
|
|
||||||
setActivityLoading(false);
|
|
||||||
}
|
|
||||||
}, [isZh, notify]);
|
|
||||||
|
|
||||||
const loadSelectedBotUsageSummary = useCallback(async (botId: string) => {
|
|
||||||
if (!botId) {
|
|
||||||
setSelectedBotUsageSummary(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await axios.get<PlatformUsageResponse>(`${APP_ENDPOINTS.apiBase}/platform/usage`, {
|
|
||||||
params: {
|
|
||||||
bot_id: botId,
|
|
||||||
limit: 1,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setSelectedBotUsageSummary(res.data?.summary || null);
|
|
||||||
} catch {
|
|
||||||
setSelectedBotUsageSummary(null);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadResourceSnapshot = useCallback(async (botId: string) => {
|
|
||||||
if (!botId) return;
|
|
||||||
setResourceLoading(true);
|
|
||||||
setResourceError('');
|
|
||||||
try {
|
|
||||||
const res = await axios.get<PlatformBotResourceSnapshot>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/resources`);
|
|
||||||
setResourceSnapshot(res.data);
|
|
||||||
} catch (error: any) {
|
|
||||||
const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.');
|
|
||||||
setResourceError(String(msg));
|
|
||||||
} finally {
|
|
||||||
setResourceLoading(false);
|
|
||||||
}
|
|
||||||
}, [isZh]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void loadOverview();
|
|
||||||
}, [loadOverview]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAdminMode) {
|
|
||||||
setUsageData(null);
|
|
||||||
setUsageLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void loadUsage(1);
|
|
||||||
}, [isAdminMode, loadUsage, usagePageSize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isAdminMode) {
|
|
||||||
setActivityStatsData(null);
|
|
||||||
setActivityLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void loadActivityStats();
|
|
||||||
}, [isAdminMode, loadActivityStats]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManagementMode) return;
|
|
||||||
setBotListPage(1);
|
|
||||||
}, [botListPageSize, isManagementMode, search]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManagementMode) return;
|
|
||||||
setBotListPage((prev) => Math.min(Math.max(prev, 1), botListPageCount));
|
|
||||||
}, [botListPageCount, isManagementMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManagementMode) return;
|
|
||||||
if (!selectedBotId && filteredBots[0]?.id) setSelectedBotId(filteredBots[0].id);
|
|
||||||
}, [filteredBots, isManagementMode, selectedBotId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManagementMode) {
|
|
||||||
setShowCompactBotSheet(false);
|
|
||||||
setCompactSheetClosing(false);
|
|
||||||
setCompactSheetMounted(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!compactMode) {
|
|
||||||
setShowCompactBotSheet(false);
|
|
||||||
setCompactSheetClosing(false);
|
|
||||||
setCompactSheetMounted(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selectedBotId && showCompactBotSheet) return;
|
|
||||||
if (!selectedBotId) setShowCompactBotSheet(false);
|
|
||||||
}, [compactMode, isManagementMode, selectedBotId, showCompactBotSheet]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManagementMode) {
|
|
||||||
setSelectedBotDetail(null);
|
|
||||||
setSelectedBotUsageSummary(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!selectedBotId) {
|
|
||||||
setSelectedBotDetail(null);
|
|
||||||
setSelectedBotUsageSummary(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let alive = true;
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get<BotState>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(selectedBotId)}`);
|
|
||||||
if (alive) {
|
|
||||||
setSelectedBotDetail(res.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (alive) {
|
|
||||||
setSelectedBotDetail(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
void loadSelectedBotUsageSummary(selectedBotId);
|
|
||||||
return () => {
|
|
||||||
alive = false;
|
|
||||||
};
|
|
||||||
}, [isManagementMode, loadSelectedBotUsageSummary, selectedBotId]);
|
|
||||||
|
|
||||||
const resourceBot = useMemo(
|
|
||||||
() => (resourceBotId ? botList.find((bot) => bot.id === resourceBotId) : undefined),
|
|
||||||
[botList, resourceBotId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedBotInfo = useMemo(() => {
|
|
||||||
if (selectedBotDetail && selectedBotDetail.id === selectedBotId) {
|
|
||||||
return {
|
|
||||||
...selectedBot,
|
|
||||||
...selectedBotDetail,
|
|
||||||
logs: (selectedBotDetail.logs && selectedBotDetail.logs.length > 0)
|
|
||||||
? selectedBotDetail.logs
|
|
||||||
: (selectedBot?.logs || []),
|
|
||||||
messages: (selectedBotDetail.messages && selectedBotDetail.messages.length > 0)
|
|
||||||
? selectedBotDetail.messages
|
|
||||||
: (selectedBot?.messages || []),
|
|
||||||
events: (selectedBotDetail.events && selectedBotDetail.events.length > 0)
|
|
||||||
? selectedBotDetail.events
|
|
||||||
: (selectedBot?.events || []),
|
|
||||||
} as BotState;
|
|
||||||
}
|
|
||||||
return selectedBot;
|
|
||||||
}, [selectedBot, selectedBotDetail, selectedBotId]);
|
|
||||||
|
|
||||||
const lastActionPreview = useMemo(
|
|
||||||
() => selectedBotInfo?.last_action?.trim() || '',
|
|
||||||
[selectedBotInfo?.last_action],
|
|
||||||
);
|
|
||||||
|
|
||||||
const overviewBots = overview?.summary.bots;
|
|
||||||
const overviewImages = overview?.summary.images;
|
|
||||||
const overviewResources = overview?.summary.resources;
|
|
||||||
const activityStats = activityStatsData || overview?.activity_stats;
|
|
||||||
const usageSummary = usageData?.summary || overview?.usage.summary;
|
|
||||||
const usageAnalytics = usageData?.analytics || overview?.usage.analytics || null;
|
|
||||||
|
|
||||||
const usageAnalyticsSeries = useMemo<PlatformUsageAnalyticsSeriesItem[]>(
|
|
||||||
() => buildPlatformUsageAnalyticsSeries(usageAnalytics, isZh),
|
|
||||||
[isZh, usageAnalytics],
|
|
||||||
);
|
|
||||||
|
|
||||||
const usageAnalyticsMax = useMemo(() => {
|
|
||||||
const maxDailyRequests = usageAnalyticsSeries.reduce(
|
|
||||||
(max, item) => Math.max(max, ...item.daily_counts.map((count) => Number(count || 0))),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
return getPlatformChartCeiling(maxDailyRequests);
|
|
||||||
}, [usageAnalyticsSeries]);
|
|
||||||
|
|
||||||
const usageAnalyticsTicks = useMemo(() => buildPlatformUsageAnalyticsTicks(usageAnalyticsMax), [usageAnalyticsMax]);
|
|
||||||
|
|
||||||
const refreshAll = useCallback(async () => {
|
const refreshAll = useCallback(async () => {
|
||||||
const jobs: Promise<unknown>[] = [loadOverview(), loadBots()];
|
const jobs: Promise<unknown>[] = [overviewState.loadOverview(), loadBots()];
|
||||||
if (isAdminMode) {
|
if (isAdminMode) {
|
||||||
jobs.push(loadUsage(), loadActivityStats());
|
jobs.push(overviewState.loadUsage(), overviewState.loadActivityStats());
|
||||||
|
}
|
||||||
|
if (management.selectedBotId) {
|
||||||
|
jobs.push(management.refreshSelectedBot());
|
||||||
}
|
}
|
||||||
if (selectedBotId) jobs.push(loadSelectedBotUsageSummary(selectedBotId));
|
|
||||||
await Promise.allSettled(jobs);
|
await Promise.allSettled(jobs);
|
||||||
}, [isAdminMode, loadActivityStats, loadBots, loadOverview, loadSelectedBotUsageSummary, loadUsage, selectedBotId]);
|
}, [
|
||||||
|
isAdminMode,
|
||||||
|
loadBots,
|
||||||
|
management,
|
||||||
|
overviewState,
|
||||||
|
]);
|
||||||
|
|
||||||
const toggleBot = useCallback(async (bot: BotState) => {
|
const toggleBot = useCallback(async (bot: BotState) => {
|
||||||
setOperatingBotId(bot.id);
|
setOperatingBotId(bot.id);
|
||||||
|
|
@ -329,8 +87,8 @@ export function usePlatformDashboard({ compactMode, mode = 'management' }: UsePl
|
||||||
updateBotStatus(bot.id, 'RUNNING');
|
updateBotStatus(bot.id, 'RUNNING');
|
||||||
}
|
}
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
notify(error?.response?.data?.detail || (isZh ? 'Bot 操作失败。' : 'Bot action failed.'), { tone: 'error' });
|
notify(getRequestErrorDetail(error) || (isZh ? 'Bot 操作失败。' : 'Bot action failed.'), { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setOperatingBotId('');
|
setOperatingBotId('');
|
||||||
}
|
}
|
||||||
|
|
@ -341,8 +99,8 @@ export function usePlatformDashboard({ compactMode, mode = 'management' }: UsePl
|
||||||
try {
|
try {
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${bot.id}/${enabled ? 'enable' : 'disable'}`);
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${bot.id}/${enabled ? 'enable' : 'disable'}`);
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
notify(error?.response?.data?.detail || (isZh ? '更新 Bot 状态失败。' : 'Failed to update bot status.'), { tone: 'error' });
|
notify(getRequestErrorDetail(error) || (isZh ? '更新 Bot 状态失败。' : 'Failed to update bot status.'), { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setOperatingBotId('');
|
setOperatingBotId('');
|
||||||
}
|
}
|
||||||
|
|
@ -363,19 +121,19 @@ export function usePlatformDashboard({ compactMode, mode = 'management' }: UsePl
|
||||||
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}`, {
|
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}`, {
|
||||||
params: { delete_workspace: true },
|
params: { delete_workspace: true },
|
||||||
});
|
});
|
||||||
if (selectedBotId === targetId) {
|
if (management.selectedBotId === targetId) {
|
||||||
setSelectedBotId('');
|
management.setSelectedBotId('');
|
||||||
setSelectedBotDetail(null);
|
management.setSelectedBotDetail(null);
|
||||||
setShowBotLastActionModal(false);
|
management.setShowBotLastActionModal(false);
|
||||||
}
|
}
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
notify(isZh ? 'Bot 已删除。' : 'Bot deleted.', { tone: 'success' });
|
notify(isZh ? 'Bot 已删除。' : 'Bot deleted.', { tone: 'success' });
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
notify(error?.response?.data?.detail || (isZh ? '删除 Bot 失败。' : 'Failed to delete bot.'), { tone: 'error' });
|
notify(getRequestErrorDetail(error) || (isZh ? '删除 Bot 失败。' : 'Failed to delete bot.'), { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setOperatingBotId('');
|
setOperatingBotId('');
|
||||||
}
|
}
|
||||||
}, [confirm, isZh, notify, refreshAll, selectedBotId]);
|
}, [confirm, isZh, management, notify, refreshAll]);
|
||||||
|
|
||||||
const clearDashboardDirectSession = useCallback(async (bot: BotState) => {
|
const clearDashboardDirectSession = useCallback(async (bot: BotState) => {
|
||||||
const targetId = String(bot.id || '').trim();
|
const targetId = String(bot.id || '').trim();
|
||||||
|
|
@ -395,131 +153,69 @@ export function usePlatformDashboard({ compactMode, mode = 'management' }: UsePl
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}/sessions/dashboard-direct/clear`);
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}/sessions/dashboard-direct/clear`);
|
||||||
notify(isZh ? '面板 Session 已清空。' : 'Dashboard session cleared.', { tone: 'success' });
|
notify(isZh ? '面板 Session 已清空。' : 'Dashboard session cleared.', { tone: 'success' });
|
||||||
await refreshAll();
|
await refreshAll();
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
notify(error?.response?.data?.detail || (isZh ? '清空面板 Session 失败。' : 'Failed to clear dashboard session.'), { tone: 'error' });
|
notify(getRequestErrorDetail(error) || (isZh ? '清空面板 Session 失败。' : 'Failed to clear dashboard session.'), { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setOperatingBotId('');
|
setOperatingBotId('');
|
||||||
}
|
}
|
||||||
}, [confirm, isZh, notify, refreshAll]);
|
}, [confirm, isZh, notify, refreshAll]);
|
||||||
|
|
||||||
const openResourceMonitor = useCallback((botId: string) => {
|
|
||||||
setResourceBotId(botId);
|
|
||||||
setShowResourceModal(true);
|
|
||||||
void loadResourceSnapshot(botId);
|
|
||||||
}, [loadResourceSnapshot]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isManagementMode) {
|
|
||||||
setCompactSheetMounted(false);
|
|
||||||
setCompactSheetClosing(false);
|
|
||||||
if (compactSheetTimerRef.current) {
|
|
||||||
window.clearTimeout(compactSheetTimerRef.current);
|
|
||||||
compactSheetTimerRef.current = null;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (compactMode && showCompactBotSheet && selectedBotInfo) {
|
|
||||||
if (compactSheetTimerRef.current) {
|
|
||||||
window.clearTimeout(compactSheetTimerRef.current);
|
|
||||||
compactSheetTimerRef.current = null;
|
|
||||||
}
|
|
||||||
setCompactSheetMounted(true);
|
|
||||||
setCompactSheetClosing(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!compactSheetMounted) return;
|
|
||||||
setCompactSheetClosing(true);
|
|
||||||
compactSheetTimerRef.current = window.setTimeout(() => {
|
|
||||||
setCompactSheetMounted(false);
|
|
||||||
setCompactSheetClosing(false);
|
|
||||||
compactSheetTimerRef.current = null;
|
|
||||||
}, 240);
|
|
||||||
return () => {
|
|
||||||
if (compactSheetTimerRef.current) {
|
|
||||||
window.clearTimeout(compactSheetTimerRef.current);
|
|
||||||
compactSheetTimerRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [compactMode, compactSheetMounted, isManagementMode, selectedBotInfo, showCompactBotSheet]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showResourceModal || !resourceBotId) return;
|
|
||||||
let stopped = false;
|
|
||||||
const tick = async () => {
|
|
||||||
if (stopped) return;
|
|
||||||
await loadResourceSnapshot(resourceBotId);
|
|
||||||
};
|
|
||||||
const timer = window.setInterval(() => {
|
|
||||||
void tick();
|
|
||||||
}, 2000);
|
|
||||||
return () => {
|
|
||||||
stopped = true;
|
|
||||||
window.clearInterval(timer);
|
|
||||||
};
|
|
||||||
}, [loadResourceSnapshot, resourceBotId, showResourceModal]);
|
|
||||||
|
|
||||||
const handleSelectBot = useCallback((botId: string) => {
|
|
||||||
setSelectedBotId(botId);
|
|
||||||
if (compactMode) setShowCompactBotSheet(true);
|
|
||||||
}, [compactMode]);
|
|
||||||
|
|
||||||
const closeCompactBotSheet = useCallback(() => setShowCompactBotSheet(false), []);
|
|
||||||
|
|
||||||
const openBotPanel = useCallback((botId: string) => {
|
const openBotPanel = useCallback((botId: string) => {
|
||||||
if (!botId || typeof window === 'undefined') return;
|
if (!botId || typeof window === 'undefined') return;
|
||||||
window.open(buildBotPanelHref(botId), '_blank', 'noopener,noreferrer');
|
window.open(buildBotPanelHref(botId), '_blank', 'noopener,noreferrer');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closeResourceModal = useCallback(() => setShowResourceModal(false), []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
botListPage,
|
botListPage: management.botListPage,
|
||||||
botListPageCount,
|
botListPageCount: management.botListPageCount,
|
||||||
botListPageSize,
|
botListPageSize: overviewState.platformPageSize,
|
||||||
closeCompactBotSheet,
|
closeCompactBotSheet: management.closeCompactBotSheet,
|
||||||
closeResourceModal,
|
closeResourceModal: management.closeResourceModal,
|
||||||
clearDashboardDirectSession,
|
clearDashboardDirectSession,
|
||||||
compactSheetClosing,
|
compactSheetClosing: management.compactSheetClosing,
|
||||||
compactSheetMounted,
|
compactSheetMounted: management.compactSheetMounted,
|
||||||
filteredBots,
|
filteredBots: management.filteredBots,
|
||||||
handleSelectBot,
|
handleSelectBot: management.handleSelectBot,
|
||||||
isZh,
|
isZh,
|
||||||
lastActionPreview,
|
lastActionPreview: management.lastActionPreview,
|
||||||
loadResourceSnapshot,
|
loadResourceSnapshot: management.loadResourceSnapshot,
|
||||||
loading,
|
loading: overviewState.loading,
|
||||||
openBotPanel,
|
openBotPanel,
|
||||||
openResourceMonitor,
|
openResourceMonitor: management.openResourceMonitor,
|
||||||
operatingBotId,
|
operatingBotId,
|
||||||
overview,
|
overview: overviewState.overview,
|
||||||
overviewBots,
|
overviewBots: overviewState.overviewBots,
|
||||||
overviewImages,
|
overviewImages: overviewState.overviewImages,
|
||||||
overviewResources,
|
overviewResources: overviewState.overviewResources,
|
||||||
pagedBots,
|
pagedBots: management.pagedBots,
|
||||||
refreshAll,
|
refreshAll,
|
||||||
removeBot,
|
removeBot,
|
||||||
resourceBot,
|
resourceBot: management.resourceBot,
|
||||||
resourceBotId,
|
resourceBotId: management.resourceBotId,
|
||||||
resourceError,
|
resourceError: management.resourceError,
|
||||||
resourceLoading,
|
resourceLoading: management.resourceLoading,
|
||||||
resourceSnapshot,
|
resourceSnapshot: management.resourceSnapshot,
|
||||||
search,
|
search: management.search,
|
||||||
selectedBotId,
|
selectedBotId: management.selectedBotId,
|
||||||
selectedBotInfo,
|
selectedBotEnabledChannels: management.selectedBotEnabledChannels,
|
||||||
selectedBotUsageSummary,
|
selectedBotInfo: management.selectedBotInfo,
|
||||||
|
selectedBotMcpCount: management.selectedBotMcpCount,
|
||||||
|
selectedBotSkillCount: management.selectedBotSkillCount,
|
||||||
|
selectedBotUsageSummary: management.selectedBotUsageSummary,
|
||||||
setBotEnabled,
|
setBotEnabled,
|
||||||
setBotListPage,
|
setBotListPage: management.setBotListPage,
|
||||||
setSearch,
|
setSearch: management.setSearch,
|
||||||
setShowBotLastActionModal,
|
setShowBotLastActionModal: management.setShowBotLastActionModal,
|
||||||
showBotLastActionModal,
|
showBotLastActionModal: management.showBotLastActionModal,
|
||||||
showResourceModal,
|
showResourceModal: management.showResourceModal,
|
||||||
toggleBot,
|
toggleBot,
|
||||||
usageAnalytics,
|
usageAnalytics: overviewState.usageAnalytics,
|
||||||
activityStats,
|
activityStats: overviewState.activityStats,
|
||||||
activityLoading,
|
activityLoading: overviewState.activityLoading,
|
||||||
usageAnalyticsMax,
|
usageAnalyticsMax: overviewState.usageAnalyticsMax,
|
||||||
usageAnalyticsSeries,
|
usageAnalyticsSeries: overviewState.usageAnalyticsSeries,
|
||||||
usageAnalyticsTicks,
|
usageAnalyticsTicks: overviewState.usageAnalyticsTicks,
|
||||||
usageLoading,
|
usageLoading: overviewState.usageLoading,
|
||||||
usageSummary,
|
usageSummary: overviewState.usageSummary,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { APP_ENDPOINTS } from '../../../config/env';
|
||||||
|
import type { BotChannel, MCPConfigResponse, WorkspaceSkillOption } from '../../dashboard/types';
|
||||||
|
import type { BotState } from '../../../types/bot';
|
||||||
|
import type { PlatformBotResourceSnapshot, PlatformUsageResponse } from '../types';
|
||||||
|
|
||||||
|
interface UsePlatformManagementStateOptions {
|
||||||
|
botList: BotState[];
|
||||||
|
compactMode: boolean;
|
||||||
|
isManagementMode: boolean;
|
||||||
|
isZh: boolean;
|
||||||
|
platformPageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlatformManagementState({
|
||||||
|
botList,
|
||||||
|
compactMode,
|
||||||
|
isManagementMode,
|
||||||
|
isZh,
|
||||||
|
platformPageSize,
|
||||||
|
}: UsePlatformManagementStateOptions) {
|
||||||
|
const [selectedBotId, setSelectedBotId] = useState('');
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [botListPage, setBotListPage] = useState(1);
|
||||||
|
const [showBotLastActionModal, setShowBotLastActionModal] = useState(false);
|
||||||
|
const [showResourceModal, setShowResourceModal] = useState(false);
|
||||||
|
const [selectedBotDetail, setSelectedBotDetail] = useState<BotState | null>(null);
|
||||||
|
const [selectedBotUsageSummary, setSelectedBotUsageSummary] = useState<PlatformUsageResponse['summary'] | null>(null);
|
||||||
|
const [selectedBotEnabledChannels, setSelectedBotEnabledChannels] = useState<string[]>([]);
|
||||||
|
const [selectedBotSkillCount, setSelectedBotSkillCount] = useState(0);
|
||||||
|
const [selectedBotMcpCount, setSelectedBotMcpCount] = useState(0);
|
||||||
|
const [resourceBotId, setResourceBotId] = useState('');
|
||||||
|
const [resourceSnapshot, setResourceSnapshot] = useState<PlatformBotResourceSnapshot | null>(null);
|
||||||
|
const [resourceLoading, setResourceLoading] = useState(false);
|
||||||
|
const [resourceError, setResourceError] = useState('');
|
||||||
|
const [showCompactBotSheet, setShowCompactBotSheet] = useState(false);
|
||||||
|
const [compactSheetClosing, setCompactSheetClosing] = useState(false);
|
||||||
|
const [compactSheetMounted, setCompactSheetMounted] = useState(false);
|
||||||
|
|
||||||
|
const compactSheetTimerRef = useRef<number | null>(null);
|
||||||
|
const selectedBotExtrasRequestRef = useRef(0);
|
||||||
|
|
||||||
|
const filteredBots = useMemo(() => {
|
||||||
|
const keyword = search.trim().toLowerCase();
|
||||||
|
if (!keyword) return botList;
|
||||||
|
return botList.filter((bot) => `${bot.name} ${bot.id}`.toLowerCase().includes(keyword));
|
||||||
|
}, [botList, search]);
|
||||||
|
|
||||||
|
const botListPageCount = useMemo(
|
||||||
|
() => Math.max(1, Math.ceil(filteredBots.length / platformPageSize)),
|
||||||
|
[filteredBots.length, platformPageSize],
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagedBots = useMemo(() => {
|
||||||
|
const page = Math.min(Math.max(1, botListPage), botListPageCount);
|
||||||
|
const start = (page - 1) * platformPageSize;
|
||||||
|
return filteredBots.slice(start, start + platformPageSize);
|
||||||
|
}, [filteredBots, botListPage, botListPageCount, platformPageSize]);
|
||||||
|
|
||||||
|
const selectedBot = useMemo(
|
||||||
|
() => (selectedBotId ? botList.find((bot) => bot.id === selectedBotId) : undefined),
|
||||||
|
[botList, selectedBotId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadSelectedBotUsageSummary = useCallback(async (botId: string) => {
|
||||||
|
if (!botId) {
|
||||||
|
setSelectedBotUsageSummary(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.get<PlatformUsageResponse>(`${APP_ENDPOINTS.apiBase}/platform/usage`, {
|
||||||
|
params: {
|
||||||
|
bot_id: botId,
|
||||||
|
limit: 1,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSelectedBotUsageSummary(res.data?.summary || null);
|
||||||
|
} catch {
|
||||||
|
setSelectedBotUsageSummary(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadSelectedBotDetail = useCallback(async (botId: string) => {
|
||||||
|
const targetBotId = String(botId || '').trim();
|
||||||
|
if (!targetBotId) {
|
||||||
|
setSelectedBotDetail(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await axios.get<BotState>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetBotId)}`);
|
||||||
|
setSelectedBotDetail(res.data);
|
||||||
|
} catch {
|
||||||
|
setSelectedBotDetail(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadSelectedBotOverviewExtras = useCallback(async (botId: string) => {
|
||||||
|
const requestId = selectedBotExtrasRequestRef.current + 1;
|
||||||
|
selectedBotExtrasRequestRef.current = requestId;
|
||||||
|
|
||||||
|
if (!botId) {
|
||||||
|
setSelectedBotEnabledChannels([]);
|
||||||
|
setSelectedBotSkillCount(0);
|
||||||
|
setSelectedBotMcpCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [channelsResult, skillsResult, mcpResult] = await Promise.allSettled([
|
||||||
|
axios.get<BotChannel[]>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/channels`),
|
||||||
|
axios.get<WorkspaceSkillOption[]>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/skills`),
|
||||||
|
axios.get<MCPConfigResponse>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/mcp-config`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (selectedBotExtrasRequestRef.current !== requestId) return;
|
||||||
|
|
||||||
|
const enabledChannels = channelsResult.status === 'fulfilled'
|
||||||
|
? Array.from(
|
||||||
|
new Set(
|
||||||
|
(Array.isArray(channelsResult.value.data) ? channelsResult.value.data : [])
|
||||||
|
.filter((channel) => channel.is_active && String(channel.channel_type || '').trim().toLowerCase() !== 'dashboard')
|
||||||
|
.map((channel) => String(channel.channel_type || '').trim().toLowerCase())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const skillCount = skillsResult.status === 'fulfilled'
|
||||||
|
? (Array.isArray(skillsResult.value.data) ? skillsResult.value.data.length : 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const mcpCount = mcpResult.status === 'fulfilled'
|
||||||
|
? Object.keys(mcpResult.value.data?.mcp_servers || {}).filter((name) => String(name || '').trim().length > 0).length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
setSelectedBotEnabledChannels(enabledChannels);
|
||||||
|
setSelectedBotSkillCount(skillCount);
|
||||||
|
setSelectedBotMcpCount(mcpCount);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadResourceSnapshot = useCallback(async (botId: string) => {
|
||||||
|
if (!botId) return;
|
||||||
|
setResourceLoading(true);
|
||||||
|
setResourceError('');
|
||||||
|
try {
|
||||||
|
const res = await axios.get<PlatformBotResourceSnapshot>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/resources`);
|
||||||
|
setResourceSnapshot(res.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.');
|
||||||
|
setResourceError(String(msg));
|
||||||
|
} finally {
|
||||||
|
setResourceLoading(false);
|
||||||
|
}
|
||||||
|
}, [isZh]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManagementMode) return;
|
||||||
|
setBotListPage(1);
|
||||||
|
}, [platformPageSize, isManagementMode, search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManagementMode) return;
|
||||||
|
setBotListPage((prev) => Math.min(Math.max(prev, 1), botListPageCount));
|
||||||
|
}, [botListPageCount, isManagementMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManagementMode) return;
|
||||||
|
if (!selectedBotId && filteredBots[0]?.id) setSelectedBotId(filteredBots[0].id);
|
||||||
|
}, [filteredBots, isManagementMode, selectedBotId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManagementMode) {
|
||||||
|
setShowCompactBotSheet(false);
|
||||||
|
setCompactSheetClosing(false);
|
||||||
|
setCompactSheetMounted(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!compactMode) {
|
||||||
|
setShowCompactBotSheet(false);
|
||||||
|
setCompactSheetClosing(false);
|
||||||
|
setCompactSheetMounted(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedBotId && showCompactBotSheet) return;
|
||||||
|
if (!selectedBotId) setShowCompactBotSheet(false);
|
||||||
|
}, [compactMode, isManagementMode, selectedBotId, showCompactBotSheet]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManagementMode) {
|
||||||
|
selectedBotExtrasRequestRef.current += 1;
|
||||||
|
setSelectedBotDetail(null);
|
||||||
|
setSelectedBotUsageSummary(null);
|
||||||
|
setSelectedBotEnabledChannels([]);
|
||||||
|
setSelectedBotSkillCount(0);
|
||||||
|
setSelectedBotMcpCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedBotId) {
|
||||||
|
selectedBotExtrasRequestRef.current += 1;
|
||||||
|
setSelectedBotDetail(null);
|
||||||
|
setSelectedBotUsageSummary(null);
|
||||||
|
setSelectedBotEnabledChannels([]);
|
||||||
|
setSelectedBotSkillCount(0);
|
||||||
|
setSelectedBotMcpCount(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let alive = true;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get<BotState>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(selectedBotId)}`);
|
||||||
|
if (alive) {
|
||||||
|
setSelectedBotDetail(res.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (alive) {
|
||||||
|
setSelectedBotDetail(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
void loadSelectedBotUsageSummary(selectedBotId);
|
||||||
|
void loadSelectedBotOverviewExtras(selectedBotId);
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, [isManagementMode, loadSelectedBotOverviewExtras, loadSelectedBotUsageSummary, selectedBotId]);
|
||||||
|
|
||||||
|
const resourceBot = useMemo(
|
||||||
|
() => (resourceBotId ? botList.find((bot) => bot.id === resourceBotId) : undefined),
|
||||||
|
[botList, resourceBotId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedBotInfo = useMemo(() => {
|
||||||
|
if (selectedBotDetail && selectedBotDetail.id === selectedBotId) {
|
||||||
|
return {
|
||||||
|
...selectedBot,
|
||||||
|
...selectedBotDetail,
|
||||||
|
logs: (selectedBotDetail.logs && selectedBotDetail.logs.length > 0)
|
||||||
|
? selectedBotDetail.logs
|
||||||
|
: (selectedBot?.logs || []),
|
||||||
|
messages: (selectedBotDetail.messages && selectedBotDetail.messages.length > 0)
|
||||||
|
? selectedBotDetail.messages
|
||||||
|
: (selectedBot?.messages || []),
|
||||||
|
events: (selectedBotDetail.events && selectedBotDetail.events.length > 0)
|
||||||
|
? selectedBotDetail.events
|
||||||
|
: (selectedBot?.events || []),
|
||||||
|
} as BotState;
|
||||||
|
}
|
||||||
|
return selectedBot;
|
||||||
|
}, [selectedBot, selectedBotDetail, selectedBotId]);
|
||||||
|
|
||||||
|
const lastActionPreview = useMemo(
|
||||||
|
() => selectedBotInfo?.last_action?.trim() || '',
|
||||||
|
[selectedBotInfo?.last_action],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isManagementMode) {
|
||||||
|
setCompactSheetMounted(false);
|
||||||
|
setCompactSheetClosing(false);
|
||||||
|
if (compactSheetTimerRef.current) {
|
||||||
|
window.clearTimeout(compactSheetTimerRef.current);
|
||||||
|
compactSheetTimerRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (compactMode && showCompactBotSheet && selectedBotInfo) {
|
||||||
|
if (compactSheetTimerRef.current) {
|
||||||
|
window.clearTimeout(compactSheetTimerRef.current);
|
||||||
|
compactSheetTimerRef.current = null;
|
||||||
|
}
|
||||||
|
setCompactSheetMounted(true);
|
||||||
|
setCompactSheetClosing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!compactSheetMounted) return;
|
||||||
|
setCompactSheetClosing(true);
|
||||||
|
compactSheetTimerRef.current = window.setTimeout(() => {
|
||||||
|
setCompactSheetMounted(false);
|
||||||
|
setCompactSheetClosing(false);
|
||||||
|
compactSheetTimerRef.current = null;
|
||||||
|
}, 240);
|
||||||
|
return () => {
|
||||||
|
if (compactSheetTimerRef.current) {
|
||||||
|
window.clearTimeout(compactSheetTimerRef.current);
|
||||||
|
compactSheetTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [compactMode, compactSheetMounted, isManagementMode, selectedBotInfo, showCompactBotSheet]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showResourceModal || !resourceBotId) return;
|
||||||
|
let stopped = false;
|
||||||
|
const tick = async () => {
|
||||||
|
if (stopped) return;
|
||||||
|
await loadResourceSnapshot(resourceBotId);
|
||||||
|
};
|
||||||
|
const timer = window.setInterval(() => {
|
||||||
|
void tick();
|
||||||
|
}, 2000);
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
window.clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, [loadResourceSnapshot, resourceBotId, showResourceModal]);
|
||||||
|
|
||||||
|
const handleSelectBot = useCallback((botId: string) => {
|
||||||
|
setSelectedBotId(botId);
|
||||||
|
if (compactMode) setShowCompactBotSheet(true);
|
||||||
|
}, [compactMode]);
|
||||||
|
|
||||||
|
const closeCompactBotSheet = useCallback(() => setShowCompactBotSheet(false), []);
|
||||||
|
const closeResourceModal = useCallback(() => setShowResourceModal(false), []);
|
||||||
|
const refreshSelectedBot = useCallback(async (botId?: string) => {
|
||||||
|
const targetBotId = String(botId || selectedBotId || '').trim();
|
||||||
|
if (!isManagementMode || !targetBotId) return;
|
||||||
|
await Promise.allSettled([
|
||||||
|
loadSelectedBotDetail(targetBotId),
|
||||||
|
loadSelectedBotUsageSummary(targetBotId),
|
||||||
|
loadSelectedBotOverviewExtras(targetBotId),
|
||||||
|
]);
|
||||||
|
}, [
|
||||||
|
isManagementMode,
|
||||||
|
loadSelectedBotDetail,
|
||||||
|
loadSelectedBotOverviewExtras,
|
||||||
|
loadSelectedBotUsageSummary,
|
||||||
|
selectedBotId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
botListPage,
|
||||||
|
botListPageCount,
|
||||||
|
closeCompactBotSheet,
|
||||||
|
closeResourceModal,
|
||||||
|
compactSheetClosing,
|
||||||
|
compactSheetMounted,
|
||||||
|
filteredBots,
|
||||||
|
handleSelectBot,
|
||||||
|
lastActionPreview,
|
||||||
|
loadResourceSnapshot,
|
||||||
|
pagedBots,
|
||||||
|
refreshSelectedBot,
|
||||||
|
resourceBot,
|
||||||
|
resourceBotId,
|
||||||
|
resourceError,
|
||||||
|
resourceLoading,
|
||||||
|
resourceSnapshot,
|
||||||
|
search,
|
||||||
|
selectedBotId,
|
||||||
|
selectedBotInfo,
|
||||||
|
selectedBotUsageSummary,
|
||||||
|
selectedBotEnabledChannels,
|
||||||
|
selectedBotSkillCount,
|
||||||
|
selectedBotMcpCount,
|
||||||
|
setBotListPage,
|
||||||
|
setSearch,
|
||||||
|
setSelectedBotId,
|
||||||
|
setSelectedBotDetail,
|
||||||
|
setShowBotLastActionModal,
|
||||||
|
setShowResourceModal,
|
||||||
|
showBotLastActionModal,
|
||||||
|
showResourceModal,
|
||||||
|
openResourceMonitor: (botId: string) => {
|
||||||
|
setResourceBotId(botId);
|
||||||
|
setShowResourceModal(true);
|
||||||
|
void loadResourceSnapshot(botId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { APP_ENDPOINTS } from '../../../config/env';
|
||||||
|
import {
|
||||||
|
normalizePlatformPageSize,
|
||||||
|
readCachedPlatformPageSize,
|
||||||
|
writeCachedPlatformPageSize,
|
||||||
|
} from '../../../utils/platformPageSize';
|
||||||
|
import type {
|
||||||
|
BotActivityStatsItem,
|
||||||
|
PlatformOverviewResponse,
|
||||||
|
PlatformUsageAnalyticsSeriesItem,
|
||||||
|
PlatformUsageResponse,
|
||||||
|
} from '../types';
|
||||||
|
import {
|
||||||
|
buildPlatformUsageAnalyticsSeries,
|
||||||
|
buildPlatformUsageAnalyticsTicks,
|
||||||
|
getPlatformChartCeiling,
|
||||||
|
} from '../utils';
|
||||||
|
|
||||||
|
interface UsePlatformOverviewStateOptions {
|
||||||
|
isAdminMode: boolean;
|
||||||
|
isZh: boolean;
|
||||||
|
notify: (message: string, options?: { tone?: 'error' | 'success' | 'warning' | 'info' }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlatformOverviewState({
|
||||||
|
isAdminMode,
|
||||||
|
isZh,
|
||||||
|
notify,
|
||||||
|
}: UsePlatformOverviewStateOptions) {
|
||||||
|
const [overview, setOverview] = useState<PlatformOverviewResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [usageData, setUsageData] = useState<PlatformUsageResponse | null>(null);
|
||||||
|
const [usageLoading, setUsageLoading] = useState(false);
|
||||||
|
const [activityStatsData, setActivityStatsData] = useState<BotActivityStatsItem[] | null>(null);
|
||||||
|
const [activityLoading, setActivityLoading] = useState(false);
|
||||||
|
const [platformPageSize, setPlatformPageSize] = useState(() => readCachedPlatformPageSize(10));
|
||||||
|
|
||||||
|
const loadOverview = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await axios.get<PlatformOverviewResponse>(`${APP_ENDPOINTS.apiBase}/platform/overview`);
|
||||||
|
setOverview(res.data);
|
||||||
|
const normalizedPageSize = normalizePlatformPageSize(
|
||||||
|
res.data?.settings?.page_size,
|
||||||
|
readCachedPlatformPageSize(10),
|
||||||
|
);
|
||||||
|
writeCachedPlatformPageSize(normalizedPageSize);
|
||||||
|
setPlatformPageSize(normalizedPageSize);
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [isZh, notify]);
|
||||||
|
|
||||||
|
const loadUsage = useCallback(async (page = 1) => {
|
||||||
|
setUsageLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await axios.get<PlatformUsageResponse>(`${APP_ENDPOINTS.apiBase}/platform/usage`, {
|
||||||
|
params: {
|
||||||
|
limit: platformPageSize,
|
||||||
|
offset: Math.max(0, page - 1) * platformPageSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setUsageData(res.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.response?.data?.detail || (isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' });
|
||||||
|
} finally {
|
||||||
|
setUsageLoading(false);
|
||||||
|
}
|
||||||
|
}, [isZh, notify, platformPageSize]);
|
||||||
|
|
||||||
|
const loadActivityStats = useCallback(async () => {
|
||||||
|
setActivityLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await axios.get<{ items: BotActivityStatsItem[] }>(`${APP_ENDPOINTS.apiBase}/platform/activity-stats`);
|
||||||
|
setActivityStatsData(Array.isArray(res.data?.items) ? res.data.items : []);
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.response?.data?.detail || (isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' });
|
||||||
|
} finally {
|
||||||
|
setActivityLoading(false);
|
||||||
|
}
|
||||||
|
}, [isZh, notify]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadOverview();
|
||||||
|
}, [loadOverview]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdminMode) {
|
||||||
|
setUsageData(null);
|
||||||
|
setUsageLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadUsage(1);
|
||||||
|
}, [isAdminMode, loadUsage, platformPageSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdminMode) {
|
||||||
|
setActivityStatsData(null);
|
||||||
|
setActivityLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadActivityStats();
|
||||||
|
}, [isAdminMode, loadActivityStats]);
|
||||||
|
|
||||||
|
const overviewBots = overview?.summary.bots;
|
||||||
|
const overviewImages = overview?.summary.images;
|
||||||
|
const overviewResources = overview?.summary.resources;
|
||||||
|
const activityStats = activityStatsData || overview?.activity_stats;
|
||||||
|
const usageSummary = usageData?.summary || overview?.usage.summary;
|
||||||
|
const usageAnalytics = usageData?.analytics || overview?.usage.analytics || null;
|
||||||
|
|
||||||
|
const usageAnalyticsSeries = useMemo<PlatformUsageAnalyticsSeriesItem[]>(
|
||||||
|
() => buildPlatformUsageAnalyticsSeries(usageAnalytics, isZh),
|
||||||
|
[isZh, usageAnalytics],
|
||||||
|
);
|
||||||
|
|
||||||
|
const usageAnalyticsMax = useMemo(() => {
|
||||||
|
const maxDailyRequests = usageAnalyticsSeries.reduce(
|
||||||
|
(max, item) => Math.max(max, ...item.daily_counts.map((count) => Number(count || 0))),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return getPlatformChartCeiling(maxDailyRequests);
|
||||||
|
}, [usageAnalyticsSeries]);
|
||||||
|
|
||||||
|
const usageAnalyticsTicks = useMemo(
|
||||||
|
() => buildPlatformUsageAnalyticsTicks(usageAnalyticsMax),
|
||||||
|
[usageAnalyticsMax],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
activityLoading,
|
||||||
|
activityStats,
|
||||||
|
loadActivityStats,
|
||||||
|
loadOverview,
|
||||||
|
loadUsage,
|
||||||
|
loading,
|
||||||
|
overview,
|
||||||
|
overviewBots,
|
||||||
|
overviewImages,
|
||||||
|
overviewResources,
|
||||||
|
platformPageSize,
|
||||||
|
usageAnalytics,
|
||||||
|
usageAnalyticsMax,
|
||||||
|
usageAnalyticsSeries,
|
||||||
|
usageAnalyticsTicks,
|
||||||
|
usageLoading,
|
||||||
|
usageSummary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import ReactMarkdown, { type Components } from 'react-markdown';
|
||||||
|
import rehypeRaw from 'rehype-raw';
|
||||||
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
|
import { MARKDOWN_SANITIZE_SCHEMA } from '../workspace/constants';
|
||||||
|
|
||||||
|
interface MarkdownRendererProps {
|
||||||
|
content: string;
|
||||||
|
components?: Components;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownRenderer({
|
||||||
|
content,
|
||||||
|
components,
|
||||||
|
}: MarkdownRendererProps) {
|
||||||
|
return (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
||||||
|
components={components}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
const memoryMap = new Map<string, string>();
|
|
||||||
const BOT_STORAGE_KEY_PREFIX = 'nanobot-bot-page-password:';
|
|
||||||
export const BOT_AUTH_INVALID_EVENT = 'nanobot:bot-auth-invalid';
|
export const BOT_AUTH_INVALID_EVENT = 'nanobot:bot-auth-invalid';
|
||||||
|
|
||||||
function normalizeBotId(raw: string): string {
|
function normalizeBotId(raw: string): string {
|
||||||
return String(raw || '').trim();
|
return String(raw || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildBotAccessStorageKey(botId: string): string {
|
|
||||||
return `${BOT_STORAGE_KEY_PREFIX}${normalizeBotId(botId)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAbsoluteUrl(input: string): string {
|
function resolveAbsoluteUrl(input: string): string {
|
||||||
const url = String(input || '').trim();
|
const url = String(input || '').trim();
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
|
|
@ -40,49 +35,6 @@ export function extractBotIdFromApiPath(rawPath: string): string | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBotAccessPassword(botId: string): string {
|
|
||||||
const key = normalizeBotId(botId);
|
|
||||||
if (!key) return '';
|
|
||||||
const cached = memoryMap.get(key) || '';
|
|
||||||
if (cached) return cached;
|
|
||||||
if (typeof window === 'undefined') return '';
|
|
||||||
const stored = window.sessionStorage.getItem(buildBotAccessStorageKey(key)) || '';
|
|
||||||
if (stored) memoryMap.set(key, stored);
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setBotAccessPassword(botId: string, password: string): void {
|
|
||||||
const key = normalizeBotId(botId);
|
|
||||||
const value = String(password || '').trim();
|
|
||||||
if (!key) return;
|
|
||||||
if (value) {
|
|
||||||
memoryMap.set(key, value);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.sessionStorage.setItem(buildBotAccessStorageKey(key), value);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearBotAccessPassword(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearBotAccessPassword(botId: string): void {
|
|
||||||
const key = normalizeBotId(botId);
|
|
||||||
if (!key) return;
|
|
||||||
memoryMap.delete(key);
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.sessionStorage.removeItem(buildBotAccessStorageKey(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearAllBotAccessPasswords(): void {
|
|
||||||
if (memoryMap.size === 0) return;
|
|
||||||
const keys = Array.from(memoryMap.keys());
|
|
||||||
memoryMap.clear();
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
keys.forEach((botId) => window.sessionStorage.removeItem(buildBotAccessStorageKey(botId)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBotAuthRoute(rawPath: string, botId: string): boolean {
|
function isBotAuthRoute(rawPath: string, botId: string): boolean {
|
||||||
const path = resolveAbsoluteUrl(rawPath);
|
const path = resolveAbsoluteUrl(rawPath);
|
||||||
return path === `/api/bots/${encodeURIComponent(botId)}/auth/login`
|
return path === `/api/bots/${encodeURIComponent(botId)}/auth/login`
|
||||||
|
|
@ -93,15 +45,25 @@ function isBotAuthRoute(rawPath: string, botId: string): boolean {
|
||||||
export function notifyBotAuthInvalid(botId: string): void {
|
export function notifyBotAuthInvalid(botId: string): void {
|
||||||
const normalizedBotId = normalizeBotId(botId);
|
const normalizedBotId = normalizeBotId(botId);
|
||||||
if (!normalizedBotId) return;
|
if (!normalizedBotId) return;
|
||||||
clearBotAccessPassword(normalizedBotId);
|
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
window.dispatchEvent(new CustomEvent(BOT_AUTH_INVALID_EVENT, { detail: { botId: normalizedBotId } }));
|
window.dispatchEvent(new CustomEvent(BOT_AUTH_INVALID_EVENT, { detail: { botId: normalizedBotId } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBotUnauthorizedError(error: any, botId?: string): boolean {
|
interface UnauthorizedErrorShape {
|
||||||
const normalizedBotId = normalizeBotId(botId || extractBotIdFromApiPath(String(error?.config?.url || '')) || '');
|
response?: {
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
|
config?: {
|
||||||
|
url?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBotUnauthorizedError(error: unknown, botId?: string): boolean {
|
||||||
|
const resolvedError = (error && typeof error === 'object' ? error : {}) as UnauthorizedErrorShape;
|
||||||
|
const normalizedBotId = normalizeBotId(botId || extractBotIdFromApiPath(String(resolvedError.config?.url || '')) || '');
|
||||||
if (!normalizedBotId) return false;
|
if (!normalizedBotId) return false;
|
||||||
return Number(error?.response?.status || 0) === 401 && !isBotAuthRoute(String(error?.config?.url || ''), normalizedBotId);
|
return Number(resolvedError.response?.status || 0) === 401
|
||||||
|
&& !isBotAuthRoute(String(resolvedError.config?.url || ''), normalizedBotId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMonitorWsUrl(base: string, botId: string): string {
|
export function buildMonitorWsUrl(base: string, botId: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const PANEL_STORAGE_KEY = 'nanobot-panel-access-password';
|
|
||||||
export const PANEL_AUTH_INVALID_EVENT = 'nanobot:panel-auth-invalid';
|
export const PANEL_AUTH_INVALID_EVENT = 'nanobot:panel-auth-invalid';
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let memoryPassword = '';
|
|
||||||
|
|
||||||
function resolveAbsoluteUrl(input: string): string {
|
function resolveAbsoluteUrl(input: string): string {
|
||||||
const url = String(input || '').trim();
|
const url = String(input || '').trim();
|
||||||
|
|
@ -27,40 +25,12 @@ function isApiRequest(url: string): boolean {
|
||||||
return /^\/api(\/|$)/i.test(path);
|
return /^\/api(\/|$)/i.test(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPanelAccessPassword(): string {
|
|
||||||
if (memoryPassword) return memoryPassword;
|
|
||||||
if (typeof window === 'undefined') return '';
|
|
||||||
const stored = window.localStorage.getItem(PANEL_STORAGE_KEY) || '';
|
|
||||||
if (stored) {
|
|
||||||
memoryPassword = stored;
|
|
||||||
}
|
|
||||||
return memoryPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setPanelAccessPassword(password: string): void {
|
|
||||||
const value = String(password || '').trim();
|
|
||||||
memoryPassword = value;
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
if (value) {
|
|
||||||
window.localStorage.setItem(PANEL_STORAGE_KEY, value);
|
|
||||||
} else {
|
|
||||||
window.localStorage.removeItem(PANEL_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearPanelAccessPassword(): void {
|
|
||||||
memoryPassword = '';
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
window.localStorage.removeItem(PANEL_STORAGE_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPanelAuthRoute(url: string): boolean {
|
function isPanelAuthRoute(url: string): boolean {
|
||||||
const path = resolveAbsoluteUrl(url);
|
const path = resolveAbsoluteUrl(url);
|
||||||
return /^\/api\/panel\/auth(?:\/|$)/i.test(path);
|
return /^\/api\/panel\/auth(?:\/|$)/i.test(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notifyPanelAuthInvalid(): void {
|
export function notifyPanelAuthInvalid(): void {
|
||||||
clearPanelAccessPassword();
|
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
window.dispatchEvent(new CustomEvent(PANEL_AUTH_INVALID_EVENT));
|
window.dispatchEvent(new CustomEvent(PANEL_AUTH_INVALID_EVENT));
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +38,6 @@ export function notifyPanelAuthInvalid(): void {
|
||||||
export function setupPanelAccessAuth(): void {
|
export function setupPanelAccessAuth(): void {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
getPanelAccessPassword();
|
|
||||||
|
|
||||||
axios.interceptors.response.use(undefined, (error) => {
|
axios.interceptors.response.use(undefined, (error) => {
|
||||||
const status = Number(error?.response?.status || 0);
|
const status = Number(error?.response?.status || 0);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue