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_PULL_PAGE_SIZE=60
|
||||
COMMAND_AUTO_UNLOCK_SECONDS=10
|
||||
DEFAULT_BOT_SYSTEM_TIMEZONE=Asia/Shanghai
|
||||
|
||||
# Panel access protection
|
||||
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_PAGE_SIZE: Final[int] = 10
|
||||
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, ...]] = (
|
||||
".pdf",
|
||||
".doc",
|
||||
|
|
|
|||
101
backend/main.py
101
backend/main.py
|
|
@ -6,10 +6,12 @@ import os
|
|||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote, unquote
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -36,6 +38,7 @@ from core.settings import (
|
|||
DATABASE_URL_DISPLAY,
|
||||
AGENT_MD_TEMPLATES_FILE,
|
||||
DEFAULT_AGENTS_MD,
|
||||
DEFAULT_BOT_SYSTEM_TIMEZONE,
|
||||
DEFAULT_IDENTITY_MD,
|
||||
DEFAULT_SOUL_MD,
|
||||
DEFAULT_TOOLS_MD,
|
||||
|
|
@ -128,6 +131,7 @@ class BotCreateRequest(BaseModel):
|
|||
cpu_cores: float = 1.0
|
||||
memory_mb: int = 1024
|
||||
storage_gb: int = 10
|
||||
system_timezone: Optional[str] = None
|
||||
soul_md: Optional[str] = None
|
||||
agents_md: Optional[str] = None
|
||||
user_md: Optional[str] = None
|
||||
|
|
@ -156,6 +160,7 @@ class BotUpdateRequest(BaseModel):
|
|||
cpu_cores: Optional[float] = None
|
||||
memory_mb: Optional[int] = None
|
||||
storage_gb: Optional[int] = None
|
||||
system_timezone: Optional[str] = None
|
||||
soul_md: Optional[str] = None
|
||||
agents_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),
|
||||
"allowed_attachment_extensions": list(platform_settings.allowed_attachment_extensions),
|
||||
},
|
||||
"bot": {
|
||||
"system_timezone": _get_default_system_timezone(),
|
||||
},
|
||||
"loading_page": platform_settings.loading_page.model_dump(),
|
||||
"chat": {
|
||||
"pull_page_size": platform_settings.chat_pull_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,
|
||||
"speech": {
|
||||
|
|
@ -1159,6 +1168,35 @@ def _normalize_env_params(raw: Any) -> Dict[str, str]:
|
|||
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}$")
|
||||
|
||||
|
||||
|
|
@ -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]:
|
||||
config_data = _read_bot_config(bot.id)
|
||||
env_params = _resolve_bot_env_params(bot.id)
|
||||
|
||||
provider_name = ""
|
||||
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)
|
||||
resources = _read_bot_resources(bot.id, config_data=config_data)
|
||||
|
||||
return {
|
||||
"llm_provider": llm_provider,
|
||||
"llm_model": llm_model,
|
||||
|
|
@ -1338,6 +1376,7 @@ def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
|||
"cpu_cores": resources["cpu_cores"],
|
||||
"memory_mb": resources["memory_mb"],
|
||||
"storage_gb": resources["storage_gb"],
|
||||
"system_timezone": env_params.get("TZ") or _get_default_system_timezone(),
|
||||
"send_progress": send_progress,
|
||||
"send_tool_hints": send_tool_hints,
|
||||
"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),
|
||||
"memory_mb": _safe_int(runtime.get("memory_mb"), 1024),
|
||||
"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_tool_hints": bool(runtime.get("send_tool_hints")),
|
||||
"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(
|
||||
session: Session,
|
||||
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):
|
||||
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(
|
||||
id=normalized_bot_id,
|
||||
name=payload.name,
|
||||
|
|
@ -2315,7 +2379,7 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
|||
session.commit()
|
||||
session.refresh(bot)
|
||||
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(
|
||||
session,
|
||||
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}")
|
||||
|
||||
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] = {}
|
||||
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.commit()
|
||||
session.refresh(bot)
|
||||
if env_params is not None:
|
||||
_write_env_store(bot_id, _normalize_env_params(env_params))
|
||||
if env_params is not None or normalized_system_timezone is not None:
|
||||
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
|
||||
if "send_progress" in runtime_overrides or "send_tool_hints" in runtime_overrides:
|
||||
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.")
|
||||
_sync_workspace_channels(session, bot_id)
|
||||
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(
|
||||
bot_id,
|
||||
image_tag=bot.image_tag,
|
||||
|
|
@ -2591,6 +2668,20 @@ async def start_bot(bot_id: str, session: Session = Depends(get_session)):
|
|||
status_code=500,
|
||||
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)
|
||||
record_activity_event(session, bot_id, "bot_started", channel="system", detail=f"Container started for {bot_id}")
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class LoadingPageSettings(BaseModel):
|
|||
class PlatformSettingsPayload(BaseModel):
|
||||
page_size: int = Field(default=10, ge=1, le=100)
|
||||
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)
|
||||
allowed_attachment_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 (
|
||||
BOTS_WORKSPACE_ROOT,
|
||||
DEFAULT_CHAT_PULL_PAGE_SIZE,
|
||||
DEFAULT_COMMAND_AUTO_UNLOCK_SECONDS,
|
||||
DEFAULT_PAGE_SIZE,
|
||||
DEFAULT_STT_AUDIO_FILTER,
|
||||
DEFAULT_STT_AUDIO_PREPROCESS,
|
||||
|
|
@ -57,6 +58,7 @@ OPERATIONAL_ACTIVITY_EVENT_TYPES = {
|
|||
SETTING_KEYS = (
|
||||
"page_size",
|
||||
"chat_pull_page_size",
|
||||
"command_auto_unlock_seconds",
|
||||
"upload_max_mb",
|
||||
"allowed_attachment_extensions",
|
||||
"workspace_download_extensions",
|
||||
|
|
@ -91,6 +93,15 @@ SYSTEM_SETTING_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
|||
"is_public": True,
|
||||
"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": {
|
||||
"name": "上传大小限制",
|
||||
"category": "upload",
|
||||
|
|
@ -217,6 +228,12 @@ def _bootstrap_platform_setting_values() -> Dict[str, Any]:
|
|||
10,
|
||||
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),
|
||||
"allowed_attachment_extensions": _legacy_env_extensions(
|
||||
"ALLOWED_ATTACHMENT_EXTENSIONS",
|
||||
|
|
@ -312,6 +329,7 @@ def default_platform_settings() -> PlatformSettingsPayload:
|
|||
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"]),
|
||||
upload_max_mb=int(bootstrap["upload_max_mb"]),
|
||||
allowed_attachment_extensions=list(bootstrap["allowed_attachment_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["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["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"])
|
||||
|
|
@ -567,6 +589,7 @@ def save_platform_settings(session: Session, payload: 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))),
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ services:
|
|||
REDIS_PREFIX: ${REDIS_PREFIX:-dashboard_nanobot}
|
||||
REDIS_DEFAULT_TTL: ${REDIS_DEFAULT_TTL:-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:-}
|
||||
STT_ENABLED: ${STT_ENABLED:-true}
|
||||
STT_MODEL: ${STT_MODEL:-ggml-small-q8_0.bin}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,76 @@ body {
|
|||
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 {
|
||||
color: var(--subtitle);
|
||||
font-size: 12px;
|
||||
|
|
@ -1162,6 +1232,17 @@ body {
|
|||
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'] .select: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 {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
right: 15px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -3460,7 +3460,6 @@
|
|||
|
||||
.workspace-preview-editor {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
|
@ -3480,6 +3479,16 @@
|
|||
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 {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { dashboardEn } from '../../i18n/dashboard.en';
|
|||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||
import { LucentSelect } from '../../components/lucent/LucentSelect';
|
||||
import { MarkdownLiteEditor } from '../../components/markdown/MarkdownLiteEditor';
|
||||
import { PasswordInput } from '../../components/PasswordInput';
|
||||
import { TopicFeedPanel, type TopicFeedItem, type TopicFeedOption } from './topic/TopicFeedPanel';
|
||||
import type { BotSkillMarketItem } from '../platform/types';
|
||||
|
|
@ -28,6 +29,7 @@ import {
|
|||
readCachedPlatformPageSize,
|
||||
writeCachedPlatformPageSize,
|
||||
} from '../../utils/platformPageSize';
|
||||
import { getSystemTimezoneOptions } from '../../utils/systemTimezones';
|
||||
|
||||
interface BotDashboardModuleProps {
|
||||
onOpenCreateWizard?: () => void;
|
||||
|
|
@ -41,6 +43,7 @@ type WorkspaceNodeType = 'dir' | 'file';
|
|||
type ChannelType = 'dashboard' | 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack' | 'email';
|
||||
type RuntimeViewMode = 'visual' | 'topic';
|
||||
type CompactPanelTab = 'chat' | 'runtime';
|
||||
type WorkspacePreviewMode = 'preview' | 'edit';
|
||||
type QuotedReply = { id?: number; text: string; ts: number };
|
||||
|
||||
interface WorkspaceNode {
|
||||
|
|
@ -294,6 +297,9 @@ interface SystemDefaultsResponse {
|
|||
limits?: {
|
||||
upload_max_mb?: number;
|
||||
};
|
||||
bot?: {
|
||||
system_timezone?: string;
|
||||
};
|
||||
workspace?: {
|
||||
allowed_attachment_extensions?: unknown;
|
||||
download_extensions?: unknown;
|
||||
|
|
@ -301,6 +307,7 @@ interface SystemDefaultsResponse {
|
|||
chat?: {
|
||||
page_size?: number;
|
||||
pull_page_size?: number;
|
||||
command_auto_unlock_seconds?: number;
|
||||
};
|
||||
topic_presets?: unknown;
|
||||
speech?: {
|
||||
|
|
@ -1095,7 +1102,9 @@ export function BotDashboardModule({
|
|||
const [isTestingProvider, setIsTestingProvider] = useState(false);
|
||||
const [providerTestResult, setProviderTestResult] = useState('');
|
||||
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 [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping' | 'enabling' | 'disabling'>>({});
|
||||
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
||||
|
|
@ -1110,8 +1119,8 @@ export function BotDashboardModule({
|
|||
const [workspaceParentPath, setWorkspaceParentPath] = useState<string | null>(null);
|
||||
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
||||
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
||||
const [workspacePreviewMode, setWorkspacePreviewMode] = useState<WorkspacePreviewMode>('preview');
|
||||
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||
const [workspacePreviewEditing, setWorkspacePreviewEditing] = useState(false);
|
||||
const [workspacePreviewSaving, setWorkspacePreviewSaving] = useState(false);
|
||||
const [workspacePreviewDraft, setWorkspacePreviewDraft] = useState('');
|
||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
||||
|
|
@ -1201,6 +1210,7 @@ export function BotDashboardModule({
|
|||
() => readCachedPlatformPageSize(0) > 0,
|
||||
);
|
||||
const [chatPullPageSize, setChatPullPageSize] = useState(60);
|
||||
const [commandAutoUnlockSeconds, setCommandAutoUnlockSeconds] = useState(10);
|
||||
const [chatHasMore, setChatHasMore] = useState(false);
|
||||
const [chatLoadingMore, setChatLoadingMore] = useState(false);
|
||||
const [chatDatePickerOpen, setChatDatePickerOpen] = useState(false);
|
||||
|
|
@ -1275,6 +1285,7 @@ export function BotDashboardModule({
|
|||
cpu_cores: clampCpuCores(bot.cpu_cores ?? 1),
|
||||
memory_mb: clampMemoryMb(bot.memory_mb ?? 1024),
|
||||
storage_gb: clampStorageGb(bot.storage_gb ?? 10),
|
||||
system_timezone: bot.system_timezone || defaultSystemTimezone,
|
||||
agents_md: bot.agents_md || '',
|
||||
soul_md: bot.soul_md || bot.system_prompt || '',
|
||||
user_md: bot.user_md || '',
|
||||
|
|
@ -1287,7 +1298,7 @@ export function BotDashboardModule({
|
|||
memory_mb: String(clampMemoryMb(bot.memory_mb ?? 1024)),
|
||||
storage_gb: String(clampStorageGb(bot.storage_gb ?? 10)),
|
||||
});
|
||||
}, []);
|
||||
}, [defaultSystemTimezone]);
|
||||
const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => {
|
||||
const query = [`path=${encodeURIComponent(filePath)}`];
|
||||
if (forceDownload) query.push('download=1');
|
||||
|
|
@ -1313,22 +1324,23 @@ export function BotDashboardModule({
|
|||
};
|
||||
const closeWorkspacePreview = () => {
|
||||
setWorkspacePreview(null);
|
||||
setWorkspacePreviewMode('preview');
|
||||
setWorkspacePreviewFullscreen(false);
|
||||
setWorkspacePreviewEditing(false);
|
||||
setWorkspacePreviewSaving(false);
|
||||
setWorkspacePreviewDraft('');
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!workspacePreview) {
|
||||
setWorkspacePreviewEditing(false);
|
||||
setWorkspacePreviewMode('preview');
|
||||
setWorkspacePreviewSaving(false);
|
||||
setWorkspacePreviewDraft('');
|
||||
return;
|
||||
}
|
||||
setWorkspacePreviewEditing(false);
|
||||
setWorkspacePreviewSaving(false);
|
||||
setWorkspacePreviewDraft(workspacePreview.content || '');
|
||||
}, [workspacePreview?.path, workspacePreview?.content]);
|
||||
const workspacePreviewCanEdit = Boolean(workspacePreview?.isMarkdown && !workspacePreview?.truncated);
|
||||
const workspacePreviewEditorEnabled = workspacePreviewCanEdit && workspacePreviewMode === 'edit';
|
||||
const triggerWorkspaceFileDownload = (filePath: string) => {
|
||||
if (!selectedBotId) return;
|
||||
const normalized = String(filePath || '').trim();
|
||||
|
|
@ -1435,15 +1447,6 @@ export function BotDashboardModule({
|
|||
}
|
||||
return src;
|
||||
}, [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 source = String(text || '');
|
||||
if (!source) return [source];
|
||||
|
|
@ -1583,6 +1586,7 @@ export function BotDashboardModule({
|
|||
cpu_cores: 1,
|
||||
memory_mb: 1024,
|
||||
storage_gb: 10,
|
||||
system_timezone: '',
|
||||
agents_md: '',
|
||||
soul_md: '',
|
||||
user_md: '',
|
||||
|
|
@ -1740,9 +1744,13 @@ export function BotDashboardModule({
|
|||
}, [templateTopicText, effectiveTopicPresetTemplates.length]);
|
||||
const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined;
|
||||
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 isChatEnabled = Boolean(canChat && !isSending);
|
||||
const isChatEnabled = Boolean(canChat && !isSendingBlocked);
|
||||
const activeControlCommand = selectedBot ? controlCommandByBot[selectedBot.id] || '' : '';
|
||||
const canSendControlCommand = Boolean(selectedBot && canChat && !isVoiceRecording && !isVoiceTranscribing);
|
||||
|
||||
|
|
@ -1780,6 +1788,10 @@ export function BotDashboardModule({
|
|||
.sort(([a], [b]) => a.localeCompare(b)),
|
||||
[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 lastAssistantFinalTs = useMemo(
|
||||
() =>
|
||||
|
|
@ -1833,6 +1845,30 @@ export function BotDashboardModule({
|
|||
if (eventText) return eventText;
|
||||
return '-';
|
||||
}, [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 hasTopicUnread = topicFeedUnreadCount > 0;
|
||||
|
||||
|
|
@ -2257,6 +2293,12 @@ export function BotDashboardModule({
|
|||
if (Number.isFinite(configured) && configured > 0) {
|
||||
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(
|
||||
res.data?.chat?.page_size,
|
||||
readCachedPlatformPageSize(10),
|
||||
|
|
@ -2287,6 +2329,10 @@ export function BotDashboardModule({
|
|||
if (Number.isFinite(pullPageSize) && pullPageSize > 0) {
|
||||
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 {
|
||||
// keep default limit
|
||||
} finally {
|
||||
|
|
@ -2427,6 +2473,7 @@ export function BotDashboardModule({
|
|||
if (!selectedBotId || !path) return;
|
||||
const normalizedPath = String(path || '').trim();
|
||||
setWorkspacePreviewFullscreen(false);
|
||||
setWorkspacePreviewMode('preview');
|
||||
if (workspaceFileAction(normalizedPath, workspaceDownloadExtensionSet) === 'download') {
|
||||
triggerWorkspaceFileDownload(normalizedPath);
|
||||
return;
|
||||
|
|
@ -2548,7 +2595,6 @@ export function BotDashboardModule({
|
|||
ext: textExt ? `.${textExt}` : '',
|
||||
isMarkdown: textExt === 'md' || textExt === 'markdown' || Boolean(res.data.is_markdown),
|
||||
});
|
||||
setWorkspacePreviewEditing(false);
|
||||
notify(t.fileSaved, { tone: 'success' });
|
||||
void loadWorkspaceTree(selectedBotId, workspaceCurrentPath);
|
||||
} catch (error: any) {
|
||||
|
|
@ -3927,6 +3973,7 @@ export function BotDashboardModule({
|
|||
updateBotStatus(id, 'RUNNING');
|
||||
await refresh();
|
||||
} catch (error: any) {
|
||||
await refresh();
|
||||
notify(error?.response?.data?.detail || t.startFail, { tone: 'error' });
|
||||
} finally {
|
||||
setOperatingBotId(null);
|
||||
|
|
@ -3958,6 +4005,7 @@ export function BotDashboardModule({
|
|||
updateBotStatus(id, 'RUNNING');
|
||||
await refresh();
|
||||
} catch (error: any) {
|
||||
await refresh();
|
||||
notify(error?.response?.data?.detail || t.restartFail, { tone: 'error' });
|
||||
} finally {
|
||||
setOperatingBotId(null);
|
||||
|
|
@ -3999,7 +4047,7 @@ export function BotDashboardModule({
|
|||
};
|
||||
|
||||
const send = async () => {
|
||||
if (!selectedBot || !canChat || isSending) return;
|
||||
if (!selectedBot || !canChat || isSendingBlocked) return;
|
||||
if (!command.trim() && pendingAttachments.length === 0 && !quotedReply) return;
|
||||
const text = normalizeUserMessageText(command);
|
||||
const quoteText = normalizeAssistantMessageText(quotedReply?.text || '');
|
||||
|
|
@ -4011,7 +4059,11 @@ export function BotDashboardModule({
|
|||
try {
|
||||
chatAutoFollowRef.current = true;
|
||||
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(
|
||||
`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/command`,
|
||||
{ command: payloadText, attachments: pendingAttachments },
|
||||
|
|
@ -4034,6 +4086,11 @@ export function BotDashboardModule({
|
|||
setQuotedReply(null);
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.detail || error?.message || t.sendFail;
|
||||
setCommandAutoUnlockDeadlineByBot((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[selectedBot.id];
|
||||
return next;
|
||||
});
|
||||
addBotMessage(selectedBot.id, {
|
||||
role: 'assistant',
|
||||
text: t.sendFailMsg(msg),
|
||||
|
|
@ -4045,7 +4102,12 @@ export function BotDashboardModule({
|
|||
} finally {
|
||||
setSendingByBot((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;
|
||||
});
|
||||
}
|
||||
|
|
@ -4956,6 +5018,7 @@ export function BotDashboardModule({
|
|||
payload.name = editForm.name;
|
||||
payload.access_password = editForm.access_password;
|
||||
payload.image_tag = editForm.image_tag;
|
||||
payload.system_timezone = editForm.system_timezone.trim() || defaultSystemTimezone;
|
||||
const selectedImageOption = baseImageOptions.find((opt) => opt.tag === editForm.image_tag);
|
||||
if (selectedImageOption?.disabled) {
|
||||
throw new Error(isZh ? '当前镜像不可用,请选择可用镜像。' : 'Selected image is unavailable.');
|
||||
|
|
@ -5783,9 +5846,9 @@ export function BotDashboardModule({
|
|||
<Paperclip size={16} className={isUploadingAttachments ? 'animate-spin' : ''} />
|
||||
</LucentIconButton>
|
||||
<button
|
||||
className={`ops-composer-submit-btn ${isChatEnabled && (isThinking || isSending) ? 'is-interrupt' : ''}`}
|
||||
className={`ops-composer-submit-btn ${showInterruptSubmitAction ? 'is-interrupt' : ''}`}
|
||||
disabled={
|
||||
isChatEnabled && (isThinking || isSending)
|
||||
showInterruptSubmitAction
|
||||
? Boolean(interruptingByBot[selectedBot.id])
|
||||
: (
|
||||
!isChatEnabled
|
||||
|
|
@ -5794,11 +5857,11 @@ export function BotDashboardModule({
|
|||
|| (!command.trim() && pendingAttachments.length === 0 && !quotedReply)
|
||||
)
|
||||
}
|
||||
onClick={() => void (isChatEnabled && (isThinking || isSending) ? interruptExecution() : send())}
|
||||
aria-label={isChatEnabled && (isThinking || isSending) ? t.interrupt : t.send}
|
||||
title={isChatEnabled && (isThinking || isSending) ? t.interrupt : t.send}
|
||||
onClick={() => void (showInterruptSubmitAction ? interruptExecution() : send())}
|
||||
aria-label={showInterruptSubmitAction ? t.interrupt : t.send}
|
||||
title={showInterruptSubmitAction ? t.interrupt : t.send}
|
||||
>
|
||||
{isChatEnabled && (isThinking || isSending) ? (
|
||||
{showInterruptSubmitAction ? (
|
||||
<Square size={15} />
|
||||
) : (
|
||||
<ArrowUp size={18} />
|
||||
|
|
@ -6251,6 +6314,7 @@ export function BotDashboardModule({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section-mini-title">{isZh ? '基础信息' : 'Basic Info'}</div>
|
||||
<label className="field-label">{t.botIdReadonly}</label>
|
||||
<input className="input" value={selectedBot?.id || ''} disabled />
|
||||
|
||||
|
|
@ -6278,6 +6342,24 @@ export function BotDashboardModule({
|
|||
))}
|
||||
</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>
|
||||
<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>
|
||||
))}
|
||||
</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 className="row-between">
|
||||
<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-title-row workspace-preview-header">
|
||||
<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="workspace-path-segments" title={workspacePreview.path}>
|
||||
{renderWorkspacePathSegments(workspacePreview.path, 'preview-path')}
|
||||
|
|
@ -7722,27 +7807,23 @@ export function BotDashboardModule({
|
|||
</span>
|
||||
</div>
|
||||
<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
|
||||
className="btn btn-secondary btn-sm icon-btn"
|
||||
onClick={() => setWorkspacePreviewFullscreen((v) => !v)}
|
||||
tooltip={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
|
||||
aria-label={workspacePreviewFullscreen ? (isZh ? '退出全屏' : 'Exit full screen') : (isZh ? '全屏预览' : 'Full screen')}
|
||||
tooltip={
|
||||
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} />}
|
||||
</LucentIconButton>
|
||||
|
|
@ -7757,7 +7838,7 @@ export function BotDashboardModule({
|
|||
</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 ? (
|
||||
<img
|
||||
|
|
@ -7785,12 +7866,17 @@ export function BotDashboardModule({
|
|||
src={buildWorkspaceRawHref(workspacePreview.path, false)}
|
||||
title={workspacePreview.path}
|
||||
/>
|
||||
) : workspacePreview.isMarkdown && workspacePreviewEditing ? (
|
||||
<textarea
|
||||
className="textarea md-area mono workspace-preview-editor"
|
||||
) : workspacePreviewEditorEnabled ? (
|
||||
<MarkdownLiteEditor
|
||||
className="workspace-preview-editor-shell"
|
||||
textareaClassName="workspace-preview-editor"
|
||||
value={workspacePreviewDraft}
|
||||
onChange={(event) => setWorkspacePreviewDraft(event.target.value)}
|
||||
onChange={setWorkspacePreviewDraft}
|
||||
spellCheck={false}
|
||||
fullHeight
|
||||
onSaveShortcut={() => {
|
||||
void saveWorkspacePreviewMarkdown();
|
||||
}}
|
||||
/>
|
||||
) : workspacePreview.isMarkdown ? (
|
||||
<div className="workspace-markdown">
|
||||
|
|
@ -7798,9 +7884,8 @@ export function BotDashboardModule({
|
|||
remarkPlugins={[remarkGfm]}
|
||||
rehypePlugins={[rehypeRaw, [rehypeSanitize, MARKDOWN_SANITIZE_SCHEMA]]}
|
||||
components={markdownComponents}
|
||||
urlTransform={transformWorkspacePreviewMarkdownUrl}
|
||||
>
|
||||
{workspacePreview.content}
|
||||
{decorateWorkspacePathsForMarkdown(workspacePreview.content || '')}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -7813,13 +7898,13 @@ export function BotDashboardModule({
|
|||
<div className="row-between">
|
||||
<span className="workspace-preview-meta mono">{workspacePreview.ext || '-'}</span>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
{workspacePreview.isMarkdown && workspacePreviewEditing ? (
|
||||
{workspacePreviewEditorEnabled ? (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => {
|
||||
setWorkspacePreviewDraft(workspacePreview.content || '');
|
||||
setWorkspacePreviewEditing(false);
|
||||
setWorkspacePreviewMode('preview');
|
||||
}}
|
||||
disabled={workspacePreviewSaving}
|
||||
>
|
||||
|
|
@ -7834,6 +7919,13 @@ export function BotDashboardModule({
|
|||
<span style={{ marginLeft: 6 }}>{t.save}</span>
|
||||
</button>
|
||||
</>
|
||||
) : workspacePreviewCanEdit ? (
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setWorkspacePreviewMode('edit')}
|
||||
>
|
||||
{t.editFile}
|
||||
</button>
|
||||
) : null}
|
||||
{workspacePreview.isHtml ? (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import { wizardEn } from '../../i18n/wizard.en';
|
|||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||
import { LucentSelect } from '../../components/lucent/LucentSelect';
|
||||
import { MarkdownLiteEditor } from '../../components/markdown/MarkdownLiteEditor';
|
||||
import { PasswordInput } from '../../components/PasswordInput';
|
||||
import { getSystemTimezoneOptions } from '../../utils/systemTimezones';
|
||||
|
||||
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
||||
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
||||
|
|
@ -39,6 +41,9 @@ interface SystemDefaultsResponse {
|
|||
tools_md?: string;
|
||||
identity_md?: string;
|
||||
};
|
||||
bot?: {
|
||||
system_timezone?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const providerPresets: Record<string, { model: string; note: { 'zh-cn': string; en: string }; apiBase?: string }> = {
|
||||
|
|
@ -113,6 +118,7 @@ const initialForm = {
|
|||
cpu_cores: 1,
|
||||
memory_mb: 1024,
|
||||
storage_gb: 10,
|
||||
system_timezone: '',
|
||||
|
||||
soul_md: '',
|
||||
agents_md: '',
|
||||
|
|
@ -156,6 +162,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
const [storageGbDraft, setStorageGbDraft] = useState(String(initialForm.storage_gb));
|
||||
const [botIdStatus, setBotIdStatus] = useState<'idle' | 'checking' | 'available' | 'exists' | 'invalid'>('idle');
|
||||
const [botIdStatusText, setBotIdStatusText] = useState('');
|
||||
const [defaultSystemTimezone, setDefaultSystemTimezone] = useState('Asia/Shanghai');
|
||||
|
||||
const readyImages = useMemo(() => images.filter((img) => img.status === 'READY'), [images]);
|
||||
const isZh = locale === 'zh';
|
||||
|
|
@ -166,6 +173,10 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
: { show: 'Show password', hide: 'Hide password' };
|
||||
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 systemTimezoneOptions = useMemo(
|
||||
() => getSystemTimezoneOptions(form.system_timezone || defaultSystemTimezone),
|
||||
[defaultSystemTimezone, form.system_timezone],
|
||||
);
|
||||
const addableChannelTypes = useMemo(
|
||||
() => optionalChannelTypes.filter((t) => !activeChannelTypes.has(t)),
|
||||
[activeChannelTypes],
|
||||
|
|
@ -183,11 +194,14 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
try {
|
||||
const res = await axios.get<SystemDefaultsResponse>(`${APP_ENDPOINTS.apiBase}/system/defaults`);
|
||||
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();
|
||||
setDefaultSystemTimezone(nextSystemTimezone);
|
||||
setDefaultAgentsTemplate(agentsTemplate);
|
||||
setForm((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
system_timezone: prev.system_timezone || nextSystemTimezone,
|
||||
soul_md: String(tpl.soul_md || '').trim() || prev.soul_md,
|
||||
agents_md: agentsTemplate,
|
||||
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),
|
||||
memory_mb: Number(form.memory_mb),
|
||||
storage_gb: Number(form.storage_gb),
|
||||
system_timezone: form.system_timezone.trim() || defaultSystemTimezone,
|
||||
soul_md: form.soul_md,
|
||||
agents_md: form.agents_md,
|
||||
user_md: form.user_md,
|
||||
|
|
@ -367,7 +382,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
}
|
||||
onCreated?.();
|
||||
onGoDashboard?.();
|
||||
setForm(initialForm);
|
||||
setForm({ ...initialForm, system_timezone: defaultSystemTimezone });
|
||||
setMaxTokensDraft(String(initialForm.max_tokens));
|
||||
setCpuCoresDraft(String(initialForm.cpu_cores));
|
||||
setMemoryMbDraft(String(initialForm.memory_mb));
|
||||
|
|
@ -674,6 +689,24 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
</div>
|
||||
<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 }}>
|
||||
{isZh ? '资源配额' : 'Resource Limits'}
|
||||
</div>
|
||||
|
|
@ -834,10 +867,9 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<textarea
|
||||
className="textarea md-area"
|
||||
<MarkdownLiteEditor
|
||||
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>
|
||||
|
|
@ -857,6 +889,9 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
<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 ? '存储' : '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.tools}: {envEntries.map(([k]) => k).join(', ') || '-'}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { RefreshCw, X } from 'lucide-react';
|
|||
import { APP_ENDPOINTS } from '../../../config/env';
|
||||
import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
|
||||
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider';
|
||||
import { MarkdownLiteEditor } from '../../../components/markdown/MarkdownLiteEditor';
|
||||
|
||||
interface TemplateManagerModalProps {
|
||||
isZh: boolean;
|
||||
|
|
@ -173,11 +174,11 @@ export function TemplateManagerModal({ isZh, open, onClose }: TemplateManagerMod
|
|||
placeholder='{"presets":[...]}'
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
className="textarea md-area mono platform-template-textarea"
|
||||
rows={18}
|
||||
<MarkdownLiteEditor
|
||||
className="platform-template-textarea"
|
||||
value={templates[activeTab] || ''}
|
||||
onChange={(event) => setTemplates((prev) => ({ ...prev, [activeTab]: event.target.value }))}
|
||||
rows={18}
|
||||
onChange={(nextValue) => setTemplates((prev) => ({ ...prev, [activeTab]: nextValue }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export interface PlatformSettings {
|
||||
page_size: number;
|
||||
chat_pull_page_size: number;
|
||||
command_auto_unlock_seconds: number;
|
||||
upload_max_mb: number;
|
||||
allowed_attachment_extensions: string[];
|
||||
workspace_download_extensions: string[];
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export interface BotState {
|
|||
cpu_cores?: number;
|
||||
memory_mb?: number;
|
||||
storage_gb?: number;
|
||||
system_timezone?: string;
|
||||
send_progress?: boolean;
|
||||
send_tool_hints?: boolean;
|
||||
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