v0.1.4-p3
parent
7d747e6fac
commit
34988b2436
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue