v0.1.2
parent
93e9547a7f
commit
3cb25ba795
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
|
@ -50,41 +49,6 @@ class BotConfigManager:
|
|||
"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] = {
|
||||
"agents": {
|
||||
"defaults": {
|
||||
|
|
@ -98,7 +62,6 @@ class BotConfigManager:
|
|||
provider_name: provider_cfg,
|
||||
},
|
||||
"channels": channels_cfg,
|
||||
"tools": tools_cfg,
|
||||
}
|
||||
|
||||
for channel in channels:
|
||||
|
|
|
|||
|
|
@ -10,23 +10,12 @@ engine = create_engine(DATABASE_URL, echo=DATABASE_ECHO)
|
|||
|
||||
|
||||
def _ensure_botinstance_columns() -> None:
|
||||
if engine.dialect.name != "sqlite":
|
||||
return
|
||||
required_columns = {
|
||||
"current_state": "TEXT DEFAULT 'IDLE'",
|
||||
"last_action": "TEXT",
|
||||
"soul_md": "TEXT",
|
||||
"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",
|
||||
"image_tag": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
||||
}
|
||||
with engine.connect() as conn:
|
||||
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
||||
|
|
@ -38,7 +27,45 @@ def _ensure_botinstance_columns() -> None:
|
|||
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:
|
||||
if engine.dialect.name != "sqlite":
|
||||
return
|
||||
required_columns = {
|
||||
"media_json": "TEXT",
|
||||
}
|
||||
|
|
@ -60,18 +87,11 @@ def _drop_legacy_skill_tables() -> None:
|
|||
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:
|
||||
SQLModel.metadata.create_all(engine)
|
||||
_drop_legacy_skill_tables()
|
||||
_drop_legacy_channel_tables()
|
||||
_ensure_botinstance_columns()
|
||||
_drop_legacy_botinstance_columns()
|
||||
_ensure_botmessage_columns()
|
||||
|
||||
|
||||
|
|
|
|||
327
backend/main.py
327
backend/main.py
|
|
@ -128,6 +128,13 @@ class CommandRequest(BaseModel):
|
|||
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:
|
||||
raw = str(value or "").strip().replace("\\", "/")
|
||||
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()
|
||||
if packet_type not in {"AGENT_STATE", "ASSISTANT_MESSAGE", "USER_COMMAND", "BUS_EVENT"}:
|
||||
return
|
||||
source_channel = str(packet.get("channel") or "").strip().lower()
|
||||
source_channel = _normalize_packet_channel(packet)
|
||||
if source_channel != "dashboard":
|
||||
return
|
||||
with Session(engine) as session:
|
||||
|
|
@ -341,6 +348,20 @@ def _normalize_channel_extra(raw: Any) -> Dict[str, Any]:
|
|||
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]:
|
||||
ctype = str(ctype or "").strip().lower()
|
||||
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):
|
||||
channels_cfg = {}
|
||||
|
||||
send_progress = bool(channels_cfg.get("sendProgress", bot.send_progress))
|
||||
send_tool_hints = bool(channels_cfg.get("sendToolHints", bot.send_tool_hints))
|
||||
send_progress, send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
||||
rows: List[Dict[str, Any]] = [
|
||||
{
|
||||
"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():
|
||||
if ctype in {"sendProgress", "sendToolHints"}:
|
||||
if ctype in {"sendProgress", "sendToolHints", "dashboard"}:
|
||||
continue
|
||||
if not isinstance(cfg, dict):
|
||||
continue
|
||||
|
|
@ -595,23 +615,161 @@ def _parse_env_params(raw: Any) -> Dict[str, str]:
|
|||
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(
|
||||
session: Session,
|
||||
bot_id: str,
|
||||
channels_override: Optional[List[Dict[str, Any]]] = None,
|
||||
global_delivery_override: Optional[Dict[str, Any]] = None,
|
||||
runtime_overrides: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
bot = session.get(BotInstance, bot_id)
|
||||
if not bot:
|
||||
return
|
||||
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)
|
||||
bot_data = bot.model_dump()
|
||||
for row in channels_data:
|
||||
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
|
||||
bot_data["send_progress"] = send_progress
|
||||
bot_data["send_tool_hints"] = send_tool_hints
|
||||
normalized_channels: List[Dict[str, Any]] = []
|
||||
for row in channels_data:
|
||||
ctype = str(row.get("channel_type") or "").strip().lower()
|
||||
|
|
@ -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)):
|
||||
image_row = session.get(NanobotImage, payload.image_tag)
|
||||
if not image_row:
|
||||
|
|
@ -992,22 +1150,6 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
|||
bot = BotInstance(
|
||||
id=payload.id,
|
||||
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,
|
||||
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.refresh(bot)
|
||||
_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))
|
||||
return bot
|
||||
_sync_workspace_channels(
|
||||
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)):
|
||||
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)):
|
||||
bot = session.get(BotInstance, bot_id)
|
||||
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):
|
||||
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
|
||||
if tools_config is not None:
|
||||
bot.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_overrides["tools_config_json"] = json.dumps(_normalize_tools_config(tools_config), ensure_ascii=False)
|
||||
|
||||
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():
|
||||
if key in db_fields:
|
||||
setattr(bot, key, value)
|
||||
|
||||
session.add(bot)
|
||||
|
|
@ -1056,8 +1255,22 @@ def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depend
|
|||
session.refresh(bot)
|
||||
if env_params is not None:
|
||||
_write_env_store(bot_id, _normalize_env_params(env_params))
|
||||
_sync_workspace_channels(session, bot_id)
|
||||
return bot
|
||||
global_delivery_override: Optional[Dict[str, Any]] = None
|
||||
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")
|
||||
|
|
@ -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")
|
||||
return {
|
||||
"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)
|
||||
if not bot:
|
||||
raise HTTPException(status_code=404, detail="Bot not found")
|
||||
|
||||
normalized = _normalize_tools_config(payload.tools_config)
|
||||
bot.tools_config_json = json.dumps(normalized, ensure_ascii=False)
|
||||
session.add(bot)
|
||||
session.commit()
|
||||
session.refresh(bot)
|
||||
|
||||
_sync_workspace_channels(session, bot_id)
|
||||
return {
|
||||
"status": "updated",
|
||||
"bot_id": bot_id,
|
||||
"tools_config": normalized,
|
||||
}
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Tools config is no longer managed by dashboard. Please set required env vars manually.",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/bots/{bot_id}/env-params")
|
||||
|
|
@ -1391,19 +1597,20 @@ def update_bot_channel(
|
|||
row["id"] = new_type
|
||||
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)
|
||||
channels_cfg = config_data.get("channels")
|
||||
if not isinstance(channels_cfg, dict):
|
||||
channels_cfg = {}
|
||||
config_data["channels"] = channels_cfg
|
||||
channels_cfg["sendProgress"] = bool(bot.send_progress)
|
||||
channels_cfg["sendToolHints"] = bool(bot.send_tool_hints)
|
||||
current_send_progress, current_send_tool_hints = _read_global_delivery_flags(channels_cfg)
|
||||
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:
|
||||
channels_cfg.pop(existing_type, None)
|
||||
if new_type != "dashboard":
|
||||
|
|
|
|||
|
|
@ -5,26 +5,6 @@ from datetime import datetime
|
|||
class BotInstance(SQLModel, table=True):
|
||||
id: str = Field(primary_key=True)
|
||||
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)
|
||||
docker_status: str = Field(default="STOPPED", index=True)
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
|
||||
const socketsRef = useRef<Record<string, WebSocket>>({});
|
||||
const heartbeatsRef = useRef<Record<string, number>>({});
|
||||
const lastUserEchoRef = 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 isZh = useAppStore((s) => s.locale === 'zh');
|
||||
const locale = useAppStore((s) => s.locale);
|
||||
|
|
@ -134,7 +141,7 @@ export function useBotsSync() {
|
|||
} catch {
|
||||
return;
|
||||
}
|
||||
const sourceChannel = String(data?.channel || '').trim().toLowerCase();
|
||||
const sourceChannel = normalizeChannelName(data?.channel || data?.source);
|
||||
const isDashboardChannel = sourceChannel === 'dashboard';
|
||||
const payload = data?.payload && typeof data.payload === 'object' ? data.payload : {};
|
||||
if (data.type === 'AGENT_STATE') {
|
||||
|
|
@ -173,15 +180,20 @@ export function useBotsSync() {
|
|||
const progressText = summarizeProgressText(content, isZh);
|
||||
updateBotState(bot.id, state, progressText);
|
||||
addBotEvent(bot.id, { state, text: progressText || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
||||
if (isDashboardChannel && isTool) {
|
||||
const chatText =
|
||||
(isZh ? '调用工具:' : 'Tool: ') + (progressText || (isZh ? '处理中...' : 'processing...'));
|
||||
if (isDashboardChannel) {
|
||||
const fullProgress = content || 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, {
|
||||
role: 'assistant',
|
||||
text: chatText,
|
||||
ts: Date.now(),
|
||||
ts: now,
|
||||
kind: 'progress',
|
||||
});
|
||||
lastProgressRef.current[bot.id] = { text: chatText, ts: now };
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -375,6 +375,37 @@
|
|||
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 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
@ -1340,6 +1371,16 @@
|
|||
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 {
|
||||
padding: 12px 14px;
|
||||
color: var(--text);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ interface WorkspacePreviewState {
|
|||
truncated: boolean;
|
||||
ext: string;
|
||||
isMarkdown: boolean;
|
||||
isImage: boolean;
|
||||
}
|
||||
|
||||
interface WorkspaceUploadResponse {
|
||||
|
|
@ -205,13 +206,57 @@ function normalizeRuntimeState(s?: string) {
|
|||
function isPreviewableWorkspaceFile(node: WorkspaceNode) {
|
||||
if (node.type !== 'file') return false;
|
||||
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) {
|
||||
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[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw
|
||||
|
|
@ -293,6 +338,7 @@ export function BotDashboardModule({
|
|||
setBotMessages,
|
||||
} = useAppStore();
|
||||
const { notify, confirm } = useLucentPrompt();
|
||||
const fileNotPreviewableLabel = locale === 'zh' ? '当前文件类型不支持预览' : 'This file type is not previewable';
|
||||
const [selectedBotId, setSelectedBotId] = useState('');
|
||||
const [command, setCommand] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
|
@ -344,11 +390,109 @@ export function BotDashboardModule({
|
|||
const [runtimeMenuOpen, setRuntimeMenuOpen] = useState(false);
|
||||
const [compactPanelTab, setCompactPanelTab] = useState<CompactPanelTab>('chat');
|
||||
const [isCompactMobile, setIsCompactMobile] = useState(false);
|
||||
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||
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(
|
||||
() => ({
|
||||
a: ({ href, children, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
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)) {
|
||||
return (
|
||||
<a href={link} target="_blank" rel="noopener noreferrer" {...props}>
|
||||
|
|
@ -362,8 +506,17 @@ export function BotDashboardModule({
|
|||
</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({
|
||||
|
|
@ -460,10 +613,23 @@ export function BotDashboardModule({
|
|||
if (eventText && eventText !== t.processing) return eventText;
|
||||
return '-';
|
||||
}, [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(
|
||||
() =>
|
||||
conversation.map((item, idx) => (
|
||||
<div key={`${item.ts}-${idx}`} className={`ops-chat-row ${item.role === 'user' ? 'is-user' : 'is-assistant'}`}>
|
||||
conversation.map((item, idx) => {
|
||||
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'}`}>
|
||||
{item.role !== 'user' && (
|
||||
<div className="ops-avatar bot" title="Nanobot">
|
||||
|
|
@ -476,16 +642,29 @@ export function BotDashboardModule({
|
|||
<span>{item.role === 'user' ? t.you : 'Nanobot'}</span>
|
||||
<span className="mono">{formatClock(item.ts)}</span>
|
||||
</div>
|
||||
<div className="ops-chat-text">
|
||||
<div className={`ops-chat-text ${collapsible && !expanded ? 'is-collapsed' : ''}`}>
|
||||
{item.text ? (
|
||||
item.role === 'user' ? (
|
||||
<div className="whitespace-pre-wrap">{normalizeUserMessageText(item.text)}</div>
|
||||
) : (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{item.text}
|
||||
{decorateWorkspacePathsForMarkdown(item.text)}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
) : 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 ? (
|
||||
<div className="ops-chat-attachments">
|
||||
{(item.attachments || []).map((rawPath) => {
|
||||
|
|
@ -518,8 +697,8 @@ export function BotDashboardModule({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
)),
|
||||
[conversation, selectedBotId, t.user, t.you],
|
||||
)}),
|
||||
[conversation, expandedProgressByKey, isZh, selectedBotId, t.user, t.you],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -555,6 +734,10 @@ export function BotDashboardModule({
|
|||
setRuntimeMenuOpen(false);
|
||||
}, [selectedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedProgressByKey({});
|
||||
}, [selectedBotId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!compactMode) {
|
||||
setIsCompactMobile(false);
|
||||
|
|
@ -616,6 +799,18 @@ export function BotDashboardModule({
|
|||
window.open(href, '_blank', 'noopener,noreferrer');
|
||||
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);
|
||||
try {
|
||||
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),
|
||||
ext: textExt ? `.${textExt}` : '',
|
||||
isMarkdown: textExt === 'md' || Boolean(res.data.is_markdown),
|
||||
isImage: false,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.detail || t.fileReadFail;
|
||||
|
|
@ -908,6 +1104,10 @@ export function BotDashboardModule({
|
|||
send_progress: Boolean(globalDelivery.sendProgress),
|
||||
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();
|
||||
notify(t.channelSaved, { tone: 'success' });
|
||||
} catch (error: any) {
|
||||
|
|
@ -2123,6 +2323,7 @@ export function BotDashboardModule({
|
|||
const key = String(envDraftKey || '').trim().toUpperCase();
|
||||
if (!key) return;
|
||||
upsertEnvParam(key, envDraftValue);
|
||||
setEnvDraftKey('');
|
||||
setEnvDraftValue('');
|
||||
}}
|
||||
title={t.addEnvParam}
|
||||
|
|
@ -2245,7 +2446,13 @@ export function BotDashboardModule({
|
|||
<span className="modal-sub mono">{workspacePreview.path}</span>
|
||||
</div>
|
||||
<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">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
{workspacePreview.content}
|
||||
|
|
|
|||
|
|
@ -823,6 +823,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
|||
const key = String(envDraftKey || '').trim().toUpperCase();
|
||||
if (!key) return;
|
||||
upsertEnvParam(key, envDraftValue);
|
||||
setEnvDraftKey('');
|
||||
setEnvDraftValue('');
|
||||
}}
|
||||
title={ui.addEnvParam}
|
||||
|
|
|
|||
Loading…
Reference in New Issue