v0.1.4
parent
85ff2fd17a
commit
c67c6c3e6c
|
|
@ -292,7 +292,6 @@ class WSConnectionManager:
|
|||
|
||||
manager = WSConnectionManager()
|
||||
|
||||
BOT_ACCESS_PASSWORD_HEADER = "x-bot-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
|
||||
|
||||
|
||||
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:
|
||||
header_value = str(request.headers.get(PANEL_ACCESS_PASSWORD_HEADER) or "").strip()
|
||||
if header_value:
|
||||
|
|
@ -341,8 +332,9 @@ def _validate_panel_access_password(supplied: str) -> Optional[str]:
|
|||
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()
|
||||
verb = str(method or "GET").strip().upper()
|
||||
if not raw.startswith("/api/"):
|
||||
return False
|
||||
if raw in {
|
||||
|
|
@ -352,21 +344,28 @@ def _is_panel_protected_api_path(path: str) -> bool:
|
|||
"/api/health/cache",
|
||||
}:
|
||||
return False
|
||||
if _is_bot_panel_management_api_path(raw):
|
||||
if _is_bot_panel_management_api_path(raw, verb):
|
||||
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):
|
||||
return False
|
||||
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()
|
||||
verb = str(method or "GET").strip().upper()
|
||||
if not raw.startswith("/api/bots/"):
|
||||
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 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")
|
||||
|
|
@ -374,7 +373,7 @@ async def bot_access_password_guard(request: Request, call_next):
|
|||
if request.method.upper() == "OPTIONS":
|
||||
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))
|
||||
if 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)
|
||||
if not bot:
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -2721,17 +2712,6 @@ async def websocket_endpoint(websocket: WebSocket, bot_id: str):
|
|||
if not bot:
|
||||
await websocket.close(code=4404, reason="Bot not found")
|
||||
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)
|
||||
docker_manager.ensure_monitor(bot_id, docker_callback)
|
||||
|
|
|
|||
|
|
@ -25,26 +25,45 @@ function AuthenticatedApp({
|
|||
const { theme, setTheme, locale, setLocale, activeBots } = useAppStore();
|
||||
const [showImageFactory, setShowImageFactory] = useState(false);
|
||||
const [showCreateWizard, setShowCreateWizard] = useState(false);
|
||||
const [singleBotPassword, setSingleBotPassword] = useState('');
|
||||
const [singleBotPasswordError, setSingleBotPasswordError] = useState('');
|
||||
const [singleBotUnlocked, setSingleBotUnlocked] = useState(false);
|
||||
useBotsSync(forcedBotId);
|
||||
const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn });
|
||||
const isSingleBotCompactView = compactMode && Boolean(String(forcedBotId || '').trim());
|
||||
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(() => {
|
||||
const forced = String(forcedBotId || '').trim();
|
||||
if (!forced) {
|
||||
document.title = t.title;
|
||||
return;
|
||||
}
|
||||
const bot = activeBots[forced];
|
||||
const botName = String(bot?.name || '').trim();
|
||||
const botName = String(forcedBot?.name || '').trim();
|
||||
document.title = botName ? `${t.title} - ${botName}` : `${t.title} - ${forced}`;
|
||||
}, [activeBots, t.title, forcedBotId]);
|
||||
}, [forced, forcedBot?.name, t.title]);
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderCollapsed(isSingleBotCompactView);
|
||||
}, [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 (
|
||||
<div className={`app-shell ${compactMode ? 'app-shell-compact' : ''}`} data-theme={theme}>
|
||||
<div className="app-frame">
|
||||
|
|
@ -180,6 +199,36 @@ function AuthenticatedApp({
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -217,8 +266,15 @@ function PanelLoginGate({
|
|||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const bypassPanelGate = Boolean(String(urlView.forcedBotId || '').trim());
|
||||
|
||||
useEffect(() => {
|
||||
if (bypassPanelGate) {
|
||||
setRequired(false);
|
||||
setAuthenticated(true);
|
||||
setChecking(false);
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
const boot = async () => {
|
||||
try {
|
||||
|
|
@ -259,7 +315,7 @@ function PanelLoginGate({
|
|||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [locale]);
|
||||
}, [bypassPanelGate, locale]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
const next = String(password || '').trim();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { normalizeAssistantMessageText, normalizeUserMessageText, summarizeProgr
|
|||
import { pickLocale } from '../i18n';
|
||||
import { botsSyncZhCn } from '../i18n/bots-sync.zh-cn';
|
||||
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' {
|
||||
const s = (v || '').toUpperCase();
|
||||
|
|
@ -122,8 +122,6 @@ export function useBotsSync(forcedBotId?: string) {
|
|||
|
||||
botIds.forEach((botId) => {
|
||||
if (hydratedMessagesRef.current[botId]) return;
|
||||
const bot = activeBots[botId];
|
||||
if (bot?.has_access_password && !getBotAccessPassword(botId)) return;
|
||||
hydratedMessagesRef.current[botId] = true;
|
||||
void (async () => {
|
||||
try {
|
||||
|
|
@ -177,9 +175,6 @@ export function useBotsSync(forcedBotId?: string) {
|
|||
if (bot.docker_status !== 'RUNNING') {
|
||||
return;
|
||||
}
|
||||
if (bot.has_access_password && !getBotAccessPassword(bot.id)) {
|
||||
return;
|
||||
}
|
||||
if (socketsRef.current[bot.id]) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { dashboardZhCn } from '../../i18n/dashboard.zh-cn';
|
|||
import { dashboardEn } from '../../i18n/dashboard.en';
|
||||
import { useLucentPrompt } from '../../components/lucent/LucentPromptProvider';
|
||||
import { LucentIconButton } from '../../components/lucent/LucentIconButton';
|
||||
import { clearBotAccessPassword, getBotAccessPassword, isBotUnauthorizedError, setBotAccessPassword } from '../../utils/botAccess';
|
||||
|
||||
interface BotDashboardModuleProps {
|
||||
onOpenCreateWizard?: () => void;
|
||||
|
|
@ -649,6 +648,8 @@ export function BotDashboardModule({
|
|||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const botOrderRef = useRef<Record<string, number>>({});
|
||||
const nextBotOrderRef = useRef(1);
|
||||
const applyEditFormFromBot = useCallback((bot?: any) => {
|
||||
if (!bot) return;
|
||||
setProviderTestResult('');
|
||||
|
|
@ -914,13 +915,37 @@ export function BotDashboardModule({
|
|||
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(
|
||||
() =>
|
||||
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 || ''));
|
||||
const aId = String(a.id || '').trim();
|
||||
const bId = String(b.id || '').trim();
|
||||
const aOrder = botOrderRef.current[aId] ?? Number.MAX_SAFE_INTEGER;
|
||||
const bOrder = botOrderRef.current[bId] ?? Number.MAX_SAFE_INTEGER;
|
||||
if (aOrder !== bOrder) return aOrder - bOrder;
|
||||
return aId.localeCompare(bId);
|
||||
}),
|
||||
[activeBots],
|
||||
);
|
||||
|
|
@ -947,97 +972,6 @@ export function BotDashboardModule({
|
|||
const noteLocale = pickLocale(locale, { 'zh-cn': 'zh-cn' as const, en: 'en' as const });
|
||||
const t = pickLocale(locale, { 'zh-cn': dashboardZhCn, en: dashboardEn });
|
||||
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 readyTags = new Set(
|
||||
availableImages
|
||||
|
|
@ -2447,8 +2381,7 @@ export function BotDashboardModule({
|
|||
let cancelled = false;
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
const granted = await ensureBotAccess(selectedBotId);
|
||||
if (!granted || cancelled) return;
|
||||
if (cancelled) return;
|
||||
await Promise.all([
|
||||
loadWorkspaceTree(selectedBotId, ''),
|
||||
loadCronJobs(selectedBotId),
|
||||
|
|
@ -2457,15 +2390,6 @@ export function BotDashboardModule({
|
|||
]);
|
||||
} catch (error: any) {
|
||||
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) {
|
||||
notify(detail, { tone: 'error' });
|
||||
}
|
||||
|
|
@ -2584,14 +2508,6 @@ export function BotDashboardModule({
|
|||
}
|
||||
|
||||
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();
|
||||
setShowBaseModal(false);
|
||||
setShowParamModal(false);
|
||||
|
|
@ -4214,52 +4130,6 @@ export function BotDashboardModule({
|
|||
</div>
|
||||
</div>
|
||||
) : 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 { appendPanelAccessPassword } from './panelAccess';
|
||||
|
||||
const BOT_PASSWORD_HEADER = 'X-Bot-Password';
|
||||
|
||||
let initialized = false;
|
||||
const memoryMap = new Map<string, string>();
|
||||
|
||||
|
|
@ -63,41 +61,17 @@ export function clearAllBotAccessPasswords(): void {
|
|||
}
|
||||
|
||||
export function isBotUnauthorizedError(error: any, botId?: string): boolean {
|
||||
if (!axios.isAxiosError(error)) return false;
|
||||
if (Number(error.response?.status) !== 401) return false;
|
||||
const detail = String(error.response?.data?.detail || '').trim().toLowerCase();
|
||||
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;
|
||||
void error;
|
||||
void botId;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function buildMonitorWsUrl(base: string, botId: string): string {
|
||||
const target = 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)}`;
|
||||
return appendPanelAccessPassword(`${String(base || '').replace(/\/$/, '')}/${encodeURIComponent(botId)}`);
|
||||
}
|
||||
|
||||
export function setupBotAccessAuth(): void {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
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;
|
||||
});
|
||||
void axios;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue