mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 07:16:04 +00:00
chore: Add PyPI package support for uvx/pip installation (#1764)
* Initial plan * Add package structure and resource path utilities - Created langbot/ package with __init__.py and __main__.py entry point - Added paths utility to find frontend and resource files from package installation - Updated config loading to use resource paths - Updated frontend serving to use resource paths - Added MANIFEST.in for package data inclusion - Updated pyproject.toml with build system and entry points Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add PyPI publishing workflow and update license - Created GitHub Actions workflow to build frontend and publish to PyPI - Added license field to pyproject.toml to fix deprecation warning - Updated .gitignore to exclude build artifacts - Tested package building successfully Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add PyPI installation documentation - Created PYPI_INSTALLATION.md with detailed installation and usage instructions - Updated README.md to feature uvx/pip installation as recommended method - Updated README_EN.md with same changes for English documentation Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Address code review feedback - Made package-data configuration more specific to langbot package only - Improved path detection with caching to avoid repeated file I/O - Removed sys.path searching which was incorrect for package data - Removed interactive input() call for non-interactive environment compatibility - Simplified error messages for version check Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix code review issues - Use specific exception types instead of bare except - Fix misleading comments about directory levels - Remove redundant existence check before makedirs with exist_ok=True - Use context manager for file opening to ensure proper cleanup Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Simplify package configuration and document behavioral differences - Removed redundant package-data configuration, relying on MANIFEST.in - Added documentation about behavioral differences between package and source installation - Clarified that include-package-data=true uses MANIFEST.in for data files Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * chore: update pyproject.toml * chore: try pack templates in langbot/ * chore: update * chore: update * chore: update * chore: update * chore: update * chore: adjust dir structure * chore: fix imports * fix: read default-pipeline-config.json * fix: read default-pipeline-config.json * fix: tests * ci: publish pypi * chore: bump version 4.6.0-beta.1 for testing * chore: add templates/** * fix: send adapters and requesters icons * chore: bump version 4.6.0b2 for testing * chore: add platform field for docker-compose.yaml --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
0
src/langbot/pkg/core/__init__.py
Normal file
0
src/langbot/pkg/core/__init__.py
Normal file
213
src/langbot/pkg/core/app.py
Normal file
213
src/langbot/pkg/core/app.py
Normal file
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import traceback
|
||||
import os
|
||||
|
||||
from ..platform import botmgr as im_mgr
|
||||
from ..platform.webhook_pusher import WebhookPusher
|
||||
from ..provider.session import sessionmgr as llm_session_mgr
|
||||
from ..provider.modelmgr import modelmgr as llm_model_mgr
|
||||
from langbot.pkg.provider.tools import toolmgr as llm_tool_mgr
|
||||
from ..config import manager as config_mgr
|
||||
from ..command import cmdmgr
|
||||
from ..plugin import connector as plugin_connector
|
||||
from ..pipeline import pool
|
||||
from ..pipeline import controller, pipelinemgr
|
||||
from ..utils import version as version_mgr, proxy as proxy_mgr
|
||||
from ..persistence import mgr as persistencemgr
|
||||
from ..api.http.controller import main as http_controller
|
||||
from ..api.http.service import user as user_service
|
||||
from ..api.http.service import model as model_service
|
||||
from ..api.http.service import pipeline as pipeline_service
|
||||
from ..api.http.service import bot as bot_service
|
||||
from ..api.http.service import knowledge as knowledge_service
|
||||
from ..api.http.service import mcp as mcp_service
|
||||
from ..api.http.service import apikey as apikey_service
|
||||
from ..api.http.service import webhook as webhook_service
|
||||
from ..discover import engine as discover_engine
|
||||
from ..storage import mgr as storagemgr
|
||||
from ..utils import logcache
|
||||
from . import taskmgr
|
||||
from . import entities as core_entities
|
||||
from ..rag.knowledge import kbmgr as rag_mgr
|
||||
from ..vector import mgr as vectordb_mgr
|
||||
|
||||
|
||||
class Application:
|
||||
"""Runtime application object and context"""
|
||||
|
||||
event_loop: asyncio.AbstractEventLoop = None
|
||||
|
||||
# asyncio_tasks: list[asyncio.Task] = []
|
||||
task_mgr: taskmgr.AsyncTaskManager = None
|
||||
|
||||
discover: discover_engine.ComponentDiscoveryEngine = None
|
||||
|
||||
platform_mgr: im_mgr.PlatformManager = None
|
||||
|
||||
webhook_pusher: WebhookPusher = None
|
||||
|
||||
cmd_mgr: cmdmgr.CommandManager = None
|
||||
|
||||
sess_mgr: llm_session_mgr.SessionManager = None
|
||||
|
||||
model_mgr: llm_model_mgr.ModelManager = None
|
||||
|
||||
rag_mgr: rag_mgr.RAGManager = None
|
||||
|
||||
# TODO move to pipeline
|
||||
tool_mgr: llm_tool_mgr.ToolManager = None
|
||||
|
||||
# ======= Config manager =======
|
||||
|
||||
command_cfg: config_mgr.ConfigManager = None # deprecated
|
||||
|
||||
pipeline_cfg: config_mgr.ConfigManager = None # deprecated
|
||||
|
||||
platform_cfg: config_mgr.ConfigManager = None # deprecated
|
||||
|
||||
provider_cfg: config_mgr.ConfigManager = None # deprecated
|
||||
|
||||
system_cfg: config_mgr.ConfigManager = None # deprecated
|
||||
|
||||
instance_config: config_mgr.ConfigManager = None
|
||||
|
||||
# ======= Metadata config manager =======
|
||||
|
||||
sensitive_meta: config_mgr.ConfigManager = None
|
||||
|
||||
pipeline_config_meta_trigger: config_mgr.ConfigManager = None
|
||||
pipeline_config_meta_safety: config_mgr.ConfigManager = None
|
||||
pipeline_config_meta_ai: config_mgr.ConfigManager = None
|
||||
pipeline_config_meta_output: config_mgr.ConfigManager = None
|
||||
|
||||
# =========================
|
||||
|
||||
plugin_connector: plugin_connector.PluginRuntimeConnector = None
|
||||
|
||||
query_pool: pool.QueryPool = None
|
||||
|
||||
ctrl: controller.Controller = None
|
||||
|
||||
pipeline_mgr: pipelinemgr.PipelineManager = None
|
||||
|
||||
ver_mgr: version_mgr.VersionManager = None
|
||||
|
||||
proxy_mgr: proxy_mgr.ProxyManager = None
|
||||
|
||||
logger: logging.Logger = None
|
||||
|
||||
persistence_mgr: persistencemgr.PersistenceManager = None
|
||||
|
||||
vector_db_mgr: vectordb_mgr.VectorDBManager = None
|
||||
|
||||
http_ctrl: http_controller.HTTPController = None
|
||||
|
||||
log_cache: logcache.LogCache = None
|
||||
|
||||
storage_mgr: storagemgr.StorageMgr = None
|
||||
|
||||
# ========= HTTP Services =========
|
||||
|
||||
user_service: user_service.UserService = None
|
||||
|
||||
llm_model_service: model_service.LLMModelsService = None
|
||||
|
||||
embedding_models_service: model_service.EmbeddingModelsService = None
|
||||
|
||||
pipeline_service: pipeline_service.PipelineService = None
|
||||
|
||||
bot_service: bot_service.BotService = None
|
||||
|
||||
knowledge_service: knowledge_service.KnowledgeService = None
|
||||
|
||||
mcp_service: mcp_service.MCPService = None
|
||||
|
||||
apikey_service: apikey_service.ApiKeyService = None
|
||||
|
||||
webhook_service: webhook_service.WebhookService = None
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def initialize(self):
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
try:
|
||||
await self.plugin_connector.initialize_plugins()
|
||||
|
||||
# 后续可能会允许动态重启其他任务
|
||||
# 故为了防止程序在非 Ctrl-C 情况下退出,这里创建一个不会结束的协程
|
||||
async def never_ending():
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self.task_mgr.create_task(
|
||||
self.platform_mgr.run(),
|
||||
name='platform-manager',
|
||||
scopes=[
|
||||
core_entities.LifecycleControlScope.APPLICATION,
|
||||
core_entities.LifecycleControlScope.PLATFORM,
|
||||
],
|
||||
)
|
||||
self.task_mgr.create_task(
|
||||
self.ctrl.run(),
|
||||
name='query-controller',
|
||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
||||
)
|
||||
self.task_mgr.create_task(
|
||||
self.http_ctrl.run(),
|
||||
name='http-api-controller',
|
||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
||||
)
|
||||
|
||||
self.task_mgr.create_task(
|
||||
never_ending(),
|
||||
name='never-ending-task',
|
||||
scopes=[core_entities.LifecycleControlScope.APPLICATION],
|
||||
)
|
||||
|
||||
await self.print_web_access_info()
|
||||
await self.task_mgr.wait_all()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.logger.error(f'Application runtime fatal exception: {e}')
|
||||
self.logger.debug(f'Traceback: {traceback.format_exc()}')
|
||||
|
||||
def dispose(self):
|
||||
self.plugin_connector.dispose()
|
||||
|
||||
async def print_web_access_info(self):
|
||||
"""Print access webui tips"""
|
||||
|
||||
from ..utils import paths
|
||||
|
||||
frontend_path = paths.get_frontend_path()
|
||||
|
||||
if not os.path.exists(frontend_path):
|
||||
self.logger.warning('WebUI 文件缺失,请根据文档部署:https://docs.langbot.app/zh')
|
||||
self.logger.warning(
|
||||
'WebUI files are missing, please deploy according to the documentation: https://docs.langbot.app/en'
|
||||
)
|
||||
return
|
||||
|
||||
host_ip = '127.0.0.1'
|
||||
|
||||
port = self.instance_config.data['api']['port']
|
||||
|
||||
tips = f"""
|
||||
=======================================
|
||||
✨ Access WebUI / 访问管理面板
|
||||
|
||||
🏠 Local Address: http://{host_ip}:{port}/
|
||||
🌐 Public Address: http://<Your Public IP>:{port}/
|
||||
|
||||
📌 Running this program in a container? Please ensure that the {port} port is exposed
|
||||
=======================================
|
||||
""".strip()
|
||||
for line in tips.split('\n'):
|
||||
self.logger.info(line)
|
||||
63
src/langbot/pkg/core/boot.py
Normal file
63
src/langbot/pkg/core/boot.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from . import app
|
||||
from . import stage
|
||||
from ..utils import constants, importutil
|
||||
|
||||
# Import startup stage implementation to register
|
||||
from . import stages
|
||||
|
||||
importutil.import_modules_in_pkg(stages)
|
||||
|
||||
|
||||
stage_order = [
|
||||
'LoadConfigStage',
|
||||
'MigrationStage',
|
||||
'GenKeysStage',
|
||||
'SetupLoggerStage',
|
||||
'BuildAppStage',
|
||||
'ShowNotesStage',
|
||||
]
|
||||
|
||||
|
||||
async def make_app(loop: asyncio.AbstractEventLoop) -> app.Application:
|
||||
# Determine if it is debug mode
|
||||
if 'DEBUG' in os.environ and os.environ['DEBUG'] in ['true', '1']:
|
||||
constants.debug_mode = True
|
||||
|
||||
ap = app.Application()
|
||||
|
||||
ap.event_loop = loop
|
||||
|
||||
# Execute startup stage
|
||||
for stage_name in stage_order:
|
||||
stage_cls = stage.preregistered_stages[stage_name]
|
||||
stage_inst = stage_cls()
|
||||
|
||||
await stage_inst.run(ap)
|
||||
|
||||
await ap.initialize()
|
||||
|
||||
return ap
|
||||
|
||||
|
||||
async def main(loop: asyncio.AbstractEventLoop):
|
||||
try:
|
||||
# Hang system signal processing
|
||||
import signal
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
app_inst.dispose()
|
||||
print('[Signal] Program exit.')
|
||||
os._exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
app_inst = await make_app(loop)
|
||||
await app_inst.run()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
0
src/langbot/pkg/core/bootutils/__init__.py
Normal file
0
src/langbot/pkg/core/bootutils/__init__.py
Normal file
9
src/langbot/pkg/core/bootutils/config.py
Normal file
9
src/langbot/pkg/core/bootutils/config.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from ...config import manager as config_mgr
|
||||
|
||||
|
||||
load_python_module_config = config_mgr.load_python_module_config
|
||||
load_json_config = config_mgr.load_json_config
|
||||
load_yaml_config = config_mgr.load_yaml_config
|
||||
79
src/langbot/pkg/core/bootutils/deps.py
Normal file
79
src/langbot/pkg/core/bootutils/deps.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import pip
|
||||
import os
|
||||
from ...utils import pkgmgr
|
||||
|
||||
# Check dependencies to prevent users from not installing
|
||||
# Left is the import name, right is the dependency name
|
||||
required_deps = {
|
||||
'requests': 'requests',
|
||||
'openai': 'openai',
|
||||
'anthropic': 'anthropic',
|
||||
'colorlog': 'colorlog',
|
||||
'aiocqhttp': 'aiocqhttp',
|
||||
'botpy': 'qq-botpy-rc',
|
||||
'PIL': 'pillow',
|
||||
'nakuru': 'nakuru-project-idk',
|
||||
'tiktoken': 'tiktoken',
|
||||
'yaml': 'pyyaml',
|
||||
'aiohttp': 'aiohttp',
|
||||
'psutil': 'psutil',
|
||||
'async_lru': 'async-lru',
|
||||
'ollama': 'ollama',
|
||||
'quart': 'quart',
|
||||
'quart_cors': 'quart-cors',
|
||||
'sqlalchemy': 'sqlalchemy[asyncio]',
|
||||
'aiosqlite': 'aiosqlite',
|
||||
'aiofiles': 'aiofiles',
|
||||
'aioshutil': 'aioshutil',
|
||||
'argon2': 'argon2-cffi',
|
||||
'jwt': 'pyjwt',
|
||||
'Crypto': 'pycryptodome',
|
||||
'lark_oapi': 'lark-oapi',
|
||||
'discord': 'discord.py',
|
||||
'cryptography': 'cryptography',
|
||||
'gewechat_client': 'gewechat-client',
|
||||
'dingtalk_stream': 'dingtalk_stream',
|
||||
'dashscope': 'dashscope',
|
||||
'telegram': 'python-telegram-bot',
|
||||
'certifi': 'certifi',
|
||||
'mcp': 'mcp',
|
||||
'sqlmodel': 'sqlmodel',
|
||||
'telegramify_markdown': 'telegramify-markdown',
|
||||
'slack_sdk': 'slack_sdk',
|
||||
'asyncpg': 'asyncpg',
|
||||
}
|
||||
|
||||
|
||||
async def check_deps() -> list[str]:
|
||||
global required_deps
|
||||
|
||||
missing_deps = []
|
||||
for dep in required_deps:
|
||||
try:
|
||||
__import__(dep)
|
||||
except ImportError:
|
||||
missing_deps.append(dep)
|
||||
return missing_deps
|
||||
|
||||
|
||||
async def install_deps(deps: list[str]):
|
||||
global required_deps
|
||||
|
||||
for dep in deps:
|
||||
pip.main(['install', required_deps[dep]])
|
||||
|
||||
|
||||
async def precheck_plugin_deps():
|
||||
print('[Startup] Prechecking plugin dependencies...')
|
||||
|
||||
# Only execute plugin dependency installation when the plugins directory exists
|
||||
if os.path.exists('plugins'):
|
||||
for dir in os.listdir('plugins'):
|
||||
subdir = os.path.join('plugins', dir)
|
||||
if not os.path.isdir(subdir):
|
||||
continue
|
||||
if 'requirements.txt' in os.listdir(subdir):
|
||||
pkgmgr.install_requirements(
|
||||
os.path.join(subdir, 'requirements.txt'),
|
||||
extra_params=[],
|
||||
)
|
||||
36
src/langbot/pkg/core/bootutils/files.py
Normal file
36
src/langbot/pkg/core/bootutils/files.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
required_files = {
|
||||
'data/config.yaml': 'templates/config.yaml',
|
||||
}
|
||||
|
||||
required_paths = [
|
||||
'temp',
|
||||
'data',
|
||||
'data/metadata',
|
||||
'data/logs',
|
||||
'data/labels',
|
||||
]
|
||||
|
||||
|
||||
async def generate_files() -> list[str]:
|
||||
global required_files, required_paths
|
||||
|
||||
from ...utils import paths as path_utils
|
||||
|
||||
for required_paths in required_paths:
|
||||
if not os.path.exists(required_paths):
|
||||
os.mkdir(required_paths)
|
||||
|
||||
generated_files = []
|
||||
for file in required_files:
|
||||
if not os.path.exists(file):
|
||||
template_path = path_utils.get_resource_path(required_files[file])
|
||||
shutil.copyfile(template_path, file)
|
||||
generated_files.append(file)
|
||||
|
||||
return generated_files
|
||||
67
src/langbot/pkg/core/bootutils/log.py
Normal file
67
src/langbot/pkg/core/bootutils/log.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import colorlog
|
||||
|
||||
from ...utils import constants
|
||||
|
||||
|
||||
log_colors_config = {
|
||||
'DEBUG': 'green', # cyan white
|
||||
'INFO': 'white',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'cyan',
|
||||
}
|
||||
|
||||
|
||||
async def init_logging(extra_handlers: list[logging.Handler] = None) -> logging.Logger:
|
||||
# Remove all existing loggers
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
|
||||
level = logging.INFO
|
||||
|
||||
if constants.debug_mode:
|
||||
level = logging.DEBUG
|
||||
|
||||
log_file_name = 'data/logs/langbot-%s.log' % time.strftime('%Y-%m-%d', time.localtime())
|
||||
|
||||
qcg_logger = logging.getLogger('langbot')
|
||||
|
||||
qcg_logger.setLevel(level)
|
||||
|
||||
color_formatter = colorlog.ColoredFormatter(
|
||||
fmt='%(log_color)s[%(asctime)s.%(msecs)03d] %(filename)s (%(lineno)d) - [%(levelname)s] : %(message)s',
|
||||
datefmt='%m-%d %H:%M:%S',
|
||||
log_colors=log_colors_config,
|
||||
)
|
||||
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
# stream_handler.setLevel(level)
|
||||
# stream_handler.setFormatter(color_formatter)
|
||||
stream_handler.stream = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
|
||||
|
||||
log_handlers: list[logging.Handler] = [
|
||||
stream_handler,
|
||||
logging.FileHandler(log_file_name, encoding='utf-8'),
|
||||
]
|
||||
log_handlers += extra_handlers if extra_handlers is not None else []
|
||||
|
||||
for handler in log_handlers:
|
||||
handler.setLevel(level)
|
||||
handler.setFormatter(color_formatter)
|
||||
qcg_logger.addHandler(handler)
|
||||
|
||||
qcg_logger.debug('Logging initialized, log level: %s' % level)
|
||||
logging.basicConfig(
|
||||
level=logging.CRITICAL, # Set log output format
|
||||
format='[DEPR][%(asctime)s.%(msecs)03d] %(pathname)s (%(lineno)d) - [%(levelname)s] :\n%(message)s',
|
||||
# Log output format
|
||||
# -8 is a placeholder, left-align the output, and output length is 8
|
||||
datefmt='%Y-%m-%d %H:%M:%S', # Time output format
|
||||
handlers=[logging.NullHandler()],
|
||||
)
|
||||
|
||||
return qcg_logger
|
||||
10
src/langbot/pkg/core/entities.py
Normal file
10
src/langbot/pkg/core/entities.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
|
||||
class LifecycleControlScope(enum.Enum):
|
||||
APPLICATION = 'application'
|
||||
PLATFORM = 'platform'
|
||||
PLUGIN = 'plugin'
|
||||
PROVIDER = 'provider'
|
||||
45
src/langbot/pkg/core/migration.py
Normal file
45
src/langbot/pkg/core/migration.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from . import app
|
||||
|
||||
|
||||
preregistered_migrations: list[typing.Type[Migration]] = []
|
||||
"""Currently not supported for extension"""
|
||||
|
||||
|
||||
def migration_class(name: str, number: int):
|
||||
"""Register a migration"""
|
||||
|
||||
def decorator(cls: typing.Type[Migration]) -> typing.Type[Migration]:
|
||||
cls.name = name
|
||||
cls.number = number
|
||||
preregistered_migrations.append(cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class Migration(abc.ABC):
|
||||
"""A version migration"""
|
||||
|
||||
name: str
|
||||
|
||||
number: int
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
@abc.abstractmethod
|
||||
async def need_migrate(self) -> bool:
|
||||
"""Determine if the current environment needs to run this migration"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def run(self):
|
||||
"""Run migration"""
|
||||
pass
|
||||
0
src/langbot/pkg/core/migrations/__init__.py
Normal file
0
src/langbot/pkg/core/migrations/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('sensitive-word-migration', 1)
|
||||
class SensitiveWordMigration(migration.Migration):
|
||||
"""敏感词迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return os.path.exists('data/config/sensitive-words.json') and not os.path.exists(
|
||||
'data/metadata/sensitive-words.json'
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
# 移动文件
|
||||
os.rename('data/config/sensitive-words.json', 'data/metadata/sensitive-words.json')
|
||||
|
||||
# 重新加载配置
|
||||
await self.ap.sensitive_meta.load_config()
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('openai-config-migration', 2)
|
||||
class OpenAIConfigMigration(migration.Migration):
|
||||
"""OpenAI配置迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'openai-config' in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
old_openai_config = self.ap.provider_cfg.data['openai-config'].copy()
|
||||
|
||||
if 'keys' not in self.ap.provider_cfg.data:
|
||||
self.ap.provider_cfg.data['keys'] = {}
|
||||
|
||||
if 'openai' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['openai'] = []
|
||||
|
||||
self.ap.provider_cfg.data['keys']['openai'] = old_openai_config['api-keys']
|
||||
|
||||
self.ap.provider_cfg.data['model'] = old_openai_config['chat-completions-params']['model']
|
||||
|
||||
del old_openai_config['chat-completions-params']['model']
|
||||
|
||||
if 'requester' not in self.ap.provider_cfg.data:
|
||||
self.ap.provider_cfg.data['requester'] = {}
|
||||
|
||||
if 'openai-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {}
|
||||
|
||||
self.ap.provider_cfg.data['requester']['openai-chat-completions'] = {
|
||||
'base-url': old_openai_config['base_url'],
|
||||
'args': old_openai_config['chat-completions-params'],
|
||||
'timeout': old_openai_config['request-timeout'],
|
||||
}
|
||||
|
||||
del self.ap.provider_cfg.data['openai-config']
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('anthropic-requester-config-completion', 3)
|
||||
class AnthropicRequesterConfigCompletionMigration(migration.Migration):
|
||||
"""OpenAI配置迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'anthropic-messages' not in self.ap.provider_cfg.data['requester']
|
||||
or 'anthropic' not in self.ap.provider_cfg.data['keys']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if 'anthropic-messages' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['anthropic-messages'] = {
|
||||
'base-url': 'https://api.anthropic.com',
|
||||
'args': {'max_tokens': 1024},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
if 'anthropic' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['anthropic'] = []
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('moonshot-config-completion', 4)
|
||||
class MoonshotConfigCompletionMigration(migration.Migration):
|
||||
"""OpenAI配置迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
or 'moonshot' not in self.ap.provider_cfg.data['keys']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if 'moonshot-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['moonshot-chat-completions'] = {
|
||||
'base-url': 'https://api.moonshot.cn/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
if 'moonshot' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['moonshot'] = []
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('deepseek-config-completion', 5)
|
||||
class DeepseekConfigCompletionMigration(migration.Migration):
|
||||
"""OpenAI配置迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
or 'deepseek' not in self.ap.provider_cfg.data['keys']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if 'deepseek-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['deepseek-chat-completions'] = {
|
||||
'base-url': 'https://api.deepseek.com',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
if 'deepseek' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['deepseek'] = []
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
19
src/langbot/pkg/core/migrations/m006_vision_config.py
Normal file
19
src/langbot/pkg/core/migrations/m006_vision_config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('vision-config', 6)
|
||||
class VisionConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'enable-vision' not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if 'enable-vision' not in self.ap.provider_cfg.data:
|
||||
self.ap.provider_cfg.data['enable-vision'] = False
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
20
src/langbot/pkg/core/migrations/m007_qcg_center_url.py
Normal file
20
src/langbot/pkg/core/migrations/m007_qcg_center_url.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('qcg-center-url-config', 7)
|
||||
class QCGCenterURLConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'qcg-center-url' not in self.ap.system_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
if 'qcg-center-url' not in self.ap.system_cfg.data:
|
||||
self.ap.system_cfg.data['qcg-center-url'] = 'https://api.qchatgpt.rockchin.top/api/v2'
|
||||
|
||||
await self.ap.system_cfg.dump_config()
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('ad-fixwin-cfg-migration', 8)
|
||||
class AdFixwinConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return isinstance(self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default'], int)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
for session_name in self.ap.pipeline_cfg.data['rate-limit']['fixwin']:
|
||||
temp_dict = {
|
||||
'window-size': 60,
|
||||
'limit': self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name],
|
||||
}
|
||||
|
||||
self.ap.pipeline_cfg.data['rate-limit']['fixwin'][session_name] = temp_dict
|
||||
|
||||
await self.ap.pipeline_cfg.dump_config()
|
||||
22
src/langbot/pkg/core/migrations/m009_msg_truncator_cfg.py
Normal file
22
src/langbot/pkg/core/migrations/m009_msg_truncator_cfg.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('msg-truncator-cfg-migration', 9)
|
||||
class MsgTruncatorConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'msg-truncate' not in self.ap.pipeline_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.pipeline_cfg.data['msg-truncate'] = {
|
||||
'method': 'round',
|
||||
'round': {'max-round': 10},
|
||||
}
|
||||
|
||||
await self.ap.pipeline_cfg.dump_config()
|
||||
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('ollama-requester-config', 10)
|
||||
class MsgTruncatorConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'ollama-chat' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.provider_cfg.data['requester']['ollama-chat'] = {
|
||||
'base-url': 'http://127.0.0.1:11434',
|
||||
'args': {},
|
||||
'timeout': 600,
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('command-prefix-config', 11)
|
||||
class CommandPrefixConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'command-prefix' not in self.ap.command_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.command_cfg.data['command-prefix'] = ['!', '!']
|
||||
|
||||
await self.ap.command_cfg.dump_config()
|
||||
19
src/langbot/pkg/core/migrations/m012_runner_config.py
Normal file
19
src/langbot/pkg/core/migrations/m012_runner_config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('runner-config', 12)
|
||||
class RunnerConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'runner' not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.provider_cfg.data['runner'] = 'local-agent'
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
29
src/langbot/pkg/core/migrations/m013_http_api_config.py
Normal file
29
src/langbot/pkg/core/migrations/m013_http_api_config.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('http-api-config', 13)
|
||||
class HttpApiConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'http-api' not in self.ap.system_cfg.data or 'persistence' not in self.ap.system_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.system_cfg.data['http-api'] = {
|
||||
'enable': True,
|
||||
'host': '0.0.0.0',
|
||||
'port': 5300,
|
||||
'jwt-expire': 604800,
|
||||
}
|
||||
|
||||
self.ap.system_cfg.data['persistence'] = {
|
||||
'sqlite': {'path': 'data/persistence.db'},
|
||||
'use': 'sqlite',
|
||||
}
|
||||
|
||||
await self.ap.system_cfg.dump_config()
|
||||
22
src/langbot/pkg/core/migrations/m014_force_delay_config.py
Normal file
22
src/langbot/pkg/core/migrations/m014_force_delay_config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('force-delay-config', 14)
|
||||
class ForceDelayConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return isinstance(self.ap.platform_cfg.data['force-delay'], list)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
|
||||
self.ap.platform_cfg.data['force-delay'] = {
|
||||
'min': self.ap.platform_cfg.data['force-delay'][0],
|
||||
'max': self.ap.platform_cfg.data['force-delay'][1],
|
||||
}
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
27
src/langbot/pkg/core/migrations/m015_gitee_ai_config.py
Normal file
27
src/langbot/pkg/core/migrations/m015_gitee_ai_config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('gitee-ai-config', 15)
|
||||
class GiteeAIConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'gitee-ai-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
or 'gitee-ai' not in self.ap.provider_cfg.data['keys']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['requester']['gitee-ai-chat-completions'] = {
|
||||
'base-url': 'https://ai.gitee.com/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
self.ap.provider_cfg.data['keys']['gitee-ai'] = ['XXXXX']
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
23
src/langbot/pkg/core/migrations/m016_dify_service_api.py
Normal file
23
src/langbot/pkg/core/migrations/m016_dify_service_api.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('dify-service-api-config', 16)
|
||||
class DifyServiceAPICfgMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'dify-service-api' not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['dify-service-api'] = {
|
||||
'base-url': 'https://api.dify.ai/v1',
|
||||
'app-type': 'chat',
|
||||
'chat': {'api-key': 'app-1234567890'},
|
||||
'workflow': {'api-key': 'app-1234567890', 'output-key': 'summary'},
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('dify-api-timeout-params', 17)
|
||||
class DifyAPITimeoutParamsMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'timeout' not in self.ap.provider_cfg.data['dify-service-api']['chat']
|
||||
or 'timeout' not in self.ap.provider_cfg.data['dify-service-api']['workflow']
|
||||
or 'agent' not in self.ap.provider_cfg.data['dify-service-api']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['dify-service-api']['chat']['timeout'] = 120
|
||||
self.ap.provider_cfg.data['dify-service-api']['workflow']['timeout'] = 120
|
||||
self.ap.provider_cfg.data['dify-service-api']['agent'] = {
|
||||
'api-key': 'app-1234567890',
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
23
src/langbot/pkg/core/migrations/m018_xai_config.py
Normal file
23
src/langbot/pkg/core/migrations/m018_xai_config.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('xai-config', 18)
|
||||
class XaiConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'xai-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['requester']['xai-chat-completions'] = {
|
||||
'base-url': 'https://api.x.ai/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
self.ap.provider_cfg.data['keys']['xai'] = ['xai-1234567890']
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
23
src/langbot/pkg/core/migrations/m019_zhipuai_config.py
Normal file
23
src/langbot/pkg/core/migrations/m019_zhipuai_config.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('zhipuai-config', 19)
|
||||
class ZhipuaiConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'zhipuai-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['requester']['zhipuai-chat-completions'] = {
|
||||
'base-url': 'https://open.bigmodel.cn/api/paas/v4',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
self.ap.provider_cfg.data['keys']['zhipuai'] = ['xxxxxxx']
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
36
src/langbot/pkg/core/migrations/m020_wecom_config.py
Normal file
36
src/langbot/pkg/core/migrations/m020_wecom_config.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('wecom-config', 20)
|
||||
class WecomConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'wecom':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'wecom',
|
||||
'enable': False,
|
||||
'host': '0.0.0.0',
|
||||
'port': 2290,
|
||||
'corpid': '',
|
||||
'secret': '',
|
||||
'token': '',
|
||||
'EncodingAESKey': '',
|
||||
'contacts_secret': '',
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
35
src/langbot/pkg/core/migrations/m021_lark_config.py
Normal file
35
src/langbot/pkg/core/migrations/m021_lark_config.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('lark-config', 21)
|
||||
class LarkConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'lark':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'lark',
|
||||
'enable': False,
|
||||
'app_id': 'cli_abcdefgh',
|
||||
'app_secret': 'XXXXXXXXXX',
|
||||
'bot_name': 'LangBot',
|
||||
'enable-webhook': False,
|
||||
'port': 2285,
|
||||
'encrypt-key': 'xxxxxxxxx',
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
23
src/langbot/pkg/core/migrations/m022_lmstudio_config.py
Normal file
23
src/langbot/pkg/core/migrations/m022_lmstudio_config.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('lmstudio-config', 22)
|
||||
class LmStudioConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
return 'lmstudio-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['requester']['lmstudio-chat-completions'] = {
|
||||
'base-url': 'http://127.0.0.1:1234/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
25
src/langbot/pkg/core/migrations/m023_siliconflow_config.py
Normal file
25
src/langbot/pkg/core/migrations/m023_siliconflow_config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('siliconflow-config', 23)
|
||||
class SiliconFlowConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
return 'siliconflow-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['keys']['siliconflow'] = ['xxxxxxx']
|
||||
|
||||
self.ap.provider_cfg.data['requester']['siliconflow-chat-completions'] = {
|
||||
'base-url': 'https://api.siliconflow.cn/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
31
src/langbot/pkg/core/migrations/m024_discord_config.py
Normal file
31
src/langbot/pkg/core/migrations/m024_discord_config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('discord-config', 24)
|
||||
class DiscordConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'discord':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'discord',
|
||||
'enable': False,
|
||||
'client_id': '1234567890',
|
||||
'token': 'XXXXXXXXXX',
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
35
src/langbot/pkg/core/migrations/m025_gewechat_config.py
Normal file
35
src/langbot/pkg/core/migrations/m025_gewechat_config.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('gewechat-config', 25)
|
||||
class GewechatConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'gewechat':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'gewechat',
|
||||
'enable': False,
|
||||
'gewechat_url': 'http://your-gewechat-server:2531',
|
||||
'gewechat_file_url': 'http://your-gewechat-server:2532',
|
||||
'port': 2286,
|
||||
'callback_url': 'http://your-callback-url:2286/gewechat/callback',
|
||||
'app_id': '',
|
||||
'token': '',
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
33
src/langbot/pkg/core/migrations/m026_qqofficial_config.py
Normal file
33
src/langbot/pkg/core/migrations/m026_qqofficial_config.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('qqofficial-config', 26)
|
||||
class QQOfficialConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'qqofficial':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'qqofficial',
|
||||
'enable': False,
|
||||
'appid': '',
|
||||
'secret': '',
|
||||
'port': 2284,
|
||||
'token': '',
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('wx-official-account-config', 27)
|
||||
class WXOfficialAccountConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'officialaccount':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'officialaccount',
|
||||
'enable': False,
|
||||
'token': '',
|
||||
'EncodingAESKey': '',
|
||||
'AppID': '',
|
||||
'AppSecret': '',
|
||||
'host': '0.0.0.0',
|
||||
'port': 2287,
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('bailian-requester-config', 28)
|
||||
class BailianRequesterConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
return 'bailian-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['keys']['bailian'] = ['sk-xxxxxxx']
|
||||
|
||||
self.ap.provider_cfg.data['requester']['bailian-chat-completions'] = {
|
||||
'base-url': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('dashscope-app-api-config', 29)
|
||||
class DashscopeAppAPICfgMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'dashscope-app-api' not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['dashscope-app-api'] = {
|
||||
'app-type': 'agent',
|
||||
'api-key': 'sk-1234567890',
|
||||
'agent': {'app-id': 'Your_app_id', 'references_quote': '参考资料来自:'},
|
||||
'workflow': {
|
||||
'app-id': 'Your_app_id',
|
||||
'references_quote': '参考资料来自:',
|
||||
'biz_params': {'city': '北京', 'date': '2023-08-10'},
|
||||
},
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
31
src/langbot/pkg/core/migrations/m030_lark_config_cmpl.py
Normal file
31
src/langbot/pkg/core/migrations/m030_lark_config_cmpl.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('lark-config-cmpl', 30)
|
||||
class LarkConfigCmplMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'lark':
|
||||
if 'enable-webhook' not in adapter:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'lark':
|
||||
if 'enable-webhook' not in adapter:
|
||||
adapter['enable-webhook'] = False
|
||||
if 'port' not in adapter:
|
||||
adapter['port'] = 2285
|
||||
if 'encrypt-key' not in adapter:
|
||||
adapter['encrypt-key'] = 'xxxxxxxxx'
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
33
src/langbot/pkg/core/migrations/m031_dingtalk_config.py
Normal file
33
src/langbot/pkg/core/migrations/m031_dingtalk_config.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('dingtalk-config', 31)
|
||||
class DingTalkConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
# for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
# if adapter['adapter'] == 'dingtalk':
|
||||
# return False
|
||||
|
||||
# return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.platform_cfg.data['platform-adapters'].append(
|
||||
{
|
||||
'adapter': 'dingtalk',
|
||||
'enable': False,
|
||||
'client_id': '',
|
||||
'client_secret': '',
|
||||
'robot_code': '',
|
||||
'robot_name': '',
|
||||
}
|
||||
)
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
25
src/langbot/pkg/core/migrations/m032_volcark_config.py
Normal file
25
src/langbot/pkg/core/migrations/m032_volcark_config.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('volcark-requester-config', 32)
|
||||
class VolcArkRequesterConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
return 'volcark-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['keys']['volcark'] = ['xxxxxxxx']
|
||||
|
||||
self.ap.provider_cfg.data['requester']['volcark-chat-completions'] = {
|
||||
'base-url': 'https://ark.cn-beijing.volces.com/api/v3',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
24
src/langbot/pkg/core/migrations/m033_dify_thinking_config.py
Normal file
24
src/langbot/pkg/core/migrations/m033_dify_thinking_config.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('dify-thinking-config', 33)
|
||||
class DifyThinkingConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
if 'options' not in self.ap.provider_cfg.data['dify-service-api']:
|
||||
return True
|
||||
|
||||
if 'convert-thinking-tips' not in self.ap.provider_cfg.data['dify-service-api']['options']:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['dify-service-api']['options'] = {'convert-thinking-tips': 'plain'}
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('gewechat-file-url-config', 34)
|
||||
class GewechatFileUrlConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'gewechat':
|
||||
if 'gewechat_file_url' not in adapter:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'gewechat':
|
||||
if 'gewechat_file_url' not in adapter:
|
||||
parsed_url = urlparse(adapter['gewechat_url'])
|
||||
adapter['gewechat_file_url'] = f'{parsed_url.scheme}://{parsed_url.hostname}:2532'
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
26
src/langbot/pkg/core/migrations/m035_wxoa_mode.py
Normal file
26
src/langbot/pkg/core/migrations/m035_wxoa_mode.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('wxoa-mode', 35)
|
||||
class WxoaModeMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'officialaccount':
|
||||
if 'Mode' not in adapter:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'officialaccount':
|
||||
if 'Mode' not in adapter:
|
||||
adapter['Mode'] = 'drop'
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
26
src/langbot/pkg/core/migrations/m036_wxoa_loading_message.py
Normal file
26
src/langbot/pkg/core/migrations/m036_wxoa_loading_message.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('wxoa-loading-message', 36)
|
||||
class WxoaLoadingMessageMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'officialaccount':
|
||||
if 'LoadingMessage' not in adapter:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] == 'officialaccount':
|
||||
if 'LoadingMessage' not in adapter:
|
||||
adapter['LoadingMessage'] = 'AI正在思考中,请发送任意内容获取回复。'
|
||||
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
18
src/langbot/pkg/core/migrations/m037_mcp_config.py
Normal file
18
src/langbot/pkg/core/migrations/m037_mcp_config.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('mcp-config', 37)
|
||||
class MCPConfigMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return 'mcp' not in self.ap.provider_cfg.data
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
self.ap.provider_cfg.data['mcp'] = {'servers': []}
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
25
src/langbot/pkg/core/migrations/m038_tg_dingtalk_markdown.py
Normal file
25
src/langbot/pkg/core/migrations/m038_tg_dingtalk_markdown.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('tg-dingtalk-markdown', 38)
|
||||
class TgDingtalkMarkdownMigration(migration.Migration):
|
||||
"""迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] in ['dingtalk', 'telegram']:
|
||||
if 'markdown_card' not in adapter:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
for adapter in self.ap.platform_cfg.data['platform-adapters']:
|
||||
if adapter['adapter'] in ['dingtalk', 'telegram']:
|
||||
if 'markdown_card' not in adapter:
|
||||
adapter['markdown_card'] = False
|
||||
await self.ap.platform_cfg.dump_config()
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('modelscope-config-completion', 39)
|
||||
class ModelScopeConfigCompletionMigration(migration.Migration):
|
||||
"""ModelScope配置迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
or 'modelscope' not in self.ap.provider_cfg.data['keys']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if 'modelscope-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['modelscope-chat-completions'] = {
|
||||
'base-url': 'https://api-inference.modelscope.cn/v1',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
if 'modelscope' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['modelscope'] = []
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
29
src/langbot/pkg/core/migrations/m040_ppio_config.py
Normal file
29
src/langbot/pkg/core/migrations/m040_ppio_config.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class('ppio-config', 40)
|
||||
class PPIOConfigMigration(migration.Migration):
|
||||
"""PPIO配置迁移"""
|
||||
|
||||
async def need_migrate(self) -> bool:
|
||||
"""判断当前环境是否需要运行此迁移"""
|
||||
return (
|
||||
'ppio-chat-completions' not in self.ap.provider_cfg.data['requester']
|
||||
or 'ppio' not in self.ap.provider_cfg.data['keys']
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
"""执行迁移"""
|
||||
if 'ppio-chat-completions' not in self.ap.provider_cfg.data['requester']:
|
||||
self.ap.provider_cfg.data['requester']['ppio-chat-completions'] = {
|
||||
'base-url': 'https://api.ppinfra.com/v3/openai',
|
||||
'args': {},
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
if 'ppio' not in self.ap.provider_cfg.data['keys']:
|
||||
self.ap.provider_cfg.data['keys']['ppio'] = []
|
||||
|
||||
await self.ap.provider_cfg.dump_config()
|
||||
43
src/langbot/pkg/core/note.py
Normal file
43
src/langbot/pkg/core/note.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from . import app
|
||||
|
||||
preregistered_notes: list[typing.Type[LaunchNote]] = []
|
||||
|
||||
|
||||
def note_class(name: str, number: int):
|
||||
"""Register a launch information"""
|
||||
|
||||
def decorator(cls: typing.Type[LaunchNote]) -> typing.Type[LaunchNote]:
|
||||
cls.name = name
|
||||
cls.number = number
|
||||
preregistered_notes.append(cls)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class LaunchNote(abc.ABC):
|
||||
"""Launch information"""
|
||||
|
||||
name: str
|
||||
|
||||
number: int
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
@abc.abstractmethod
|
||||
async def need_show(self) -> bool:
|
||||
"""Determine if the current environment needs to display this launch information"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
"""Generate launch information"""
|
||||
pass
|
||||
0
src/langbot/pkg/core/notes/__init__.py
Normal file
0
src/langbot/pkg/core/notes/__init__.py
Normal file
16
src/langbot/pkg/core/notes/n001_classic_msgs.py
Normal file
16
src/langbot/pkg/core/notes/n001_classic_msgs.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from .. import note
|
||||
|
||||
|
||||
@note.note_class('ClassicNotes', 1)
|
||||
class ClassicNotes(note.LaunchNote):
|
||||
"""Classic launch information"""
|
||||
|
||||
async def need_show(self) -> bool:
|
||||
return True
|
||||
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
yield await self.ap.ver_mgr.show_version_update()
|
||||
26
src/langbot/pkg/core/notes/n002_selection_mode_on_windows.py
Normal file
26
src/langbot/pkg/core/notes/n002_selection_mode_on_windows.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import os
|
||||
import logging
|
||||
|
||||
from .. import note
|
||||
|
||||
|
||||
@note.note_class('SelectionModeOnWindows', 2)
|
||||
class SelectionModeOnWindows(note.LaunchNote):
|
||||
"""Selection mode prompt information on Windows"""
|
||||
|
||||
async def need_show(self) -> bool:
|
||||
return os.name == 'nt'
|
||||
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
yield (
|
||||
"""您正在使用 Windows 系统,若窗口左上角显示处于”选择“模式,程序将被暂停运行,此时请右键窗口中空白区域退出选择模式。""",
|
||||
logging.INFO,
|
||||
)
|
||||
|
||||
yield (
|
||||
"""You are using Windows system, if the top left corner of the window displays "Selection" mode, the program will be paused running, please right-click on the blank area in the window to exit the selection mode.""",
|
||||
logging.INFO,
|
||||
)
|
||||
17
src/langbot/pkg/core/notes/n003_print_version.py
Normal file
17
src/langbot/pkg/core/notes/n003_print_version.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from .. import note
|
||||
|
||||
|
||||
@note.note_class('PrintVersion', 3)
|
||||
class PrintVersion(note.LaunchNote):
|
||||
"""Print Version Information"""
|
||||
|
||||
async def need_show(self) -> bool:
|
||||
return True
|
||||
|
||||
async def yield_note(self) -> typing.AsyncGenerator[typing.Tuple[str, int], None]:
|
||||
yield f'Current Version: {self.ap.ver_mgr.get_current_version()}', logging.INFO
|
||||
32
src/langbot/pkg/core/stage.py
Normal file
32
src/langbot/pkg/core/stage.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import typing
|
||||
|
||||
from . import app
|
||||
|
||||
|
||||
preregistered_stages: dict[str, typing.Type[BootingStage]] = {}
|
||||
"""Pre-registered request processing stages. All request processing stage classes are registered in this dictionary during initialization.
|
||||
|
||||
Currently not supported for extension
|
||||
"""
|
||||
|
||||
|
||||
def stage_class(name: str):
|
||||
def decorator(cls: typing.Type[BootingStage]) -> typing.Type[BootingStage]:
|
||||
preregistered_stages[name] = cls
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class BootingStage(abc.ABC):
|
||||
"""Booting stage"""
|
||||
|
||||
name: str = None
|
||||
|
||||
@abc.abstractmethod
|
||||
async def run(self, ap: app.Application):
|
||||
"""Run"""
|
||||
pass
|
||||
0
src/langbot/pkg/core/stages/__init__.py
Normal file
0
src/langbot/pkg/core/stages/__init__.py
Normal file
143
src/langbot/pkg/core/stages/build_app.py
Normal file
143
src/langbot/pkg/core/stages/build_app.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from .. import stage, app
|
||||
from ...utils import version, proxy
|
||||
from ...pipeline import pool, controller, pipelinemgr
|
||||
from ...plugin import connector as plugin_connector
|
||||
from ...command import cmdmgr
|
||||
from ...provider.session import sessionmgr as llm_session_mgr
|
||||
from ...provider.modelmgr import modelmgr as llm_model_mgr
|
||||
from ...provider.tools import toolmgr as llm_tool_mgr
|
||||
from ...rag.knowledge import kbmgr as rag_mgr
|
||||
from ...platform import botmgr as im_mgr
|
||||
from ...platform.webhook_pusher import WebhookPusher
|
||||
from ...persistence import mgr as persistencemgr
|
||||
from ...api.http.controller import main as http_controller
|
||||
from ...api.http.service import user as user_service
|
||||
from ...api.http.service import model as model_service
|
||||
from ...api.http.service import pipeline as pipeline_service
|
||||
from ...api.http.service import bot as bot_service
|
||||
from ...api.http.service import knowledge as knowledge_service
|
||||
from ...api.http.service import mcp as mcp_service
|
||||
from ...api.http.service import apikey as apikey_service
|
||||
from ...api.http.service import webhook as webhook_service
|
||||
from ...discover import engine as discover_engine
|
||||
from ...storage import mgr as storagemgr
|
||||
from ...utils import logcache
|
||||
from ...vector import mgr as vectordb_mgr
|
||||
from .. import taskmgr
|
||||
|
||||
|
||||
@stage.stage_class('BuildAppStage')
|
||||
class BuildAppStage(stage.BootingStage):
|
||||
"""Build LangBot application"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
"""Build LangBot application"""
|
||||
ap.task_mgr = taskmgr.AsyncTaskManager(ap)
|
||||
|
||||
discover = discover_engine.ComponentDiscoveryEngine(ap)
|
||||
discover.discover_blueprint('templates/components.yaml')
|
||||
ap.discover = discover
|
||||
|
||||
proxy_mgr = proxy.ProxyManager(ap)
|
||||
await proxy_mgr.initialize()
|
||||
ap.proxy_mgr = proxy_mgr
|
||||
|
||||
ver_mgr = version.VersionManager(ap)
|
||||
await ver_mgr.initialize()
|
||||
ap.ver_mgr = ver_mgr
|
||||
|
||||
ap.query_pool = pool.QueryPool()
|
||||
|
||||
log_cache = logcache.LogCache()
|
||||
ap.log_cache = log_cache
|
||||
|
||||
storage_mgr_inst = storagemgr.StorageMgr(ap)
|
||||
await storage_mgr_inst.initialize()
|
||||
ap.storage_mgr = storage_mgr_inst
|
||||
|
||||
persistence_mgr_inst = persistencemgr.PersistenceManager(ap)
|
||||
ap.persistence_mgr = persistence_mgr_inst
|
||||
await persistence_mgr_inst.initialize()
|
||||
|
||||
async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None:
|
||||
await asyncio.sleep(3)
|
||||
await plugin_connector_inst.initialize()
|
||||
|
||||
plugin_connector_inst = plugin_connector.PluginRuntimeConnector(ap, runtime_disconnect_callback)
|
||||
await plugin_connector_inst.initialize()
|
||||
ap.plugin_connector = plugin_connector_inst
|
||||
|
||||
cmd_mgr_inst = cmdmgr.CommandManager(ap)
|
||||
await cmd_mgr_inst.initialize()
|
||||
ap.cmd_mgr = cmd_mgr_inst
|
||||
|
||||
llm_model_mgr_inst = llm_model_mgr.ModelManager(ap)
|
||||
await llm_model_mgr_inst.initialize()
|
||||
ap.model_mgr = llm_model_mgr_inst
|
||||
|
||||
llm_session_mgr_inst = llm_session_mgr.SessionManager(ap)
|
||||
await llm_session_mgr_inst.initialize()
|
||||
ap.sess_mgr = llm_session_mgr_inst
|
||||
|
||||
llm_tool_mgr_inst = llm_tool_mgr.ToolManager(ap)
|
||||
await llm_tool_mgr_inst.initialize()
|
||||
ap.tool_mgr = llm_tool_mgr_inst
|
||||
|
||||
im_mgr_inst = im_mgr.PlatformManager(ap=ap)
|
||||
await im_mgr_inst.initialize()
|
||||
ap.platform_mgr = im_mgr_inst
|
||||
|
||||
# Initialize webhook pusher
|
||||
webhook_pusher_inst = WebhookPusher(ap)
|
||||
ap.webhook_pusher = webhook_pusher_inst
|
||||
|
||||
pipeline_mgr = pipelinemgr.PipelineManager(ap)
|
||||
await pipeline_mgr.initialize()
|
||||
ap.pipeline_mgr = pipeline_mgr
|
||||
|
||||
rag_mgr_inst = rag_mgr.RAGManager(ap)
|
||||
await rag_mgr_inst.initialize()
|
||||
ap.rag_mgr = rag_mgr_inst
|
||||
|
||||
# 初始化向量数据库管理器
|
||||
vectordb_mgr_inst = vectordb_mgr.VectorDBManager(ap)
|
||||
await vectordb_mgr_inst.initialize()
|
||||
ap.vector_db_mgr = vectordb_mgr_inst
|
||||
|
||||
http_ctrl = http_controller.HTTPController(ap)
|
||||
await http_ctrl.initialize()
|
||||
ap.http_ctrl = http_ctrl
|
||||
|
||||
user_service_inst = user_service.UserService(ap)
|
||||
ap.user_service = user_service_inst
|
||||
|
||||
llm_model_service_inst = model_service.LLMModelsService(ap)
|
||||
ap.llm_model_service = llm_model_service_inst
|
||||
|
||||
embedding_models_service_inst = model_service.EmbeddingModelsService(ap)
|
||||
ap.embedding_models_service = embedding_models_service_inst
|
||||
|
||||
pipeline_service_inst = pipeline_service.PipelineService(ap)
|
||||
ap.pipeline_service = pipeline_service_inst
|
||||
|
||||
bot_service_inst = bot_service.BotService(ap)
|
||||
ap.bot_service = bot_service_inst
|
||||
|
||||
knowledge_service_inst = knowledge_service.KnowledgeService(ap)
|
||||
ap.knowledge_service = knowledge_service_inst
|
||||
|
||||
mcp_service_inst = mcp_service.MCPService(ap)
|
||||
ap.mcp_service = mcp_service_inst
|
||||
|
||||
apikey_service_inst = apikey_service.ApiKeyService(ap)
|
||||
ap.apikey_service = apikey_service_inst
|
||||
|
||||
webhook_service_inst = webhook_service.WebhookService(ap)
|
||||
ap.webhook_service = webhook_service_inst
|
||||
|
||||
ctrl = controller.Controller(ap)
|
||||
ap.ctrl = ctrl
|
||||
24
src/langbot/pkg/core/stages/genkeys.py
Normal file
24
src/langbot/pkg/core/stages/genkeys.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from .. import stage, app
|
||||
|
||||
|
||||
@stage.stage_class('GenKeysStage')
|
||||
class GenKeysStage(stage.BootingStage):
|
||||
"""Generate keys stage"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
"""Generate keys"""
|
||||
|
||||
if not ap.instance_config.data['system']['jwt']['secret']:
|
||||
ap.instance_config.data['system']['jwt']['secret'] = secrets.token_hex(16)
|
||||
await ap.instance_config.dump_config()
|
||||
|
||||
if 'recovery_key' not in ap.instance_config.data['system']:
|
||||
ap.instance_config.data['system']['recovery_key'] = ''
|
||||
|
||||
if not ap.instance_config.data['system']['recovery_key']:
|
||||
ap.instance_config.data['system']['recovery_key'] = secrets.token_hex(3).upper()
|
||||
await ap.instance_config.dump_config()
|
||||
158
src/langbot/pkg/core/stages/load_config.py
Normal file
158
src/langbot/pkg/core/stages/load_config.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
import yaml
|
||||
import importlib.resources as resources
|
||||
|
||||
from .. import stage, app
|
||||
from ..bootutils import config
|
||||
|
||||
|
||||
def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
||||
"""Apply environment variable overrides to data/config.yaml
|
||||
|
||||
Environment variables should be uppercase and use __ (double underscore)
|
||||
to represent nested keys. For example:
|
||||
- CONCURRENCY__PIPELINE overrides concurrency.pipeline
|
||||
- PLUGIN__RUNTIME_WS_URL overrides plugin.runtime_ws_url
|
||||
|
||||
Arrays and dict types are ignored.
|
||||
|
||||
Args:
|
||||
cfg: Configuration dictionary
|
||||
|
||||
Returns:
|
||||
Updated configuration dictionary
|
||||
"""
|
||||
|
||||
def convert_value(value: str, original_value: Any) -> Any:
|
||||
"""Convert string value to appropriate type based on original value
|
||||
|
||||
Args:
|
||||
value: String value from environment variable
|
||||
original_value: Original value to infer type from
|
||||
|
||||
Returns:
|
||||
Converted value (falls back to string if conversion fails)
|
||||
"""
|
||||
if isinstance(original_value, bool):
|
||||
return value.lower() in ('true', '1', 'yes', 'on')
|
||||
elif isinstance(original_value, int):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
# If conversion fails, keep as string (user error, but non-breaking)
|
||||
return value
|
||||
elif isinstance(original_value, float):
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
# If conversion fails, keep as string (user error, but non-breaking)
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
# Process environment variables
|
||||
for env_key, env_value in os.environ.items():
|
||||
# Check if the environment variable is uppercase and contains __
|
||||
if not env_key.isupper():
|
||||
continue
|
||||
if '__' not in env_key:
|
||||
continue
|
||||
|
||||
print(f'apply env overrides to config: env_key: {env_key}, env_value: {env_value}')
|
||||
|
||||
# Convert environment variable name to config path
|
||||
# e.g., CONCURRENCY__PIPELINE -> ['concurrency', 'pipeline']
|
||||
keys = [key.lower() for key in env_key.split('__')]
|
||||
|
||||
# Navigate to the target value and validate the path
|
||||
current = cfg
|
||||
|
||||
for i, key in enumerate(keys):
|
||||
if not isinstance(current, dict) or key not in current:
|
||||
break
|
||||
|
||||
if i == len(keys) - 1:
|
||||
# At the final key - check if it's a scalar value
|
||||
if isinstance(current[key], (dict, list)):
|
||||
# Skip dict and list types
|
||||
pass
|
||||
else:
|
||||
# Valid scalar value - convert and set it
|
||||
converted_value = convert_value(env_value, current[key])
|
||||
current[key] = converted_value
|
||||
else:
|
||||
# Navigate deeper
|
||||
current = current[key]
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
@stage.stage_class('LoadConfigStage')
|
||||
class LoadConfigStage(stage.BootingStage):
|
||||
"""Load config file stage"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
"""Load config file"""
|
||||
|
||||
# # ======= deprecated =======
|
||||
# if os.path.exists('data/config/command.json'):
|
||||
# ap.command_cfg = await config.load_json_config(
|
||||
# 'data/config/command.json',
|
||||
# 'templates/legacy/command.json',
|
||||
# completion=False,
|
||||
# )
|
||||
|
||||
# if os.path.exists('data/config/pipeline.json'):
|
||||
# ap.pipeline_cfg = await config.load_json_config(
|
||||
# 'data/config/pipeline.json',
|
||||
# 'templates/legacy/pipeline.json',
|
||||
# completion=False,
|
||||
# )
|
||||
|
||||
# if os.path.exists('data/config/platform.json'):
|
||||
# ap.platform_cfg = await config.load_json_config(
|
||||
# 'data/config/platform.json',
|
||||
# 'templates/legacy/platform.json',
|
||||
# completion=False,
|
||||
# )
|
||||
|
||||
# if os.path.exists('data/config/provider.json'):
|
||||
# ap.provider_cfg = await config.load_json_config(
|
||||
# 'data/config/provider.json',
|
||||
# 'templates/legacy/provider.json',
|
||||
# completion=False,
|
||||
# )
|
||||
|
||||
# if os.path.exists('data/config/system.json'):
|
||||
# ap.system_cfg = await config.load_json_config(
|
||||
# 'data/config/system.json',
|
||||
# 'templates/legacy/system.json',
|
||||
# completion=False,
|
||||
# )
|
||||
|
||||
# # ======= deprecated =======
|
||||
|
||||
ap.instance_config = await config.load_yaml_config('data/config.yaml', 'config.yaml', completion=False)
|
||||
|
||||
# Apply environment variable overrides to data/config.yaml
|
||||
ap.instance_config.data = _apply_env_overrides_to_config(ap.instance_config.data)
|
||||
|
||||
await ap.instance_config.dump_config()
|
||||
|
||||
ap.sensitive_meta = await config.load_json_config(
|
||||
'data/metadata/sensitive-words.json',
|
||||
'metadata/sensitive-words.json',
|
||||
)
|
||||
await ap.sensitive_meta.dump_config()
|
||||
|
||||
async def load_resource_yaml_template_data(resource_name: str) -> dict:
|
||||
with resources.files('langbot.templates').joinpath(resource_name).open('r', encoding='utf-8') as f:
|
||||
return yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
ap.pipeline_config_meta_trigger = await load_resource_yaml_template_data('metadata/pipeline/trigger.yaml')
|
||||
ap.pipeline_config_meta_safety = await load_resource_yaml_template_data('metadata/pipeline/safety.yaml')
|
||||
ap.pipeline_config_meta_ai = await load_resource_yaml_template_data('metadata/pipeline/ai.yaml')
|
||||
ap.pipeline_config_meta_output = await load_resource_yaml_template_data('metadata/pipeline/output.yaml')
|
||||
43
src/langbot/pkg/core/stages/migrate.py
Normal file
43
src/langbot/pkg/core/stages/migrate.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from .. import stage, app
|
||||
from .. import migration
|
||||
from ...utils import importutil
|
||||
from .. import migrations
|
||||
|
||||
importutil.import_modules_in_pkg(migrations)
|
||||
|
||||
|
||||
@stage.stage_class('MigrationStage')
|
||||
class MigrationStage(stage.BootingStage):
|
||||
"""Migration stage
|
||||
|
||||
These migrations are legacy, only performed in version 3.x
|
||||
"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
"""Run migration"""
|
||||
|
||||
if any(
|
||||
[
|
||||
ap.command_cfg is None,
|
||||
ap.pipeline_cfg is None,
|
||||
ap.platform_cfg is None,
|
||||
ap.provider_cfg is None,
|
||||
ap.system_cfg is None,
|
||||
]
|
||||
): # only run migration when version is 3.x
|
||||
return
|
||||
|
||||
migrations = migration.preregistered_migrations
|
||||
|
||||
# Sort by migration number
|
||||
migrations.sort(key=lambda x: x.number)
|
||||
|
||||
for migration_cls in migrations:
|
||||
migration_instance = migration_cls(ap)
|
||||
|
||||
if await migration_instance.need_migrate():
|
||||
await migration_instance.run()
|
||||
print(f'Migration {migration_instance.name} executed')
|
||||
46
src/langbot/pkg/core/stages/setup_logger.py
Normal file
46
src/langbot/pkg/core/stages/setup_logger.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from .. import stage, app
|
||||
from ..bootutils import log
|
||||
|
||||
|
||||
class PersistenceHandler(logging.Handler, object):
|
||||
"""
|
||||
Save logs to database
|
||||
"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, name, ap: app.Application):
|
||||
logging.Handler.__init__(self)
|
||||
self.ap = ap
|
||||
|
||||
def emit(self, record):
|
||||
"""
|
||||
emit function is a required function for custom handler classes, here you can process the log messages as needed, such as sending logs to the server
|
||||
|
||||
Emit a record
|
||||
"""
|
||||
try:
|
||||
msg = self.format(record)
|
||||
if self.ap.log_cache is not None:
|
||||
self.ap.log_cache.add_log(msg)
|
||||
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
@stage.stage_class('SetupLoggerStage')
|
||||
class SetupLoggerStage(stage.BootingStage):
|
||||
"""Setup logger stage"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
"""Setup logger"""
|
||||
persistence_handler = PersistenceHandler('LoggerHandler', ap)
|
||||
|
||||
extra_handlers = []
|
||||
extra_handlers = [persistence_handler]
|
||||
|
||||
ap.logger = await log.init_logging(extra_handlers)
|
||||
36
src/langbot/pkg/core/stages/show_notes.py
Normal file
36
src/langbot/pkg/core/stages/show_notes.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from .. import stage, app, note
|
||||
from ...utils import importutil
|
||||
|
||||
from .. import notes
|
||||
|
||||
importutil.import_modules_in_pkg(notes)
|
||||
|
||||
|
||||
@stage.stage_class('ShowNotesStage')
|
||||
class ShowNotesStage(stage.BootingStage):
|
||||
"""Show notes stage"""
|
||||
|
||||
async def run(self, ap: app.Application):
|
||||
# Sort
|
||||
note.preregistered_notes.sort(key=lambda x: x.number)
|
||||
|
||||
for note_cls in note.preregistered_notes:
|
||||
try:
|
||||
note_inst = note_cls(ap)
|
||||
if await note_inst.need_show():
|
||||
|
||||
async def ayield_note(note_inst: note.LaunchNote):
|
||||
async for ret in note_inst.yield_note():
|
||||
if not ret:
|
||||
continue
|
||||
msg, level = ret
|
||||
if msg:
|
||||
ap.logger.log(level, msg)
|
||||
|
||||
asyncio.create_task(ayield_note(note_inst))
|
||||
except Exception:
|
||||
continue
|
||||
236
src/langbot/pkg/core/taskmgr.py
Normal file
236
src/langbot/pkg/core/taskmgr.py
Normal file
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import typing
|
||||
import datetime
|
||||
|
||||
from . import app
|
||||
from . import entities as core_entities
|
||||
|
||||
|
||||
class TaskContext:
|
||||
"""Task tracking context"""
|
||||
|
||||
current_action: str
|
||||
"""Current action being executed"""
|
||||
|
||||
log: str
|
||||
"""Log"""
|
||||
|
||||
def __init__(self):
|
||||
self.current_action = 'default'
|
||||
self.log = ''
|
||||
|
||||
def _log(self, msg: str):
|
||||
self.log += msg + '\n'
|
||||
|
||||
def set_current_action(self, action: str):
|
||||
self.current_action = action
|
||||
|
||||
def trace(
|
||||
self,
|
||||
msg: str,
|
||||
action: str = None,
|
||||
):
|
||||
if action is not None:
|
||||
self.set_current_action(action)
|
||||
|
||||
self._log(f'{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | {self.current_action} | {msg}')
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {'current_action': self.current_action, 'log': self.log}
|
||||
|
||||
@staticmethod
|
||||
def new() -> TaskContext:
|
||||
return TaskContext()
|
||||
|
||||
@staticmethod
|
||||
def placeholder() -> TaskContext:
|
||||
global placeholder_context
|
||||
|
||||
if placeholder_context is None:
|
||||
placeholder_context = TaskContext()
|
||||
|
||||
return placeholder_context
|
||||
|
||||
|
||||
placeholder_context: TaskContext | None = None
|
||||
|
||||
|
||||
class TaskWrapper:
|
||||
"""Task wrapper"""
|
||||
|
||||
_id_index: int = 0
|
||||
"""Task ID index"""
|
||||
|
||||
id: int
|
||||
"""Task ID"""
|
||||
|
||||
task_type: str = 'system' # Task type: system or user
|
||||
"""Task type"""
|
||||
|
||||
kind: str = 'system_task' # Task type determined by the initiator, usually the same task type
|
||||
"""Task type"""
|
||||
|
||||
name: str = ''
|
||||
"""Task unique name"""
|
||||
|
||||
label: str = ''
|
||||
"""Task display name"""
|
||||
|
||||
task_context: TaskContext
|
||||
"""Task context"""
|
||||
|
||||
task: asyncio.Task
|
||||
"""Task"""
|
||||
|
||||
task_stack: list = None
|
||||
"""Task stack"""
|
||||
|
||||
ap: app.Application
|
||||
"""Application instance"""
|
||||
|
||||
scopes: list[core_entities.LifecycleControlScope]
|
||||
"""Task scope"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ap: app.Application,
|
||||
coro: typing.Coroutine,
|
||||
task_type: str = 'system',
|
||||
kind: str = 'system_task',
|
||||
name: str = '',
|
||||
label: str = '',
|
||||
context: TaskContext = None,
|
||||
scopes: list[core_entities.LifecycleControlScope] = [core_entities.LifecycleControlScope.APPLICATION],
|
||||
):
|
||||
self.id = TaskWrapper._id_index
|
||||
TaskWrapper._id_index += 1
|
||||
self.ap = ap
|
||||
self.task_context = context or TaskContext()
|
||||
self.task = self.ap.event_loop.create_task(coro)
|
||||
self.task_type = task_type
|
||||
self.kind = kind
|
||||
self.name = name
|
||||
self.label = label if label != '' else name
|
||||
self.task.set_name(name)
|
||||
self.scopes = scopes
|
||||
|
||||
def assume_exception(self):
|
||||
try:
|
||||
exception = self.task.exception()
|
||||
if self.task_stack is None:
|
||||
self.task_stack = self.task.get_stack()
|
||||
return exception
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def assume_result(self):
|
||||
try:
|
||||
return self.task.result()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
exception_traceback = None
|
||||
if self.assume_exception() is not None:
|
||||
exception_traceback = 'Traceback (most recent call last):\n'
|
||||
|
||||
for frame in self.task_stack:
|
||||
exception_traceback += (
|
||||
f' File "{frame.f_code.co_filename}", line {frame.f_lineno}, in {frame.f_code.co_name}\n'
|
||||
)
|
||||
|
||||
exception_traceback += f' {self.assume_exception().__str__()}\n'
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'task_type': self.task_type,
|
||||
'kind': self.kind,
|
||||
'name': self.name,
|
||||
'label': self.label,
|
||||
'scopes': [scope.value for scope in self.scopes],
|
||||
'task_context': self.task_context.to_dict(),
|
||||
'runtime': {
|
||||
'done': self.task.done(),
|
||||
'state': self.task._state,
|
||||
'exception': self.assume_exception().__str__() if self.assume_exception() is not None else None,
|
||||
'exception_traceback': exception_traceback,
|
||||
'result': self.assume_result() if self.assume_result() is not None else None,
|
||||
},
|
||||
}
|
||||
|
||||
def cancel(self):
|
||||
self.task.cancel()
|
||||
|
||||
|
||||
class AsyncTaskManager:
|
||||
"""Save all asynchronous tasks in the app
|
||||
Include system-level and user-level (plugin installation, update, etc. initiated by users directly)"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
tasks: list[TaskWrapper]
|
||||
"""All tasks"""
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
self.tasks = []
|
||||
|
||||
def create_task(
|
||||
self,
|
||||
coro: typing.Coroutine,
|
||||
task_type: str = 'system',
|
||||
kind: str = 'system-task',
|
||||
name: str = '',
|
||||
label: str = '',
|
||||
context: TaskContext = None,
|
||||
scopes: list[core_entities.LifecycleControlScope] = [core_entities.LifecycleControlScope.APPLICATION],
|
||||
) -> TaskWrapper:
|
||||
wrapper = TaskWrapper(self.ap, coro, task_type, kind, name, label, context, scopes)
|
||||
self.tasks.append(wrapper)
|
||||
return wrapper
|
||||
|
||||
def create_user_task(
|
||||
self,
|
||||
coro: typing.Coroutine,
|
||||
kind: str = 'user-task',
|
||||
name: str = '',
|
||||
label: str = '',
|
||||
context: TaskContext = None,
|
||||
scopes: list[core_entities.LifecycleControlScope] = [core_entities.LifecycleControlScope.APPLICATION],
|
||||
) -> TaskWrapper:
|
||||
return self.create_task(coro, 'user', kind, name, label, context, scopes)
|
||||
|
||||
async def wait_all(self):
|
||||
await asyncio.gather(*[t.task for t in self.tasks], return_exceptions=True)
|
||||
|
||||
def get_all_tasks(self) -> list[TaskWrapper]:
|
||||
return self.tasks
|
||||
|
||||
def get_tasks_dict(
|
||||
self,
|
||||
type: str = None,
|
||||
) -> dict:
|
||||
return {
|
||||
'tasks': [t.to_dict() for t in self.tasks if type is None or t.task_type == type],
|
||||
'id_index': TaskWrapper._id_index,
|
||||
}
|
||||
|
||||
def get_task_by_id(self, id: int) -> TaskWrapper | None:
|
||||
for t in self.tasks:
|
||||
if t.id == id:
|
||||
return t
|
||||
return None
|
||||
|
||||
def cancel_by_scope(self, scope: core_entities.LifecycleControlScope):
|
||||
for wrapper in self.tasks:
|
||||
if not wrapper.task.done() and scope in wrapper.scopes:
|
||||
wrapper.task.cancel()
|
||||
|
||||
def cancel_task(self, task_id: int):
|
||||
for wrapper in self.tasks:
|
||||
if wrapper.id == task_id:
|
||||
if not wrapper.task.done():
|
||||
wrapper.task.cancel()
|
||||
return
|
||||
Reference in New Issue
Block a user