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
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]:
path = _env_store_path(bot_id)
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}
@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")
def get_bot_logs(bot_id: str, tail: int = 300, session: Session = Depends(get_session)):
bot = session.get(BotInstance, bot_id)

View File

@ -1,8 +1,6 @@
"""LiteLLM provider implementation for multi-provider support."""
import ast
import hashlib
import json
import os
import secrets
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"})
_ANTHROPIC_EXTRA_KEYS = frozenset({"thinking_blocks"})
_ALNUM = string.ascii_letters + string.digits
_ARG_PATCH_VERBOSE_VALUES = {"1", "true", "yes", "on"}
def _short_tool_id() -> str:
"""Generate a 9-char alphanumeric ID compatible with all providers (incl. Mistral)."""
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):
"""
LLM provider using LiteLLM for multi-provider support.
@ -222,7 +183,6 @@ class LiteLLMProvider(LLMProvider):
allowed = _ALLOWED_MSG_KEYS | extra_keys
sanitized = LLMProvider._sanitize_request_messages(messages, allowed)
id_map: dict[str, str] = {}
patched_arguments = 0
def map_id(value: Any) -> Any:
if not isinstance(value, str):
@ -240,26 +200,11 @@ class LiteLLMProvider(LLMProvider):
continue
tc_clean = dict(tc)
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)
clean["tool_calls"] = normalized_tool_calls
if "tool_call_id" in clean and 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
async def chat(

View File

@ -6,6 +6,7 @@ import {
ChevronLeft,
ChevronRight,
Cpu,
Eraser,
Eye,
ExternalLink,
FileText,
@ -152,6 +153,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
const [usageLoading, setUsageLoading] = useState(false);
const [usagePage, setUsagePage] = useState(1);
const [usagePageSize, setUsagePageSize] = useState(() => readCachedPlatformPageSize(10));
const [pageSizeReady, setPageSizeReady] = useState(() => readCachedPlatformPageSize(0) > 0);
const [botListPage, setBotListPage] = useState(1);
const [botListPageSize, setBotListPageSize] = useState(() => readCachedPlatformPageSize(10));
const [showCompactBotSheet, setShowCompactBotSheet] = useState(false);
@ -203,6 +205,7 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
} catch (error: any) {
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
} finally {
setPageSizeReady(true);
setLoading(false);
}
};
@ -250,8 +253,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
}, []);
useEffect(() => {
if (!pageSizeReady) return;
void loadUsage(1);
}, [usagePageSize]);
}, [pageSizeReady, usagePageSize]);
useEffect(() => {
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) => {
if (!botId) return;
setResourceLoading(true);
@ -391,8 +420,8 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
const overviewImages = overview?.summary.images;
const overviewResources = overview?.summary.resources;
const usageSummary = usageData?.summary || overview?.usage.summary;
const usageItems = usageData?.items || overview?.usage.items || [];
const usageTotal = usageData?.total || 0;
const usageItems = pageSizeReady ? usageData?.items || [] : [];
const usageTotal = pageSizeReady ? usageData?.total || 0 : 0;
const usagePageCount = Math.max(1, Math.ceil(usageTotal / usagePageSize));
const selectedBotInfo = selectedBotDetail && selectedBotDetail.id === selectedBotId ? { ...selectedBot, ...selectedBotDetail } : selectedBot;
const lastActionPreview = selectedBotInfo?.last_action?.trim() || '';
@ -502,6 +531,16 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
>
<Gauge size={14} />
</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
className="btn btn-danger btn-sm icon-btn"
type="button"
@ -622,6 +661,9 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
<span>{isZh ? '总计' : 'Total'}</span>
<span>{isZh ? '时间 / 来源' : 'Time / Source'}</span>
</div>
{!pageSizeReady ? (
<div className="ops-empty-inline">{isZh ? '正在同步分页设置...' : 'Syncing page size...'}</div>
) : null}
{usageItems.map((item) => (
<div key={item.id} className="platform-usage-row">
<div>
@ -644,35 +686,37 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
</div>
</div>
))}
{!usageLoading && usageItems.length === 0 ? (
{pageSizeReady && !usageLoading && usageItems.length === 0 ? (
<div className="ops-empty-inline">{isZh ? '暂无请求用量数据。' : 'No usage records yet.'}</div>
) : null}
</div>
<div className="platform-usage-pager">
<span className="pager-status">{isZh ? `${usagePage} / ${usagePageCount} 页,共 ${usageTotal}` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`}</span>
<div className="platform-usage-pager-actions">
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={usageLoading || usagePage <= 1}
onClick={() => void loadUsage(usagePage - 1)}
tooltip={isZh ? '上一页' : 'Previous'}
aria-label={isZh ? '上一页' : 'Previous'}
>
<ChevronLeft size={16} />
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={usageLoading || usagePage >= usagePageCount}
onClick={() => void loadUsage(usagePage + 1)}
tooltip={isZh ? '下一页' : 'Next'}
aria-label={isZh ? '下一页' : 'Next'}
>
<ChevronRight size={16} />
</LucentIconButton>
{pageSizeReady ? (
<div className="platform-usage-pager">
<span className="pager-status">{isZh ? `${usagePage} / ${usagePageCount} 页,共 ${usageTotal}` : `Page ${usagePage} / ${usagePageCount}, ${usageTotal} rows`}</span>
<div className="platform-usage-pager-actions">
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={usageLoading || usagePage <= 1}
onClick={() => void loadUsage(usagePage - 1)}
tooltip={isZh ? '上一页' : 'Previous'}
aria-label={isZh ? '上一页' : 'Previous'}
>
<ChevronLeft size={16} />
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={usageLoading || usagePage >= usagePageCount}
onClick={() => void loadUsage(usagePage + 1)}
tooltip={isZh ? '下一页' : 'Next'}
aria-label={isZh ? '下一页' : 'Next'}
>
<ChevronRight size={16} />
</LucentIconButton>
</div>
</div>
</div>
) : null}
</section>
);
@ -720,7 +764,10 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
</div>
<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 running = bot.docker_status === 'RUNNING';
const selected = bot.id === selectedBotId;
@ -772,36 +819,38 @@ export function PlatformDashboardPage({ compactMode }: PlatformDashboardPageProp
</div>
</div>
);
})}
{filteredBots.length === 0 ? (
}) : null}
{pageSizeReady && filteredBots.length === 0 ? (
<div className="ops-bot-list-empty">{isZh ? '暂无 Bot或当前搜索没有结果。' : 'No bots found, or the current search returned no results.'}</div>
) : null}
</div>
<div className="platform-usage-pager">
<span className="pager-status">{isZh ? `${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`}</span>
<div className="platform-usage-pager-actions">
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={botListPage <= 1}
onClick={() => setBotListPage((value) => Math.max(1, value - 1))}
tooltip={isZh ? '上一页' : 'Previous'}
aria-label={isZh ? '上一页' : 'Previous'}
>
<ChevronLeft size={16} />
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={botListPage >= botListPageCount}
onClick={() => setBotListPage((value) => Math.min(botListPageCount, value + 1))}
tooltip={isZh ? '下一页' : 'Next'}
aria-label={isZh ? '下一页' : 'Next'}
>
<ChevronRight size={16} />
</LucentIconButton>
{pageSizeReady ? (
<div className="platform-usage-pager">
<span className="pager-status">{isZh ? `${botListPage} / ${botListPageCount} 页,共 ${filteredBots.length} 个 Bot` : `Page ${botListPage} / ${botListPageCount}, ${filteredBots.length} bots`}</span>
<div className="platform-usage-pager-actions">
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={botListPage <= 1}
onClick={() => setBotListPage((value) => Math.max(1, value - 1))}
tooltip={isZh ? '上一页' : 'Previous'}
aria-label={isZh ? '上一页' : 'Previous'}
>
<ChevronLeft size={16} />
</LucentIconButton>
<LucentIconButton
className="btn btn-secondary btn-sm icon-btn pager-icon-btn"
type="button"
disabled={botListPage >= botListPageCount}
onClick={() => setBotListPage((value) => Math.min(botListPageCount, value + 1))}
tooltip={isZh ? '下一页' : 'Next'}
aria-label={isZh ? '下一页' : 'Next'}
>
<ChevronRight size={16} />
</LucentIconButton>
</div>
</div>
</div>
) : null}
</section>
{!compactMode ? (