v0.1.2
parent
93e9547a7f
commit
3cb25ba795
|
|
@ -1,4 +1,3 @@
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
@ -50,41 +49,6 @@ class BotConfigManager:
|
||||||
"sendToolHints": bool(bot_data.get("send_tool_hints", False)),
|
"sendToolHints": bool(bot_data.get("send_tool_hints", False)),
|
||||||
}
|
}
|
||||||
|
|
||||||
tools_cfg: Dict[str, Any] = {
|
|
||||||
"web": {
|
|
||||||
"search": {
|
|
||||||
"apiKey": "",
|
|
||||||
"maxResults": 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
raw_tools_cfg = bot_data.get("tools_config_json")
|
|
||||||
parsed_tools_cfg: Dict[str, Any] = {}
|
|
||||||
if isinstance(raw_tools_cfg, str) and raw_tools_cfg.strip():
|
|
||||||
try:
|
|
||||||
loaded = json.loads(raw_tools_cfg)
|
|
||||||
if isinstance(loaded, dict):
|
|
||||||
parsed_tools_cfg = loaded
|
|
||||||
except Exception:
|
|
||||||
parsed_tools_cfg = {}
|
|
||||||
elif isinstance(raw_tools_cfg, dict):
|
|
||||||
parsed_tools_cfg = raw_tools_cfg
|
|
||||||
|
|
||||||
web_cfg = parsed_tools_cfg.get("web") if isinstance(parsed_tools_cfg, dict) else None
|
|
||||||
if isinstance(web_cfg, dict):
|
|
||||||
search_cfg = web_cfg.get("search")
|
|
||||||
if isinstance(search_cfg, dict):
|
|
||||||
api_key = str(search_cfg.get("apiKey") or search_cfg.get("api_key") or "").strip()
|
|
||||||
max_results_raw = search_cfg.get("maxResults", search_cfg.get("max_results", 5))
|
|
||||||
try:
|
|
||||||
max_results = int(max_results_raw)
|
|
||||||
except Exception:
|
|
||||||
max_results = 5
|
|
||||||
max_results = max(1, min(max_results, 10))
|
|
||||||
tools_cfg["web"]["search"]["apiKey"] = api_key
|
|
||||||
tools_cfg["web"]["search"]["maxResults"] = max_results
|
|
||||||
|
|
||||||
config_data: Dict[str, Any] = {
|
config_data: Dict[str, Any] = {
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
|
|
@ -98,7 +62,6 @@ class BotConfigManager:
|
||||||
provider_name: provider_cfg,
|
provider_name: provider_cfg,
|
||||||
},
|
},
|
||||||
"channels": channels_cfg,
|
"channels": channels_cfg,
|
||||||
"tools": tools_cfg,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,12 @@ engine = create_engine(DATABASE_URL, echo=DATABASE_ECHO)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_botinstance_columns() -> None:
|
def _ensure_botinstance_columns() -> None:
|
||||||
|
if engine.dialect.name != "sqlite":
|
||||||
|
return
|
||||||
required_columns = {
|
required_columns = {
|
||||||
"current_state": "TEXT DEFAULT 'IDLE'",
|
"current_state": "TEXT DEFAULT 'IDLE'",
|
||||||
"last_action": "TEXT",
|
"last_action": "TEXT",
|
||||||
"soul_md": "TEXT",
|
"image_tag": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
||||||
"agents_md": "TEXT",
|
|
||||||
"user_md": "TEXT",
|
|
||||||
"tools_md": "TEXT",
|
|
||||||
"tools_config_json": "TEXT",
|
|
||||||
"identity_md": "TEXT",
|
|
||||||
"api_base": "TEXT",
|
|
||||||
"temperature": "REAL DEFAULT 0.2",
|
|
||||||
"top_p": "REAL DEFAULT 1.0",
|
|
||||||
"max_tokens": "INTEGER DEFAULT 8192",
|
|
||||||
"presence_penalty": "REAL DEFAULT 0.0",
|
|
||||||
"frequency_penalty": "REAL DEFAULT 0.0",
|
|
||||||
"send_progress": "BOOLEAN DEFAULT 0",
|
|
||||||
"send_tool_hints": "BOOLEAN DEFAULT 0",
|
|
||||||
}
|
}
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
||||||
|
|
@ -38,7 +27,45 @@ def _ensure_botinstance_columns() -> None:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_legacy_botinstance_columns() -> None:
|
||||||
|
if engine.dialect.name != "sqlite":
|
||||||
|
return
|
||||||
|
legacy_columns = [
|
||||||
|
"avatar_model",
|
||||||
|
"avatar_skin",
|
||||||
|
"system_prompt",
|
||||||
|
"soul_md",
|
||||||
|
"agents_md",
|
||||||
|
"user_md",
|
||||||
|
"tools_md",
|
||||||
|
"tools_config_json",
|
||||||
|
"identity_md",
|
||||||
|
"llm_provider",
|
||||||
|
"llm_model",
|
||||||
|
"api_key",
|
||||||
|
"api_base",
|
||||||
|
"temperature",
|
||||||
|
"top_p",
|
||||||
|
"max_tokens",
|
||||||
|
"presence_penalty",
|
||||||
|
"frequency_penalty",
|
||||||
|
"send_progress",
|
||||||
|
"send_tool_hints",
|
||||||
|
"bot_env_json",
|
||||||
|
]
|
||||||
|
with engine.connect() as conn:
|
||||||
|
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
||||||
|
existing = {str(row[1]) for row in existing_rows}
|
||||||
|
for col in legacy_columns:
|
||||||
|
if col not in existing:
|
||||||
|
continue
|
||||||
|
conn.execute(text(f'ALTER TABLE botinstance DROP COLUMN "{col}"'))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def _ensure_botmessage_columns() -> None:
|
def _ensure_botmessage_columns() -> None:
|
||||||
|
if engine.dialect.name != "sqlite":
|
||||||
|
return
|
||||||
required_columns = {
|
required_columns = {
|
||||||
"media_json": "TEXT",
|
"media_json": "TEXT",
|
||||||
}
|
}
|
||||||
|
|
@ -60,18 +87,11 @@ def _drop_legacy_skill_tables() -> None:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def _drop_legacy_channel_tables() -> None:
|
|
||||||
"""Drop deprecated channel route table (channel config source moved to config.json)."""
|
|
||||||
with engine.connect() as conn:
|
|
||||||
conn.execute(text("DROP TABLE IF EXISTS channelroute"))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def init_database() -> None:
|
def init_database() -> None:
|
||||||
SQLModel.metadata.create_all(engine)
|
SQLModel.metadata.create_all(engine)
|
||||||
_drop_legacy_skill_tables()
|
_drop_legacy_skill_tables()
|
||||||
_drop_legacy_channel_tables()
|
|
||||||
_ensure_botinstance_columns()
|
_ensure_botinstance_columns()
|
||||||
|
_drop_legacy_botinstance_columns()
|
||||||
_ensure_botmessage_columns()
|
_ensure_botmessage_columns()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
327
backend/main.py
327
backend/main.py
|
|
@ -128,6 +128,13 @@ class CommandRequest(BaseModel):
|
||||||
attachments: Optional[List[str]] = None
|
attachments: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_packet_channel(packet: Dict[str, Any]) -> str:
|
||||||
|
raw = str(packet.get("channel") or packet.get("source") or "").strip().lower()
|
||||||
|
if raw in {"dashboard", "dashboard_channel", "dashboard-channel"}:
|
||||||
|
return "dashboard"
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
def _normalize_media_item(bot_id: str, value: Any) -> str:
|
def _normalize_media_item(bot_id: str, value: Any) -> str:
|
||||||
raw = str(value or "").strip().replace("\\", "/")
|
raw = str(value or "").strip().replace("\\", "/")
|
||||||
if not raw:
|
if not raw:
|
||||||
|
|
@ -159,7 +166,7 @@ def _persist_runtime_packet(bot_id: str, packet: Dict[str, Any]):
|
||||||
packet_type = str(packet.get("type", "")).upper()
|
packet_type = str(packet.get("type", "")).upper()
|
||||||
if packet_type not in {"AGENT_STATE", "ASSISTANT_MESSAGE", "USER_COMMAND", "BUS_EVENT"}:
|
if packet_type not in {"AGENT_STATE", "ASSISTANT_MESSAGE", "USER_COMMAND", "BUS_EVENT"}:
|
||||||
return
|
return
|
||||||
source_channel = str(packet.get("channel") or "").strip().lower()
|
source_channel = _normalize_packet_channel(packet)
|
||||||
if source_channel != "dashboard":
|
if source_channel != "dashboard":
|
||||||
return
|
return
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
|
@ -341,6 +348,20 @@ def _normalize_channel_extra(raw: Any) -> Dict[str, Any]:
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _read_global_delivery_flags(channels_cfg: Any) -> tuple[bool, bool]:
|
||||||
|
if not isinstance(channels_cfg, dict):
|
||||||
|
return False, False
|
||||||
|
send_progress = channels_cfg.get("sendProgress")
|
||||||
|
send_tool_hints = channels_cfg.get("sendToolHints")
|
||||||
|
dashboard_cfg = channels_cfg.get("dashboard")
|
||||||
|
if isinstance(dashboard_cfg, dict):
|
||||||
|
if send_progress is None and "sendProgress" in dashboard_cfg:
|
||||||
|
send_progress = dashboard_cfg.get("sendProgress")
|
||||||
|
if send_tool_hints is None and "sendToolHints" in dashboard_cfg:
|
||||||
|
send_tool_hints = dashboard_cfg.get("sendToolHints")
|
||||||
|
return bool(send_progress), bool(send_tool_hints)
|
||||||
|
|
||||||
|
|
||||||
def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Dict[str, Any]:
|
def _channel_cfg_to_api_dict(bot_id: str, ctype: str, cfg: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
ctype = str(ctype or "").strip().lower()
|
ctype = str(ctype or "").strip().lower()
|
||||||
enabled = bool(cfg.get("enabled", True))
|
enabled = bool(cfg.get("enabled", True))
|
||||||
|
|
@ -472,8 +493,7 @@ def _get_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]:
|
||||||
if not isinstance(channels_cfg, dict):
|
if not isinstance(channels_cfg, dict):
|
||||||
channels_cfg = {}
|
channels_cfg = {}
|
||||||
|
|
||||||
send_progress = bool(channels_cfg.get("sendProgress", bot.send_progress))
|
send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
||||||
send_tool_hints = bool(channels_cfg.get("sendToolHints", bot.send_tool_hints))
|
|
||||||
rows: List[Dict[str, Any]] = [
|
rows: List[Dict[str, Any]] = [
|
||||||
{
|
{
|
||||||
"id": "dashboard",
|
"id": "dashboard",
|
||||||
|
|
@ -492,7 +512,7 @@ def _get_bot_channels_from_config(bot: BotInstance) -> List[Dict[str, Any]]:
|
||||||
]
|
]
|
||||||
|
|
||||||
for ctype, cfg in channels_cfg.items():
|
for ctype, cfg in channels_cfg.items():
|
||||||
if ctype in {"sendProgress", "sendToolHints"}:
|
if ctype in {"sendProgress", "sendToolHints", "dashboard"}:
|
||||||
continue
|
continue
|
||||||
if not isinstance(cfg, dict):
|
if not isinstance(cfg, dict):
|
||||||
continue
|
continue
|
||||||
|
|
@ -595,23 +615,161 @@ def _parse_env_params(raw: Any) -> Dict[str, str]:
|
||||||
return _normalize_env_params(raw)
|
return _normalize_env_params(raw)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(raw: Any, default: float) -> float:
|
||||||
|
try:
|
||||||
|
return float(raw)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_int(raw: Any, default: int) -> int:
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _read_workspace_md(bot_id: str, filename: str, default_value: str) -> str:
|
||||||
|
path = os.path.join(_workspace_root(bot_id), filename)
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return default_value
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
|
||||||
|
def _read_bot_runtime_snapshot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
|
config_data = _read_bot_config(bot.id)
|
||||||
|
|
||||||
|
provider_name = ""
|
||||||
|
provider_cfg: Dict[str, Any] = {}
|
||||||
|
providers_cfg = config_data.get("providers")
|
||||||
|
if isinstance(providers_cfg, dict):
|
||||||
|
for p_name, p_cfg in providers_cfg.items():
|
||||||
|
provider_name = str(p_name or "").strip()
|
||||||
|
if isinstance(p_cfg, dict):
|
||||||
|
provider_cfg = p_cfg
|
||||||
|
break
|
||||||
|
|
||||||
|
agents_defaults: Dict[str, Any] = {}
|
||||||
|
agents_cfg = config_data.get("agents")
|
||||||
|
if isinstance(agents_cfg, dict):
|
||||||
|
defaults = agents_cfg.get("defaults")
|
||||||
|
if isinstance(defaults, dict):
|
||||||
|
agents_defaults = defaults
|
||||||
|
|
||||||
|
channels_cfg = config_data.get("channels")
|
||||||
|
send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
||||||
|
|
||||||
|
tools_cfg = _normalize_tools_config(config_data.get("tools"))
|
||||||
|
llm_provider = provider_name or "dashscope"
|
||||||
|
llm_model = str(agents_defaults.get("model") or "")
|
||||||
|
api_key = str(provider_cfg.get("apiKey") or "").strip()
|
||||||
|
api_base = str(provider_cfg.get("apiBase") or "").strip()
|
||||||
|
|
||||||
|
soul_md = _read_workspace_md(bot.id, "SOUL.md", DEFAULT_SOUL_MD)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"llm_provider": llm_provider,
|
||||||
|
"llm_model": llm_model,
|
||||||
|
"api_key": api_key,
|
||||||
|
"api_base": api_base,
|
||||||
|
"temperature": _safe_float(agents_defaults.get("temperature"), 0.2),
|
||||||
|
"top_p": _safe_float(agents_defaults.get("topP"), 1.0),
|
||||||
|
"max_tokens": _safe_int(agents_defaults.get("maxTokens"), 8192),
|
||||||
|
"send_progress": send_progress,
|
||||||
|
"send_tool_hints": send_tool_hints,
|
||||||
|
"soul_md": soul_md,
|
||||||
|
"agents_md": _read_workspace_md(bot.id, "AGENTS.md", DEFAULT_AGENTS_MD),
|
||||||
|
"user_md": _read_workspace_md(bot.id, "USER.md", DEFAULT_USER_MD),
|
||||||
|
"tools_md": _read_workspace_md(bot.id, "TOOLS.md", DEFAULT_TOOLS_MD),
|
||||||
|
"identity_md": _read_workspace_md(bot.id, "IDENTITY.md", DEFAULT_IDENTITY_MD),
|
||||||
|
"system_prompt": soul_md,
|
||||||
|
"tools_config_json": json.dumps(tools_cfg, ensure_ascii=False),
|
||||||
|
"tools_config": tools_cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
|
runtime = _read_bot_runtime_snapshot(bot)
|
||||||
|
return {
|
||||||
|
"id": bot.id,
|
||||||
|
"name": bot.name,
|
||||||
|
"avatar_model": "base",
|
||||||
|
"avatar_skin": "blue_suit",
|
||||||
|
"image_tag": bot.image_tag,
|
||||||
|
"llm_provider": runtime.get("llm_provider") or "",
|
||||||
|
"llm_model": runtime.get("llm_model") or "",
|
||||||
|
"system_prompt": runtime.get("system_prompt") or "",
|
||||||
|
"api_base": runtime.get("api_base") or "",
|
||||||
|
"temperature": _safe_float(runtime.get("temperature"), 0.2),
|
||||||
|
"top_p": _safe_float(runtime.get("top_p"), 1.0),
|
||||||
|
"max_tokens": _safe_int(runtime.get("max_tokens"), 8192),
|
||||||
|
"send_progress": bool(runtime.get("send_progress")),
|
||||||
|
"send_tool_hints": bool(runtime.get("send_tool_hints")),
|
||||||
|
"soul_md": runtime.get("soul_md") or "",
|
||||||
|
"agents_md": runtime.get("agents_md") or "",
|
||||||
|
"user_md": runtime.get("user_md") or "",
|
||||||
|
"tools_md": runtime.get("tools_md") or "",
|
||||||
|
"identity_md": runtime.get("identity_md") or "",
|
||||||
|
"workspace_dir": bot.workspace_dir,
|
||||||
|
"docker_status": bot.docker_status,
|
||||||
|
"current_state": bot.current_state,
|
||||||
|
"last_action": bot.last_action,
|
||||||
|
"created_at": bot.created_at,
|
||||||
|
"updated_at": bot.updated_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _sync_workspace_channels(
|
def _sync_workspace_channels(
|
||||||
session: Session,
|
session: Session,
|
||||||
bot_id: str,
|
bot_id: str,
|
||||||
channels_override: Optional[List[Dict[str, Any]]] = None,
|
channels_override: Optional[List[Dict[str, Any]]] = None,
|
||||||
|
global_delivery_override: Optional[Dict[str, Any]] = None,
|
||||||
|
runtime_overrides: Optional[Dict[str, Any]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
return
|
return
|
||||||
|
snapshot = _read_bot_runtime_snapshot(bot)
|
||||||
|
bot_data: Dict[str, Any] = {
|
||||||
|
"name": bot.name,
|
||||||
|
"system_prompt": snapshot.get("system_prompt") or DEFAULT_SOUL_MD,
|
||||||
|
"soul_md": snapshot.get("soul_md") or DEFAULT_SOUL_MD,
|
||||||
|
"agents_md": snapshot.get("agents_md") or DEFAULT_AGENTS_MD,
|
||||||
|
"user_md": snapshot.get("user_md") or DEFAULT_USER_MD,
|
||||||
|
"tools_md": snapshot.get("tools_md") or DEFAULT_TOOLS_MD,
|
||||||
|
"identity_md": snapshot.get("identity_md") or DEFAULT_IDENTITY_MD,
|
||||||
|
"llm_provider": snapshot.get("llm_provider") or "dashscope",
|
||||||
|
"llm_model": snapshot.get("llm_model") or "",
|
||||||
|
"api_key": snapshot.get("api_key") or "",
|
||||||
|
"api_base": snapshot.get("api_base") or "",
|
||||||
|
"temperature": _safe_float(snapshot.get("temperature"), 0.2),
|
||||||
|
"top_p": _safe_float(snapshot.get("top_p"), 1.0),
|
||||||
|
"max_tokens": _safe_int(snapshot.get("max_tokens"), 8192),
|
||||||
|
"send_progress": bool(snapshot.get("send_progress")),
|
||||||
|
"send_tool_hints": bool(snapshot.get("send_tool_hints")),
|
||||||
|
"tools_config_json": json.dumps(_normalize_tools_config(snapshot.get("tools_config")), ensure_ascii=False),
|
||||||
|
}
|
||||||
|
if isinstance(runtime_overrides, dict):
|
||||||
|
for key, value in runtime_overrides.items():
|
||||||
|
bot_data[key] = value
|
||||||
|
if "tools_config" in bot_data:
|
||||||
|
bot_data["tools_config_json"] = json.dumps(_normalize_tools_config(bot_data.get("tools_config")), ensure_ascii=False)
|
||||||
|
|
||||||
|
send_progress = bool(bot_data.get("send_progress", False))
|
||||||
|
send_tool_hints = bool(bot_data.get("send_tool_hints", False))
|
||||||
|
if isinstance(global_delivery_override, dict):
|
||||||
|
if "sendProgress" in global_delivery_override:
|
||||||
|
send_progress = bool(global_delivery_override.get("sendProgress"))
|
||||||
|
if "sendToolHints" in global_delivery_override:
|
||||||
|
send_tool_hints = bool(global_delivery_override.get("sendToolHints"))
|
||||||
|
|
||||||
channels_data = channels_override if channels_override is not None else _get_bot_channels_from_config(bot)
|
channels_data = channels_override if channels_override is not None else _get_bot_channels_from_config(bot)
|
||||||
bot_data = bot.model_dump()
|
bot_data["send_progress"] = send_progress
|
||||||
for row in channels_data:
|
bot_data["send_tool_hints"] = send_tool_hints
|
||||||
if str(row.get("channel_type") or "").strip().lower() != "dashboard":
|
|
||||||
continue
|
|
||||||
extra = _normalize_channel_extra(row.get("extra_config"))
|
|
||||||
bot_data["send_progress"] = bool(extra.get("sendProgress", bot.send_progress))
|
|
||||||
bot_data["send_tool_hints"] = bool(extra.get("sendToolHints", bot.send_tool_hints))
|
|
||||||
break
|
|
||||||
normalized_channels: List[Dict[str, Any]] = []
|
normalized_channels: List[Dict[str, Any]] = []
|
||||||
for row in channels_data:
|
for row in channels_data:
|
||||||
ctype = str(row.get("channel_type") or "").strip().lower()
|
ctype = str(row.get("channel_type") or "").strip().lower()
|
||||||
|
|
@ -979,7 +1137,7 @@ async def test_provider(payload: dict):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/bots", response_model=BotInstance)
|
@app.post("/api/bots")
|
||||||
def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)):
|
def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session)):
|
||||||
image_row = session.get(NanobotImage, payload.image_tag)
|
image_row = session.get(NanobotImage, payload.image_tag)
|
||||||
if not image_row:
|
if not image_row:
|
||||||
|
|
@ -992,22 +1150,6 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
||||||
bot = BotInstance(
|
bot = BotInstance(
|
||||||
id=payload.id,
|
id=payload.id,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
system_prompt=payload.system_prompt or payload.soul_md or DEFAULT_SOUL_MD,
|
|
||||||
soul_md=payload.soul_md or DEFAULT_SOUL_MD,
|
|
||||||
agents_md=payload.agents_md or DEFAULT_AGENTS_MD,
|
|
||||||
user_md=payload.user_md or DEFAULT_USER_MD,
|
|
||||||
tools_md=payload.tools_md or DEFAULT_TOOLS_MD,
|
|
||||||
tools_config_json=json.dumps(_normalize_tools_config(payload.tools_config), ensure_ascii=False),
|
|
||||||
identity_md=payload.identity_md or DEFAULT_IDENTITY_MD,
|
|
||||||
llm_provider=payload.llm_provider,
|
|
||||||
llm_model=payload.llm_model,
|
|
||||||
api_key=payload.api_key,
|
|
||||||
api_base=payload.api_base,
|
|
||||||
temperature=payload.temperature,
|
|
||||||
top_p=payload.top_p,
|
|
||||||
max_tokens=payload.max_tokens,
|
|
||||||
send_progress=bool(payload.send_progress) if payload.send_progress is not None else False,
|
|
||||||
send_tool_hints=bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False,
|
|
||||||
image_tag=payload.image_tag,
|
image_tag=payload.image_tag,
|
||||||
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, payload.id),
|
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, payload.id),
|
||||||
)
|
)
|
||||||
|
|
@ -1016,16 +1158,44 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(bot)
|
session.refresh(bot)
|
||||||
_write_env_store(payload.id, _normalize_env_params(payload.env_params))
|
_write_env_store(payload.id, _normalize_env_params(payload.env_params))
|
||||||
_sync_workspace_channels(session, payload.id, channels_override=_normalize_initial_channels(payload.id, payload.channels))
|
_sync_workspace_channels(
|
||||||
return bot
|
session,
|
||||||
|
payload.id,
|
||||||
|
channels_override=_normalize_initial_channels(payload.id, payload.channels),
|
||||||
|
global_delivery_override={
|
||||||
|
"sendProgress": bool(payload.send_progress) if payload.send_progress is not None else False,
|
||||||
|
"sendToolHints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False,
|
||||||
|
},
|
||||||
|
runtime_overrides={
|
||||||
|
"llm_provider": payload.llm_provider,
|
||||||
|
"llm_model": payload.llm_model,
|
||||||
|
"api_key": payload.api_key,
|
||||||
|
"api_base": payload.api_base or "",
|
||||||
|
"temperature": payload.temperature,
|
||||||
|
"top_p": payload.top_p,
|
||||||
|
"max_tokens": payload.max_tokens,
|
||||||
|
"system_prompt": payload.system_prompt or payload.soul_md or DEFAULT_SOUL_MD,
|
||||||
|
"soul_md": payload.soul_md or payload.system_prompt or DEFAULT_SOUL_MD,
|
||||||
|
"agents_md": payload.agents_md or DEFAULT_AGENTS_MD,
|
||||||
|
"user_md": payload.user_md or DEFAULT_USER_MD,
|
||||||
|
"tools_md": payload.tools_md or DEFAULT_TOOLS_MD,
|
||||||
|
"identity_md": payload.identity_md or DEFAULT_IDENTITY_MD,
|
||||||
|
"tools_config_json": json.dumps(_normalize_tools_config(payload.tools_config), ensure_ascii=False),
|
||||||
|
"send_progress": bool(payload.send_progress) if payload.send_progress is not None else False,
|
||||||
|
"send_tool_hints": bool(payload.send_tool_hints) if payload.send_tool_hints is not None else False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
session.refresh(bot)
|
||||||
|
return _serialize_bot(bot)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/bots", response_model=List[BotInstance])
|
@app.get("/api/bots")
|
||||||
def list_bots(session: Session = Depends(get_session)):
|
def list_bots(session: Session = Depends(get_session)):
|
||||||
return session.exec(select(BotInstance)).all()
|
bots = session.exec(select(BotInstance)).all()
|
||||||
|
return [_serialize_bot(bot) for bot in bots]
|
||||||
|
|
||||||
|
|
||||||
@app.put("/api/bots/{bot_id}", response_model=BotInstance)
|
@app.put("/api/bots/{bot_id}")
|
||||||
def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depends(get_session)):
|
def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
|
|
@ -1043,12 +1213,41 @@ def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depend
|
||||||
if not docker_manager.has_image(image_tag):
|
if not docker_manager.has_image(image_tag):
|
||||||
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
|
||||||
|
runtime_overrides: Dict[str, Any] = {}
|
||||||
tools_config = update_data.pop("tools_config", None) if isinstance(update_data, dict) else None
|
tools_config = update_data.pop("tools_config", None) if isinstance(update_data, dict) else None
|
||||||
if tools_config is not None:
|
if tools_config is not None:
|
||||||
bot.tools_config_json = json.dumps(_normalize_tools_config(tools_config), ensure_ascii=False)
|
runtime_overrides["tools_config_json"] = json.dumps(_normalize_tools_config(tools_config), ensure_ascii=False)
|
||||||
env_params = update_data.pop("env_params", None) if isinstance(update_data, dict) else None
|
|
||||||
|
|
||||||
|
runtime_fields = {
|
||||||
|
"llm_provider",
|
||||||
|
"llm_model",
|
||||||
|
"api_key",
|
||||||
|
"api_base",
|
||||||
|
"temperature",
|
||||||
|
"top_p",
|
||||||
|
"max_tokens",
|
||||||
|
"soul_md",
|
||||||
|
"agents_md",
|
||||||
|
"user_md",
|
||||||
|
"tools_md",
|
||||||
|
"identity_md",
|
||||||
|
"send_progress",
|
||||||
|
"send_tool_hints",
|
||||||
|
"system_prompt",
|
||||||
|
}
|
||||||
|
for field in runtime_fields:
|
||||||
|
if field in update_data:
|
||||||
|
runtime_overrides[field] = update_data.pop(field)
|
||||||
|
|
||||||
|
if "system_prompt" in runtime_overrides and "soul_md" not in runtime_overrides:
|
||||||
|
runtime_overrides["soul_md"] = runtime_overrides["system_prompt"]
|
||||||
|
if "soul_md" in runtime_overrides and "system_prompt" not in runtime_overrides:
|
||||||
|
runtime_overrides["system_prompt"] = runtime_overrides["soul_md"]
|
||||||
|
|
||||||
|
db_fields = {"name", "image_tag"}
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
|
if key in db_fields:
|
||||||
setattr(bot, key, value)
|
setattr(bot, key, value)
|
||||||
|
|
||||||
session.add(bot)
|
session.add(bot)
|
||||||
|
|
@ -1056,8 +1255,22 @@ def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depend
|
||||||
session.refresh(bot)
|
session.refresh(bot)
|
||||||
if env_params is not None:
|
if env_params is not None:
|
||||||
_write_env_store(bot_id, _normalize_env_params(env_params))
|
_write_env_store(bot_id, _normalize_env_params(env_params))
|
||||||
_sync_workspace_channels(session, bot_id)
|
global_delivery_override: Optional[Dict[str, Any]] = None
|
||||||
return bot
|
if "send_progress" in runtime_overrides or "send_tool_hints" in runtime_overrides:
|
||||||
|
global_delivery_override = {}
|
||||||
|
if "send_progress" in runtime_overrides:
|
||||||
|
global_delivery_override["sendProgress"] = bool(runtime_overrides.get("send_progress"))
|
||||||
|
if "send_tool_hints" in runtime_overrides:
|
||||||
|
global_delivery_override["sendToolHints"] = bool(runtime_overrides.get("send_tool_hints"))
|
||||||
|
|
||||||
|
_sync_workspace_channels(
|
||||||
|
session,
|
||||||
|
bot_id,
|
||||||
|
runtime_overrides=runtime_overrides if runtime_overrides else None,
|
||||||
|
global_delivery_override=global_delivery_override,
|
||||||
|
)
|
||||||
|
session.refresh(bot)
|
||||||
|
return _serialize_bot(bot)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/bots/{bot_id}/start")
|
@app.post("/api/bots/{bot_id}/start")
|
||||||
|
|
@ -1155,7 +1368,9 @@ def get_bot_tools_config(bot_id: str, session: Session = Depends(get_session)):
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
return {
|
return {
|
||||||
"bot_id": bot_id,
|
"bot_id": bot_id,
|
||||||
"tools_config": _parse_tools_config(bot.tools_config_json),
|
"tools_config": {},
|
||||||
|
"managed_by_dashboard": False,
|
||||||
|
"hint": "Tools config is disabled in dashboard. Configure tool-related env vars manually.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1164,19 +1379,10 @@ def update_bot_tools_config(bot_id: str, payload: BotToolsConfigUpdateRequest, s
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
raise HTTPException(
|
||||||
normalized = _normalize_tools_config(payload.tools_config)
|
status_code=400,
|
||||||
bot.tools_config_json = json.dumps(normalized, ensure_ascii=False)
|
detail="Tools config is no longer managed by dashboard. Please set required env vars manually.",
|
||||||
session.add(bot)
|
)
|
||||||
session.commit()
|
|
||||||
session.refresh(bot)
|
|
||||||
|
|
||||||
_sync_workspace_channels(session, bot_id)
|
|
||||||
return {
|
|
||||||
"status": "updated",
|
|
||||||
"bot_id": bot_id,
|
|
||||||
"tools_config": normalized,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/bots/{bot_id}/env-params")
|
@app.get("/api/bots/{bot_id}/env-params")
|
||||||
|
|
@ -1391,19 +1597,20 @@ def update_bot_channel(
|
||||||
row["id"] = new_type
|
row["id"] = new_type
|
||||||
row["locked"] = new_type == "dashboard"
|
row["locked"] = new_type == "dashboard"
|
||||||
|
|
||||||
if new_type == "dashboard":
|
|
||||||
extra = _normalize_channel_extra(row.get("extra_config"))
|
|
||||||
bot.send_progress = bool(extra.get("sendProgress", bot.send_progress))
|
|
||||||
bot.send_tool_hints = bool(extra.get("sendToolHints", bot.send_tool_hints))
|
|
||||||
session.add(bot)
|
|
||||||
|
|
||||||
config_data = _read_bot_config(bot_id)
|
config_data = _read_bot_config(bot_id)
|
||||||
channels_cfg = config_data.get("channels")
|
channels_cfg = config_data.get("channels")
|
||||||
if not isinstance(channels_cfg, dict):
|
if not isinstance(channels_cfg, dict):
|
||||||
channels_cfg = {}
|
channels_cfg = {}
|
||||||
config_data["channels"] = channels_cfg
|
config_data["channels"] = channels_cfg
|
||||||
channels_cfg["sendProgress"] = bool(bot.send_progress)
|
current_send_progress, current_send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
||||||
channels_cfg["sendToolHints"] = bool(bot.send_tool_hints)
|
if new_type == "dashboard":
|
||||||
|
extra = _normalize_channel_extra(row.get("extra_config"))
|
||||||
|
channels_cfg["sendProgress"] = bool(extra.get("sendProgress", current_send_progress))
|
||||||
|
channels_cfg["sendToolHints"] = bool(extra.get("sendToolHints", current_send_tool_hints))
|
||||||
|
else:
|
||||||
|
channels_cfg["sendProgress"] = current_send_progress
|
||||||
|
channels_cfg["sendToolHints"] = current_send_tool_hints
|
||||||
|
channels_cfg.pop("dashboard", None)
|
||||||
if existing_type != "dashboard" and existing_type in channels_cfg and existing_type != new_type:
|
if existing_type != "dashboard" and existing_type in channels_cfg and existing_type != new_type:
|
||||||
channels_cfg.pop(existing_type, None)
|
channels_cfg.pop(existing_type, None)
|
||||||
if new_type != "dashboard":
|
if new_type != "dashboard":
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,6 @@ from datetime import datetime
|
||||||
class BotInstance(SQLModel, table=True):
|
class BotInstance(SQLModel, table=True):
|
||||||
id: str = Field(primary_key=True)
|
id: str = Field(primary_key=True)
|
||||||
name: str
|
name: str
|
||||||
avatar_model: str = Field(default="base")
|
|
||||||
avatar_skin: Optional[str] = Field(default="blue_suit")
|
|
||||||
system_prompt: Optional[str] = Field(default=None)
|
|
||||||
soul_md: Optional[str] = Field(default=None)
|
|
||||||
agents_md: Optional[str] = Field(default=None)
|
|
||||||
user_md: Optional[str] = Field(default=None)
|
|
||||||
tools_md: Optional[str] = Field(default=None)
|
|
||||||
tools_config_json: Optional[str] = Field(default=None) # JSON string for runtime tools config
|
|
||||||
identity_md: Optional[str] = Field(default=None)
|
|
||||||
llm_provider: str
|
|
||||||
llm_model: str
|
|
||||||
api_key: str # Encrypted
|
|
||||||
api_base: Optional[str] = Field(default=None)
|
|
||||||
temperature: float = Field(default=0.2)
|
|
||||||
top_p: float = Field(default=1.0)
|
|
||||||
max_tokens: int = Field(default=8192)
|
|
||||||
presence_penalty: float = Field(default=0.0)
|
|
||||||
frequency_penalty: float = Field(default=0.0)
|
|
||||||
send_progress: bool = Field(default=False)
|
|
||||||
send_tool_hints: bool = Field(default=False)
|
|
||||||
workspace_dir: str = Field(unique=True)
|
workspace_dir: str = Field(unique=True)
|
||||||
docker_status: str = Field(default="STOPPED", index=True)
|
docker_status: str = Field(default="STOPPED", index=True)
|
||||||
current_state: Optional[str] = Field(default="IDLE")
|
current_state: Optional[str] = Field(default="IDLE")
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,19 @@ function normalizeMedia(raw: unknown): string[] {
|
||||||
return raw.map((v) => String(v || '').trim()).filter((v) => v.length > 0);
|
return raw.map((v) => String(v || '').trim()).filter((v) => v.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeChannelName(raw: unknown): string {
|
||||||
|
const channel = String(raw || '').trim().toLowerCase();
|
||||||
|
if (channel === 'dashboard_channel' || channel === 'dashboard-channel') return 'dashboard';
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
export function useBotsSync() {
|
export function useBotsSync() {
|
||||||
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
|
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
|
||||||
const socketsRef = useRef<Record<string, WebSocket>>({});
|
const socketsRef = useRef<Record<string, WebSocket>>({});
|
||||||
const heartbeatsRef = useRef<Record<string, number>>({});
|
const heartbeatsRef = useRef<Record<string, number>>({});
|
||||||
const lastUserEchoRef = useRef<Record<string, { text: string; ts: number }>>({});
|
const lastUserEchoRef = useRef<Record<string, { text: string; ts: number }>>({});
|
||||||
const lastAssistantRef = useRef<Record<string, { text: string; ts: number }>>({});
|
const lastAssistantRef = useRef<Record<string, { text: string; ts: number }>>({});
|
||||||
|
const lastProgressRef = useRef<Record<string, { text: string; ts: number }>>({});
|
||||||
const hydratedMessagesRef = useRef<Record<string, boolean>>({});
|
const hydratedMessagesRef = useRef<Record<string, boolean>>({});
|
||||||
const isZh = useAppStore((s) => s.locale === 'zh');
|
const isZh = useAppStore((s) => s.locale === 'zh');
|
||||||
const locale = useAppStore((s) => s.locale);
|
const locale = useAppStore((s) => s.locale);
|
||||||
|
|
@ -134,7 +141,7 @@ export function useBotsSync() {
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sourceChannel = String(data?.channel || '').trim().toLowerCase();
|
const sourceChannel = normalizeChannelName(data?.channel || data?.source);
|
||||||
const isDashboardChannel = sourceChannel === 'dashboard';
|
const isDashboardChannel = sourceChannel === 'dashboard';
|
||||||
const payload = data?.payload && typeof data.payload === 'object' ? data.payload : {};
|
const payload = data?.payload && typeof data.payload === 'object' ? data.payload : {};
|
||||||
if (data.type === 'AGENT_STATE') {
|
if (data.type === 'AGENT_STATE') {
|
||||||
|
|
@ -173,15 +180,20 @@ export function useBotsSync() {
|
||||||
const progressText = summarizeProgressText(content, isZh);
|
const progressText = summarizeProgressText(content, isZh);
|
||||||
updateBotState(bot.id, state, progressText);
|
updateBotState(bot.id, state, progressText);
|
||||||
addBotEvent(bot.id, { state, text: progressText || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
addBotEvent(bot.id, { state, text: progressText || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
||||||
if (isDashboardChannel && isTool) {
|
if (isDashboardChannel) {
|
||||||
const chatText =
|
const fullProgress = content || progressText || (isZh ? '处理中...' : 'Processing...');
|
||||||
(isZh ? '调用工具:' : 'Tool: ') + (progressText || (isZh ? '处理中...' : 'processing...'));
|
const chatText = isTool ? `${isZh ? '工具调用' : 'Tool Call'}\n${fullProgress}` : fullProgress;
|
||||||
|
const now = Date.now();
|
||||||
|
const prev = lastProgressRef.current[bot.id];
|
||||||
|
if (!prev || prev.text !== chatText || now - prev.ts > 1200) {
|
||||||
addBotMessage(bot.id, {
|
addBotMessage(bot.id, {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
text: chatText,
|
text: chatText,
|
||||||
ts: Date.now(),
|
ts: now,
|
||||||
kind: 'progress',
|
kind: 'progress',
|
||||||
});
|
});
|
||||||
|
lastProgressRef.current[bot.id] = { text: chatText, ts: now };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,37 @@
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-chat-text.is-collapsed {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-chat-text.is-collapsed::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 42px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to bottom, transparent, color-mix(in oklab, var(--panel-soft) 88%, var(--panel) 12%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-chat-more-btn {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--brand) 56%, var(--line) 44%);
|
||||||
|
background: color-mix(in oklab, var(--panel) 72%, var(--brand-soft) 28%);
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-chat-text > *:first-child {
|
.ops-chat-text > *:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1340,6 +1371,16 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-preview-image {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-markdown {
|
.workspace-markdown {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ interface WorkspacePreviewState {
|
||||||
truncated: boolean;
|
truncated: boolean;
|
||||||
ext: string;
|
ext: string;
|
||||||
isMarkdown: boolean;
|
isMarkdown: boolean;
|
||||||
|
isImage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceUploadResponse {
|
interface WorkspaceUploadResponse {
|
||||||
|
|
@ -205,13 +206,57 @@ function normalizeRuntimeState(s?: string) {
|
||||||
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
||||||
if (node.type !== 'file') return false;
|
if (node.type !== 'file') return false;
|
||||||
const ext = (node.ext || '').toLowerCase();
|
const ext = (node.ext || '').toLowerCase();
|
||||||
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf'].includes(ext);
|
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp'].includes(ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPdfPath(path: string) {
|
function isPdfPath(path: string) {
|
||||||
return String(path || '').trim().toLowerCase().endsWith('.pdf');
|
return String(path || '').trim().toLowerCase().endsWith('.pdf');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImagePath(path: string) {
|
||||||
|
const normalized = String(path || '').trim().toLowerCase();
|
||||||
|
return normalized.endsWith('.png') || normalized.endsWith('.jpg') || normalized.endsWith('.jpeg') || normalized.endsWith('.webp');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPreviewableWorkspacePath(path: string) {
|
||||||
|
const normalized = String(path || '').trim().toLowerCase();
|
||||||
|
return ['.md', '.json', '.log', '.txt', '.csv', '.pdf', '.png', '.jpg', '.jpeg', '.webp'].some((ext) =>
|
||||||
|
normalized.endsWith(ext),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKSPACE_LINK_PREFIX = 'https://workspace.local/open/';
|
||||||
|
|
||||||
|
function buildWorkspaceLink(path: string) {
|
||||||
|
return `${WORKSPACE_LINK_PREFIX}${encodeURIComponent(path)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkspaceLink(href: string): string | null {
|
||||||
|
const link = String(href || '').trim();
|
||||||
|
if (!link.startsWith(WORKSPACE_LINK_PREFIX)) return null;
|
||||||
|
const encoded = link.slice(WORKSPACE_LINK_PREFIX.length);
|
||||||
|
try {
|
||||||
|
const decoded = decodeURIComponent(encoded || '').trim();
|
||||||
|
return decoded || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorateWorkspacePathsForMarkdown(text: string) {
|
||||||
|
const source = String(text || '');
|
||||||
|
const normalizedExistingLinks = source.replace(
|
||||||
|
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\s*\n?\s*\((https:\/\/workspace\.local\/open\/[^)\s]+)\)/g,
|
||||||
|
'[$1]($2)',
|
||||||
|
);
|
||||||
|
const workspacePathPattern = /\/root\/\.nanobot\/workspace\/[^\s<>"'`)\]},。!?;:、]+/g;
|
||||||
|
return normalizedExistingLinks.replace(workspacePathPattern, (fullPath) => {
|
||||||
|
const normalized = normalizeDashboardAttachmentPath(fullPath);
|
||||||
|
if (!normalized) return fullPath;
|
||||||
|
return `[${fullPath}](${buildWorkspaceLink(normalized)})`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAttachmentPaths(raw: unknown): string[] {
|
function normalizeAttachmentPaths(raw: unknown): string[] {
|
||||||
if (!Array.isArray(raw)) return [];
|
if (!Array.isArray(raw)) return [];
|
||||||
return raw
|
return raw
|
||||||
|
|
@ -293,6 +338,7 @@ export function BotDashboardModule({
|
||||||
setBotMessages,
|
setBotMessages,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const { notify, confirm } = useLucentPrompt();
|
const { notify, confirm } = useLucentPrompt();
|
||||||
|
const fileNotPreviewableLabel = locale === 'zh' ? '当前文件类型不支持预览' : 'This file type is not previewable';
|
||||||
const [selectedBotId, setSelectedBotId] = useState('');
|
const [selectedBotId, setSelectedBotId] = useState('');
|
||||||
const [command, setCommand] = useState('');
|
const [command, setCommand] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
@ -344,11 +390,109 @@ export function BotDashboardModule({
|
||||||
const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false);
|
const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false);
|
||||||
const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>('chat');
|
const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>('chat');
|
||||||
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
||||||
|
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const openWorkspacePathFromChat = (path: string) => {
|
||||||
|
const normalized = String(path || '').trim();
|
||||||
|
if (!normalized) return;
|
||||||
|
if (!isPreviewableWorkspacePath(normalized)) {
|
||||||
|
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void openWorkspaceFilePreview(normalized);
|
||||||
|
};
|
||||||
|
const renderWorkspaceAwareText = (text: string, keyPrefix: string): ReactNode[] => {
|
||||||
|
const source = String(text || '');
|
||||||
|
if (!source) return [source];
|
||||||
|
const pattern =
|
||||||
|
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\s]+)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\]},。!?;:、]+|https:\/\/workspace\.local\/open\/[^\s)]+/g;
|
||||||
|
const nodes: ReactNode[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let matchIndex = 0;
|
||||||
|
let match = pattern.exec(source);
|
||||||
|
while (match) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
nodes.push(source.slice(lastIndex, match.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = match[0];
|
||||||
|
const markdownPath = match[1] ? String(match[1]) : '';
|
||||||
|
const markdownHref = match[2] ? String(match[2]) : '';
|
||||||
|
let normalizedPath = '';
|
||||||
|
let displayText = raw;
|
||||||
|
|
||||||
|
if (markdownPath && markdownHref) {
|
||||||
|
normalizedPath = normalizeDashboardAttachmentPath(markdownPath);
|
||||||
|
displayText = markdownPath;
|
||||||
|
} else if (raw.startsWith(WORKSPACE_LINK_PREFIX)) {
|
||||||
|
normalizedPath = String(parseWorkspaceLink(raw) || '').trim();
|
||||||
|
displayText = normalizedPath ? `/root/.nanobot/workspace/${normalizedPath}` : raw;
|
||||||
|
} else if (raw.startsWith('/root/.nanobot/workspace/')) {
|
||||||
|
normalizedPath = normalizeDashboardAttachmentPath(raw);
|
||||||
|
displayText = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedPath) {
|
||||||
|
nodes.push(
|
||||||
|
<a
|
||||||
|
key={`${keyPrefix}-ws-${matchIndex}`}
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
openWorkspacePathFromChat(normalizedPath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayText}
|
||||||
|
</a>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nodes.push(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = match.index + raw.length;
|
||||||
|
matchIndex += 1;
|
||||||
|
match = pattern.exec(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < source.length) {
|
||||||
|
nodes.push(source.slice(lastIndex));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
const renderWorkspaceAwareChildren = (children: ReactNode, keyPrefix: string): ReactNode => {
|
||||||
|
const list = Array.isArray(children) ? children : [children];
|
||||||
|
const mapped = list.flatMap((child, idx) => {
|
||||||
|
if (typeof child === 'string') {
|
||||||
|
return renderWorkspaceAwareText(child, `${keyPrefix}-${idx}`);
|
||||||
|
}
|
||||||
|
return [child];
|
||||||
|
});
|
||||||
|
return mapped;
|
||||||
|
};
|
||||||
const markdownComponents = useMemo(
|
const markdownComponents = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
a: ({ href, children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
a: ({ href, children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||||
const link = String(href || '').trim();
|
const link = String(href || '').trim();
|
||||||
|
const workspacePath = parseWorkspaceLink(link);
|
||||||
|
if (workspacePath) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!isPreviewableWorkspacePath(workspacePath)) {
|
||||||
|
notify(fileNotPreviewableLabel, { tone: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void openWorkspaceFilePreview(workspacePath);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isExternalHttpLink(link)) {
|
if (isExternalHttpLink(link)) {
|
||||||
return (
|
return (
|
||||||
<a href={link} target="_blank" rel="noopener noreferrer" {...props}>
|
<a href={link} target="_blank" rel="noopener noreferrer" {...props}>
|
||||||
|
|
@ -362,8 +506,17 @@ export function BotDashboardModule({
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
p: ({ children, ...props }: { children?: ReactNode }) => (
|
||||||
|
<p {...props}>{renderWorkspaceAwareChildren(children, 'md-p')}</p>
|
||||||
|
),
|
||||||
|
li: ({ children, ...props }: { children?: ReactNode }) => (
|
||||||
|
<li {...props}>{renderWorkspaceAwareChildren(children, 'md-li')}</li>
|
||||||
|
),
|
||||||
|
code: ({ children, ...props }: { children?: ReactNode }) => (
|
||||||
|
<code {...props}>{renderWorkspaceAwareChildren(children, 'md-code')}</code>
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
[],
|
[fileNotPreviewableLabel, notify, selectedBotId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
|
|
@ -460,10 +613,23 @@ export function BotDashboardModule({
|
||||||
if (eventText && eventText !== t.processing) return eventText;
|
if (eventText && eventText !== t.processing) return eventText;
|
||||||
return '-';
|
return '-';
|
||||||
}, [selectedBot, latestEvent, isZh, t.processing]);
|
}, [selectedBot, latestEvent, isZh, t.processing]);
|
||||||
|
|
||||||
|
const shouldCollapseProgress = (text: string) => {
|
||||||
|
const normalized = String(text || '').trim();
|
||||||
|
if (!normalized) return false;
|
||||||
|
const lines = normalized.split('\n').length;
|
||||||
|
return lines > 6 || normalized.length > 520;
|
||||||
|
};
|
||||||
|
|
||||||
const conversationNodes = useMemo(
|
const conversationNodes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
conversation.map((item, idx) => (
|
conversation.map((item, idx) => {
|
||||||
<div key={`${item.ts}-${idx}`} className={`ops-chat-row ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
const itemKey = `${item.ts}-${idx}`;
|
||||||
|
const isProgressBubble = item.role !== 'user' && (item.kind || 'final') === 'progress';
|
||||||
|
const collapsible = isProgressBubble && shouldCollapseProgress(item.text);
|
||||||
|
const expanded = Boolean(expandedProgressByKey[itemKey]);
|
||||||
|
return (
|
||||||
|
<div key={itemKey} className={`ops-chat-row ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
||||||
<div className={`ops-chat-item ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
<div className={`ops-chat-item ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
||||||
{item.role !== 'user' && (
|
{item.role !== 'user' && (
|
||||||
<div className="ops-avatar bot" title="Nanobot">
|
<div className="ops-avatar bot" title="Nanobot">
|
||||||
|
|
@ -476,16 +642,29 @@ export function BotDashboardModule({
|
||||||
<span>{item.role === 'user' ? t.you : 'Nanobot'}</span>
|
<span>{item.role === 'user' ? t.you : 'Nanobot'}</span>
|
||||||
<span className="mono">{formatClock(item.ts)}</span>
|
<span className="mono">{formatClock(item.ts)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ops-chat-text">
|
<div className={`ops-chat-text ${collapsible && !expanded ? 'is-collapsed' : ''}`}>
|
||||||
{item.text ? (
|
{item.text ? (
|
||||||
item.role === 'user' ? (
|
item.role === 'user' ? (
|
||||||
<div className="whitespace-pre-wrap">{normalizeUserMessageText(item.text)}</div>
|
<div className="whitespace-pre-wrap">{normalizeUserMessageText(item.text)}</div>
|
||||||
) : (
|
) : (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{item.text}
|
{decorateWorkspacePathsForMarkdown(item.text)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
{collapsible ? (
|
||||||
|
<button
|
||||||
|
className="ops-chat-more-btn"
|
||||||
|
onClick={() =>
|
||||||
|
setExpandedProgressByKey((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[itemKey]: !prev[itemKey],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{expanded ? (isZh ? '收起' : 'Less') : (isZh ? '更多' : 'More')}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
{(item.attachments || []).length > 0 ? (
|
{(item.attachments || []).length > 0 ? (
|
||||||
<div className="ops-chat-attachments">
|
<div className="ops-chat-attachments">
|
||||||
{(item.attachments || []).map((rawPath) => {
|
{(item.attachments || []).map((rawPath) => {
|
||||||
|
|
@ -518,8 +697,8 @@ export function BotDashboardModule({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)),
|
)}),
|
||||||
[conversation, selectedBotId, t.user, t.you],
|
[conversation, expandedProgressByKey, isZh, selectedBotId, t.user, t.you],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -555,6 +734,10 @@ export function BotDashboardModule({
|
||||||
setRuntimeMenuOpen(false);
|
setRuntimeMenuOpen(false);
|
||||||
}, [selectedBotId]);
|
}, [selectedBotId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpandedProgressByKey({});
|
||||||
|
}, [selectedBotId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!compactMode) {
|
if (!compactMode) {
|
||||||
setIsCompactMobile(false);
|
setIsCompactMobile(false);
|
||||||
|
|
@ -616,6 +799,18 @@ export function BotDashboardModule({
|
||||||
window.open(href, '_blank', 'noopener,noreferrer');
|
window.open(href, '_blank', 'noopener,noreferrer');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isImagePath(normalizedPath)) {
|
||||||
|
const fileExt = (normalizedPath.split('.').pop() || '').toLowerCase();
|
||||||
|
setWorkspacePreview({
|
||||||
|
path: normalizedPath,
|
||||||
|
content: '',
|
||||||
|
truncated: false,
|
||||||
|
ext: fileExt ? `.${fileExt}` : '',
|
||||||
|
isMarkdown: false,
|
||||||
|
isImage: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
setWorkspaceFileLoading(true);
|
setWorkspaceFileLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<WorkspaceFileResponse>(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/file`, {
|
const res = await axios.get<WorkspaceFileResponse>(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/file`, {
|
||||||
|
|
@ -637,6 +832,7 @@ export function BotDashboardModule({
|
||||||
truncated: Boolean(res.data.truncated),
|
truncated: Boolean(res.data.truncated),
|
||||||
ext: textExt ? `.${textExt}` : '',
|
ext: textExt ? `.${textExt}` : '',
|
||||||
isMarkdown: textExt === 'md' || Boolean(res.data.is_markdown),
|
isMarkdown: textExt === 'md' || Boolean(res.data.is_markdown),
|
||||||
|
isImage: false,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const msg = error?.response?.data?.detail || t.fileReadFail;
|
const msg = error?.response?.data?.detail || t.fileReadFail;
|
||||||
|
|
@ -908,6 +1104,10 @@ export function BotDashboardModule({
|
||||||
send_progress: Boolean(globalDelivery.sendProgress),
|
send_progress: Boolean(globalDelivery.sendProgress),
|
||||||
send_tool_hints: Boolean(globalDelivery.sendToolHints),
|
send_tool_hints: Boolean(globalDelivery.sendToolHints),
|
||||||
});
|
});
|
||||||
|
if (selectedBot.docker_status === 'RUNNING') {
|
||||||
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/stop`);
|
||||||
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/start`);
|
||||||
|
}
|
||||||
await refresh();
|
await refresh();
|
||||||
notify(t.channelSaved, { tone: 'success' });
|
notify(t.channelSaved, { tone: 'success' });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -2123,6 +2323,7 @@ export function BotDashboardModule({
|
||||||
const key = String(envDraftKey || '').trim().toUpperCase();
|
const key = String(envDraftKey || '').trim().toUpperCase();
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
upsertEnvParam(key, envDraftValue);
|
upsertEnvParam(key, envDraftValue);
|
||||||
|
setEnvDraftKey('');
|
||||||
setEnvDraftValue('');
|
setEnvDraftValue('');
|
||||||
}}
|
}}
|
||||||
title={t.addEnvParam}
|
title={t.addEnvParam}
|
||||||
|
|
@ -2245,7 +2446,13 @@ export function BotDashboardModule({
|
||||||
<span className="modal-sub mono">{workspacePreview.path}</span>
|
<span className="modal-sub mono">{workspacePreview.path}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`workspace-preview-body ${workspacePreview.isMarkdown ? 'markdown' : ''}`}>
|
<div className={`workspace-preview-body ${workspacePreview.isMarkdown ? 'markdown' : ''}`}>
|
||||||
{workspacePreview.isMarkdown ? (
|
{workspacePreview.isImage ? (
|
||||||
|
<img
|
||||||
|
className="workspace-preview-image"
|
||||||
|
src={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}`}
|
||||||
|
alt={workspacePreview.path.split('/').pop() || 'workspace-image'}
|
||||||
|
/>
|
||||||
|
) : workspacePreview.isMarkdown ? (
|
||||||
<div className="workspace-markdown">
|
<div className="workspace-markdown">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{workspacePreview.content}
|
{workspacePreview.content}
|
||||||
|
|
|
||||||
|
|
@ -823,6 +823,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
const key = String(envDraftKey || '').trim().toUpperCase();
|
const key = String(envDraftKey || '').trim().toUpperCase();
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
upsertEnvParam(key, envDraftValue);
|
upsertEnvParam(key, envDraftValue);
|
||||||
|
setEnvDraftKey('');
|
||||||
setEnvDraftValue('');
|
setEnvDraftValue('');
|
||||||
}}
|
}}
|
||||||
title={ui.addEnvParam}
|
title={ui.addEnvParam}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue