v0.1.4-p3
parent
e766025c94
commit
9448d7d4f3
|
|
@ -44,6 +44,8 @@ REDIS_DEFAULT_TTL=60
|
||||||
|
|
||||||
# Chat history page size for upward lazy loading (per request)
|
# Chat history page size for upward lazy loading (per request)
|
||||||
CHAT_PULL_PAGE_SIZE=60
|
CHAT_PULL_PAGE_SIZE=60
|
||||||
|
COMMAND_AUTO_UNLOCK_SECONDS=10
|
||||||
|
DEFAULT_BOT_SYSTEM_TIMEZONE=Asia/Shanghai
|
||||||
|
|
||||||
# Panel access protection
|
# Panel access protection
|
||||||
PANEL_ACCESS_PASSWORD=change_me_panel_password
|
PANEL_ACCESS_PASSWORD=change_me_panel_password
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,10 @@ DATABASE_POOL_RECYCLE: Final[int] = _env_int("DATABASE_POOL_RECYCLE", 1800, 30,
|
||||||
DEFAULT_UPLOAD_MAX_MB: Final[int] = 100
|
DEFAULT_UPLOAD_MAX_MB: Final[int] = 100
|
||||||
DEFAULT_PAGE_SIZE: Final[int] = 10
|
DEFAULT_PAGE_SIZE: Final[int] = 10
|
||||||
DEFAULT_CHAT_PULL_PAGE_SIZE: Final[int] = 60
|
DEFAULT_CHAT_PULL_PAGE_SIZE: Final[int] = 60
|
||||||
|
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS: Final[int] = _env_int("COMMAND_AUTO_UNLOCK_SECONDS", 10, 1, 600)
|
||||||
|
DEFAULT_BOT_SYSTEM_TIMEZONE: Final[str] = str(
|
||||||
|
os.getenv("DEFAULT_BOT_SYSTEM_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai"
|
||||||
|
).strip() or "Asia/Shanghai"
|
||||||
DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS: Final[tuple[str, ...]] = (
|
DEFAULT_WORKSPACE_DOWNLOAD_EXTENSIONS: Final[tuple[str, ...]] = (
|
||||||
".pdf",
|
".pdf",
|
||||||
".doc",
|
".doc",
|
||||||
|
|
|
||||||
101
backend/main.py
101
backend/main.py
|
|
@ -6,10 +6,12 @@ import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from urllib.parse import quote, unquote
|
from urllib.parse import quote, unquote
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
@ -36,6 +38,7 @@ from core.settings import (
|
||||||
DATABASE_URL_DISPLAY,
|
DATABASE_URL_DISPLAY,
|
||||||
AGENT_MD_TEMPLATES_FILE,
|
AGENT_MD_TEMPLATES_FILE,
|
||||||
DEFAULT_AGENTS_MD,
|
DEFAULT_AGENTS_MD,
|
||||||
|
DEFAULT_BOT_SYSTEM_TIMEZONE,
|
||||||
DEFAULT_IDENTITY_MD,
|
DEFAULT_IDENTITY_MD,
|
||||||
DEFAULT_SOUL_MD,
|
DEFAULT_SOUL_MD,
|
||||||
DEFAULT_TOOLS_MD,
|
DEFAULT_TOOLS_MD,
|
||||||
|
|
@ -128,6 +131,7 @@ class BotCreateRequest(BaseModel):
|
||||||
cpu_cores: float = 1.0
|
cpu_cores: float = 1.0
|
||||||
memory_mb: int = 1024
|
memory_mb: int = 1024
|
||||||
storage_gb: int = 10
|
storage_gb: int = 10
|
||||||
|
system_timezone: Optional[str] = None
|
||||||
soul_md: Optional[str] = None
|
soul_md: Optional[str] = None
|
||||||
agents_md: Optional[str] = None
|
agents_md: Optional[str] = None
|
||||||
user_md: Optional[str] = None
|
user_md: Optional[str] = None
|
||||||
|
|
@ -156,6 +160,7 @@ class BotUpdateRequest(BaseModel):
|
||||||
cpu_cores: Optional[float] = None
|
cpu_cores: Optional[float] = None
|
||||||
memory_mb: Optional[int] = None
|
memory_mb: Optional[int] = None
|
||||||
storage_gb: Optional[int] = None
|
storage_gb: Optional[int] = None
|
||||||
|
system_timezone: Optional[str] = None
|
||||||
soul_md: Optional[str] = None
|
soul_md: Optional[str] = None
|
||||||
agents_md: Optional[str] = None
|
agents_md: Optional[str] = None
|
||||||
user_md: Optional[str] = None
|
user_md: Optional[str] = None
|
||||||
|
|
@ -675,10 +680,14 @@ def get_system_defaults():
|
||||||
"download_extensions": list(platform_settings.workspace_download_extensions),
|
"download_extensions": list(platform_settings.workspace_download_extensions),
|
||||||
"allowed_attachment_extensions": list(platform_settings.allowed_attachment_extensions),
|
"allowed_attachment_extensions": list(platform_settings.allowed_attachment_extensions),
|
||||||
},
|
},
|
||||||
|
"bot": {
|
||||||
|
"system_timezone": _get_default_system_timezone(),
|
||||||
|
},
|
||||||
"loading_page": platform_settings.loading_page.model_dump(),
|
"loading_page": platform_settings.loading_page.model_dump(),
|
||||||
"chat": {
|
"chat": {
|
||||||
"pull_page_size": platform_settings.chat_pull_page_size,
|
"pull_page_size": platform_settings.chat_pull_page_size,
|
||||||
"page_size": platform_settings.page_size,
|
"page_size": platform_settings.page_size,
|
||||||
|
"command_auto_unlock_seconds": platform_settings.command_auto_unlock_seconds,
|
||||||
},
|
},
|
||||||
"topic_presets": topic_presets.get("presets") or TOPIC_PRESET_TEMPLATES,
|
"topic_presets": topic_presets.get("presets") or TOPIC_PRESET_TEMPLATES,
|
||||||
"speech": {
|
"speech": {
|
||||||
|
|
@ -1159,6 +1168,35 @@ def _normalize_env_params(raw: Any) -> Dict[str, str]:
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _get_default_system_timezone() -> str:
|
||||||
|
value = str(DEFAULT_BOT_SYSTEM_TIMEZONE or "").strip() or "Asia/Shanghai"
|
||||||
|
try:
|
||||||
|
ZoneInfo(value)
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
return "Asia/Shanghai"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_system_timezone(raw: Any) -> str:
|
||||||
|
value = str(raw or "").strip()
|
||||||
|
if not value:
|
||||||
|
return _get_default_system_timezone()
|
||||||
|
try:
|
||||||
|
ZoneInfo(value)
|
||||||
|
except Exception as exc:
|
||||||
|
raise ValueError("Invalid system timezone. Use an IANA timezone such as Asia/Shanghai.") from exc
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_bot_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))
|
||||||
|
try:
|
||||||
|
env_params["TZ"] = _normalize_system_timezone(env_params.get("TZ"))
|
||||||
|
except ValueError:
|
||||||
|
env_params["TZ"] = _get_default_system_timezone()
|
||||||
|
return env_params
|
||||||
|
|
||||||
|
|
||||||
_MCP_SERVER_NAME_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
|
_MCP_SERVER_NAME_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1295,6 +1333,7 @@ def _read_workspace_md(bot_id: str, filename: str, default_value: str) -> str:
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
provider_name = ""
|
provider_name = ""
|
||||||
provider_cfg: Dict[str, Any] = {}
|
provider_cfg: Dict[str, Any] = {}
|
||||||
|
|
@ -1326,7 +1365,6 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
|
|
||||||
soul_md = _read_workspace_md(bot.id, "SOUL.md", DEFAULT_SOUL_MD)
|
soul_md = _read_workspace_md(bot.id, "SOUL.md", DEFAULT_SOUL_MD)
|
||||||
resources = _read_bot_resources(bot.id, config_data=config_data)
|
resources = _read_bot_resources(bot.id, config_data=config_data)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"llm_provider": llm_provider,
|
"llm_provider": llm_provider,
|
||||||
"llm_model": llm_model,
|
"llm_model": llm_model,
|
||||||
|
|
@ -1338,6 +1376,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(),
|
||||||
"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,
|
||||||
|
|
@ -1370,6 +1409,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()),
|
||||||
"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 "",
|
||||||
|
|
@ -1400,6 +1440,24 @@ def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_AGENT_LOOP_READY_MARKER = "Agent loop started"
|
||||||
|
|
||||||
|
|
||||||
|
async def _wait_for_agent_loop_ready(
|
||||||
|
bot_id: str,
|
||||||
|
timeout_seconds: float = 12.0,
|
||||||
|
poll_interval_seconds: float = 0.5,
|
||||||
|
) -> bool:
|
||||||
|
deadline = time.monotonic() + max(1.0, timeout_seconds)
|
||||||
|
marker = _AGENT_LOOP_READY_MARKER.lower()
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
logs = docker_manager.get_recent_logs(bot_id, tail=200)
|
||||||
|
if any(marker in str(line or "").lower() for line in logs):
|
||||||
|
return True
|
||||||
|
await asyncio.sleep(max(0.1, poll_interval_seconds))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _sync_workspace_channels(
|
def _sync_workspace_channels(
|
||||||
session: Session,
|
session: Session,
|
||||||
bot_id: str,
|
bot_id: str,
|
||||||
|
|
@ -2302,6 +2360,12 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
||||||
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)
|
||||||
|
try:
|
||||||
|
normalized_env_params["TZ"] = _normalize_system_timezone(payload.system_timezone)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
bot = BotInstance(
|
bot = BotInstance(
|
||||||
id=normalized_bot_id,
|
id=normalized_bot_id,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
|
|
@ -2315,7 +2379,7 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(bot)
|
session.refresh(bot)
|
||||||
resource_limits = _normalize_resource_limits(payload.cpu_cores, payload.memory_mb, payload.storage_gb)
|
resource_limits = _normalize_resource_limits(payload.cpu_cores, payload.memory_mb, payload.storage_gb)
|
||||||
_write_env_store(normalized_bot_id, _normalize_env_params(payload.env_params))
|
_write_env_store(normalized_bot_id, normalized_env_params)
|
||||||
_sync_workspace_channels(
|
_sync_workspace_channels(
|
||||||
session,
|
session,
|
||||||
normalized_bot_id,
|
normalized_bot_id,
|
||||||
|
|
@ -2477,6 +2541,13 @@ def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depend
|
||||||
raise HTTPException(status_code=400, detail=f"Docker image not found locally: {image_tag}")
|
raise HTTPException(status_code=400, detail=f"Docker image not found locally: {image_tag}")
|
||||||
|
|
||||||
env_params = update_data.pop("env_params", None) if isinstance(update_data, dict) else None
|
env_params = update_data.pop("env_params", None) if isinstance(update_data, dict) else None
|
||||||
|
system_timezone = update_data.pop("system_timezone", None) if isinstance(update_data, dict) else None
|
||||||
|
normalized_system_timezone: Optional[str] = None
|
||||||
|
if system_timezone is not None:
|
||||||
|
try:
|
||||||
|
normalized_system_timezone = _normalize_system_timezone(system_timezone)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
runtime_overrides: Dict[str, Any] = {}
|
runtime_overrides: Dict[str, Any] = {}
|
||||||
update_data.pop("tools_config", None) if isinstance(update_data, dict) else None
|
update_data.pop("tools_config", None) if isinstance(update_data, dict) else None
|
||||||
|
|
||||||
|
|
@ -2535,8 +2606,13 @@ def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depend
|
||||||
session.add(bot)
|
session.add(bot)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(bot)
|
session.refresh(bot)
|
||||||
if env_params is not None:
|
if env_params is not None or normalized_system_timezone is not None:
|
||||||
_write_env_store(bot_id, _normalize_env_params(env_params))
|
next_env_params = _resolve_bot_env_params(bot_id)
|
||||||
|
if env_params is not None:
|
||||||
|
next_env_params = _normalize_env_params(env_params)
|
||||||
|
if normalized_system_timezone is not None:
|
||||||
|
next_env_params["TZ"] = normalized_system_timezone
|
||||||
|
_write_env_store(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:
|
||||||
global_delivery_override = {}
|
global_delivery_override = {}
|
||||||
|
|
@ -2565,7 +2641,8 @@ async def start_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
raise HTTPException(status_code=403, detail="Bot is disabled. Enable it first.")
|
raise HTTPException(status_code=403, detail="Bot is disabled. Enable it first.")
|
||||||
_sync_workspace_channels(session, bot_id)
|
_sync_workspace_channels(session, bot_id)
|
||||||
runtime_snapshot = _read_bot_runtime_snapshot(bot)
|
runtime_snapshot = _read_bot_runtime_snapshot(bot)
|
||||||
env_params = _read_env_store(bot_id)
|
env_params = _resolve_bot_env_params(bot_id)
|
||||||
|
_write_env_store(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,
|
||||||
|
|
@ -2591,6 +2668,20 @@ async def start_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="Bot container failed shortly after startup. Check bot logs/config.",
|
detail="Bot container failed shortly after startup. Check bot logs/config.",
|
||||||
)
|
)
|
||||||
|
agent_loop_ready = await _wait_for_agent_loop_ready(bot_id)
|
||||||
|
if not agent_loop_ready:
|
||||||
|
docker_manager.stop_bot(bot_id)
|
||||||
|
bot.docker_status = "STOPPED"
|
||||||
|
if str(bot.current_state or "").upper() not in {"ERROR"}:
|
||||||
|
bot.current_state = "IDLE"
|
||||||
|
bot.updated_at = datetime.utcnow()
|
||||||
|
session.add(bot)
|
||||||
|
session.commit()
|
||||||
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Bot启动异常:容器已启动,但 Agent loop 未正常就绪,请检查 Bot 日志或 MCP 配置。",
|
||||||
|
)
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class LoadingPageSettings(BaseModel):
|
||||||
class PlatformSettingsPayload(BaseModel):
|
class PlatformSettingsPayload(BaseModel):
|
||||||
page_size: int = Field(default=10, ge=1, le=100)
|
page_size: int = Field(default=10, ge=1, le=100)
|
||||||
chat_pull_page_size: int = Field(default=60, ge=10, le=500)
|
chat_pull_page_size: int = Field(default=60, ge=10, le=500)
|
||||||
|
command_auto_unlock_seconds: int = Field(default=10, ge=1, le=600)
|
||||||
upload_max_mb: int = Field(default=100, ge=1, le=2048)
|
upload_max_mb: int = Field(default=100, ge=1, le=2048)
|
||||||
allowed_attachment_extensions: List[str] = Field(default_factory=list)
|
allowed_attachment_extensions: List[str] = Field(default_factory=list)
|
||||||
workspace_download_extensions: List[str] = Field(default_factory=list)
|
workspace_download_extensions: List[str] = Field(default_factory=list)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from core.database import engine
|
||||||
from core.settings import (
|
from core.settings import (
|
||||||
BOTS_WORKSPACE_ROOT,
|
BOTS_WORKSPACE_ROOT,
|
||||||
DEFAULT_CHAT_PULL_PAGE_SIZE,
|
DEFAULT_CHAT_PULL_PAGE_SIZE,
|
||||||
|
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
||||||
DEFAULT_PAGE_SIZE,
|
DEFAULT_PAGE_SIZE,
|
||||||
DEFAULT_STT_AUDIO_FILTER,
|
DEFAULT_STT_AUDIO_FILTER,
|
||||||
DEFAULT_STT_AUDIO_PREPROCESS,
|
DEFAULT_STT_AUDIO_PREPROCESS,
|
||||||
|
|
@ -57,6 +58,7 @@ OPERATIONAL_ACTIVITY_EVENT_TYPES = {
|
||||||
SETTING_KEYS = (
|
SETTING_KEYS = (
|
||||||
"page_size",
|
"page_size",
|
||||||
"chat_pull_page_size",
|
"chat_pull_page_size",
|
||||||
|
"command_auto_unlock_seconds",
|
||||||
"upload_max_mb",
|
"upload_max_mb",
|
||||||
"allowed_attachment_extensions",
|
"allowed_attachment_extensions",
|
||||||
"workspace_download_extensions",
|
"workspace_download_extensions",
|
||||||
|
|
@ -91,6 +93,15 @@ SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||||
"is_public": True,
|
"is_public": True,
|
||||||
"sort_order": 8,
|
"sort_order": 8,
|
||||||
},
|
},
|
||||||
|
"command_auto_unlock_seconds": {
|
||||||
|
"name": "发送按钮自动恢复秒数",
|
||||||
|
"category": "chat",
|
||||||
|
"description": "对话发送后按钮保持停止态的最长秒数,超时后自动恢复为可发送状态。",
|
||||||
|
"value_type": "integer",
|
||||||
|
"value": DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
||||||
|
"is_public": True,
|
||||||
|
"sort_order": 9,
|
||||||
|
},
|
||||||
"upload_max_mb": {
|
"upload_max_mb": {
|
||||||
"name": "上传大小限制",
|
"name": "上传大小限制",
|
||||||
"category": "upload",
|
"category": "upload",
|
||||||
|
|
@ -217,6 +228,12 @@ def _bootstrap_platform_setting_values() -> Dict[str, Any]:
|
||||||
10,
|
10,
|
||||||
500,
|
500,
|
||||||
),
|
),
|
||||||
|
"command_auto_unlock_seconds": _legacy_env_int(
|
||||||
|
"COMMAND_AUTO_UNLOCK_SECONDS",
|
||||||
|
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
||||||
|
1,
|
||||||
|
600,
|
||||||
|
),
|
||||||
"upload_max_mb": _legacy_env_int("UPLOAD_MAX_MB", DEFAULT_UPLOAD_MAX_MB, 1, 2048),
|
"upload_max_mb": _legacy_env_int("UPLOAD_MAX_MB", DEFAULT_UPLOAD_MAX_MB, 1, 2048),
|
||||||
"allowed_attachment_extensions": _legacy_env_extensions(
|
"allowed_attachment_extensions": _legacy_env_extensions(
|
||||||
"ALLOWED_ATTACHMENT_EXTENSIONS",
|
"ALLOWED_ATTACHMENT_EXTENSIONS",
|
||||||
|
|
@ -312,6 +329,7 @@ def default_platform_settings() -> PlatformSettingsPayload:
|
||||||
return PlatformSettingsPayload(
|
return PlatformSettingsPayload(
|
||||||
page_size=int(bootstrap["page_size"]),
|
page_size=int(bootstrap["page_size"]),
|
||||||
chat_pull_page_size=int(bootstrap["chat_pull_page_size"]),
|
chat_pull_page_size=int(bootstrap["chat_pull_page_size"]),
|
||||||
|
command_auto_unlock_seconds=int(bootstrap["command_auto_unlock_seconds"]),
|
||||||
upload_max_mb=int(bootstrap["upload_max_mb"]),
|
upload_max_mb=int(bootstrap["upload_max_mb"]),
|
||||||
allowed_attachment_extensions=list(bootstrap["allowed_attachment_extensions"]),
|
allowed_attachment_extensions=list(bootstrap["allowed_attachment_extensions"]),
|
||||||
workspace_download_extensions=list(bootstrap["workspace_download_extensions"]),
|
workspace_download_extensions=list(bootstrap["workspace_download_extensions"]),
|
||||||
|
|
@ -544,6 +562,10 @@ def get_platform_settings(session: Session) -> PlatformSettingsPayload:
|
||||||
merged = defaults.model_dump()
|
merged = defaults.model_dump()
|
||||||
merged["page_size"] = max(1, min(100, int(data.get("page_size") or merged["page_size"])))
|
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["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["upload_max_mb"] = int(data.get("upload_max_mb") or merged["upload_max_mb"])
|
merged["upload_max_mb"] = int(data.get("upload_max_mb") or merged["upload_max_mb"])
|
||||||
merged["allowed_attachment_extensions"] = _normalize_extension_list(
|
merged["allowed_attachment_extensions"] = _normalize_extension_list(
|
||||||
data.get("allowed_attachment_extensions", merged["allowed_attachment_extensions"])
|
data.get("allowed_attachment_extensions", merged["allowed_attachment_extensions"])
|
||||||
|
|
@ -567,6 +589,7 @@ def save_platform_settings(session: Session, payload: PlatformSettingsPayload) -
|
||||||
normalized = PlatformSettingsPayload(
|
normalized = PlatformSettingsPayload(
|
||||||
page_size=max(1, min(100, int(payload.page_size))),
|
page_size=max(1, min(100, int(payload.page_size))),
|
||||||
chat_pull_page_size=max(10, min(500, int(payload.chat_pull_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))),
|
||||||
upload_max_mb=payload.upload_max_mb,
|
upload_max_mb=payload.upload_max_mb,
|
||||||
allowed_attachment_extensions=_normalize_extension_list(payload.allowed_attachment_extensions),
|
allowed_attachment_extensions=_normalize_extension_list(payload.allowed_attachment_extensions),
|
||||||
workspace_download_extensions=_normalize_extension_list(payload.workspace_download_extensions),
|
workspace_download_extensions=_normalize_extension_list(payload.workspace_download_extensions),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ services:
|
||||||
REDIS_PREFIX: ${REDIS_PREFIX:-dashboard_nanobot}
|
REDIS_PREFIX: ${REDIS_PREFIX:-dashboard_nanobot}
|
||||||
REDIS_DEFAULT_TTL: ${REDIS_DEFAULT_TTL:-60}
|
REDIS_DEFAULT_TTL: ${REDIS_DEFAULT_TTL:-60}
|
||||||
CHAT_PULL_PAGE_SIZE: ${CHAT_PULL_PAGE_SIZE:-60}
|
CHAT_PULL_PAGE_SIZE: ${CHAT_PULL_PAGE_SIZE:-60}
|
||||||
|
COMMAND_AUTO_UNLOCK_SECONDS: ${COMMAND_AUTO_UNLOCK_SECONDS:-10}
|
||||||
|
DEFAULT_BOT_SYSTEM_TIMEZONE: ${DEFAULT_BOT_SYSTEM_TIMEZONE:-Asia/Shanghai}
|
||||||
PANEL_ACCESS_PASSWORD: ${PANEL_ACCESS_PASSWORD:-}
|
PANEL_ACCESS_PASSWORD: ${PANEL_ACCESS_PASSWORD:-}
|
||||||
STT_ENABLED: ${STT_ENABLED:-true}
|
STT_ENABLED: ${STT_ENABLED:-true}
|
||||||
STT_MODEL: ${STT_MODEL:-ggml-small-q8_0.bin}
|
STT_MODEL: ${STT_MODEL:-ggml-small-q8_0.bin}
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,76 @@ body {
|
||||||
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-lite-editor {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-editor.is-full-height {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-editor.is-full-height .md-lite-toolbar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--line) 70%, var(--text) 8%);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in oklab, var(--panel-soft) 82%, var(--panel) 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-toolbtn {
|
||||||
|
border: 1px solid color-mix(in oklab, var(--line) 78%, var(--text) 8%);
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-toolbtn:hover {
|
||||||
|
border-color: color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
||||||
|
color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-toolbar-spacer {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-toolbar-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-textarea {
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-lite-editor.is-full-height .md-lite-textarea {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
.field-label {
|
.field-label {
|
||||||
color: var(--subtitle);
|
color: var(--subtitle);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -1162,6 +1232,17 @@ body {
|
||||||
color: #132a54;
|
color: #132a54;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell[data-theme='light'] .md-lite-toolbar {
|
||||||
|
background: #f5f9ff;
|
||||||
|
border-color: #bccbe7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell[data-theme='light'] .md-lite-toolbtn {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: #c4d2eb;
|
||||||
|
color: #17305e;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell[data-theme='light'] .input:disabled,
|
.app-shell[data-theme='light'] .input:disabled,
|
||||||
.app-shell[data-theme='light'] .select:disabled,
|
.app-shell[data-theme='light'] .select:disabled,
|
||||||
.app-shell[data-theme='light'] .textarea:disabled {
|
.app-shell[data-theme='light'] .textarea:disabled {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
import { useMemo, useRef, type KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||||
|
|
||||||
|
interface MarkdownLiteEditorProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
className?: string;
|
||||||
|
textareaClassName?: string;
|
||||||
|
fullHeight?: boolean;
|
||||||
|
spellCheck?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onSaveShortcut?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INDENT = ' ';
|
||||||
|
|
||||||
|
function joinClassNames(...values: Array<string | false | null | undefined>) {
|
||||||
|
return values.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampSelection(textarea: HTMLTextAreaElement, start: number, end: number) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.setSelectionRange(start, end);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSelection(
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
onChange: (value: string) => void,
|
||||||
|
prefix: string,
|
||||||
|
suffix = '',
|
||||||
|
placeholder = '',
|
||||||
|
) {
|
||||||
|
const { value, selectionStart, selectionEnd } = textarea;
|
||||||
|
const selected = value.slice(selectionStart, selectionEnd);
|
||||||
|
const body = selected || placeholder;
|
||||||
|
const nextValue = `${value.slice(0, selectionStart)}${prefix}${body}${suffix}${value.slice(selectionEnd)}`;
|
||||||
|
onChange(nextValue);
|
||||||
|
const anchor = selectionStart + prefix.length;
|
||||||
|
const focus = anchor + body.length;
|
||||||
|
clampSelection(textarea, anchor, focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertBlockSnippet(
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
onChange: (value: string) => void,
|
||||||
|
snippet: string,
|
||||||
|
selectionOffsetStart = 0,
|
||||||
|
selectionOffsetEnd = snippet.length,
|
||||||
|
) {
|
||||||
|
const { value, selectionStart, selectionEnd } = textarea;
|
||||||
|
const needsLeadingBreak = selectionStart > 0 && value.charAt(selectionStart - 1) !== '\n';
|
||||||
|
const needsTrailingBreak = selectionEnd < value.length && value.charAt(selectionEnd) !== '\n';
|
||||||
|
const prefix = needsLeadingBreak ? '\n' : '';
|
||||||
|
const suffix = needsTrailingBreak ? '\n' : '';
|
||||||
|
const insertion = `${prefix}${snippet}${suffix}`;
|
||||||
|
const nextValue = `${value.slice(0, selectionStart)}${insertion}${value.slice(selectionEnd)}`;
|
||||||
|
onChange(nextValue);
|
||||||
|
const anchor = selectionStart + prefix.length + selectionOffsetStart;
|
||||||
|
const focus = selectionStart + prefix.length + selectionOffsetEnd;
|
||||||
|
clampSelection(textarea, anchor, focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceLineBlock(
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
onChange: (value: string) => void,
|
||||||
|
transform: (line: string, index: number) => string,
|
||||||
|
) {
|
||||||
|
const { value, selectionStart, selectionEnd } = textarea;
|
||||||
|
const lineStart = value.lastIndexOf('\n', Math.max(0, selectionStart - 1)) + 1;
|
||||||
|
const nextBreak = value.indexOf('\n', selectionEnd);
|
||||||
|
const lineEnd = nextBreak === -1 ? value.length : nextBreak;
|
||||||
|
const block = value.slice(lineStart, lineEnd);
|
||||||
|
const nextBlock = block.split('\n').map(transform).join('\n');
|
||||||
|
const nextValue = `${value.slice(0, lineStart)}${nextBlock}${value.slice(lineEnd)}`;
|
||||||
|
onChange(nextValue);
|
||||||
|
clampSelection(textarea, lineStart, lineStart + nextBlock.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function indentSelection(textarea: HTMLTextAreaElement, onChange: (value: string) => void, reverse = false) {
|
||||||
|
const { value, selectionStart, selectionEnd } = textarea;
|
||||||
|
const lineStart = value.lastIndexOf('\n', Math.max(0, selectionStart - 1)) + 1;
|
||||||
|
const nextBreak = value.indexOf('\n', selectionEnd);
|
||||||
|
const lineEnd = nextBreak === -1 ? value.length : nextBreak;
|
||||||
|
const block = value.slice(lineStart, lineEnd);
|
||||||
|
const lines = block.split('\n');
|
||||||
|
const nextLines = lines.map((line) => {
|
||||||
|
if (!reverse) return `${INDENT}${line}`;
|
||||||
|
if (line.startsWith(INDENT)) return line.slice(INDENT.length);
|
||||||
|
if (line.startsWith('\t')) return line.slice(1);
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
const nextBlock = nextLines.join('\n');
|
||||||
|
const nextValue = `${value.slice(0, lineStart)}${nextBlock}${value.slice(lineEnd)}`;
|
||||||
|
onChange(nextValue);
|
||||||
|
clampSelection(textarea, lineStart, lineStart + nextBlock.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownLiteEditor({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
rows = 12,
|
||||||
|
className,
|
||||||
|
textareaClassName,
|
||||||
|
fullHeight = false,
|
||||||
|
spellCheck = false,
|
||||||
|
disabled = false,
|
||||||
|
onSaveShortcut,
|
||||||
|
}: MarkdownLiteEditorProps) {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const tableSnippet = '| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| Value 1 | Value 2 | Value 3 |';
|
||||||
|
const tableHeaderStart = 2;
|
||||||
|
const tableHeaderEnd = '| Column 1 | Column 2 | Column 3 |'.length - 2;
|
||||||
|
|
||||||
|
const actions = useMemo(
|
||||||
|
() => [
|
||||||
|
{ key: 'h1', label: 'H1', title: 'Heading 1', run: () => replaceLineBlock(textareaRef.current!, onChange, (line) => `# ${line}`) },
|
||||||
|
{ key: 'h2', label: 'H2', title: 'Heading 2', run: () => replaceLineBlock(textareaRef.current!, onChange, (line) => `## ${line}`) },
|
||||||
|
{ key: 'bold', label: 'B', title: 'Bold', run: () => replaceSelection(textareaRef.current!, onChange, '**', '**', 'bold text') },
|
||||||
|
{ key: 'italic', label: 'I', title: 'Italic', run: () => replaceSelection(textareaRef.current!, onChange, '*', '*', 'italic text') },
|
||||||
|
{ key: 'ul', label: '- List', title: 'Bullet List', run: () => replaceLineBlock(textareaRef.current!, onChange, (line) => `- ${line}`) },
|
||||||
|
{ key: 'ol', label: '1. List', title: 'Ordered List', run: () => replaceLineBlock(textareaRef.current!, onChange, (line, index) => `${index + 1}. ${line}`) },
|
||||||
|
{ key: 'quote', label: '> Quote', title: 'Quote', run: () => replaceLineBlock(textareaRef.current!, onChange, (line) => `> ${line}`) },
|
||||||
|
{ key: 'code', label: '</>', title: 'Code Block', run: () => replaceSelection(textareaRef.current!, onChange, '```\n', '\n```', 'code') },
|
||||||
|
{ key: 'link', label: 'Link', title: 'Link', run: () => replaceSelection(textareaRef.current!, onChange, '[', '](https://)', 'link text') },
|
||||||
|
{
|
||||||
|
key: 'table',
|
||||||
|
label: 'Table',
|
||||||
|
title: 'Insert Table',
|
||||||
|
run: () => insertBlockSnippet(textareaRef.current!, onChange, tableSnippet, tableHeaderStart, tableHeaderEnd),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onChange, tableHeaderEnd, tableHeaderStart, tableSnippet],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyDown = (event: ReactKeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (!textarea || disabled) return;
|
||||||
|
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
indentSelection(textarea, onChange, event.shiftKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's' && onSaveShortcut) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSaveShortcut();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={joinClassNames('md-lite-editor', fullHeight && 'is-full-height', className)}>
|
||||||
|
<div className="md-lite-toolbar" role="toolbar" aria-label="Markdown editor toolbar">
|
||||||
|
{actions.map((action) => (
|
||||||
|
<button
|
||||||
|
key={action.key}
|
||||||
|
className="md-lite-toolbtn"
|
||||||
|
type="button"
|
||||||
|
title={action.title}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
if (disabled || !textareaRef.current) return;
|
||||||
|
action.run();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<span className="md-lite-toolbar-spacer" />
|
||||||
|
<span className="md-lite-toolbar-hint">Tab</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className={joinClassNames('textarea md-area mono md-lite-textarea', textareaClassName)}
|
||||||
|
value={value}
|
||||||
|
rows={rows}
|
||||||
|
placeholder={placeholder}
|
||||||
|
spellCheck={spellCheck}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1322,8 +1322,8 @@
|
||||||
|
|
||||||
.ops-composer-float-controls {
|
.ops-composer-float-controls {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 14px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 15px;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -3460,7 +3460,6 @@
|
||||||
|
|
||||||
.workspace-preview-editor {
|
.workspace-preview-editor {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|
@ -3480,6 +3479,16 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-preview-editor-shell {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-preview-image {
|
.workspace-preview-image {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { dashboardEn } from '../../i18n/dashboard.en';
|
||||||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||||
import { LucentSelect } from '../../components/lucent/LucentSelect';
|
import { LucentSelect } from '../../components/lucent/LucentSelect';
|
||||||
|
import { MarkdownLiteEditor } from '../../components/markdown/MarkdownLiteEditor';
|
||||||
import { PasswordInput } from '../../components/PasswordInput';
|
import { PasswordInput } from '../../components/PasswordInput';
|
||||||
import { TopicFeedPanel, type TopicFeedItem, type TopicFeedOption } from './topic/TopicFeedPanel';
|
import { TopicFeedPanel, type TopicFeedItem, type TopicFeedOption } from './topic/TopicFeedPanel';
|
||||||
import type { BotSkillMarketItem } from '../platform/types';
|
import type { BotSkillMarketItem } from '../platform/types';
|
||||||
|
|
@ -28,6 +29,7 @@ import {
|
||||||
readCachedPlatformPageSize,
|
readCachedPlatformPageSize,
|
||||||
writeCachedPlatformPageSize,
|
writeCachedPlatformPageSize,
|
||||||
} from '../../utils/platformPageSize';
|
} from '../../utils/platformPageSize';
|
||||||
|
import { getSystemTimezoneOptions } from '../../utils/systemTimezones';
|
||||||
|
|
||||||
interface BotDashboardModuleProps {
|
interface BotDashboardModuleProps {
|
||||||
onOpenCreateWizard?: () => void;
|
onOpenCreateWizard?: () => void;
|
||||||
|
|
@ -41,6 +43,7 @@ type WorkspaceNodeType = 'dir' | 'file';
|
||||||
type ChannelType = 'dashboard' | 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack' | 'email';
|
type ChannelType = 'dashboard' | 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack' | 'email';
|
||||||
type RuntimeViewMode = 'visual' | 'topic';
|
type RuntimeViewMode = 'visual' | 'topic';
|
||||||
type CompactPanelTab = 'chat' | 'runtime';
|
type CompactPanelTab = 'chat' | 'runtime';
|
||||||
|
type WorkspacePreviewMode = 'preview' | 'edit';
|
||||||
type QuotedReply = { id?: number; text: string; ts: number };
|
type QuotedReply = { id?: number; text: string; ts: number };
|
||||||
|
|
||||||
interface WorkspaceNode {
|
interface WorkspaceNode {
|
||||||
|
|
@ -294,6 +297,9 @@ interface SystemDefaultsResponse {
|
||||||
limits?: {
|
limits?: {
|
||||||
upload_max_mb?: number;
|
upload_max_mb?: number;
|
||||||
};
|
};
|
||||||
|
bot?: {
|
||||||
|
system_timezone?: string;
|
||||||
|
};
|
||||||
workspace?: {
|
workspace?: {
|
||||||
allowed_attachment_extensions?: unknown;
|
allowed_attachment_extensions?: unknown;
|
||||||
download_extensions?: unknown;
|
download_extensions?: unknown;
|
||||||
|
|
@ -301,6 +307,7 @@ interface SystemDefaultsResponse {
|
||||||
chat?: {
|
chat?: {
|
||||||
page_size?: number;
|
page_size?: number;
|
||||||
pull_page_size?: number;
|
pull_page_size?: number;
|
||||||
|
command_auto_unlock_seconds?: number;
|
||||||
};
|
};
|
||||||
topic_presets?: unknown;
|
topic_presets?: unknown;
|
||||||
speech?: {
|
speech?: {
|
||||||
|
|
@ -1095,7 +1102,9 @@ export function BotDashboardModule({
|
||||||
const [isTestingProvider, setIsTestingProvider] = useState(false);
|
const [isTestingProvider, setIsTestingProvider] = useState(false);
|
||||||
const [providerTestResult, setProviderTestResult] = useState('');
|
const [providerTestResult, setProviderTestResult] = useState('');
|
||||||
const [operatingBotId, setOperatingBotId] = useState<string | null>(null);
|
const [operatingBotId, setOperatingBotId] = useState<string | null>(null);
|
||||||
const [sendingByBot, setSendingByBot] = useState<Record<string, boolean>>({});
|
const [sendingByBot, setSendingByBot] = useState<Record<string, number>>({});
|
||||||
|
const [commandAutoUnlockDeadlineByBot, setCommandAutoUnlockDeadlineByBot] = useState<Record<string, number>>({});
|
||||||
|
const [defaultSystemTimezone, setDefaultSystemTimezone] = useState('Asia/Shanghai');
|
||||||
const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({});
|
const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({});
|
||||||
const [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping' | 'enabling' | 'disabling'>>({});
|
const [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping' | 'enabling' | 'disabling'>>({});
|
||||||
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
@ -1110,8 +1119,8 @@ export function BotDashboardModule({
|
||||||
const [workspaceParentPath, setWorkspaceParentPath] = useState<string | null>(null);
|
const [workspaceParentPath, setWorkspaceParentPath] = useState<string | null>(null);
|
||||||
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
||||||
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
||||||
|
const [workspacePreviewMode, setWorkspacePreviewMode] = useState<WorkspacePreviewMode>('preview');
|
||||||
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||||
const [workspacePreviewEditing, setWorkspacePreviewEditing] = useState(false);
|
|
||||||
const [workspacePreviewSaving, setWorkspacePreviewSaving] = useState(false);
|
const [workspacePreviewSaving, setWorkspacePreviewSaving] = useState(false);
|
||||||
const [workspacePreviewDraft, setWorkspacePreviewDraft] = useState('');
|
const [workspacePreviewDraft, setWorkspacePreviewDraft] = useState('');
|
||||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
||||||
|
|
@ -1201,6 +1210,7 @@ export function BotDashboardModule({
|
||||||
() => readCachedPlatformPageSize(0) > 0,
|
() => readCachedPlatformPageSize(0) > 0,
|
||||||
);
|
);
|
||||||
const [chatPullPageSize, setChatPullPageSize] = useState(60);
|
const [chatPullPageSize, setChatPullPageSize] = useState(60);
|
||||||
|
const [commandAutoUnlockSeconds, setCommandAutoUnlockSeconds] = useState(10);
|
||||||
const [chatHasMore, setChatHasMore] = useState(false);
|
const [chatHasMore, setChatHasMore] = useState(false);
|
||||||
const [chatLoadingMore, setChatLoadingMore] = useState(false);
|
const [chatLoadingMore, setChatLoadingMore] = useState(false);
|
||||||
const [chatDatePickerOpen, setChatDatePickerOpen] = useState(false);
|
const [chatDatePickerOpen, setChatDatePickerOpen] = useState(false);
|
||||||
|
|
@ -1275,6 +1285,7 @@ export function BotDashboardModule({
|
||||||
cpu_cores: clampCpuCores(bot.cpu_cores ?? 1),
|
cpu_cores: clampCpuCores(bot.cpu_cores ?? 1),
|
||||||
memory_mb: clampMemoryMb(bot.memory_mb ?? 1024),
|
memory_mb: clampMemoryMb(bot.memory_mb ?? 1024),
|
||||||
storage_gb: clampStorageGb(bot.storage_gb ?? 10),
|
storage_gb: clampStorageGb(bot.storage_gb ?? 10),
|
||||||
|
system_timezone: bot.system_timezone || defaultSystemTimezone,
|
||||||
agents_md: bot.agents_md || '',
|
agents_md: bot.agents_md || '',
|
||||||
soul_md: bot.soul_md || bot.system_prompt || '',
|
soul_md: bot.soul_md || bot.system_prompt || '',
|
||||||
user_md: bot.user_md || '',
|
user_md: bot.user_md || '',
|
||||||
|
|
@ -1287,7 +1298,7 @@ export function BotDashboardModule({
|
||||||
memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)),
|
memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)),
|
||||||
storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)),
|
storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)),
|
||||||
});
|
});
|
||||||
}, []);
|
}, [defaultSystemTimezone]);
|
||||||
const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => {
|
const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => {
|
||||||
const query = [`path=${encodeURIComponent(filePath)}`];
|
const query = [`path=${encodeURIComponent(filePath)}`];
|
||||||
if (forceDownload) query.push('download=1');
|
if (forceDownload) query.push('download=1');
|
||||||
|
|
@ -1313,22 +1324,23 @@ export function BotDashboardModule({
|
||||||
};
|
};
|
||||||
const closeWorkspacePreview = () => {
|
const closeWorkspacePreview = () => {
|
||||||
setWorkspacePreview(null);
|
setWorkspacePreview(null);
|
||||||
|
setWorkspacePreviewMode('preview');
|
||||||
setWorkspacePreviewFullscreen(false);
|
setWorkspacePreviewFullscreen(false);
|
||||||
setWorkspacePreviewEditing(false);
|
|
||||||
setWorkspacePreviewSaving(false);
|
setWorkspacePreviewSaving(false);
|
||||||
setWorkspacePreviewDraft('');
|
setWorkspacePreviewDraft('');
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspacePreview) {
|
if (!workspacePreview) {
|
||||||
setWorkspacePreviewEditing(false);
|
setWorkspacePreviewMode('preview');
|
||||||
setWorkspacePreviewSaving(false);
|
setWorkspacePreviewSaving(false);
|
||||||
setWorkspacePreviewDraft('');
|
setWorkspacePreviewDraft('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setWorkspacePreviewEditing(false);
|
|
||||||
setWorkspacePreviewSaving(false);
|
setWorkspacePreviewSaving(false);
|
||||||
setWorkspacePreviewDraft(workspacePreview.content || '');
|
setWorkspacePreviewDraft(workspacePreview.content || '');
|
||||||
}, [workspacePreview?.path, workspacePreview?.content]);
|
}, [workspacePreview?.path, workspacePreview?.content]);
|
||||||
|
const workspacePreviewCanEdit = Boolean(workspacePreview?.isMarkdown && !workspacePreview?.truncated);
|
||||||
|
const workspacePreviewEditorEnabled = workspacePreviewCanEdit && workspacePreviewMode === 'edit';
|
||||||
const triggerWorkspaceFileDownload = (filePath: string) => {
|
const triggerWorkspaceFileDownload = (filePath: string) => {
|
||||||
if (!selectedBotId) return;
|
if (!selectedBotId) return;
|
||||||
const normalized = String(filePath || '').trim();
|
const normalized = String(filePath || '').trim();
|
||||||
|
|
@ -1435,15 +1447,6 @@ export function BotDashboardModule({
|
||||||
}
|
}
|
||||||
return src;
|
return src;
|
||||||
}, [selectedBotId]);
|
}, [selectedBotId]);
|
||||||
const transformWorkspacePreviewMarkdownUrl = (url: string, key: string): string => {
|
|
||||||
if (!workspacePreview?.isMarkdown || !selectedBotId) return url;
|
|
||||||
const resolvedWorkspacePath = resolveWorkspaceDocumentPath(url, workspacePreview.path);
|
|
||||||
if (!resolvedWorkspacePath) return url;
|
|
||||||
if (key === 'href') {
|
|
||||||
return buildWorkspaceLink(resolvedWorkspacePath);
|
|
||||||
}
|
|
||||||
return buildWorkspacePreviewHref(resolvedWorkspacePath);
|
|
||||||
};
|
|
||||||
const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => {
|
const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => {
|
||||||
const source = String(text || '');
|
const source = String(text || '');
|
||||||
if (!source) return [source];
|
if (!source) return [source];
|
||||||
|
|
@ -1583,6 +1586,7 @@ export function BotDashboardModule({
|
||||||
cpu_cores: 1,
|
cpu_cores: 1,
|
||||||
memory_mb: 1024,
|
memory_mb: 1024,
|
||||||
storage_gb: 10,
|
storage_gb: 10,
|
||||||
|
system_timezone: '',
|
||||||
agents_md: '',
|
agents_md: '',
|
||||||
soul_md: '',
|
soul_md: '',
|
||||||
user_md: '',
|
user_md: '',
|
||||||
|
|
@ -1740,9 +1744,13 @@ export function BotDashboardModule({
|
||||||
}, [templateTopicText, effectiveTopicPresetTemplates.length]);
|
}, [templateTopicText, effectiveTopicPresetTemplates.length]);
|
||||||
const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined;
|
const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined;
|
||||||
const selectedBotEnabled = Boolean(selectedBot && selectedBot.enabled !== false);
|
const selectedBotEnabled = Boolean(selectedBot && selectedBot.enabled !== false);
|
||||||
const isSending = selectedBot ? Boolean(sendingByBot[selectedBot.id]) : false;
|
const selectedBotSendingCount = selectedBot ? Number(sendingByBot[selectedBot.id] || 0) : 0;
|
||||||
|
const isSending = selectedBotSendingCount > 0;
|
||||||
|
const selectedBotAutoUnlockDeadline = selectedBot ? Number(commandAutoUnlockDeadlineByBot[selectedBot.id] || 0) : 0;
|
||||||
|
const isCommandAutoUnlockWindowActive = selectedBotAutoUnlockDeadline > Date.now();
|
||||||
|
const isSendingBlocked = isSending && isCommandAutoUnlockWindowActive;
|
||||||
const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
||||||
const isChatEnabled = Boolean(canChat && !isSending);
|
const isChatEnabled = Boolean(canChat && !isSendingBlocked);
|
||||||
const activeControlCommand = selectedBot ? controlCommandByBot[selectedBot.id] || '' : '';
|
const activeControlCommand = selectedBot ? controlCommandByBot[selectedBot.id] || '' : '';
|
||||||
const canSendControlCommand = Boolean(selectedBot && canChat && !isVoiceRecording && !isVoiceTranscribing);
|
const canSendControlCommand = Boolean(selectedBot && canChat && !isVoiceRecording && !isVoiceTranscribing);
|
||||||
|
|
||||||
|
|
@ -1780,6 +1788,10 @@ export function BotDashboardModule({
|
||||||
.sort(([a], [b]) => a.localeCompare(b)),
|
.sort(([a], [b]) => a.localeCompare(b)),
|
||||||
[envParams],
|
[envParams],
|
||||||
);
|
);
|
||||||
|
const systemTimezoneOptions = useMemo(
|
||||||
|
() => getSystemTimezoneOptions(editForm.system_timezone || defaultSystemTimezone),
|
||||||
|
[defaultSystemTimezone, editForm.system_timezone],
|
||||||
|
);
|
||||||
const lastUserTs = useMemo(() => [...conversation].reverse().find((m) => m.role === 'user')?.ts || 0, [conversation]);
|
const lastUserTs = useMemo(() => [...conversation].reverse().find((m) => m.role === 'user')?.ts || 0, [conversation]);
|
||||||
const lastAssistantFinalTs = useMemo(
|
const lastAssistantFinalTs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -1833,6 +1845,30 @@ export function BotDashboardModule({
|
||||||
if (eventText) return eventText;
|
if (eventText) return eventText;
|
||||||
return '-';
|
return '-';
|
||||||
}, [selectedBot, latestEvent]);
|
}, [selectedBot, latestEvent]);
|
||||||
|
const showInterruptSubmitAction = Boolean(
|
||||||
|
canChat && ((isThinking && isCommandAutoUnlockWindowActive) || isSendingBlocked),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedBot?.id || selectedBotAutoUnlockDeadline <= 0) return;
|
||||||
|
const remaining = selectedBotAutoUnlockDeadline - Date.now();
|
||||||
|
if (remaining <= 0) {
|
||||||
|
setCommandAutoUnlockDeadlineByBot((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[selectedBot.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
setCommandAutoUnlockDeadlineByBot((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[selectedBot.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, remaining + 20);
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [selectedBot?.id, selectedBotAutoUnlockDeadline]);
|
||||||
const resourceBot = useMemo(() => bots.find((b) => b.id === resourceBotId), [bots, resourceBotId]);
|
const resourceBot = useMemo(() => bots.find((b) => b.id === resourceBotId), [bots, resourceBotId]);
|
||||||
const hasTopicUnread = topicFeedUnreadCount > 0;
|
const hasTopicUnread = topicFeedUnreadCount > 0;
|
||||||
|
|
||||||
|
|
@ -2257,6 +2293,12 @@ export function BotDashboardModule({
|
||||||
if (Number.isFinite(configured) && configured > 0) {
|
if (Number.isFinite(configured) && configured > 0) {
|
||||||
setUploadMaxMb(Math.max(1, Math.floor(configured)));
|
setUploadMaxMb(Math.max(1, Math.floor(configured)));
|
||||||
}
|
}
|
||||||
|
const nextSystemTimezone = String(res.data?.bot?.system_timezone || 'Asia/Shanghai').trim() || 'Asia/Shanghai';
|
||||||
|
setDefaultSystemTimezone(nextSystemTimezone);
|
||||||
|
setEditForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
system_timezone: prev.system_timezone || nextSystemTimezone,
|
||||||
|
}));
|
||||||
const configuredPageSize = normalizePlatformPageSize(
|
const configuredPageSize = normalizePlatformPageSize(
|
||||||
res.data?.chat?.page_size,
|
res.data?.chat?.page_size,
|
||||||
readCachedPlatformPageSize(10),
|
readCachedPlatformPageSize(10),
|
||||||
|
|
@ -2287,6 +2329,10 @@ export function BotDashboardModule({
|
||||||
if (Number.isFinite(pullPageSize) && pullPageSize > 0) {
|
if (Number.isFinite(pullPageSize) && pullPageSize > 0) {
|
||||||
setChatPullPageSize(Math.max(10, Math.min(500, Math.floor(pullPageSize))));
|
setChatPullPageSize(Math.max(10, Math.min(500, Math.floor(pullPageSize))));
|
||||||
}
|
}
|
||||||
|
const autoUnlockSeconds = Number(res.data?.chat?.command_auto_unlock_seconds);
|
||||||
|
if (Number.isFinite(autoUnlockSeconds) && autoUnlockSeconds > 0) {
|
||||||
|
setCommandAutoUnlockSeconds(Math.max(1, Math.min(600, Math.floor(autoUnlockSeconds))));
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// keep default limit
|
// keep default limit
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -2427,6 +2473,7 @@ export function BotDashboardModule({
|
||||||
if (!selectedBotId || !path) return;
|
if (!selectedBotId || !path) return;
|
||||||
const normalizedPath = String(path || '').trim();
|
const normalizedPath = String(path || '').trim();
|
||||||
setWorkspacePreviewFullscreen(false);
|
setWorkspacePreviewFullscreen(false);
|
||||||
|
setWorkspacePreviewMode('preview');
|
||||||
if (workspaceFileAction(normalizedPath, workspaceDownloadExtensionSet) === 'download') {
|
if (workspaceFileAction(normalizedPath, workspaceDownloadExtensionSet) === 'download') {
|
||||||
triggerWorkspaceFileDownload(normalizedPath);
|
triggerWorkspaceFileDownload(normalizedPath);
|
||||||
return;
|
return;
|
||||||
|
|
@ -2548,7 +2595,6 @@ export function BotDashboardModule({
|
||||||
ext: textExt ? `.${textExt}` : '',
|
ext: textExt ? `.${textExt}` : '',
|
||||||
isMarkdown: textExt === 'md' || textExt === 'markdown' || Boolean(res.data.is_markdown),
|
isMarkdown: textExt === 'md' || textExt === 'markdown' || Boolean(res.data.is_markdown),
|
||||||
});
|
});
|
||||||
setWorkspacePreviewEditing(false);
|
|
||||||
notify(t.fileSaved, { tone: 'success' });
|
notify(t.fileSaved, { tone: 'success' });
|
||||||
void loadWorkspaceTree(selectedBotId, workspaceCurrentPath);
|
void loadWorkspaceTree(selectedBotId, workspaceCurrentPath);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -3927,6 +3973,7 @@ export function BotDashboardModule({
|
||||||
updateBotStatus(id, 'RUNNING');
|
updateBotStatus(id, 'RUNNING');
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
await refresh();
|
||||||
notify(error?.response?.data?.detail || t.startFail, { tone: 'error' });
|
notify(error?.response?.data?.detail || t.startFail, { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setOperatingBotId(null);
|
setOperatingBotId(null);
|
||||||
|
|
@ -3958,6 +4005,7 @@ export function BotDashboardModule({
|
||||||
updateBotStatus(id, 'RUNNING');
|
updateBotStatus(id, 'RUNNING');
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
await refresh();
|
||||||
notify(error?.response?.data?.detail || t.restartFail, { tone: 'error' });
|
notify(error?.response?.data?.detail || t.restartFail, { tone: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setOperatingBotId(null);
|
setOperatingBotId(null);
|
||||||
|
|
@ -3999,7 +4047,7 @@ export function BotDashboardModule({
|
||||||
};
|
};
|
||||||
|
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
if (!selectedBot || !canChat || isSending) return;
|
if (!selectedBot || !canChat || isSendingBlocked) return;
|
||||||
if (!command.trim() && pendingAttachments.length === 0 && !quotedReply) return;
|
if (!command.trim() && pendingAttachments.length === 0 && !quotedReply) return;
|
||||||
const text = normalizeUserMessageText(command);
|
const text = normalizeUserMessageText(command);
|
||||||
const quoteText = normalizeAssistantMessageText(quotedReply?.text || '');
|
const quoteText = normalizeAssistantMessageText(quotedReply?.text || '');
|
||||||
|
|
@ -4011,7 +4059,11 @@ export function BotDashboardModule({
|
||||||
try {
|
try {
|
||||||
chatAutoFollowRef.current = true;
|
chatAutoFollowRef.current = true;
|
||||||
requestAnimationFrame(() => syncChatScrollToBottom('auto'));
|
requestAnimationFrame(() => syncChatScrollToBottom('auto'));
|
||||||
setSendingByBot((prev) => ({ ...prev, [selectedBot.id]: true }));
|
setSendingByBot((prev) => ({ ...prev, [selectedBot.id]: Number(prev[selectedBot.id] || 0) + 1 }));
|
||||||
|
setCommandAutoUnlockDeadlineByBot((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selectedBot.id]: Date.now() + (commandAutoUnlockSeconds * 1000),
|
||||||
|
}));
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`,
|
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`,
|
||||||
{ command: payloadText, attachments: pendingAttachments },
|
{ command: payloadText, attachments: pendingAttachments },
|
||||||
|
|
@ -4034,6 +4086,11 @@ export function BotDashboardModule({
|
||||||
setQuotedReply(null);
|
setQuotedReply(null);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
||||||
|
setCommandAutoUnlockDeadlineByBot((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[selectedBot.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
addBotMessage(selectedBot.id, {
|
addBotMessage(selectedBot.id, {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: t.sendFailMsg(msg),
|
text: t.sendFailMsg(msg),
|
||||||
|
|
@ -4045,7 +4102,12 @@ export function BotDashboardModule({
|
||||||
} finally {
|
} finally {
|
||||||
setSendingByBot((prev) => {
|
setSendingByBot((prev) => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
delete next[selectedBot.id];
|
const remaining = Number(next[selectedBot.id] || 0) - 1;
|
||||||
|
if (remaining > 0) {
|
||||||
|
next[selectedBot.id] = remaining;
|
||||||
|
} else {
|
||||||
|
delete next[selectedBot.id];
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -4956,6 +5018,7 @@ export function BotDashboardModule({
|
||||||
payload.name = editForm.name;
|
payload.name = editForm.name;
|
||||||
payload.access_password = editForm.access_password;
|
payload.access_password = editForm.access_password;
|
||||||
payload.image_tag = editForm.image_tag;
|
payload.image_tag = editForm.image_tag;
|
||||||
|
payload.system_timezone = editForm.system_timezone.trim() || defaultSystemTimezone;
|
||||||
const selectedImageOption = baseImageOptions.find((opt) => opt.tag === editForm.image_tag);
|
const selectedImageOption = baseImageOptions.find((opt) => opt.tag === editForm.image_tag);
|
||||||
if (selectedImageOption?.disabled) {
|
if (selectedImageOption?.disabled) {
|
||||||
throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.');
|
throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.');
|
||||||
|
|
@ -5783,9 +5846,9 @@ export function BotDashboardModule({
|
||||||
<Paperclip size={16} className={isUploadingAttachments ? 'animate-spin' : ''} />
|
<Paperclip size={16} className={isUploadingAttachments ? 'animate-spin' : ''} />
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
<button
|
<button
|
||||||
className={`ops-composer-submit-btn ${isChatEnabled && (isThinking || isSending) ? 'is-interrupt' : ''}`}
|
className={`ops-composer-submit-btn ${showInterruptSubmitAction ? 'is-interrupt' : ''}`}
|
||||||
disabled={
|
disabled={
|
||||||
isChatEnabled && (isThinking || isSending)
|
showInterruptSubmitAction
|
||||||
? Boolean(interruptingByBot[selectedBot.id])
|
? Boolean(interruptingByBot[selectedBot.id])
|
||||||
: (
|
: (
|
||||||
!isChatEnabled
|
!isChatEnabled
|
||||||
|
|
@ -5794,11 +5857,11 @@ export function BotDashboardModule({
|
||||||
|| (!command.trim() && pendingAttachments.length === 0 && !quotedReply)
|
|| (!command.trim() && pendingAttachments.length === 0 && !quotedReply)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={() => void (isChatEnabled && (isThinking || isSending) ? interruptExecution() : send())}
|
onClick={() => void (showInterruptSubmitAction ? interruptExecution() : send())}
|
||||||
aria-label={isChatEnabled && (isThinking || isSending) ? t.interrupt : t.send}
|
aria-label={showInterruptSubmitAction ? t.interrupt : t.send}
|
||||||
title={isChatEnabled && (isThinking || isSending) ? t.interrupt : t.send}
|
title={showInterruptSubmitAction ? t.interrupt : t.send}
|
||||||
>
|
>
|
||||||
{isChatEnabled && (isThinking || isSending) ? (
|
{showInterruptSubmitAction ? (
|
||||||
<Square size={15} />
|
<Square size={15} />
|
||||||
) : (
|
) : (
|
||||||
<ArrowUp size={18} />
|
<ArrowUp size={18} />
|
||||||
|
|
@ -6251,6 +6314,7 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="section-mini-title">{isZh ? '基础信息' : 'Basic Info'}</div>
|
||||||
<label className="field-label">{t.botIdReadonly}</label>
|
<label className="field-label">{t.botIdReadonly}</label>
|
||||||
<input className="input" value={selectedBot?.id || ''} disabled />
|
<input className="input" value={selectedBot?.id || ''} disabled />
|
||||||
|
|
||||||
|
|
@ -6278,6 +6342,24 @@ export function BotDashboardModule({
|
||||||
))}
|
))}
|
||||||
</LucentSelect>
|
</LucentSelect>
|
||||||
|
|
||||||
|
<label className="field-label">{isZh ? '系统时区' : 'System Timezone'}</label>
|
||||||
|
<LucentSelect
|
||||||
|
value={editForm.system_timezone || defaultSystemTimezone}
|
||||||
|
onChange={(e) => setEditForm((p) => ({ ...p, system_timezone: e.target.value }))}
|
||||||
|
>
|
||||||
|
{systemTimezoneOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</LucentSelect>
|
||||||
|
<div className="field-label">
|
||||||
|
{isZh ? '提示:系统时区保存后,在 Bot 重启后生效。' : 'Tip: timezone changes take effect after the bot restarts.'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
||||||
|
{isZh ? '硬件资源' : 'Hardware Resources'}
|
||||||
|
</div>
|
||||||
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
|
|
@ -7671,7 +7753,10 @@ export function BotDashboardModule({
|
||||||
<button key={tab} className={`agent-tab ${agentTab === tab ? 'active' : ''}`} onClick={() => setAgentTab(tab)}>{tab}.md</button>
|
<button key={tab} className={`agent-tab ${agentTab === tab ? 'active' : ''}`} onClick={() => setAgentTab(tab)}>{tab}.md</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<textarea className="textarea md-area" value={String(editForm[tabMap[agentTab]])} onChange={(e) => setEditForm((p) => ({ ...p, [tabMap[agentTab]]: e.target.value }))} />
|
<MarkdownLiteEditor
|
||||||
|
value={String(editForm[tabMap[agentTab]])}
|
||||||
|
onChange={(nextValue) => setEditForm((p) => ({ ...p, [tabMap[agentTab]]: nextValue }))}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="row-between">
|
<div className="row-between">
|
||||||
<button className="btn btn-secondary" onClick={() => setShowAgentModal(false)}>{t.cancel}</button>
|
<button className="btn btn-secondary" onClick={() => setShowAgentModal(false)}>{t.cancel}</button>
|
||||||
|
|
@ -7706,7 +7791,7 @@ export function BotDashboardModule({
|
||||||
<div className={`modal-card modal-preview ${workspacePreviewFullscreen ? 'modal-preview-fullscreen' : ''}`} onClick={(e) => e.stopPropagation()}>
|
<div className={`modal-card modal-preview ${workspacePreviewFullscreen ? 'modal-preview-fullscreen' : ''}`} onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="modal-title-row workspace-preview-header">
|
<div className="modal-title-row workspace-preview-header">
|
||||||
<div className="workspace-preview-header-text">
|
<div className="workspace-preview-header-text">
|
||||||
<h3>{t.filePreview}</h3>
|
<h3>{workspacePreviewEditorEnabled ? t.editFile : t.filePreview}</h3>
|
||||||
<span className="modal-sub mono workspace-preview-path-row">
|
<span className="modal-sub mono workspace-preview-path-row">
|
||||||
<span className="workspace-path-segments" title={workspacePreview.path}>
|
<span className="workspace-path-segments" title={workspacePreview.path}>
|
||||||
{renderWorkspacePathSegments(workspacePreview.path, 'preview-path')}
|
{renderWorkspacePathSegments(workspacePreview.path, 'preview-path')}
|
||||||
|
|
@ -7722,27 +7807,23 @@ export function BotDashboardModule({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="workspace-preview-header-actions">
|
<div className="workspace-preview-header-actions">
|
||||||
{workspacePreview.isMarkdown ? (
|
|
||||||
<LucentIconButton
|
|
||||||
className="btn btn-secondary btn-sm icon-btn"
|
|
||||||
onClick={() => {
|
|
||||||
if (workspacePreview.truncated) {
|
|
||||||
notify(t.fileEditDisabled, { tone: 'warning' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setWorkspacePreviewEditing((value) => !value);
|
|
||||||
}}
|
|
||||||
tooltip={workspacePreviewEditing ? t.previewMode : t.editFile}
|
|
||||||
aria-label={workspacePreviewEditing ? t.previewMode : t.editFile}
|
|
||||||
>
|
|
||||||
{workspacePreviewEditing ? <Eye size={14} /> : <Pencil size={14} />}
|
|
||||||
</LucentIconButton>
|
|
||||||
) : null}
|
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-secondary btn-sm icon-btn"
|
className="btn btn-secondary btn-sm icon-btn"
|
||||||
onClick={() => setWorkspacePreviewFullscreen((v) => !v)}
|
onClick={() => setWorkspacePreviewFullscreen((v) => !v)}
|
||||||
tooltip={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
|
tooltip={
|
||||||
aria-label={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
|
workspacePreviewFullscreen
|
||||||
|
? (isZh ? '退出全屏' : 'Exit full screen')
|
||||||
|
: workspacePreviewEditorEnabled
|
||||||
|
? (isZh ? '全屏编辑' : 'Full screen editor')
|
||||||
|
: (isZh ? '全屏预览' : 'Full screen')
|
||||||
|
}
|
||||||
|
aria-label={
|
||||||
|
workspacePreviewFullscreen
|
||||||
|
? (isZh ? '退出全屏' : 'Exit full screen')
|
||||||
|
: workspacePreviewEditorEnabled
|
||||||
|
? (isZh ? '全屏编辑' : 'Full screen editor')
|
||||||
|
: (isZh ? '全屏预览' : 'Full screen')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{workspacePreviewFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
{workspacePreviewFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
|
|
@ -7757,7 +7838,7 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`workspace-preview-body ${workspacePreview.isMarkdown ? 'markdown' : ''} ${workspacePreviewEditing ? 'is-editing' : ''} ${workspacePreview.isImage || workspacePreview.isVideo || workspacePreview.isAudio ? 'media' : ''}`}
|
className={`workspace-preview-body ${workspacePreview.isMarkdown ? 'markdown' : ''} ${workspacePreviewEditorEnabled ? 'is-editing' : ''} ${workspacePreview.isImage || workspacePreview.isVideo || workspacePreview.isAudio ? 'media' : ''}`}
|
||||||
>
|
>
|
||||||
{workspacePreview.isImage ? (
|
{workspacePreview.isImage ? (
|
||||||
<img
|
<img
|
||||||
|
|
@ -7785,12 +7866,17 @@ export function BotDashboardModule({
|
||||||
src={buildWorkspaceRawHref(workspacePreview.path, false)}
|
src={buildWorkspaceRawHref(workspacePreview.path, false)}
|
||||||
title={workspacePreview.path}
|
title={workspacePreview.path}
|
||||||
/>
|
/>
|
||||||
) : workspacePreview.isMarkdown && workspacePreviewEditing ? (
|
) : workspacePreviewEditorEnabled ? (
|
||||||
<textarea
|
<MarkdownLiteEditor
|
||||||
className="textarea md-area mono workspace-preview-editor"
|
className="workspace-preview-editor-shell"
|
||||||
|
textareaClassName="workspace-preview-editor"
|
||||||
value={workspacePreviewDraft}
|
value={workspacePreviewDraft}
|
||||||
onChange={(event) => setWorkspacePreviewDraft(event.target.value)}
|
onChange={setWorkspacePreviewDraft}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
fullHeight
|
||||||
|
onSaveShortcut={() => {
|
||||||
|
void saveWorkspacePreviewMarkdown();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : workspacePreview.isMarkdown ? (
|
) : workspacePreview.isMarkdown ? (
|
||||||
<div className="workspace-markdown">
|
<div className="workspace-markdown">
|
||||||
|
|
@ -7798,9 +7884,8 @@ export function BotDashboardModule({
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
||||||
components={markdownComponents}
|
components={markdownComponents}
|
||||||
urlTransform={transformWorkspacePreviewMarkdownUrl}
|
|
||||||
>
|
>
|
||||||
{workspacePreview.content}
|
{decorateWorkspacePathsForMarkdown(workspacePreview.content || '')}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -7813,13 +7898,13 @@ export function BotDashboardModule({
|
||||||
<div className="row-between">
|
<div className="row-between">
|
||||||
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
||||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||||
{workspacePreview.isMarkdown && workspacePreviewEditing ? (
|
{workspacePreviewEditorEnabled ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setWorkspacePreviewDraft(workspacePreview.content || '');
|
setWorkspacePreviewDraft(workspacePreview.content || '');
|
||||||
setWorkspacePreviewEditing(false);
|
setWorkspacePreviewMode('preview');
|
||||||
}}
|
}}
|
||||||
disabled={workspacePreviewSaving}
|
disabled={workspacePreviewSaving}
|
||||||
>
|
>
|
||||||
|
|
@ -7834,6 +7919,13 @@ export function BotDashboardModule({
|
||||||
<span style={{ marginLeft: 6 }}>{t.save}</span>
|
<span style={{ marginLeft: 6 }}>{t.save}</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
) : workspacePreviewCanEdit ? (
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => setWorkspacePreviewMode('edit')}
|
||||||
|
>
|
||||||
|
{t.editFile}
|
||||||
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{workspacePreview.isHtml ? (
|
{workspacePreview.isHtml ? (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import { wizardEn } from '../../i18n/wizard.en';
|
||||||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||||
import { LucentSelect } from '../../components/lucent/LucentSelect';
|
import { LucentSelect } from '../../components/lucent/LucentSelect';
|
||||||
|
import { MarkdownLiteEditor } from '../../components/markdown/MarkdownLiteEditor';
|
||||||
import { PasswordInput } from '../../components/PasswordInput';
|
import { PasswordInput } from '../../components/PasswordInput';
|
||||||
|
import { getSystemTimezoneOptions } from '../../utils/systemTimezones';
|
||||||
|
|
||||||
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
||||||
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
||||||
|
|
@ -39,6 +41,9 @@ interface SystemDefaultsResponse {
|
||||||
tools_md?: string;
|
tools_md?: string;
|
||||||
identity_md?: string;
|
identity_md?: string;
|
||||||
};
|
};
|
||||||
|
bot?: {
|
||||||
|
system_timezone?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerPresets: Record<string, { model: string; note: { 'zh-cn': string; en: string }; apiBase?: string }> = {
|
const providerPresets: Record<string, { model: string; note: { 'zh-cn': string; en: string }; apiBase?: string }> = {
|
||||||
|
|
@ -113,6 +118,7 @@ const initialForm = {
|
||||||
cpu_cores: 1,
|
cpu_cores: 1,
|
||||||
memory_mb: 1024,
|
memory_mb: 1024,
|
||||||
storage_gb: 10,
|
storage_gb: 10,
|
||||||
|
system_timezone: '',
|
||||||
|
|
||||||
soul_md: '',
|
soul_md: '',
|
||||||
agents_md: '',
|
agents_md: '',
|
||||||
|
|
@ -156,6 +162,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
const [storageGbDraft, setStorageGbDraft] = useState(String(initialForm.storage_gb));
|
const [storageGbDraft, setStorageGbDraft] = useState(String(initialForm.storage_gb));
|
||||||
const [botIdStatus, setBotIdStatus] = useState<'idle' | 'checking' | 'available' | 'exists' | 'invalid'>('idle');
|
const [botIdStatus, setBotIdStatus] = useState<'idle' | 'checking' | 'available' | 'exists' | 'invalid'>('idle');
|
||||||
const [botIdStatusText, setBotIdStatusText] = useState('');
|
const [botIdStatusText, setBotIdStatusText] = useState('');
|
||||||
|
const [defaultSystemTimezone, setDefaultSystemTimezone] = useState('Asia/Shanghai');
|
||||||
|
|
||||||
const readyImages = useMemo(() => images.filter((img) => img.status === 'READY'), [images]);
|
const readyImages = useMemo(() => images.filter((img) => img.status === 'READY'), [images]);
|
||||||
const isZh = locale === 'zh';
|
const isZh = locale === 'zh';
|
||||||
|
|
@ -166,6 +173,10 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
: { show: 'Show password', hide: 'Hide password' };
|
: { show: 'Show password', hide: 'Hide password' };
|
||||||
const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const });
|
const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const });
|
||||||
const activeChannelTypes = useMemo(() => new Set(form.channels.map((c) => c.channel_type)), [form.channels]);
|
const activeChannelTypes = useMemo(() => new Set(form.channels.map((c) => c.channel_type)), [form.channels]);
|
||||||
|
const systemTimezoneOptions = useMemo(
|
||||||
|
() => getSystemTimezoneOptions(form.system_timezone || defaultSystemTimezone),
|
||||||
|
[defaultSystemTimezone, form.system_timezone],
|
||||||
|
);
|
||||||
const addableChannelTypes = useMemo(
|
const addableChannelTypes = useMemo(
|
||||||
() => optionalChannelTypes.filter((t) => !activeChannelTypes.has(t)),
|
() => optionalChannelTypes.filter((t) => !activeChannelTypes.has(t)),
|
||||||
[activeChannelTypes],
|
[activeChannelTypes],
|
||||||
|
|
@ -183,11 +194,14 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<SystemDefaultsResponse>(`${APP_ENDPOINTS.apiBase}/system/defaults`);
|
const res = await axios.get<SystemDefaultsResponse>(`${APP_ENDPOINTS.apiBase}/system/defaults`);
|
||||||
const tpl = res.data?.templates || {};
|
const tpl = res.data?.templates || {};
|
||||||
|
const nextSystemTimezone = String(res.data?.bot?.system_timezone || 'Asia/Shanghai').trim() || 'Asia/Shanghai';
|
||||||
const agentsTemplate = String(tpl.agents_md || '').trim();
|
const agentsTemplate = String(tpl.agents_md || '').trim();
|
||||||
|
setDefaultSystemTimezone(nextSystemTimezone);
|
||||||
setDefaultAgentsTemplate(agentsTemplate);
|
setDefaultAgentsTemplate(agentsTemplate);
|
||||||
setForm((prev) => {
|
setForm((prev) => {
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
|
system_timezone: prev.system_timezone || nextSystemTimezone,
|
||||||
soul_md: String(tpl.soul_md || '').trim() || prev.soul_md,
|
soul_md: String(tpl.soul_md || '').trim() || prev.soul_md,
|
||||||
agents_md: agentsTemplate,
|
agents_md: agentsTemplate,
|
||||||
user_md: String(tpl.user_md || '').trim() || prev.user_md,
|
user_md: String(tpl.user_md || '').trim() || prev.user_md,
|
||||||
|
|
@ -344,6 +358,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
cpu_cores: Number(form.cpu_cores),
|
cpu_cores: Number(form.cpu_cores),
|
||||||
memory_mb: Number(form.memory_mb),
|
memory_mb: Number(form.memory_mb),
|
||||||
storage_gb: Number(form.storage_gb),
|
storage_gb: Number(form.storage_gb),
|
||||||
|
system_timezone: form.system_timezone.trim() || defaultSystemTimezone,
|
||||||
soul_md: form.soul_md,
|
soul_md: form.soul_md,
|
||||||
agents_md: form.agents_md,
|
agents_md: form.agents_md,
|
||||||
user_md: form.user_md,
|
user_md: form.user_md,
|
||||||
|
|
@ -367,7 +382,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
}
|
}
|
||||||
onCreated?.();
|
onCreated?.();
|
||||||
onGoDashboard?.();
|
onGoDashboard?.();
|
||||||
setForm(initialForm);
|
setForm({ ...initialForm, system_timezone: defaultSystemTimezone });
|
||||||
setMaxTokensDraft(String(initialForm.max_tokens));
|
setMaxTokensDraft(String(initialForm.max_tokens));
|
||||||
setCpuCoresDraft(String(initialForm.cpu_cores));
|
setCpuCoresDraft(String(initialForm.cpu_cores));
|
||||||
setMemoryMbDraft(String(initialForm.memory_mb));
|
setMemoryMbDraft(String(initialForm.memory_mb));
|
||||||
|
|
@ -674,6 +689,24 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
</div>
|
</div>
|
||||||
<input className="input" placeholder={ui.botName} value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
|
<input className="input" placeholder={ui.botName} value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
|
||||||
|
|
||||||
|
<div className="section-mini-title">
|
||||||
|
{isZh ? '基础信息' : 'Basic Info'}
|
||||||
|
</div>
|
||||||
|
<label className="field-label">{isZh ? '系统时区' : 'System Timezone'}</label>
|
||||||
|
<LucentSelect
|
||||||
|
value={form.system_timezone || defaultSystemTimezone}
|
||||||
|
onChange={(e) => setForm((p) => ({ ...p, system_timezone: e.target.value }))}
|
||||||
|
>
|
||||||
|
{systemTimezoneOptions.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</LucentSelect>
|
||||||
|
<div className="field-label">
|
||||||
|
{isZh ? '提示:系统时区保存后,在 Bot 重启后生效。' : 'Tip: timezone changes take effect after the bot restarts.'}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
||||||
{isZh ? '资源配额' : 'Resource Limits'}
|
{isZh ? '资源配额' : 'Resource Limits'}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -834,10 +867,9 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<textarea
|
<MarkdownLiteEditor
|
||||||
className="textarea md-area"
|
|
||||||
value={String(form[tabMap[agentTab]])}
|
value={String(form[tabMap[agentTab]])}
|
||||||
onChange={(e) => setForm((p) => ({ ...p, [tabMap[agentTab]]: e.target.value }))}
|
onChange={(nextValue) => setForm((p) => ({ ...p, [tabMap[agentTab]]: nextValue }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -857,6 +889,9 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
<div>CPU: {Number(form.cpu_cores) === 0 ? (isZh ? '不限' : 'Unlimited') : form.cpu_cores}</div>
|
<div>CPU: {Number(form.cpu_cores) === 0 ? (isZh ? '不限' : 'Unlimited') : form.cpu_cores}</div>
|
||||||
<div>{isZh ? '内存' : 'Memory'}: {Number(form.memory_mb) === 0 ? (isZh ? '不限' : 'Unlimited') : `${form.memory_mb} MB`}</div>
|
<div>{isZh ? '内存' : 'Memory'}: {Number(form.memory_mb) === 0 ? (isZh ? '不限' : 'Unlimited') : `${form.memory_mb} MB`}</div>
|
||||||
<div>{isZh ? '存储' : 'Storage'}: {Number(form.storage_gb) === 0 ? (isZh ? '不限' : 'Unlimited') : `${form.storage_gb} GB`}</div>
|
<div>{isZh ? '存储' : 'Storage'}: {Number(form.storage_gb) === 0 ? (isZh ? '不限' : 'Unlimited') : `${form.storage_gb} GB`}</div>
|
||||||
|
<div>
|
||||||
|
{isZh ? '系统时区' : 'System Timezone'}: {form.system_timezone || defaultSystemTimezone}
|
||||||
|
</div>
|
||||||
<div>{ui.channels}: {configuredChannelsLabel}</div>
|
<div>{ui.channels}: {configuredChannelsLabel}</div>
|
||||||
<div>{ui.tools}: {envEntries.map(([k]) => k).join(', ') || '-'}</div>
|
<div>{ui.tools}: {envEntries.map(([k]) => k).join(', ') || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { RefreshCw, X } from 'lucide-react';
|
||||||
import { APP_ENDPOINTS } from '../../../config/env';
|
import { APP_ENDPOINTS } from '../../../config/env';
|
||||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||||
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
||||||
|
import { MarkdownLiteEditor } from '../../../components/markdown/MarkdownLiteEditor';
|
||||||
|
|
||||||
interface TemplateManagerModalProps {
|
interface TemplateManagerModalProps {
|
||||||
isZh: boolean;
|
isZh: boolean;
|
||||||
|
|
@ -173,11 +174,11 @@ export function TemplateManagerModal({ isZh, open, onClose }: TemplateManagerMod
|
||||||
placeholder='{"presets":[...]}'
|
placeholder='{"presets":[...]}'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<textarea
|
<MarkdownLiteEditor
|
||||||
className="textarea md-area mono platform-template-textarea"
|
className="platform-template-textarea"
|
||||||
rows={18}
|
|
||||||
value={templates[activeTab] || ''}
|
value={templates[activeTab] || ''}
|
||||||
onChange={(event) => setTemplates((prev) => ({ ...prev, [activeTab]: event.target.value }))}
|
rows={18}
|
||||||
|
onChange={(nextValue) => setTemplates((prev) => ({ ...prev, [activeTab]: nextValue }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export interface PlatformSettings {
|
export interface PlatformSettings {
|
||||||
page_size: number;
|
page_size: number;
|
||||||
chat_pull_page_size: number;
|
chat_pull_page_size: number;
|
||||||
|
command_auto_unlock_seconds: number;
|
||||||
upload_max_mb: number;
|
upload_max_mb: number;
|
||||||
allowed_attachment_extensions: string[];
|
allowed_attachment_extensions: string[];
|
||||||
workspace_download_extensions: string[];
|
workspace_download_extensions: string[];
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export interface BotState {
|
||||||
cpu_cores?: number;
|
cpu_cores?: number;
|
||||||
memory_mb?: number;
|
memory_mb?: number;
|
||||||
storage_gb?: number;
|
storage_gb?: number;
|
||||||
|
system_timezone?: string;
|
||||||
send_progress?: boolean;
|
send_progress?: boolean;
|
||||||
send_tool_hints?: boolean;
|
send_tool_hints?: boolean;
|
||||||
soul_md?: string;
|
soul_md?: string;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
export interface SystemTimezoneOption {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRIMARY_SYSTEM_TIMEZONE_OPTIONS: SystemTimezoneOption[] = [
|
||||||
|
{ value: 'Asia/Shanghai' },
|
||||||
|
{ value: 'Europe/London' },
|
||||||
|
{ value: 'Europe/Berlin' },
|
||||||
|
{ value: 'America/New_York' },
|
||||||
|
{ value: 'America/Los_Angeles' },
|
||||||
|
{ value: 'Asia/Tokyo' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getSystemTimezoneOptions(currentValue?: string): SystemTimezoneOption[] {
|
||||||
|
const normalized = String(currentValue || '').trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return [...PRIMARY_SYSTEM_TIMEZONE_OPTIONS];
|
||||||
|
}
|
||||||
|
if (PRIMARY_SYSTEM_TIMEZONE_OPTIONS.some((option) => option.value === normalized)) {
|
||||||
|
return [...PRIMARY_SYSTEM_TIMEZONE_OPTIONS];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: normalized,
|
||||||
|
},
|
||||||
|
...PRIMARY_SYSTEM_TIMEZONE_OPTIONS,
|
||||||
|
];
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue