v0.1.4
parent
85ff2fd17a
commit
c67c6c3e6c
|
|
@ -292,7 +292,6 @@ class WSConnectionManager:
|
||||||
|
|
||||||
manager = WSConnectionManager()
|
manager = WSConnectionManager()
|
||||||
|
|
||||||
BOT_ACCESS_PASSWORD_HEADER = "x-bot-password"
|
|
||||||
PANEL_ACCESS_PASSWORD_HEADER = "x-panel-password"
|
PANEL_ACCESS_PASSWORD_HEADER = "x-panel-password"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -313,14 +312,6 @@ def _extract_bot_id_from_api_path(path: str) -> Optional[str]:
|
||||||
return str(decoded).strip() or None
|
return str(decoded).strip() or None
|
||||||
|
|
||||||
|
|
||||||
def _get_supplied_bot_password_http(request: Request) -> str:
|
|
||||||
header_value = str(request.headers.get(BOT_ACCESS_PASSWORD_HEADER) or "").strip()
|
|
||||||
if header_value:
|
|
||||||
return header_value
|
|
||||||
query_value = str(request.query_params.get("access_password") or "").strip()
|
|
||||||
return query_value
|
|
||||||
|
|
||||||
|
|
||||||
def _get_supplied_panel_password_http(request: Request) -> str:
|
def _get_supplied_panel_password_http(request: Request) -> str:
|
||||||
header_value = str(request.headers.get(PANEL_ACCESS_PASSWORD_HEADER) or "").strip()
|
header_value = str(request.headers.get(PANEL_ACCESS_PASSWORD_HEADER) or "").strip()
|
||||||
if header_value:
|
if header_value:
|
||||||
|
|
@ -341,8 +332,9 @@ def _validate_panel_access_password(supplied: str) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _is_panel_protected_api_path(path: str) -> bool:
|
def _is_panel_protected_api_path(path: str, method: str = "GET") -> bool:
|
||||||
raw = str(path or "").strip()
|
raw = str(path or "").strip()
|
||||||
|
verb = str(method or "GET").strip().upper()
|
||||||
if not raw.startswith("/api/"):
|
if not raw.startswith("/api/"):
|
||||||
return False
|
return False
|
||||||
if raw in {
|
if raw in {
|
||||||
|
|
@ -352,21 +344,28 @@ def _is_panel_protected_api_path(path: str) -> bool:
|
||||||
"/api/health/cache",
|
"/api/health/cache",
|
||||||
}:
|
}:
|
||||||
return False
|
return False
|
||||||
if _is_bot_panel_management_api_path(raw):
|
if _is_bot_panel_management_api_path(raw, verb):
|
||||||
return True
|
return True
|
||||||
# Bot-scoped content/chat APIs are protected by the bot's own access password only.
|
# Other bot-scoped APIs are not protected by panel password.
|
||||||
if _extract_bot_id_from_api_path(raw):
|
if _extract_bot_id_from_api_path(raw):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _is_bot_panel_management_api_path(path: str) -> bool:
|
def _is_bot_panel_management_api_path(path: str, method: str = "GET") -> bool:
|
||||||
raw = str(path or "").strip()
|
raw = str(path or "").strip()
|
||||||
|
verb = str(method or "GET").strip().upper()
|
||||||
if not raw.startswith("/api/bots/"):
|
if not raw.startswith("/api/bots/"):
|
||||||
return False
|
return False
|
||||||
if not _extract_bot_id_from_api_path(raw):
|
bot_id = _extract_bot_id_from_api_path(raw)
|
||||||
|
if not bot_id:
|
||||||
return False
|
return False
|
||||||
return raw.endswith("/start") or raw.endswith("/stop") or raw.endswith("/deactivate") or raw == f"/api/bots/{_extract_bot_id_from_api_path(raw)}"
|
return (
|
||||||
|
raw.endswith("/start")
|
||||||
|
or raw.endswith("/stop")
|
||||||
|
or raw.endswith("/deactivate")
|
||||||
|
or (verb in {"PUT", "DELETE"} and raw == f"/api/bots/{bot_id}")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
|
|
@ -374,7 +373,7 @@ async def bot_access_password_guard(request: Request, call_next):
|
||||||
if request.method.upper() == "OPTIONS":
|
if request.method.upper() == "OPTIONS":
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
if _is_panel_protected_api_path(request.url.path):
|
if _is_panel_protected_api_path(request.url.path, request.method):
|
||||||
panel_error = _validate_panel_access_password(_get_supplied_panel_password_http(request))
|
panel_error = _validate_panel_access_password(_get_supplied_panel_password_http(request))
|
||||||
if panel_error:
|
if panel_error:
|
||||||
return JSONResponse(status_code=401, content={"detail": panel_error})
|
return JSONResponse(status_code=401, content={"detail": panel_error})
|
||||||
|
|
@ -387,14 +386,6 @@ async def bot_access_password_guard(request: Request, call_next):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
return JSONResponse(status_code=404, content={"detail": "Bot not found"})
|
return JSONResponse(status_code=404, content={"detail": "Bot not found"})
|
||||||
configured_password = str(bot.access_password or "").strip()
|
|
||||||
if configured_password:
|
|
||||||
supplied = _get_supplied_bot_password_http(request)
|
|
||||||
if not supplied:
|
|
||||||
return JSONResponse(status_code=401, content={"detail": "Bot access password required"})
|
|
||||||
if supplied != configured_password:
|
|
||||||
return JSONResponse(status_code=401, content={"detail": "Invalid bot access password"})
|
|
||||||
|
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2721,17 +2712,6 @@ async def websocket_endpoint(websocket: WebSocket, bot_id: str):
|
||||||
if not bot:
|
if not bot:
|
||||||
await websocket.close(code=4404, reason="Bot not found")
|
await websocket.close(code=4404, reason="Bot not found")
|
||||||
return
|
return
|
||||||
configured_password = str(bot.access_password or "").strip()
|
|
||||||
if configured_password:
|
|
||||||
supplied = str(
|
|
||||||
websocket.headers.get(BOT_ACCESS_PASSWORD_HEADER) or websocket.query_params.get("access_password") or ""
|
|
||||||
).strip()
|
|
||||||
if not supplied:
|
|
||||||
await websocket.close(code=4401, reason="Bot access password required")
|
|
||||||
return
|
|
||||||
if supplied != configured_password:
|
|
||||||
await websocket.close(code=4401, reason="Invalid bot access password")
|
|
||||||
return
|
|
||||||
|
|
||||||
await manager.connect(bot_id, websocket)
|
await manager.connect(bot_id, websocket)
|
||||||
docker_manager.ensure_monitor(bot_id, docker_callback)
|
docker_manager.ensure_monitor(bot_id, docker_callback)
|
||||||
|
|
|
||||||
|
|
@ -25,26 +25,45 @@ function AuthenticatedApp({
|
||||||
const { theme, setTheme, locale, setLocale, activeBots } = useAppStore();
|
const { theme, setTheme, locale, setLocale, activeBots } = useAppStore();
|
||||||
const [showImageFactory, setShowImageFactory] = useState(false);
|
const [showImageFactory, setShowImageFactory] = useState(false);
|
||||||
const [showCreateWizard, setShowCreateWizard] = useState(false);
|
const [showCreateWizard, setShowCreateWizard] = useState(false);
|
||||||
|
const [singleBotPassword, setSingleBotPassword] = useState('');
|
||||||
|
const [singleBotPasswordError, setSingleBotPasswordError] = useState('');
|
||||||
|
const [singleBotUnlocked, setSingleBotUnlocked] = useState(false);
|
||||||
useBotsSync(forcedBotId);
|
useBotsSync(forcedBotId);
|
||||||
const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn });
|
const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn });
|
||||||
const isSingleBotCompactView = compactMode && Boolean(String(forcedBotId || '').trim());
|
const isSingleBotCompactView = compactMode && Boolean(String(forcedBotId || '').trim());
|
||||||
const [headerCollapsed, setHeaderCollapsed] = useState(isSingleBotCompactView);
|
const [headerCollapsed, setHeaderCollapsed] = useState(isSingleBotCompactView);
|
||||||
|
const forced = String(forcedBotId || '').trim();
|
||||||
|
const forcedBot = forced ? activeBots[forced] : undefined;
|
||||||
|
const shouldPromptSingleBotPassword = Boolean(forced && forcedBot?.has_access_password && !singleBotUnlocked);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const forced = String(forcedBotId || '').trim();
|
|
||||||
if (!forced) {
|
if (!forced) {
|
||||||
document.title = t.title;
|
document.title = t.title;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bot = activeBots[forced];
|
const botName = String(forcedBot?.name || '').trim();
|
||||||
const botName = String(bot?.name || '').trim();
|
|
||||||
document.title = botName ? `${t.title} - ${botName}` : `${t.title} - ${forced}`;
|
document.title = botName ? `${t.title} - ${botName}` : `${t.title} - ${forced}`;
|
||||||
}, [activeBots, t.title, forcedBotId]);
|
}, [forced, forcedBot?.name, t.title]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeaderCollapsed(isSingleBotCompactView);
|
setHeaderCollapsed(isSingleBotCompactView);
|
||||||
}, [isSingleBotCompactView, forcedBotId]);
|
}, [isSingleBotCompactView, forcedBotId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSingleBotUnlocked(false);
|
||||||
|
setSingleBotPassword('');
|
||||||
|
setSingleBotPasswordError('');
|
||||||
|
}, [forced]);
|
||||||
|
|
||||||
|
const unlockSingleBot = () => {
|
||||||
|
if (!String(singleBotPassword || '').trim()) {
|
||||||
|
setSingleBotPasswordError(locale === 'zh' ? '请输入 Bot 密码。' : 'Enter the bot password.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSingleBotPasswordError('');
|
||||||
|
setSingleBotUnlocked(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`app-shell ${compactMode ? 'app-shell-compact' : ''}`} data-theme={theme}>
|
<div className={`app-shell ${compactMode ? 'app-shell-compact' : ''}`} data-theme={theme}>
|
||||||
<div className="app-frame">
|
<div className="app-frame">
|
||||||
|
|
@ -180,6 +199,36 @@ function AuthenticatedApp({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldPromptSingleBotPassword ? (
|
||||||
|
<div className="modal-mask app-modal-mask">
|
||||||
|
<div className="app-login-card" onClick={(event) => event.stopPropagation()}>
|
||||||
|
<img src="/app-bot-icon.svg" alt="Nanobot" className="app-login-icon" />
|
||||||
|
<h1>{forcedBot?.name || forced}</h1>
|
||||||
|
<p>{locale === 'zh' ? '请输入该 Bot 的访问密码后继续。' : 'Enter the bot password to continue.'}</p>
|
||||||
|
<div className="app-login-form">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
value={singleBotPassword}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSingleBotPassword(event.target.value);
|
||||||
|
if (singleBotPasswordError) setSingleBotPasswordError('');
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') unlockSingleBot();
|
||||||
|
}}
|
||||||
|
placeholder={locale === 'zh' ? 'Bot 密码' : 'Bot password'}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{singleBotPasswordError ? <div className="app-login-error">{singleBotPasswordError}</div> : null}
|
||||||
|
<button className="btn btn-primary app-login-submit" onClick={unlockSingleBot}>
|
||||||
|
{locale === 'zh' ? '进入' : 'Continue'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -217,8 +266,15 @@ function PanelLoginGate({
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const bypassPanelGate = Boolean(String(urlView.forcedBotId || '').trim());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (bypassPanelGate) {
|
||||||
|
setRequired(false);
|
||||||
|
setAuthenticated(true);
|
||||||
|
setChecking(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let alive = true;
|
let alive = true;
|
||||||
const boot = async () => {
|
const boot = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -259,7 +315,7 @@ function PanelLoginGate({
|
||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
};
|
};
|
||||||
}, [locale]);
|
}, [bypassPanelGate, locale]);
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const next = String(password || '').trim();
|
const next = String(password || '').trim();
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { normalizeAssistantMessageText, normalizeUserMessageText, summarizeProgr
|
||||||
import { pickLocale } from '../i18n';
|
import { pickLocale } from '../i18n';
|
||||||
import { botsSyncZhCn } from '../i18n/bots-sync.zh-cn';
|
import { botsSyncZhCn } from '../i18n/bots-sync.zh-cn';
|
||||||
import { botsSyncEn } from '../i18n/bots-sync.en';
|
import { botsSyncEn } from '../i18n/bots-sync.en';
|
||||||
import { buildMonitorWsUrl, getBotAccessPassword } from '../utils/botAccess';
|
import { buildMonitorWsUrl } from '../utils/botAccess';
|
||||||
|
|
||||||
function normalizeState(v: string): 'THINKING' | 'TOOL_CALL' | 'SUCCESS' | 'ERROR' | 'INFO' {
|
function normalizeState(v: string): 'THINKING' | 'TOOL_CALL' | 'SUCCESS' | 'ERROR' | 'INFO' {
|
||||||
const s = (v || '').toUpperCase();
|
const s = (v || '').toUpperCase();
|
||||||
|
|
@ -122,8 +122,6 @@ export function useBotsSync(forcedBotId?: string) {
|
||||||
|
|
||||||
botIds.forEach((botId) => {
|
botIds.forEach((botId) => {
|
||||||
if (hydratedMessagesRef.current[botId]) return;
|
if (hydratedMessagesRef.current[botId]) return;
|
||||||
const bot = activeBots[botId];
|
|
||||||
if (bot?.has_access_password && !getBotAccessPassword(botId)) return;
|
|
||||||
hydratedMessagesRef.current[botId] = true;
|
hydratedMessagesRef.current[botId] = true;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -177,9 +175,6 @@ export function useBotsSync(forcedBotId?: string) {
|
||||||
if (bot.docker_status !== 'RUNNING') {
|
if (bot.docker_status !== 'RUNNING') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bot.has_access_password && !getBotAccessPassword(bot.id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (socketsRef.current[bot.id]) {
|
if (socketsRef.current[bot.id]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import { dashboardZhCn } from '../../i18n/dashboard.zh-cn';
|
||||||
import { dashboardEn } from '../../i18n/dashboard.en';
|
import { dashboardEn } from '../../i18n/dashboard.en';
|
||||||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||||
import { clearBotAccessPassword, getBotAccessPassword, isBotUnauthorizedError, setBotAccessPassword } from '../../utils/botAccess';
|
|
||||||
|
|
||||||
interface BotDashboardModuleProps {
|
interface BotDashboardModuleProps {
|
||||||
onOpenCreateWizard?: () => void;
|
onOpenCreateWizard?: () => void;
|
||||||
|
|
@ -649,6 +648,8 @@ export function BotDashboardModule({
|
||||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const botOrderRef = useRef<Record<string, number>>({});
|
||||||
|
const nextBotOrderRef = useRef(1);
|
||||||
const applyEditFormFromBot = useCallback((bot?: any) => {
|
const applyEditFormFromBot = useCallback((bot?: any) => {
|
||||||
if (!bot) return;
|
if (!bot) return;
|
||||||
setProviderTestResult('');
|
setProviderTestResult('');
|
||||||
|
|
@ -914,13 +915,37 @@ export function BotDashboardModule({
|
||||||
storage_gb: '10',
|
storage_gb: '10',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ordered = Object.values(activeBots).sort((a, b) => {
|
||||||
|
const aCreated = parseBotTimestamp(a.created_at);
|
||||||
|
const bCreated = parseBotTimestamp(b.created_at);
|
||||||
|
if (aCreated !== bCreated) return aCreated - bCreated;
|
||||||
|
return String(a.id || '').localeCompare(String(b.id || ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
ordered.forEach((bot) => {
|
||||||
|
const id = String(bot.id || '').trim();
|
||||||
|
if (!id) return;
|
||||||
|
if (botOrderRef.current[id] !== undefined) return;
|
||||||
|
botOrderRef.current[id] = nextBotOrderRef.current;
|
||||||
|
nextBotOrderRef.current += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const alive = new Set(ordered.map((bot) => String(bot.id || '').trim()).filter(Boolean));
|
||||||
|
Object.keys(botOrderRef.current).forEach((id) => {
|
||||||
|
if (!alive.has(id)) delete botOrderRef.current[id];
|
||||||
|
});
|
||||||
|
}, [activeBots]);
|
||||||
|
|
||||||
const bots = useMemo(
|
const bots = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.values(activeBots).sort((a, b) => {
|
Object.values(activeBots).sort((a, b) => {
|
||||||
const aCreated = parseBotTimestamp(a.created_at);
|
const aId = String(a.id || '').trim();
|
||||||
const bCreated = parseBotTimestamp(b.created_at);
|
const bId = String(b.id || '').trim();
|
||||||
if (aCreated !== bCreated) return aCreated - bCreated;
|
const aOrder = botOrderRef.current[aId] ?? Number.MAX_SAFE_INTEGER;
|
||||||
return String(a.id || '').localeCompare(String(b.id || ''));
|
const bOrder = botOrderRef.current[bId] ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
if (aOrder !== bOrder) return aOrder - bOrder;
|
||||||
|
return aId.localeCompare(bId);
|
||||||
}),
|
}),
|
||||||
[activeBots],
|
[activeBots],
|
||||||
);
|
);
|
||||||
|
|
@ -947,97 +972,6 @@ export function BotDashboardModule({
|
||||||
const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const });
|
const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const });
|
||||||
const t = pickLocale(locale, { 'zh-cn': dashboardZhCn, en: dashboardEn });
|
const t = pickLocale(locale, { 'zh-cn': dashboardZhCn, en: dashboardEn });
|
||||||
const lc = isZh ? channelsZhCn : channelsEn;
|
const lc = isZh ? channelsZhCn : channelsEn;
|
||||||
const botAccessCheckRef = useRef<Record<string, Promise<boolean> | undefined>>({});
|
|
||||||
const botPasswordResolverRef = useRef<((value: string | null) => void) | null>(null);
|
|
||||||
const [botPasswordDialog, setBotPasswordDialog] = useState<{
|
|
||||||
open: boolean;
|
|
||||||
botName: string;
|
|
||||||
invalid: boolean;
|
|
||||||
value: string;
|
|
||||||
}>({
|
|
||||||
open: false,
|
|
||||||
botName: '',
|
|
||||||
invalid: false,
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const promptForBotPassword = (botName: string, invalid: boolean): Promise<string | null> => {
|
|
||||||
setBotPasswordDialog({
|
|
||||||
open: true,
|
|
||||||
botName,
|
|
||||||
invalid,
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
botPasswordResolverRef.current = resolve;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeBotPasswordDialog = (value: string | null) => {
|
|
||||||
const resolver = botPasswordResolverRef.current;
|
|
||||||
botPasswordResolverRef.current = null;
|
|
||||||
setBotPasswordDialog((prev) => ({ ...prev, open: false, value: '' }));
|
|
||||||
if (resolver) resolver(value && String(value).trim() ? String(value).trim() : null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyBotPassword = async (botId: string): Promise<boolean> => {
|
|
||||||
await axios.get(`${APP_ENDPOINTS.apiBase}/bots/${botId}/channels`);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureBotAccess = async (botId: string): Promise<boolean> => {
|
|
||||||
const normalizedBotId = String(botId || '').trim();
|
|
||||||
if (!normalizedBotId) return false;
|
|
||||||
const bot = activeBots[normalizedBotId];
|
|
||||||
if (!bot?.has_access_password) return true;
|
|
||||||
|
|
||||||
const inFlight = botAccessCheckRef.current[normalizedBotId];
|
|
||||||
if (inFlight) return inFlight;
|
|
||||||
|
|
||||||
const checkPromise = (async () => {
|
|
||||||
const botName = String(bot.name || bot.id || normalizedBotId).trim();
|
|
||||||
let askForNewPassword = false;
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
||||||
let password = getBotAccessPassword(normalizedBotId);
|
|
||||||
if (!password || askForNewPassword) {
|
|
||||||
const input = await promptForBotPassword(botName, askForNewPassword);
|
|
||||||
if (input === null) {
|
|
||||||
notify(isZh ? '已取消密码输入,无法访问该机器人。' : 'Password input cancelled. Bot access blocked.', {
|
|
||||||
tone: 'warning',
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
setBotAccessPassword(normalizedBotId, input);
|
|
||||||
password = input;
|
|
||||||
}
|
|
||||||
if (!password) {
|
|
||||||
askForNewPassword = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await verifyBotPassword(normalizedBotId);
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (isBotUnauthorizedError(error, normalizedBotId)) {
|
|
||||||
clearBotAccessPassword(normalizedBotId);
|
|
||||||
askForNewPassword = true;
|
|
||||||
notify(isZh ? '访问密码错误,请重试。' : 'Access password is invalid. Please retry.', { tone: 'warning' });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})();
|
|
||||||
|
|
||||||
botAccessCheckRef.current[normalizedBotId] = checkPromise;
|
|
||||||
try {
|
|
||||||
return await checkPromise;
|
|
||||||
} finally {
|
|
||||||
delete botAccessCheckRef.current[normalizedBotId];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const baseImageOptions = useMemo<BaseImageOption[]>(() => {
|
const baseImageOptions = useMemo<BaseImageOption[]>(() => {
|
||||||
const readyTags = new Set(
|
const readyTags = new Set(
|
||||||
availableImages
|
availableImages
|
||||||
|
|
@ -2447,8 +2381,7 @@ export function BotDashboardModule({
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const loadAll = async () => {
|
const loadAll = async () => {
|
||||||
try {
|
try {
|
||||||
const granted = await ensureBotAccess(selectedBotId);
|
if (cancelled) return;
|
||||||
if (!granted || cancelled) return;
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
loadWorkspaceTree(selectedBotId, ''),
|
loadWorkspaceTree(selectedBotId, ''),
|
||||||
loadCronJobs(selectedBotId),
|
loadCronJobs(selectedBotId),
|
||||||
|
|
@ -2457,15 +2390,6 @@ export function BotDashboardModule({
|
||||||
]);
|
]);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const detail = String(error?.response?.data?.detail || '').trim();
|
const detail = String(error?.response?.data?.detail || '').trim();
|
||||||
if (isBotUnauthorizedError(error, selectedBotId)) {
|
|
||||||
clearBotAccessPassword(selectedBotId);
|
|
||||||
if (!cancelled) {
|
|
||||||
notify(isZh ? '访问密码校验失败,请重新进入该机器人。' : 'Bot password check failed. Reopen the bot and retry.', {
|
|
||||||
tone: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cancelled && detail) {
|
if (!cancelled && detail) {
|
||||||
notify(detail, { tone: 'error' });
|
notify(detail, { tone: 'error' });
|
||||||
}
|
}
|
||||||
|
|
@ -2584,14 +2508,6 @@ export function BotDashboardModule({
|
||||||
}
|
}
|
||||||
|
|
||||||
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${targetBotId}`, payload);
|
await axios.put(`${APP_ENDPOINTS.apiBase}/bots/${targetBotId}`, payload);
|
||||||
if (mode === 'base') {
|
|
||||||
const nextPassword = String(editForm.access_password || '').trim();
|
|
||||||
if (nextPassword) {
|
|
||||||
setBotAccessPassword(targetBotId, nextPassword);
|
|
||||||
} else {
|
|
||||||
clearBotAccessPassword(targetBotId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await refresh();
|
await refresh();
|
||||||
setShowBaseModal(false);
|
setShowBaseModal(false);
|
||||||
setShowParamModal(false);
|
setShowParamModal(false);
|
||||||
|
|
@ -4214,52 +4130,6 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{botPasswordDialog.open ? (
|
|
||||||
<div className="modal-mask" onClick={() => closeBotPasswordDialog(null)}>
|
|
||||||
<div className="modal-card" style={{ width: 'min(520px, calc(100vw - 28px))' }} onClick={(event) => event.stopPropagation()}>
|
|
||||||
<div className="modal-title-row modal-title-with-close">
|
|
||||||
<div className="modal-title-main">
|
|
||||||
<h3>
|
|
||||||
{botPasswordDialog.invalid
|
|
||||||
? (isZh ? `访问密码错误:${botPasswordDialog.botName}` : `Invalid access password: ${botPasswordDialog.botName}`)
|
|
||||||
: (isZh ? `请输入访问密码:${botPasswordDialog.botName}` : `Enter access password for ${botPasswordDialog.botName}`)}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="modal-title-actions">
|
|
||||||
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => closeBotPasswordDialog(null)} tooltip={isZh ? '关闭' : 'Close'} aria-label={isZh ? '关闭' : 'Close'}>
|
|
||||||
<X size={14} />
|
|
||||||
</LucentIconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="stack" style={{ gap: 12 }}>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
autoFocus
|
|
||||||
type="password"
|
|
||||||
value={botPasswordDialog.value}
|
|
||||||
onChange={(event) =>
|
|
||||||
setBotPasswordDialog((prev) => ({
|
|
||||||
...prev,
|
|
||||||
value: event.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') closeBotPasswordDialog(botPasswordDialog.value);
|
|
||||||
}}
|
|
||||||
placeholder={isZh ? '输入 Bot 访问密码' : 'Enter bot access password'}
|
|
||||||
/>
|
|
||||||
<div className="row-between">
|
|
||||||
<button className="btn btn-secondary" onClick={() => closeBotPasswordDialog(null)}>
|
|
||||||
{isZh ? '取消' : 'Cancel'}
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-primary" onClick={() => closeBotPasswordDialog(botPasswordDialog.value)}>
|
|
||||||
{isZh ? '确认' : 'Confirm'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { appendPanelAccessPassword } from './panelAccess';
|
import { appendPanelAccessPassword } from './panelAccess';
|
||||||
|
|
||||||
const BOT_PASSWORD_HEADER = 'X-Bot-Password';
|
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
const memoryMap = new Map<string, string>();
|
const memoryMap = new Map<string, string>();
|
||||||
|
|
||||||
|
|
@ -63,41 +61,17 @@ export function clearAllBotAccessPasswords(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isBotUnauthorizedError(error: any, botId?: string): boolean {
|
export function isBotUnauthorizedError(error: any, botId?: string): boolean {
|
||||||
if (!axios.isAxiosError(error)) return false;
|
void error;
|
||||||
if (Number(error.response?.status) !== 401) return false;
|
void botId;
|
||||||
const detail = String(error.response?.data?.detail || '').trim().toLowerCase();
|
return false;
|
||||||
if (!detail.includes('bot access password')) return false;
|
|
||||||
if (!botId) return true;
|
|
||||||
|
|
||||||
const fromConfig = extractBotIdFromApiPath(String(error.config?.url || ''));
|
|
||||||
const fromRequest = extractBotIdFromApiPath(String(error.request?.responseURL || ''));
|
|
||||||
const expected = normalizeBotId(botId);
|
|
||||||
return expected === fromConfig || expected === fromRequest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMonitorWsUrl(base: string, botId: string): string {
|
export function buildMonitorWsUrl(base: string, botId: string): string {
|
||||||
const target = appendPanelAccessPassword(`${String(base || '').replace(/\/$/, '')}/${encodeURIComponent(botId)}`);
|
return appendPanelAccessPassword(`${String(base || '').replace(/\/$/, '')}/${encodeURIComponent(botId)}`);
|
||||||
const password = getBotAccessPassword(botId);
|
|
||||||
if (!password) return target;
|
|
||||||
const joiner = target.includes('?') ? '&' : '?';
|
|
||||||
return `${target}${joiner}access_password=${encodeURIComponent(password)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupBotAccessAuth(): void {
|
export function setupBotAccessAuth(): void {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
void axios;
|
||||||
axios.interceptors.request.use((config) => {
|
|
||||||
const botId = extractBotIdFromApiPath(String(config.url || ''));
|
|
||||||
if (!botId) return config;
|
|
||||||
const password = getBotAccessPassword(botId);
|
|
||||||
if (!password) return config;
|
|
||||||
|
|
||||||
const headers = config.headers || {};
|
|
||||||
if (!(BOT_PASSWORD_HEADER in (headers as Record<string, unknown>))) {
|
|
||||||
(headers as Record<string, string>)[BOT_PASSWORD_HEADER] = password;
|
|
||||||
config.headers = headers;
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue