From 2939ae61e8433cd4459cffaa7026bd886e55b6b2 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:21:06 +0800 Subject: [PATCH 01/41] =?UTF-8?q?=F0=9F=91=BD=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=E9=80=82=E9=85=8D=20pydantic=20v2=20API=20=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 validator 装饰器迁移为 field_validator 并使用 mode="before" - 将 .dict() 调用替换为 .model_dump() 以进行模型序列化 - 将 .json() 调用替换为 .model_dump_json() 以输出 JSON - 将 parse_obj() 调用替换为 model_validate() 以进行模型解析 - 将 parse_file() 调用替换为 model_validate_json() 以进行文件解析 - 移除 GenericModel 用法,改用 BaseModel 配合 Generic - 将 __root__ 替换为 root(使用 RootModel) - 将 __fields__ 替换为 model_fields 以访问字段信息 - 将 Config 内部类转换为 ClassVar 类型变量 - 为 Optional 字段添加显式 default=None 以符合 v2 规范 --- nb_cli_plugin_webui/app/config.py | 14 +++++--- .../app/handlers/process/schemas.py | 4 +-- nb_cli_plugin_webui/app/handlers/project.py | 26 +++++++------- nb_cli_plugin_webui/app/handlers/store.py | 4 +-- nb_cli_plugin_webui/app/models/base.py | 34 ++++++------------- nb_cli_plugin_webui/app/models/store.py | 4 +-- nb_cli_plugin_webui/app/process/router.py | 2 +- .../app/project/config/schemas.py | 8 ++--- nb_cli_plugin_webui/app/project/service.py | 10 +++--- nb_cli_plugin_webui/app/schemas.py | 4 +-- nb_cli_plugin_webui/app/status/router.py | 2 +- nb_cli_plugin_webui/app/status/schemas.py | 2 +- nb_cli_plugin_webui/app/utils/security/jwt.py | 2 +- 13 files changed, 54 insertions(+), 62 deletions(-) diff --git a/nb_cli_plugin_webui/app/config.py b/nb_cli_plugin_webui/app/config.py index d5355ad..32a3d1d 100644 --- a/nb_cli_plugin_webui/app/config.py +++ b/nb_cli_plugin_webui/app/config.py @@ -75,7 +75,12 @@ def log_level_str(self) -> str: return self.log_level.value def to_json(self) -> str: - return json.dumps(self.dict(), cls=SpecialTypeJSONEncoder) + return json.dumps( + self.model_dump(mode="python"), + cls=SpecialTypeJSONEncoder, + indent=2, + ensure_ascii=False, + ) def reset_token(self, token: str) -> None: self.salt = SecretStr(salt.gen_salt()) @@ -88,13 +93,12 @@ def check_necessary_config(self) -> bool: self.base_dir and self.secret_key and self.hashed_token and self.salt ) - def get_description(self, field_name: str) -> str: - return self.__fields__[field_name].field_info.description - + def get_description(self, field_name: str) -> str | None: + return self.__class__.model_fields[field_name].description class ConfigParser(AppConfig): def load(self, path: Path): - new_conf = self.parse_file(path) + new_conf = AppConfig.model_validate_json(path.read_text(encoding="utf-8")) self.__dict__.update(new_conf.__dict__) def store(self, path: Path): diff --git a/nb_cli_plugin_webui/app/handlers/process/schemas.py b/nb_cli_plugin_webui/app/handlers/process/schemas.py index 7645e8e..1d6ef5c 100644 --- a/nb_cli_plugin_webui/app/handlers/process/schemas.py +++ b/nb_cli_plugin_webui/app/handlers/process/schemas.py @@ -34,7 +34,7 @@ class ProcessPerformance(BaseModel): class ProcessInfo(BaseModel): - status_code: Optional[int] + status_code: Optional[int] = None total_log: int is_running: bool - performance: Optional[ProcessPerformance] + performance: Optional[ProcessPerformance] = None diff --git a/nb_cli_plugin_webui/app/handlers/project.py b/nb_cli_plugin_webui/app/handlers/project.py index e6d74d8..43a1d93 100644 --- a/nb_cli_plugin_webui/app/handlers/project.py +++ b/nb_cli_plugin_webui/app/handlers/project.py @@ -5,7 +5,7 @@ from nb_cli.config import ConfigManager from dotenv import set_key, dotenv_values from tomlkit.toml_document import TOMLDocument -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel, RootModel, ValidationError from nb_cli.exceptions import ProjectNotFoundError from nb_cli.config import SimpleInfo as CliSimpleInfo from nb_cli.config.parser import CONFIG_FILE_ENCODING @@ -26,8 +26,8 @@ PROJECT_DATA_ENCODING = "utf-8" -class NoneBotProjectList(BaseModel): - __root__: Dict[str, NoneBotProjectMeta] +class NoneBotProjectList(RootModel): + root: Dict[str, NoneBotProjectMeta] class NoneBotProjectManager: @@ -51,7 +51,9 @@ def __init__(self, *, project_id: str) -> None: @classmethod def _load(cls) -> NoneBotProjectList: try: - return NoneBotProjectList.parse_file(PROJECT_DATA_PATH, encoding="utf-8") + return NoneBotProjectList.model_validate_json( + PROJECT_DATA_PATH.read_text(PROJECT_DATA_ENCODING) + ) except FileNotFoundError as err: raise err except ValidationError as err: @@ -59,21 +61,21 @@ def _load(cls) -> NoneBotProjectList: @classmethod def get_project(cls) -> Dict[str, NoneBotProjectMeta]: - return cls._load().__root__ + return cls._load().root def store(self, data: NoneBotProjectMeta) -> None: if not PROJECT_DATA_PATH.exists(): - file = NoneBotProjectList(__root__={self.project_id: data}) + file = NoneBotProjectList(root={self.project_id: data}) else: file = self._load() - file.__root__[self.project_id] = data + file.root[self.project_id] = data - PROJECT_DATA_PATH.write_text(file.json(), encoding="utf-8") + PROJECT_DATA_PATH.write_text(file.model_dump_json(), encoding=PROJECT_DATA_ENCODING) def read(self) -> NoneBotProjectMeta: try: load = self._load() - data = load.__root__ + data = load.root except FileNotFoundError: raise FileNotFoundError(f"{PROJECT_DATA_FILE} Not found") info = data.get(self.project_id) @@ -124,8 +126,8 @@ async def add_project( def remove_project(self) -> None: data = self._load() - data.__root__.pop(self.project_id) - PROJECT_DATA_PATH.write_text(data.json(), encoding="utf-8") + data.root.pop(self.project_id) + PROJECT_DATA_PATH.write_text(data.model_dump_json(), encoding=PROJECT_DATA_ENCODING) def modify_meta(self, k: str, v: Any) -> None: data = self.read() @@ -176,7 +178,7 @@ async def update_plugin_config(self) -> None: pkg_version = await get_pkg_version(plugin, self.config_manager.python_path) plugin_metadata["version"] = pkg_version - metadata = Plugin.parse_obj(plugin_metadata) + metadata = Plugin.model_validate(plugin_metadata) if metadata.module_name == "unknown": metadata.module_name = plugin diff --git a/nb_cli_plugin_webui/app/handlers/store.py b/nb_cli_plugin_webui/app/handlers/store.py index 622a904..c7ef701 100644 --- a/nb_cli_plugin_webui/app/handlers/store.py +++ b/nb_cli_plugin_webui/app/handlers/store.py @@ -69,7 +69,7 @@ async def load_module_data( _("Invalid module type: {module_type}").format(module_type=module_type) ) - module_name: str = getattr(module_class.__config__, "module_name") + module_name: str = module_class._registry_name exceptions: List[Exception] = list() urls = [ f"https://registry.nonebot.dev/{module_name}.json", @@ -91,7 +91,7 @@ async def _request(url: str) -> httpx.Response: try: resp = await future items = resp.json() - result = [module_class.parse_obj(item) for item in items] + result = [module_class.model_validate(item) for item in items] for task in tasks: if not task.done(): task.cancel() diff --git a/nb_cli_plugin_webui/app/models/base.py b/nb_cli_plugin_webui/app/models/base.py index 59f1a1d..bd3443c 100644 --- a/nb_cli_plugin_webui/app/models/base.py +++ b/nb_cli_plugin_webui/app/models/base.py @@ -1,6 +1,7 @@ -from typing import Any, Dict, List, Generic, TypeVar, Optional +from typing import Any, Dict, List, Generic, TypeVar, ClassVar, Optional -from pydantic import Field, BaseModel, validator +from pydantic import Field, BaseModel +from pydantic.functional_validators import field_validator class ModuleTag(BaseModel): @@ -21,45 +22,30 @@ class ModuleInfo(BaseModel): extra: Dict[Any, Any] = Field(default_factory=dict) # noneflow:PublishInfo 类型, 为通用移至此处 - tags: Optional[List[ModuleTag]] + tags: Optional[List[ModuleTag]] = None is_official: bool = False version: str = "0.0.0" # WebUI 拓展类型 is_download: bool = False - @validator("project_link", pre=True, always=True) - def set_project_link_default(cls, v): - return v if v is not None else "unknown" - - @validator("author", pre=True, always=True) - def set_author_default(cls, v): - return v if v is not None else "unknown" - - @validator("homepage", pre=True, always=True) - def set_homepage_default(cls, v): + @field_validator("project_link", "author", "homepage", mode="before") + @classmethod + def set_default_if_none(cls, v): return v if v is not None else "unknown" class Plugin(ModuleInfo): config: dict = dict() - - class Config: - module_name = "plugins" + _registry_name: ClassVar[str] = "plugins" class Adapter(ModuleInfo): - pass - - class Config: - module_name = "adapters" + _registry_name: ClassVar[str] = "adapters" class Driver(ModuleInfo): - pass - - class Config: - module_name = "drivers" + _registry_name: ClassVar[str] = "drivers" _T = TypeVar("_T", bound=Plugin) diff --git a/nb_cli_plugin_webui/app/models/store.py b/nb_cli_plugin_webui/app/models/store.py index 2947b9c..f484fb2 100644 --- a/nb_cli_plugin_webui/app/models/store.py +++ b/nb_cli_plugin_webui/app/models/store.py @@ -26,8 +26,8 @@ class Plugin(BasePlugin): - https://github.com/nonebot/noneflow/blob/main/src/utils/store_test/models.py#L14 """ - type: Optional[PluginType] - supported_adapters: Optional[list[str]] + type: Optional[PluginType] = None + supported_adapters: Optional[list[str]] = None valid: bool time: str version: str diff --git a/nb_cli_plugin_webui/app/process/router.py b/nb_cli_plugin_webui/app/process/router.py index b80ee43..29202cd 100644 --- a/nb_cli_plugin_webui/app/process/router.py +++ b/nb_cli_plugin_webui/app/process/router.py @@ -100,7 +100,7 @@ def unregister_listener(log_storage: LogStorage[ProcessLog]): log_listeners.pop(websocket) async def log_listener(log: ProcessLog): - await websocket.send_text(log.json()) + await websocket.send_text(log.model_dump_json()) async def receive_listener(recv: dict): nonlocal log_storage diff --git a/nb_cli_plugin_webui/app/project/config/schemas.py b/nb_cli_plugin_webui/app/project/config/schemas.py index 1934af1..fe480a7 100644 --- a/nb_cli_plugin_webui/app/project/config/schemas.py +++ b/nb_cli_plugin_webui/app/project/config/schemas.py @@ -17,21 +17,21 @@ class ConfigType(str, Enum): class ModuleConfigSimpleInfo(BaseModel): title: str - description: Optional[str] + description: Optional[str] = None name: str class Item(BaseModel): - enum: Optional[List[Any]] + enum: Optional[List[Any]] = None type: str class ModuleConfigChild(ModuleConfigSimpleInfo): default: Any conf_type: str - enum: Optional[List[Any]] + enum: Optional[List[Any]] = None configured: Any - items: Optional[Item] + items: Optional[Item] = None unique_items: bool is_secret: bool latest_change: str = ".env" diff --git a/nb_cli_plugin_webui/app/project/service.py b/nb_cli_plugin_webui/app/project/service.py index bb2253f..c01e587 100644 --- a/nb_cli_plugin_webui/app/project/service.py +++ b/nb_cli_plugin_webui/app/project/service.py @@ -35,11 +35,11 @@ def create_nonebot_project(data: CreateProjectData) -> str: context = ProjectContext() context.variables["project_name"] = project_name context.variables["drivers"] = json.dumps( - {driver.project_link: driver.dict() for driver in data.drivers} + {driver.project_link: driver.model_dump() for driver in data.drivers} ) context.packages.extend(drivers) context.variables["adapters"] = json.dumps( - {adapter.project_link: adapter.dict() for adapter in data.adapters} + {adapter.project_link: adapter.model_dump() for adapter in data.adapters} ) context.packages.extend(adapters) @@ -103,10 +103,10 @@ async def install_dependencies(): async def add_project_info(): _adapters: List[ModuleInfo] = [ - ModuleInfo.parse_obj(adapter.dict()) for adapter in data.adapters + ModuleInfo.model_validate(adapter.model_dump()) for adapter in data.adapters ] _drivers: List[ModuleInfo] = [ - ModuleInfo.parse_obj(driver.dict()) for driver in data.drivers + ModuleInfo.model_validate(driver.model_dump()) for driver in data.drivers ] project_id = generate_complexity_string(6) @@ -154,7 +154,7 @@ async def add_nonebot_project(data: AddProjectData) -> str: context = ProjectContext() context.variables["project_name"] = project_name context.variables["adapters"] = json.dumps( - {adapter.project_link: adapter.dict() for adapter in installed_adapters} + {adapter.project_link: adapter.model_dump() for adapter in installed_adapters} ) context.packages.extend([adapter.project_link for adapter in installed_adapters]) diff --git a/nb_cli_plugin_webui/app/schemas.py b/nb_cli_plugin_webui/app/schemas.py index ca0afd0..8af5e86 100644 --- a/nb_cli_plugin_webui/app/schemas.py +++ b/nb_cli_plugin_webui/app/schemas.py @@ -1,9 +1,9 @@ from typing import Generic, TypeVar -from pydantic.generics import GenericModel +from pydantic import BaseModel _T = TypeVar("_T") -class GenericResponse(GenericModel, Generic[_T]): +class GenericResponse(BaseModel, Generic[_T]): detail: _T diff --git a/nb_cli_plugin_webui/app/status/router.py b/nb_cli_plugin_webui/app/status/router.py index 65351bc..cbaec1d 100644 --- a/nb_cli_plugin_webui/app/status/router.py +++ b/nb_cli_plugin_webui/app/status/router.py @@ -72,5 +72,5 @@ async def send_status_info(websocket: WebSocket, process: Optional[Processor]) - net=utils.get_net_info(), ), process=process.get_status() if process else None, - ).dict() + ).model_dump() ) diff --git a/nb_cli_plugin_webui/app/status/schemas.py b/nb_cli_plugin_webui/app/status/schemas.py index 5a377c8..f91c9f6 100644 --- a/nb_cli_plugin_webui/app/status/schemas.py +++ b/nb_cli_plugin_webui/app/status/schemas.py @@ -53,4 +53,4 @@ class SystemInfo(BaseModel): class StatusInfo(BaseModel): system: SystemInfo - process: Optional[ProcessInfo] + process: Optional[ProcessInfo] = None diff --git a/nb_cli_plugin_webui/app/utils/security/jwt.py b/nb_cli_plugin_webui/app/utils/security/jwt.py index eec749f..e3a0434 100644 --- a/nb_cli_plugin_webui/app/utils/security/jwt.py +++ b/nb_cli_plugin_webui/app/utils/security/jwt.py @@ -16,7 +16,7 @@ def create_jwt( ) -> str: to_encode = payload.copy() expire = datetime.utcnow() + expire_seconds - to_encode.update(JWTMeta(exp=expire, sub=JWT_SUBJECT).dict()) + to_encode.update(JWTMeta(exp=expire, sub=JWT_SUBJECT).model_dump()) return jwt.encode(to_encode, secret_key, algorithm=ALGORITHM) From c719323d8e54ba07ea8f2acd310a25bd1d0d38a2 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:34:56 +0800 Subject: [PATCH 02/41] =?UTF-8?q?=F0=9F=94=A7=20chore:=20=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 .pdm-python 到 .gitignore 忽略列表 - 为 Vite 开发服务器添加 API 代理配置 - 添加 pdm 开发脚本命令 - 为 server.py 添加 main 入点以便直接运行 --- .gitignore | 3 +++ frontend/vite.config.ts | 8 ++++++++ nb_cli_plugin_webui/server.py | 10 ++++++++++ pyproject.toml | 4 ++++ 4 files changed, 25 insertions(+) diff --git a/.gitignore b/.gitignore index d7c26f5..cb32275 100644 --- a/.gitignore +++ b/.gitignore @@ -413,6 +413,7 @@ ipython_config.py # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml +.pdm-python # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -524,3 +525,5 @@ dist-ssr *.sw? openapi.json + +.claude \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 6497c54..8787165 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,6 +11,14 @@ export default defineConfig({ '@': fileURLToPath(new URL('./src', import.meta.url)) } }, + server: { + proxy: { + '/api': { + target: 'http://localhost:12345', + changeOrigin: true + } + } + }, build: { outDir: '../nb_cli_plugin_webui/dist' } diff --git a/nb_cli_plugin_webui/server.py b/nb_cli_plugin_webui/server.py index 7407617..1a624b4 100644 --- a/nb_cli_plugin_webui/server.py +++ b/nb_cli_plugin_webui/server.py @@ -33,3 +33,13 @@ async def run_server(host: str, port: int): ) ) return await server.serve() + + +if __name__ == "__main__": + uvicorn.run( + "nb_cli_plugin_webui.app:app", + host="localhost", + port=12345, + reload=True, + reload_dirs=["nb_cli_plugin_webui"], + ) diff --git a/pyproject.toml b/pyproject.toml index 59e4645..8640fda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,10 @@ extra_standard_library = ["typing_extensions"] [tool.pycln] path = "." +[tool.pdm.scripts] +dev-backend = "python -m nb_cli_plugin_webui.server" +dev-frontend = { cmd = "pnpm dev", working_dir = "frontend" } + [build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" From be93ba629304733e602370b4c807274745edcfb6 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:36:30 +0800 Subject: [PATCH 03/41] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E9=BB=98=E8=AE=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在启动时自动检测并生成默认配置文件(开发模式) - 添加静态文件目录存在性检查,避免启动错误 - 改进配置加载逻辑,支持损坏配置文件的降级处理 - 优化启动事件处理流程 --- nb_cli_plugin_webui/app/application.py | 36 ++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/nb_cli_plugin_webui/app/application.py b/nb_cli_plugin_webui/app/application.py index a348c01..acf1862 100644 --- a/nb_cli_plugin_webui/app/application.py +++ b/nb_cli_plugin_webui/app/application.py @@ -8,11 +8,15 @@ from fastapi.staticfiles import StaticFiles as BaseStaticFiles from starlette.exceptions import HTTPException as StarlettleHTTPException +from pydantic import SecretStr + from nb_cli_plugin_webui import get_version -from .config import Config +from .config import Config, CONFIG_FILE_PATH, AppConfig, SpecialTypeJSONEncoder from .logging import logger as log from .utils.scheduler import scheduler +from .utils.security import salt +from .utils.string_utils import generate_complexity_string from .router import router as api_router from .handlers.process import ProcessManager from .handlers import driver_store_manager, plugin_store_manager, adapter_store_manager @@ -53,7 +57,8 @@ async def get_response(self, path: str, scope: Scope) -> Response: frontend = FastAPI(openapi_url="") -frontend.mount("/", StaticFiles(directory=STATIC_PATH, html=True), "NoneBot WebUI") +if STATIC_PATH.exists(): + frontend.mount("/", StaticFiles(directory=STATIC_PATH, html=True), "NoneBot WebUI") api = FastAPI( @@ -79,6 +84,33 @@ async def startup_event(): if "WEBUI_BUILD" in os.environ: log.info("Running in docker.") + # 自动生成默认配置(开发模式) + if not CONFIG_FILE_PATH.exists() and "WEBUI_BUILD" not in os.environ: + _salt = salt.gen_salt() + _token = generate_complexity_string(use_digits=True, use_punctuation=True) + default_config = AppConfig( + base_dir=str(), + host="localhost", + port="12345", + secret_key=salt.get_token_hash( + generate_complexity_string(32, use_digits=True, use_punctuation=True) + ), + salt=SecretStr(_salt), + hashed_token=salt.get_token_hash(_salt + _token), + ) + CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) + CONFIG_FILE_PATH.write_text(default_config.to_json(), encoding="utf-8") + Config.load(CONFIG_FILE_PATH) + log.info("Generated default config for development.") + log.info(f"Access token: {_token}") + elif CONFIG_FILE_PATH.exists(): + try: + Config.load(CONFIG_FILE_PATH) + except Exception: + log.warning("Config file is broken, using default values.") + else: + log.info("Running in docker, config will be read from environment.") + log.info("Starting NoneBot CLI WebUI.") log.info(f"NoneBot CLI WebUI version: {get_version()}") if Config.debug: From a0be5e47290eaac4014aae2fdb7ff2b6f17a5ff6 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:39:00 +0800 Subject: [PATCH 04/41] =?UTF-8?q?=F0=9F=94=A5=20refactor(frontend):=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=89=8B=E5=8A=A8=E5=BC=80=E5=8F=91=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E6=94=B9=E7=94=A8=20Vite=20=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除登录页面的开发模式表单和 host/port 输入 - 移除设置页面的开发模式开关 - 将 isDebug 改为 computed 属性,基于 import.meta.env.DEV 自动判断 - 移除 init.ts 中的 debugUrl 本地存储读取 - 简化前端状态管理逻辑 --- frontend/src/client/init.ts | 3 +- .../src/components/header/WebUISettings.vue | 12 ------ frontend/src/stores/CustomStore.ts | 23 ++--------- frontend/src/views/Login/LoginForm.vue | 41 +------------------ 4 files changed, 6 insertions(+), 73 deletions(-) diff --git a/frontend/src/client/init.ts b/frontend/src/client/init.ts index 907a8ff..0c91e8c 100644 --- a/frontend/src/client/init.ts +++ b/frontend/src/client/init.ts @@ -17,7 +17,6 @@ export const initWebUI = async () => { app.mount('#app') const token = localStorage.getItem('token') || '' - const base = localStorage.getItem('debugUrl') || '' if (!token) { router.push('/login') @@ -25,7 +24,7 @@ export const initWebUI = async () => { } client.setConfig({ - baseUrl: base + baseUrl: '' }) client.interceptors.request.use((request) => { request.headers.set('Authorization', `Bearer ${token}`) diff --git a/frontend/src/components/header/WebUISettings.vue b/frontend/src/components/header/WebUISettings.vue index e08aced..151e071 100644 --- a/frontend/src/components/header/WebUISettings.vue +++ b/frontend/src/components/header/WebUISettings.vue @@ -32,18 +32,6 @@ const drawerRef = ref | null>() -
- -
-
@@ -77,24 +56,6 @@ const login = async () => {
- -
- - - -
From 82a3e9ac392f99ebd3545836e1427ae15c2b413c Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:40:46 +0800 Subject: [PATCH 05/41] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(cli):=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20CLI=20=E5=91=BD=E4=BB=A4=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=BA=A4=E4=BA=92=E5=BC=8F=E9=85=8D=E7=BD=AE=E5=90=91?= =?UTF-8?q?=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 config.py 中的 generate_config 函数(117行) - 提取配置检查逻辑到 _ensure_config 函数 - 添加 dev 命令,同时启动前端和后端开发服务器 - 简化 run 命令,移除冗余的命令行选项 - 改进配置自动生成流程,提供更好的用户体验 - 优化命令执行顺序,先检查子命令再验证环境 --- nb_cli_plugin_webui/app/config.py | 127 +--------------------------- nb_cli_plugin_webui/cli/__init__.py | 123 ++++++++++++++++----------- 2 files changed, 76 insertions(+), 174 deletions(-) diff --git a/nb_cli_plugin_webui/app/config.py b/nb_cli_plugin_webui/app/config.py index 32a3d1d..259a0a9 100644 --- a/nb_cli_plugin_webui/app/config.py +++ b/nb_cli_plugin_webui/app/config.py @@ -4,20 +4,10 @@ from typing import Any from pathlib import Path -import click -from nb_cli.cli import CLI_DEFAULT_STYLE from pydantic import Field, BaseModel, SecretStr -from noneprompt import InputPrompt, ConfirmPrompt - -from nb_cli_plugin_webui.i18n import _ from .utils.security import salt from .utils.storage import get_config_file -from .utils.string_utils import ( - TokenComplexityError, - check_string_complexity, - generate_complexity_string, -) CONFIG_FILE = "config.json" CONFIG_FILE_PATH = get_config_file(CONFIG_FILE) @@ -99,124 +89,11 @@ def get_description(self, field_name: str) -> str | None: class ConfigParser(AppConfig): def load(self, path: Path): new_conf = AppConfig.model_validate_json(path.read_text(encoding="utf-8")) - self.__dict__.update(new_conf.__dict__) + for name in self.model_fields: + setattr(self, name, getattr(new_conf, name)) def store(self, path: Path): path.write_text(self.to_json(), encoding="utf-8") Config = ConfigParser() - - -async def generate_config(): - click.secho(_("Welcome to use NB CLI WebUI."), fg="green") - click.secho("") - click.secho(_("[Token Setting]"), fg="green") - click.secho(_("Token is your key to access WebUI.")) - if await ConfirmPrompt(_("Do you want it generated?")).prompt_async( - style=CLI_DEFAULT_STYLE - ): - token = generate_complexity_string(use_digits=True, use_punctuation=True) - else: - token = await InputPrompt(_("Please enter token:")).prompt_async( - style=CLI_DEFAULT_STYLE - ) - while True: - try: - check_string_complexity(token) - break - except TokenComplexityError as err: - click.secho(str(err)) - - token = await InputPrompt(_("Please enter again:")).prompt_async( - style=CLI_DEFAULT_STYLE - ) - - token = token.replace('"', "'") - - click.secho(_("Your token is:")) - click.secho(f"\n{token}\n", fg="green") - click.secho(_("ATTENTION, TOKEN ONLY SHOW ONCE."), fg="red", bold=True) - - click.secho("") - click.secho(_("[Server Setting]"), fg="green") - host = "localhost" - port = "12345" - if await ConfirmPrompt( - _("Do you want to decide (host) and (port) by yourself?") - ).prompt_async(style=CLI_DEFAULT_STYLE): - host = await InputPrompt(_("Please enter host:")).prompt_async( - style=CLI_DEFAULT_STYLE - ) - port = await InputPrompt(_("Please enter port:")).prompt_async( - style=CLI_DEFAULT_STYLE - ) - while True: - try: - if int(port) < 1024 or int(port) > 49151: - raise ValueError - break - except ValueError: - click.secho( - _("Port must be between 1024 and 49151. (Recommend: > 10000)") - ) - port = await InputPrompt(_("Please enter port:")).prompt_async( - style=CLI_DEFAULT_STYLE - ) - - click.secho(_("Your webui url will be:")) - click.secho(f"http://{host}:{port}/", fg="green") - else: - click.secho(_("Your webui url will decide by nb-cli.")) - - click.secho("") - click.secho(_("[General Setting]"), fg="green") - click.secho(_("- Docker will ignore this and use current directory."), fg="yellow") - click.secho(_("- Base directory. Example:")) - click.secho((" * Linux: /home/(user)/")) - click.secho((" * MacOS: /Users/(user)/")) - click.secho((" * Windows: C:/Users/Public/Pictures")) - click.secho(_("- NoneBot will be stored here.")) - while True: - base_dir = await InputPrompt(_("Please enter base directory:")).prompt_async( - style=CLI_DEFAULT_STYLE - ) - path = Path(base_dir) - - if base_dir and path.is_absolute() and path.is_dir(): - break - - if not base_dir: - click.secho(_("Directory must not be empty.")) - if not path.exists(): - click.secho(_("Directory does not exist.")) - if not path.is_absolute(): - click.secho(_("Path must be absolute.")) - if not path.is_dir(): - click.secho(_("Path must be folder.")) - - click.secho("") - click.secho(_("[Setting Overview]"), fg="green") - click.secho(_("Token: {token}").format(token=token)) - click.secho(_("WebUI URL: http://{host}:{port}/").format(host=host, port=port)) - click.secho(_("Base directory: {base_dir}").format(base_dir=base_dir)) - if not await ConfirmPrompt(_("Are you sure?")).prompt_async( - style=CLI_DEFAULT_STYLE - ): - click.secho(_("Cleaning...")) - return - - _salt = salt.gen_salt() - hashed_token = salt.get_token_hash(_salt + token) - - user_config = AppConfig( - base_dir=base_dir, - host=host, - port=port, - secret_key=SecretStr( - generate_complexity_string(32, use_digits=True, use_punctuation=True) - ), - salt=SecretStr(_salt), - hashed_token=SecretStr(hashed_token), - ) - CONFIG_FILE_PATH.write_text(user_config.to_json(), encoding="utf-8") diff --git a/nb_cli_plugin_webui/cli/__init__.py b/nb_cli_plugin_webui/cli/__init__.py index 6b48f25..d240bb0 100644 --- a/nb_cli_plugin_webui/cli/__init__.py +++ b/nb_cli_plugin_webui/cli/__init__.py @@ -1,9 +1,11 @@ import os import webbrowser +import subprocess +from pathlib import Path from typing import List, cast import click -from pydantic import ValidationError +from pydantic import SecretStr, ValidationError from nb_cli.i18n import _ as nb_cli_i18n from noneprompt import Choice, ListPrompt, ConfirmPrompt, CancelledError from nb_cli.cli import CLI_DEFAULT_STYLE, ClickAliasedGroup, run_sync, run_async @@ -11,45 +13,59 @@ from nb_cli_plugin_webui.i18n import _ from nb_cli_plugin_webui.app.application import STATIC_PATH from nb_cli_plugin_webui.app.handlers.project import PROJECT_DATA_PATH -from nb_cli_plugin_webui.app.config import CONFIG_FILE_PATH, Config, generate_config +from nb_cli_plugin_webui.app.config import CONFIG_FILE_PATH, Config, AppConfig +from nb_cli_plugin_webui.app.utils.security import salt +from nb_cli_plugin_webui.app.utils.string_utils import generate_complexity_string from .token import token from .config import config from .docker import docker +async def _ensure_config(): + if CONFIG_FILE_PATH.exists(): + try: + Config.load(CONFIG_FILE_PATH) + except ValidationError: + click.secho(_("Config file is broken, run `nb ui clear` to fix."), fg="red") + raise + return + + _salt = salt.gen_salt() + _token = generate_complexity_string(use_digits=True, use_punctuation=True) + default_config = AppConfig( + base_dir=str(), + host="localhost", + port="12345", + secret_key=SecretStr( + generate_complexity_string(32, use_digits=True, use_punctuation=True) + ), + salt=SecretStr(_salt), + hashed_token=SecretStr(salt.get_token_hash(_salt + _token)), + ) + CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) + CONFIG_FILE_PATH.write_text(default_config.to_json(), encoding="utf-8") + Config.load(CONFIG_FILE_PATH) + click.secho(_("Generated default config."), fg="green") + click.secho(_("Access token: {token}").format(token=_token), fg="green") + + @click.group( cls=ClickAliasedGroup, invoke_without_command=True, help=_("Start up NB CLI UI.") ) @click.pass_context @run_async async def webui(ctx: click.Context): + if ctx.invoked_subcommand is not None: + return + if not STATIC_PATH.exists(): click.secho( _("WebUI dist directory not found, please reinstall to fix."), fg="red" ) return - if not CONFIG_FILE_PATH.exists(): - if "WEBUI_BUILD" in os.environ: - click.secho( - _("Config not found in docker, run `nb ui docker` to fix."), - fg="yellow", - ) - return - - if not Config.check_necessary_config(): - await generate_config() - return - else: - try: - Config.load(CONFIG_FILE_PATH) - except ValidationError: - click.secho(_("Config file is broken, run `nb ui clear` to fix."), fg="red") - return - - if ctx.invoked_subcommand is not None: - return + await _ensure_config() command = cast(ClickAliasedGroup, ctx.command) @@ -78,36 +94,20 @@ async def webui(ctx: click.Context): @webui.command(help=_("Run WebUI.")) -@click.option( - "-h", - "--host", - type=str, - show_default=True, - help=_("The host required to access NB CLI UI."), - default=None, -) -@click.option( - "-p", - "--port", - type=int, - show_default=True, - help=_("The port required to access NB CLI UI."), - default=None, -) @run_async -async def run(host: str, port: int): +async def run(): + if not STATIC_PATH.exists(): + click.secho( + _("WebUI dist directory not found, please reinstall to fix."), fg="red" + ) + return + + await _ensure_config() + from nb_cli_plugin_webui import server - if not host: - host = Config.host - if not port: - port = int(Config.port) - else: - if port < 1024 or port > 49151: - click.secho( - _("Port must be between 1024 and 49151. (Recommend: > 10000)"), fg="red" - ) - return + host = Config.host + port = int(Config.port) try: webbrowser.open(f"http://{host}:{port}/") @@ -116,6 +116,31 @@ async def run(host: str, port: int): await server.run_server(host, port) +@webui.command(help=_("Run frontend dev server with backend.")) +@run_async +async def dev(): + await _ensure_config() + + frontend_path = Path(__file__).parent.parent.parent / "frontend" + if not frontend_path.exists(): + click.secho(_("Frontend directory not found."), fg="red") + return + + click.secho(_("Starting frontend dev server..."), fg="green") + frontend_proc = subprocess.Popen( + ["pnpm", "dev"], + cwd=frontend_path, + ) + + click.secho(_("Starting backend server..."), fg="green") + try: + from nb_cli_plugin_webui import server + await server.run_server(Config.host, int(Config.port)) + finally: + frontend_proc.terminate() + frontend_proc.wait() + + @webui.command(help=_("Clear WebUI data.")) @run_async async def clear(): From 23cbb90d255296b3475a982a215ee6c95e292179 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:41:38 +0800 Subject: [PATCH 06/41] =?UTF-8?q?=F0=9F=94=A7=20chore:=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20pdm.lock=20=E5=88=B0=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 忽略 pdm.lock 文件以保持项目环境与其余开发者一致(poetry.lock) --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cb32275..11a2d73 100644 --- a/.gitignore +++ b/.gitignore @@ -526,4 +526,7 @@ dist-ssr openapi.json -.claude \ No newline at end of file +.claude + +# pdm.lock 为了保持项目环境与其余开发者一致(poetry.lock),故忽略 pdm.lock +pdm.lock \ No newline at end of file From 3cf50dee38b342311ac3ce6200991cae091f7580 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:42:47 +0800 Subject: [PATCH 07/41] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE=E7=94=9F=E6=88=90=E4=B8=AD?= =?UTF-8?q?=20secret=5Fkey=20=E7=9A=84=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 secret_key 从 salt.get_token_hash() 改为 SecretStr() 以匹配 AppConfig 的字段类型 --- nb_cli_plugin_webui/app/application.py | 31 ++++---------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/nb_cli_plugin_webui/app/application.py b/nb_cli_plugin_webui/app/application.py index acf1862..77f22f5 100644 --- a/nb_cli_plugin_webui/app/application.py +++ b/nb_cli_plugin_webui/app/application.py @@ -8,15 +8,11 @@ from fastapi.staticfiles import StaticFiles as BaseStaticFiles from starlette.exceptions import HTTPException as StarlettleHTTPException -from pydantic import SecretStr - from nb_cli_plugin_webui import get_version -from .config import Config, CONFIG_FILE_PATH, AppConfig, SpecialTypeJSONEncoder +from .config import Config, CONFIG_FILE_PATH, SpecialTypeJSONEncoder from .logging import logger as log from .utils.scheduler import scheduler -from .utils.security import salt -from .utils.string_utils import generate_complexity_string from .router import router as api_router from .handlers.process import ProcessManager from .handlers import driver_store_manager, plugin_store_manager, adapter_store_manager @@ -84,32 +80,13 @@ async def startup_event(): if "WEBUI_BUILD" in os.environ: log.info("Running in docker.") - # 自动生成默认配置(开发模式) - if not CONFIG_FILE_PATH.exists() and "WEBUI_BUILD" not in os.environ: - _salt = salt.gen_salt() - _token = generate_complexity_string(use_digits=True, use_punctuation=True) - default_config = AppConfig( - base_dir=str(), - host="localhost", - port="12345", - secret_key=salt.get_token_hash( - generate_complexity_string(32, use_digits=True, use_punctuation=True) - ), - salt=SecretStr(_salt), - hashed_token=salt.get_token_hash(_salt + _token), - ) - CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) - CONFIG_FILE_PATH.write_text(default_config.to_json(), encoding="utf-8") - Config.load(CONFIG_FILE_PATH) - log.info("Generated default config for development.") - log.info(f"Access token: {_token}") - elif CONFIG_FILE_PATH.exists(): + if CONFIG_FILE_PATH.exists(): try: Config.load(CONFIG_FILE_PATH) except Exception: log.warning("Config file is broken, using default values.") - else: - log.info("Running in docker, config will be read from environment.") + elif "WEBUI_BUILD" not in os.environ: + log.warning("Config not found, using default values.") log.info("Starting NoneBot CLI WebUI.") log.info(f"NoneBot CLI WebUI version: {get_version()}") From e36aa7425d6dbb1aaf93f20f7aa1ff0d4f7ce18f Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 17:44:00 +0800 Subject: [PATCH 08/41] =?UTF-8?q?=F0=9F=94=A5=20cleanup:=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 application.py 中未使用的 SpecialTypeJSONEncoder 导入 --- nb_cli_plugin_webui/app/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nb_cli_plugin_webui/app/application.py b/nb_cli_plugin_webui/app/application.py index 77f22f5..384dfad 100644 --- a/nb_cli_plugin_webui/app/application.py +++ b/nb_cli_plugin_webui/app/application.py @@ -10,7 +10,7 @@ from nb_cli_plugin_webui import get_version -from .config import Config, CONFIG_FILE_PATH, SpecialTypeJSONEncoder +from .config import Config, CONFIG_FILE_PATH from .logging import logger as log from .utils.scheduler import scheduler from .router import router as api_router From 0d46511ec018e8e37084c0738c1b4da515a51e92 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 18:58:01 +0800 Subject: [PATCH 09/41] =?UTF-8?q?:wrench:=20chore:=20=E5=8D=87=E7=BA=A7=20?= =?UTF-8?q?pre-commit=20=E9=92=A9=E5=AD=90=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 pycln 从 v2.1.3 升级至 v2.6.0 - 将 isort 从 5.12.0 升级至 9.0.0a3 - 将 black 从 23.7.0 升级至 26.5.0 - 将 prettier 从 v3.0.3 升级至 v4.0.0-alpha.8 --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be6618f..6c9e0ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,26 +7,26 @@ ci: autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks" repos: - repo: https://github.com/hadialqattan/pycln - rev: v2.1.3 + rev: v2.6.0 hooks: - id: pycln args: [--config, pyproject.toml] stages: [commit] - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 9.0.0a3 hooks: - id: isort stages: [commit] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 26.5.0 hooks: - id: black stages: [commit] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.8 hooks: - id: prettier stages: [commit] From 023261859e1ff9aeafcaff608dcfc586fe137524 Mon Sep 17 00:00:00 2001 From: XuChenXu <91937041+ChenXu233@users.noreply.github.com> Date: Mon, 18 May 2026 18:59:43 +0800 Subject: [PATCH 10/41] =?UTF-8?q?:art:=20style(frontend):=20=E5=BA=94?= =?UTF-8?q?=E7=94=A8=20prettier=20=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一引号风格为双引号 - 添加语句尾分号 - 重排长行以提高可读性 --- frontend/src/App.vue | 8 +- frontend/src/assets/main.css | 2 +- frontend/src/components/EditorItem.vue | 36 ++-- .../components/Modals/AddBot/AddBotIndex.vue | 52 +++--- .../src/components/Modals/AddBot/index.ts | 58 +++---- .../Modals/CreateBot/CreateBotIndex.vue | 72 ++++---- .../Modals/CreateBot/MirrorSelect.vue | 46 ++--- .../components/header/NotificationItem.vue | 20 +-- frontend/src/components/sidebar/SideMenu.vue | 43 +++-- .../ExtensionManage/ExtensionManageIndex.vue | 160 +++++++++--------- frontend/src/views/Operation/BotDetails.vue | 96 ++++++----- 11 files changed, 310 insertions(+), 283 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 13c4a63..e2af2f2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,9 +1,9 @@