v 0.1.3
parent
92db4da304
commit
6e9eba0e55
|
|
@ -16,6 +16,7 @@ def _ensure_botinstance_columns() -> None:
|
||||||
"current_state": "TEXT DEFAULT 'IDLE'",
|
"current_state": "TEXT DEFAULT 'IDLE'",
|
||||||
"last_action": "TEXT",
|
"last_action": "TEXT",
|
||||||
"image_tag": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
"image_tag": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
||||||
|
"access_password": "TEXT DEFAULT ''",
|
||||||
}
|
}
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
||||||
|
|
|
||||||
125
backend/main.py
125
backend/main.py
|
|
@ -8,11 +8,12 @@ import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile, WebSocket, WebSocketDisconnect
|
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse, JSONResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
|
|
@ -72,6 +73,7 @@ class ChannelConfigUpdateRequest(BaseModel):
|
||||||
class BotCreateRequest(BaseModel):
|
class BotCreateRequest(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
access_password: Optional[str] = None
|
||||||
llm_provider: str
|
llm_provider: str
|
||||||
llm_model: str
|
llm_model: str
|
||||||
api_key: str
|
api_key: str
|
||||||
|
|
@ -98,6 +100,7 @@ class BotCreateRequest(BaseModel):
|
||||||
|
|
||||||
class BotUpdateRequest(BaseModel):
|
class BotUpdateRequest(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
access_password: Optional[str] = None
|
||||||
llm_provider: Optional[str] = None
|
llm_provider: Optional[str] = None
|
||||||
llm_model: Optional[str] = None
|
llm_model: Optional[str] = None
|
||||||
api_key: Optional[str] = None
|
api_key: Optional[str] = None
|
||||||
|
|
@ -276,6 +279,57 @@ class WSConnectionManager:
|
||||||
|
|
||||||
manager = WSConnectionManager()
|
manager = WSConnectionManager()
|
||||||
|
|
||||||
|
BOT_ACCESS_PASSWORD_HEADER = "x-bot-password"
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_bot_id_from_api_path(path: str) -> Optional[str]:
|
||||||
|
raw = str(path or "").strip()
|
||||||
|
if not raw.startswith("/api/bots/"):
|
||||||
|
return None
|
||||||
|
rest = raw[len("/api/bots/") :]
|
||||||
|
if not rest:
|
||||||
|
return None
|
||||||
|
bot_id_segment = rest.split("/", 1)[0].strip()
|
||||||
|
if not bot_id_segment:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
decoded = unquote(bot_id_segment)
|
||||||
|
except Exception:
|
||||||
|
decoded = bot_id_segment
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def bot_access_password_guard(request: Request, call_next):
|
||||||
|
if request.method.upper() == "OPTIONS":
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
bot_id = _extract_bot_id_from_api_path(request.url.path)
|
||||||
|
if not bot_id:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def docker_callback(bot_id: str, packet: Dict[str, Any]):
|
def docker_callback(bot_id: str, packet: Dict[str, Any]):
|
||||||
_persist_runtime_packet(bot_id, packet)
|
_persist_runtime_packet(bot_id, packet)
|
||||||
|
|
@ -821,6 +875,8 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"id": bot.id,
|
"id": bot.id,
|
||||||
"name": bot.name,
|
"name": bot.name,
|
||||||
|
"access_password": bot.access_password or "",
|
||||||
|
"has_access_password": bool(str(bot.access_password or "").strip()),
|
||||||
"avatar_model": "base",
|
"avatar_model": "base",
|
||||||
"avatar_skin": "blue_suit",
|
"avatar_skin": "blue_suit",
|
||||||
"image_tag": bot.image_tag,
|
"image_tag": bot.image_tag,
|
||||||
|
|
@ -1039,6 +1095,30 @@ def _env_store_path(bot_id: str) -> str:
|
||||||
return os.path.join(_bot_data_root(bot_id), "env.json")
|
return os.path.join(_bot_data_root(bot_id), "env.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _sessions_root(bot_id: str) -> str:
|
||||||
|
return os.path.join(_workspace_root(bot_id), "sessions")
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_bot_sessions(bot_id: str) -> int:
|
||||||
|
"""Remove persisted session files for the bot workspace."""
|
||||||
|
root = _sessions_root(bot_id)
|
||||||
|
if not os.path.isdir(root):
|
||||||
|
return 0
|
||||||
|
deleted = 0
|
||||||
|
for name in os.listdir(root):
|
||||||
|
path = os.path.join(root, name)
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
continue
|
||||||
|
if not name.lower().endswith(".jsonl"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
deleted += 1
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
def _read_env_store(bot_id: str) -> Dict[str, str]:
|
def _read_env_store(bot_id: str) -> Dict[str, str]:
|
||||||
path = _env_store_path(bot_id)
|
path = _env_store_path(bot_id)
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
|
|
@ -1134,6 +1214,16 @@ def _is_visual_attachment_path(path: str) -> bool:
|
||||||
return _is_image_attachment_path(path) or _is_video_attachment_path(path)
|
return _is_image_attachment_path(path) or _is_video_attachment_path(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _workspace_stat_ctime_iso(stat: os.stat_result) -> str:
|
||||||
|
ts = getattr(stat, "st_birthtime", None)
|
||||||
|
if ts is None:
|
||||||
|
ts = getattr(stat, "st_ctime", None)
|
||||||
|
try:
|
||||||
|
return datetime.utcfromtimestamp(float(ts)).isoformat() + "Z"
|
||||||
|
except Exception:
|
||||||
|
return datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z"
|
||||||
|
|
||||||
|
|
||||||
def _build_workspace_tree(path: str, root: str, depth: int) -> List[Dict[str, Any]]:
|
def _build_workspace_tree(path: str, root: str, depth: int) -> List[Dict[str, Any]]:
|
||||||
rows: List[Dict[str, Any]] = []
|
rows: List[Dict[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
|
|
@ -1150,6 +1240,7 @@ def _build_workspace_tree(path: str, root: str, depth: int) -> List[Dict[str, An
|
||||||
base: Dict[str, Any] = {
|
base: Dict[str, Any] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"path": rel_path,
|
"path": rel_path,
|
||||||
|
"ctime": _workspace_stat_ctime_iso(stat),
|
||||||
"mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z",
|
"mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z",
|
||||||
}
|
}
|
||||||
if os.path.isdir(abs_path):
|
if os.path.isdir(abs_path):
|
||||||
|
|
@ -1185,6 +1276,7 @@ def _list_workspace_dir(path: str, root: str) -> List[Dict[str, Any]]:
|
||||||
"type": "dir" if os.path.isdir(abs_path) else "file",
|
"type": "dir" if os.path.isdir(abs_path) else "file",
|
||||||
"size": stat.st_size if os.path.isfile(abs_path) else None,
|
"size": stat.st_size if os.path.isfile(abs_path) else None,
|
||||||
"ext": os.path.splitext(name)[1].lower() if os.path.isfile(abs_path) else "",
|
"ext": os.path.splitext(name)[1].lower() if os.path.isfile(abs_path) else "",
|
||||||
|
"ctime": _workspace_stat_ctime_iso(stat),
|
||||||
"mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z",
|
"mtime": datetime.utcfromtimestamp(stat.st_mtime).isoformat() + "Z",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -1331,6 +1423,7 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
||||||
bot = BotInstance(
|
bot = BotInstance(
|
||||||
id=payload.id,
|
id=payload.id,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
|
access_password=str(payload.access_password or ""),
|
||||||
image_tag=payload.image_tag,
|
image_tag=payload.image_tag,
|
||||||
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, payload.id),
|
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, payload.id),
|
||||||
)
|
)
|
||||||
|
|
@ -1497,7 +1590,7 @@ def update_bot(bot_id: str, payload: BotUpdateRequest, session: Session = Depend
|
||||||
)
|
)
|
||||||
runtime_overrides.update(normalized_resources)
|
runtime_overrides.update(normalized_resources)
|
||||||
|
|
||||||
db_fields = {"name", "image_tag"}
|
db_fields = {"name", "image_tag", "access_password"}
|
||||||
for key, value in update_data.items():
|
for key, value in update_data.items():
|
||||||
if key in db_fields:
|
if key in db_fields:
|
||||||
setattr(bot, key, value)
|
setattr(bot, key, value)
|
||||||
|
|
@ -2128,12 +2221,19 @@ def clear_bot_messages(bot_id: str, session: Session = Depends(get_session)):
|
||||||
for row in rows:
|
for row in rows:
|
||||||
session.delete(row)
|
session.delete(row)
|
||||||
deleted += 1
|
deleted += 1
|
||||||
|
cleared_sessions = _clear_bot_sessions(bot_id)
|
||||||
|
# Also reset in-memory session cache for running bot process.
|
||||||
|
if str(bot.docker_status or "").upper() == "RUNNING":
|
||||||
|
try:
|
||||||
|
docker_manager.send_command(bot_id, "/new")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
bot.last_action = ""
|
bot.last_action = ""
|
||||||
bot.current_state = "IDLE"
|
bot.current_state = "IDLE"
|
||||||
bot.updated_at = datetime.utcnow()
|
bot.updated_at = datetime.utcnow()
|
||||||
session.add(bot)
|
session.add(bot)
|
||||||
session.commit()
|
session.commit()
|
||||||
return {"bot_id": bot_id, "deleted": deleted}
|
return {"bot_id": bot_id, "deleted": deleted, "cleared_sessions": cleared_sessions}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/bots/{bot_id}/logs")
|
@app.get("/api/bots/{bot_id}/logs")
|
||||||
|
|
@ -2382,6 +2482,23 @@ async def upload_workspace_files(
|
||||||
|
|
||||||
@app.websocket("/ws/monitor/{bot_id}")
|
@app.websocket("/ws/monitor/{bot_id}")
|
||||||
async def websocket_endpoint(websocket: WebSocket, bot_id: str):
|
async def websocket_endpoint(websocket: WebSocket, bot_id: str):
|
||||||
|
with Session(engine) as session:
|
||||||
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
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)
|
await manager.connect(bot_id, websocket)
|
||||||
docker_manager.ensure_monitor(bot_id, docker_callback)
|
docker_manager.ensure_monitor(bot_id, docker_callback)
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from datetime import datetime
|
||||||
class BotInstance(SQLModel, table=True):
|
class BotInstance(SQLModel, table=True):
|
||||||
id: str = Field(primary_key=True)
|
id: str = Field(primary_key=True)
|
||||||
name: str
|
name: str
|
||||||
|
access_password: str = Field(default="")
|
||||||
workspace_dir: str = Field(unique=True)
|
workspace_dir: str = Field(unique=True)
|
||||||
docker_status: str = Field(default="STOPPED", index=True)
|
docker_status: str = Field(default="STOPPED", index=True)
|
||||||
current_state: Optional[str] = Field(default="IDLE")
|
current_state: Optional[str] = Field(default="IDLE")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +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';
|
||||||
|
|
||||||
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();
|
||||||
|
|
@ -56,6 +57,24 @@ function isLikelyEchoOfUserInput(progressText: string, userText: string): boolea
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractToolCallProgressHint(raw: string, isZh: boolean): string | null {
|
||||||
|
const text = String(raw || '').replace(/<\/?tool_call>/gi, '').trim();
|
||||||
|
if (!text) return null;
|
||||||
|
const hasToolCallSignal =
|
||||||
|
/"name"\s*:/.test(text) && /"arguments"\s*:/.test(text);
|
||||||
|
if (!hasToolCallSignal) return null;
|
||||||
|
|
||||||
|
const nameMatch = text.match(/"name"\s*:\s*"([^"]+)"/);
|
||||||
|
const toolName = String(nameMatch?.[1] || '').trim();
|
||||||
|
if (!toolName) return null;
|
||||||
|
|
||||||
|
const queryMatch = text.match(/"query"\s*:\s*"([^"]+)"/);
|
||||||
|
const pathMatch = text.match(/"path"\s*:\s*"([^"]+)"/);
|
||||||
|
const target = String(queryMatch?.[1] || pathMatch?.[1] || '').trim();
|
||||||
|
const callLabel = target ? `${toolName}("${target.slice(0, 80)}${target.length > 80 ? '…' : ''}")` : toolName;
|
||||||
|
return `${isZh ? '工具调用' : 'Tool Call'}\n${callLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function useBotsSync() {
|
export function useBotsSync() {
|
||||||
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>>({});
|
||||||
|
|
@ -97,6 +116,8 @@ export function useBotsSync() {
|
||||||
|
|
||||||
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 {
|
||||||
|
|
@ -150,11 +171,14 @@ export function useBotsSync() {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ws = new WebSocket(`${APP_ENDPOINTS.wsBase}/${bot.id}`);
|
const ws = new WebSocket(buildMonitorWsUrl(APP_ENDPOINTS.wsBase, bot.id));
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
const beat = window.setInterval(() => {
|
const beat = window.setInterval(() => {
|
||||||
if (ws.readyState === WebSocket.OPEN) {
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
|
@ -220,11 +244,12 @@ export function useBotsSync() {
|
||||||
if (data.type === 'BUS_EVENT') {
|
if (data.type === 'BUS_EVENT') {
|
||||||
const content = normalizeAssistantMessageText(String(data.content || payload.content || ''));
|
const content = normalizeAssistantMessageText(String(data.content || payload.content || ''));
|
||||||
const isProgress = Boolean(data.is_progress);
|
const isProgress = Boolean(data.is_progress);
|
||||||
const isTool = Boolean(data.is_tool);
|
const toolHintFromText = extractToolCallProgressHint(content, isZh);
|
||||||
|
const isTool = Boolean(data.is_tool) || Boolean(toolHintFromText);
|
||||||
if (isProgress) {
|
if (isProgress) {
|
||||||
const state = normalizeBusState(isTool);
|
const state = normalizeBusState(isTool);
|
||||||
const progressText = summarizeProgressText(content, isZh);
|
const progressText = summarizeProgressText(content, isZh);
|
||||||
const fullProgress = content || progressText || (isZh ? '处理中...' : 'Processing...');
|
const fullProgress = toolHintFromText || content || progressText || (isZh ? '处理中...' : 'Processing...');
|
||||||
updateBotState(bot.id, state, fullProgress);
|
updateBotState(bot.id, state, fullProgress);
|
||||||
addBotEvent(bot.id, { state, text: fullProgress || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
addBotEvent(bot.id, { state, text: fullProgress || t.progress, ts: Date.now(), channel: sourceChannel || undefined });
|
||||||
if (isDashboardChannel) {
|
if (isDashboardChannel) {
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const dashboardEn = {
|
||||||
selectBot: 'Select a bot to inspect',
|
selectBot: 'Select a bot to inspect',
|
||||||
runtime: 'Runtime Status',
|
runtime: 'Runtime Status',
|
||||||
base: 'Base',
|
base: 'Base',
|
||||||
params: 'Params',
|
params: 'Model',
|
||||||
channels: 'Channels',
|
channels: 'Channels',
|
||||||
skills: 'Skills',
|
skills: 'Skills',
|
||||||
tools: 'Tools',
|
tools: 'Tools',
|
||||||
|
|
@ -114,7 +114,9 @@ export const dashboardEn = {
|
||||||
previewTitle: 'Click to preview',
|
previewTitle: 'Click to preview',
|
||||||
fileNotPreviewable: 'File type not previewable',
|
fileNotPreviewable: 'File type not previewable',
|
||||||
baseConfig: 'Base Configuration',
|
baseConfig: 'Base Configuration',
|
||||||
baseConfigSub: 'Only editable: name and LLM access settings',
|
baseConfigSub: 'Edit name, access password, and resource limits',
|
||||||
|
accessPassword: 'Access Password',
|
||||||
|
accessPasswordPlaceholder: 'Leave blank to disable password',
|
||||||
botIdReadonly: 'Bot ID (Read-only)',
|
botIdReadonly: 'Bot ID (Read-only)',
|
||||||
botName: 'Bot Name',
|
botName: 'Bot Name',
|
||||||
botNamePlaceholder: 'Bot name',
|
botNamePlaceholder: 'Bot name',
|
||||||
|
|
@ -127,7 +129,7 @@ export const dashboardEn = {
|
||||||
testModelConnection: 'Test model connection',
|
testModelConnection: 'Test model connection',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
modelParams: 'Model Parameters',
|
modelParams: 'Model',
|
||||||
saveParams: 'Save Params',
|
saveParams: 'Save Params',
|
||||||
agentFiles: 'Agent Files',
|
agentFiles: 'Agent Files',
|
||||||
saveFiles: 'Save Files',
|
saveFiles: 'Save Files',
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const dashboardZhCn = {
|
||||||
selectBot: '请选择 Bot 查看详情',
|
selectBot: '请选择 Bot 查看详情',
|
||||||
runtime: '运行状态',
|
runtime: '运行状态',
|
||||||
base: '基础',
|
base: '基础',
|
||||||
params: '参数',
|
params: '模型',
|
||||||
channels: '渠道',
|
channels: '渠道',
|
||||||
skills: '技能',
|
skills: '技能',
|
||||||
tools: '工具',
|
tools: '工具',
|
||||||
|
|
@ -114,7 +114,9 @@ export const dashboardZhCn = {
|
||||||
previewTitle: '点击打开预览',
|
previewTitle: '点击打开预览',
|
||||||
fileNotPreviewable: '该文件类型不可预览',
|
fileNotPreviewable: '该文件类型不可预览',
|
||||||
baseConfig: '基础配置',
|
baseConfig: '基础配置',
|
||||||
baseConfigSub: '仅允许修改名称和大模型接入信息',
|
baseConfigSub: '可修改名称、访问密码与资源配额',
|
||||||
|
accessPassword: '访问密码',
|
||||||
|
accessPasswordPlaceholder: '留空表示不设置访问密码',
|
||||||
botIdReadonly: 'Bot ID(只读)',
|
botIdReadonly: 'Bot ID(只读)',
|
||||||
botName: 'Bot 名称',
|
botName: 'Bot 名称',
|
||||||
botNamePlaceholder: '输入 Bot 名称',
|
botNamePlaceholder: '输入 Bot 名称',
|
||||||
|
|
@ -127,7 +129,7 @@ export const dashboardZhCn = {
|
||||||
testModelConnection: '测试模型连接',
|
testModelConnection: '测试模型连接',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
modelParams: '模型参数设置',
|
modelParams: '模型',
|
||||||
saveParams: '保存参数',
|
saveParams: '保存参数',
|
||||||
agentFiles: '代理文件配置',
|
agentFiles: '代理文件配置',
|
||||||
saveFiles: '保存文件',
|
saveFiles: '保存文件',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ export const wizardEn = {
|
||||||
loadImages: 'Load images',
|
loadImages: 'Load images',
|
||||||
noReady: 'No READY image.',
|
noReady: 'No READY image.',
|
||||||
baseInfo: 'Base Info',
|
baseInfo: 'Base Info',
|
||||||
|
accessPassword: 'Access Password',
|
||||||
|
accessPasswordPlaceholder: 'Access password (optional)',
|
||||||
botIdPlaceholder: 'Bot ID',
|
botIdPlaceholder: 'Bot ID',
|
||||||
botName: 'Bot Name',
|
botName: 'Bot Name',
|
||||||
modelAccess: 'Model Access',
|
modelAccess: 'Model Access',
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ export const wizardZhCn = {
|
||||||
loadImages: '加载镜像列表',
|
loadImages: '加载镜像列表',
|
||||||
noReady: '暂无 READY 镜像。',
|
noReady: '暂无 READY 镜像。',
|
||||||
baseInfo: '基础信息',
|
baseInfo: '基础信息',
|
||||||
|
accessPassword: '访问密码',
|
||||||
|
accessPasswordPlaceholder: '访问密码(可选)',
|
||||||
botIdPlaceholder: 'Bot ID(如 analyst_bot_01)',
|
botIdPlaceholder: 'Bot ID(如 analyst_bot_01)',
|
||||||
botName: 'Bot 名称',
|
botName: 'Bot 名称',
|
||||||
modelAccess: '模型接入',
|
modelAccess: '模型接入',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { LucentPromptProvider } from './components/lucent/LucentPromptProvider.tsx'
|
import { LucentPromptProvider } from './components/lucent/LucentPromptProvider.tsx'
|
||||||
|
import { setupBotAccessAuth } from './utils/botAccess.ts'
|
||||||
|
|
||||||
|
setupBotAccessAuth();
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
|
||||||
|
|
@ -1155,6 +1155,18 @@
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-skills-list-scroll {
|
||||||
|
max-height: min(56vh, 520px);
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-cron-list-scroll {
|
||||||
|
max-height: min(58vh, 560px);
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-cron-item {
|
.ops-cron-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
|
|
@ -1352,6 +1364,18 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-path {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-toolbar-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-refresh-icon-btn {
|
.workspace-refresh-icon-btn {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|
@ -1499,6 +1523,45 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-hover-panel {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 140;
|
||||||
|
width: min(420px, calc(100vw - 16px));
|
||||||
|
border: 1px solid color-mix(in oklab, var(--line) 70%, var(--brand) 30%);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: color-mix(in oklab, var(--panel) 90%, #000 10%);
|
||||||
|
box-shadow: 0 12px 26px rgba(7, 13, 26, 0.28);
|
||||||
|
padding: 8px 10px;
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-hover-panel.is-above {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry-info-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry-info-label {
|
||||||
|
color: var(--subtitle);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry-info-value {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-hint {
|
.workspace-hint {
|
||||||
background: var(--panel-soft);
|
background: var(--panel-soft);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ 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;
|
||||||
|
|
@ -38,10 +39,18 @@ interface WorkspaceNode {
|
||||||
type: WorkspaceNodeType;
|
type: WorkspaceNodeType;
|
||||||
size?: number;
|
size?: number;
|
||||||
ext?: string;
|
ext?: string;
|
||||||
|
ctime?: string;
|
||||||
mtime?: string;
|
mtime?: string;
|
||||||
children?: WorkspaceNode[];
|
children?: WorkspaceNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WorkspaceHoverCardState {
|
||||||
|
node: WorkspaceNode;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
above: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface WorkspaceTreeResponse {
|
interface WorkspaceTreeResponse {
|
||||||
bot_id: string;
|
bot_id: string;
|
||||||
root: string;
|
root: string;
|
||||||
|
|
@ -449,6 +458,26 @@ function formatPercent(value: number): string {
|
||||||
return `${Math.max(0, n).toFixed(2)}%`;
|
return `${Math.max(0, n).toFixed(2)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatWorkspaceTime(raw: string | undefined, isZh: boolean): string {
|
||||||
|
const text = String(raw || '').trim();
|
||||||
|
if (!text) return '-';
|
||||||
|
const dt = new Date(text);
|
||||||
|
if (Number.isNaN(dt.getTime())) return '-';
|
||||||
|
try {
|
||||||
|
return dt.toLocaleString(isZh ? 'zh-CN' : 'en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return dt.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatCronSchedule(job: CronJob, isZh: boolean) {
|
function formatCronSchedule(job: CronJob, isZh: boolean) {
|
||||||
const s = job.schedule || {};
|
const s = job.schedule || {};
|
||||||
if (s.kind === 'every' && Number(s.everyMs) > 0) {
|
if (s.kind === 'every' && Number(s.everyMs) > 0) {
|
||||||
|
|
@ -512,7 +541,7 @@ export function BotDashboardModule({
|
||||||
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
const [workspaceFileLoading, setWorkspaceFileLoading] = useState(false);
|
||||||
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
const [workspacePreview, setWorkspacePreview] = useState<WorkspacePreviewState | null>(null);
|
||||||
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
const [workspacePreviewFullscreen, setWorkspacePreviewFullscreen] = useState(false);
|
||||||
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(true);
|
const [workspaceAutoRefresh, setWorkspaceAutoRefresh] = useState(false);
|
||||||
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
const [pendingAttachments, setPendingAttachments] = useState<string[]>([]);
|
||||||
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
const [isUploadingAttachments, setIsUploadingAttachments] = useState(false);
|
||||||
const filePickerRef = useRef<HTMLInputElement | null>(null);
|
const filePickerRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
@ -545,9 +574,15 @@ export function BotDashboardModule({
|
||||||
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
const [expandedProgressByKey, setExpandedProgressByKey] = useState<Record<string, boolean>>({});
|
||||||
const [feedbackSavingByMessageId, setFeedbackSavingByMessageId] = useState<Record<number, boolean>>({});
|
const [feedbackSavingByMessageId, setFeedbackSavingByMessageId] = useState<Record<number, boolean>>({});
|
||||||
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
const [showRuntimeActionModal, setShowRuntimeActionModal] = useState(false);
|
||||||
|
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||||
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
const runtimeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) =>
|
const buildWorkspaceDownloadHref = (filePath: string, forceDownload: boolean = true) => {
|
||||||
`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(filePath)}${forceDownload ? '&download=1' : ''}`;
|
const accessPassword = selectedBotId ? getBotAccessPassword(selectedBotId) : '';
|
||||||
|
const query = [`path=${encodeURIComponent(filePath)}`];
|
||||||
|
if (forceDownload) query.push('download=1');
|
||||||
|
if (accessPassword) query.push(`access_password=${encodeURIComponent(accessPassword)}`);
|
||||||
|
return `${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?${query.join('&')}`;
|
||||||
|
};
|
||||||
const closeWorkspacePreview = () => {
|
const closeWorkspacePreview = () => {
|
||||||
setWorkspacePreview(null);
|
setWorkspacePreview(null);
|
||||||
setWorkspacePreviewFullscreen(false);
|
setWorkspacePreviewFullscreen(false);
|
||||||
|
|
@ -744,6 +779,7 @@ export function BotDashboardModule({
|
||||||
|
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
|
access_password: '',
|
||||||
llm_provider: '',
|
llm_provider: '',
|
||||||
llm_model: '',
|
llm_model: '',
|
||||||
image_tag: '',
|
image_tag: '',
|
||||||
|
|
@ -777,6 +813,77 @@ 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 promptForBotPassword = (botName: string, invalid: boolean): string | null => {
|
||||||
|
const title = invalid
|
||||||
|
? (isZh ? `访问密码错误:${botName}` : `Invalid access password: ${botName}`)
|
||||||
|
: (isZh ? `请输入访问密码:${botName}` : `Enter access password for ${botName}`);
|
||||||
|
const value = window.prompt(title, '');
|
||||||
|
if (value === null) return null;
|
||||||
|
const trimmed = String(value || '').trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
|
@ -901,6 +1008,23 @@ export function BotDashboardModule({
|
||||||
const runtimeActionDisplay = runtimeActionHasMore ? runtimeActionSummary : runtimeAction;
|
const runtimeActionDisplay = runtimeActionHasMore ? runtimeActionSummary : runtimeAction;
|
||||||
const resourceBot = useMemo(() => bots.find((b) => b.id === resourceBotId), [bots, resourceBotId]);
|
const resourceBot = useMemo(() => bots.find((b) => b.id === resourceBotId), [bots, resourceBotId]);
|
||||||
|
|
||||||
|
const hideWorkspaceHoverCard = () => setWorkspaceHoverCard(null);
|
||||||
|
|
||||||
|
const showWorkspaceHoverCard = (node: WorkspaceNode, anchor: HTMLElement) => {
|
||||||
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
const panelHeight = 160;
|
||||||
|
const panelWidth = 420;
|
||||||
|
const gap = 8;
|
||||||
|
const viewportPadding = 8;
|
||||||
|
const belowSpace = window.innerHeight - rect.bottom;
|
||||||
|
const aboveSpace = rect.top;
|
||||||
|
const above = belowSpace < panelHeight && aboveSpace > panelHeight;
|
||||||
|
const leftRaw = rect.left + 8;
|
||||||
|
const left = Math.max(viewportPadding, Math.min(leftRaw, window.innerWidth - panelWidth - viewportPadding));
|
||||||
|
const top = above ? rect.top - gap : rect.bottom + gap;
|
||||||
|
setWorkspaceHoverCard({ node, top, left, above });
|
||||||
|
};
|
||||||
|
|
||||||
const shouldCollapseProgress = (text: string) => {
|
const shouldCollapseProgress = (text: string) => {
|
||||||
const normalized = String(text || '').trim();
|
const normalized = String(text || '').trim();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
|
@ -1098,8 +1222,20 @@ export function BotDashboardModule({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpandedProgressByKey({});
|
setExpandedProgressByKey({});
|
||||||
setShowRuntimeActionModal(false);
|
setShowRuntimeActionModal(false);
|
||||||
|
setWorkspaceHoverCard(null);
|
||||||
}, [selectedBotId]);
|
}, [selectedBotId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceHoverCard) return;
|
||||||
|
const close = () => setWorkspaceHoverCard(null);
|
||||||
|
window.addEventListener('scroll', close, true);
|
||||||
|
window.addEventListener('resize', close);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', close, true);
|
||||||
|
window.removeEventListener('resize', close);
|
||||||
|
};
|
||||||
|
}, [workspaceHoverCard]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
const loadSystemDefaults = async () => {
|
const loadSystemDefaults = async () => {
|
||||||
|
|
@ -1138,6 +1274,7 @@ export function BotDashboardModule({
|
||||||
setProviderTestResult('');
|
setProviderTestResult('');
|
||||||
setEditForm({
|
setEditForm({
|
||||||
name: bot.name || '',
|
name: bot.name || '',
|
||||||
|
access_password: bot.access_password || '',
|
||||||
llm_provider: bot.llm_provider || 'dashscope',
|
llm_provider: bot.llm_provider || 'dashscope',
|
||||||
llm_model: bot.llm_model || '',
|
llm_model: bot.llm_model || '',
|
||||||
image_tag: bot.image_tag || '',
|
image_tag: bot.image_tag || '',
|
||||||
|
|
@ -1913,10 +2050,37 @@ export function BotDashboardModule({
|
||||||
setEnvParams({});
|
setEnvParams({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void loadWorkspaceTree(selectedBotId, '');
|
let cancelled = false;
|
||||||
void loadCronJobs(selectedBotId);
|
const loadAll = async () => {
|
||||||
void loadBotSkills(selectedBotId);
|
try {
|
||||||
void loadBotEnvParams(selectedBotId);
|
const granted = await ensureBotAccess(selectedBotId);
|
||||||
|
if (!granted || cancelled) return;
|
||||||
|
await Promise.all([
|
||||||
|
loadWorkspaceTree(selectedBotId, ''),
|
||||||
|
loadCronJobs(selectedBotId),
|
||||||
|
loadBotSkills(selectedBotId),
|
||||||
|
loadBotEnvParams(selectedBotId),
|
||||||
|
]);
|
||||||
|
} 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
void loadAll();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [selectedBotId]);
|
}, [selectedBotId]);
|
||||||
|
|
||||||
|
|
@ -1952,9 +2116,7 @@ export function BotDashboardModule({
|
||||||
const payload: Record<string, string | number> = {};
|
const payload: Record<string, string | number> = {};
|
||||||
if (mode === 'base') {
|
if (mode === 'base') {
|
||||||
payload.name = editForm.name;
|
payload.name = editForm.name;
|
||||||
payload.llm_provider = editForm.llm_provider;
|
payload.access_password = editForm.access_password;
|
||||||
payload.llm_model = editForm.llm_model;
|
|
||||||
payload.api_base = editForm.api_base;
|
|
||||||
payload.image_tag = editForm.image_tag;
|
payload.image_tag = editForm.image_tag;
|
||||||
const selectedImageOption = baseImageOptions.find((opt) => opt.tag === editForm.image_tag);
|
const selectedImageOption = baseImageOptions.find((opt) => opt.tag === editForm.image_tag);
|
||||||
if (selectedImageOption?.disabled) {
|
if (selectedImageOption?.disabled) {
|
||||||
|
|
@ -1966,32 +2128,39 @@ export function BotDashboardModule({
|
||||||
source_dir: 'manual',
|
source_dir: 'manual',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (editForm.api_key.trim()) payload.api_key = editForm.api_key.trim();
|
|
||||||
}
|
|
||||||
if (mode === 'params') {
|
|
||||||
payload.temperature = clampTemperature(Number(editForm.temperature));
|
|
||||||
payload.top_p = Number(editForm.top_p);
|
|
||||||
const normalizedMaxTokens = clampMaxTokens(Number(paramDraft.max_tokens));
|
|
||||||
const normalizedCpuCores = clampCpuCores(Number(paramDraft.cpu_cores));
|
const normalizedCpuCores = clampCpuCores(Number(paramDraft.cpu_cores));
|
||||||
const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb));
|
const normalizedMemoryMb = clampMemoryMb(Number(paramDraft.memory_mb));
|
||||||
const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb));
|
const normalizedStorageGb = clampStorageGb(Number(paramDraft.storage_gb));
|
||||||
payload.max_tokens = normalizedMaxTokens;
|
|
||||||
payload.cpu_cores = normalizedCpuCores;
|
payload.cpu_cores = normalizedCpuCores;
|
||||||
payload.memory_mb = normalizedMemoryMb;
|
payload.memory_mb = normalizedMemoryMb;
|
||||||
payload.storage_gb = normalizedStorageGb;
|
payload.storage_gb = normalizedStorageGb;
|
||||||
setEditForm((p) => ({
|
setEditForm((p) => ({
|
||||||
...p,
|
...p,
|
||||||
max_tokens: normalizedMaxTokens,
|
|
||||||
cpu_cores: normalizedCpuCores,
|
cpu_cores: normalizedCpuCores,
|
||||||
memory_mb: normalizedMemoryMb,
|
memory_mb: normalizedMemoryMb,
|
||||||
storage_gb: normalizedStorageGb,
|
storage_gb: normalizedStorageGb,
|
||||||
}));
|
}));
|
||||||
setParamDraft({
|
setParamDraft((p) => ({
|
||||||
max_tokens: String(normalizedMaxTokens),
|
...p,
|
||||||
cpu_cores: String(normalizedCpuCores),
|
cpu_cores: String(normalizedCpuCores),
|
||||||
memory_mb: String(normalizedMemoryMb),
|
memory_mb: String(normalizedMemoryMb),
|
||||||
storage_gb: String(normalizedStorageGb),
|
storage_gb: String(normalizedStorageGb),
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
if (mode === 'params') {
|
||||||
|
payload.llm_provider = editForm.llm_provider;
|
||||||
|
payload.llm_model = editForm.llm_model;
|
||||||
|
payload.api_base = editForm.api_base;
|
||||||
|
if (editForm.api_key.trim()) payload.api_key = editForm.api_key.trim();
|
||||||
|
payload.temperature = clampTemperature(Number(editForm.temperature));
|
||||||
|
payload.top_p = Number(editForm.top_p);
|
||||||
|
const normalizedMaxTokens = clampMaxTokens(Number(paramDraft.max_tokens));
|
||||||
|
payload.max_tokens = normalizedMaxTokens;
|
||||||
|
setEditForm((p) => ({
|
||||||
|
...p,
|
||||||
|
max_tokens: normalizedMaxTokens,
|
||||||
|
}));
|
||||||
|
setParamDraft((p) => ({ ...p, max_tokens: String(normalizedMaxTokens) }));
|
||||||
}
|
}
|
||||||
if (mode === 'agent') {
|
if (mode === 'agent') {
|
||||||
payload.agents_md = editForm.agents_md;
|
payload.agents_md = editForm.agents_md;
|
||||||
|
|
@ -2002,6 +2171,14 @@ 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);
|
||||||
|
|
@ -2124,7 +2301,7 @@ export function BotDashboardModule({
|
||||||
title={t.openFolderTitle}
|
title={t.openFolderTitle}
|
||||||
>
|
>
|
||||||
<FolderOpen size={14} />
|
<FolderOpen size={14} />
|
||||||
<span className="workspace-entry-name">{node.name}</span>
|
<span className="workspace-entry-name" title={node.name}>{node.name}</span>
|
||||||
<span className="workspace-entry-meta">{t.folder}</span>
|
<span className="workspace-entry-meta">{t.folder}</span>
|
||||||
</button>,
|
</button>,
|
||||||
);
|
);
|
||||||
|
|
@ -2139,10 +2316,14 @@ export function BotDashboardModule({
|
||||||
className={`workspace-entry file ${previewable ? '' : 'disabled'}`}
|
className={`workspace-entry file ${previewable ? '' : 'disabled'}`}
|
||||||
disabled={!previewable || workspaceFileLoading}
|
disabled={!previewable || workspaceFileLoading}
|
||||||
onClick={() => void openWorkspaceFilePreview(node.path)}
|
onClick={() => void openWorkspaceFilePreview(node.path)}
|
||||||
|
onMouseEnter={(event) => showWorkspaceHoverCard(node, event.currentTarget)}
|
||||||
|
onMouseLeave={hideWorkspaceHoverCard}
|
||||||
|
onFocus={(event) => showWorkspaceHoverCard(node, event.currentTarget)}
|
||||||
|
onBlur={hideWorkspaceHoverCard}
|
||||||
title={previewable ? (downloadOnlyFile ? t.download : t.previewTitle) : t.fileNotPreviewable}
|
title={previewable ? (downloadOnlyFile ? t.download : t.previewTitle) : t.fileNotPreviewable}
|
||||||
>
|
>
|
||||||
<FileText size={14} />
|
<FileText size={14} />
|
||||||
<span className="workspace-entry-name">{node.name}</span>
|
<span className="workspace-entry-name" title={node.name}>{node.name}</span>
|
||||||
<span className="workspace-entry-meta mono">{node.ext || '-'}</span>
|
<span className="workspace-entry-meta mono">{node.ext || '-'}</span>
|
||||||
</button>,
|
</button>,
|
||||||
);
|
);
|
||||||
|
|
@ -2593,6 +2774,8 @@ export function BotDashboardModule({
|
||||||
<div className="section-mini-title">{t.workspaceOutputs}</div>
|
<div className="section-mini-title">{t.workspaceOutputs}</div>
|
||||||
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
{workspaceError ? <div className="ops-empty-inline">{workspaceError}</div> : null}
|
||||||
<div className="workspace-toolbar">
|
<div className="workspace-toolbar">
|
||||||
|
<span className="workspace-path mono">{workspaceCurrentPath || '/'}</span>
|
||||||
|
<div className="workspace-toolbar-actions">
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="workspace-refresh-icon-btn"
|
className="workspace-refresh-icon-btn"
|
||||||
disabled={workspaceLoading || !selectedBotId}
|
disabled={workspaceLoading || !selectedBotId}
|
||||||
|
|
@ -2612,7 +2795,7 @@ export function BotDashboardModule({
|
||||||
/>
|
/>
|
||||||
<span className="workspace-auto-switch-track" />
|
<span className="workspace-auto-switch-track" />
|
||||||
</label>
|
</label>
|
||||||
<span className="workspace-path mono">{workspaceCurrentPath || '/'}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="workspace-panel">
|
<div className="workspace-panel">
|
||||||
<div className="workspace-list">
|
<div className="workspace-list">
|
||||||
|
|
@ -2758,6 +2941,15 @@ export function BotDashboardModule({
|
||||||
<label className="field-label">{t.botName}</label>
|
<label className="field-label">{t.botName}</label>
|
||||||
<input className="input" value={editForm.name} onChange={(e) => setEditForm((p) => ({ ...p, name: e.target.value }))} placeholder={t.botNamePlaceholder} />
|
<input className="input" value={editForm.name} onChange={(e) => setEditForm((p) => ({ ...p, name: e.target.value }))} placeholder={t.botNamePlaceholder} />
|
||||||
|
|
||||||
|
<label className="field-label">{t.accessPassword}</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="password"
|
||||||
|
value={editForm.access_password}
|
||||||
|
onChange={(e) => setEditForm((p) => ({ ...p, access_password: e.target.value }))}
|
||||||
|
placeholder={t.accessPasswordPlaceholder}
|
||||||
|
/>
|
||||||
|
|
||||||
<label className="field-label">{t.baseImageReadonly}</label>
|
<label className="field-label">{t.baseImageReadonly}</label>
|
||||||
<select
|
<select
|
||||||
className="select"
|
className="select"
|
||||||
|
|
@ -2776,83 +2968,6 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<label className="field-label">Provider</label>
|
|
||||||
<select className="select" value={editForm.llm_provider} onChange={(e) => onBaseProviderChange(e.target.value)}>
|
|
||||||
<option value="openrouter">openrouter</option>
|
|
||||||
<option value="dashscope">dashscope (aliyun qwen)</option>
|
|
||||||
<option value="openai">openai</option>
|
|
||||||
<option value="deepseek">deepseek</option>
|
|
||||||
<option value="kimi">kimi (moonshot)</option>
|
|
||||||
<option value="minimax">minimax</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label className="field-label">{t.modelName}</label>
|
|
||||||
<input className="input" value={editForm.llm_model} onChange={(e) => setEditForm((p) => ({ ...p, llm_model: e.target.value }))} placeholder={t.modelNamePlaceholder} />
|
|
||||||
|
|
||||||
<label className="field-label">{t.newApiKey}</label>
|
|
||||||
<input className="input" type="password" value={editForm.api_key} onChange={(e) => setEditForm((p) => ({ ...p, api_key: e.target.value }))} placeholder={t.newApiKeyPlaceholder} />
|
|
||||||
|
|
||||||
<label className="field-label">API Base</label>
|
|
||||||
<input className="input" value={editForm.api_base} onChange={(e) => setEditForm((p) => ({ ...p, api_base: e.target.value }))} placeholder="API Base URL" />
|
|
||||||
|
|
||||||
<div className="card" style={{ fontSize: 12, color: 'var(--muted)' }}>
|
|
||||||
{providerPresets[editForm.llm_provider]?.note[noteLocale]}
|
|
||||||
</div>
|
|
||||||
<button className="btn btn-secondary" onClick={() => void testProviderConnection()} disabled={isTestingProvider}>
|
|
||||||
{isTestingProvider ? t.testing : t.testModelConnection}
|
|
||||||
</button>
|
|
||||||
{providerTestResult && <div className="card">{providerTestResult}</div>}
|
|
||||||
<div className="row-between">
|
|
||||||
<button className="btn btn-secondary" onClick={() => setShowBaseModal(false)}>{t.cancel}</button>
|
|
||||||
<button className="btn btn-primary" disabled={isSaving} onClick={() => void saveBot('base')}>{t.save}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showParamModal && (
|
|
||||||
<div className="modal-mask" onClick={() => setShowParamModal(false)}>
|
|
||||||
<div className="modal-card" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="modal-title-row modal-title-with-close">
|
|
||||||
<div className="modal-title-main">
|
|
||||||
<h3>{t.modelParams}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="modal-title-actions">
|
|
||||||
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowParamModal(false)} tooltip={t.close} aria-label={t.close}>
|
|
||||||
<X size={14} />
|
|
||||||
</LucentIconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="slider-row">
|
|
||||||
<label className="field-label">Temperature: {Number(editForm.temperature).toFixed(2)}</label>
|
|
||||||
<input type="range" min="0" max="1" step="0.01" value={editForm.temperature} onChange={(e) => setEditForm((p) => ({ ...p, temperature: clampTemperature(Number(e.target.value)) }))} />
|
|
||||||
</div>
|
|
||||||
<div className="slider-row">
|
|
||||||
<label className="field-label">Top P: {Number(editForm.top_p).toFixed(2)}</label>
|
|
||||||
<input type="range" min="0" max="1" step="0.01" value={editForm.top_p} onChange={(e) => setEditForm((p) => ({ ...p, top_p: Number(e.target.value) }))} />
|
|
||||||
</div>
|
|
||||||
<label className="field-label">Max Tokens</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
min="256"
|
|
||||||
max="32768"
|
|
||||||
value={paramDraft.max_tokens}
|
|
||||||
onChange={(e) => setParamDraft((p) => ({ ...p, max_tokens: e.target.value }))}
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
|
||||||
{[4096, 8192, 16384, 32768].map((value) => (
|
|
||||||
<button
|
|
||||||
key={value}
|
|
||||||
className="btn btn-secondary btn-sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setParamDraft((p) => ({ ...p, max_tokens: String(value) }))}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
||||||
<input
|
<input
|
||||||
className="input"
|
className="input"
|
||||||
|
|
@ -2884,6 +2999,84 @@ export function BotDashboardModule({
|
||||||
onChange={(e) => setParamDraft((p) => ({ ...p, storage_gb: e.target.value }))}
|
onChange={(e) => setParamDraft((p) => ({ ...p, storage_gb: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
<div className="field-label">{isZh ? '提示:填写 0 表示不限制(保存后需手动重启 Bot 生效)。' : 'Tip: value 0 means unlimited (takes effect after manual bot restart).'}</div>
|
<div className="field-label">{isZh ? '提示:填写 0 表示不限制(保存后需手动重启 Bot 生效)。' : 'Tip: value 0 means unlimited (takes effect after manual bot restart).'}</div>
|
||||||
|
<div className="row-between">
|
||||||
|
<button className="btn btn-secondary" onClick={() => setShowBaseModal(false)}>{t.cancel}</button>
|
||||||
|
<button className="btn btn-primary" disabled={isSaving} onClick={() => void saveBot('base')}>{t.save}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showParamModal && (
|
||||||
|
<div className="modal-mask" onClick={() => setShowParamModal(false)}>
|
||||||
|
<div className="modal-card" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-title-row modal-title-with-close">
|
||||||
|
<div className="modal-title-main">
|
||||||
|
<h3>{t.modelParams}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="modal-title-actions">
|
||||||
|
<LucentIconButton className="btn btn-secondary btn-sm icon-btn" onClick={() => setShowParamModal(false)} tooltip={t.close} aria-label={t.close}>
|
||||||
|
<X size={14} />
|
||||||
|
</LucentIconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label className="field-label">Provider</label>
|
||||||
|
<select className="select" value={editForm.llm_provider} onChange={(e) => onBaseProviderChange(e.target.value)}>
|
||||||
|
<option value="openrouter">openrouter</option>
|
||||||
|
<option value="dashscope">dashscope (aliyun qwen)</option>
|
||||||
|
<option value="openai">openai</option>
|
||||||
|
<option value="deepseek">deepseek</option>
|
||||||
|
<option value="kimi">kimi (moonshot)</option>
|
||||||
|
<option value="minimax">minimax</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label className="field-label">{t.modelName}</label>
|
||||||
|
<input className="input" value={editForm.llm_model} onChange={(e) => setEditForm((p) => ({ ...p, llm_model: e.target.value }))} placeholder={t.modelNamePlaceholder} />
|
||||||
|
|
||||||
|
<label className="field-label">{t.newApiKey}</label>
|
||||||
|
<input className="input" type="password" value={editForm.api_key} onChange={(e) => setEditForm((p) => ({ ...p, api_key: e.target.value }))} placeholder={t.newApiKeyPlaceholder} />
|
||||||
|
|
||||||
|
<label className="field-label">API Base</label>
|
||||||
|
<input className="input" value={editForm.api_base} onChange={(e) => setEditForm((p) => ({ ...p, api_base: e.target.value }))} placeholder="API Base URL" />
|
||||||
|
|
||||||
|
<div className="card" style={{ fontSize: 12, color: 'var(--muted)' }}>
|
||||||
|
{providerPresets[editForm.llm_provider]?.note[noteLocale]}
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-secondary" onClick={() => void testProviderConnection()} disabled={isTestingProvider}>
|
||||||
|
{isTestingProvider ? t.testing : t.testModelConnection}
|
||||||
|
</button>
|
||||||
|
{providerTestResult && <div className="card">{providerTestResult}</div>}
|
||||||
|
|
||||||
|
<div className="slider-row">
|
||||||
|
<label className="field-label">Temperature: {Number(editForm.temperature).toFixed(2)}</label>
|
||||||
|
<input type="range" min="0" max="1" step="0.01" value={editForm.temperature} onChange={(e) => setEditForm((p) => ({ ...p, temperature: clampTemperature(Number(e.target.value)) }))} />
|
||||||
|
</div>
|
||||||
|
<div className="slider-row">
|
||||||
|
<label className="field-label">Top P: {Number(editForm.top_p).toFixed(2)}</label>
|
||||||
|
<input type="range" min="0" max="1" step="0.01" value={editForm.top_p} onChange={(e) => setEditForm((p) => ({ ...p, top_p: Number(e.target.value) }))} />
|
||||||
|
</div>
|
||||||
|
<label className="field-label">Max Tokens</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min="256"
|
||||||
|
max="32768"
|
||||||
|
value={paramDraft.max_tokens}
|
||||||
|
onChange={(e) => setParamDraft((p) => ({ ...p, max_tokens: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
{[4096, 8192, 16384, 32768].map((value) => (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setParamDraft((p) => ({ ...p, max_tokens: String(value) }))}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<div className="row-between">
|
<div className="row-between">
|
||||||
<button className="btn btn-secondary" onClick={() => setShowParamModal(false)}>{t.cancel}</button>
|
<button className="btn btn-secondary" onClick={() => setShowParamModal(false)}>{t.cancel}</button>
|
||||||
<button className="btn btn-primary" disabled={isSaving} onClick={() => void saveBot('params')}>{t.saveParams}</button>
|
<button className="btn btn-primary" disabled={isSaving} onClick={() => void saveBot('params')}>{t.saveParams}</button>
|
||||||
|
|
@ -3025,7 +3218,7 @@ export function BotDashboardModule({
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="wizard-channel-list">
|
<div className="wizard-channel-list ops-skills-list-scroll">
|
||||||
{botSkills.length === 0 ? (
|
{botSkills.length === 0 ? (
|
||||||
<div className="ops-empty-inline">{t.skillsEmpty}</div>
|
<div className="ops-empty-inline">{t.skillsEmpty}</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -3204,7 +3397,7 @@ export function BotDashboardModule({
|
||||||
) : cronJobs.length === 0 ? (
|
) : cronJobs.length === 0 ? (
|
||||||
<div className="ops-empty-inline">{t.cronEmpty}</div>
|
<div className="ops-empty-inline">{t.cronEmpty}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="ops-cron-list">
|
<div className="ops-cron-list ops-cron-list-scroll">
|
||||||
{cronJobs.map((job) => {
|
{cronJobs.map((job) => {
|
||||||
const stopping = cronActionJobId === job.id;
|
const stopping = cronActionJobId === job.id;
|
||||||
const channel = String(job.payload?.channel || '').trim();
|
const channel = String(job.payload?.channel || '').trim();
|
||||||
|
|
@ -3335,13 +3528,13 @@ export function BotDashboardModule({
|
||||||
{workspacePreview.isImage ? (
|
{workspacePreview.isImage ? (
|
||||||
<img
|
<img
|
||||||
className="workspace-preview-image"
|
className="workspace-preview-image"
|
||||||
src={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}`}
|
src={buildWorkspaceDownloadHref(workspacePreview.path, false)}
|
||||||
alt={workspacePreview.path.split('/').pop() || 'workspace-image'}
|
alt={workspacePreview.path.split('/').pop() || 'workspace-image'}
|
||||||
/>
|
/>
|
||||||
) : workspacePreview.isHtml ? (
|
) : workspacePreview.isHtml ? (
|
||||||
<iframe
|
<iframe
|
||||||
className="workspace-preview-embed"
|
className="workspace-preview-embed"
|
||||||
src={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}`}
|
src={buildWorkspaceDownloadHref(workspacePreview.path, false)}
|
||||||
title={workspacePreview.path}
|
title={workspacePreview.path}
|
||||||
/>
|
/>
|
||||||
) : workspacePreview.isMarkdown ? (
|
) : workspacePreview.isMarkdown ? (
|
||||||
|
|
@ -3374,7 +3567,7 @@ export function BotDashboardModule({
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
href={`${APP_ENDPOINTS.apiBase}/bots/${selectedBotId}/workspace/download?path=${encodeURIComponent(workspacePreview.path)}&download=1`}
|
href={buildWorkspaceDownloadHref(workspacePreview.path, true)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
download={workspacePreview.path.split('/').pop() || 'workspace-file'}
|
download={workspacePreview.path.split('/').pop() || 'workspace-file'}
|
||||||
|
|
@ -3387,6 +3580,30 @@ export function BotDashboardModule({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{workspaceHoverCard ? (
|
||||||
|
<div
|
||||||
|
className={`workspace-hover-panel ${workspaceHoverCard.above ? 'is-above' : ''}`}
|
||||||
|
style={{ top: workspaceHoverCard.top, left: workspaceHoverCard.left }}
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
<div className="workspace-entry-info-row">
|
||||||
|
<span className="workspace-entry-info-label">{isZh ? '全称' : 'Name'}</span>
|
||||||
|
<span className="workspace-entry-info-value mono">{workspaceHoverCard.node.name || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="workspace-entry-info-row">
|
||||||
|
<span className="workspace-entry-info-label">{isZh ? '创建时间' : 'Created'}</span>
|
||||||
|
<span className="workspace-entry-info-value">{formatWorkspaceTime(workspaceHoverCard.node.ctime, isZh)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="workspace-entry-info-row">
|
||||||
|
<span className="workspace-entry-info-label">{isZh ? '修改时间' : 'Modified'}</span>
|
||||||
|
<span className="workspace-entry-info-value">{formatWorkspaceTime(workspaceHoverCard.node.mtime, isZh)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="workspace-entry-info-row">
|
||||||
|
<span className="workspace-entry-info-label">{isZh ? '文件大小' : 'Size'}</span>
|
||||||
|
<span className="workspace-entry-info-value mono">{Number.isFinite(Number(workspaceHoverCard.node.size)) ? formatBytes(Number(workspaceHoverCard.node.size)) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export function normalizeAssistantMessageText(input: string) {
|
||||||
|
|
||||||
// Remove dashboard wrapper if channel accidentally outputs raw marker line.
|
// Remove dashboard wrapper if channel accidentally outputs raw marker line.
|
||||||
text = text.replace(/__DASHBOARD_DATA_START__/g, '').replace(/__DASHBOARD_DATA_END__/g, '').trim();
|
text = text.replace(/__DASHBOARD_DATA_START__/g, '').replace(/__DASHBOARD_DATA_END__/g, '').trim();
|
||||||
|
text = text.replace(/<\/?tool_call>/gi, '').trim();
|
||||||
|
|
||||||
// Keep HTML-enabled markdown readable: cap excessive HTML line breaks/empty paragraphs.
|
// Keep HTML-enabled markdown readable: cap excessive HTML line breaks/empty paragraphs.
|
||||||
text = text.replace(/(?:<br\s*\/?>\s*){4,}/gi, '<br><br>');
|
text = text.replace(/(?:<br\s*\/?>\s*){4,}/gi, '<br><br>');
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { wizardZhCn } from '../../i18n/wizard.zh-cn';
|
||||||
import { wizardEn } from '../../i18n/wizard.en';
|
import { wizardEn } from '../../i18n/wizard.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 { setBotAccessPassword } from '../../utils/botAccess';
|
||||||
|
|
||||||
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
type AgentTab = 'AGENTS' | 'SOUL' | 'USER' | 'TOOLS' | 'IDENTITY';
|
||||||
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
type ChannelType = 'feishu' | 'qq' | 'dingtalk' | 'telegram' | 'slack';
|
||||||
|
|
@ -96,6 +97,7 @@ const providerPresets: Record<string, { model: string; note: { 'zh-cn': string;
|
||||||
const initialForm = {
|
const initialForm = {
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
access_password: '',
|
||||||
llm_provider: 'dashscope',
|
llm_provider: 'dashscope',
|
||||||
llm_model: providerPresets.dashscope.model,
|
llm_model: providerPresets.dashscope.model,
|
||||||
api_key: '',
|
api_key: '',
|
||||||
|
|
@ -274,6 +276,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, {
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots`, {
|
||||||
id: form.id,
|
id: form.id,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
|
access_password: form.access_password,
|
||||||
llm_provider: form.llm_provider,
|
llm_provider: form.llm_provider,
|
||||||
llm_model: form.llm_model,
|
llm_model: form.llm_model,
|
||||||
api_key: form.api_key,
|
api_key: form.api_key,
|
||||||
|
|
@ -304,6 +307,10 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
env_params: form.env_params,
|
env_params: form.env_params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (String(form.access_password || '').trim()) {
|
||||||
|
setBotAccessPassword(form.id, form.access_password);
|
||||||
|
}
|
||||||
|
|
||||||
if (autoStart) {
|
if (autoStart) {
|
||||||
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${form.id}/start`);
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${form.id}/start`);
|
||||||
}
|
}
|
||||||
|
|
@ -589,7 +596,48 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
<div className="section-mini-title">{ui.baseInfo}</div>
|
<div className="section-mini-title">{ui.baseInfo}</div>
|
||||||
<input className="input" placeholder={ui.botIdPlaceholder} value={form.id} onChange={(e) => setForm((p) => ({ ...p, id: e.target.value }))} />
|
<input className="input" placeholder={ui.botIdPlaceholder} value={form.id} onChange={(e) => setForm((p) => ({ ...p, id: e.target.value }))} />
|
||||||
<input className="input" placeholder={ui.botName} value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
|
<input className="input" placeholder={ui.botName} value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
|
||||||
|
<input className="input" type="password" placeholder={ui.accessPasswordPlaceholder} value={form.access_password} onChange={(e) => setForm((p) => ({ ...p, access_password: e.target.value }))} />
|
||||||
|
|
||||||
|
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
||||||
|
{isZh ? '资源配额' : 'Resource Limits'}
|
||||||
|
</div>
|
||||||
|
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="16"
|
||||||
|
step="0.1"
|
||||||
|
value={cpuCoresDraft}
|
||||||
|
onChange={(e) => setCpuCoresDraft(e.target.value)}
|
||||||
|
onBlur={(e) => commitCpuCoresDraft(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label className="field-label">{isZh ? '内存 (MB)' : 'Memory (MB)'}</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="65536"
|
||||||
|
step="128"
|
||||||
|
value={memoryMbDraft}
|
||||||
|
onChange={(e) => setMemoryMbDraft(e.target.value)}
|
||||||
|
onBlur={(e) => commitMemoryMbDraft(e.target.value)}
|
||||||
|
/>
|
||||||
|
<label className="field-label">{isZh ? '存储 (GB)' : 'Storage (GB)'}</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="1024"
|
||||||
|
step="1"
|
||||||
|
value={storageGbDraft}
|
||||||
|
onChange={(e) => setStorageGbDraft(e.target.value)}
|
||||||
|
onBlur={(e) => commitStorageGbDraft(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="field-label">{isZh ? '提示:填写 0 表示不限制。' : 'Tip: value 0 means unlimited.'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stack card wizard-step2-card">
|
||||||
<div className="section-mini-title">{ui.modelAccess}</div>
|
<div className="section-mini-title">{ui.modelAccess}</div>
|
||||||
<select className="select" value={form.llm_provider} onChange={(e) => onProviderChange(e.target.value)}>
|
<select className="select" value={form.llm_provider} onChange={(e) => onProviderChange(e.target.value)}>
|
||||||
<option value="openrouter">openrouter</option>
|
<option value="openrouter">openrouter</option>
|
||||||
|
|
@ -602,7 +650,6 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
<input className="input" placeholder={ui.modelNamePlaceholder} value={form.llm_model} onChange={(e) => setForm((p) => ({ ...p, llm_model: e.target.value }))} />
|
<input className="input" placeholder={ui.modelNamePlaceholder} value={form.llm_model} onChange={(e) => setForm((p) => ({ ...p, llm_model: e.target.value }))} />
|
||||||
<input className="input" type="password" placeholder="API Key" value={form.api_key} onChange={(e) => setForm((p) => ({ ...p, api_key: e.target.value }))} />
|
<input className="input" type="password" placeholder="API Key" value={form.api_key} onChange={(e) => setForm((p) => ({ ...p, api_key: e.target.value }))} />
|
||||||
<input className="input" placeholder="API Base" value={form.api_base} onChange={(e) => setForm((p) => ({ ...p, api_base: e.target.value }))} />
|
<input className="input" placeholder="API Base" value={form.api_base} onChange={(e) => setForm((p) => ({ ...p, api_base: e.target.value }))} />
|
||||||
|
|
||||||
<div className="card wizard-note-card" style={{ fontSize: 12, color: 'var(--muted)' }}>
|
<div className="card wizard-note-card" style={{ fontSize: 12, color: 'var(--muted)' }}>
|
||||||
{providerPresets[form.llm_provider]?.note[noteLocale]}
|
{providerPresets[form.llm_provider]?.note[noteLocale]}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -610,9 +657,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
{isTestingProvider ? ui.testing : ui.test}
|
{isTestingProvider ? ui.testing : ui.test}
|
||||||
</button>
|
</button>
|
||||||
{testResult && <div className="card wizard-note-card">{testResult}</div>}
|
{testResult && <div className="card wizard-note-card">{testResult}</div>}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stack card wizard-step2-card">
|
|
||||||
<div className="section-mini-title">{ui.modelParams}</div>
|
<div className="section-mini-title">{ui.modelParams}</div>
|
||||||
<div className="slider-row">
|
<div className="slider-row">
|
||||||
<label className="field-label">Temperature: {form.temperature.toFixed(2)}</label>
|
<label className="field-label">Temperature: {form.temperature.toFixed(2)}</label>
|
||||||
|
|
@ -652,44 +697,6 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="section-mini-title" style={{ marginTop: 10 }}>
|
|
||||||
{isZh ? '资源配额' : 'Resource Limits'}
|
|
||||||
</div>
|
|
||||||
<label className="field-label">{isZh ? 'CPU 核心数' : 'CPU Cores'}</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="16"
|
|
||||||
step="0.1"
|
|
||||||
value={cpuCoresDraft}
|
|
||||||
onChange={(e) => setCpuCoresDraft(e.target.value)}
|
|
||||||
onBlur={(e) => commitCpuCoresDraft(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label className="field-label">{isZh ? '内存 (MB)' : 'Memory (MB)'}</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="65536"
|
|
||||||
step="128"
|
|
||||||
value={memoryMbDraft}
|
|
||||||
onChange={(e) => setMemoryMbDraft(e.target.value)}
|
|
||||||
onBlur={(e) => commitMemoryMbDraft(e.target.value)}
|
|
||||||
/>
|
|
||||||
<label className="field-label">{isZh ? '存储 (GB)' : 'Storage (GB)'}</label>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="1024"
|
|
||||||
step="1"
|
|
||||||
value={storageGbDraft}
|
|
||||||
onChange={(e) => setStorageGbDraft(e.target.value)}
|
|
||||||
onBlur={(e) => commitStorageGbDraft(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div className="field-label">{isZh ? '提示:填写 0 表示不限制。' : 'Tip: value 0 means unlimited.'}</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stack card wizard-step2-card">
|
<div className="stack card wizard-step2-card">
|
||||||
|
|
@ -765,6 +772,7 @@ export function BotWizardModule({ onCreated, onGoDashboard }: BotWizardModulePro
|
||||||
<div>{ui.image}: <span className="mono">{form.image_tag}</span></div>
|
<div>{ui.image}: <span className="mono">{form.image_tag}</span></div>
|
||||||
<div>Bot ID: <span className="mono">{form.id}</span></div>
|
<div>Bot ID: <span className="mono">{form.id}</span></div>
|
||||||
<div>{ui.name}: {form.name}</div>
|
<div>{ui.name}: {form.name}</div>
|
||||||
|
<div>{ui.accessPassword}: {form.access_password ? (isZh ? '已设置' : 'Configured') : (isZh ? '未设置' : 'Not set')}</div>
|
||||||
<div>Provider: {form.llm_provider}</div>
|
<div>Provider: {form.llm_provider}</div>
|
||||||
<div>{ui.model}: {form.llm_model}</div>
|
<div>{ui.model}: {form.llm_model}</div>
|
||||||
<div>Temperature: {form.temperature.toFixed(2)}</div>
|
<div>Temperature: {form.temperature.toFixed(2)}</div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ export interface BotEvent {
|
||||||
export interface BotState {
|
export interface BotState {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
access_password?: string;
|
||||||
|
has_access_password?: boolean;
|
||||||
avatar_model: string;
|
avatar_model: string;
|
||||||
avatar_skin?: string;
|
avatar_skin?: string;
|
||||||
docker_status: string;
|
docker_status: string;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const BOT_PASSWORD_HEADER = 'X-Bot-Password';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
const memoryMap = new Map<string, string>();
|
||||||
|
|
||||||
|
function normalizeBotId(raw: string): string {
|
||||||
|
return String(raw || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAbsoluteUrl(input: string): string {
|
||||||
|
const url = String(input || '').trim();
|
||||||
|
if (!url) return '';
|
||||||
|
try {
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return new URL(url).pathname;
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return new URL(url, window.location.origin).pathname;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractBotIdFromApiPath(rawPath: string): string | null {
|
||||||
|
const path = resolveAbsoluteUrl(rawPath);
|
||||||
|
if (!path) return null;
|
||||||
|
const match = path.match(/\/api\/bots\/([^/?#]+)/i);
|
||||||
|
if (!match?.[1]) return null;
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(match[1]).trim() || null;
|
||||||
|
} catch {
|
||||||
|
return String(match[1]).trim() || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBotAccessPassword(botId: string): string {
|
||||||
|
const key = normalizeBotId(botId);
|
||||||
|
if (!key) return '';
|
||||||
|
return memoryMap.get(key) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setBotAccessPassword(botId: string, password: string): void {
|
||||||
|
const key = normalizeBotId(botId);
|
||||||
|
const value = String(password || '').trim();
|
||||||
|
if (!key || !value) return;
|
||||||
|
memoryMap.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearBotAccessPassword(botId: string): void {
|
||||||
|
const key = normalizeBotId(botId);
|
||||||
|
if (!key) return;
|
||||||
|
memoryMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAllBotAccessPasswords(): void {
|
||||||
|
if (memoryMap.size === 0) return;
|
||||||
|
memoryMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBotUnauthorizedError(error: any, botId?: string): boolean {
|
||||||
|
if (!axios.isAxiosError(error)) return false;
|
||||||
|
if (Number(error.response?.status) !== 401) 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 {
|
||||||
|
const target = `${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 {
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue