feat: enhance itchat adapter with runtime status tracking and UI integration

This commit is contained in:
fdc310
2026-07-02 13:56:24 +08:00
parent 78fb40a28a
commit d4e8ccd161
8 changed files with 292 additions and 61 deletions
@@ -1,6 +1,7 @@
import quart import quart
import mimetypes import mimetypes
import asyncio import asyncio
import os
from ... import group from ... import group
from langbot.pkg.utils import importutil from langbot.pkg.utils import importutil
@@ -688,6 +689,10 @@ class AdaptersRouterGroup(group.RouterGroup):
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
status_dir = os.path.join('data', 'itchat')
os.makedirs(status_dir, exist_ok=True)
default_status_path = os.path.join(status_dir, 'itchat.pkl')
qr_path = os.path.join(status_dir, f'{session_id}-QR.png')
session = { session = {
'status': 'pending', 'status': 'pending',
@@ -704,12 +709,11 @@ class AdaptersRouterGroup(group.RouterGroup):
def _run_itchat_login(): def _run_itchat_login():
try: try:
import os
from itchat.core import Core from itchat.core import Core
from itchat.content import TEXT as _TEXT from itchat.content import TEXT as _TEXT
from langbot.pkg.platform.sources.itchat import ItchatAdapter
for f in ('itchat.pkl', 'QR.png'): for f in (qr_path,):
try: try:
os.remove(f) os.remove(f)
except OSError: except OSError:
@@ -722,16 +726,8 @@ class AdaptersRouterGroup(group.RouterGroup):
try: try:
_core.get_friends(update=True) _core.get_friends(update=True)
user_info = _core.loginInfo.get('User', {}) user_info = _core.loginInfo.get('User', {})
nick = ( nick = ItchatAdapter._get_obj_value(user_info, 'NickName', 'unknown')
getattr(user_info, 'NickName', '') or user_info.get('NickName', '') wxid = ItchatAdapter._get_obj_value(user_info, 'UserName')
if isinstance(user_info, dict)
else 'unknown'
)
wxid = (
getattr(user_info, 'UserName', '') or user_info.get('UserName', '')
if isinstance(user_info, dict)
else ''
)
except Exception: except Exception:
nick = 'unknown' nick = 'unknown'
wxid = '' wxid = ''
@@ -742,8 +738,12 @@ class AdaptersRouterGroup(group.RouterGroup):
print(f'[itchat-login] Login success: {nick}', flush=True) print(f'[itchat-login] Login success: {nick}', flush=True)
# Dump login status so the adapter can hot-reload it # Dump login status so the adapter can hot-reload it
try: try:
_core.dump_login_status('itchat.pkl') account_status_path = ItchatAdapter.login_status_path_for_account(wxid)
print('[itchat-login] Session saved to itchat.pkl', flush=True) _core.dump_login_status(account_status_path)
if account_status_path != default_status_path:
_core.dump_login_status(default_status_path)
session['login_status_path'] = account_status_path
print(f'[itchat-login] Session saved to {account_status_path}', flush=True)
except Exception: except Exception:
pass pass
# Stop the message loop - we only needed the session for QR login # Stop the message loop - we only needed the session for QR login
@@ -813,6 +813,7 @@ class AdaptersRouterGroup(group.RouterGroup):
'session_id': session_id, 'session_id': session_id,
'status': 'success', 'status': 'success',
'nickname': session['nickname'], 'nickname': session['nickname'],
'wxid': session.get('wxid', ''),
} }
) )
@@ -845,8 +846,7 @@ class AdaptersRouterGroup(group.RouterGroup):
if session['status'] == 'success': if session['status'] == 'success':
data['nickname'] = session.get('nickname', '') data['nickname'] = session.get('nickname', '')
data['wxid'] = session.get('wxid', '') data['wxid'] = session.get('wxid', '')
# Keep session alive (don't pop) - itchat thread keeps running _itchat_login_sessions.pop(session_id, None)
# so the pickle file is valid for the adapter to reuse
elif session['status'] == 'error': elif session['status'] == 'error':
data['error'] = session['error'] data['error'] = session['error']
_itchat_login_sessions.pop(session_id, None) _itchat_login_sessions.pop(session_id, None)
+2
View File
@@ -57,6 +57,8 @@ class BotService:
runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid) runtime_bot = await self.ap.platform_mgr.get_bot_by_uuid(bot_uuid)
if runtime_bot is not None: if runtime_bot is not None:
adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id adapter_runtime_values['bot_account_id'] = runtime_bot.adapter.bot_account_id
if hasattr(runtime_bot.adapter, 'get_runtime_status'):
adapter_runtime_values['runtime_status'] = runtime_bot.adapter.get_runtime_status()
# Webhook URL for unified webhook adapters (independent of bot running state) # Webhook URL for unified webhook adapters (independent of bot running state)
if persistence_bot['adapter'] in [ if persistence_bot['adapter'] in [
+151 -34
View File
@@ -12,8 +12,10 @@ from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import os import os
import re
import tempfile import tempfile
import threading import threading
import time
import traceback import traceback
import typing import typing
@@ -228,7 +230,7 @@ class ItchatEventConverter(abstract_platform_adapter.AbstractEventConverter):
return platform_events.GroupMessage( return platform_events.GroupMessage(
sender=platform_entities.GroupMember( sender=platform_entities.GroupMember(
id=actual_nick or actual_user, # use nickname for @ mention id=actual_user or actual_nick,
member_name=actual_nick or actual_user, member_name=actual_nick or actual_user,
permission=platform_entities.Permission.Member, permission=platform_entities.Permission.Member,
group=platform_entities.Group( group=platform_entities.Group(
@@ -285,6 +287,11 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
_core: typing.Optional[ItchatCore] = pydantic.PrivateAttr(default=None) _core: typing.Optional[ItchatCore] = pydantic.PrivateAttr(default=None)
_bot_nickname: str = pydantic.PrivateAttr(default='') _bot_nickname: str = pydantic.PrivateAttr(default='')
_bot_uuid: typing.Optional[str] = pydantic.PrivateAttr(default=None) _bot_uuid: typing.Optional[str] = pydantic.PrivateAttr(default=None)
_startup_error: typing.Optional[str] = pydantic.PrivateAttr(default=None)
_connection_status: str = pydantic.PrivateAttr(default='disconnected')
_connection_error: str = pydantic.PrivateAttr(default='')
_last_connected_at: typing.Optional[float] = pydantic.PrivateAttr(default=None)
_last_disconnected_at: typing.Optional[float] = pydantic.PrivateAttr(default=None)
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
@@ -308,6 +315,67 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
self._logged_in = threading.Event() self._logged_in = threading.Event()
self._itchat_thread = None self._itchat_thread = None
self._core = ItchatCore() self._core = ItchatCore()
self._startup_error = None
self._connection_status = 'disconnected'
self._connection_error = ''
self._last_connected_at = None
self._last_disconnected_at = None
@staticmethod
def _get_obj_value(obj: typing.Any, key: str, default: str = '') -> str:
if isinstance(obj, dict):
return obj.get(key, default) or default
return getattr(obj, key, default) or default
@staticmethod
def _safe_status_name(value: str) -> str:
cleaned = re.sub(r'[^A-Za-z0-9_.@-]+', '_', value.strip())
cleaned = cleaned.strip('._')
return cleaned or 'itchat'
@staticmethod
def login_status_dir() -> str:
path = os.path.join('data', 'itchat')
os.makedirs(path, exist_ok=True)
return path
@classmethod
def login_status_path_for_account(cls, account_id: str = '') -> str:
filename = f'{cls._safe_status_name(account_id)}.pkl' if account_id else 'itchat.pkl'
return os.path.join(cls.login_status_dir(), filename)
def _login_status_path(self) -> str:
configured_path = self.config.get('login_status_path', '').strip()
if configured_path:
return configured_path
account_id = self.config.get('account_id', '').strip()
if account_id:
account_path = self.login_status_path_for_account(account_id)
if os.path.exists(account_path):
return account_path
return self.login_status_path_for_account()
def set_bot_uuid(self, bot_uuid: str):
self._bot_uuid = bot_uuid
def _set_connection_status(self, status: str, error: str = ''):
self._connection_status = status
self._connection_error = error
now = time.time()
if status == 'connected':
self._last_connected_at = now
elif status in {'disconnected', 'error'}:
self._last_disconnected_at = now
def get_runtime_status(self) -> dict:
return {
'connection_status': self._connection_status,
'connection_error': self._connection_error,
'last_connected_at': self._last_connected_at,
'last_disconnected_at': self._last_disconnected_at,
}
def _on_login(self): def _on_login(self):
"""Called by itchat after successful QR code login.""" """Called by itchat after successful QR code login."""
@@ -318,16 +386,8 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
# Get bot's own WeChat info from loginInfo['User'] # Get bot's own WeChat info from loginInfo['User']
user_info = self._core.loginInfo.get('User', {}) user_info = self._core.loginInfo.get('User', {})
nick_name = ( nick_name = self._get_obj_value(user_info, 'NickName')
getattr(user_info, 'NickName', '') or user_info.get('NickName', '') user_name = self._get_obj_value(user_info, 'UserName')
if isinstance(user_info, dict)
else ''
)
user_name = (
getattr(user_info, 'UserName', '') or user_info.get('UserName', '')
if isinstance(user_info, dict)
else ''
)
# bot_account_id: config override or auto-detected wxid # bot_account_id: config override or auto-detected wxid
# Used by AtBotRule for matching At.target # Used by AtBotRule for matching At.target
@@ -336,23 +396,27 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
# _bot_nickname: config override or auto-detected nickname # _bot_nickname: config override or auto-detected nickname
configured_nick = self.config.get('nickname', '').strip() configured_nick = self.config.get('nickname', '').strip()
self._bot_nickname = configured_nick or nick_name self._bot_nickname = configured_nick or nick_name
self._set_connection_status('connected')
# Log available groups try:
chatrooms = self._core.search_chatrooms() chatrooms = self._core.search_chatrooms() or []
group_names = [] group_names = []
for c in chatrooms: for c in chatrooms:
name = getattr(c, 'NickName', '') or c.get('NickName', '') if isinstance(c, dict) else str(c) name = self._get_obj_value(c, 'NickName', str(c))
if name: if name:
group_names.append(name) group_names.append(name)
if group_names: if group_names:
self._log_sync( self._log_sync(
f'itchat login as {nick_name} ({user_name}) | Groups ({len(group_names)}): {", ".join(group_names[:10])}{"..." if len(group_names) > 10 else ""}' f'itchat login as {nick_name} ({user_name}) | Groups ({len(group_names)}): {", ".join(group_names[:10])}{"..." if len(group_names) > 10 else ""}'
) )
else: else:
self._log_sync(f'itchat login as {nick_name} ({user_name}) | No groups found') self._log_sync(f'itchat login as {nick_name} ({user_name}) | No groups found')
except Exception as e:
self._log_sync(f'itchat login as {nick_name} ({user_name}) | Failed to list groups: {e}', 'warning')
except Exception as e: except Exception as e:
self.bot_account_id = f'WeChat Bot (Error: {e})' self.bot_account_id = f'WeChat Bot (Error: {e})'
self._set_connection_status('error', str(e))
finally: finally:
self._logged_in.set() self._logged_in.set()
@@ -412,6 +476,7 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
def _on_exit(self): def _on_exit(self):
"""Called by itchat on exit.""" """Called by itchat on exit."""
self._set_connection_status('disconnected')
self._log_sync('itchat session exited') self._log_sync('itchat session exited')
def _register_itchat_handlers(self): def _register_itchat_handlers(self):
@@ -472,7 +537,7 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
self._loop, self._loop,
) )
except Exception: except Exception:
self.logger.error(f'Error dispatching itchat message: {traceback.format_exc()}') self._log_sync(f'Error dispatching itchat message: {traceback.format_exc()}', 'error')
async def send_message( async def send_message(
self, self,
@@ -513,7 +578,7 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
await loop.run_in_executor(None, self._core.send, f'@fil@{temp_path}', target_id) await loop.run_in_executor(None, self._core.send, f'@fil@{temp_path}', target_id)
self._cleanup_temp(temp_path) self._cleanup_temp(temp_path)
except Exception: except Exception:
self.logger.error(f'Failed to send itchat message: {traceback.format_exc()}') await self.logger.error(f'Failed to send itchat message: {traceback.format_exc()}')
def _save_to_temp(self, item: dict, prefix: str) -> typing.Optional[str]: def _save_to_temp(self, item: dict, prefix: str) -> typing.Optional[str]:
"""Save base64 or URL data to a temp file and return the path.""" """Save base64 or URL data to a temp file and return the path."""
@@ -540,7 +605,7 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
f.write(resp.content) f.write(resp.content)
return temp_path return temp_path
except Exception: except Exception:
self.logger.error(f'Failed to save temp file: {traceback.format_exc()}') self._log_sync(f'Failed to save temp file: {traceback.format_exc()}', 'error')
return None return None
def _cleanup_temp(self, path: str): def _cleanup_temp(self, path: str):
@@ -551,6 +616,34 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
except OSError: except OSError:
pass pass
def _prepare_reply_message(
self,
message_source: platform_events.MessageEvent,
message: platform_message.MessageChain,
) -> platform_message.MessageChain:
"""Render group sender mentions with display names while keeping internal IDs stable."""
if not isinstance(message_source, platform_events.GroupMessage):
return message
source_msg = message_source.source_platform_object or {}
actual_user = source_msg.get('ActualUserName', '')
actual_nick = source_msg.get('ActualNickName', '')
if not actual_user or not actual_nick:
return message
components: list[platform_message.MessageComponent] = []
changed = False
for component in message:
if isinstance(component, platform_message.At) and str(component.target) == str(actual_user):
components.append(platform_message.Plain(text=f'@{actual_nick} '))
changed = True
else:
components.append(component)
if not changed:
return message
return platform_message.MessageChain(components)
async def reply_message( async def reply_message(
self, self,
message_source: platform_events.MessageEvent, message_source: platform_events.MessageEvent,
@@ -567,7 +660,7 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if not from_user: if not from_user:
return return
await self.send_message('friend', from_user, message) await self.send_message('friend', from_user, self._prepare_reply_message(message_source, message))
def register_listener( def register_listener(
self, self,
@@ -594,6 +687,9 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
itchat will reuse it without requiring a new QR scan. itchat will reuse it without requiring a new QR scan.
""" """
self._loop = asyncio.get_running_loop() self._loop = asyncio.get_running_loop()
self._logged_in.clear()
self._startup_error = None
self._set_connection_status('connecting')
await self.logger.info('itchat adapter starting...') await self.logger.info('itchat adapter starting...')
@@ -603,27 +699,45 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
# Run itchat in a daemon thread (it blocks) # Run itchat in a daemon thread (it blocks)
def _run_itchat(): def _run_itchat():
try: try:
status_path = self._login_status_path()
if not os.path.exists(status_path):
self._startup_error = (
f'No cached WeChat session found at {status_path}. '
'Please scan the QR code in the bot config page first.'
)
self._set_connection_status('error', self._startup_error)
self._log_sync(self._startup_error, 'error')
self._logged_in.set()
return
# Use hotReload to reuse the cached session from QR login # Use hotReload to reuse the cached session from QR login
# If no cache exists, fail fast instead of triggering QR login # If no cache exists, fail fast instead of triggering QR login
result = self._core.load_login_status( result = self._core.load_login_status(
'itchat.pkl', loginCallback=self._on_login, exitCallback=self._on_exit status_path, loginCallback=self._on_login, exitCallback=self._on_exit
) )
if result['BaseResponse']['Ret'] != 0: if result.get('BaseResponse', {}).get('Ret') != 0:
self.logger.error( self._startup_error = (
'No cached WeChat session found. Please scan the QR code in the bot config page first.' f'Cached WeChat session at {status_path} is invalid. '
'Please scan the QR code in the bot config page again.'
) )
self._set_connection_status('error', self._startup_error)
self._log_sync(self._startup_error, 'error')
self._logged_in.set() self._logged_in.set()
return return
# Session loaded, start message loop # Session loaded, start message loop
self._log_sync('WeChat session loaded from cache') self._log_sync(f'WeChat session loaded from cache: {status_path}')
# Clear stale messages that itchat fetched during hot-reload # Clear stale messages that itchat fetched during hot-reload
self._drain_msglist() self._drain_msglist()
self._core.run(blockThread=True) self._core.run(blockThread=True)
self._set_connection_status('disconnected')
self._log_sync('itchat message loop stopped', 'error')
except Exception as e: except Exception as e:
self._log_sync(f'itchat run error: {e}', 'error') error = f'itchat run error: {e}'
self._set_connection_status('error', error)
self._log_sync(error, 'error')
self._logged_in.set() self._logged_in.set()
self._itchat_thread = threading.Thread(target=_run_itchat, daemon=True, name='itchat-thread') self._itchat_thread = threading.Thread(target=_run_itchat, daemon=True, name='itchat-thread')
@@ -634,6 +748,8 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if not self._logged_in.is_set(): if not self._logged_in.is_set():
raise RuntimeError('itchat login timed out (300s)') raise RuntimeError('itchat login timed out (300s)')
if self._startup_error:
raise RuntimeError(self._startup_error)
await self.logger.info(f'itchat adapter running, bot: {self.bot_account_id}') await self.logger.info(f'itchat adapter running, bot: {self.bot_account_id}')
@@ -650,5 +766,6 @@ class ItchatAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
self._core.isLogging = False self._core.isLogging = False
except Exception: except Exception:
pass pass
self._set_connection_status('disconnected')
await self.logger.info('itchat adapter stopped') await self.logger.info('itchat adapter stopped')
return True return True
+87 -9
View File
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { import {
@@ -26,9 +27,79 @@ import type { BotSessionMonitorHandle } from '@/app/home/bots/components/bot-ses
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext'; import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Settings, FileText, Users, RefreshCw, Trash2 } from 'lucide-react'; import {
Settings,
FileText,
Users,
RefreshCw,
Trash2,
CircleCheck,
CircleAlert,
Loader2,
CircleOff,
} from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import type { Bot, BotAdapterRuntimeStatus } from '@/app/infra/entities/api';
function getBotRuntimeStatus(bot: Bot | null): BotAdapterRuntimeStatus | null {
return bot?.adapter_runtime_values?.runtime_status ?? null;
}
function RuntimeStatusBadge({
status,
}: {
status: BotAdapterRuntimeStatus | null;
}) {
const { t } = useTranslation();
if (!status) return null;
const value = status?.connection_status ?? 'disconnected';
const config = {
connected: {
label: t('bots.runtimeConnected'),
className: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700',
icon: CircleCheck,
},
connecting: {
label: t('bots.runtimeConnecting'),
className: 'border-amber-500/30 bg-amber-500/10 text-amber-700',
icon: Loader2,
},
disconnected: {
label: t('bots.runtimeDisconnected'),
className: 'border-muted-foreground/20 bg-muted text-muted-foreground',
icon: CircleOff,
},
error: {
label: t('bots.runtimeError'),
className: 'border-destructive/30 bg-destructive/10 text-destructive',
icon: CircleAlert,
},
}[value];
const Icon = config.icon;
return (
<div className="flex min-w-0 items-center gap-2">
<Badge
variant="outline"
className={cn('h-6 gap-1.5 px-2 text-xs', config.className)}
>
<Icon
className={cn('size-3.5', value === 'connecting' && 'animate-spin')}
/>
{config.label}
</Badge>
{status?.connection_error && (
<span className="max-w-[360px] truncate text-xs text-destructive">
{status.connection_error}
</span>
)}
</div>
);
}
export default function BotDetailContent({ id }: { id: string }) { export default function BotDetailContent({ id }: { id: string }) {
const isCreateMode = id === 'new'; const isCreateMode = id === 'new';
@@ -58,16 +129,24 @@ export default function BotDetailContent({ id }: { id: string }) {
// Enable state managed here so the header switch works // Enable state managed here so the header switch works
const [botEnabled, setBotEnabled] = useState(true); const [botEnabled, setBotEnabled] = useState(true);
const [enableLoaded, setEnableLoaded] = useState(false); const [enableLoaded, setEnableLoaded] = useState(false);
const [botDetail, setBotDetail] = useState<Bot | null>(null);
const fetchBotDetail = useCallback(async () => {
if (isCreateMode) return;
const res = await httpClient.getBot(id);
setBotDetail(res.bot);
setBotEnabled(res.bot.enable ?? true);
setEnableLoaded(true);
}, [id, isCreateMode]);
// Fetch bot enable state // Fetch bot enable state
useEffect(() => { useEffect(() => {
if (!isCreateMode) { if (!isCreateMode) {
httpClient.getBot(id).then((res) => { fetchBotDetail();
setBotEnabled(res.bot.enable ?? true); const timer = window.setInterval(fetchBotDetail, 5000);
setEnableLoaded(true); return () => window.clearInterval(timer);
});
} }
}, [id, isCreateMode]); }, [fetchBotDetail, isCreateMode]);
const handleEnableToggle = useCallback( const handleEnableToggle = useCallback(
async (checked: boolean) => { async (checked: boolean) => {
@@ -95,9 +174,7 @@ export default function BotDetailContent({ id }: { id: string }) {
function handleFormSubmit() { function handleFormSubmit() {
// Re-sync enable state after form save (form may update enable too) // Re-sync enable state after form save (form may update enable too)
httpClient.getBot(id).then((res) => { fetchBotDetail();
setBotEnabled(res.bot.enable ?? true);
});
refreshBots(); refreshBots();
} }
@@ -173,6 +250,7 @@ export default function BotDetailContent({ id }: { id: string }) {
</Label> </Label>
</div> </div>
)} )}
<RuntimeStatusBadge status={getBotRuntimeStatus(botDetail)} />
</div> </div>
<Button <Button
type="submit" type="submit"
+23 -1
View File
@@ -179,6 +179,28 @@ export interface ApiRespPlatformBot {
bot: Bot; bot: Bot;
} }
export type BotAdapterConnectionStatus =
| 'connecting'
| 'connected'
| 'disconnected'
| 'error';
export interface BotAdapterRuntimeStatus {
connection_status?: BotAdapterConnectionStatus;
connection_error?: string;
last_connected_at?: number | null;
last_disconnected_at?: number | null;
}
export interface BotAdapterRuntimeValues {
bot_account_id?: string;
webhook_url?: string | null;
webhook_full_url?: string | null;
extra_webhook_full_url?: string | null;
runtime_status?: BotAdapterRuntimeStatus;
[key: string]: unknown;
}
export interface Bot { export interface Bot {
uuid?: string; uuid?: string;
name: string; name: string;
@@ -191,7 +213,7 @@ export interface Bot {
pipeline_routing_rules?: PipelineRoutingRule[]; pipeline_routing_rules?: PipelineRoutingRule[];
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
adapter_runtime_values?: object; adapter_runtime_values?: BotAdapterRuntimeValues;
} }
export type RoutingRuleOperator = export type RoutingRuleOperator =
+4
View File
@@ -354,6 +354,10 @@ const enUS = {
log: 'Log', log: 'Log',
configuration: 'Configuration', configuration: 'Configuration',
logs: 'Logs', logs: 'Logs',
runtimeConnected: 'Connected',
runtimeConnecting: 'Connecting',
runtimeDisconnected: 'Disconnected',
runtimeError: 'Connection error',
basicInfo: 'Basic Information', basicInfo: 'Basic Information',
basicInfoDescription: 'Set the bot name and description', basicInfoDescription: 'Set the bot name and description',
routingConnection: 'Routing & Connection', routingConnection: 'Routing & Connection',
+4
View File
@@ -360,6 +360,10 @@ const jaJP = {
log: 'ログ', log: 'ログ',
configuration: '設定', configuration: '設定',
logs: 'ログ', logs: 'ログ',
runtimeConnected: '接続済み',
runtimeConnecting: '接続中',
runtimeDisconnected: '切断済み',
runtimeError: '接続エラー',
basicInfo: '基本情報', basicInfo: '基本情報',
basicInfoDescription: 'ボットの名前と説明を設定', basicInfoDescription: 'ボットの名前と説明を設定',
routingConnection: 'ルーティングと接続', routingConnection: 'ルーティングと接続',
+4
View File
@@ -339,6 +339,10 @@ const zhHans = {
log: '日志', log: '日志',
configuration: '配置', configuration: '配置',
logs: '日志', logs: '日志',
runtimeConnected: '已连接',
runtimeConnecting: '连接中',
runtimeDisconnected: '已掉线',
runtimeError: '连接错误',
basicInfo: '基础信息', basicInfo: '基础信息',
basicInfoDescription: '设置机器人名称和描述', basicInfoDescription: '设置机器人名称和描述',
routingConnection: '路由与连接', routingConnection: '路由与连接',