Conversation
…us endpoints Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
…bSocket, loading UI, confirmation dialogs Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Introduce two build variants (full and cli-only) and make the GUI an optional module across the stack. Highlights: - CI: GitHub Actions workflow now builds `full` (includes web GUI) and `cli-only` (excludes GUI) artifacts and names them accordingly; frontend build is skipped for CLI-only. - docs: new docs/BUILD_VARIANTS.md describing Full vs CLI build behaviors and CI matrix; updated GUI docs with API additions and usage notes. - runtime: add src/control/runtime.py to detect packaged vs source runs, resolve project root and locate web/dist. - CLI: cli.py now detects availability of the GUI module and only registers the `gui` subcommand when present; prints a friendly message if GUI is missing. - Server: src/control/server.py uses runtime to find web assets, waits for port before opening browser, adds CORS config driven by env, enforces JSON Content-Type for API write methods to mitigate localhost CSRF, adds /api/config/validate, improves static file safety and SPA fallback. - Config API & Process manager: src/control/config_api.py and src/control/process_manager.py use runtime.get_project_root; ProcessManager gains subprocess command builder for packaged runs, gateway health caching + async status API, safer log/stdout cleanup, and other robustness fixes. - Core: processor now only removes progress file on normal exit (preserves it on exception for debugging/GUI timeout handling). - Frontend: web changes include API client validateConfig and proper Content-Type headers for write calls, ConfigEditor adds YAML validation, reload/debounce and unsaved-change prompts, Dashboard shows external process/gateway indications and freshness checks, App displays host in footer. Overall this change modularizes the GUI, supports smaller CLI-only builds, and hardens runtime/static serving and UX for both packaged and source execution.
Clean up unused imports across the codebase to satisfy linters and reduce clutter. Removed an unused `os` import from cli.py, dropped the unused `Optional` typing from src/core/processor.py, and removed unused `os`, `tempfile` imports and an unnecessary `PROJECT_ROOT` reference in tests/test_control.py (only `write_config` is imported now). No functional changes intended.
Reformatting and style cleanup: wrap long if conditions in src/control/process_manager.py (gateway health cache checks in both sync and async paths) with clearer parentheses/indentation, and split the FastAPI imports in src/control/server.py into a multi-line import list. Also add a minor whitespace adjustment in create_control_app. No functional changes intended.
…yout - Add bilingual support (English/Chinese) with i18n.ts module - Add working directory display in header - Update status API to include working_directory - Redesign Logs page with side-by-side Gateway/Process panels - Improve WebSocket connection management - Add language selector toggle in header Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
- Update Dashboard component with full i18n support - Update ConfigEditor component with full i18n support - Replace all hardcoded strings with translation keys - Add interpolation support for dynamic messages - Update component props to accept language parameter Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
- Update README.md with detailed WebUI feature list - Update docs/GUI.md with comprehensive feature descriptions - Add working_directory field to API documentation - Document bilingual support and side-by-side logs layout - All backend files have Chinese docstrings and comments Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
- Update test_control.py to verify working_directory in status - Add assertions to ensure working_directory is present and non-empty - Complete test coverage for new API features Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
- Add 'type' keyword to Language import in ConfigEditor.tsx - Fixes TS1484 error with verbatimModuleSyntax enabled - Verified: consistent with Dashboard.tsx, App.tsx, and Logs.tsx Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
…ionality Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
…m Dashboard Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
…s, and improve responsiveness Co-authored-by: BlueSkyXN <63384277+BlueSkyXN@users.noreply.github.com>
- Replace plain textarea with sidebar + section-based editor - Add 9 config sections: Global, Datasource, Concurrency, Columns, Validation, Models, Channels, Prompt, Routing - Support seamless visual/raw YAML mode switching - Add 10 reusable form components (TextInput, NumberInput, Toggle, etc.) - Add js-yaml for client-side YAML parsing/serialization - Add ~90 i18n keys (EN/ZH) for all form labels - Conditional rendering for datasource connection configs - Collapsible array editors for models and channels Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…com/BlueSkyXN/AI-DataFlux into copilot/add-web-gui-for-ai-dataflux
…flux Add Web GUI control panel for AI-DataFlux
| """ | ||
| real_path = _validate_path(path) | ||
|
|
||
| if not os.path.isfile(real_path): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Copilot Autofix
AI 14 days ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| if not os.path.isfile(real_path): | ||
| raise HTTPException(404, f"File not found: {path}") | ||
|
|
||
| with open(real_path, "r", encoding="utf-8") as f: |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix uncontrolled path usage you should (1) define a trusted root directory, (2) build all user-supplied paths relative to that root, (3) normalize the resulting path with os.path.realpath or os.path.normpath, and (4) verify that the normalized path is still inside the trusted root using a robust check such as os.path.commonpath. Avoid relying on simple string prefix checks or un-normalized roots, and ensure you handle edge cases like different drive letters on Windows.
For this codebase, the best targeted fix is to make _validate_path fully normalize both PROJECT_ROOT and the computed path, and then compare using their normalized forms. Concretely:
- Normalize
PROJECT_ROOTonce when computing it, e.g.PROJECT_ROOT = os.path.realpath(str(runtime.get_project_root())). - In
_validate_path, computerealasos.path.realpath(os.path.join(PROJECT_ROOT, path))as before, but then normalize bothPROJECT_ROOTandrealviaos.path.realpath(or use the already-normalized global) when callingos.path.commonpath. - Compare
commonwith the normalized root (ROOT_REAL) rather than the possibly non-normalizedPROJECT_ROOT.
This still keeps behavior the same (user is restricted to files under the project root), but removes subtle mismatches that could confuse static analysis and closes edge cases where a non-normalized root might behave unexpectedly. No changes are needed in read_config or write_config, since they already funnel access through _validate_path; we just strengthen the helper and the PROJECT_ROOT definition. The only file to edit is src/control/config_api.py.
| @@ -26,7 +26,7 @@ | ||
| # - 源码:仓库根 | ||
| # - 打包:优先 cwd(看起来像项目根),否则可执行文件目录 | ||
| # - 可通过环境变量覆盖 | ||
| PROJECT_ROOT = str(runtime.get_project_root()) | ||
| PROJECT_ROOT = os.path.realpath(str(runtime.get_project_root())) | ||
|
|
||
|
|
||
| def _validate_path(path: str) -> str: | ||
| @@ -47,8 +47,9 @@ | ||
|
|
||
| # 使用 commonpath 检查是否在项目目录内 (跨平台安全) | ||
| try: | ||
| common = os.path.commonpath([PROJECT_ROOT, real]) | ||
| if common != PROJECT_ROOT: | ||
| root_real = os.path.realpath(PROJECT_ROOT) | ||
| common = os.path.commonpath([root_real, real]) | ||
| if common != root_real: | ||
| raise HTTPException(403, "Path outside project directory") | ||
| except ValueError: | ||
| # Windows 上不同驱动器会抛出 ValueError |
| backed_up = False | ||
|
|
||
| # 如果文件已存在,创建备份 | ||
| if os.path.isfile(real_path): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix uncontrolled-path issues, ensure that any user-supplied path is interpreted strictly relative to a trusted root, normalize both the root and the combined path to canonical absolute form, and reject any path that escapes the root or is absolute on its own.
For this codebase, the best targeted fix is: (1) normalize PROJECT_ROOT once to an absolute canonical path (via os.path.realpath), and (2) adjust _validate_path so that it robustly computes the canonical path of the requested file and verifies that it lies under the canonical project root. This means:
- Define
PROJECT_ROOTasos.path.realpath(runtime.get_project_root())instead of juststr(...). This ensures it is absolute and normalized. - In
_validate_path, buildtarget = os.path.join(PROJECT_ROOT, path), then computereal = os.path.realpath(target). - Use
os.path.commonpath([PROJECT_ROOT, real])and verify that it equalsPROJECT_ROOT. On Windows, continue to catchValueErrorfor differing drives. - Optionally, strip leading path separators from
pathto avoidos.path.join(PROJECT_ROOT, "/etc/passwd")discarding the root (though the current commonpath+realpath check would already prevent this ifPROJECT_ROOTis canonical; still, it’s harmless and clarifies intent). Given the instruction to avoid changing functionality, we will not add additional rejections beyond the strengthened canonical-root handling.
No changes are needed in write_config or read_config beyond relying on the improved _validate_path. The behavior remains the same for valid in-tree paths, but malicious or malformed paths are more reliably rejected.
| @@ -26,7 +26,7 @@ | ||
| # - 源码:仓库根 | ||
| # - 打包:优先 cwd(看起来像项目根),否则可执行文件目录 | ||
| # - 可通过环境变量覆盖 | ||
| PROJECT_ROOT = str(runtime.get_project_root()) | ||
| PROJECT_ROOT = os.path.realpath(str(runtime.get_project_root())) | ||
|
|
||
|
|
||
| def _validate_path(path: str) -> str: | ||
| @@ -43,7 +43,8 @@ | ||
| HTTPException: 403 - 路径在项目目录外 | ||
| """ | ||
| # 解析符号链接和 .. 等 | ||
| real = os.path.realpath(os.path.join(PROJECT_ROOT, path)) | ||
| target = os.path.join(PROJECT_ROOT, path) | ||
| real = os.path.realpath(target) | ||
|
|
||
| # 使用 commonpath 检查是否在项目目录内 (跨平台安全) | ||
| try: |
| # 如果文件已存在,创建备份 | ||
| if os.path.isfile(real_path): | ||
| backup_path = real_path + ".bak" | ||
| shutil.copy2(real_path, backup_path) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Copilot Autofix
AI 14 days ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| # 如果文件已存在,创建备份 | ||
| if os.path.isfile(real_path): | ||
| backup_path = real_path + ".bak" | ||
| shutil.copy2(real_path, backup_path) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Copilot Autofix
AI 14 days ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| os.replace(tmp_path, real_path) # 原子操作 | ||
| except Exception as e: | ||
| # 清理临时文件 | ||
| if os.path.exists(tmp_path): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix “uncontrolled data used in path expression” issues you must fully normalize user‑controlled paths, anchor them under a trusted root, and reject any path that escapes that root before using it with filesystem operations. The sanitizer should be applied exactly once at the boundary where untrusted data enters your file‑API layer, and all internal code should then only use the validated, absolute path.
For this codebase, the best fix is to strengthen _validate_path so that:
PROJECT_ROOTis normalized to an absolute, real path in a canonical form.- The user‑controlled
pathis joined to that canonical root usingos.path.join, and the result is normalized withos.path.realpath. - We then use
os.path.commonpathon the normalized root and the normalized candidate path and verify they are equal. - Optionally, we can slightly restructure
_validate_pathto make it more obvious to CodeQL that it is a dedicated validation/sanitization step, which may help it treatrealas “untainted” afterwards.
We will keep the API and external behavior the same: read_config and write_config will still accept a path relative to the project root, and write_config will still create .bak and .tmp files next to the target. The changes are limited to src/control/config_api.py:
- Normalize
PROJECT_ROOTat definition time by wrappingruntime.get_project_root()inos.path.realpath(os.path.abspath(...)). This ensures the root used in thecommonpathcheck is canonical. - In
_validate_path, introduce aproject_root_reallocal variable that reuses the normalized root (for clarity), buildcandidatewithos.path.join(project_root_real, path), normalize it withos.path.realpath, and then computecommon = os.path.commonpath([project_root_real, candidate]). Ifcommon != project_root_real, raise 403. This keeps the security behavior but makes the invariants explicit.
No changes are needed in write_config itself: once _validate_path guarantees that real_path is inside PROJECT_ROOT, real_path + ".tmp" and real_path + ".bak" are also safely inside the project root.
| @@ -26,7 +26,7 @@ | ||
| # - 源码:仓库根 | ||
| # - 打包:优先 cwd(看起来像项目根),否则可执行文件目录 | ||
| # - 可通过环境变量覆盖 | ||
| PROJECT_ROOT = str(runtime.get_project_root()) | ||
| PROJECT_ROOT = os.path.realpath(os.path.abspath(str(runtime.get_project_root()))) | ||
|
|
||
|
|
||
| def _validate_path(path: str) -> str: | ||
| @@ -42,19 +42,22 @@ | ||
| Raises: | ||
| HTTPException: 403 - 路径在项目目录外 | ||
| """ | ||
| # 解析符号链接和 .. 等 | ||
| real = os.path.realpath(os.path.join(PROJECT_ROOT, path)) | ||
| # 规范化项目根目录,确保为绝对路径 | ||
| project_root_real = PROJECT_ROOT | ||
|
|
||
| # 将用户提供的相对路径解析为绝对路径,并解析符号链接和 ".." 等 | ||
| candidate = os.path.realpath(os.path.join(project_root_real, path)) | ||
|
|
||
| # 使用 commonpath 检查是否在项目目录内 (跨平台安全) | ||
| try: | ||
| common = os.path.commonpath([PROJECT_ROOT, real]) | ||
| if common != PROJECT_ROOT: | ||
| common = os.path.commonpath([project_root_real, candidate]) | ||
| if common != project_root_real: | ||
| raise HTTPException(403, "Path outside project directory") | ||
| except ValueError: | ||
| # Windows 上不同驱动器会抛出 ValueError | ||
| raise HTTPException(403, "Path outside project directory") | ||
|
|
||
| return real | ||
| return candidate | ||
|
|
||
|
|
||
| def read_config(path: str) -> str: |
| except Exception as e: | ||
| # 清理临时文件 | ||
| if os.path.exists(tmp_path): | ||
| os.remove(tmp_path) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Copilot Autofix
AI 14 days ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| except ValueError: | ||
| # Windows 上不同驱动器会抛出 ValueError | ||
| raise HTTPException(404, "Not found") | ||
| if os.path.isfile(file_path): |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, the fix is to ensure that any path derived from untrusted input is strictly validated before being used in file operations. For directory-based validation, this means resolving the candidate path (using os.path.realpath or os.path.normpath) and then confirming it is still within an intended root directory. If it is not, or if any resolution error occurs, the server should return an error instead of accessing the file.
For this specific case, the best fix with minimal functional change is to (a) explicitly reject absolute paths provided by the client, and (b) keep the realpath/commonpath based containment check as the authoritative guard. Rejecting absolute paths makes the intent clearer and narrows the accepted input; the containment check then ensures that even crafted relative paths cannot escape WEB_DIST_DIR. We can do this entirely within serve_static in src/control/server.py. Concretely:
- Inside
serve_static, before computingfile_path, checkif os.path.isabs(path): raise HTTPException(404, "Not found")so that any attempt to use an absolute path is denied. - Keep the existing
base_dir = os.path.realpath(WEB_DIST_DIR),file_path = os.path.realpath(os.path.join(base_dir, path)), andos.path.commonpathcheck unchanged, since they already enforce that the resolved file lives insideWEB_DIST_DIR.
No new imports are necessary, sinceosis already imported and providespath.isabs. This preserves existing behavior for legitimate relative paths and tightens/clarifies the validation on the taintedpathvariable that flows intofile_path.
| @@ -385,6 +385,9 @@ | ||
| async def serve_static(path: str): | ||
| """服务静态文件或 SPA fallback""" | ||
| # 防止 path traversal:仅允许访问 WEB_DIST_DIR 内的文件 | ||
| # 显式拒绝绝对路径,所有请求路径必须是相对的 | ||
| if os.path.isabs(path): | ||
| raise HTTPException(404, "Not found") | ||
| base_dir = os.path.realpath(WEB_DIST_DIR) | ||
| file_path = os.path.realpath(os.path.join(base_dir, path)) | ||
| try: |
| # Windows 上不同驱动器会抛出 ValueError | ||
| raise HTTPException(404, "Not found") | ||
| if os.path.isfile(file_path): | ||
| return FileResponse(file_path) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix uncontrolled path usage you should (1) treat the incoming path as relative, (2) reject dangerous patterns like absolute paths or .. segments, (3) normalize/resolve the path against a trusted base directory, and (4) only serve files if the final canonical path is inside that base directory.
For this specific endpoint, the best way to harden it without changing functionality is:
- Normalize and validate the raw
pathsegment as a relative path:- Reject empty or whitespace-only strings.
- Use
os.path.isabs(path)and reject absolute paths. - Use
os.path.normpath(path)and ensure it does not start with..or contain any..segments that would escape upward.
- After that, keep the existing
realpath+commonpathcheck, which protects against symlink tricks and further ensuresfile_pathis underWEB_DIST_DIR. - Only if all checks pass, serve the file or fall back to the SPA
index.html.
All required imports (os) are already present in src/control/server.py, so we only need to update the serve_static function body (lines 385–403) within the file.
| @@ -386,7 +386,27 @@ | ||
| """服务静态文件或 SPA fallback""" | ||
| # 防止 path traversal:仅允许访问 WEB_DIST_DIR 内的文件 | ||
| base_dir = os.path.realpath(WEB_DIST_DIR) | ||
| file_path = os.path.realpath(os.path.join(base_dir, path)) | ||
|
|
||
| # 只允许相对路径,禁止绝对路径和父目录跳转 | ||
| if not path or path.strip() == "": | ||
| # 空路径等同于访问根,由上面的 "/" 路由处理 | ||
| raise HTTPException(404, "Not found") | ||
| # 拒绝绝对路径(如 "/etc/passwd", "C:\\windows\\...") | ||
| if os.path.isabs(path): | ||
| raise HTTPException(404, "Not found") | ||
|
|
||
| # 规范化路径,检查是否包含非法的父目录跳转 | ||
| normalized_path = os.path.normpath(path) | ||
| # 规范化后仍然是绝对路径或者以 ".." 开头/包含上跳目录时拒绝 | ||
| if ( | ||
| os.path.isabs(normalized_path) | ||
| or normalized_path == os.pardir | ||
| or normalized_path.startswith(os.pardir + os.sep) | ||
| or os.sep + os.pardir + os.sep in os.sep + normalized_path + os.sep | ||
| ): | ||
| raise HTTPException(404, "Not found") | ||
|
|
||
| file_path = os.path.realpath(os.path.join(base_dir, normalized_path)) | ||
| try: | ||
| common = os.path.commonpath([base_dir, file_path]) | ||
| if common != base_dir: |
| yaml.safe_load(request.content) | ||
| return {"valid": True} | ||
| except yaml.YAMLError as e: | ||
| return {"valid": False, "error": str(e)} |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 14 days ago
In general, to fix information exposure via exceptions you should avoid sending raw exception messages or stack traces to clients. Instead, log the detailed error on the server (for debugging) and return a generic, non-sensitive error response to the user, optionally including minimal structured metadata (like “line” and “column”) that is safe to expose.
For this concrete case in src/control/server.py, the best fix without changing overall behavior is:
- Catch
yaml.YAMLErroras we already do. - Extract only safe, high-level information from the exception (such as line and column of the error) if available, but do NOT return
str(e)itself. - Optionally log the full exception (including stack trace) using the existing
loggingsetup so developers still get diagnostics. - Return a JSON structure like
{"valid": False, "error": "YAML parsing failed", "details": {...}}wheredetailsonly contains non-sensitive, structured fields.
Concretely:
- Inside
api_validate_config, replacereturn {"valid": False, "error": str(e)}with logic that:- Logs the exception using
logging.exception("Invalid YAML content"). - Builds a safe error payload, possibly inspecting attributes like
e.problem_markif present.
- Logs the exception using
- Do not change imports beyond what’s already there;
loggingis already imported at the top of the file, so we can reuse it.
All changes are confined to the api_validate_config function in src/control/server.py.
| @@ -274,8 +274,25 @@ | ||
| yaml.safe_load(request.content) | ||
| return {"valid": True} | ||
| except yaml.YAMLError as e: | ||
| return {"valid": False, "error": str(e)} | ||
| # Log full exception details on the server for debugging, | ||
| # but do not expose them directly to the client. | ||
| logging.exception("YAML validation failed") | ||
|
|
||
| # Build a safe, structured error response with minimal details. | ||
| error_response = {"valid": False, "error": "YAML parsing failed"} | ||
|
|
||
| # Optionally include non-sensitive location details if available. | ||
| # yaml.YAMLError may have a 'problem_mark' attribute indicating | ||
| # the line and column where parsing failed. | ||
| mark = getattr(e, "problem_mark", None) | ||
| if mark is not None: | ||
| error_response["location"] = { | ||
| "line": getattr(mark, "line", None), | ||
| "column": getattr(mark, "column", None), | ||
| } | ||
|
|
||
| return error_response | ||
|
|
||
| # ========== Gateway API ========== | ||
|
|
||
| @app.post("/api/gateway/start") |
|
PR 概述:AI-DataFlux Web GUI 与双重构建系统 CLI 现已具备自适应能力,仅在 GUI 后端可用时才会暴露 gui 子命令。PyInstaller 和 Nuitka 的工作流均已更新,支持在各平台上构建和打包这两种变体。此外,文档也进行了扩展,详细说明了两个版本的区别及用法。 核心变更详情
自适应设计:该命令仅在 src.control.server 模块存在时才会注册,从而确保 "纯命令行版" (CLI-only) 的构建保持纯净。 相关文件:cli.py, README.md
体积优化:"纯命令行版" (CLI-only) 变体排除了 GUI 模块和静态资源,因此体积更小,占用资源更少。 相关文件:.github/workflows/build-pyinstaller.yml, .github/workflows/build-nuitka.yml
README 更新:在 README.md 中增加了清晰的说明、使用指南以及 "完整版" 与 "纯命令行版" 的功能对比,并解释了 CLI 如何根据可用功能进行自适应。 相关文件:docs/BUILD_VARIANTS.md, README.md
进度集成:process 子命令现在支持可选的进度文件参数,以便更好地与 GUI 集成。 相关文件:cli.py
资源捆绑:确保静态资源在 PyInstaller 和 Nuitka 的 "完整版" 构建中被正确捆绑。 相关文件:.github/workflows/build-pyinstaller.yml, .github/workflows/build-nuitka.yml 变更影响 |
There was a problem hiding this comment.
Pull request overview
This pull request introduces a comprehensive Web GUI control panel for AI-DataFlux version 3.0.0, adding visual management capabilities for Gateway and Process services.
Changes:
- Adds complete React-based web frontend with Dashboard, Config Editor, and Logs viewer
- Implements FastAPI-based control server for process management and configuration API
- Introduces dual build variants (Full with GUI, CLI-only without GUI)
- Adds bilingual support (English/Chinese) throughout the interface
Reviewed changes
Copilot reviewed 56 out of 59 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| web/* | Complete React/TypeScript frontend application with Vite build system |
| src/control/* | FastAPI control server, process manager, and configuration API |
| src/core/processor.py | Added progress file tracking for GUI status display |
| cli.py | Added gui subcommand with conditional registration based on module availability |
| tests/* | New control server tests and updated CLI tests |
| docs/GUI.md | Comprehensive documentation for the Web GUI feature |
| docs/BUILD_VARIANTS.md | Documentation for Full vs CLI-only build variants |
| .github/workflows/* | Updated CI/CD to build both Full and CLI-only versions |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| </div> | ||
| </div> |
There was a problem hiding this comment.
The Dashboard component has an extra closing </div> tag at line 358 that doesn't match any opening tag. Line 194 opens <div className="h-full flex flex-col"> and line 357 closes it with </div>, but line 358 has another </div> which is incorrect. This will cause a React rendering error.
| const handleChooseFile = useCallback(() => { | ||
| const input = document.createElement('input'); | ||
| input.type = 'file'; | ||
| input.accept = '.yaml,.yml'; | ||
| input.onchange = (e) => { | ||
| const file = (e.target as HTMLInputElement).files?.[0]; | ||
| if (file) { | ||
| // Use relative path from file name | ||
| setTempConfigPath(file.name); | ||
| } | ||
| }; | ||
| input.click(); | ||
| }, []); | ||
|
|
||
| const handleChooseFolder = useCallback(() => { | ||
| const input = document.createElement('input'); | ||
| input.type = 'file'; | ||
| input.setAttribute('webkitdirectory', ''); | ||
| input.setAttribute('directory', ''); | ||
| input.onchange = (e) => { | ||
| const files = (e.target as HTMLInputElement).files; | ||
| if (files && files.length > 0) { | ||
| // Get the directory path from the first file | ||
| const filePath = files[0].webkitRelativePath || files[0].name; | ||
| const dirPath = filePath.split('/')[0]; | ||
| setTempWorkingDir(dirPath); | ||
| } | ||
| }; | ||
| input.click(); | ||
| }, []); |
There was a problem hiding this comment.
Security concern: The browser file/folder chooser implementation using webkitRelativePath only gets the relative path from the user's file selection, not an absolute path usable by the backend. When a user selects a folder, the code extracts dirPath = filePath.split('/')[0] which will be just the folder name, not a full path. This means the working directory will be set to a relative path that may not exist or may refer to the wrong location when passed to the backend.
The same issue affects the file chooser which uses file.name - this is just the filename without any path information. These values should either be validated or the feature should use a proper file system API that can provide actual paths.
| # 创建任务 (需要在事件循环中) | ||
| try: | ||
| loop = asyncio.get_running_loop() | ||
| self._read_tasks[name] = loop.create_task(read_logs()) | ||
| except RuntimeError: | ||
| # 没有运行中的事件循环,延迟创建 | ||
| pass |
There was a problem hiding this comment.
Race condition: The _start_log_reader method creates an asyncio task to read logs, but wraps the task creation in a try-except that catches RuntimeError when there's no running event loop. In this case, it simply passes and doesn't create the task. This means if the process manager starts processes before the FastAPI event loop is running, log reading will silently fail and logs won't be captured.
A better approach would be to defer task creation until the event loop is available, or ensure the event loop is running before starting processes.
| # 创建任务 (需要在事件循环中) | |
| try: | |
| loop = asyncio.get_running_loop() | |
| self._read_tasks[name] = loop.create_task(read_logs()) | |
| except RuntimeError: | |
| # 没有运行中的事件循环,延迟创建 | |
| pass | |
| # 创建任务:如果事件循环尚未运行,任务会在事件循环启动后执行 | |
| loop = asyncio.get_event_loop() | |
| self._read_tasks[name] = loop.create_task(read_logs()) |
| p_gateway.add_argument( | ||
| "-c", "--config", default="config.yaml", help="Config file path" | ||
| ) |
There was a problem hiding this comment.
The config_path validation function checks for .yaml or .yml extensions, but the function is named _validate_config_path and is applied to the gateway --config argument. However, it's only checking the extension and not validating if the file exists. The docstring states "仅检查基本格式,不检查存在性" (only checks basic format, doesn't check existence), which is intentional for startup scenarios.
However, the validation is inconsistent: the function is applied to gateway's --config argument but NOT to process's --config argument (line 474 vs line 463). This means gateway gets validation but process doesn't, leading to inconsistent behavior.
| tmp_path = self.progress_file + ".tmp" | ||
| with open(tmp_path, "w", encoding="utf-8") as f: | ||
| json.dump(data, f) | ||
| os.replace(tmp_path, self.progress_file) | ||
| except Exception as e: | ||
| logging.debug(f"写入进度文件失败: {e}") |
There was a problem hiding this comment.
The progress file write operation in _write_progress() lacks proper error handling for the os.replace() call. While there's a broad try-except that catches all exceptions and logs them as debug messages, if os.replace() fails, the temporary file is not cleaned up. This could lead to accumulation of .tmp files over time.
Recommended fix: Add cleanup of the temporary file in the except block before logging the error.
| except Exception: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| pass | |
| except Exception as e: | |
| logging.warning("Failed to close stdout for process %s: %s", name, e) |
| except OSError: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except OSError: | |
| pass | |
| except OSError as e: | |
| # 进度文件清理失败对主流程无影响,忽略错误,但记录调试日志便于排查 | |
| logging.debug(f"Failed to remove progress file {self.progress_file!r}: {e}") |
| candidates.append(embedded_root / "web" / "dist") | ||
| try: | ||
| candidates.append(Path(sys.executable).resolve().parent / "web" / "dist") | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| except Exception: | |
| # Best-effort: if resolving sys.executable fails, just skip this candidate. |
| writer.close() | ||
| try: | ||
| await writer.wait_closed() | ||
| except Exception: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| except Exception: | |
| # Best-effort cleanup: failures while waiting for the writer to close | |
| # are non-fatal and can be safely ignored here. |
| task.add_done_callback( | ||
| lambda t: t.exception() if t.done() and not t.cancelled() else None | ||
| ) | ||
| except RuntimeError: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except RuntimeError: | |
| except RuntimeError: | |
| # 当没有运行中的事件循环(例如应用关闭阶段)时,get_running_loop 会抛出 RuntimeError, | |
| # 这种情况下我们静默忽略并不广播日志是可接受的行为。 |
This pull request introduces a major enhancement to AI-DataFlux: the addition of a Web GUI control panel and a new dual build system supporting both "Full" (with GUI) and "CLI-only" (without GUI) versions. The CLI is now adaptive, exposing the
guisubcommand only when the GUI backend is available. The workflows for both PyInstaller and Nuitka have been updated to build and package both variants across platforms, and the documentation has been expanded to explain the differences and usage of each version.The most important changes are:
1. Web GUI Control Panel Integration
guisubcommand tocli.pythat starts a local Web GUI control panel for managing services, editing configs, and viewing logs. The command is registered only if thesrc.control.servermodule is present, ensuring CLI-only builds remain clean. (cli.py, README.md, [1] F2d50726L508R508, [2] [3]2. Dual Build System: Full vs CLI-only
build-pyinstaller.ymlandbuild-nuitka.yml) to produce both Full (with GUI and frontend assets) and CLI-only (without GUI/frontend) binaries for all supported platforms. The CLI-only variant excludes the GUI modules and static assets, resulting in a smaller footprint. (.github/workflows/build-pyinstaller.yml,.github/workflows/build-nuitka.yml, [1] [2] [3] [4] [5]3. Documentation and User Guidance
docs/BUILD_VARIANTS.md, and expanded theREADME.mdwith clear explanations, usage instructions, and feature comparisons between Full and CLI-only versions, including how the CLI adapts to the available features. (docs/BUILD_VARIANTS.md, README.md, README.mdR105-R137)4. CLI Improvements and Validation
processsubcommand optionally accept a progress file for GUI integration. (cli.py, [1] [2] [3] [4] [5]5. Frontend Build and Packaging
.github/workflows/build-pyinstaller.yml,.github/workflows/build-nuitka.yml, [1] [2]These changes provide a more flexible distribution model, improve usability for both end-users and developers, and lay the groundwork for further modularization and optional feature support.