v0.1.4-p3

main
mula.liu 2026-03-18 05:54:22 +08:00
parent 7d747e6fac
commit 34988b2436
3 changed files with 143 additions and 110 deletions

View File

@ -1558,6 +1558,17 @@ def _clear_bot_sessions(bot_id: str) -> int:
return deleted return deleted
def _clear_bot_dashboard_direct_session(bot_id: str) -> Dict[str, Any]:
"""Truncate the dashboard:direct session file while preserving the workspace session root."""
root = _sessions_root(bot_id)
os.makedirs(root, exist_ok=True)
path = os.path.join(root, "dashboard_direct.jsonl")
existed = os.path.exists(path)
with open(path, "w", encoding="utf-8"):
pass
return {"path": path, "existed": existed}
def _read_env_store(bot_id: str) -> Dict[str, str]: def _read_env_store(bot_id: str) -> Dict[str, str]:
path = _env_store_path(bot_id) path = _env_store_path(bot_id)
if not os.path.isfile(path): if not os.path.isfile(path):
@ -3021,6 +3032,34 @@ def clear_bot_messages(bot_id: str, session: Session = Depends(get_session)):
return {"bot_id": bot_id, "deleted": deleted, "cleared_sessions": cleared_sessions} return {"bot_id": bot_id, "deleted": deleted, "cleared_sessions": cleared_sessions}
@app.post("/api/bots/{bot_id}/sessions/dashboard-direct/clear")
def clear_bot_dashboard_direct_session(bot_id: str, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id)
if not bot:
raise HTTPException(status_code=404, detail="Bot not found")
result = _clear_bot_dashboard_direct_session(bot_id)
if str(bot.docker_status or "").upper() == "RUNNING":
try:
docker_manager.send_command(bot_id, "/new")
except Exception:
pass
bot.updated_at = datetime.utcnow()
session.add(bot)
record_activity_event(
session,
bot_id,
"dashboard_session_cleared",
channel="dashboard",
detail="Cleared dashboard_direct session file",
metadata={"session_file": result["path"], "previously_existed": result["existed"]},
)
session.commit()
_invalidate_bot_detail_cache(bot_id)
return {"bot_id": bot_id, "cleared": True, "session_file": result["path"], "previously_existed": result["existed"]}
@app.get("/api/bots/{bot_id}/logs") @app.get("/api/bots/{bot_id}/logs")
def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)): def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id) bot = session.get(BotInstance, bot_id)

View File

@ -1,8 +1,6 @@
"""LiteLLM provider implementation for multi-provider support.""" """LiteLLM provider implementation for multi-provider support."""
import ast
import hashlib import hashlib
import json
import os import os
import secrets import secrets
import string import string
@ -20,49 +18,12 @@ from nanobot.providers.registry import find_by_model, find_gateway
_ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content"}) _ALLOWED_MSG_KEYS = frozenset({"role", "content", "tool_calls", "tool_call_id", "name", "reasoning_content"})
_ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"}) _ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"})
_ALNUM = string.ascii_letters + string.digits _ALNUM = string.ascii_letters + string.digits
_ARG_PATCH_VERBOSE_VALUES = {"1", "true", "yes", "on"}
def _short_tool_id() -> str: def _short_tool_id() -> str:
"""Generate a 9-char alphanumeric ID compatible with all providers (incl. Mistral).""" """Generate a 9-char alphanumeric ID compatible with all providers (incl. Mistral)."""
return "".join(secrets.choice(_ALNUM) for _ in range(9)) return "".join(secrets.choice(_ALNUM) for _ in range(9))
def _should_log_argument_patch() -> bool:
value = str(os.getenv("DASHBOARD_LITELLM_PATCH_VERBOSE") or "").strip().lower()
return value in _ARG_PATCH_VERBOSE_VALUES
def _coerce_tool_arguments_json(value: Any) -> str:
"""Return provider-safe JSON text for OpenAI-style function.arguments."""
if value is None:
return "{}"
if isinstance(value, str):
text = value.strip()
if not text:
return "{}"
try:
json.loads(text)
return text
except Exception:
pass
try:
parsed = ast.literal_eval(text)
except Exception:
parsed = None
else:
try:
return json.dumps(parsed, ensure_ascii=False)
except Exception:
pass
return json.dumps({"raw": text}, ensure_ascii=False)
try:
return json.dumps(value, ensure_ascii=False)
except Exception:
return json.dumps({"raw": str(value)}, ensure_ascii=False)
class LiteLLMProvider(LLMProvider): class LiteLLMProvider(LLMProvider):
""" """
LLM provider using LiteLLM for multi-provider support. LLM provider using LiteLLM for multi-provider support.
@ -222,7 +183,6 @@ class LiteLLMProvider(LLMProvider):
allowed = _ALLOWED_MSG_KEYS | extra_keys allowed = _ALLOWED_MSG_KEYS | extra_keys
sanitized = LLMProvider._sanitize_request_messages(messages, allowed) sanitized = LLMProvider._sanitize_request_messages(messages, allowed)
id_map: dict[str, str] = {} id_map: dict[str, str] = {}
patched_arguments = 0
def map_id(value: Any) -> Any: def map_id(value: Any) -> Any:
if not isinstance(value, str): if not isinstance(value, str):
@ -240,26 +200,11 @@ class LiteLLMProvider(LLMProvider):
continue continue
tc_clean = dict(tc) tc_clean = dict(tc)
tc_clean["id"] = map_id(tc_clean.get("id")) tc_clean["id"] = map_id(tc_clean.get("id"))
function = tc_clean.get("function")
if isinstance(function, dict) and "arguments" in function:
function_clean = dict(function)
original_arguments = function_clean.get("arguments")
normalized_arguments = _coerce_tool_arguments_json(original_arguments)
if original_arguments != normalized_arguments:
patched_arguments += 1
function_clean["arguments"] = normalized_arguments
tc_clean["function"] = function_clean
normalized_tool_calls.append(tc_clean) normalized_tool_calls.append(tc_clean)
clean["tool_calls"] = normalized_tool_calls clean["tool_calls"] = normalized_tool_calls
if "tool_call_id" in clean and clean["tool_call_id"]: if "tool_call_id" in clean and clean["tool_call_id"]:
clean["tool_call_id"] = map_id(clean["tool_call_id"]) clean["tool_call_id"] = map_id(clean["tool_call_id"])
if patched_arguments and _should_log_argument_patch():
logger.info(
"Normalized {} historical tool/function argument payload(s) to JSON strings for LiteLLM",
patched_arguments,
)
return sanitized return sanitized
async def chat( async def chat(

View File

@ -6,6 +6,7 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Cpu, Cpu,
Eraser,
Eye, Eye,
ExternalLink, ExternalLink,
FileText, FileText,
@ -152,6 +153,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
const [usageLoading, setUsageLoading] = useState(false); const [usageLoading, setUsageLoading] = useState(false);
const [usagePage, setUsagePage] = useState(1); const [usagePage, setUsagePage] = useState(1);
const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10)); const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10));
const [pageSizeReady, setPageSizeReady] = useState(() => readCachedPlatformPageSize(0) > 0);
const [botListPage, setBotListPage] = useState(1); const [botListPage, setBotListPage] = useState(1);
const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10)); const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10));
const [showCompactBotSheet, setShowCompactBotSheet] = useState(false); const [showCompactBotSheet, setShowCompactBotSheet] = useState(false);
@ -203,6 +205,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
} catch (error: any) { } catch (error: any) {
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
} finally { } finally {
setPageSizeReady(true);
setLoading(false); setLoading(false);
} }
}; };
@ -250,8 +253,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!pageSizeReady) return;
void loadUsage(1); void loadUsage(1);
}, [usagePageSize]); }, [pageSizeReady, usagePageSize]);
useEffect(() => { useEffect(() => {
setBotListPage(1); setBotListPage(1);
@ -366,6 +370,31 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
} }
}; };
const clearDashboardDirectSession = async (bot: BotState) => {
const targetId = String(bot.id || '').trim();
if (!targetId) return;
const ok = await confirm({
title: isZh ? '清除面板 Session' : 'Clear Dashboard Session',
message: isZh
? `确认清空 Bot ${targetId} 的 dashboard_direct.jsonl 内容?\n\n这会重置面板对话上下文若 Bot 正在运行,还会同步切到新会话。`
: `Clear dashboard_direct.jsonl for Bot ${targetId}?\n\nThis resets the dashboard conversation context. If the bot is running, it will also switch to a fresh session.`,
tone: 'warning',
confirmText: isZh ? '清除' : 'Clear',
});
if (!ok) return;
setOperatingBotId(targetId);
try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(targetId)}/sessions/dashboard-direct/clear`);
notify(isZh ? '面板 Session 已清空。' : 'Dashboard session cleared.', { tone: 'success' });
await refreshAll();
} catch (error: any) {
notify(error?.response?.data?.detail || (isZh ? '清空面板 Session 失败。' : 'Failed to clear dashboard session.'), { tone: 'error' });
} finally {
setOperatingBotId('');
}
};
const loadResourceSnapshot = async (botId: string) => { const loadResourceSnapshot = async (botId: string) => {
if (!botId) return; if (!botId) return;
setResourceLoading(true); setResourceLoading(true);
@ -391,8 +420,8 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
const overviewImages = overview?.summary.images; const overviewImages = overview?.summary.images;
const overviewResources = overview?.summary.resources; const overviewResources = overview?.summary.resources;
const usageSummary = usageData?.summary || overview?.usage.summary; const usageSummary = usageData?.summary || overview?.usage.summary;
const usageItems = usageData?.items || overview?.usage.items || []; const usageItems = pageSizeReady ? usageData?.items || [] : [];
const usageTotal = usageData?.total || 0; const usageTotal = pageSizeReady ? usageData?.total || 0 : 0;
const usagePageCount = Math.max(1, Math.ceil(usageTotal / usagePageSize)); const usagePageCount = Math.max(1, Math.ceil(usageTotal / usagePageSize));
const selectedBotInfo = selectedBotDetail && selectedBotDetail.id === selectedBotId ? { ...selectedBot, ...selectedBotDetail } : selectedBot; const selectedBotInfo = selectedBotDetail && selectedBotDetail.id === selectedBotId ? { ...selectedBot, ...selectedBotDetail } : selectedBot;
const lastActionPreview = selectedBotInfo?.last_action?.trim() || ''; const lastActionPreview = selectedBotInfo?.last_action?.trim() || '';
@ -502,6 +531,16 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
> >
<Gauge size={14} /> <Gauge size={14} />
</LucentIconButton> </LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn"
type="button"
disabled={operatingBotId === selectedBotInfo.id}
onClick={() => void clearDashboardDirectSession(selectedBotInfo)}
tooltip={isZh ? '清除面板 Session' : 'Clear Dashboard Session'}
aria-label={isZh ? '清除面板 Session' : 'Clear Dashboard Session'}
>
<Eraser size={14} />
</LucentIconButton>
<LucentIconButton <LucentIconButton
className="btn btn-danger btn-sm icon-btn" className="btn btn-danger btn-sm icon-btn"
type="button" type="button"
@ -622,6 +661,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
<span>{isZh ? '总计' : 'Total'}</span> <span>{isZh ? '总计' : 'Total'}</span>
<span>{isZh ? '时间 / 来源' : 'Time / Source'}</span> <span>{isZh ? '时间 / 来源' : 'Time / Source'}</span>
</div> </div>
{!pageSizeReady ? (
<div className="ops-empty-inline">{isZh ? '正在同步分页设置...' : 'Syncing page size...'}</div>
) : null}
{usageItems.map((item) => ( {usageItems.map((item) => (
<div key={item.id} className="platform-usage-row"> <div key={item.id} className="platform-usage-row">
<div> <div>
@ -644,35 +686,37 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
</div> </div>
</div> </div>
))} ))}
{!usageLoading && usageItems.length === 0 ? ( {pageSizeReady && !usageLoading && usageItems.length === 0 ? (
<div className="ops-empty-inline">{isZh ? '暂无请求用量数据。' : 'No usage records yet.'}</div> <div className="ops-empty-inline">{isZh ? '暂无请求用量数据。' : 'No usage records yet.'}</div>
) : null} ) : null}
</div> </div>
<div className="platform-usage-pager"> {pageSizeReady ? (
<span className="pager-status">{isZh ? `${usagePage} / ${usagePageCount} 页,共 ${usageTotal}` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`}</span> <div className="platform-usage-pager">
<div className="platform-usage-pager-actions"> <span className="pager-status">{isZh ? `${usagePage} / ${usagePageCount} 页,共 ${usageTotal}` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`}</span>
<LucentIconButton <div className="platform-usage-pager-actions">
className="btn btn-secondary btn-sm icon-btn pager-icon-btn" <LucentIconButton
type="button" className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
disabled={usageLoading || usagePage <= 1} type="button"
onClick={() => void loadUsage(usagePage - 1)} disabled={usageLoading || usagePage <= 1}
tooltip={isZh ? '上一页' : 'Previous'} onClick={() => void loadUsage(usagePage - 1)}
aria-label={isZh ? '上一页' : 'Previous'} tooltip={isZh ? '上一页' : 'Previous'}
> aria-label={isZh ? '上一页' : 'Previous'}
<ChevronLeft size={16} /> >
</LucentIconButton> <ChevronLeft size={16} />
<LucentIconButton </LucentIconButton>
className="btn btn-secondary btn-sm icon-btn pager-icon-btn" <LucentIconButton
type="button" className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
disabled={usageLoading || usagePage >= usagePageCount} type="button"
onClick={() => void loadUsage(usagePage + 1)} disabled={usageLoading || usagePage >= usagePageCount}
tooltip={isZh ? '下一页' : 'Next'} onClick={() => void loadUsage(usagePage + 1)}
aria-label={isZh ? '下一页' : 'Next'} tooltip={isZh ? '下一页' : 'Next'}
> aria-label={isZh ? '下一页' : 'Next'}
<ChevronRight size={16} /> >
</LucentIconButton> <ChevronRight size={16} />
</LucentIconButton>
</div>
</div> </div>
</div> ) : null}
</section> </section>
); );
@ -720,7 +764,10 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
</div> </div>
<div className="list-scroll platform-bot-list-scroll"> <div className="list-scroll platform-bot-list-scroll">
{pagedBots.map((bot) => { {!pageSizeReady ? (
<div className="ops-bot-list-empty">{isZh ? '正在同步分页设置...' : 'Syncing page size...'}</div>
) : null}
{pageSizeReady ? pagedBots.map((bot) => {
const enabled = bot.enabled !== false; const enabled = bot.enabled !== false;
const running = bot.docker_status === 'RUNNING'; const running = bot.docker_status === 'RUNNING';
const selected = bot.id === selectedBotId; const selected = bot.id === selectedBotId;
@ -772,36 +819,38 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
</div> </div>
</div> </div>
); );
})} }) : null}
{filteredBots.length === 0 ? ( {pageSizeReady && filteredBots.length === 0 ? (
<div className="ops-bot-list-empty">{isZh ? '暂无 Bot或当前搜索没有结果。' : 'No bots found, or the current search returned no results.'}</div> <div className="ops-bot-list-empty">{isZh ? '暂无 Bot或当前搜索没有结果。' : 'No bots found, or the current search returned no results.'}</div>
) : null} ) : null}
</div> </div>
<div className="platform-usage-pager"> {pageSizeReady ? (
<span className="pager-status">{isZh ? `${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`}</span> <div className="platform-usage-pager">
<div className="platform-usage-pager-actions"> <span className="pager-status">{isZh ? `${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`}</span>
<LucentIconButton <div className="platform-usage-pager-actions">
className="btn btn-secondary btn-sm icon-btn pager-icon-btn" <LucentIconButton
type="button" className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
disabled={botListPage <= 1} type="button"
onClick={() => setBotListPage((value) => Math.max(1, value - 1))} disabled={botListPage <= 1}
tooltip={isZh ? '上一页' : 'Previous'} onClick={() => setBotListPage((value) => Math.max(1, value - 1))}
aria-label={isZh ? '上一页' : 'Previous'} tooltip={isZh ? '上一页' : 'Previous'}
> aria-label={isZh ? '上一页' : 'Previous'}
<ChevronLeft size={16} /> >
</LucentIconButton> <ChevronLeft size={16} />
<LucentIconButton </LucentIconButton>
className="btn btn-secondary btn-sm icon-btn pager-icon-btn" <LucentIconButton
type="button" className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
disabled={botListPage >= botListPageCount} type="button"
onClick={() => setBotListPage((value) => Math.min(botListPageCount, value + 1))} disabled={botListPage >= botListPageCount}
tooltip={isZh ? '下一页' : 'Next'} onClick={() => setBotListPage((value) => Math.min(botListPageCount, value + 1))}
aria-label={isZh ? '下一页' : 'Next'} tooltip={isZh ? '下一页' : 'Next'}
> aria-label={isZh ? '下一页' : 'Next'}
<ChevronRight size={16} /> >
</LucentIconButton> <ChevronRight size={16} />
</LucentIconButton>
</div>
</div> </div>
</div> ) : null}
</section> </section>
{!compactMode ? ( {!compactMode ? (