fix git bugs.

main
mula.liu 2026-04-13 21:03:07 +08:00
parent 9f98d3f68d
commit 3622117d85
57 changed files with 445 additions and 276 deletions

View File

@ -27,6 +27,13 @@ class BotDockerManager:
self._storage_limit_supported: Optional[bool] = None self._storage_limit_supported: Optional[bool] = None
self._storage_limit_warning_emitted = False self._storage_limit_warning_emitted = False
def _new_short_lived_client(self):
try:
return docker.from_env(timeout=6)
except Exception as e:
print(f"[DockerManager] failed to create short-lived client: {e}")
return None
@staticmethod @staticmethod
def _normalize_resource_limits( def _normalize_resource_limits(
cpu_cores: Optional[float], cpu_cores: Optional[float],
@ -612,21 +619,41 @@ class BotDockerManager:
self._last_delivery_error[bot_id] = reason self._last_delivery_error[bot_id] = reason
return False return False
def _read_log_lines(self, bot_id: str, tail: Optional[int] = None) -> List[str]: def _read_log_lines_with_client(self, client, bot_id: str, tail: Optional[int] = None) -> List[str]:
container = client.containers.get(f"worker_{bot_id}")
raw = container.logs(tail=max(1, int(tail))) if tail is not None else container.logs()
if isinstance(raw, (bytes, bytearray)):
text = raw.decode("utf-8", errors="ignore")
else:
text = str(raw or "")
return [line for line in text.splitlines() if line.strip()]
def _read_log_lines(self, bot_id: str, tail: Optional[int] = None, *, fresh_client: bool = False) -> List[str]:
if fresh_client:
client = self._new_short_lived_client()
if not client:
return []
try:
return self._read_log_lines_with_client(client, bot_id, tail=tail)
except Exception as e:
print(f"[DockerManager] Error reading logs for {bot_id} with short-lived client: {e}")
return []
finally:
try:
client.close()
except Exception:
pass
if not self.client: if not self.client:
return [] return []
container_name = f"worker_{bot_id}"
try: try:
container = self.client.containers.get(container_name) return self._read_log_lines_with_client(self.client, bot_id, tail=tail)
raw = container.logs(tail=max(1, int(tail))) if tail is not None else container.logs()
text = raw.decode("utf-8", errors="ignore")
return [line for line in text.splitlines() if line.strip()]
except Exception as e: except Exception as e:
print(f"[DockerManager] Error reading logs for {bot_id}: {e}") print(f"[DockerManager] Error reading logs for {bot_id}: {e}")
return [] return []
def get_recent_logs(self, bot_id: str, tail: int = 300) -> List[str]: def get_recent_logs(self, bot_id: str, tail: int = 300, *, fresh_client: bool = False) -> List[str]:
return self._read_log_lines(bot_id, tail=max(1, int(tail))) return self._read_log_lines(bot_id, tail=max(1, int(tail)), fresh_client=fresh_client)
def get_logs_page( def get_logs_page(
self, self,
@ -634,6 +661,8 @@ class BotDockerManager:
offset: int = 0, offset: int = 0,
limit: int = 50, limit: int = 50,
reverse: bool = True, reverse: bool = True,
*,
fresh_client: bool = False,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
safe_offset = max(0, int(offset)) safe_offset = max(0, int(offset))
safe_limit = max(1, int(limit)) safe_limit = max(1, int(limit))
@ -641,7 +670,7 @@ class BotDockerManager:
# Docker logs API supports tail but not arbitrary offsets. For reverse pagination # Docker logs API supports tail but not arbitrary offsets. For reverse pagination
# we only read the minimal newest slice needed for the requested page. # we only read the minimal newest slice needed for the requested page.
tail_count = safe_offset + safe_limit + 1 tail_count = safe_offset + safe_limit + 1
lines = self._read_log_lines(bot_id, tail=tail_count) lines = self._read_log_lines(bot_id, tail=tail_count, fresh_client=fresh_client)
ordered = list(reversed(lines)) ordered = list(reversed(lines))
page = ordered[safe_offset:safe_offset + safe_limit] page = ordered[safe_offset:safe_offset + safe_limit]
has_more = len(lines) > safe_offset + safe_limit has_more = len(lines) > safe_offset + safe_limit
@ -654,7 +683,7 @@ class BotDockerManager:
"reverse": reverse, "reverse": reverse,
} }
lines = self._read_log_lines(bot_id, tail=None) lines = self._read_log_lines(bot_id, tail=None, fresh_client=fresh_client)
total = len(lines) total = len(lines)
page = lines[safe_offset:safe_offset + safe_limit] page = lines[safe_offset:safe_offset + safe_limit]
return { return {

View File

@ -84,10 +84,14 @@ def get_bot_logs(
offset=max(0, int(offset)), offset=max(0, int(offset)),
limit=max(1, int(limit)), limit=max(1, int(limit)),
reverse=bool(reverse), reverse=bool(reverse),
fresh_client=True,
) )
return {"bot_id": bot_id, **page} return {"bot_id": bot_id, **page}
effective_tail = max(1, int(tail or 300)) effective_tail = max(1, int(tail or 300))
return {"bot_id": bot_id, "logs": docker_manager.get_recent_logs(bot_id, tail=effective_tail)} return {
"bot_id": bot_id,
"logs": docker_manager.get_recent_logs(bot_id, tail=effective_tail, fresh_client=True),
}
async def relogin_weixin(session: Session, *, bot_id: str) -> Dict[str, Any]: async def relogin_weixin(session: Session, *, bot_id: str) -> Dict[str, Any]:

View File

@ -48,7 +48,7 @@ function AppShell() {
const [botCompactPanelTab, setBotCompactPanelTab] = useState<CompactBotPanelTab>('chat'); const [botCompactPanelTab, setBotCompactPanelTab] = useState<CompactBotPanelTab>('chat');
const forcedBotId = route.kind === 'bot' ? route.botId : ''; const forcedBotId = route.kind === 'bot' ? route.botId : '';
useBotsSync(forcedBotId || undefined); useBotsSync(forcedBotId || undefined, route.kind === 'bot');
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;

View File

@ -0,0 +1,24 @@
import { createContext } from 'react';
export type PromptTone = 'info' | 'success' | 'warning' | 'error';
export interface NotifyOptions {
title?: string;
tone?: PromptTone;
durationMs?: number;
}
export interface ConfirmOptions {
title?: string;
message: string;
tone?: PromptTone;
confirmText?: string;
cancelText?: string;
}
export interface LucentPromptApi {
notify: (message: string, options?: NotifyOptions) => void;
confirm: (options: ConfirmOptions) => Promise<boolean>;
}
export const LucentPromptContext = createContext<LucentPromptApi | null>(null);

View File

@ -1,25 +1,11 @@
import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react'; import { useCallback, useMemo, useRef, useState, type ReactNode } from 'react';
import { AlertCircle, AlertTriangle, CheckCircle2, Info, X } from 'lucide-react'; import { AlertCircle, AlertTriangle, CheckCircle2, Info, X } from 'lucide-react';
import { useAppStore } from '../../store/appStore'; import { useAppStore } from '../../store/appStore';
import type { ConfirmOptions, LucentPromptApi, NotifyOptions, PromptTone } from './LucentPromptContext';
import { LucentPromptContext } from './LucentPromptContext';
import { LucentIconButton } from './LucentIconButton'; import { LucentIconButton } from './LucentIconButton';
import './lucent-prompt.css'; import './lucent-prompt.css';
type PromptTone = 'info' | 'success' | 'warning' | 'error';
interface NotifyOptions {
title?: string;
tone?: PromptTone;
durationMs?: number;
}
interface ConfirmOptions {
title?: string;
message: string;
tone?: PromptTone;
confirmText?: string;
cancelText?: string;
}
interface ToastItem { interface ToastItem {
id: number; id: number;
title?: string; title?: string;
@ -36,13 +22,6 @@ interface ConfirmState {
resolve: (value: boolean) => void; resolve: (value: boolean) => void;
} }
interface LucentPromptApi {
notify: (message: string, options?: NotifyOptions) => void;
confirm: (options: ConfirmOptions) => Promise<boolean>;
}
const LucentPromptContext = createContext<LucentPromptApi | null>(null);
function ToneIcon({ tone }: { tone: PromptTone }) { function ToneIcon({ tone }: { tone: PromptTone }) {
if (tone === 'success') return <CheckCircle2 size={16} />; if (tone === 'success') return <CheckCircle2 size={16} />;
if (tone === 'warning') return <AlertTriangle size={16} />; if (tone === 'warning') return <AlertTriangle size={16} />;
@ -158,11 +137,3 @@ export function LucentPromptProvider({ children }: { children: ReactNode }) {
</LucentPromptContext.Provider> </LucentPromptContext.Provider>
); );
} }
export function useLucentPrompt() {
const ctx = useContext(LucentPromptContext);
if (!ctx) {
throw new Error('useLucentPrompt must be used inside LucentPromptProvider');
}
return ctx;
}

View File

@ -0,0 +1,11 @@
import { useContext } from 'react';
import { LucentPromptContext } from './LucentPromptContext';
export function useLucentPrompt() {
const ctx = useContext(LucentPromptContext);
if (!ctx) {
throw new Error('useLucentPrompt must be used inside LucentPromptProvider');
}
return ctx;
}

View File

@ -121,7 +121,7 @@ function extractToolCallProgressHint(raw: string, isZh: boolean): string | null
return `${isZh ? '工具调用' : 'Tool Call'}\n${callLabel}`; return `${isZh ? '工具调用' : 'Tool Call'}\n${callLabel}`;
} }
export function useBotsSync(forcedBotId?: string) { export function useBotsSync(forcedBotId?: string, enableMonitorSockets: boolean = true) {
const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore(); const { activeBots, setBots, updateBotState, addBotLog, addBotMessage, addBotEvent, setBotMessages } = useAppStore();
const socketsRef = useRef<Record<string, WebSocket>>({}); const socketsRef = useRef<Record<string, WebSocket>>({});
const heartbeatsRef = useRef<Record<string, number>>({}); const heartbeatsRef = useRef<Record<string, number>>({});
@ -254,6 +254,16 @@ export function useBotsSync(forcedBotId?: string) {
}, [syncBotMessages]); }, [syncBotMessages]);
useEffect(() => { useEffect(() => {
if (!enableMonitorSockets) {
Object.values(socketsRef.current).forEach((ws) => ws.close());
Object.values(heartbeatsRef.current).forEach((timerId) => window.clearInterval(timerId));
heartbeatsRef.current = {};
socketsRef.current = {};
return () => {
// no-op
};
}
const runningIds = new Set( const runningIds = new Set(
Object.values(activeBots) Object.values(activeBots)
.filter((bot) => bot.docker_status === 'RUNNING') .filter((bot) => bot.docker_status === 'RUNNING')
@ -421,7 +431,7 @@ export function useBotsSync(forcedBotId?: string) {
return () => { return () => {
// no-op: clean in unmount effect below // no-op: clean in unmount effect below
}; };
}, [activeBots, addBotEvent, addBotLog, addBotMessage, forced, isZh, syncBotMessages, t.progress, t.replied, t.stateUpdated, updateBotState]); }, [activeBots, addBotEvent, addBotLog, addBotMessage, enableMonitorSockets, forced, isZh, syncBotMessages, t.progress, t.replied, t.stateUpdated, updateBotState]);
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

@ -9,6 +9,7 @@ export const dashboardEn = {
channelSaved: 'Channel saved (effective after bot restart).', channelSaved: 'Channel saved (effective after bot restart).',
channelSaveFail: 'Failed to save channel.', channelSaveFail: 'Failed to save channel.',
channelAddFail: 'Failed to add channel.', channelAddFail: 'Failed to add channel.',
channelDeleted: 'Channel deleted.',
channelDeleteConfirm: (channelType: string) => `Delete channel ${channelType}?`, channelDeleteConfirm: (channelType: string) => `Delete channel ${channelType}?`,
channelDeleteFail: 'Failed to delete channel.', channelDeleteFail: 'Failed to delete channel.',
stopFail: 'Stop failed. Check backend logs.', stopFail: 'Stop failed. Check backend logs.',

View File

@ -9,6 +9,7 @@ export const dashboardZhCn = {
channelSaved: '渠道配置已保存(重启 Bot 后生效)。', channelSaved: '渠道配置已保存(重启 Bot 后生效)。',
channelSaveFail: '渠道保存失败。', channelSaveFail: '渠道保存失败。',
channelAddFail: '新增渠道失败。', channelAddFail: '新增渠道失败。',
channelDeleted: '渠道已删除。',
channelDeleteConfirm: (channelType: string) => `确认删除渠道 ${channelType}`, channelDeleteConfirm: (channelType: string) => `确认删除渠道 ${channelType}`,
channelDeleteFail: '删除渠道失败。', channelDeleteFail: '删除渠道失败。',
stopFail: '停止失败,请查看后端日志。', stopFail: '停止失败,请查看后端日志。',

View File

@ -2,6 +2,7 @@ import type { ChatMessage } from '../../../types/bot';
import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText'; import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText';
import { normalizeAttachmentPaths } from '../../../shared/workspace/utils'; import { normalizeAttachmentPaths } from '../../../shared/workspace/utils';
import { normalizeDashboardAttachmentPath } from '../../../shared/workspace/workspaceMarkdown'; import { normalizeDashboardAttachmentPath } from '../../../shared/workspace/workspaceMarkdown';
import type { BotMessageResponseRow } from '../types';
export function formatClock(ts: number) { export function formatClock(ts: number) {
const d = new Date(ts); const d = new Date(ts);
@ -37,7 +38,7 @@ export function formatDateInputValue(ts: number): string {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
export function mapBotMessageResponseRow(row: any): ChatMessage { export function mapBotMessageResponseRow(row: BotMessageResponseRow): ChatMessage {
const roleRaw = String(row?.role || '').toLowerCase(); const roleRaw = String(row?.role || '').toLowerCase();
const role: ChatMessage['role'] = const role: ChatMessage['role'] =
roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system' ? roleRaw : 'assistant'; roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system' ? roleRaw : 'assistant';

View File

@ -3,6 +3,7 @@ import { memo, type RefObject } from 'react';
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import type { BotState } from '../../../types/bot';
import type { CompactPanelTab } from '../types'; import type { CompactPanelTab } from '../types';
import './DashboardMenus.css'; import './DashboardMenus.css';
import './BotListPanel.css'; import './BotListPanel.css';
@ -33,9 +34,9 @@ interface BotListLabels {
} }
interface BotListPanelProps { interface BotListPanelProps {
bots: any[]; bots: BotState[];
filteredBots: any[]; filteredBots: BotState[];
pagedBots: any[]; pagedBots: BotState[];
selectedBotId: string; selectedBotId: string;
normalizedBotListQuery: string; normalizedBotListQuery: string;
botListQuery: string; botListQuery: string;

View File

@ -4,6 +4,7 @@ import type { RefObject } from 'react';
import { DrawerShell } from '../../../components/DrawerShell'; import { DrawerShell } from '../../../components/DrawerShell';
import { PasswordInput } from '../../../components/PasswordInput'; import { PasswordInput } from '../../../components/PasswordInput';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import type { ChannelLabels, DashboardLabels } from '../localeTypes';
import type { BotChannel, ChannelType, WeixinLoginStatus } from '../types'; import type { BotChannel, ChannelType, WeixinLoginStatus } from '../types';
import './DashboardManagementModals.css'; import './DashboardManagementModals.css';
@ -12,6 +13,8 @@ interface PasswordToggleLabels {
hide: string; hide: string;
} }
type ChannelConfigLabels = ChannelLabels & Pick<DashboardLabels, 'cancel' | 'close'>;
function parseChannelListValue(raw: unknown): string { function parseChannelListValue(raw: unknown): string {
if (!Array.isArray(raw)) return ''; if (!Array.isArray(raw)) return '';
return raw return raw
@ -57,7 +60,7 @@ function ChannelFieldsEditor({
onPatch, onPatch,
}: { }: {
channel: BotChannel; channel: BotChannel;
labels: Record<string, any>; labels: ChannelConfigLabels;
passwordToggleLabels: PasswordToggleLabels; passwordToggleLabels: PasswordToggleLabels;
onPatch: (patch: Partial<BotChannel>) => void; onPatch: (patch: Partial<BotChannel>) => void;
}) { }) {
@ -289,7 +292,7 @@ interface ChannelConfigModalProps {
weixinLoginStatus: WeixinLoginStatus | null; weixinLoginStatus: WeixinLoginStatus | null;
hasSelectedBot: boolean; hasSelectedBot: boolean;
isZh: boolean; isZh: boolean;
labels: Record<string, any>; labels: ChannelConfigLabels;
passwordToggleLabels: PasswordToggleLabels; passwordToggleLabels: PasswordToggleLabels;
onClose: () => void; onClose: () => void;
onUpdateGlobalDeliveryFlag: (key: 'sendProgress' | 'sendToolHints', value: boolean) => void; onUpdateGlobalDeliveryFlag: (key: 'sendProgress' | 'sendToolHints', value: boolean) => void;

View File

@ -5,6 +5,7 @@ import { DrawerShell } from '../../../components/DrawerShell';
import { PasswordInput } from '../../../components/PasswordInput'; import { PasswordInput } from '../../../components/PasswordInput';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { LucentSelect } from '../../../components/lucent/LucentSelect'; import { LucentSelect } from '../../../components/lucent/LucentSelect';
import type { DashboardLabels } from '../localeTypes';
import type { MCPServerDraft, WorkspaceSkillOption } from '../types'; import type { MCPServerDraft, WorkspaceSkillOption } from '../types';
import './DashboardManagementModals.css'; import './DashboardManagementModals.css';
@ -19,7 +20,7 @@ interface SkillsModalProps {
isSkillUploading: boolean; isSkillUploading: boolean;
isZh: boolean; isZh: boolean;
hasSelectedBot: boolean; hasSelectedBot: boolean;
labels: Record<string, any>; labels: DashboardLabels;
skillZipPickerRef: RefObject<HTMLInputElement | null>; skillZipPickerRef: RefObject<HTMLInputElement | null>;
skillAddMenuRef: RefObject<HTMLDivElement | null>; skillAddMenuRef: RefObject<HTMLDivElement | null>;
skillAddMenuOpen: boolean; skillAddMenuOpen: boolean;
@ -144,7 +145,7 @@ interface McpConfigModalProps {
newMcpPanelOpen: boolean; newMcpPanelOpen: boolean;
isSavingMcp: boolean; isSavingMcp: boolean;
isZh: boolean; isZh: boolean;
labels: Record<string, any>; labels: DashboardLabels;
passwordToggleLabels: PasswordToggleLabels; passwordToggleLabels: PasswordToggleLabels;
onClose: () => void; onClose: () => void;
getMcpUiKey: (row: Pick<MCPServerDraft, 'name' | 'url'>, fallbackIndex: number) => string; getMcpUiKey: (row: Pick<MCPServerDraft, 'name' | 'url'>, fallbackIndex: number) => string;

View File

@ -3,6 +3,7 @@ import type { RefObject } from 'react';
import { DrawerShell } from '../../../components/DrawerShell'; import { DrawerShell } from '../../../components/DrawerShell';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import type { DashboardLabels } from '../localeTypes';
import type { BotTopic, TopicPresetTemplate } from '../types'; import type { BotTopic, TopicPresetTemplate } from '../types';
import './DashboardManagementModals.css'; import './DashboardManagementModals.css';
@ -28,7 +29,7 @@ interface TopicConfigModalProps {
isSavingTopic: boolean; isSavingTopic: boolean;
hasSelectedBot: boolean; hasSelectedBot: boolean;
isZh: boolean; isZh: boolean;
labels: Record<string, any>; labels: DashboardLabels;
onClose: () => void; onClose: () => void;
getTopicUiKey: (topic: Pick<BotTopic, 'topic_key' | 'id'>, fallbackIndex: number) => string; getTopicUiKey: (topic: Pick<BotTopic, 'topic_key' | 'id'>, fallbackIndex: number) => string;
countRoutingTextList: (raw: string) => number; countRoutingTextList: (raw: string) => number;

View File

@ -1,6 +1,8 @@
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { DashboardLabels } from '../localeTypes';
import type { MCPConfigResponse, MCPServerConfig, MCPServerDraft } from '../types'; import type { MCPConfigResponse, MCPServerConfig, MCPServerDraft } from '../types';
import { mapMcpResponseToDrafts } from '../utils'; import { mapMcpResponseToDrafts } from '../utils';
@ -28,7 +30,7 @@ interface PromptApi {
interface McpManagerDeps extends PromptApi { interface McpManagerDeps extends PromptApi {
selectedBotId: string; selectedBotId: string;
isZh: boolean; isZh: boolean;
t: any; t: DashboardLabels;
currentMcpServers: MCPServerDraft[]; currentMcpServers: MCPServerDraft[];
currentPersistedMcpServers: MCPServerDraft[]; currentPersistedMcpServers: MCPServerDraft[];
currentNewMcpDraft: MCPServerDraft; currentNewMcpDraft: MCPServerDraft;
@ -172,8 +174,8 @@ export function createMcpManager({
resetNewMcpDraft(); resetNewMcpDraft();
} }
notify(t.mcpSaved, { tone: 'success' }); notify(t.mcpSaved, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.mcpSaveFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.mcpSaveFail), { tone: 'error' });
} finally { } finally {
setIsSavingMcp(false); setIsSavingMcp(false);
} }

View File

@ -1,6 +1,8 @@
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { DashboardLabels } from '../localeTypes';
import { isSystemFallbackTopic, normalizePresetTextList, resolvePresetText } from '../topic/topicPresetUtils'; import { isSystemFallbackTopic, normalizePresetTextList, resolvePresetText } from '../topic/topicPresetUtils';
import type { BotTopic, TopicPresetTemplate } from '../types'; import type { BotTopic, TopicPresetTemplate } from '../types';
@ -28,7 +30,7 @@ interface PromptApi {
interface TopicManagerDeps extends PromptApi { interface TopicManagerDeps extends PromptApi {
selectedBotId: string; selectedBotId: string;
isZh: boolean; isZh: boolean;
t: any; t: DashboardLabels;
effectiveTopicPresetTemplates: TopicPresetTemplate[]; effectiveTopicPresetTemplates: TopicPresetTemplate[];
setShowTopicModal: (value: boolean) => void; setShowTopicModal: (value: boolean) => void;
setTopics: (value: BotTopic[] | ((prev: BotTopic[]) => BotTopic[])) => void; setTopics: (value: BotTopic[] | ((prev: BotTopic[]) => BotTopic[])) => void;
@ -263,8 +265,8 @@ export function createTopicManager({
}); });
await loadTopics(selectedBotId); await loadTopics(selectedBotId);
notify(t.topicSaved, { tone: 'success' }); notify(t.topicSaved, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.topicSaveFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.topicSaveFail), { tone: 'error' });
} finally { } finally {
setIsSavingTopic(false); setIsSavingTopic(false);
} }
@ -298,8 +300,8 @@ export function createTopicManager({
resetNewTopicDraft(); resetNewTopicDraft();
setNewTopicPanelOpen(false); setNewTopicPanelOpen(false);
notify(t.topicSaved, { tone: 'success' }); notify(t.topicSaved, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.topicSaveFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.topicSaveFail), { tone: 'error' });
} finally { } finally {
setIsSavingTopic(false); setIsSavingTopic(false);
} }
@ -320,8 +322,8 @@ export function createTopicManager({
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/topics/${encodeURIComponent(topicKey)}`); await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/topics/${encodeURIComponent(topicKey)}`);
await loadTopics(selectedBotId); await loadTopics(selectedBotId);
notify(t.topicDeleted, { tone: 'success' }); notify(t.topicDeleted, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.topicDeleteFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.topicDeleteFail), { tone: 'error' });
} finally { } finally {
setIsSavingTopic(false); setIsSavingTopic(false);
} }

View File

@ -1,6 +1,6 @@
import { useId, useState } from 'react'; import { useId, useState } from 'react';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt';
import { pickLocale } from '../../../i18n'; import { pickLocale } from '../../../i18n';
import { useBotWorkspace } from '../../../shared/workspace/useBotWorkspace'; import { useBotWorkspace } from '../../../shared/workspace/useBotWorkspace';
import { channelsEn } from '../../../i18n/channels.en'; import { channelsEn } from '../../../i18n/channels.en';

View File

@ -2,7 +2,10 @@ import { useCallback, useEffect, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { BotState } from '../../../types/bot';
import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders'; import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders';
import type { DashboardLabels } from '../localeTypes';
import type { AgentTab, BotEditForm, BotParamDraft, NanobotImage } from '../types'; import type { AgentTab, BotEditForm, BotParamDraft, NanobotImage } from '../types';
import { clampCpuCores, clampMaxTokens, clampMemoryMb, clampStorageGb, clampTemperature } from '../utils'; import { clampCpuCores, clampMaxTokens, clampMemoryMb, clampStorageGb, clampTemperature } from '../utils';
@ -53,15 +56,15 @@ interface NotifyOptions {
interface UseDashboardBotEditorOptions { interface UseDashboardBotEditorOptions {
defaultSystemTimezone: string; defaultSystemTimezone: string;
ensureSelectedBotDetail: () => Promise<any>; ensureSelectedBotDetail: () => Promise<BotState | undefined>;
isZh: boolean; isZh: boolean;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
refresh: () => Promise<void>; refresh: () => Promise<void>;
selectedBotId: string; selectedBotId: string;
selectedBot?: any; selectedBot?: BotState;
setRuntimeMenuOpen: (open: boolean) => void; setRuntimeMenuOpen: (open: boolean) => void;
availableImages: NanobotImage[]; availableImages: NanobotImage[];
t: any; t: DashboardLabels;
} }
export function useDashboardBotEditor({ export function useDashboardBotEditor({
@ -86,7 +89,7 @@ export function useDashboardBotEditor({
const [showParamModal, setShowParamModal] = useState(false); const [showParamModal, setShowParamModal] = useState(false);
const [showAgentModal, setShowAgentModal] = useState(false); const [showAgentModal, setShowAgentModal] = useState(false);
const applyEditFormFromBot = useCallback((bot?: any) => { const applyEditFormFromBot = useCallback((bot?: BotState) => {
if (!bot) return; if (!bot) return;
const provider = String(bot.llm_provider || '').trim().toLowerCase(); const provider = String(bot.llm_provider || '').trim().toLowerCase();
setProviderTestResult(''); setProviderTestResult('');
@ -212,8 +215,8 @@ export function useDashboardBotEditor({
} else { } else {
setProviderTestResult(t.connFail(res.data?.detail || 'unknown error')); setProviderTestResult(t.connFail(res.data?.detail || 'unknown error'));
} }
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || error?.message || 'request failed'; const msg = resolveApiErrorMessage(error, 'request failed');
setProviderTestResult(t.connFail(msg)); setProviderTestResult(t.connFail(msg));
} finally { } finally {
setIsTestingProvider(false); setIsTestingProvider(false);
@ -286,8 +289,8 @@ export function useDashboardBotEditor({
await refresh(); await refresh();
closeModals(); closeModals();
notify(t.configUpdated, { tone: 'success' }); notify(t.configUpdated, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || t.saveFail; const msg = resolveApiErrorMessage(error, t.saveFail);
notify(msg, { tone: 'error' }); notify(msg, { tone: 'error' });
} finally { } finally {
setIsSaving(false); setIsSaving(false);

View File

@ -2,7 +2,10 @@ import { useCallback, useEffect } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { BotState } from '../../../types/bot'; import type { BotState } from '../../../types/bot';
import type { ChatMessage } from '../../../types/bot';
import type { DashboardLabels } from '../localeTypes';
type PromptTone = 'info' | 'success' | 'warning' | 'error'; type PromptTone = 'info' | 'success' | 'warning' | 'error';
@ -27,9 +30,9 @@ interface UseDashboardBotManagementOptions {
refresh: () => Promise<void>; refresh: () => Promise<void>;
selectedBot?: BotState; selectedBot?: BotState;
selectedBotId: string; selectedBotId: string;
setBotMessages: (botId: string, messages: any[]) => void; setBotMessages: (botId: string, messages: ChatMessage[]) => void;
setSelectedBotId: (botId: string) => void; setSelectedBotId: (botId: string) => void;
t: any; t: DashboardLabels;
} }
export function useDashboardBotManagement({ export function useDashboardBotManagement({
@ -92,8 +95,8 @@ export function useDashboardBotManagement({
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/messages`); await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBot.id}/messages`);
setBotMessages(selectedBot.id, []); setBotMessages(selectedBot.id, []);
notify(t.clearHistoryDone, { tone: 'success' }); notify(t.clearHistoryDone, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
const message = error?.response?.data?.detail || t.clearHistoryFail; const message = resolveApiErrorMessage(error, t.clearHistoryFail);
notify(message, { tone: 'error' }); notify(message, { tone: 'error' });
} }
}, [confirm, notify, selectedBot, setBotMessages, t]); }, [confirm, notify, selectedBot, setBotMessages, t]);

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState, type SetStateAction } from 'react';
import { channelsEn } from '../../../i18n/channels.en';
import type { BotState } from '../../../types/bot'; import type { BotState } from '../../../types/bot';
import type { ChannelLabels } from '../localeTypes';
import { optionalChannelTypes } from '../constants'; import { optionalChannelTypes } from '../constants';
import { createChannelManager, type ChannelManagerLabels, type GlobalDeliveryState } from '../config-managers/channelManager'; import { createChannelManager, type ChannelManagerLabels, type GlobalDeliveryState } from '../config-managers/channelManager';
import type { BotChannel, WeixinLoginStatus } from '../types'; import type { BotChannel, WeixinLoginStatus } from '../types';
@ -34,7 +34,7 @@ interface UseDashboardChannelConfigOptions {
selectedBot?: Pick<BotState, 'id' | 'docker_status' | 'send_progress' | 'send_tool_hints'> | null; selectedBot?: Pick<BotState, 'id' | 'docker_status' | 'send_progress' | 'send_tool_hints'> | null;
selectedBotId: string; selectedBotId: string;
t: ChannelManagerLabels & { cancel: string; close: string }; t: ChannelManagerLabels & { cancel: string; close: string };
lc: typeof channelsEn; lc: ChannelLabels;
weixinLoginStatus: WeixinLoginStatus | null; weixinLoginStatus: WeixinLoginStatus | null;
} }

View File

@ -2,8 +2,10 @@ import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateA
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { ChatMessage } from '../../../types/bot'; import type { ChatMessage } from '../../../types/bot';
import type { BotMessagesByDateResponse } from '../types'; import type { DashboardLabels } from '../localeTypes';
import type { BotMessageResponseRow, BotMessagesByDateResponse } from '../types';
import { import {
formatConversationDate, formatConversationDate,
formatDateInputValue, formatDateInputValue,
@ -23,7 +25,7 @@ interface UseDashboardChatHistoryOptions {
setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void; setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void;
notify: (message: string, options?: DashboardChatNotifyOptions) => void; notify: (message: string, options?: DashboardChatNotifyOptions) => void;
confirm: (options: DashboardChatConfirmOptions) => Promise<boolean>; confirm: (options: DashboardChatConfirmOptions) => Promise<boolean>;
t: any; t: DashboardLabels;
isZh: boolean; isZh: boolean;
} }
@ -127,7 +129,7 @@ export function useDashboardChatHistory({
const fetchBotMessages = useCallback(async (botId: string): Promise<ChatMessage[]> => { const fetchBotMessages = useCallback(async (botId: string): Promise<ChatMessage[]> => {
const safeLimit = Math.max(100, Math.min(500, chatPullPageSize * 3)); const safeLimit = Math.max(100, Math.min(500, chatPullPageSize * 3));
const res = await axios.get<any[]>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/messages`, { const res = await axios.get<BotMessageResponseRow[]>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/messages`, {
params: { limit: safeLimit }, params: { limit: safeLimit },
}); });
const rows = Array.isArray(res.data) ? res.data : []; const rows = Array.isArray(res.data) ? res.data : [];
@ -171,7 +173,7 @@ export function useDashboardChatHistory({
: Math.max(10, Math.min(500, chatPullPageSize)); : Math.max(10, Math.min(500, chatPullPageSize));
const beforeIdRaw = Number(options?.beforeId); const beforeIdRaw = Number(options?.beforeId);
const beforeId = Number.isFinite(beforeIdRaw) && beforeIdRaw > 0 ? Math.floor(beforeIdRaw) : undefined; const beforeId = Number.isFinite(beforeIdRaw) && beforeIdRaw > 0 ? Math.floor(beforeIdRaw) : undefined;
const res = await axios.get<{ items?: any[]; has_more?: boolean; next_before_id?: number | null }>( const res = await axios.get<{ items?: BotMessageResponseRow[]; has_more?: boolean; next_before_id?: number | null }>(
`${APP_ENDPOINTS.apiBase}/bots/${botId}/messages/page`, `${APP_ENDPOINTS.apiBase}/bots/${botId}/messages/page`,
{ {
params: { params: {
@ -364,8 +366,8 @@ export function useDashboardChatHistory({
{ tone: 'warning' }, { tone: 'warning' },
); );
} }
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '按日期读取对话失败。' : 'Failed to load conversation by date.'), { notify(resolveApiErrorMessage(error, isZh ? '按日期读取对话失败。' : 'Failed to load conversation by date.'), {
tone: 'error', tone: 'error',
}); });
} finally { } finally {

View File

@ -2,8 +2,10 @@ import { useCallback, useEffect, useState, type MutableRefObject, type RefObject
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { ChatMessage } from '../../../types/bot'; import type { ChatMessage } from '../../../types/bot';
import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText'; import { normalizeAssistantMessageText, normalizeUserMessageText } from '../../../shared/text/messageText';
import type { DashboardLabels } from '../localeTypes';
import type { DashboardChatConfirmOptions, DashboardChatNotifyOptions } from './dashboardChatShared'; import type { DashboardChatConfirmOptions, DashboardChatNotifyOptions } from './dashboardChatShared';
interface UseDashboardChatMessageActionsOptions { interface UseDashboardChatMessageActionsOptions {
@ -16,7 +18,7 @@ interface UseDashboardChatMessageActionsOptions {
setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void; setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void;
notify: (message: string, options?: DashboardChatNotifyOptions) => void; notify: (message: string, options?: DashboardChatNotifyOptions) => void;
confirm: (options: DashboardChatConfirmOptions) => Promise<boolean>; confirm: (options: DashboardChatConfirmOptions) => Promise<boolean>;
t: any; t: DashboardLabels;
} }
export function useDashboardChatMessageActions({ export function useDashboardChatMessageActions({
@ -76,8 +78,8 @@ export function useDashboardChatMessageActions({
} else { } else {
notify(nextFeedback === 'up' ? t.feedbackUpSaved : t.feedbackDownSaved, { tone: 'success' }); notify(nextFeedback === 'up' ? t.feedbackUpSaved : t.feedbackDownSaved, { tone: 'success' });
} }
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || t.feedbackSaveFail; const msg = resolveApiErrorMessage(error, t.feedbackSaveFail);
notify(msg, { tone: 'error' }); notify(msg, { tone: 'error' });
} finally { } finally {
setFeedbackSavingByMessageId((prev) => { setFeedbackSavingByMessageId((prev) => {
@ -189,8 +191,8 @@ export function useDashboardChatMessageActions({
await deleteConversationMessageOnServer(targetMessageId); await deleteConversationMessageOnServer(targetMessageId);
removeConversationMessageLocally(message, targetMessageId); removeConversationMessageLocally(message, targetMessageId);
notify(t.deleteMessageDone, { tone: 'success' }); notify(t.deleteMessageDone, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
if (error?.response?.status === 404) { if (axios.isAxiosError(error) && error.response?.status === 404) {
try { try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${targetMessageId}/delete`); await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${targetMessageId}/delete`);
removeConversationMessageLocally(message, targetMessageId); removeConversationMessageLocally(message, targetMessageId);
@ -200,7 +202,7 @@ export function useDashboardChatMessageActions({
// continue to secondary re-match fallback below // continue to secondary re-match fallback below
} }
} }
if (error?.response?.status === 404) { if (axios.isAxiosError(error) && error.response?.status === 404) {
const refreshedMessageId = Number(await resolveMessageIdFromLatest(message)); const refreshedMessageId = Number(await resolveMessageIdFromLatest(message));
if (Number.isFinite(refreshedMessageId) && refreshedMessageId > 0 && refreshedMessageId !== targetMessageId) { if (Number.isFinite(refreshedMessageId) && refreshedMessageId > 0 && refreshedMessageId !== targetMessageId) {
try { try {
@ -208,24 +210,24 @@ export function useDashboardChatMessageActions({
removeConversationMessageLocally(message, refreshedMessageId); removeConversationMessageLocally(message, refreshedMessageId);
notify(t.deleteMessageDone, { tone: 'success' }); notify(t.deleteMessageDone, { tone: 'success' });
return; return;
} catch (retryError: any) { } catch (retryError: unknown) {
if (retryError?.response?.status === 404) { if (axios.isAxiosError(retryError) && retryError.response?.status === 404) {
try { try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${refreshedMessageId}/delete`); await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/messages/${refreshedMessageId}/delete`);
removeConversationMessageLocally(message, refreshedMessageId); removeConversationMessageLocally(message, refreshedMessageId);
notify(t.deleteMessageDone, { tone: 'success' }); notify(t.deleteMessageDone, { tone: 'success' });
return; return;
} catch (postRetryError: any) { } catch (postRetryError: unknown) {
notify(postRetryError?.response?.data?.detail || t.deleteMessageFail, { tone: 'error' }); notify(resolveApiErrorMessage(postRetryError, t.deleteMessageFail), { tone: 'error' });
return; return;
} }
} }
notify(retryError?.response?.data?.detail || t.deleteMessageFail, { tone: 'error' }); notify(resolveApiErrorMessage(retryError, t.deleteMessageFail), { tone: 'error' });
return; return;
} }
} }
} }
notify(error?.response?.data?.detail || t.deleteMessageFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.deleteMessageFail), { tone: 'error' });
} finally { } finally {
setDeletingMessageIdMap((prev) => { setDeletingMessageIdMap((prev) => {
const next = { ...prev }; const next = { ...prev };

View File

@ -1,4 +1,7 @@
import { useCallback, useState, type ChangeEvent } from 'react'; import { useCallback, useState, type ChangeEvent } from 'react';
import type { BotState } from '../../../types/bot';
import type { BotSkillMarketItem } from '../../platform/types';
import type { ChannelLabels, DashboardLabels } from '../localeTypes';
import { useDashboardSkillsConfig } from './useDashboardSkillsConfig'; import { useDashboardSkillsConfig } from './useDashboardSkillsConfig';
import { useDashboardChannelConfig } from './useDashboardChannelConfig'; import { useDashboardChannelConfig } from './useDashboardChannelConfig';
import { useDashboardMcpConfig } from './useDashboardMcpConfig'; import { useDashboardMcpConfig } from './useDashboardMcpConfig';
@ -25,14 +28,14 @@ interface UseDashboardConfigPanelsOptions {
confirm: (options: ConfirmOptions) => Promise<boolean>; confirm: (options: ConfirmOptions) => Promise<boolean>;
cronActionJobId: string | null; cronActionJobId: string | null;
cronActionType?: 'starting' | 'stopping' | 'deleting' | ''; cronActionType?: 'starting' | 'stopping' | 'deleting' | '';
cronJobs: any[]; cronJobs: import('../types').CronJob[];
cronLoading: boolean; cronLoading: boolean;
createEnvParam: (key: string, value: string) => Promise<boolean>; createEnvParam: (key: string, value: string) => Promise<boolean>;
deleteCronJob: (jobId: string) => Promise<void>; deleteCronJob: (jobId: string) => Promise<void>;
deleteEnvParam: (key: string) => Promise<boolean>; deleteEnvParam: (key: string) => Promise<boolean>;
effectiveTopicPresetTemplates: TopicPresetTemplate[]; effectiveTopicPresetTemplates: TopicPresetTemplate[];
envEntries: [string, string][]; envEntries: [string, string][];
installMarketSkill: (skill: any) => Promise<void>; installMarketSkill: (skill: BotSkillMarketItem) => Promise<void>;
isMarketSkillsLoading: boolean; isMarketSkillsLoading: boolean;
isSkillUploading: boolean; isSkillUploading: boolean;
isZh: boolean; isZh: boolean;
@ -43,7 +46,7 @@ interface UseDashboardConfigPanelsOptions {
loadTopicFeedStats: (botId: string) => Promise<void>; loadTopicFeedStats: (botId: string) => Promise<void>;
loadWeixinLoginStatus: (botId: string, silent?: boolean) => Promise<void>; loadWeixinLoginStatus: (botId: string, silent?: boolean) => Promise<void>;
marketSkillInstallingId: number | null; marketSkillInstallingId: number | null;
marketSkills: any[]; marketSkills: BotSkillMarketItem[];
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
onPickSkillZip: (event: ChangeEvent<HTMLInputElement>) => Promise<void>; onPickSkillZip: (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
passwordToggleLabels: { show: string; hide: string }; passwordToggleLabels: { show: string; hide: string };
@ -52,12 +55,12 @@ interface UseDashboardConfigPanelsOptions {
removeBotSkill: (skill: WorkspaceSkillOption) => Promise<void>; removeBotSkill: (skill: WorkspaceSkillOption) => Promise<void>;
resetSupportState: () => void; resetSupportState: () => void;
saveSingleEnvParam: (originalKey: string, nextKey: string, nextValue: string) => Promise<boolean>; saveSingleEnvParam: (originalKey: string, nextKey: string, nextValue: string) => Promise<boolean>;
selectedBot?: any; selectedBot?: BotState | null;
selectedBotId: string; selectedBotId: string;
startCronJob: (jobId: string) => Promise<void>; startCronJob: (jobId: string) => Promise<void>;
stopCronJob: (jobId: string) => Promise<void>; stopCronJob: (jobId: string) => Promise<void>;
t: any; t: DashboardLabels;
lc: any; lc: ChannelLabels;
weixinLoginStatus: WeixinLoginStatus | null; weixinLoginStatus: WeixinLoginStatus | null;
} }

View File

@ -1,6 +1,7 @@
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import type { ChatMessage } from '../../../types/bot'; import type { ChatMessage } from '../../../types/bot';
import type { DashboardLabels } from '../localeTypes';
import type { DashboardChatConfirmOptions, DashboardChatNotifyOptions } from './dashboardChatShared'; import type { DashboardChatConfirmOptions, DashboardChatNotifyOptions } from './dashboardChatShared';
import { useDashboardChatComposer } from './useDashboardChatComposer'; import { useDashboardChatComposer } from './useDashboardChatComposer';
import { useDashboardChatHistory } from './useDashboardChatHistory'; import { useDashboardChatHistory } from './useDashboardChatHistory';
@ -24,7 +25,7 @@ interface UseDashboardConversationOptions {
setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void; setBotMessageFeedback: (botId: string, messageId: number, feedback: 'up' | 'down' | null) => void;
notify: (message: string, options?: DashboardChatNotifyOptions) => void; notify: (message: string, options?: DashboardChatNotifyOptions) => void;
confirm: (options: DashboardChatConfirmOptions) => Promise<boolean>; confirm: (options: DashboardChatConfirmOptions) => Promise<boolean>;
t: any; t: DashboardLabels;
isZh: boolean; isZh: boolean;
} }

View File

@ -1,5 +1,7 @@
import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'; import { useEffect, type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
type PromptTone = 'info' | 'success' | 'warning' | 'error'; type PromptTone = 'info' | 'success' | 'warning' | 'error';
interface NotifyOptions { interface NotifyOptions {
@ -131,8 +133,8 @@ export function useDashboardLifecycle({
loadInitialConfigData(selectedBotId), loadInitialConfigData(selectedBotId),
]); ]);
requestAnimationFrame(() => scrollConversationToBottom('auto')); requestAnimationFrame(() => scrollConversationToBottom('auto'));
} catch (error: any) { } catch (error: unknown) {
const detail = String(error?.response?.data?.detail || '').trim(); const detail = resolveApiErrorMessage(error, '').trim();
if (!cancelled && detail) { if (!cancelled && detail) {
notify(detail, { tone: 'error' }); notify(detail, { tone: 'error' });
} }

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { createMcpManager } from '../config-managers/mcpManager'; import { createMcpManager } from '../config-managers/mcpManager';
import type { DashboardLabels, DashboardSelectedBot } from '../localeTypes';
import type { MCPServerDraft } from '../types'; import type { MCPServerDraft } from '../types';
type PromptTone = 'info' | 'success' | 'warning' | 'error'; type PromptTone = 'info' | 'success' | 'warning' | 'error';
@ -25,9 +26,9 @@ interface UseDashboardMcpConfigOptions {
isZh: boolean; isZh: boolean;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
passwordToggleLabels: { show: string; hide: string }; passwordToggleLabels: { show: string; hide: string };
selectedBot?: any; selectedBot?: DashboardSelectedBot;
selectedBotId: string; selectedBotId: string;
t: any; t: DashboardLabels;
} }
export function useDashboardMcpConfig({ export function useDashboardMcpConfig({

View File

@ -2,7 +2,9 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { BotState } from '../../../types/bot'; import type { BotState } from '../../../types/bot';
import type { ChannelLabels, DashboardLabels } from '../localeTypes';
import type { BotResourceSnapshot, NanobotImage, WeixinLoginStatus } from '../types'; import type { BotResourceSnapshot, NanobotImage, WeixinLoginStatus } from '../types';
type PromptTone = 'info' | 'success' | 'warning' | 'error'; type PromptTone = 'info' | 'success' | 'warning' | 'error';
@ -28,8 +30,8 @@ interface UseDashboardRuntimeControlOptions {
selectedBotId: string; selectedBotId: string;
selectedBot?: BotState; selectedBot?: BotState;
isZh: boolean; isZh: boolean;
t: any; t: DashboardLabels;
lc: any; lc: ChannelLabels;
mergeBot: (bot: BotState) => void; mergeBot: (bot: BotState) => void;
setBots: (bots: BotState[]) => void; setBots: (bots: BotState[]) => void;
updateBotStatus: (botId: string, dockerStatus: string) => void; updateBotStatus: (botId: string, dockerStatus: string) => void;
@ -112,8 +114,9 @@ export function useDashboardRuntimeControl({
try { try {
const res = await axios.get<BotResourceSnapshot>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/resources`); const res = await axios.get<BotResourceSnapshot>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/resources`);
setResourceSnapshot(res.data); setResourceSnapshot(res.data);
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'); const fallback = isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.';
const msg = resolveApiErrorMessage(error, fallback);
setResourceError(String(msg)); setResourceError(String(msg));
} finally { } finally {
setResourceLoading(false); setResourceLoading(false);
@ -126,7 +129,8 @@ export function useDashboardRuntimeControl({
void loadResourceSnapshot(botId); void loadResourceSnapshot(botId);
}, [loadResourceSnapshot]); }, [loadResourceSnapshot]);
const loadWeixinLoginStatus = useCallback(async (botId: string, _silent: boolean = false) => { const loadWeixinLoginStatus = useCallback(async (botId: string, silent?: boolean) => {
void silent;
if (!botId) { if (!botId) {
setWeixinLoginStatus(null); setWeixinLoginStatus(null);
return; return;
@ -169,8 +173,8 @@ export function useDashboardRuntimeControl({
notify(lc.weixinReloginDone, { tone: 'success' }); notify(lc.weixinReloginDone, { tone: 'success' });
await loadWeixinLoginStatus(selectedBot.id); await loadWeixinLoginStatus(selectedBot.id);
await refresh(); await refresh();
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || lc.weixinReloginFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, lc.weixinReloginFail), { tone: 'error' });
} }
}, [lc.weixinReloginDone, lc.weixinReloginFail, loadWeixinLoginStatus, notify, refresh, selectedBot]); }, [lc.weixinReloginDone, lc.weixinReloginFail, loadWeixinLoginStatus, notify, refresh, selectedBot]);
@ -289,9 +293,9 @@ export function useDashboardRuntimeControl({
updateBotStatus(id, 'RUNNING'); updateBotStatus(id, 'RUNNING');
await refresh(); await refresh();
await ensureControlVisible(startedAt); await ensureControlVisible(startedAt);
} catch (error: any) { } catch (error: unknown) {
await refresh(); await refresh();
notify(error?.response?.data?.detail || t.startFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.startFail), { tone: 'error' });
await ensureControlVisible(startedAt); await ensureControlVisible(startedAt);
} finally { } finally {
setOperatingBotId(null); setOperatingBotId(null);
@ -320,9 +324,9 @@ export function useDashboardRuntimeControl({
updateBotStatus(id, 'RUNNING'); updateBotStatus(id, 'RUNNING');
await refresh(); await refresh();
await ensureControlVisible(startedAt); await ensureControlVisible(startedAt);
} catch (error: any) { } catch (error: unknown) {
await refresh(); await refresh();
notify(error?.response?.data?.detail || t.restartFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.restartFail), { tone: 'error' });
await ensureControlVisible(startedAt); await ensureControlVisible(startedAt);
} finally { } finally {
setOperatingBotId(null); setOperatingBotId(null);
@ -347,8 +351,8 @@ export function useDashboardRuntimeControl({
} }
await refresh(); await refresh();
notify(enabled ? t.enableDone : t.disableDone, { tone: 'success' }); notify(enabled ? t.enableDone : t.disableDone, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (enabled ? t.enableFail : t.disableFail), { tone: 'error' }); notify(resolveApiErrorMessage(error, enabled ? t.enableFail : t.disableFail), { tone: 'error' });
} finally { } finally {
setOperatingBotId(null); setOperatingBotId(null);
clearControlState(id); clearControlState(id);

View File

@ -1,23 +1,25 @@
import { useCallback, useEffect, useRef, useState, type ChangeEvent, type Dispatch, type SetStateAction } from 'react'; import { useCallback, useEffect, useRef, useState, type ChangeEvent, type Dispatch, type SetStateAction } from 'react';
import type { BotSkillMarketItem } from '../../platform/types';
import type { DashboardLabels, DashboardSelectedBot } from '../localeTypes';
import type { WorkspaceSkillOption } from '../types'; import type { WorkspaceSkillOption } from '../types';
import { formatBytes } from '../utils'; import { formatBytes } from '../utils';
interface UseDashboardSkillsConfigOptions { interface UseDashboardSkillsConfigOptions {
botSkills: WorkspaceSkillOption[]; botSkills: WorkspaceSkillOption[];
closeRuntimeMenu: () => void; closeRuntimeMenu: () => void;
installMarketSkill: (skill: any) => Promise<void>; installMarketSkill: (skill: BotSkillMarketItem) => Promise<void>;
isMarketSkillsLoading: boolean; isMarketSkillsLoading: boolean;
isSkillUploading: boolean; isSkillUploading: boolean;
isZh: boolean; isZh: boolean;
labels: Record<string, any>; labels: DashboardLabels;
loadBotSkills: (botId: string) => Promise<void>; loadBotSkills: (botId: string) => Promise<void>;
loadMarketSkills: (botId: string) => Promise<void>; loadMarketSkills: (botId: string) => Promise<void>;
marketSkillInstallingId: number | null; marketSkillInstallingId: number | null;
marketSkills: any[]; marketSkills: BotSkillMarketItem[];
onPickSkillZip: (event: ChangeEvent<HTMLInputElement>) => Promise<void>; onPickSkillZip: (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
removeBotSkill: (skill: WorkspaceSkillOption) => Promise<void>; removeBotSkill: (skill: WorkspaceSkillOption) => Promise<void>;
selectedBot?: any; selectedBot?: DashboardSelectedBot;
} }
export function useDashboardSkillsConfig({ export function useDashboardSkillsConfig({
@ -115,7 +117,7 @@ export function useDashboardSkillsConfig({
if (!selectedBot) return; if (!selectedBot) return;
await loadMarketSkills(selectedBot.id); await loadMarketSkills(selectedBot.id);
}, },
onInstall: async (skill: any) => { onInstall: async (skill: BotSkillMarketItem) => {
await installMarketSkill(skill); await installMarketSkill(skill);
if (selectedBot) { if (selectedBot) {
await loadBotSkills(selectedBot.id); await loadBotSkills(selectedBot.id);

View File

@ -2,7 +2,9 @@ import { useCallback, useMemo, useState, type ChangeEvent } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { BotSkillMarketItem } from '../../platform/types'; import type { BotSkillMarketItem } from '../../platform/types';
import type { DashboardLabels } from '../localeTypes';
import type { import type {
BotEnvParams, BotEnvParams,
CronJob, CronJob,
@ -34,7 +36,7 @@ interface ConfirmOptions {
interface UseDashboardSupportDataOptions { interface UseDashboardSupportDataOptions {
selectedBotId: string; selectedBotId: string;
isZh: boolean; isZh: boolean;
t: any; t: DashboardLabels;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
confirm: (options: ConfirmOptions) => Promise<boolean>; confirm: (options: ConfirmOptions) => Promise<boolean>;
onCloseEnvParamsModal?: () => void; onCloseEnvParamsModal?: () => void;
@ -138,12 +140,12 @@ export function useDashboardSupportData({
return merged; return merged;
}); });
if (!append) setTopicFeedError(''); if (!append) setTopicFeedError('');
} catch (error: any) { } catch (error: unknown) {
if (!append) { if (!append) {
setTopicFeedItems([]); setTopicFeedItems([]);
setTopicFeedNextCursor(null); setTopicFeedNextCursor(null);
} }
setTopicFeedError(error?.response?.data?.detail || (isZh ? '读取主题消息失败。' : 'Failed to load topic feed.')); setTopicFeedError(resolveApiErrorMessage(error, isZh ? '读取主题消息失败。' : 'Failed to load topic feed.'));
} finally { } finally {
if (append) { if (append) {
setTopicFeedLoadingMore(false); setTopicFeedLoadingMore(false);
@ -202,12 +204,12 @@ export function useDashboardSupportData({
try { try {
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/topic-items/${targetId}`); await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/topic-items/${targetId}`);
setTopicFeedItems((prev) => prev.filter((row) => Number(row.id) !== targetId)); setTopicFeedItems((prev) => prev.filter((row) => Number(row.id) !== targetId));
if (!Boolean(item?.is_read)) { if (!item?.is_read) {
setTopicFeedUnreadCount((prev) => Math.max(0, prev - 1)); setTopicFeedUnreadCount((prev) => Math.max(0, prev - 1));
} }
notify(isZh ? '主题消息已删除。' : 'Topic item deleted.', { tone: 'success' }); notify(isZh ? '主题消息已删除。' : 'Topic item deleted.', { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '删除主题消息失败。' : 'Failed to delete topic item.'), { notify(resolveApiErrorMessage(error, isZh ? '删除主题消息失败。' : 'Failed to delete topic item.'), {
tone: 'error', tone: 'error',
}); });
} finally { } finally {
@ -231,9 +233,9 @@ export function useDashboardSupportData({
try { try {
const res = await axios.get<BotSkillMarketItem[]>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/skill-market`); const res = await axios.get<BotSkillMarketItem[]>(`${APP_ENDPOINTS.apiBase}/bots/${botId}/skill-market`);
setMarketSkills(Array.isArray(res.data) ? res.data : []); setMarketSkills(Array.isArray(res.data) ? res.data : []);
} catch (error: any) { } catch (error: unknown) {
setMarketSkills([]); setMarketSkills([]);
notify(error?.response?.data?.detail || t.toolsLoadFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.toolsLoadFail), { tone: 'error' });
} finally { } finally {
setIsMarketSkillsLoading(false); setIsMarketSkillsLoading(false);
} }
@ -268,8 +270,8 @@ export function useDashboardSupportData({
} }
notify(t.envParamsSaved, { tone: 'success' }); notify(t.envParamsSaved, { tone: 'success' });
return true; return true;
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.envParamsSaveFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.envParamsSaveFail), { tone: 'error' });
return false; return false;
} }
}, [notify, onCloseEnvParamsModal, selectedBotId, t.envParamsSaveFail, t.envParamsSaved]); }, [notify, onCloseEnvParamsModal, selectedBotId, t.envParamsSaveFail, t.envParamsSaved]);
@ -320,10 +322,10 @@ export function useDashboardSupportData({
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/skills/${encodeURIComponent(skill.id)}`); await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/skills/${encodeURIComponent(skill.id)}`);
await loadBotSkills(selectedBotId); await loadBotSkills(selectedBotId);
await loadMarketSkills(selectedBotId); await loadMarketSkills(selectedBotId);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.toolsRemoveFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.toolsRemoveFail), { tone: 'error' });
} }
}, [confirm, loadBotSkills, loadMarketSkills, notify, selectedBotId, t.removeSkill, t.toolsRemoveConfirm, t.toolsRemoveFail]); }, [confirm, loadBotSkills, loadMarketSkills, notify, selectedBotId, t]);
const installMarketSkill = useCallback(async (marketSkill: BotSkillMarketItem) => { const installMarketSkill = useCallback(async (marketSkill: BotSkillMarketItem) => {
if (!selectedBotId) return; if (!selectedBotId) return;
@ -342,8 +344,8 @@ export function useDashboardSupportData({
: `Installed: ${(res.data?.installed || []).join(', ') || marketSkill.display_name || marketSkill.skill_key}`, : `Installed: ${(res.data?.installed || []).join(', ') || marketSkill.display_name || marketSkill.skill_key}`,
{ tone: 'success' }, { tone: 'success' },
); );
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.toolsAddFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.toolsAddFail), { tone: 'error' });
await loadMarketSkills(selectedBotId); await loadMarketSkills(selectedBotId);
} finally { } finally {
setMarketSkillInstallingId(null); setMarketSkillInstallingId(null);
@ -370,8 +372,8 @@ export function useDashboardSupportData({
const nextSkills = Array.isArray(res.data?.skills) ? res.data.skills : []; const nextSkills = Array.isArray(res.data?.skills) ? res.data.skills : [];
setBotSkills(nextSkills); setBotSkills(nextSkills);
await loadMarketSkills(selectedBotId); await loadMarketSkills(selectedBotId);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.toolsAddFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.toolsAddFail), { tone: 'error' });
} finally { } finally {
setIsSkillUploading(false); setIsSkillUploading(false);
event.target.value = ''; event.target.value = '';
@ -400,8 +402,8 @@ export function useDashboardSupportData({
try { try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/start`); await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/start`);
await loadCronJobs(selectedBotId); await loadCronJobs(selectedBotId);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.cronStartFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.cronStartFail), { tone: 'error' });
} finally { } finally {
setCronActionJobId(''); setCronActionJobId('');
setCronActionType(''); setCronActionType('');
@ -415,8 +417,8 @@ export function useDashboardSupportData({
try { try {
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/stop`); await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}/stop`);
await loadCronJobs(selectedBotId); await loadCronJobs(selectedBotId);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.cronStopFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.cronStopFail), { tone: 'error' });
} finally { } finally {
setCronActionJobId(''); setCronActionJobId('');
setCronActionType(''); setCronActionType('');
@ -436,13 +438,13 @@ export function useDashboardSupportData({
try { try {
await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}`); await axios.delete(`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/cron/jobs/${jobId}`);
await loadCronJobs(selectedBotId); await loadCronJobs(selectedBotId);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.cronDeleteFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.cronDeleteFail), { tone: 'error' });
} finally { } finally {
setCronActionJobId(''); setCronActionJobId('');
setCronActionType(''); setCronActionType('');
} }
}, [confirm, loadCronJobs, notify, selectedBotId, t.cronDelete, t.cronDeleteConfirm, t.cronDeleteFail]); }, [confirm, loadCronJobs, notify, selectedBotId, t]);
return { return {
botSkills, botSkills,

View File

@ -1,5 +1,6 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import type { DashboardLabels } from '../localeTypes';
import { fetchDashboardSystemTemplates, updateDashboardSystemTemplates } from '../api/system'; import { fetchDashboardSystemTemplates, updateDashboardSystemTemplates } from '../api/system';
import { parseTopicPresets } from '../topic/topicPresetUtils'; import { parseTopicPresets } from '../topic/topicPresetUtils';
import type { TopicPresetTemplate } from '../types'; import type { TopicPresetTemplate } from '../types';
@ -16,7 +17,7 @@ interface UseDashboardTemplateManagerOptions {
currentTopicPresetCount: number; currentTopicPresetCount: number;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
setTopicPresetTemplates: (value: TopicPresetTemplate[]) => void; setTopicPresetTemplates: (value: TopicPresetTemplate[]) => void;
t: any; t: DashboardLabels;
} }
function countAgentTemplateFields(raw: string) { function countAgentTemplateFields(raw: string) {
@ -109,8 +110,9 @@ export function useDashboardTemplateManager({
topic_presets: parsedTopic, topic_presets: parsedTopic,
}; };
} }
} catch (error: any) { } catch (error: unknown) {
notify(error?.message || t.templateParseFail, { tone: 'error' }); const message = error instanceof Error ? error.message : t.templateParseFail;
notify(message || t.templateParseFail, { tone: 'error' });
return; return;
} }

View File

@ -1,6 +1,7 @@
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { createTopicManager } from '../config-managers/topicManager'; import { createTopicManager } from '../config-managers/topicManager';
import type { DashboardLabels, DashboardSelectedBot } from '../localeTypes';
import { resolvePresetText } from '../topic/topicPresetUtils'; import { resolvePresetText } from '../topic/topicPresetUtils';
import type { BotTopic, TopicPresetTemplate } from '../types'; import type { BotTopic, TopicPresetTemplate } from '../types';
@ -26,9 +27,9 @@ interface UseDashboardTopicConfigOptions {
effectiveTopicPresetTemplates: TopicPresetTemplate[]; effectiveTopicPresetTemplates: TopicPresetTemplate[];
isZh: boolean; isZh: boolean;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
selectedBot?: any; selectedBot?: DashboardSelectedBot;
selectedBotId: string; selectedBotId: string;
t: any; t: DashboardLabels;
} }
export function useDashboardTopicConfig({ export function useDashboardTopicConfig({

View File

@ -2,7 +2,9 @@ import { useEffect, useRef, useState, type Dispatch, type RefObject, type SetSta
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import { normalizeUserMessageText } from '../../../shared/text/messageText'; import { normalizeUserMessageText } from '../../../shared/text/messageText';
import type { DashboardLabels } from '../localeTypes';
type PromptTone = 'info' | 'success' | 'warning' | 'error'; type PromptTone = 'info' | 'success' | 'warning' | 'error';
@ -20,7 +22,7 @@ interface UseDashboardVoiceInputOptions {
setCommand: Dispatch<SetStateAction<string>>; setCommand: Dispatch<SetStateAction<string>>;
composerTextareaRef: RefObject<HTMLTextAreaElement | null>; composerTextareaRef: RefObject<HTMLTextAreaElement | null>;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
t: any; t: DashboardLabels;
} }
export function useDashboardVoiceInput({ export function useDashboardVoiceInput({
@ -89,13 +91,11 @@ export function useDashboardVoiceInput({
}); });
window.requestAnimationFrame(() => composerTextareaRef.current?.focus()); window.requestAnimationFrame(() => composerTextareaRef.current?.focus());
notify(t.voiceTranscribeDone, { tone: 'success' }); notify(t.voiceTranscribeDone, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
const msg = String(error?.response?.data?.detail || '').trim(); const msg = resolveApiErrorMessage(error, '').trim();
console.error('Speech transcription failed', { console.error('Speech transcription failed', {
botId, botId,
message: msg || t.voiceTranscribeFail, message: msg || t.voiceTranscribeFail,
status: error?.response?.status,
response: error?.response?.data,
error, error,
}); });
notify(msg || t.voiceTranscribeFail, { tone: 'error' }); notify(msg || t.voiceTranscribeFail, { tone: 'error' });

View File

@ -0,0 +1,17 @@
import { channelsEn } from '../../i18n/channels.en';
import { dashboardEn } from '../../i18n/dashboard.en';
import type { BotState } from '../../types/bot';
type WidenLocaleValue<T> =
T extends string ? string
: T extends number ? number
: T extends boolean ? boolean
: T extends (...args: infer Args) => infer Result ? (...args: Args) => Result
: T extends readonly (infer Item)[] ? WidenLocaleValue<Item>[]
: T extends object ? { [K in keyof T]: WidenLocaleValue<T[K]> }
: T;
export type DashboardLabels = WidenLocaleValue<typeof dashboardEn>;
export type ChannelLabels = WidenLocaleValue<typeof channelsEn>;
export type DashboardChannelLabels = DashboardLabels & ChannelLabels;
export type DashboardSelectedBot = BotState | null | undefined;

View File

@ -274,7 +274,7 @@ export function TopicFeedPanel({
const itemId = Number(item.id || 0); const itemId = Number(item.id || 0);
const level = String(item.level || 'info').trim().toLowerCase(); const level = String(item.level || 'info').trim().toLowerCase();
const levelText = level === 'warn' ? 'WARN' : level === 'error' ? 'ERROR' : level === 'success' ? 'SUCCESS' : 'INFO'; const levelText = level === 'warn' ? 'WARN' : level === 'error' ? 'ERROR' : level === 'success' ? 'SUCCESS' : 'INFO';
const unread = !Boolean(item.is_read); const unread = !item.is_read;
const card = deriveTopicSummaryCard(item); const card = deriveTopicSummaryCard(item);
const rawContent = String(item.content || '').trim(); const rawContent = String(item.content || '').trim();
return ( return (

View File

@ -22,8 +22,17 @@ export type StagedSubmissionDraft = {
}; };
export type BotEnvParams = Record<string, string>; export type BotEnvParams = Record<string, string>;
export interface BotMessageResponseRow {
id?: number | string | null;
role?: string | null;
feedback?: string | null;
text?: string | null;
media?: unknown;
ts?: number | string | null;
}
export interface BotMessagesByDateResponse { export interface BotMessagesByDateResponse {
items?: any[]; items?: BotMessageResponseRow[];
anchor_id?: number | null; anchor_id?: number | null;
resolved_ts?: number | null; resolved_ts?: number | null;
matched_exact_date?: boolean; matched_exact_date?: boolean;

View File

@ -2,11 +2,12 @@ import { useEffect, useMemo, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { RefreshCw, Trash2 } from 'lucide-react'; import { RefreshCw, Trash2 } from 'lucide-react';
import { APP_ENDPOINTS } from '../../config/env'; import { APP_ENDPOINTS } from '../../config/env';
import { resolveApiErrorMessage } from '../../shared/http/apiErrors';
import { useAppStore } from '../../store/appStore'; import { useAppStore } from '../../store/appStore';
import { pickLocale } from '../../i18n'; import { pickLocale } from '../../i18n';
import { imageFactoryZhCn } from '../../i18n/image-factory.zh-cn';
import { imageFactoryEn } from '../../i18n/image-factory.en'; import { imageFactoryEn } from '../../i18n/image-factory.en';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; import { imageFactoryZhCn } from '../../i18n/image-factory.zh-cn';
import { useLucentPrompt } from '../../components/lucent/useLucentPrompt';
import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../components/lucent/LucentIconButton';
interface NanobotImage { interface NanobotImage {
@ -100,8 +101,8 @@ export function ImageFactoryModule() {
source_dir: 'manual', source_dir: 'manual',
}); });
await fetchData(); await fetchData();
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || t.registerFail; const msg = resolveApiErrorMessage(error, t.registerFail);
notify(msg, { tone: 'error' }); notify(msg, { tone: 'error' });
} finally { } finally {
setIsRegisteringTag(''); setIsRegisteringTag('');
@ -120,8 +121,8 @@ export function ImageFactoryModule() {
try { try {
await axios.delete(`${APP_ENDPOINTS.apiBase}/images/${encodeURIComponent(tag)}`); await axios.delete(`${APP_ENDPOINTS.apiBase}/images/${encodeURIComponent(tag)}`);
await fetchData(); await fetchData();
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || t.deleteFail; const msg = resolveApiErrorMessage(error, t.deleteFail);
notify(msg, { tone: 'error' }); notify(msg, { tone: 'error' });
} finally { } finally {
setIsDeletingTag(''); setIsDeletingTag('');

View File

@ -5,7 +5,7 @@ import { channelsZhCn } from '../../i18n/channels.zh-cn';
import { wizardEn } from '../../i18n/wizard.en'; import { wizardEn } from '../../i18n/wizard.en';
import { wizardZhCn } from '../../i18n/wizard.zh-cn'; import { wizardZhCn } from '../../i18n/wizard.zh-cn';
import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../components/lucent/LucentIconButton';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../components/lucent/useLucentPrompt';
import { useAppStore } from '../../store/appStore'; import { useAppStore } from '../../store/appStore';
import { BotWizardAgentStep } from './components/BotWizardAgentStep'; import { BotWizardAgentStep } from './components/BotWizardAgentStep';
import { BotWizardBaseStep } from './components/BotWizardBaseStep'; import { BotWizardBaseStep } from './components/BotWizardBaseStep';

View File

@ -5,12 +5,13 @@ import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { LucentSelect } from '../../../components/lucent/LucentSelect'; import { LucentSelect } from '../../../components/lucent/LucentSelect';
import { PasswordInput } from '../../../components/PasswordInput'; import { PasswordInput } from '../../../components/PasswordInput';
import { buildLlmProviderOptions } from '../../../utils/llmProviders'; import { buildLlmProviderOptions } from '../../../utils/llmProviders';
import type { OnboardingChannelLabels, WizardLabels } from '../localeTypes';
import type { BotWizardForm } from '../types'; import type { BotWizardForm } from '../types';
interface BotWizardBaseStepProps { interface BotWizardBaseStepProps {
isZh: boolean; isZh: boolean;
ui: any; ui: WizardLabels;
lc: any; lc: OnboardingChannelLabels;
passwordToggleLabels: { show: string; hide: string }; passwordToggleLabels: { show: string; hide: string };
form: BotWizardForm; form: BotWizardForm;
setForm: Dispatch<SetStateAction<BotWizardForm>>; setForm: Dispatch<SetStateAction<BotWizardForm>>;

View File

@ -3,12 +3,13 @@ import { Plus, Trash2 } from 'lucide-react';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { LucentSelect } from '../../../components/lucent/LucentSelect'; import { LucentSelect } from '../../../components/lucent/LucentSelect';
import { PasswordInput } from '../../../components/PasswordInput'; import { PasswordInput } from '../../../components/PasswordInput';
import type { OnboardingChannelLabels } from '../localeTypes';
import type { ChannelType, WizardChannelConfig } from '../types'; import type { ChannelType, WizardChannelConfig } from '../types';
import { EMPTY_CHANNEL_PICKER } from '../types'; import { EMPTY_CHANNEL_PICKER } from '../types';
interface BotWizardChannelModalProps { interface BotWizardChannelModalProps {
open: boolean; open: boolean;
lc: any; lc: OnboardingChannelLabels;
passwordToggleLabels: { show: string; hide: string }; passwordToggleLabels: { show: string; hide: string };
channels: WizardChannelConfig[]; channels: WizardChannelConfig[];
sendProgress: boolean; sendProgress: boolean;
@ -32,7 +33,7 @@ function renderChannelFields({
}: { }: {
channel: WizardChannelConfig; channel: WizardChannelConfig;
idx: number; idx: number;
lc: any; lc: OnboardingChannelLabels;
passwordToggleLabels: { show: string; hide: string }; passwordToggleLabels: { show: string; hide: string };
onUpdateChannel: (index: number, patch: Partial<WizardChannelConfig>) => void; onUpdateChannel: (index: number, patch: Partial<WizardChannelConfig>) => void;
}) { }) {

View File

@ -1,9 +1,10 @@
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import type { WizardLabels } from '../localeTypes';
import type { BotWizardForm, NanobotImage } from '../types'; import type { BotWizardForm, NanobotImage } from '../types';
interface BotWizardImageStepProps { interface BotWizardImageStepProps {
ui: any; ui: WizardLabels;
readyImages: NanobotImage[]; readyImages: NanobotImage[];
isLoadingImages: boolean; isLoadingImages: boolean;
form: BotWizardForm; form: BotWizardForm;

View File

@ -1,8 +1,9 @@
import type { WizardLabels } from '../localeTypes';
import type { BotWizardForm } from '../types'; import type { BotWizardForm } from '../types';
interface BotWizardReviewStepProps { interface BotWizardReviewStepProps {
isZh: boolean; isZh: boolean;
ui: any; ui: WizardLabels;
autoStart: boolean; autoStart: boolean;
setAutoStart: (value: boolean) => void; setAutoStart: (value: boolean) => void;
form: BotWizardForm; form: BotWizardForm;

View File

@ -2,10 +2,11 @@ import { Plus, Trash2 } from 'lucide-react';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { PasswordInput } from '../../../components/PasswordInput'; import { PasswordInput } from '../../../components/PasswordInput';
import type { WizardLabels } from '../localeTypes';
interface BotWizardToolsModalProps { interface BotWizardToolsModalProps {
open: boolean; open: boolean;
ui: any; ui: WizardLabels;
closeLabel: string; closeLabel: string;
envEntries: Array<[string, string]>; envEntries: Array<[string, string]>;
envDraftKey: string; envDraftKey: string;

View File

@ -2,8 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders'; import { getLlmProviderDefaultApiBase } from '../../../utils/llmProviders';
import { getSystemTimezoneOptions } from '../../../utils/systemTimezones'; import { getSystemTimezoneOptions } from '../../../utils/systemTimezones';
import type { WizardLabels } from '../localeTypes';
import type { import type {
AgentTab, AgentTab,
BotWizardForm, BotWizardForm,
@ -26,7 +28,7 @@ interface NotifyOptions {
} }
interface UseBotWizardOptions { interface UseBotWizardOptions {
ui: any; ui: WizardLabels;
notify: (message: string, options?: NotifyOptions) => void; notify: (message: string, options?: NotifyOptions) => void;
onCreated?: () => void; onCreated?: () => void;
onGoDashboard?: () => void; onGoDashboard?: () => void;
@ -172,8 +174,8 @@ export function useBotWizard({
await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(raw)}`); await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(raw)}`);
setBotIdStatus('exists'); setBotIdStatus('exists');
setBotIdStatusText(ui.botIdExists); setBotIdStatusText(ui.botIdExists);
} catch (error: any) { } catch (error: unknown) {
if (error?.response?.status === 404) { if (axios.isAxiosError(error) && error.response?.status === 404) {
setBotIdStatus('available'); setBotIdStatus('available');
setBotIdStatusText(ui.botIdAvailable); setBotIdStatusText(ui.botIdAvailable);
return; return;
@ -292,8 +294,8 @@ export function useBotWizard({
} else { } else {
setTestResult(ui.connFailed(res.data?.detail || 'unknown error')); setTestResult(ui.connFailed(res.data?.detail || 'unknown error'));
} }
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || error?.message || 'request failed'; const msg = resolveApiErrorMessage(error, 'request failed');
setTestResult(ui.connFailed(msg)); setTestResult(ui.connFailed(msg));
} finally { } finally {
setIsTestingProvider(false); setIsTestingProvider(false);
@ -355,12 +357,12 @@ export function useBotWizard({
setBotIdStatus('idle'); setBotIdStatus('idle');
setBotIdStatusText(''); setBotIdStatusText('');
notify(ui.created, { tone: 'success' }); notify(ui.created, { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
if (axios.isCancel(error) || error?.code === 'ERR_CANCELED') { if (axios.isAxiosError(error) && (axios.isCancel(error) || error.code === 'ERR_CANCELED')) {
notify(ui.createCanceled, { tone: 'info' }); notify(ui.createCanceled, { tone: 'info' });
return; return;
} }
const msg = error?.response?.data?.detail || ui.createFailed; const msg = resolveApiErrorMessage(error, ui.createFailed);
notify(msg, { tone: 'error' }); notify(msg, { tone: 'error' });
} finally { } finally {
if (createAbortControllerRef.current === controller) { if (createAbortControllerRef.current === controller) {

View File

@ -0,0 +1,14 @@
import { channelsEn } from '../../i18n/channels.en';
import { wizardEn } from '../../i18n/wizard.en';
type WidenLocaleValue<T> =
T extends string ? string
: T extends number ? number
: T extends boolean ? boolean
: T extends (...args: infer Args) => infer Result ? (...args: Args) => Result
: T extends readonly (infer Item)[] ? WidenLocaleValue<Item>[]
: T extends object ? { [K in keyof T]: WidenLocaleValue<T[K]> }
: T;
export type WizardLabels = WidenLocaleValue<typeof wizardEn>;
export type OnboardingChannelLabels = WidenLocaleValue<typeof channelsEn>;

View File

@ -1,11 +1,12 @@
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ChevronLeft, ChevronRight, RefreshCw, ShieldCheck } from 'lucide-react'; import { ChevronLeft, ChevronRight, RefreshCw, ShieldCheck } from 'lucide-react';
import '../../components/skill-market/SkillMarketShared.css'; import '../../components/skill-market/SkillMarketShared.css';
import { ProtectedSearchInput } from '../../components/ProtectedSearchInput'; import { ProtectedSearchInput } from '../../components/ProtectedSearchInput';
import { LucentIconButton } from '../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../components/lucent/LucentIconButton';
import { LucentSelect } from '../../components/lucent/LucentSelect'; import { LucentSelect } from '../../components/lucent/LucentSelect';
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../components/lucent/useLucentPrompt';
import { resolveApiErrorMessage } from '../../shared/http/apiErrors';
import { fetchPlatformLoginLogs, fetchPreferredPlatformPageSize } from './api/settings'; import { fetchPlatformLoginLogs, fetchPreferredPlatformPageSize } from './api/settings';
import type { PlatformLoginLogItem } from './types'; import type { PlatformLoginLogItem } from './types';
import './PlatformDashboardPage.css'; import './PlatformDashboardPage.css';
@ -65,7 +66,7 @@ export function PlatformLoginLogPage({ isZh }: PlatformLoginLogPageProps) {
const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
const loadRows = async () => { const loadRows = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const data = await fetchPlatformLoginLogs({ const data = await fetchPlatformLoginLogs({
@ -77,14 +78,14 @@ export function PlatformLoginLogPage({ isZh }: PlatformLoginLogPageProps) {
}); });
setItems(Array.isArray(data?.items) ? data.items : []); setItems(Array.isArray(data?.items) ? data.items : []);
setTotal(Number(data?.total || 0)); setTotal(Number(data?.total || 0));
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '读取登录日志失败。' : 'Failed to load login logs.'), { notify(resolveApiErrorMessage(error, isZh ? '读取登录日志失败。' : 'Failed to load login logs.'), {
tone: 'error', tone: 'error',
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [authType, isZh, notify, page, pageSize, search, status]);
useEffect(() => { useEffect(() => {
void loadRows(); void loadRows();

View File

@ -3,7 +3,7 @@ import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import type { SkillMarketItem } from '../types'; import type { SkillMarketItem } from '../types';
export interface SkillMarketListResponse extends Array<SkillMarketItem> {} export type SkillMarketListResponse = SkillMarketItem[];
export function fetchPlatformSkillMarket() { export function fetchPlatformSkillMarket() {
return axios.get<SkillMarketListResponse>(`${APP_ENDPOINTS.apiBase}/platform/skills`).then((res) => { return axios.get<SkillMarketListResponse>(`${APP_ENDPOINTS.apiBase}/platform/skills`).then((res) => {

View File

@ -3,7 +3,7 @@ import { ChevronLeft, ChevronRight, RefreshCw, Terminal } from 'lucide-react';
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt';
import { dashboardEn } from '../../../i18n/dashboard.en'; import { dashboardEn } from '../../../i18n/dashboard.en';
import { dashboardZhCn } from '../../../i18n/dashboard.zh-cn'; import { dashboardZhCn } from '../../../i18n/dashboard.zh-cn';
import { pickLocale } from '../../../i18n'; import { pickLocale } from '../../../i18n';

View File

@ -1,11 +1,12 @@
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ChevronLeft, ChevronRight, Pencil, Plus, RefreshCw, Settings2, Trash2 } from 'lucide-react'; import { ChevronLeft, ChevronRight, Pencil, Plus, RefreshCw, Settings2, Trash2 } from 'lucide-react';
import '../../../components/skill-market/SkillMarketShared.css'; import '../../../components/skill-market/SkillMarketShared.css';
import { DrawerShell } from '../../../components/DrawerShell'; import { DrawerShell } from '../../../components/DrawerShell';
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { PlatformSettings, SystemSettingItem } from '../types'; import type { PlatformSettings, SystemSettingItem } from '../types';
import { import {
createPlatformSystemSetting, createPlatformSystemSetting,
@ -88,19 +89,19 @@ function PlatformSettingsView({
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const loadRows = async () => { const loadRows = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const data = await fetchPlatformSystemSettings(); const data = await fetchPlatformSystemSettings();
setItems(Array.isArray(data?.items) ? data.items : []); setItems(Array.isArray(data?.items) ? data.items : []);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '读取系统参数失败。' : 'Failed to load system settings.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '读取系统参数失败。' : 'Failed to load system settings.'), { tone: 'error' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [isZh, notify]);
const refreshSnapshot = async () => { const refreshSnapshot = useCallback(async () => {
try { try {
const data = await fetchPlatformSettings(); const data = await fetchPlatformSettings();
onSaved(data); onSaved(data);
@ -108,14 +109,14 @@ function PlatformSettingsView({
// Ignore snapshot refresh failures here; the table is still the source of truth in the view. // Ignore snapshot refresh failures here; the table is still the source of truth in the view.
} }
setPageSize(await fetchPreferredPlatformPageSize(10)); setPageSize(await fetchPreferredPlatformPageSize(10));
}; }, [onSaved]);
useEffect(() => { useEffect(() => {
setPage(1); setPage(1);
void (async () => { void (async () => {
await Promise.allSettled([loadRows(), refreshSnapshot()]); await Promise.allSettled([loadRows(), refreshSnapshot()]);
})(); })();
}, []); }, [loadRows, refreshSnapshot]);
useEffect(() => { useEffect(() => {
setPage(1); setPage(1);
@ -159,10 +160,10 @@ function PlatformSettingsView({
await refreshSnapshot(); await refreshSnapshot();
notify(isZh ? '系统参数已保存。' : 'System setting saved.', { tone: 'success' }); notify(isZh ? '系统参数已保存。' : 'System setting saved.', { tone: 'success' });
setShowEditor(false); setShowEditor(false);
} catch (error: any) { } catch (error: unknown) {
const detail = error instanceof SyntaxError const detail = error instanceof SyntaxError
? (isZh ? 'JSON 参数格式错误。' : 'Invalid JSON value.') ? (isZh ? 'JSON 参数格式错误。' : 'Invalid JSON value.')
: (error?.response?.data?.detail || (isZh ? '保存参数失败。' : 'Failed to save setting.')); : resolveApiErrorMessage(error, isZh ? '保存参数失败。' : 'Failed to save setting.');
notify(detail, { tone: 'error' }); notify(detail, { tone: 'error' });
} finally { } finally {
setSaving(false); setSaving(false);
@ -268,8 +269,8 @@ function PlatformSettingsView({
await deletePlatformSystemSetting(item.key); await deletePlatformSystemSetting(item.key);
await loadRows(); await loadRows();
await refreshSnapshot(); await refreshSnapshot();
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '删除失败。' : 'Delete failed.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '删除失败。' : 'Delete failed.'), { tone: 'error' });
} }
})(); })();
}} }}

View File

@ -1,11 +1,12 @@
import { useEffect, useMemo, useState, type ChangeEvent } from 'react'; import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react';
import { ChevronLeft, ChevronRight, FileArchive, Hammer, Pencil, Plus, RefreshCw, Trash2, Upload } from 'lucide-react'; import { ChevronLeft, ChevronRight, FileArchive, Hammer, Pencil, Plus, RefreshCw, Trash2, Upload } from 'lucide-react';
import '../../../components/skill-market/SkillMarketShared.css'; import '../../../components/skill-market/SkillMarketShared.css';
import { DrawerShell } from '../../../components/DrawerShell'; import { DrawerShell } from '../../../components/DrawerShell';
import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput'; import { ProtectedSearchInput } from '../../../components/ProtectedSearchInput';
import type { SkillMarketItem } from '../types'; import type { SkillMarketItem } from '../types';
import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import { fetchPreferredPlatformPageSize } from '../api/settings'; import { fetchPreferredPlatformPageSize } from '../api/settings';
import { import {
createPlatformSkillMarketItem, createPlatformSkillMarketItem,
@ -74,21 +75,21 @@ function SkillMarketManagerView({
? '技能市场仅接收人工上传的 ZIP 技能包。平台将统一保存技能元数据与归档文件,并为 Bot 安装提供标准化来源,不再自动扫描 /data/skills 目录。' ? '技能市场仅接收人工上传的 ZIP 技能包。平台将统一保存技能元数据与归档文件,并为 Bot 安装提供标准化来源,不再自动扫描 /data/skills 目录。'
: 'The marketplace accepts manually uploaded ZIP skill packages only. The platform stores the package metadata and archives as the standard source for bot installation, without auto-scanning /data/skills.'; : 'The marketplace accepts manually uploaded ZIP skill packages only. The platform stores the package metadata and archives as the standard source for bot installation, without auto-scanning /data/skills.';
const loadRows = async () => { const loadRows = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const rows = await fetchPlatformSkillMarket(); const rows = await fetchPlatformSkillMarket();
setItems(rows); setItems(rows);
return rows; return rows;
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '读取技能市场失败。' : 'Failed to load the skill marketplace.'), { notify(resolveApiErrorMessage(error, isZh ? '读取技能市场失败。' : 'Failed to load the skill marketplace.'), {
tone: 'error', tone: 'error',
}); });
return [] as SkillMarketItem[]; return [] as SkillMarketItem[];
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [isZh, notify]);
useEffect(() => { useEffect(() => {
void loadRows(); void loadRows();
@ -96,7 +97,7 @@ function SkillMarketManagerView({
void (async () => { void (async () => {
setPageSize(await fetchPreferredPlatformPageSize(10)); setPageSize(await fetchPreferredPlatformPageSize(10));
})(); })();
}, []); }, [loadRows]);
useEffect(() => { useEffect(() => {
setPage(1); setPage(1);
@ -167,8 +168,8 @@ function SkillMarketManagerView({
setPage(Math.floor(nextIndex / pageSize) + 1); setPage(Math.floor(nextIndex / pageSize) + 1);
} }
} }
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '保存技能失败。' : 'Failed to save skill.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '保存技能失败。' : 'Failed to save skill.'), { tone: 'error' });
} finally { } finally {
setSaving(false); setSaving(false);
} }
@ -190,8 +191,8 @@ function SkillMarketManagerView({
} }
await loadRows(); await loadRows();
notify(isZh ? '技能已删除。' : 'Skill deleted.', { tone: 'success' }); notify(isZh ? '技能已删除。' : 'Skill deleted.', { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '删除技能失败。' : 'Failed to delete skill.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '删除技能失败。' : 'Failed to delete skill.'), { tone: 'error' });
} }
}; };

View File

@ -1,9 +1,10 @@
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { FileText, RefreshCw } from 'lucide-react'; import { FileText, RefreshCw } from 'lucide-react';
import '../../../components/skill-market/SkillMarketShared.css'; import '../../../components/skill-market/SkillMarketShared.css';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt';
import { MarkdownLiteEditor } from '../../../components/markdown/MarkdownLiteEditor'; import { MarkdownLiteEditor } from '../../../components/markdown/MarkdownLiteEditor';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import { fetchPlatformSystemTemplates, updatePlatformSystemTemplates } from '../api/templates'; import { fetchPlatformSystemTemplates, updatePlatformSystemTemplates } from '../api/templates';
interface TemplateManagerPageProps { interface TemplateManagerPageProps {
@ -71,33 +72,45 @@ function TemplateManagerView({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const applyTemplates = useCallback((res: Awaited<ReturnType<typeof fetchPlatformSystemTemplates>>) => {
setTemplates({
soul_md: String(res?.agent_md_templates?.soul_md || ''),
agents_md: String(res?.agent_md_templates?.agents_md || ''),
user_md: String(res?.agent_md_templates?.user_md || ''),
tools_md: String(res?.agent_md_templates?.tools_md || ''),
identity_md: String(res?.agent_md_templates?.identity_md || ''),
});
setTopicPresetsText(JSON.stringify(res?.topic_presets || { presets: [] }, null, 2));
}, []);
const reloadTemplates = useCallback(async () => {
setLoading(true);
try {
const res = await fetchPlatformSystemTemplates();
applyTemplates(res);
return true;
} catch (error: unknown) {
notify(resolveApiErrorMessage(error, isZh ? '读取模板失败。' : 'Failed to load templates.'), { tone: 'error' });
return false;
} finally {
setLoading(false);
}
}, [applyTemplates, isZh, notify]);
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;
const load = async () => { const load = async () => {
setLoading(true); const ok = await reloadTemplates();
try { if (!alive || !ok) return;
const res = await fetchPlatformSystemTemplates(); if (alive) {
if (!alive) return;
setTemplates({
soul_md: String(res?.agent_md_templates?.soul_md || ''),
agents_md: String(res?.agent_md_templates?.agents_md || ''),
user_md: String(res?.agent_md_templates?.user_md || ''),
tools_md: String(res?.agent_md_templates?.tools_md || ''),
identity_md: String(res?.agent_md_templates?.identity_md || ''),
});
setTopicPresetsText(JSON.stringify(res?.topic_presets || { presets: [] }, null, 2));
setActiveTab('agents_md'); setActiveTab('agents_md');
} catch (error: any) {
notify(error?.response?.data?.detail || (isZh ? '读取模板失败。' : 'Failed to load templates.'), { tone: 'error' });
} finally {
if (alive) setLoading(false);
} }
}; };
void load(); void load();
return () => { return () => {
alive = false; alive = false;
}; };
}, [isZh, notify]); }, [reloadTemplates]);
const activeMeta = useMemo(() => templateMeta[activeTab], [activeTab]); const activeMeta = useMemo(() => templateMeta[activeTab], [activeTab]);
@ -144,24 +157,7 @@ function TemplateManagerView({
className="btn btn-secondary btn-sm skill-market-button-with-icon" className="btn btn-secondary btn-sm skill-market-button-with-icon"
type="button" type="button"
disabled={loading} disabled={loading}
onClick={() => void (async () => { onClick={() => void reloadTemplates()}
setLoading(true);
try {
const res = await fetchPlatformSystemTemplates();
setTemplates({
soul_md: String(res?.agent_md_templates?.soul_md || ''),
agents_md: String(res?.agent_md_templates?.agents_md || ''),
user_md: String(res?.agent_md_templates?.user_md || ''),
tools_md: String(res?.agent_md_templates?.tools_md || ''),
identity_md: String(res?.agent_md_templates?.identity_md || ''),
});
setTopicPresetsText(JSON.stringify(res?.topic_presets || { presets: [] }, null, 2));
} catch (error: any) {
notify(error?.response?.data?.detail || (isZh ? '刷新模板失败。' : 'Failed to refresh templates.'), { tone: 'error' });
} finally {
setLoading(false);
}
})()}
> >
{loading ? <RefreshCw size={14} className="animate-spin" /> : null} {loading ? <RefreshCw size={14} className="animate-spin" /> : null}
<span>{isZh ? '重载' : 'Reload'}</span> <span>{isZh ? '重载' : 'Reload'}</span>
@ -203,10 +199,10 @@ function TemplateManagerView({
topic_presets: topicPresets, topic_presets: topicPresets,
}); });
notify(isZh ? '模版已保存。' : 'Templates saved.', { tone: 'success' }); notify(isZh ? '模版已保存。' : 'Templates saved.', { tone: 'success' });
} catch (error: any) { } catch (error: unknown) {
const detail = error instanceof SyntaxError const detail = error instanceof SyntaxError
? (isZh ? 'Topic 预设 JSON 解析失败。' : 'Invalid topic presets JSON.') ? (isZh ? 'Topic 预设 JSON 解析失败。' : 'Invalid topic presets JSON.')
: (error?.response?.data?.detail || (isZh ? '保存模板失败。' : 'Failed to save templates.')); : resolveApiErrorMessage(error, isZh ? '保存模板失败。' : 'Failed to save templates.');
notify(detail, { tone: 'error' }); notify(detail, { tone: 'error' });
} finally { } finally {
setSaving(false); setSaving(false);

View File

@ -1,7 +1,7 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { useLucentPrompt } from '../../../components/lucent/LucentPromptProvider'; import { useLucentPrompt } from '../../../components/lucent/useLucentPrompt';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { sortBotsByCreatedAtDesc } from '../../../shared/bot/sortBots'; import { sortBotsByCreatedAtDesc } from '../../../shared/bot/sortBots';
import { useAppStore } from '../../../store/appStore'; import { useAppStore } from '../../../store/appStore';

View File

@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import type { BotChannel, MCPConfigResponse, WorkspaceSkillOption } from '../../dashboard/types'; import type { BotChannel, MCPConfigResponse, WorkspaceSkillOption } from '../../dashboard/types';
import type { BotState } from '../../../types/bot'; import type { BotState } from '../../../types/bot';
import type { PlatformBotResourceSnapshot, PlatformUsageResponse } from '../types'; import type { PlatformBotResourceSnapshot, PlatformUsageResponse } from '../types';
@ -147,8 +148,8 @@ export function usePlatformManagementState({
try { try {
const res = await axios.get<PlatformBotResourceSnapshot>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/resources`); const res = await axios.get<PlatformBotResourceSnapshot>(`${APP_ENDPOINTS.apiBase}/bots/${encodeURIComponent(botId)}/resources`);
setResourceSnapshot(res.data); setResourceSnapshot(res.data);
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || (isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.'); const msg = resolveApiErrorMessage(error, isZh ? '读取资源监控失败。' : 'Failed to load resource metrics.');
setResourceError(String(msg)); setResourceError(String(msg));
} finally { } finally {
setResourceLoading(false); setResourceLoading(false);

View File

@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../../config/env'; import { APP_ENDPOINTS } from '../../../config/env';
import { resolveApiErrorMessage } from '../../../shared/http/apiErrors';
import { import {
normalizePlatformPageSize, normalizePlatformPageSize,
readCachedPlatformPageSize, readCachedPlatformPageSize,
@ -49,8 +50,8 @@ export function usePlatformOverviewState({
); );
writeCachedPlatformPageSize(normalizedPageSize); writeCachedPlatformPageSize(normalizedPageSize);
setPlatformPageSize(normalizedPageSize); setPlatformPageSize(normalizedPageSize);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '读取平台总览失败。' : 'Failed to load platform overview.'), { tone: 'error' });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -66,8 +67,8 @@ export function usePlatformOverviewState({
}, },
}); });
setUsageData(res.data); setUsageData(res.data);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '读取用量统计失败。' : 'Failed to load usage analytics.'), { tone: 'error' });
} finally { } finally {
setUsageLoading(false); setUsageLoading(false);
} }
@ -78,8 +79,8 @@ export function usePlatformOverviewState({
try { try {
const res = await axios.get<{ items: BotActivityStatsItem[] }>(`${APP_ENDPOINTS.apiBase}/platform/activity-stats`); const res = await axios.get<{ items: BotActivityStatsItem[] }>(`${APP_ENDPOINTS.apiBase}/platform/activity-stats`);
setActivityStatsData(Array.isArray(res.data?.items) ? res.data.items : []); setActivityStatsData(Array.isArray(res.data?.items) ? res.data.items : []);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || (isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' }); notify(resolveApiErrorMessage(error, isZh ? '读取 Bot 活跃度统计失败。' : 'Failed to load bot activity analytics.'), { tone: 'error' });
} finally { } finally {
setActivityLoading(false); setActivityLoading(false);
} }

View File

@ -0,0 +1,18 @@
import axios from 'axios';
export interface ApiErrorDetail {
detail?: string;
}
export function resolveApiErrorMessage(error: unknown, fallback: string): string {
if (axios.isAxiosError<ApiErrorDetail>(error)) {
const detail = String(error.response?.data?.detail || '').trim();
if (detail) return detail;
const message = String(error.message || '').trim();
if (message) return message;
} else if (error instanceof Error) {
const message = String(error.message || '').trim();
if (message) return message;
}
return fallback;
}

View File

@ -15,7 +15,7 @@ import {
} from './utils'; } from './utils';
import type { WorkspaceAttachmentPolicySnapshot, WorkspaceNotifyOptions } from './workspaceShared'; import type { WorkspaceAttachmentPolicySnapshot, WorkspaceNotifyOptions } from './workspaceShared';
import { useWorkspaceAttachments, type WorkspaceAttachmentLabels } from './useWorkspaceAttachments'; import { useWorkspaceAttachments, type WorkspaceAttachmentLabels } from './useWorkspaceAttachments';
import { useWorkspacePreview } from './useWorkspacePreview'; import { useWorkspacePreview, type WorkspacePreviewLabels } from './useWorkspacePreview';
interface WorkspaceTreeLabels { interface WorkspaceTreeLabels {
workspaceLoadFail: string; workspaceLoadFail: string;
@ -45,7 +45,7 @@ interface UseBotWorkspaceOptions {
refreshAttachmentPolicy: () => Promise<WorkspaceAttachmentPolicySnapshot>; refreshAttachmentPolicy: () => Promise<WorkspaceAttachmentPolicySnapshot>;
restorePendingAttachments?: (botId: string) => string[]; restorePendingAttachments?: (botId: string) => string[];
notify: (message: string, options?: WorkspaceNotifyOptions) => void; notify: (message: string, options?: WorkspaceNotifyOptions) => void;
t: WorkspaceTreeLabels & WorkspaceAttachmentLabels & Record<string, unknown>; t: WorkspaceTreeLabels & WorkspaceAttachmentLabels & WorkspacePreviewLabels & Record<string, unknown>;
isZh: boolean; isZh: boolean;
fileNotPreviewableLabel: string; fileNotPreviewableLabel: string;
} }

View File

@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import axios from 'axios'; import axios from 'axios';
import { APP_ENDPOINTS } from '../../config/env'; import { APP_ENDPOINTS } from '../../config/env';
import { resolveApiErrorMessage } from '../http/apiErrors';
import type { WorkspaceFileResponse, WorkspacePreviewMode, WorkspacePreviewState } from './types'; import type { WorkspaceFileResponse, WorkspacePreviewMode, WorkspacePreviewState } from './types';
import { import {
buildWorkspaceDownloadHref, buildWorkspaceDownloadHref,
@ -18,13 +19,22 @@ import {
} from './utils'; } from './utils';
import type { WorkspaceNotifyOptions } from './workspaceShared'; import type { WorkspaceNotifyOptions } from './workspaceShared';
export interface WorkspacePreviewLabels {
fileReadFail: string;
fileEditDisabled: string;
fileSaveFail: string;
fileSaved: string;
urlCopied: string;
urlCopyFail: string;
}
interface UseWorkspacePreviewOptions { interface UseWorkspacePreviewOptions {
selectedBotId: string; selectedBotId: string;
workspaceCurrentPath: string; workspaceCurrentPath: string;
workspaceDownloadExtensionSet: ReadonlySet<string>; workspaceDownloadExtensionSet: ReadonlySet<string>;
loadWorkspaceTree: (botId: string, path?: string) => Promise<void>; loadWorkspaceTree: (botId: string, path?: string) => Promise<void>;
notify: (message: string, options?: WorkspaceNotifyOptions) => void; notify: (message: string, options?: WorkspaceNotifyOptions) => void;
t: any; t: WorkspacePreviewLabels;
isZh: boolean; isZh: boolean;
} }
@ -61,6 +71,8 @@ export function useWorkspacePreview({
const workspacePreviewCanEdit = Boolean(workspacePreview?.isMarkdown && !workspacePreview?.truncated); const workspacePreviewCanEdit = Boolean(workspacePreview?.isMarkdown && !workspacePreview?.truncated);
const workspacePreviewEditorEnabled = workspacePreviewCanEdit && workspacePreviewMode === 'edit'; const workspacePreviewEditorEnabled = workspacePreviewCanEdit && workspacePreviewMode === 'edit';
const workspacePreviewPath = workspacePreview?.path || '';
const workspacePreviewContent = workspacePreview?.content || '';
const getWorkspaceDownloadHref = useCallback((filePath: string, forceDownload: boolean = true) => const getWorkspaceDownloadHref = useCallback((filePath: string, forceDownload: boolean = true) =>
buildWorkspaceDownloadHref(selectedBotId, filePath, forceDownload), buildWorkspaceDownloadHref(selectedBotId, filePath, forceDownload),
@ -153,8 +165,8 @@ export function useWorkspacePreview({
isVideo: false, isVideo: false,
isAudio: false, isAudio: false,
}); });
} catch (error: any) { } catch (error: unknown) {
const msg = error?.response?.data?.detail || t.fileReadFail; const msg = resolveApiErrorMessage(error, t.fileReadFail);
notify(msg, { tone: 'error' }); notify(msg, { tone: 'error' });
} finally { } finally {
setWorkspaceFileLoading(false); setWorkspaceFileLoading(false);
@ -187,8 +199,8 @@ export function useWorkspacePreview({
}); });
notify(t.fileSaved, { tone: 'success' }); notify(t.fileSaved, { tone: 'success' });
void loadWorkspaceTree(selectedBotId, workspaceCurrentPath); void loadWorkspaceTree(selectedBotId, workspaceCurrentPath);
} catch (error: any) { } catch (error: unknown) {
notify(error?.response?.data?.detail || t.fileSaveFail, { tone: 'error' }); notify(resolveApiErrorMessage(error, t.fileSaveFail), { tone: 'error' });
} finally { } finally {
setWorkspacePreviewSaving(false); setWorkspacePreviewSaving(false);
} }
@ -266,15 +278,15 @@ export function useWorkspacePreview({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!workspacePreview) { if (!workspacePreviewPath) {
setWorkspacePreviewMode('preview'); setWorkspacePreviewMode('preview');
setWorkspacePreviewSaving(false); setWorkspacePreviewSaving(false);
setWorkspacePreviewDraft(''); setWorkspacePreviewDraft('');
return; return;
} }
setWorkspacePreviewSaving(false); setWorkspacePreviewSaving(false);
setWorkspacePreviewDraft(workspacePreview.content || ''); setWorkspacePreviewDraft(workspacePreviewContent);
}, [workspacePreview?.content, workspacePreview?.path]); }, [workspacePreviewContent, workspacePreviewPath]);
return { return {
closeWorkspacePreview, closeWorkspacePreview,

View File

@ -4,7 +4,7 @@ export const WORKSPACE_LINK_PREFIX = 'https://workspace.local/open/';
const WORKSPACE_ABS_PATH_PATTERN = const WORKSPACE_ABS_PATH_PATTERN =
/\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi; /\/root\/\.nanobot\/workspace\/[^\n\r<>"'`]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps)\b/gi;
const WORKSPACE_RELATIVE_PATH_PATTERN = const WORKSPACE_RELATIVE_PATH_PATTERN =
/(^|[\s(\[])(\/[^\n\r<>"'`)\]]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))(?![A-Za-z0-9_./-])/gim; /(^|[\s([])(\/[^\n\r<>"'`)\]]+?\.(?:md|markdown|json|txt|log|csv|tsv|yaml|yml|toml|html|htm|pdf|png|jpg|jpeg|gif|webp|svg|mp3|wav|m4a|flac|ogg|opus|aac|amr|wma|mp4|mov|avi|mkv|webm|m4v|3gp|mpeg|mpg|ts|doc|docx|xls|xlsx|xlsm|ppt|pptx|odt|ods|odp|wps))(?![A-Za-z0-9_./-])/gim;
const WORKSPACE_RENDER_PATTERN = const WORKSPACE_RENDER_PATTERN =
/\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\r\n]*)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\],,。!?;:]+|https:\/\/workspace\.local\/open\/[^)\r\n]+/gi; /\[(\/root\/\.nanobot\/workspace\/[^\]]+)\]\((https:\/\/workspace\.local\/open\/[^)\r\n]*)\)|\/root\/\.nanobot\/workspace\/[^\s<>"'`)\],,。!?;:]+|https:\/\/workspace\.local\/open\/[^)\r\n]+/gi;