v0.1.4-p2
parent
04b807da8e
commit
3a151d7e8f
|
|
@ -35,21 +35,54 @@ engine = create_engine(DATABASE_URL, **_engine_kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_botinstance_columns() -> None:
|
def _ensure_botinstance_columns() -> None:
|
||||||
if engine.dialect.name != "sqlite":
|
dialect = engine.dialect.name
|
||||||
return
|
|
||||||
required_columns = {
|
required_columns = {
|
||||||
"current_state": "TEXT DEFAULT 'IDLE'",
|
"current_state": {
|
||||||
"last_action": "TEXT",
|
"sqlite": "TEXT DEFAULT 'IDLE'",
|
||||||
"image_tag": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
"postgresql": "TEXT DEFAULT 'IDLE'",
|
||||||
"access_password": "TEXT DEFAULT ''",
|
"mysql": "VARCHAR(64) DEFAULT 'IDLE'",
|
||||||
|
},
|
||||||
|
"last_action": {
|
||||||
|
"sqlite": "TEXT",
|
||||||
|
"postgresql": "TEXT",
|
||||||
|
"mysql": "LONGTEXT",
|
||||||
|
},
|
||||||
|
"image_tag": {
|
||||||
|
"sqlite": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
||||||
|
"postgresql": "TEXT DEFAULT 'nanobot-base:v0.1.4'",
|
||||||
|
"mysql": "VARCHAR(255) DEFAULT 'nanobot-base:v0.1.4'",
|
||||||
|
},
|
||||||
|
"access_password": {
|
||||||
|
"sqlite": "TEXT DEFAULT ''",
|
||||||
|
"postgresql": "TEXT DEFAULT ''",
|
||||||
|
"mysql": "VARCHAR(255) DEFAULT ''",
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"sqlite": "INTEGER NOT NULL DEFAULT 1",
|
||||||
|
"postgresql": "BOOLEAN NOT NULL DEFAULT TRUE",
|
||||||
|
"mysql": "BOOLEAN NOT NULL DEFAULT TRUE",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inspector = inspect(engine)
|
||||||
|
if not inspector.has_table("botinstance"):
|
||||||
|
return
|
||||||
with engine.connect() as conn:
|
with engine.connect() as conn:
|
||||||
existing_rows = conn.execute(text("PRAGMA table_info(botinstance)")).fetchall()
|
existing = {
|
||||||
existing = {str(row[1]) for row in existing_rows}
|
str(row.get("name"))
|
||||||
for col, ddl in required_columns.items():
|
for row in inspect(conn).get_columns("botinstance")
|
||||||
|
if row.get("name")
|
||||||
|
}
|
||||||
|
for col, ddl_map in required_columns.items():
|
||||||
if col in existing:
|
if col in existing:
|
||||||
continue
|
continue
|
||||||
|
ddl = ddl_map.get(dialect) or ddl_map.get("sqlite")
|
||||||
conn.execute(text(f"ALTER TABLE botinstance ADD COLUMN {col} {ddl}"))
|
conn.execute(text(f"ALTER TABLE botinstance ADD COLUMN {col} {ddl}"))
|
||||||
|
if "enabled" in existing:
|
||||||
|
if dialect == "sqlite":
|
||||||
|
conn.execute(text("UPDATE botinstance SET enabled = 1 WHERE enabled IS NULL"))
|
||||||
|
else:
|
||||||
|
conn.execute(text("UPDATE botinstance SET enabled = TRUE WHERE enabled IS NULL"))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ class ChannelConfigUpdateRequest(BaseModel):
|
||||||
class BotCreateRequest(BaseModel):
|
class BotCreateRequest(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
enabled: Optional[bool] = True
|
||||||
access_password: Optional[str] = None
|
access_password: Optional[str] = None
|
||||||
llm_provider: str
|
llm_provider: str
|
||||||
llm_model: str
|
llm_model: str
|
||||||
|
|
@ -131,6 +132,7 @@ class BotCreateRequest(BaseModel):
|
||||||
|
|
||||||
class BotUpdateRequest(BaseModel):
|
class BotUpdateRequest(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
|
enabled: Optional[bool] = None
|
||||||
access_password: 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
|
||||||
|
|
@ -411,11 +413,24 @@ def _is_bot_panel_management_api_path(path: str, method: str = "GET") -> bool:
|
||||||
return (
|
return (
|
||||||
raw.endswith("/start")
|
raw.endswith("/start")
|
||||||
or raw.endswith("/stop")
|
or raw.endswith("/stop")
|
||||||
|
or raw.endswith("/enable")
|
||||||
|
or raw.endswith("/disable")
|
||||||
or raw.endswith("/deactivate")
|
or raw.endswith("/deactivate")
|
||||||
or (verb in {"PUT", "DELETE"} and raw == f"/api/bots/{bot_id}")
|
or (verb in {"PUT", "DELETE"} and raw == f"/api/bots/{bot_id}")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_bot_enable_api_path(path: str, method: str = "GET") -> bool:
|
||||||
|
raw = str(path or "").strip()
|
||||||
|
verb = str(method or "GET").strip().upper()
|
||||||
|
if verb != "POST":
|
||||||
|
return False
|
||||||
|
bot_id = _extract_bot_id_from_api_path(raw)
|
||||||
|
if not bot_id:
|
||||||
|
return False
|
||||||
|
return raw == f"/api/bots/{bot_id}/enable"
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def bot_access_password_guard(request: Request, call_next):
|
async def bot_access_password_guard(request: Request, call_next):
|
||||||
if request.method.upper() == "OPTIONS":
|
if request.method.upper() == "OPTIONS":
|
||||||
|
|
@ -434,6 +449,13 @@ async def bot_access_password_guard(request: Request, call_next):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
return JSONResponse(status_code=404, content={"detail": "Bot not found"})
|
return JSONResponse(status_code=404, content={"detail": "Bot not found"})
|
||||||
|
enabled = bool(getattr(bot, "enabled", True))
|
||||||
|
if not enabled:
|
||||||
|
is_enable_api = _is_bot_enable_api_path(request.url.path, request.method)
|
||||||
|
is_read_api = request.method.upper() == "GET"
|
||||||
|
is_auth_login = request.method.upper() == "POST" and request.url.path == f"/api/bots/{bot_id}/auth/login"
|
||||||
|
if not (is_enable_api or is_read_api or is_auth_login):
|
||||||
|
return JSONResponse(status_code=403, content={"detail": "Bot is disabled. Enable it first."})
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1215,6 +1237,7 @@ def _serialize_bot(bot: BotInstance) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"id": bot.id,
|
"id": bot.id,
|
||||||
"name": bot.name,
|
"name": bot.name,
|
||||||
|
"enabled": bool(getattr(bot, "enabled", True)),
|
||||||
"access_password": bot.access_password or "",
|
"access_password": bot.access_password or "",
|
||||||
"has_access_password": bool(str(bot.access_password or "").strip()),
|
"has_access_password": bool(str(bot.access_password or "").strip()),
|
||||||
"avatar_model": "base",
|
"avatar_model": "base",
|
||||||
|
|
@ -1250,6 +1273,7 @@ def _serialize_bot_list_item(bot: BotInstance) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"id": bot.id,
|
"id": bot.id,
|
||||||
"name": bot.name,
|
"name": bot.name,
|
||||||
|
"enabled": bool(getattr(bot, "enabled", True)),
|
||||||
"has_access_password": bool(str(bot.access_password or "").strip()),
|
"has_access_password": bool(str(bot.access_password or "").strip()),
|
||||||
"image_tag": bot.image_tag,
|
"image_tag": bot.image_tag,
|
||||||
"docker_status": bot.docker_status,
|
"docker_status": bot.docker_status,
|
||||||
|
|
@ -1847,6 +1871,7 @@ def create_bot(payload: BotCreateRequest, session: Session = Depends(get_session
|
||||||
bot = BotInstance(
|
bot = BotInstance(
|
||||||
id=normalized_bot_id,
|
id=normalized_bot_id,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
|
enabled=bool(payload.enabled) if payload.enabled is not None else True,
|
||||||
access_password=str(payload.access_password or ""),
|
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, normalized_bot_id),
|
workspace_dir=os.path.join(BOTS_WORKSPACE_ROOT, normalized_bot_id),
|
||||||
|
|
@ -2059,7 +2084,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", "access_password"}
|
db_fields = {"name", "image_tag", "access_password", "enabled"}
|
||||||
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)
|
||||||
|
|
@ -2093,6 +2118,8 @@ async def start_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
if not bool(getattr(bot, "enabled", True)):
|
||||||
|
raise HTTPException(status_code=403, detail="Bot is disabled. Enable it first.")
|
||||||
_sync_workspace_channels(session, bot_id)
|
_sync_workspace_channels(session, bot_id)
|
||||||
runtime_snapshot = _read_bot_runtime_snapshot(bot)
|
runtime_snapshot = _read_bot_runtime_snapshot(bot)
|
||||||
env_params = _read_env_store(bot_id)
|
env_params = _read_env_store(bot_id)
|
||||||
|
|
@ -2132,6 +2159,8 @@ def stop_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
if not bool(getattr(bot, "enabled", True)):
|
||||||
|
raise HTTPException(status_code=403, detail="Bot is disabled. Enable it first.")
|
||||||
|
|
||||||
docker_manager.stop_bot(bot_id)
|
docker_manager.stop_bot(bot_id)
|
||||||
bot.docker_status = "STOPPED"
|
bot.docker_status = "STOPPED"
|
||||||
|
|
@ -2141,6 +2170,36 @@ def stop_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
return {"status": "stopped"}
|
return {"status": "stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/bots/{bot_id}/enable")
|
||||||
|
def enable_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
|
bot.enabled = True
|
||||||
|
session.add(bot)
|
||||||
|
session.commit()
|
||||||
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
|
return {"status": "enabled", "enabled": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/bots/{bot_id}/disable")
|
||||||
|
def disable_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
if not bot:
|
||||||
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
|
docker_manager.stop_bot(bot_id)
|
||||||
|
bot.enabled = False
|
||||||
|
bot.docker_status = "STOPPED"
|
||||||
|
if str(bot.current_state or "").upper() not in {"ERROR"}:
|
||||||
|
bot.current_state = "IDLE"
|
||||||
|
session.add(bot)
|
||||||
|
session.commit()
|
||||||
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
|
return {"status": "disabled", "enabled": False}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/bots/{bot_id}/deactivate")
|
@app.post("/api/bots/{bot_id}/deactivate")
|
||||||
def deactivate_bot(bot_id: str, session: Session = Depends(get_session)):
|
def deactivate_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
bot = session.get(BotInstance, bot_id)
|
bot = session.get(BotInstance, bot_id)
|
||||||
|
|
@ -2148,7 +2207,10 @@ def deactivate_bot(bot_id: str, session: Session = Depends(get_session)):
|
||||||
raise HTTPException(status_code=404, detail="Bot not found")
|
raise HTTPException(status_code=404, detail="Bot not found")
|
||||||
|
|
||||||
docker_manager.stop_bot(bot_id)
|
docker_manager.stop_bot(bot_id)
|
||||||
|
bot.enabled = False
|
||||||
bot.docker_status = "STOPPED"
|
bot.docker_status = "STOPPED"
|
||||||
|
if str(bot.current_state or "").upper() not in {"ERROR"}:
|
||||||
|
bot.current_state = "IDLE"
|
||||||
session.add(bot)
|
session.add(bot)
|
||||||
session.commit()
|
session.commit()
|
||||||
_invalidate_bot_detail_cache(bot_id)
|
_invalidate_bot_detail_cache(bot_id)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
enabled: bool = Field(default=True, index=True)
|
||||||
access_password: str = Field(default="")
|
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)
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,16 @@ export const dashboardEn = {
|
||||||
templateTopicInvalid: 'Invalid topic preset JSON.',
|
templateTopicInvalid: 'Invalid topic preset JSON.',
|
||||||
templateParseFail: 'Template JSON parse failed.',
|
templateParseFail: 'Template JSON parse failed.',
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
|
disabled: 'Disabled',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
start: 'Start',
|
start: 'Start',
|
||||||
|
enable: 'Enable',
|
||||||
|
disable: 'Disable',
|
||||||
|
disableConfirm: (id: string) => `Disable bot ${id}? It will be stopped and locked from operations.`,
|
||||||
|
enableDone: 'Bot enabled.',
|
||||||
|
disableDone: 'Bot disabled and stopped.',
|
||||||
|
enableFail: 'Enable failed. Check backend logs.',
|
||||||
|
disableFail: 'Disable failed. Check backend logs.',
|
||||||
restart: 'Restart Bot',
|
restart: 'Restart Bot',
|
||||||
restartConfirm: (id: string) => `Restart bot ${id}?`,
|
restartConfirm: (id: string) => `Restart bot ${id}?`,
|
||||||
restartFail: 'Restart failed. Check backend logs.',
|
restartFail: 'Restart failed. Check backend logs.',
|
||||||
|
|
@ -122,6 +130,7 @@ export const dashboardEn = {
|
||||||
interruptSent: 'Interrupt command sent.',
|
interruptSent: 'Interrupt command sent.',
|
||||||
botStarting: 'Bot is starting...',
|
botStarting: 'Bot is starting...',
|
||||||
botStopping: 'Bot is stopping...',
|
botStopping: 'Bot is stopping...',
|
||||||
|
botDisabledHint: 'Bot is disabled. Enable it before operating.',
|
||||||
chatDisabled: 'Bot is stopped. Chat area is disabled.',
|
chatDisabled: 'Bot is stopped. Chat area is disabled.',
|
||||||
selectBot: 'Select a bot to inspect',
|
selectBot: 'Select a bot to inspect',
|
||||||
runtime: 'Runtime Status',
|
runtime: 'Runtime Status',
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,16 @@ export const dashboardZhCn = {
|
||||||
templateTopicInvalid: '主题模板格式错误。',
|
templateTopicInvalid: '主题模板格式错误。',
|
||||||
templateParseFail: '模板 JSON 解析失败。',
|
templateParseFail: '模板 JSON 解析失败。',
|
||||||
image: '镜像',
|
image: '镜像',
|
||||||
|
disabled: '已禁用',
|
||||||
stop: '停止',
|
stop: '停止',
|
||||||
start: '启动',
|
start: '启动',
|
||||||
|
enable: '启用',
|
||||||
|
disable: '禁用',
|
||||||
|
disableConfirm: (id: string) => `确认禁用 Bot ${id}?禁用后将立即停止且不可操作。`,
|
||||||
|
enableDone: 'Bot 已启用。',
|
||||||
|
disableDone: 'Bot 已禁用并停止。',
|
||||||
|
enableFail: '启用失败,请查看后端日志。',
|
||||||
|
disableFail: '禁用失败,请查看后端日志。',
|
||||||
restart: '重启 Bot',
|
restart: '重启 Bot',
|
||||||
restartConfirm: (id: string) => `确认重启 Bot ${id}?`,
|
restartConfirm: (id: string) => `确认重启 Bot ${id}?`,
|
||||||
restartFail: '重启失败,请查看后端日志。',
|
restartFail: '重启失败,请查看后端日志。',
|
||||||
|
|
@ -122,6 +130,7 @@ export const dashboardZhCn = {
|
||||||
interruptSent: '已发送中断指令。',
|
interruptSent: '已发送中断指令。',
|
||||||
botStarting: 'Bot 正在启动中...',
|
botStarting: 'Bot 正在启动中...',
|
||||||
botStopping: 'Bot 正在停止中...',
|
botStopping: 'Bot 正在停止中...',
|
||||||
|
botDisabledHint: 'Bot 已禁用,请先启用后再进行操作。',
|
||||||
chatDisabled: 'Bot 已停止,对话区已禁用。',
|
chatDisabled: 'Bot 已停止,对话区已禁用。',
|
||||||
selectBot: '请选择 Bot 查看详情',
|
selectBot: '请选择 Bot 查看详情',
|
||||||
runtime: '运行状态',
|
runtime: '运行状态',
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-list .list-scroll {
|
.ops-bot-list .list-scroll {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
padding-top: 4px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
max-height: 72vh;
|
max-height: 72vh;
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +209,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--panel-soft);
|
background: linear-gradient(145deg, color-mix(in oklab, var(--panel-soft) 86%, var(--panel) 14%), color-mix(in oklab, var(--panel-soft) 94%, transparent 6%));
|
||||||
padding: 10px 10px 10px 14px;
|
padding: 10px 10px 10px 14px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -215,18 +217,42 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-card:hover {
|
.ops-bot-card:hover {
|
||||||
border-color: var(--brand);
|
border-color: color-mix(in oklab, var(--brand) 58%, var(--line) 42%);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 18px color-mix(in oklab, var(--brand) 14%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-card.is-active {
|
.ops-bot-card.is-active {
|
||||||
border-color: var(--brand);
|
border-color: color-mix(in oklab, var(--brand) 80%, var(--line) 20%);
|
||||||
box-shadow: 0 10px 24px color-mix(in oklab, var(--brand) 22%, transparent), inset 0 0 0 1px var(--brand);
|
box-shadow:
|
||||||
background: color-mix(in oklab, var(--panel-soft) 76%, var(--brand-soft) 24%);
|
0 0 0 2px color-mix(in oklab, var(--brand) 70%, transparent),
|
||||||
transform: translateY(-1px);
|
0 16px 30px color-mix(in oklab, var(--brand) 28%, transparent),
|
||||||
|
inset 0 0 0 1px color-mix(in oklab, var(--brand) 84%, transparent);
|
||||||
|
transform: translateY(0);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-bot-card.is-active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid color-mix(in oklab, var(--brand) 78%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-card.state-running {
|
||||||
|
background: linear-gradient(145deg, color-mix(in oklab, var(--ok) 14%, var(--panel-soft) 86%), color-mix(in oklab, var(--ok) 8%, var(--panel) 92%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-card.state-stopped {
|
||||||
|
background: linear-gradient(145deg, color-mix(in oklab, #b79aa2 14%, var(--panel-soft) 86%), color-mix(in oklab, #b79aa2 7%, var(--panel) 93%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-card.state-disabled {
|
||||||
|
background: linear-gradient(145deg, color-mix(in oklab, #9ca3b5 14%, var(--panel-soft) 86%), color-mix(in oklab, #9ca3b5 7%, var(--panel) 93%));
|
||||||
|
}
|
||||||
|
|
||||||
.ops-bot-top {
|
.ops-bot-top {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
@ -249,26 +275,89 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ops-bot-actions-main {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch-track {
|
||||||
|
position: relative;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--line) 82%, transparent);
|
||||||
|
background: color-mix(in oklab, #9ca3b5 42%, var(--panel-soft) 58%);
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch-track::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--text) 75%, #fff 25%);
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch input:checked + .ops-bot-enable-switch-track {
|
||||||
|
border-color: color-mix(in oklab, var(--ok) 66%, var(--line) 34%);
|
||||||
|
background: color-mix(in oklab, var(--ok) 46%, var(--panel-soft) 54%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch input:checked + .ops-bot-enable-switch-track::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-enable-switch input:disabled + .ops-bot-enable-switch-track {
|
||||||
|
opacity: 0.58;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.ops-bot-strip {
|
.ops-bot-strip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
width: 4px;
|
width: 3px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #d14b4b;
|
background: color-mix(in oklab, var(--line) 80%, transparent);
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-strip.is-running {
|
.ops-bot-strip.is-running {
|
||||||
background: #2ca476;
|
background: linear-gradient(180deg, color-mix(in oklab, var(--ok) 80%, #9be8c6 20%), color-mix(in oklab, var(--ok) 54%, transparent));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-strip.is-stopped {
|
.ops-bot-strip.is-stopped {
|
||||||
background: #d14b4b;
|
background: linear-gradient(180deg, color-mix(in oklab, var(--err) 74%, #e7b1ba 26%), color-mix(in oklab, var(--err) 54%, transparent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ops-bot-strip.is-disabled {
|
||||||
|
background: linear-gradient(180deg, color-mix(in oklab, #9ca3b5 82%, #d4d9e2 18%), color-mix(in oklab, #9ca3b5 52%, transparent));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-bot-icon-btn {
|
.ops-bot-icon-btn {
|
||||||
|
|
@ -3303,6 +3392,26 @@
|
||||||
background: #f7fbff;
|
background: #f7fbff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-shell[data-theme='light'] .ops-bot-card.state-running {
|
||||||
|
background: linear-gradient(145deg, #edfdf6, #f7fffb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell[data-theme='light'] .ops-bot-card.state-stopped {
|
||||||
|
background: linear-gradient(145deg, #fdf0f2, #fff7f8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell[data-theme='light'] .ops-bot-card.state-disabled {
|
||||||
|
background: linear-gradient(145deg, #eff2f6, #f8fafc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell[data-theme='light'] .ops-bot-card.is-active {
|
||||||
|
border-color: #3f74df;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px rgba(63, 116, 223, 0.45),
|
||||||
|
0 16px 32px rgba(63, 116, 223, 0.26),
|
||||||
|
inset 0 0 0 1px rgba(63, 116, 223, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell[data-theme='light'] .ops-preview,
|
.app-shell[data-theme='light'] .ops-preview,
|
||||||
.app-shell[data-theme='light'] .ops-status-pill {
|
.app-shell[data-theme='light'] .ops-status-pill {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
|
||||||
|
|
@ -956,7 +956,7 @@ export function BotDashboardModule({
|
||||||
const [operatingBotId, setOperatingBotId] = useState<string | null>(null);
|
const [operatingBotId, setOperatingBotId] = useState<string | null>(null);
|
||||||
const [sendingByBot, setSendingByBot] = useState<Record<string, boolean>>({});
|
const [sendingByBot, setSendingByBot] = useState<Record<string, boolean>>({});
|
||||||
const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({});
|
const [interruptingByBot, setInterruptingByBot] = useState<Record<string, boolean>>({});
|
||||||
const [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping'>>({});
|
const [controlStateByBot, setControlStateByBot] = useState<Record<string, 'starting' | 'stopping' | 'enabling' | 'disabling'>>({});
|
||||||
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
const chatBottomRef = useRef<HTMLDivElement | null>(null);
|
||||||
const chatScrollRef = useRef<HTMLDivElement | null>(null);
|
const chatScrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
const chatAutoFollowRef = useRef(true);
|
const chatAutoFollowRef = useRef(true);
|
||||||
|
|
@ -1080,6 +1080,14 @@ export function BotDashboardModule({
|
||||||
const [templateAgentText, setTemplateAgentText] = useState('');
|
const [templateAgentText, setTemplateAgentText] = useState('');
|
||||||
const [templateTopicText, setTemplateTopicText] = useState('');
|
const [templateTopicText, setTemplateTopicText] = useState('');
|
||||||
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
const [workspaceHoverCard, setWorkspaceHoverCard] = useState<WorkspaceHoverCardState | null>(null);
|
||||||
|
const botSearchInputName = useMemo(
|
||||||
|
() => `nbot-search-${Math.random().toString(36).slice(2, 10)}`,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const workspaceSearchInputName = useMemo(
|
||||||
|
() => `nbot-workspace-search-${Math.random().toString(36).slice(2, 10)}`,
|
||||||
|
[],
|
||||||
|
);
|
||||||
const voiceRecorderRef = useRef<MediaRecorder | null>(null);
|
const voiceRecorderRef = useRef<MediaRecorder | null>(null);
|
||||||
const voiceStreamRef = useRef<MediaStream | null>(null);
|
const voiceStreamRef = useRef<MediaStream | null>(null);
|
||||||
const voiceChunksRef = useRef<BlobPart[]>([]);
|
const voiceChunksRef = useRef<BlobPart[]>([]);
|
||||||
|
|
@ -1440,6 +1448,7 @@ export function BotDashboardModule({
|
||||||
[activeBots],
|
[activeBots],
|
||||||
);
|
);
|
||||||
const hasForcedBot = Boolean(String(forcedBotId || '').trim());
|
const hasForcedBot = Boolean(String(forcedBotId || '').trim());
|
||||||
|
const singleBotHomeMode = hasForcedBot;
|
||||||
const compactListFirstMode = compactMode && !hasForcedBot;
|
const compactListFirstMode = compactMode && !hasForcedBot;
|
||||||
const isCompactListPage = compactListFirstMode && !selectedBotId;
|
const isCompactListPage = compactListFirstMode && !selectedBotId;
|
||||||
const showCompactBotPageClose = compactListFirstMode && Boolean(selectedBotId);
|
const showCompactBotPageClose = compactListFirstMode && Boolean(selectedBotId);
|
||||||
|
|
@ -1545,8 +1554,9 @@ export function BotDashboardModule({
|
||||||
}
|
}
|
||||||
}, [templateTopicText, effectiveTopicPresetTemplates.length]);
|
}, [templateTopicText, effectiveTopicPresetTemplates.length]);
|
||||||
const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined;
|
const selectedBotControlState = selectedBot ? controlStateByBot[selectedBot.id] : undefined;
|
||||||
|
const selectedBotEnabled = Boolean(selectedBot && selectedBot.enabled !== false);
|
||||||
const isSending = selectedBot ? Boolean(sendingByBot[selectedBot.id]) : false;
|
const isSending = selectedBot ? Boolean(sendingByBot[selectedBot.id]) : false;
|
||||||
const canChat = Boolean(selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
const canChat = Boolean(selectedBotEnabled && selectedBot && selectedBot.docker_status === 'RUNNING' && !selectedBotControlState);
|
||||||
const isChatEnabled = Boolean(canChat && !isSending);
|
const isChatEnabled = Boolean(canChat && !isSending);
|
||||||
|
|
||||||
const conversation = useMemo(() => mergeConversation(messages), [messages]);
|
const conversation = useMemo(() => mergeConversation(messages), [messages]);
|
||||||
|
|
@ -3502,7 +3512,7 @@ export function BotDashboardModule({
|
||||||
|
|
||||||
const batchStartBots = async () => {
|
const batchStartBots = async () => {
|
||||||
if (isBatchOperating) return;
|
if (isBatchOperating) return;
|
||||||
const candidates = bots.filter((bot) => String(bot.docker_status || '').toUpperCase() !== 'RUNNING');
|
const candidates = bots.filter((bot) => bot.enabled !== false && String(bot.docker_status || '').toUpperCase() !== 'RUNNING');
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
notify(t.batchStartNone, { tone: 'warning' });
|
notify(t.batchStartNone, { tone: 'warning' });
|
||||||
return;
|
return;
|
||||||
|
|
@ -3536,7 +3546,7 @@ export function BotDashboardModule({
|
||||||
|
|
||||||
const batchStopBots = async () => {
|
const batchStopBots = async () => {
|
||||||
if (isBatchOperating) return;
|
if (isBatchOperating) return;
|
||||||
const candidates = bots.filter((bot) => String(bot.docker_status || '').toUpperCase() === 'RUNNING');
|
const candidates = bots.filter((bot) => bot.enabled !== false && String(bot.docker_status || '').toUpperCase() === 'RUNNING');
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
notify(t.batchStopNone, { tone: 'warning' });
|
notify(t.batchStopNone, { tone: 'warning' });
|
||||||
return;
|
return;
|
||||||
|
|
@ -3639,6 +3649,35 @@ export function BotDashboardModule({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setBotEnabled = async (id: string, enabled: boolean) => {
|
||||||
|
setOperatingBotId(id);
|
||||||
|
setControlStateByBot((prev) => ({ ...prev, [id]: enabled ? 'enabling' : 'disabling' }));
|
||||||
|
try {
|
||||||
|
if (enabled) {
|
||||||
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/enable`);
|
||||||
|
} else {
|
||||||
|
const ok = await confirm({
|
||||||
|
title: t.disable,
|
||||||
|
message: t.disableConfirm(id),
|
||||||
|
tone: 'warning',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
await axios.post(`${APP_ENDPOINTS.apiBase}/bots/${id}/disable`);
|
||||||
|
}
|
||||||
|
await refresh();
|
||||||
|
notify(enabled ? t.enableDone : t.disableDone, { tone: 'success' });
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.response?.data?.detail || (enabled ? t.enableFail : t.disableFail), { tone: 'error' });
|
||||||
|
} finally {
|
||||||
|
setOperatingBotId(null);
|
||||||
|
setControlStateByBot((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
if (!selectedBot || !canChat || isSending) return;
|
if (!selectedBot || !canChat || isSending) return;
|
||||||
if (!command.trim() && pendingAttachments.length === 0 && !quotedReply) return;
|
if (!command.trim() && pendingAttachments.length === 0 && !quotedReply) return;
|
||||||
|
|
@ -4686,15 +4725,22 @@ export function BotDashboardModule({
|
||||||
<div className="ops-searchbar">
|
<div className="ops-searchbar">
|
||||||
<input
|
<input
|
||||||
className="input ops-search-input ops-search-input-with-icon"
|
className="input ops-search-input ops-search-input-with-icon"
|
||||||
|
type="search"
|
||||||
value={botListQuery}
|
value={botListQuery}
|
||||||
onChange={(e) => setBotListQuery(e.target.value)}
|
onChange={(e) => setBotListQuery(e.target.value)}
|
||||||
placeholder={t.botSearchPlaceholder}
|
placeholder={t.botSearchPlaceholder}
|
||||||
aria-label={t.botSearchPlaceholder}
|
aria-label={t.botSearchPlaceholder}
|
||||||
autoComplete="off"
|
autoComplete="new-password"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
name="bot-search"
|
inputMode="search"
|
||||||
|
name={botSearchInputName}
|
||||||
|
id={botSearchInputName}
|
||||||
|
data-form-type="other"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
data-bwignore="true"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -4720,18 +4766,22 @@ export function BotDashboardModule({
|
||||||
const selected = selectedBotId === bot.id;
|
const selected = selectedBotId === bot.id;
|
||||||
const controlState = controlStateByBot[bot.id];
|
const controlState = controlStateByBot[bot.id];
|
||||||
const isOperating = operatingBotId === bot.id;
|
const isOperating = operatingBotId === bot.id;
|
||||||
|
const isEnabled = bot.enabled !== false;
|
||||||
const isStarting = controlState === 'starting';
|
const isStarting = controlState === 'starting';
|
||||||
const isStopping = controlState === 'stopping';
|
const isStopping = controlState === 'stopping';
|
||||||
|
const isEnabling = controlState === 'enabling';
|
||||||
|
const isDisabling = controlState === 'disabling';
|
||||||
|
const isRunning = String(bot.docker_status || '').toUpperCase() === 'RUNNING';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={bot.id}
|
key={bot.id}
|
||||||
className={`ops-bot-card ${selected ? 'is-active' : ''}`}
|
className={`ops-bot-card ${selected ? 'is-active' : ''} ${isEnabled ? (isRunning ? 'state-running' : 'state-stopped') : 'state-disabled'}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedBotId(bot.id);
|
setSelectedBotId(bot.id);
|
||||||
if (compactMode) setCompactPanelTab('chat');
|
if (compactMode) setCompactPanelTab('chat');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={`ops-bot-strip ${bot.docker_status === 'RUNNING' ? 'is-running' : 'is-stopped'}`} aria-hidden="true" />
|
<span className={`ops-bot-strip ${isEnabled ? (isRunning ? 'is-running' : 'is-stopped') : 'is-disabled'}`} aria-hidden="true" />
|
||||||
<div className="row-between ops-bot-top">
|
<div className="row-between ops-bot-top">
|
||||||
<div className="ops-bot-name-wrap">
|
<div className="ops-bot-name-wrap">
|
||||||
<div className="ops-bot-name-row">
|
<div className="ops-bot-name-row">
|
||||||
|
|
@ -4757,34 +4807,42 @@ export function BotDashboardModule({
|
||||||
<div className="mono ops-bot-id">{bot.id}</div>
|
<div className="mono ops-bot-id">{bot.id}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ops-bot-top-actions">
|
<div className="ops-bot-top-actions">
|
||||||
|
{!isEnabled ? (
|
||||||
|
<span className="badge badge-err">{t.disabled}</span>
|
||||||
|
) : null}
|
||||||
<span className={bot.docker_status === 'RUNNING' ? 'badge badge-ok' : 'badge badge-unknown'}>{bot.docker_status}</span>
|
<span className={bot.docker_status === 'RUNNING' ? 'badge badge-ok' : 'badge badge-unknown'}>{bot.docker_status}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ops-bot-meta">{t.image}: <span className="mono">{bot.image_tag || '-'}</span></div>
|
<div className="ops-bot-meta">{t.image}: <span className="mono">{bot.image_tag || '-'}</span></div>
|
||||||
<div className="ops-bot-actions">
|
<div className="ops-bot-actions">
|
||||||
<LucentIconButton
|
<label
|
||||||
className="btn btn-sm ops-bot-icon-btn ops-bot-action-monitor"
|
className="ops-bot-enable-switch"
|
||||||
onClick={(e) => {
|
title={isEnabled ? t.disable : t.enable}
|
||||||
e.stopPropagation();
|
onClick={(e) => e.stopPropagation()}
|
||||||
openResourceMonitor(bot.id);
|
|
||||||
}}
|
|
||||||
tooltip={isZh ? '资源监测' : 'Resource Monitor'}
|
|
||||||
aria-label={isZh ? '资源监测' : 'Resource Monitor'}
|
|
||||||
>
|
>
|
||||||
<Gauge size={14} />
|
<input
|
||||||
</LucentIconButton>
|
type="checkbox"
|
||||||
{bot.docker_status === 'RUNNING' ? (
|
checked={isEnabled}
|
||||||
|
disabled={isOperating || isEnabling || isDisabling}
|
||||||
|
onChange={(e) => {
|
||||||
|
void setBotEnabled(bot.id, e.target.checked);
|
||||||
|
}}
|
||||||
|
aria-label={isZh ? '启用/停用' : 'Enable/Disable'}
|
||||||
|
/>
|
||||||
|
<span className="ops-bot-enable-switch-track" />
|
||||||
|
</label>
|
||||||
|
<div className="ops-bot-actions-main">
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-sm ops-bot-icon-btn ops-bot-action-stop"
|
className={`btn btn-sm ops-bot-icon-btn ${isRunning ? 'ops-bot-action-stop' : 'ops-bot-action-start'}`}
|
||||||
disabled={isOperating}
|
disabled={isOperating || !isEnabled}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void stopBot(bot.id, bot.docker_status);
|
void (isRunning ? stopBot(bot.id, bot.docker_status) : startBot(bot.id, bot.docker_status));
|
||||||
}}
|
}}
|
||||||
tooltip={t.stop}
|
tooltip={isRunning ? t.stop : t.start}
|
||||||
aria-label={t.stop}
|
aria-label={isRunning ? t.stop : t.start}
|
||||||
>
|
>
|
||||||
{isStopping ? (
|
{isStarting || isStopping ? (
|
||||||
<span className="ops-control-pending">
|
<span className="ops-control-pending">
|
||||||
<span className="ops-control-dots" aria-hidden="true">
|
<span className="ops-control-dots" aria-hidden="true">
|
||||||
<i />
|
<i />
|
||||||
|
|
@ -4792,41 +4850,33 @@ export function BotDashboardModule({
|
||||||
<i />
|
<i />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : <Square size={14} />}
|
) : isRunning ? <Square size={14} /> : <Power size={14} />}
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
) : (
|
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-sm ops-bot-icon-btn ops-bot-action-start"
|
className="btn btn-sm ops-bot-icon-btn ops-bot-action-monitor"
|
||||||
disabled={isOperating}
|
disabled={isOperating || !isEnabled}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void startBot(bot.id, bot.docker_status);
|
openResourceMonitor(bot.id);
|
||||||
}}
|
}}
|
||||||
tooltip={t.start}
|
tooltip={isZh ? '资源监测' : 'Resource Monitor'}
|
||||||
aria-label={t.start}
|
aria-label={isZh ? '资源监测' : 'Resource Monitor'}
|
||||||
>
|
>
|
||||||
{isStarting ? (
|
<Gauge size={14} />
|
||||||
<span className="ops-control-pending">
|
|
||||||
<span className="ops-control-dots" aria-hidden="true">
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
<i />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : <Power size={14} />}
|
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
)}
|
<LucentIconButton
|
||||||
<LucentIconButton
|
className="btn btn-sm ops-bot-icon-btn ops-bot-action-delete"
|
||||||
className="btn btn-sm ops-bot-icon-btn ops-bot-action-delete"
|
disabled={isOperating || !isEnabled}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void removeBot(bot.id);
|
void removeBot(bot.id);
|
||||||
}}
|
}}
|
||||||
tooltip={t.delete}
|
tooltip={t.delete}
|
||||||
aria-label={t.delete}
|
aria-label={t.delete}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -5138,7 +5188,9 @@ export function BotDashboardModule({
|
||||||
? t.botStarting
|
? t.botStarting
|
||||||
: selectedBotControlState === 'stopping'
|
: selectedBotControlState === 'stopping'
|
||||||
? t.botStopping
|
? t.botStopping
|
||||||
: t.chatDisabled}
|
: !selectedBotEnabled
|
||||||
|
? t.botDisabledHint
|
||||||
|
: t.chatDisabled}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -5163,18 +5215,21 @@ export function BotDashboardModule({
|
||||||
<div className="row-between ops-runtime-head">
|
<div className="row-between ops-runtime-head">
|
||||||
<h2 style={{ fontSize: 18 }}>{t.runtime}</h2>
|
<h2 style={{ fontSize: 18 }}>{t.runtime}</h2>
|
||||||
<div className="ops-panel-tools" ref={runtimeMenuRef}>
|
<div className="ops-panel-tools" ref={runtimeMenuRef}>
|
||||||
<LucentIconButton
|
{!singleBotHomeMode ? (
|
||||||
className="btn btn-secondary btn-sm icon-btn"
|
<LucentIconButton
|
||||||
onClick={() => void restartBot(selectedBot.id, selectedBot.docker_status)}
|
className="btn btn-secondary btn-sm icon-btn"
|
||||||
disabled={operatingBotId === selectedBot.id}
|
onClick={() => void restartBot(selectedBot.id, selectedBot.docker_status)}
|
||||||
tooltip={t.restart}
|
disabled={operatingBotId === selectedBot.id || !selectedBotEnabled}
|
||||||
aria-label={t.restart}
|
tooltip={t.restart}
|
||||||
>
|
aria-label={t.restart}
|
||||||
<RotateCcw size={14} className={operatingBotId === selectedBot.id ? 'animate-spin' : ''} />
|
>
|
||||||
</LucentIconButton>
|
<RotateCcw size={14} className={operatingBotId === selectedBot.id ? 'animate-spin' : ''} />
|
||||||
|
</LucentIconButton>
|
||||||
|
) : null}
|
||||||
<LucentIconButton
|
<LucentIconButton
|
||||||
className="btn btn-secondary btn-sm icon-btn"
|
className="btn btn-secondary btn-sm icon-btn"
|
||||||
onClick={() => setRuntimeMenuOpen((v) => !v)}
|
onClick={() => setRuntimeMenuOpen((v) => !v)}
|
||||||
|
disabled={!selectedBotEnabled}
|
||||||
tooltip={runtimeMoreLabel}
|
tooltip={runtimeMoreLabel}
|
||||||
aria-label={runtimeMoreLabel}
|
aria-label={runtimeMoreLabel}
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
|
|
@ -5184,126 +5239,130 @@ export function BotDashboardModule({
|
||||||
</LucentIconButton>
|
</LucentIconButton>
|
||||||
{runtimeMenuOpen ? (
|
{runtimeMenuOpen ? (
|
||||||
<div className="ops-more-menu" role="menu" aria-label={runtimeMoreLabel}>
|
<div className="ops-more-menu" role="menu" aria-label={runtimeMoreLabel}>
|
||||||
<button
|
{!singleBotHomeMode ? (
|
||||||
className="ops-more-item"
|
<>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
void (async () => {
|
onClick={() => {
|
||||||
const detail = await ensureSelectedBotDetail();
|
setRuntimeMenuOpen(false);
|
||||||
applyEditFormFromBot(detail);
|
void (async () => {
|
||||||
setProviderTestResult('');
|
const detail = await ensureSelectedBotDetail();
|
||||||
setShowBaseModal(true);
|
applyEditFormFromBot(detail);
|
||||||
})();
|
setProviderTestResult('');
|
||||||
}}
|
setShowBaseModal(true);
|
||||||
>
|
})();
|
||||||
<Settings2 size={14} />
|
}}
|
||||||
<span>{t.base}</span>
|
>
|
||||||
</button>
|
<Settings2 size={14} />
|
||||||
<button
|
<span>{t.base}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
void (async () => {
|
onClick={() => {
|
||||||
const detail = await ensureSelectedBotDetail();
|
setRuntimeMenuOpen(false);
|
||||||
applyEditFormFromBot(detail);
|
void (async () => {
|
||||||
setShowParamModal(true);
|
const detail = await ensureSelectedBotDetail();
|
||||||
})();
|
applyEditFormFromBot(detail);
|
||||||
}}
|
setShowParamModal(true);
|
||||||
>
|
})();
|
||||||
<SlidersHorizontal size={14} />
|
}}
|
||||||
<span>{t.params}</span>
|
>
|
||||||
</button>
|
<SlidersHorizontal size={14} />
|
||||||
<button
|
<span>{t.params}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
if (!selectedBot) return;
|
onClick={() => {
|
||||||
openChannelModal(selectedBot.id);
|
setRuntimeMenuOpen(false);
|
||||||
}}
|
if (!selectedBot) return;
|
||||||
>
|
openChannelModal(selectedBot.id);
|
||||||
<Waypoints size={14} />
|
}}
|
||||||
<span>{t.channels}</span>
|
>
|
||||||
</button>
|
<Waypoints size={14} />
|
||||||
<button
|
<span>{t.channels}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
if (!selectedBot) return;
|
onClick={() => {
|
||||||
openTopicModal(selectedBot.id);
|
setRuntimeMenuOpen(false);
|
||||||
}}
|
if (!selectedBot) return;
|
||||||
>
|
openTopicModal(selectedBot.id);
|
||||||
<MessageSquareText size={14} />
|
}}
|
||||||
<span>{t.topic}</span>
|
>
|
||||||
</button>
|
<MessageSquareText size={14} />
|
||||||
<button
|
<span>{t.topic}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
if (!selectedBot) return;
|
onClick={() => {
|
||||||
void loadBotEnvParams(selectedBot.id);
|
setRuntimeMenuOpen(false);
|
||||||
setShowEnvParamsModal(true);
|
if (!selectedBot) return;
|
||||||
}}
|
void loadBotEnvParams(selectedBot.id);
|
||||||
>
|
setShowEnvParamsModal(true);
|
||||||
<Settings2 size={14} />
|
}}
|
||||||
<span>{t.envParams}</span>
|
>
|
||||||
</button>
|
<Settings2 size={14} />
|
||||||
<button
|
<span>{t.envParams}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
if (!selectedBot) return;
|
onClick={() => {
|
||||||
void loadBotSkills(selectedBot.id);
|
setRuntimeMenuOpen(false);
|
||||||
setShowSkillsModal(true);
|
if (!selectedBot) return;
|
||||||
}}
|
void loadBotSkills(selectedBot.id);
|
||||||
>
|
setShowSkillsModal(true);
|
||||||
<Hammer size={14} />
|
}}
|
||||||
<span>{t.skills}</span>
|
>
|
||||||
</button>
|
<Hammer size={14} />
|
||||||
<button
|
<span>{t.skills}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
if (!selectedBot) return;
|
onClick={() => {
|
||||||
openMcpModal(selectedBot.id);
|
setRuntimeMenuOpen(false);
|
||||||
}}
|
if (!selectedBot) return;
|
||||||
>
|
openMcpModal(selectedBot.id);
|
||||||
<Boxes size={14} />
|
}}
|
||||||
<span>{t.mcp}</span>
|
>
|
||||||
</button>
|
<Boxes size={14} />
|
||||||
<button
|
<span>{t.mcp}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
if (selectedBot) void loadCronJobs(selectedBot.id);
|
onClick={() => {
|
||||||
setShowCronModal(true);
|
setRuntimeMenuOpen(false);
|
||||||
}}
|
if (selectedBot) void loadCronJobs(selectedBot.id);
|
||||||
>
|
setShowCronModal(true);
|
||||||
<Clock3 size={14} />
|
}}
|
||||||
<span>{t.cronViewer}</span>
|
>
|
||||||
</button>
|
<Clock3 size={14} />
|
||||||
<button
|
<span>{t.cronViewer}</span>
|
||||||
className="ops-more-item"
|
</button>
|
||||||
role="menuitem"
|
<button
|
||||||
onClick={() => {
|
className="ops-more-item"
|
||||||
setRuntimeMenuOpen(false);
|
role="menuitem"
|
||||||
void (async () => {
|
onClick={() => {
|
||||||
const detail = await ensureSelectedBotDetail();
|
setRuntimeMenuOpen(false);
|
||||||
applyEditFormFromBot(detail);
|
void (async () => {
|
||||||
setShowAgentModal(true);
|
const detail = await ensureSelectedBotDetail();
|
||||||
})();
|
applyEditFormFromBot(detail);
|
||||||
}}
|
setShowAgentModal(true);
|
||||||
>
|
})();
|
||||||
<FileText size={14} />
|
}}
|
||||||
<span>{t.agent}</span>
|
>
|
||||||
</button>
|
<FileText size={14} />
|
||||||
|
<span>{t.agent}</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
className="ops-more-item"
|
className="ops-more-item"
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
|
|
@ -5383,10 +5442,22 @@ export function BotDashboardModule({
|
||||||
<div className="ops-searchbar">
|
<div className="ops-searchbar">
|
||||||
<input
|
<input
|
||||||
className="input ops-search-input ops-search-input-with-icon"
|
className="input ops-search-input ops-search-input-with-icon"
|
||||||
|
type="search"
|
||||||
value={workspaceQuery}
|
value={workspaceQuery}
|
||||||
onChange={(e) => setWorkspaceQuery(e.target.value)}
|
onChange={(e) => setWorkspaceQuery(e.target.value)}
|
||||||
placeholder={t.workspaceSearchPlaceholder}
|
placeholder={t.workspaceSearchPlaceholder}
|
||||||
aria-label={t.workspaceSearchPlaceholder}
|
aria-label={t.workspaceSearchPlaceholder}
|
||||||
|
autoComplete="new-password"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="none"
|
||||||
|
spellCheck={false}
|
||||||
|
inputMode="search"
|
||||||
|
name={workspaceSearchInputName}
|
||||||
|
id={workspaceSearchInputName}
|
||||||
|
data-form-type="other"
|
||||||
|
data-lpignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
data-bwignore="true"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export const useAppStore = create<AppStore>((set) => ({
|
||||||
nextBots[bot.id] = {
|
nextBots[bot.id] = {
|
||||||
...prev,
|
...prev,
|
||||||
...bot,
|
...bot,
|
||||||
|
enabled: preferDefined(bot.enabled, prev?.enabled),
|
||||||
image_tag: preferDefined(bot.image_tag, prev?.image_tag),
|
image_tag: preferDefined(bot.image_tag, prev?.image_tag),
|
||||||
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
|
llm_provider: preferDefined(bot.llm_provider, prev?.llm_provider),
|
||||||
llm_model: preferDefined(bot.llm_model, prev?.llm_model),
|
llm_model: preferDefined(bot.llm_model, prev?.llm_model),
|
||||||
|
|
@ -103,6 +104,7 @@ export const useAppStore = create<AppStore>((set) => ({
|
||||||
[bot.id]: {
|
[bot.id]: {
|
||||||
...prev,
|
...prev,
|
||||||
...bot,
|
...bot,
|
||||||
|
enabled: preferDefined(bot.enabled, prev?.enabled),
|
||||||
logs: prev?.logs ?? bot.logs ?? [],
|
logs: prev?.logs ?? bot.logs ?? [],
|
||||||
messages: prev?.messages ?? bot.messages ?? [],
|
messages: prev?.messages ?? bot.messages ?? [],
|
||||||
events: prev?.events ?? bot.events ?? [],
|
events: prev?.events ?? bot.events ?? [],
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export interface BotEvent {
|
||||||
export interface BotState {
|
export interface BotState {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
enabled?: boolean;
|
||||||
access_password?: string;
|
access_password?: string;
|
||||||
has_access_password?: boolean;
|
has_access_password?: boolean;
|
||||||
avatar_model: string;
|
avatar_model: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue