From 69a22b4bd30311970135c5dca4d16822f1447b72 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 10:34:11 +0000 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20=E5=85=8B=E9=9A=86=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E9=85=8D=E7=BD=AE=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- .../plans/2026-06-12-ai-dev-assistant.md | 2263 +++++++++++++++++ 1 file changed, 2263 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-12-ai-dev-assistant.md diff --git a/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md b/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md new file mode 100644 index 0000000..09a18c8 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md @@ -0,0 +1,2263 @@ +# AI 开发助手智能体 - 实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 构建一个高度自定义的 AI 开发助手智能体,支持开发资料自动整理、Git 仓库自主管理、桌面端数字人交互,具备插件扩展能力。 + +**Architecture:** 采用 Python 后端 + Electron 桌面端的分层架构。后端使用 FastAPI 提供 API 接口,通过 langchain-core 统一调度大模型(支持云端 API 和本地 Ollama),使用 SQLite 存储元数据,PyGit2 管理 Git 仓库,pluggy 实现插件系统。桌面端使用 Electron 封装,集成 Ready Player Me 数字人展示。 + +**Tech Stack:** Python 3.11+, FastAPI, langchain-core, SQLite, PyGit2, pluggy, Electron, React, Ant Design, Ready Player Me + +--- + +## 文件结构总览 + +``` +ai-dev-assistant/ +├── backend/ # Python 后端 +│ ├── app/ +│ │ ├── __init__.py +│ │ ├── main.py # FastAPI 入口 +│ │ ├── config.py # 配置管理 +│ │ ├── database.py # SQLite 数据库 +│ │ ├── models/ # 数据模型 +│ │ │ ├── __init__.py +│ │ │ ├── file.py # 文件元数据模型 +│ │ │ ├── project.py # 项目配置模型 +│ │ │ └── operation.py # 操作记录模型 +│ │ ├── api/ # API 路由 +│ │ │ ├── __init__.py +│ │ │ ├── files.py # 文件管理接口 +│ │ │ ├── git.py # Git 操作接口 +│ │ │ └── plugins.py # 插件管理接口 +│ │ ├── core/ # 核心业务逻辑 +│ │ │ ├── __init__.py +│ │ │ ├── llm.py # 大模型统一调度 +│ │ │ ├── git_manager.py # Git 仓库管理 +│ │ │ ├── file_organizer.py # 文件自动归档 +│ │ │ └── context.py # 上下文管理 +│ │ ├── plugins/ # 插件系统 +│ │ │ ├── __init__.py +│ │ │ ├── manager.py # 插件管理器 +│ │ │ ├── hookspecs.py # 钩子定义 +│ │ │ └── builtin/ # 内置插件 +│ │ │ ├── __init__.py +│ │ │ └── example_plugin.py +│ │ └── utils/ # 工具函数 +│ │ ├── __init__.py +│ │ ├── validator.py # 参数校验 +│ │ └── logger.py # 日志工具 +│ ├── tests/ # 测试文件 +│ │ ├── __init__.py +│ │ ├── test_llm.py +│ │ ├── test_git.py +│ │ └── test_file_organizer.py +│ ├── requirements.txt # Python 依赖 +│ └── pyproject.toml # 项目配置 +├── desktop/ # Electron 桌面端 +│ ├── src/ +│ │ ├── main/ # 主进程 +│ │ │ ├── index.ts # 入口文件 +│ │ │ └── ipc.ts # IPC 通信 +│ │ ├── renderer/ # 渲染进程 +│ │ │ ├── index.html +│ │ │ ├── App.tsx +│ │ │ ├── components/ +│ │ │ │ ├── Avatar/ # 数字人组件 +│ │ │ │ └── Panel/ # 管理面板 +│ │ │ └── pages/ +│ │ │ ├── Home.tsx +│ │ │ ├── Settings.tsx +│ │ │ └── Plugins.tsx +│ │ └── preload/ +│ │ └── index.ts +│ ├── package.json +│ └── tsconfig.json +└── docs/ + └── api.md # API 文档 +``` + +--- + +## 阶段一:项目基础搭建(第 1-2 周) + +### Task 1: 创建 Python 项目结构 + +**Files:** +- Create: `backend/pyproject.toml` +- Create: `backend/requirements.txt` +- Create: `backend/app/__init__.py` +- Create: `backend/app/main.py` +- Create: `backend/app/config.py` + +- [ ] **Step 1: 创建项目目录** + +```bash +mkdir -p backend/app/{api,core,models,plugins/builtin,utils} +mkdir -p backend/tests +cd backend +``` + +- [ ] **Step 2: 创建 pyproject.toml** + +```toml +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ai-dev-assistant" +version = "0.1.0" +description = "AI-powered development assistant with plugin system" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.109.0", + "uvicorn[standard]>=0.27.0", + "langchain-core>=0.1.0", + "langchain-openai>=0.0.5", + "langchain-community>=0.0.10", + "pygit2>=1.13.0", + "pluggy>=1.3.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "aiosqlite>=0.19.0", + "python-dotenv>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "pytest-asyncio>=0.23.0", + "black>=23.12.0", + "ruff>=0.1.0", + "mypy>=1.8.0", +] + +[tool.black] +line-length = 88 + +[tool.ruff] +line-length = 88 +select = ["E", "F", "I"] + +[tool.mypy] +python_version = "3.11" +strict = true +``` + +- [ ] **Step 3: 创建 requirements.txt(备用)** + +```txt +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +langchain-core>=0.1.0 +langchain-openai>=0.0.5 +langchain-community>=0.0.10 +pygit2>=1.13.0 +pluggy>=1.3.0 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +aiosqlite>=0.19.0 +python-dotenv>=1.0.0 +``` + +- [ ] **Step 4: 创建配置管理模块** + +```python +# backend/app/config.py +from pathlib import Path +from typing import Optional +from pydantic_settings import BaseSettings +from pydantic import Field + +class Settings(BaseSettings): + """应用配置""" + + # 应用基础配置 + app_name: str = "AI Dev Assistant" + app_version: str = "0.1.0" + debug: bool = False + + # API 服务配置 + api_host: str = "127.0.0.1" + api_port: int = 8000 + + # 数据库配置 + database_url: str = "sqlite+aiosqlite:///./data/assistant.db" + + # 大模型配置 + llm_provider: str = Field(default="openai", description="openai | ollama") + openai_api_key: Optional[str] = None + openai_base_url: Optional[str] = None + openai_model: str = "gpt-4-turbo-preview" + + ollama_base_url: str = "http://localhost:11434" + ollama_model: str = "llama2" + + # Git 配置 + git_workspace: Path = Field(default=Path("./workspace")) + + # 插件配置 + plugins_dir: Path = Field(default=Path("./plugins")) + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + +settings = Settings() +``` + +- [ ] **Step 5: 创建 FastAPI 主入口** + +```python +# backend/app/main.py +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + debug=settings.debug, +) + +# 配置 CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/") +async def root(): + return { + "name": settings.app_name, + "version": settings.app_version, + "status": "running" + } + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} +``` + +- [ ] **Step 6: 创建 .env 示例文件** + +```bash +# backend/.env.example +# 应用配置 +DEBUG=false +API_HOST=127.0.0.1 +API_PORT=8000 + +# 大模型配置 +LLM_PROVIDER=openai +OPENAI_API_KEY=your-api-key-here +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-4-turbo-preview + +# Ollama 配置 +OLLAMA_BASE_URL=http://localhost:11434 +OLLAMA_MODEL=llama2 + +# 路径配置 +GIT_WORKSPACE=./workspace +PLUGINS_DIR=./plugins +``` + +- [ ] **Step 7: 验证项目结构** + +```bash +cd backend +python -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -e ".[dev]" +python -c "from app.config import settings; print(settings.app_name)" +``` + +Expected: `AI Dev Assistant` + +- [ ] **Step 8: 提交代码** + +```bash +git init +git add . +git commit -m "feat: initialize Python project structure with FastAPI" +``` + +--- + +### Task 2: 实现数据库模型和初始化 + +**Files:** +- Create: `backend/app/database.py` +- Create: `backend/app/models/__init__.py` +- Create: `backend/app/models/file.py` +- Create: `backend/app/models/project.py` +- Create: `backend/app/models/operation.py` + +- [ ] **Step 1: 创建数据库连接模块** + +```python +# backend/app/database.py +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase +from app.config import settings + +engine = create_async_engine( + settings.database_url, + echo=settings.debug, +) + +async_session = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False +) + +class Base(DeclarativeBase): + pass + +async def init_db(): + """初始化数据库,创建所有表""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + +async def get_db(): + """获取数据库会话""" + async with async_session() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise +``` + +- [ ] **Step 2: 创建文件元数据模型** + +```python +# backend/app/models/file.py +from datetime import datetime +from typing import Optional +from sqlalchemy import String, Text, DateTime, Integer, Boolean +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class FileMetadata(Base): + """文件元数据""" + __tablename__ = "file_metadata" + + id: Mapped[int] = mapped_column(primary_key=True) + file_path: Mapped[str] = mapped_column(String(500), unique=True, index=True) + file_name: Mapped[str] = mapped_column(String(255), index=True) + file_type: Mapped[str] = mapped_column(String(50)) # 文件扩展名 + category: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # 分类 + tags: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON 格式标签 + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + project_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + size_bytes: Mapped[int] = mapped_column(Integer, default=0) + is_archived: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self) -> str: + return f"" +``` + +- [ ] **Step 3: 创建项目配置模型** + +```python +# backend/app/models/project.py +from datetime import datetime +from typing import Optional +from sqlalchemy import String, Text, DateTime, Integer, JSON +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class Project(Base): + """项目配置""" + __tablename__ = "projects" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(255), unique=True, index=True) + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + git_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) + local_path: Mapped[str] = mapped_column(String(500)) + config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # 项目特定配置 + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self) -> str: + return f"" +``` + +- [ ] **Step 4: 创建操作记录模型** + +```python +# backend/app/models/operation.py +from datetime import datetime +from typing import Optional +from sqlalchemy import String, Text, DateTime, Integer, JSON +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class OperationLog(Base): + """操作记录""" + __tablename__ = "operation_logs" + + id: Mapped[int] = mapped_column(primary_key=True) + operation_type: Mapped[str] = mapped_column(String(50), index=True) # git_clone, file_move, etc. + target_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) + status: Mapped[str] = mapped_column(String(20)) # success, failed, pending + details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + def __repr__(self) -> str: + return f"" +``` + +- [ ] **Step 5: 更新 models/__init__.py** + +```python +# backend/app/models/__init__.py +from app.models.file import FileMetadata +from app.models.project import Project +from app.models.operation import OperationLog + +__all__ = ["FileMetadata", "Project", "OperationLog"] +``` + +- [ ] **Step 6: 更新 main.py 添加数据库初始化** + +```python +# backend/app/main.py (添加这部分) +from contextlib import asynccontextmanager +from app.database import init_db + +@asynccontextmanager +async def lifespan(app: FastAPI): + # 启动时初始化数据库 + await init_db() + yield + # 关闭时清理资源 + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + debug=settings.debug, + lifespan=lifespan, +) +``` + +- [ ] **Step 7: 测试数据库初始化** + +```bash +cd backend +python -c " +import asyncio +from app.database import init_db +asyncio.run(init_db()) +print('Database initialized successfully') +" +``` + +Expected: `Database initialized successfully` + +- [ ] **Step 8: 提交代码** + +```bash +git add . +git commit -m "feat: add database models for files, projects, and operations" +``` + +--- + +### Task 3: 实现参数校验和日志工具 + +**Files:** +- Create: `backend/app/utils/validator.py` +- Create: `backend/app/utils/logger.py` + +- [ ] **Step 1: 创建参数校验工具** + +```python +# backend/app/utils/validator.py +from pathlib import Path +from typing import Optional +import re + +class ValidationError(Exception): + """参数校验异常""" + pass + +def validate_file_path(path: str) -> Path: + """校验文件路径合法性""" + if not path or not path.strip(): + raise ValidationError("文件路径不能为空") + + # 检查危险字符 + dangerous_chars = ['..', '~', '$', '`', '|', '&', ';'] + for char in dangerous_chars: + if char in path: + raise ValidationError(f"文件路径包含非法字符: {char}") + + try: + return Path(path).resolve() + except Exception as e: + raise ValidationError(f"无效的文件路径: {e}") + +def validate_git_url(url: str) -> str: + """校验 Git 仓库 URL""" + if not url or not url.strip(): + raise ValidationError("Git URL 不能为空") + + # 支持 HTTPS 和 SSH 格式 + https_pattern = r'^https?://[\w\-\.]+(:\d+)?/[\w\-\.]+/[\w\-\.]+(\.git)?$' + ssh_pattern = r'^git@[\w\-\.]+:[\w\-\.]+/[\w\-\.]+(\.git)?$' + + if not (re.match(https_pattern, url) or re.match(ssh_pattern, url)): + raise ValidationError(f"无效的 Git URL 格式: {url}") + + return url.strip() + +def validate_model_config(provider: str, model: str) -> None: + """校验大模型配置""" + valid_providers = ['openai', 'ollama'] + if provider not in valid_providers: + raise ValidationError(f"不支持的模型提供商: {provider},支持: {valid_providers}") + + if not model or not model.strip(): + raise ValidationError("模型名称不能为空") + +def validate_category(category: Optional[str]) -> Optional[str]: + """校验分类名称""" + if category is None: + return None + + category = category.strip() + if len(category) > 100: + raise ValidationError("分类名称不能超过 100 个字符") + + return category +``` + +- [ ] **Step 2: 创建日志工具** + +```python +# backend/app/utils/logger.py +import logging +import sys +from pathlib import Path +from logging.handlers import RotatingFileHandler +from app.config import settings + +def setup_logger(name: str, log_file: Optional[str] = None) -> logging.Logger: + """配置日志记录器""" + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG if settings.debug else logging.INFO) + + # 控制台输出 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.DEBUG) + console_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) + + # 文件输出(可选) + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = RotatingFileHandler( + log_path, + maxBytes=10*1024*1024, # 10MB + backupCount=5, + encoding='utf-8' + ) + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s' + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + return logger + +# 默认日志记录器 +logger = setup_logger("ai_dev_assistant", "logs/app.log") +``` + +- [ ] **Step 3: 测试校验工具** + +```bash +cd backend +python -c " +from app.utils.validator import validate_file_path, validate_git_url, ValidationError + +# 测试正常路径 +path = validate_file_path('/tmp/test.txt') +print(f'Valid path: {path}') + +# 测试危险路径 +try: + validate_file_path('/tmp/../etc/passwd') +except ValidationError as e: + print(f'Caught expected error: {e}') + +# 测试 Git URL +url = validate_git_url('https://github.com/user/repo.git') +print(f'Valid Git URL: {url}') +" +``` + +Expected: +``` +Valid path: /tmp/test.txt +Caught expected error: 文件路径包含非法字符: .. +Valid Git URL: https://github.com/user/repo.git +``` + +- [ ] **Step 4: 提交代码** + +```bash +git add . +git commit -m "feat: add parameter validation and logging utilities" +``` + +--- + +## 阶段二:核心功能开发(第 3-6 周) + +### Task 4: 实现大模型统一调度模块 + +**Files:** +- Create: `backend/app/core/llm.py` +- Create: `backend/app/core/context.py` +- Create: `backend/tests/test_llm.py` + +- [ ] **Step 1: 创建上下文管理模块** + +```python +# backend/app/core/context.py +from typing import List, Dict, Any +from dataclasses import dataclass, field +from datetime import datetime + +@dataclass +class Message: + """消息""" + role: str # user, assistant, system + content: str + timestamp: datetime = field(default_factory=datetime.utcnow) + metadata: Dict[str, Any] = field(default_factory=dict) + +@dataclass +class ConversationContext: + """对话上下文""" + session_id: str + messages: List[Message] = field(default_factory=list) + max_history: int = 20 + + def add_message(self, role: str, content: str, **metadata): + """添加消息""" + msg = Message(role=role, content=content, metadata=metadata) + self.messages.append(msg) + + # 保持历史长度 + if len(self.messages) > self.max_history: + self.messages = self.messages[-self.max_history:] + + def get_messages_for_llm(self) -> List[Dict[str, str]]: + """获取发送给 LLM 的消息格式""" + return [ + {"role": msg.role, "content": msg.content} + for msg in self.messages + ] + + def clear(self): + """清空上下文""" + self.messages.clear() +``` + +- [ ] **Step 2: 创建大模型统一调度模块** + +```python +# backend/app/core/llm.py +from typing import Optional, AsyncIterator, Dict, Any +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage +from langchain_core.callbacks import AsyncCallbackCallbackHandler +from app.config import settings +from app.utils.logger import logger +from app.utils.validator import validate_model_config + +class LLMManager: + """大模型统一管理器""" + + def __init__(self): + self._llm: Optional[BaseChatModel] = None + self._provider: Optional[str] = None + self._model: Optional[str] = None + + def _create_openai_llm(self) -> BaseChatModel: + """创建 OpenAI 模型""" + from langchain_openai import ChatOpenAI + + if not settings.openai_api_key: + raise ValueError("OpenAI API Key 未配置") + + return ChatOpenAI( + model=settings.openai_model, + api_key=settings.openai_api_key, + base_url=settings.openai_base_url, + temperature=0.7, + max_tokens=2000, + ) + + def _create_ollama_llm(self) -> BaseChatModel: + """创建 Ollama 模型""" + from langchain_community.chat_models import ChatOllama + + return ChatOllama( + model=settings.ollama_model, + base_url=settings.ollama_base_url, + temperature=0.7, + num_predict=2000, + ) + + def get_llm(self) -> BaseChatModel: + """获取大模型实例""" + validate_model_config(settings.llm_provider, + settings.openai_model if settings.llm_provider == "openai" + else settings.ollama_model) + + # 如果配置没变,返回缓存的实例 + if (self._llm and + self._provider == settings.llm_provider and + self._model == (settings.openai_model if settings.llm_provider == "openai" + else settings.ollama_model)): + return self._llm + + # 创建新实例 + if settings.llm_provider == "openai": + self._llm = self._create_openai_llm() + elif settings.llm_provider == "ollama": + self._llm = self._create_ollama_llm() + else: + raise ValueError(f"不支持的模型提供商: {settings.llm_provider}") + + self._provider = settings.llm_provider + self._model = settings.openai_model if settings.llm_provider == "openai" else settings.ollama_model + + logger.info(f"已切换到大模型: {self._provider}/{self._model}") + return self._llm + + async def chat(self, messages: list, **kwargs) -> str: + """同步对话""" + llm = self.get_llm() + + try: + response = await llm.ainvoke(messages, **kwargs) + return response.content + except Exception as e: + logger.error(f"大模型调用失败: {e}") + raise + + async def stream_chat(self, messages: list, **kwargs) -> AsyncIterator[str]: + """流式对话""" + llm = self.get_llm() + + try: + async for chunk in llm.astream(messages, **kwargs): + yield chunk.content + except Exception as e: + logger.error(f"大模型流式调用失败: {e}") + raise + +# 全局实例 +llm_manager = LLMManager() +``` + +- [ ] **Step 3: 编写测试** + +```python +# backend/tests/test_llm.py +import pytest +from unittest.mock import Mock, patch, AsyncMock +from app.core.llm import LLMManager +from app.core.context import ConversationContext + +def test_context_add_message(): + """测试上下文添加消息""" + ctx = ConversationContext(session_id="test") + ctx.add_message("user", "hello") + ctx.add_message("assistant", "hi") + + assert len(ctx.messages) == 2 + assert ctx.messages[0].role == "user" + assert ctx.messages[1].content == "hi" + +def test_context_max_history(): + """测试上下文历史长度限制""" + ctx = ConversationContext(session_id="test", max_history=3) + + for i in range(5): + ctx.add_message("user", f"msg {i}") + + assert len(ctx.messages) == 3 + assert ctx.messages[0].content == "msg 2" + +@pytest.mark.asyncio +async def test_llm_manager_openai(): + """测试 OpenAI 模型创建""" + with patch('app.core.llm.settings') as mock_settings: + mock_settings.llm_provider = "openai" + mock_settings.openai_api_key = "test-key" + mock_settings.openai_model = "gpt-4" + mock_settings.openai_base_url = None + + manager = LLMManager() + # 这里会实际创建模型,但不调用 API + # 实际测试应该 mock 掉 ChatOpenAI + +@pytest.mark.asyncio +async def test_llm_chat(): + """测试对话功能""" + manager = LLMManager() + + # Mock 掉实际的 LLM 调用 + with patch.object(manager, 'get_llm') as mock_get_llm: + mock_llm = AsyncMock() + mock_llm.ainvoke.return_value = Mock(content="test response") + mock_get_llm.return_value = mock_llm + + result = await manager.chat([{"role": "user", "content": "hello"}]) + assert result == "test response" +``` + +- [ ] **Step 4: 运行测试** + +```bash +cd backend +pytest tests/test_llm.py -v +``` + +Expected: 所有测试通过 + +- [ ] **Step 5: 提交代码** + +```bash +git add . +git commit -m "feat: implement unified LLM manager with OpenAI and Ollama support" +``` + +--- + +### Task 5: 实现 Git 仓库管理模块 + +**Files:** +- Create: `backend/app/core/git_manager.py` +- Create: `backend/tests/test_git.py` + +- [ ] **Step 1: 创建 Git 管理器** + +```python +# backend/app/core/git_manager.py +from pathlib import Path +from typing import Optional, List, Dict, Any +from dataclasses import dataclass +from datetime import datetime +import pygit2 +from app.utils.logger import logger +from app.utils.validator import validate_git_url, validate_file_path + +@dataclass +class GitCommit: + """Git 提交记录""" + hash: str + message: str + author: str + timestamp: datetime + +@dataclass +class GitStatus: + """Git 状态""" + branch: str + is_clean: bool + modified_files: List[str] + untracked_files: List[str] + +class GitManager: + """Git 仓库管理器""" + + def __init__(self, workspace: Path): + self.workspace = workspace.resolve() + self.workspace.mkdir(parents=True, exist_ok=True) + + def clone_repository(self, url: str, name: Optional[str] = None) -> Path: + """克隆 Git 仓库""" + validated_url = validate_git_url(url) + + # 生成仓库名称 + if not name: + name = validated_url.split('/')[-1].replace('.git', '') + + repo_path = self.workspace / name + + if repo_path.exists(): + raise ValueError(f"仓库目录已存在: {repo_path}") + + logger.info(f"开始克隆仓库: {validated_url} -> {repo_path}") + + try: + # 使用回调函数显示进度 + class CloneProgress(pygit2.RemoteCallbacks): + def transfer_progress(self, stats): + if stats.received_objects > 0: + pct = stats.received_objects / stats.total_objects * 100 + logger.debug(f"克隆进度: {pct:.1f}%") + + repo = pygit2.clone_repository( + validated_url, + str(repo_path), + callbacks=CloneProgress() + ) + + logger.info(f"仓库克隆成功: {repo_path}") + return repo_path + + except Exception as e: + logger.error(f"克隆仓库失败: {e}") + # 清理失败的克隆 + if repo_path.exists(): + import shutil + shutil.rmtree(repo_path) + raise + + def open_repository(self, path: Path) -> pygit2.Repository: + """打开本地仓库""" + repo_path = validate_file_path(str(path)) + + if not repo_path.exists(): + raise ValueError(f"仓库路径不存在: {repo_path}") + + try: + return pygit2.Repository(str(repo_path)) + except Exception as e: + raise ValueError(f"无法打开 Git 仓库: {e}") + + def get_status(self, repo_path: Path) -> GitStatus: + """获取仓库状态""" + repo = self.open_repository(repo_path) + + # 获取当前分支 + if repo.head_is_unborn: + branch = "HEAD (unborn)" + else: + branch = repo.head.shorthand + + # 获取状态 + status = repo.status() + modified = [] + untracked = [] + + for filepath, flags in status.items(): + if flags & pygit2.GIT_STATUS_WT_MODIFIED: + modified.append(filepath) + if flags & pygit2.GIT_STATUS_WT_NEW: + untracked.append(filepath) + + return GitStatus( + branch=branch, + is_clean=len(status) == 0, + modified_files=modified, + untracked_files=untracked + ) + + def get_recent_commits(self, repo_path: Path, limit: int = 10) -> List[GitCommit]: + """获取最近的提交记录""" + repo = self.open_repository(repo_path) + + if repo.head_is_unborn: + return [] + + commits = [] + for commit in repo.walk(repo.head.target, pygit2.GIT_SORT_TIME): + commits.append(GitCommit( + hash=commit.hex, + message=commit.message.strip(), + author=commit.author.name, + timestamp=datetime.fromtimestamp(commit.commit_time) + )) + + if len(commits) >= limit: + break + + return commits + + def pull_repository(self, repo_path: Path) -> Dict[str, Any]: + """拉取远程更新""" + repo = self.open_repository(repo_path) + + if repo.head_is_unborn: + raise ValueError("仓库没有初始提交") + + # 获取当前分支的远程跟踪 + branch = repo.head.shorthand + remote_name = repo.branches[branch].upstream.remote_name if repo.branches[branch].upstream else "origin" + + remote = repo.remotes[remote_name] + remote.fetch() + + # 合并远程分支 + remote_ref = f"refs/remotes/{remote_name}/{branch}" + remote_commit = repo.references[remote_ref].target + + # 执行快进合并 + repo.merge(remote_commit) + + if repo.state == pygit2.GIT_REPO_STATE_MERGE: + # 如果有冲突,需要手动解决 + raise ValueError("合并冲突,需要手动解决") + + return { + "status": "success", + "branch": branch, + "remote": remote_name + } + + def commit_changes(self, repo_path: Path, message: str, files: Optional[List[str]] = None) -> str: + """提交更改""" + repo = self.open_repository(repo_path) + + # 添加文件到索引 + if files: + for file in files: + repo.index.add(file) + else: + # 添加所有更改 + repo.index.add_all() + + repo.index.write() + + # 创建提交 + tree = repo.index.write_tree() + + if repo.head_is_unborn: + # 首次提交 + sig = repo.default_signature + commit = repo.create_commit( + "HEAD", + sig, + sig, + message, + tree, + [] + ) + else: + # 正常提交 + parent = repo.head.peel(pygit2.Commit) + sig = repo.default_signature + commit = repo.create_commit( + "HEAD", + sig, + sig, + message, + tree, + [parent.id] + ) + + logger.info(f"提交成功: {commit}") + return str(commit) + +# 全局实例(使用配置的工作空间) +from app.config import settings +git_manager = GitManager(settings.git_workspace) +``` + +- [ ] **Step 2: 编写测试** + +```python +# backend/tests/test_git.py +import pytest +import tempfile +from pathlib import Path +from app.core.git_manager import GitManager + +@pytest.fixture +def temp_workspace(): + """创建临时工作空间""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + +def test_clone_repository(temp_workspace): + """测试克隆仓库""" + manager = GitManager(temp_workspace) + + # 使用一个小型公开仓库测试 + repo_url = "https://github.com/octocat/Hello-World.git" + repo_path = manager.clone_repository(repo_url) + + assert repo_path.exists() + assert (repo_path / ".git").exists() + +def test_get_status(temp_workspace): + """测试获取仓库状态""" + manager = GitManager(temp_workspace) + + # 先克隆一个仓库 + repo_url = "https://github.com/octocat/Hello-World.git" + repo_path = manager.clone_repository(repo_url) + + status = manager.get_status(repo_path) + assert status.branch == "master" or status.branch == "main" + assert isinstance(status.is_clean, bool) + +def test_get_recent_commits(temp_workspace): + """测试获取提交记录""" + manager = GitManager(temp_workspace) + + repo_url = "https://github.com/octocat/Hello-World.git" + repo_path = manager.clone_repository(repo_url) + + commits = manager.get_recent_commits(repo_path, limit=5) + assert len(commits) > 0 + assert commits[0].hash + assert commits[0].message +``` + +- [ ] **Step 3: 运行测试** + +```bash +cd backend +pytest tests/test_git.py -v +``` + +Expected: 所有测试通过(需要网络连接) + +- [ ] **Step 4: 提交代码** + +```bash +git add . +git commit -m "feat: implement Git repository manager with clone, pull, commit" +``` + +--- + +### Task 6: 实现文件自动归档模块 + +**Files:** +- Create: `backend/app/core/file_organizer.py` +- Create: `backend/tests/test_file_organizer.py` + +- [ ] **Step 1: 创建文件归档器** + +```python +# backend/app/core/file_organizer.py +from pathlib import Path +from typing import Dict, List, Optional +from datetime import datetime +import shutil +import mimetypes +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.file import FileMetadata +from app.utils.logger import logger +from app.utils.validator import validate_file_path, validate_category + +class FileOrganizer: + """文件自动归档管理器""" + + # 默认分类规则 + DEFAULT_CATEGORIES = { + "code": [".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".cpp", ".c", ".go", ".rs"], + "document": [".md", ".txt", ".doc", ".docx", ".pdf", ".rst"], + "data": [".json", ".xml", ".yaml", ".yml", ".csv", ".sql"], + "image": [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"], + "config": [".env", ".toml", ".ini", ".cfg", ".conf"], + "archive": [".zip", ".tar", ".gz", ".rar", ".7z"], + } + + def __init__(self, db: AsyncSession): + self.db = db + + def classify_file(self, file_path: Path) -> str: + """根据文件扩展名自动分类""" + ext = file_path.suffix.lower() + + for category, extensions in self.DEFAULT_CATEGORIES.items(): + if ext in extensions: + return category + + return "other" + + async def scan_directory(self, directory: Path, recursive: bool = True) -> List[FileMetadata]: + """扫描目录,创建文件元数据""" + dir_path = validate_file_path(str(directory)) + + if not dir_path.exists(): + raise ValueError(f"目录不存在: {dir_path}") + + files = [] + pattern = "**/*" if recursive else "*" + + for file_path in dir_path.glob(pattern): + if file_path.is_file(): + metadata = await self.create_file_metadata(file_path) + files.append(metadata) + + logger.info(f"扫描完成,找到 {len(files)} 个文件") + return files + + async def create_file_metadata(self, file_path: Path) -> FileMetadata: + """创建文件元数据""" + path = validate_file_path(str(file_path)) + + if not path.exists(): + raise ValueError(f"文件不存在: {path}") + + # 自动分类 + category = self.classify_file(path) + + # 获取文件信息 + stat = path.stat() + + metadata = FileMetadata( + file_path=str(path), + file_name=path.name, + file_type=path.suffix.lower(), + category=category, + size_bytes=stat.st_size, + created_at=datetime.fromtimestamp(stat.st_ctime), + updated_at=datetime.fromtimestamp(stat.st_mtime), + ) + + return metadata + + async def save_metadata(self, metadata: FileMetadata) -> FileMetadata: + """保存文件元数据到数据库""" + # 检查是否已存在 + stmt = select(FileMetadata).where(FileMetadata.file_path == metadata.file_path) + result = await self.db.execute(stmt) + existing = result.scalar_one_or_none() + + if existing: + # 更新现有记录 + for key, value in metadata.__dict__.items(): + if key != '_sa_instance_state' and key != 'id': + setattr(existing, key, value) + await self.db.flush() + return existing + else: + # 创建新记录 + self.db.add(metadata) + await self.db.flush() + return metadata + + async def organize_file(self, source: Path, target_dir: Path, category: Optional[str] = None) -> FileMetadata: + """归档文件到目标目录""" + source_path = validate_file_path(str(source)) + target_base = validate_file_path(str(target_dir)) + + if not source_path.exists(): + raise ValueError(f"源文件不存在: {source_path}") + + # 确定分类 + if not category: + category = self.classify_file(source_path) + + category = validate_category(category) + + # 创建分类目录 + category_dir = target_base / category + category_dir.mkdir(parents=True, exist_ok=True) + + # 移动文件 + target_path = category_dir / source_path.name + + # 处理文件名冲突 + counter = 1 + original_target = target_path + while target_path.exists(): + stem = original_target.stem + suffix = original_target.suffix + target_path = category_dir / f"{stem}_{counter}{suffix}" + counter += 1 + + shutil.move(str(source_path), str(target_path)) + logger.info(f"文件已归档: {source_path} -> {target_path}") + + # 创建并保存元数据 + metadata = await self.create_file_metadata(target_path) + metadata.category = category + return await self.save_metadata(metadata) + + async def search_files( + self, + keyword: Optional[str] = None, + category: Optional[str] = None, + file_type: Optional[str] = None, + limit: int = 100 + ) -> List[FileMetadata]: + """搜索文件""" + stmt = select(FileMetadata) + + if keyword: + stmt = stmt.where(FileMetadata.file_name.contains(keyword)) + + if category: + stmt = stmt.where(FileMetadata.category == category) + + if file_type: + stmt = stmt.where(FileMetadata.file_type == file_type) + + stmt = stmt.limit(limit) + + result = await self.db.execute(stmt) + return result.scalars().all() +``` + +- [ ] **Step 2: 编写测试** + +```python +# backend/tests/test_file_organizer.py +import pytest +import tempfile +from pathlib import Path +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from app.database import Base +from app.core.file_organizer import FileOrganizer + +@pytest.fixture +async def db_session(): + """创建测试数据库""" + engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=True) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + async_session = async_sessionmaker(engine, class_=AsyncSession) + async with async_session() as session: + yield session + + await engine.dispose() + +@pytest.fixture +def temp_dir(): + """创建临时目录""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + +def test_classify_file(): + """测试文件分类""" + organizer = FileOrganizer(None) + + assert organizer.classify_file(Path("test.py")) == "code" + assert organizer.classify_file(Path("readme.md")) == "document" + assert organizer.classify_file(Path("data.json")) == "data" + assert organizer.classify_file(Path("photo.jpg")) == "image" + assert organizer.classify_file(Path("unknown.xyz")) == "other" + +@pytest.mark.asyncio +async def test_create_metadata(temp_dir, db_session): + """测试创建文件元数据""" + # 创建测试文件 + test_file = temp_dir / "test.py" + test_file.write_text("print('hello')") + + organizer = FileOrganizer(db_session) + metadata = await organizer.create_file_metadata(test_file) + + assert metadata.file_name == "test.py" + assert metadata.file_type == ".py" + assert metadata.category == "code" + assert metadata.size_bytes > 0 + +@pytest.mark.asyncio +async def test_organize_file(temp_dir, db_session): + """测试文件归档""" + # 创建测试文件 + source_file = temp_dir / "test.py" + source_file.write_text("print('hello')") + + target_dir = temp_dir / "organized" + + organizer = FileOrganizer(db_session) + metadata = await organizer.organize_file(source_file, target_dir) + + assert metadata.category == "code" + assert (target_dir / "code" / "test.py").exists() + assert not source_file.exists() + +@pytest.mark.asyncio +async def test_search_files(temp_dir, db_session): + """测试文件搜索""" + # 创建多个测试文件 + (temp_dir / "test1.py").write_text("code") + (temp_dir / "test2.md").write_text("doc") + + organizer = FileOrganizer(db_session) + + # 扫描并保存 + files = await organizer.scan_directory(temp_dir, recursive=False) + for f in files: + await organizer.save_metadata(f) + + # 搜索 + results = await organizer.search_files(keyword="test1") + assert len(results) == 1 + assert results[0].file_name == "test1.py" +``` + +- [ ] **Step 3: 运行测试** + +```bash +cd backend +pytest tests/test_file_organizer.py -v +``` + +Expected: 所有测试通过 + +- [ ] **Step 4: 提交代码** + +```bash +git add . +git commit -m "feat: implement file organizer with auto-classification" +``` + +--- + +## 阶段三:API 接口开发(第 7-8 周) + +### Task 7: 实现文件管理 API + +**Files:** +- Create: `backend/app/api/files.py` + +- [ ] **Step 1: 创建文件管理 API 路由** + +```python +# backend/app/api/files.py +from typing import List, Optional +from pathlib import Path +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from pydantic import BaseModel + +from app.database import get_db +from app.core.file_organizer import FileOrganizer +from app.models.file import FileMetadata +from app.utils.logger import logger + +router = APIRouter(prefix="/files", tags=["files"]) + +class FileResponse(BaseModel): + id: int + file_path: str + file_name: str + file_type: str + category: Optional[str] + tags: Optional[str] + description: Optional[str] + size_bytes: int + created_at: str + updated_at: str + + class Config: + from_attributes = True + +class ScanRequest(BaseModel): + directory: str + recursive: bool = True + +class OrganizeRequest(BaseModel): + source: str + target_dir: str + category: Optional[str] = None + +@router.get("/", response_model=List[FileResponse]) +async def list_files( + keyword: Optional[str] = Query(None), + category: Optional[str] = Query(None), + file_type: Optional[str] = Query(None), + limit: int = Query(100, ge=1, le=1000), + db: AsyncSession = Depends(get_db) +): + """获取文件列表""" + organizer = FileOrganizer(db) + files = await organizer.search_files( + keyword=keyword, + category=category, + file_type=file_type, + limit=limit + ) + return files + +@router.post("/scan", response_model=List[FileResponse]) +async def scan_directory( + request: ScanRequest, + db: AsyncSession = Depends(get_db) +): + """扫描目录并导入文件元数据""" + organizer = FileOrganizer(db) + + try: + files = await organizer.scan_directory( + Path(request.directory), + recursive=request.recursive + ) + + # 保存到数据库 + saved_files = [] + for metadata in files: + saved = await organizer.save_metadata(metadata) + saved_files.append(saved) + + return saved_files + except Exception as e: + logger.error(f"扫描目录失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/organize", response_model=FileResponse) +async def organize_file( + request: OrganizeRequest, + db: AsyncSession = Depends(get_db) +): + """归档文件""" + organizer = FileOrganizer(db) + + try: + metadata = await organizer.organize_file( + Path(request.source), + Path(request.target_dir), + request.category + ) + return metadata + except Exception as e: + logger.error(f"归档文件失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/{file_id}", response_model=FileResponse) +async def get_file( + file_id: int, + db: AsyncSession = Depends(get_db) +): + """获取单个文件详情""" + from sqlalchemy import select + + stmt = select(FileMetadata).where(FileMetadata.id == file_id) + result = await db.execute(stmt) + file = result.scalar_one_or_none() + + if not file: + raise HTTPException(status_code=404, detail="文件不存在") + + return file + +@router.delete("/{file_id}") +async def delete_file( + file_id: int, + db: AsyncSession = Depends(get_db) +): + """删除文件记录(不删除实际文件)""" + from sqlalchemy import select, delete + + stmt = select(FileMetadata).where(FileMetadata.id == file_id) + result = await db.execute(stmt) + file = result.scalar_one_or_none() + + if not file: + raise HTTPException(status_code=404, detail="文件不存在") + + await db.execute(delete(FileMetadata).where(FileMetadata.id == file_id)) + return {"message": "删除成功"} +``` + +- [ ] **Step 2: 更新 main.py 注册路由** + +```python +# backend/app/main.py (添加这部分) +from app.api import files + +app.include_router(files.router, prefix="/api/v1") +``` + +- [ ] **Step 3: 测试 API** + +```bash +cd backend +uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 +``` + +访问 http://127.0.0.1:8000/docs 查看自动生成的 API 文档 + +- [ ] **Step 4: 提交代码** + +```bash +git add . +git commit -m "feat: add file management REST API endpoints" +``` + +--- + +### Task 8: 实现 Git 操作 API + +**Files:** +- Create: `backend/app/api/git.py` + +- [ ] **Step 1: 创建 Git API 路由** + +```python +# backend/app/api/git.py +from typing import List, Optional +from pathlib import Path +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from app.core.git_manager import git_manager +from app.utils.logger import logger + +router = APIRouter(prefix="/git", tags=["git"]) + +class CloneRequest(BaseModel): + url: str + name: Optional[str] = None + +class CommitRequest(BaseModel): + repo_path: str + message: str + files: Optional[List[str]] = None + +class GitStatusResponse(BaseModel): + branch: str + is_clean: bool + modified_files: List[str] + untracked_files: List[str] + +class GitCommitResponse(BaseModel): + hash: str + message: str + author: str + timestamp: str + +@router.post("/clone") +async def clone_repository(request: CloneRequest): + """克隆 Git 仓库""" + try: + repo_path = git_manager.clone_repository(request.url, request.name) + return { + "status": "success", + "repo_path": str(repo_path), + "message": f"仓库克隆成功: {repo_path}" + } + except Exception as e: + logger.error(f"克隆仓库失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/status/{repo_path:path}", response_model=GitStatusResponse) +async def get_git_status(repo_path: str): + """获取仓库状态""" + try: + status = git_manager.get_status(Path(repo_path)) + return status + except Exception as e: + logger.error(f"获取仓库状态失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.get("/commits/{repo_path:path}", response_model=List[GitCommitResponse]) +async def get_recent_commits( + repo_path: str, + limit: int = 10 +): + """获取最近的提交记录""" + try: + commits = git_manager.get_recent_commits(Path(repo_path), limit) + return [ + GitCommitResponse( + hash=c.hash, + message=c.message, + author=c.author, + timestamp=c.timestamp.isoformat() + ) + for c in commits + ] + except Exception as e: + logger.error(f"获取提交记录失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/pull") +async def pull_repository(repo_path: str): + """拉取远程更新""" + try: + result = git_manager.pull_repository(Path(repo_path)) + return result + except Exception as e: + logger.error(f"拉取更新失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/commit") +async def commit_changes(request: CommitRequest): + """提交更改""" + try: + commit_hash = git_manager.commit_changes( + Path(request.repo_path), + request.message, + request.files + ) + return { + "status": "success", + "commit_hash": commit_hash, + "message": "提交成功" + } + except Exception as e: + logger.error(f"提交失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) +``` + +- [ ] **Step 2: 更新 main.py 注册路由** + +```python +# backend/app/main.py (添加这部分) +from app.api import git + +app.include_router(git.router, prefix="/api/v1") +``` + +- [ ] **Step 3: 提交代码** + +```bash +git add . +git commit -m "feat: add Git operations REST API endpoints" +``` + +--- + +## 阶段四:插件系统开发(第 9-10 周) + +### Task 9: 实现插件系统核心 + +**Files:** +- Create: `backend/app/plugins/hookspecs.py` +- Create: `backend/app/plugins/manager.py` +- Create: `backend/app/plugins/builtin/example_plugin.py` + +- [ ] **Step 1: 定义插件钩子规范** + +```python +# backend/app/plugins/hookspecs.py +import pluggy + +# 创建 hookspec 标记器 +hookspec = pluggy.HookspecMarker("ai_dev_assistant") + +# 创建 hook 实现器标记 +hookimpl = pluggy.HookimplMarker("ai_dev_assistant") + +class PluginHookSpec: + """插件钩子规范定义""" + + @hookspec + def on_file_created(self, file_path: str, metadata: dict): + """文件创建时的钩子""" + pass + + @hookspec + def on_file_modified(self, file_path: str, metadata: dict): + """文件修改时的钩子""" + pass + + @hookspec + def on_git_commit(self, repo_path: str, commit_hash: str, message: str): + """Git 提交时的钩子""" + pass + + @hookspec + def on_llm_request(self, provider: str, model: str, messages: list): + """大模型请求前的钩子""" + pass + + @hookspec + def on_llm_response(self, provider: str, model: str, response: str): + """大模型响应后的钩子""" + pass + + @hookspec + def register_commands(self): + """注册自定义命令""" + pass +``` + +- [ ] **Step 2: 创建插件管理器** + +```python +# backend/app/plugins/manager.py +from pathlib import Path +from typing import Dict, List, Any +import pluggy +import importlib +import sys +from app.plugins.hookspecs import PluginHookSpec +from app.utils.logger import logger + +class PluginManager: + """插件管理器""" + + def __init__(self, plugins_dir: Path): + self.plugins_dir = plugins_dir + self.pm = pluggy.PluginManager("ai_dev_assistant") + self.pm.add_hookspecs(PluginHookSpec) + self._loaded_plugins: Dict[str, Any] = {} + + def discover_plugins(self): + """发现并加载插件""" + if not self.plugins_dir.exists(): + logger.warning(f"插件目录不存在: {self.plugins_dir}") + return + + # 添加插件目录到 Python 路径 + sys.path.insert(0, str(self.plugins_dir)) + + # 扫描 Python 文件 + for plugin_file in self.plugins_dir.glob("*.py"): + if plugin_file.name.startswith("_"): + continue + + module_name = plugin_file.stem + try: + module = importlib.import_module(module_name) + self.load_plugin(module_name, module) + except Exception as e: + logger.error(f"加载插件失败 {module_name}: {e}") + + def load_plugin(self, name: str, module: Any): + """加载单个插件""" + # 查找插件类(继承自 PluginHookSpec 或有 hookimpl 标记) + plugin_instance = None + + for attr_name in dir(module): + attr = getattr(module, attr_name) + if isinstance(attr, type) and hasattr(attr, 'on_file_created'): + plugin_instance = attr() + break + + if plugin_instance: + self.pm.register(plugin_instance, name=name) + self._loaded_plugins[name] = plugin_instance + logger.info(f"插件加载成功: {name}") + else: + logger.warning(f"插件 {name} 没有有效的钩子实现") + + def unload_plugin(self, name: str): + """卸载插件""" + if name in self._loaded_plugins: + self.pm.unregister(self._loaded_plugins[name]) + del self._loaded_plugins[name] + logger.info(f"插件已卸载: {name}") + + def list_plugins(self) -> List[str]: + """列出所有已加载的插件""" + return list(self._loaded_plugins.keys()) + + def call_hook(self, hook_name: str, **kwargs): + """调用插件钩子""" + hook = getattr(self.pm.hook, hook_name) + return hook(**kwargs) + +# 全局实例 +from app.config import settings +plugin_manager = PluginManager(settings.plugins_dir) +``` + +- [ ] **Step 3: 创建示例插件** + +```python +# backend/app/plugins/builtin/example_plugin.py +from app.plugins.hookspecs import hookimpl +from app.utils.logger import logger + +class ExamplePlugin: + """示例插件""" + + @hookimpl + def on_file_created(self, file_path: str, metadata: dict): + logger.info(f"[ExamplePlugin] 文件创建: {file_path}") + + @hookimpl + def on_git_commit(self, repo_path: str, commit_hash: str, message: str): + logger.info(f"[ExamplePlugin] Git 提交: {commit_hash[:8]} - {message}") + + @hookimpl + def on_llm_request(self, provider: str, model: str, messages: list): + logger.info(f"[ExamplePlugin] LLM 请求: {provider}/{model}") +``` + +- [ ] **Step 4: 提交代码** + +```bash +git add . +git commit -m "feat: implement plugin system with pluggy" +``` + +--- + +## 阶段五:桌面端开发(第 11-14 周) + +### Task 10: 创建 Electron 项目结构 + +**Files:** +- Create: `desktop/package.json` +- Create: `desktop/tsconfig.json` +- Create: `desktop/src/main/index.ts` +- Create: `desktop/src/renderer/index.html` +- Create: `desktop/src/renderer/App.tsx` + +- [ ] **Step 1: 初始化 Electron 项目** + +```bash +mkdir -p desktop/src/{main,renderer,preload} +cd desktop +npm init -y +``` + +- [ ] **Step 2: 创建 package.json** + +```json +{ + "name": "ai-dev-assistant-desktop", + "version": "0.1.0", + "description": "AI Dev Assistant Desktop Client", + "main": "dist/main/index.js", + "scripts": { + "dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"", + "dev:main": "tsc -p tsconfig.main.json --watch", + "dev:renderer": "vite", + "build": "npm run build:main && npm run build:renderer", + "build:main": "tsc -p tsconfig.main.json", + "build:renderer": "vite build", + "start": "electron .", + "package": "electron-builder" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "antd": "^5.13.0", + "@ant-design/icons": "^5.2.6", + "axios": "^1.6.0" + }, + "devDependencies": { + "electron": "^28.0.0", + "electron-builder": "^24.9.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.0" + } +} +``` + +- [ ] **Step 3: 创建 TypeScript 配置** + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} +``` + +- [ ] **Step 4: 创建主进程入口** + +```typescript +// desktop/src/main/index.ts +import { app, BrowserWindow, ipcMain } from 'electron'; +import path from 'path'; + +let mainWindow: BrowserWindow | null = null; + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + // 开发环境加载 Vite 开发服务器 + if (process.env.NODE_ENV === 'development') { + mainWindow.loadURL('http://localhost:5173'); + mainWindow.webContents.openDevTools(); + } else { + // 生产环境加载打包后的文件 + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// IPC 通信处理 +ipcMain.handle('get-backend-url', () => { + return 'http://localhost:8000/api/v1'; +}); +``` + +- [ ] **Step 5: 创建渲染进程入口** + +```html + + + + + + + AI Dev Assistant + + +
+ + + +``` + +- [ ] **Step 6: 创建 React 应用** + +```tsx +// desktop/src/renderer/App.tsx +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; + +import Home from './pages/Home'; +import Settings from './pages/Settings'; +import Plugins from './pages/Plugins'; + +const App: React.FC = () => { + return ( + + + + } /> + } /> + } /> + + + + ); +}; + +const root = createRoot(document.getElementById('root')!); +root.render(); +``` + +- [ ] **Step 7: 提交代码** + +```bash +git add . +git commit -m "feat: initialize Electron desktop application" +``` + +--- + +### Task 11: 实现数字人展示组件 + +**Files:** +- Create: `desktop/src/renderer/components/Avatar/index.tsx` + +- [ ] **Step 1: 创建数字人组件** + +```tsx +// desktop/src/renderer/components/Avatar/index.tsx +import React, { useEffect, useRef } from 'react'; +import { Card } from 'antd'; + +interface AvatarProps { + modelUrl?: string; + isSpeaking?: boolean; +} + +const Avatar: React.FC = ({ + modelUrl = 'https://models.readyplayer.me/65a8f3c8b8e3b0f6a7d4e5f6.glb', + isSpeaking = false +}) => { + const containerRef = useRef(null); + + useEffect(() => { + // 这里集成 Ready Player Me 的 WebGL 渲染器 + // 实际实现需要加载 3D 模型和动画 + console.log('Avatar component mounted', { modelUrl, isSpeaking }); + + return () => { + // 清理资源 + }; + }, [modelUrl, isSpeaking]); + + return ( + +
+ {isSpeaking ? '正在说话...' : 'AI 数字人'} +
+
+ ); +}; + +export default Avatar; +``` + +- [ ] **Step 2: 提交代码** + +```bash +git add . +git commit -m "feat: add AI avatar component with Ready Player Me integration" +``` + +--- + +## 阶段六:集成测试和优化(第 15-16 周) + +### Task 12: 编写集成测试 + +**Files:** +- Create: `backend/tests/integration/test_api.py` + +- [ ] **Step 1: 创建 API 集成测试** + +```python +# backend/tests/integration/test_api.py +import pytest +from fastapi.testclient import TestClient +from app.main import app + +@pytest.fixture +def client(): + return TestClient(app) + +def test_health_check(client): + """测试健康检查接口""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json()["status"] == "healthy" + +def test_list_files(client): + """测试获取文件列表""" + response = client.get("/api/v1/files/") + assert response.status_code == 200 + assert isinstance(response.json(), list) + +def test_git_status_invalid_path(client): + """测试获取无效路径的 Git 状态""" + response = client.get("/api/v1/git/status/nonexistent") + assert response.status_code == 400 +``` + +- [ ] **Step 2: 运行集成测试** + +```bash +cd backend +pytest tests/integration/ -v +``` + +Expected: 所有测试通过 + +- [ ] **Step 3: 提交代码** + +```bash +git add . +git commit -m "test: add integration tests for API endpoints" +``` + +--- + +## 开发时间线总结 + +| 阶段 | 周期 | 主要任务 | 交付物 | +|------|------|---------|--------| +| 阶段一 | 第 1-2 周 | 项目基础搭建 | Python 项目结构、数据库模型、工具函数 | +| 阶段二 | 第 3-6 周 | 核心功能开发 | 大模型调度、Git 管理、文件归档 | +| 阶段三 | 第 7-8 周 | API 接口开发 | RESTful API、自动文档 | +| 阶段四 | 第 9-10 周 | 插件系统 | 插件管理器、钩子规范、示例插件 | +| 阶段五 | 第 11-14 周 | 桌面端开发 | Electron 应用、数字人组件 | +| 阶段六 | 第 15-16 周 | 集成测试 | 测试用例、性能优化 | + +--- + +## 关键注意事项 + +1. **参数校验**:所有外部输入必须经过 validator 校验,防止注入攻击 +2. **错误处理**:每个模块都要有完善的异常捕获和日志记录 +3. **类型提示**:严格使用 Python 类型注解,启用 mypy 检查 +4. **测试覆盖**:核心功能必须有单元测试,API 必须有集成测试 +5. **文档同步**:代码注释和 API 文档要保持更新 +6. **版本控制**:每完成一个 Task 就提交,commit message 要清晰 + +--- + +**Plan complete and saved to `docs/superpowers/plans/2026-06-12-ai-dev-assistant.md`. Two execution options:** + +**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration + +**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints + +**Which approach?** From 808a419a373f535177058825baec2a158677b622 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 10:39:40 +0000 Subject: [PATCH 02/16] feat: initialize Python project structure with FastAPI --- backend/.env.example | 18 ++++++++ backend/ai_dev_assistant.egg-info/PKG-INFO | 22 +++++++++ backend/ai_dev_assistant.egg-info/SOURCES.txt | 9 ++++ .../dependency_links.txt | 1 + .../ai_dev_assistant.egg-info/requires.txt | 18 ++++++++ .../ai_dev_assistant.egg-info/top_level.txt | 1 + backend/app/__init__.py | 1 + .../app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 172 bytes .../app/__pycache__/config.cpython-314.pyc | Bin 0 -> 2203 bytes backend/app/config.py | 42 ++++++++++++++++++ backend/app/main.py | 33 ++++++++++++++ backend/pyproject.toml | 42 ++++++++++++++++++ backend/requirements.txt | 11 +++++ 13 files changed, 198 insertions(+) create mode 100644 backend/.env.example create mode 100644 backend/ai_dev_assistant.egg-info/PKG-INFO create mode 100644 backend/ai_dev_assistant.egg-info/SOURCES.txt create mode 100644 backend/ai_dev_assistant.egg-info/dependency_links.txt create mode 100644 backend/ai_dev_assistant.egg-info/requires.txt create mode 100644 backend/ai_dev_assistant.egg-info/top_level.txt create mode 100644 backend/app/__init__.py create mode 100644 backend/app/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/__pycache__/config.cpython-314.pyc create mode 100644 backend/app/config.py create mode 100644 backend/app/main.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements.txt diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..771b69b --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +# 应用配置 +DEBUG=false +API_HOST=127.0.0.1 +API_PORT=8000 + +# 大模型配置 +LLM_PROVIDER=openai +OPENAI_API_KEY=your-api-key-here +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-4-turbo-preview + +# Ollama 配置 +OLLAMA_BASE_URL=http://localhost:11434 +OLLAMA_MODEL=llama2 + +# 路径配置 +GIT_WORKSPACE=./workspace +PLUGINS_DIR=./plugins diff --git a/backend/ai_dev_assistant.egg-info/PKG-INFO b/backend/ai_dev_assistant.egg-info/PKG-INFO new file mode 100644 index 0000000..75991c5 --- /dev/null +++ b/backend/ai_dev_assistant.egg-info/PKG-INFO @@ -0,0 +1,22 @@ +Metadata-Version: 2.4 +Name: ai-dev-assistant +Version: 0.1.0 +Summary: AI-powered development assistant with plugin system +Requires-Python: >=3.11 +Requires-Dist: fastapi>=0.109.0 +Requires-Dist: uvicorn[standard]>=0.27.0 +Requires-Dist: langchain-core>=0.1.0 +Requires-Dist: langchain-openai>=0.0.5 +Requires-Dist: langchain-community>=0.0.10 +Requires-Dist: pygit2>=1.13.0 +Requires-Dist: pluggy>=1.3.0 +Requires-Dist: pydantic>=2.5.0 +Requires-Dist: pydantic-settings>=2.1.0 +Requires-Dist: aiosqlite>=0.19.0 +Requires-Dist: python-dotenv>=1.0.0 +Provides-Extra: dev +Requires-Dist: pytest>=7.4.0; extra == "dev" +Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev" +Requires-Dist: black>=23.12.0; extra == "dev" +Requires-Dist: ruff>=0.1.0; extra == "dev" +Requires-Dist: mypy>=1.8.0; extra == "dev" diff --git a/backend/ai_dev_assistant.egg-info/SOURCES.txt b/backend/ai_dev_assistant.egg-info/SOURCES.txt new file mode 100644 index 0000000..ad9895c --- /dev/null +++ b/backend/ai_dev_assistant.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +pyproject.toml +ai_dev_assistant.egg-info/PKG-INFO +ai_dev_assistant.egg-info/SOURCES.txt +ai_dev_assistant.egg-info/dependency_links.txt +ai_dev_assistant.egg-info/requires.txt +ai_dev_assistant.egg-info/top_level.txt +app/__init__.py +app/config.py +app/main.py \ No newline at end of file diff --git a/backend/ai_dev_assistant.egg-info/dependency_links.txt b/backend/ai_dev_assistant.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/ai_dev_assistant.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/backend/ai_dev_assistant.egg-info/requires.txt b/backend/ai_dev_assistant.egg-info/requires.txt new file mode 100644 index 0000000..d48a8ae --- /dev/null +++ b/backend/ai_dev_assistant.egg-info/requires.txt @@ -0,0 +1,18 @@ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +langchain-core>=0.1.0 +langchain-openai>=0.0.5 +langchain-community>=0.0.10 +pygit2>=1.13.0 +pluggy>=1.3.0 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +aiosqlite>=0.19.0 +python-dotenv>=1.0.0 + +[dev] +pytest>=7.4.0 +pytest-asyncio>=0.23.0 +black>=23.12.0 +ruff>=0.1.0 +mypy>=1.8.0 diff --git a/backend/ai_dev_assistant.egg-info/top_level.txt b/backend/ai_dev_assistant.egg-info/top_level.txt new file mode 100644 index 0000000..b80f0bd --- /dev/null +++ b/backend/ai_dev_assistant.egg-info/top_level.txt @@ -0,0 +1 @@ +app diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..43a7c25 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +"""AI Dev Assistant Backend""" diff --git a/backend/app/__pycache__/__init__.cpython-314.pyc b/backend/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14838d5ffd098414dcace4a65c002c230b756902 GIT binary patch literal 172 zcmdPqPcjVa9l literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/config.cpython-314.pyc b/backend/app/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62d9463acbc9c9aecb190d1af8fd95dbeae4f7c0 GIT binary patch literal 2203 zcmaJ?-EY%Y6u(ZK#BGwMO`9}rp-t&$C`~A>)&ku$*oImGWit{Fn~LScZtLON;rgbi z5f2gjLYnpl54>$}@UlN((xgfI2NuzX?n;&Rp$YMZS0whhb8M#sX__sc`#T>#_xhaQ zy`Jexb`rSk-)BC3OC;n^R9aUg0EZs~Xpoyk7FNlMz=RRRq1Dhzn1wwYUKLj&EaG8t zHM$aGu@RCXqePDM6FIsbSqS^Nn6xg9l7%2K>yTq(q#!*E!?sit zaN?rs=+|||3~SQ?J#xv=O|24S@P?yx{sgKDD_ti{kO>RjjInT**syLU${`k!!z?O` zEG9>oBu8;}*0~;sRZ4P)+_@O?ld^;yXGwUv;!}@O!mXqU9jVza1pIb}7On?3G;Ms%6 z&%gNQ*^l4XyDzNf-`98Y7aYfMm};?l=t8Hn8UOys<~x5?A+Yk!u$e{ddbezbZPT!vH~JY=l^hO3P+K=ASoqCOLbW11)v8wUJ=@waHtS+hw|450 z8rzs%dP3F;F)k^JrEcqr!aEgZ+tz9(;)J5yuBm3=Nh!*PK^MxgeLHGnbP7=hIWWRUdzO;XAY)=}X{a~R3ASzgrR#{-165LLQe}GsP8srAK8Cv?B z5Dl!y$VbAKP$JibPssU@EKEX7fI|Tmz#eY1ivbRSJJIR=5w&mjK!irgcvL{jOdIO%o}L2x@sI@1(~-8d=5Gr|3S#w8GIYBbVA6H!km|`x_siArFSWTw?|Bz(t{P#f5SB+pS-gukPj68dvtmryFbg6NSdL z{oHKh%5OvSi1J?CQ2vSdc>U@0`MtsA##Qgd@3hC~pH43B<(3+kUx}f}xJy91dK+r` z8S$%%>su>y@>muAw5Cz^D!`*`w+g44M@-&R-PPd70!Nz@{Bv literal 0 HcmV?d00001 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..ae34760 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,42 @@ +from pathlib import Path +from typing import Optional +from pydantic_settings import BaseSettings +from pydantic import Field + + +class Settings(BaseSettings): + """应用配置""" + + # 应用基础配置 + app_name: str = "AI Dev Assistant" + app_version: str = "0.1.0" + debug: bool = False + + # API 服务配置 + api_host: str = "127.0.0.1" + api_port: int = 8000 + + # 数据库配置 + database_url: str = "sqlite+aiosqlite:///./data/assistant.db" + + # 大模型配置 + llm_provider: str = Field(default="openai", description="openai | ollama") + openai_api_key: Optional[str] = None + openai_base_url: Optional[str] = None + openai_model: str = "gpt-4-turbo-preview" + + ollama_base_url: str = "http://localhost:11434" + ollama_model: str = "llama2" + + # Git 配置 + git_workspace: Path = Field(default=Path("./workspace")) + + # 插件配置 + plugins_dir: Path = Field(default=Path("./plugins")) + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +settings = Settings() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..4de822a --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,33 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + debug=settings.debug, +) + +# 配置 CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000", "http://localhost:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + return { + "name": settings.app_name, + "version": settings.app_version, + "status": "running" + } + + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..1aac911 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "ai-dev-assistant" +version = "0.1.0" +description = "AI-powered development assistant with plugin system" +requires-python = ">=3.11" +dependencies = [ + "fastapi>=0.109.0", + "uvicorn[standard]>=0.27.0", + "langchain-core>=0.1.0", + "langchain-openai>=0.0.5", + "langchain-community>=0.0.10", + "pygit2>=1.13.0", + "pluggy>=1.3.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "aiosqlite>=0.19.0", + "python-dotenv>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.4.0", + "pytest-asyncio>=0.23.0", + "black>=23.12.0", + "ruff>=0.1.0", + "mypy>=1.8.0", +] + +[tool.black] +line-length = 88 + +[tool.ruff] +line-length = 88 +select = ["E", "F", "I"] + +[tool.mypy] +python_version = "3.11" +strict = true diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..48ccfce --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,11 @@ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +langchain-core>=0.1.0 +langchain-openai>=0.0.5 +langchain-community>=0.0.10 +pygit2>=1.13.0 +pluggy>=1.3.0 +pydantic>=2.5.0 +pydantic-settings>=2.1.0 +aiosqlite>=0.19.0 +python-dotenv>=1.0.0 From cfbba695d6695c4baff6753d3102719773841ea9 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 10:41:12 +0000 Subject: [PATCH 03/16] feat: add database models for files, projects, and operations --- backend/app/database.py | 32 ++++++++++++++++++++++++++++++++ backend/app/main.py | 10 ++++++++++ backend/app/models/__init__.py | 5 +++++ backend/app/models/file.py | 25 +++++++++++++++++++++++++ backend/app/models/operation.py | 20 ++++++++++++++++++++ backend/app/models/project.py | 21 +++++++++++++++++++++ 6 files changed, 113 insertions(+) create mode 100644 backend/app/database.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/file.py create mode 100644 backend/app/models/operation.py create mode 100644 backend/app/models/project.py diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..a2e2bc3 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,32 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import DeclarativeBase +from app.config import settings + +engine = create_async_engine( + settings.database_url, + echo=settings.debug, +) + +async_session = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False +) + +class Base(DeclarativeBase): + pass + +async def init_db(): + """初始化数据库,创建所有表""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + +async def get_db(): + """获取数据库会话""" + async with async_session() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise diff --git a/backend/app/main.py b/backend/app/main.py index 4de822a..e5e7414 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,12 +1,22 @@ +from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.config import settings +from app.database import init_db + +@asynccontextmanager +async def lifespan(app: FastAPI): + # 启动时初始化数据库 + await init_db() + yield + # 关闭时清理资源 app = FastAPI( title=settings.app_name, version=settings.app_version, debug=settings.debug, + lifespan=lifespan, ) # 配置 CORS diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..9b83909 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,5 @@ +from app.models.file import FileMetadata +from app.models.project import Project +from app.models.operation import OperationLog + +__all__ = ["FileMetadata", "Project", "OperationLog"] diff --git a/backend/app/models/file.py b/backend/app/models/file.py new file mode 100644 index 0000000..fe2ba7f --- /dev/null +++ b/backend/app/models/file.py @@ -0,0 +1,25 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy import String, Text, DateTime, Integer, Boolean +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class FileMetadata(Base): + """文件元数据""" + __tablename__ = "file_metadata" + + id: Mapped[int] = mapped_column(primary_key=True) + file_path: Mapped[str] = mapped_column(String(500), unique=True, index=True) + file_name: Mapped[str] = mapped_column(String(255), index=True) + file_type: Mapped[str] = mapped_column(String(50)) # 文件扩展名 + category: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # 分类 + tags: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON 格式标签 + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + project_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + size_bytes: Mapped[int] = mapped_column(Integer, default=0) + is_archived: Mapped[bool] = mapped_column(Boolean, default=False) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self) -> str: + return f"" diff --git a/backend/app/models/operation.py b/backend/app/models/operation.py new file mode 100644 index 0000000..7d3b407 --- /dev/null +++ b/backend/app/models/operation.py @@ -0,0 +1,20 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy import String, Text, DateTime, Integer, JSON +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class OperationLog(Base): + """操作记录""" + __tablename__ = "operation_logs" + + id: Mapped[int] = mapped_column(primary_key=True) + operation_type: Mapped[str] = mapped_column(String(50), index=True) # git_clone, file_move, etc. + target_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) + status: Mapped[str] = mapped_column(String(20)) # success, failed, pending + details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + + def __repr__(self) -> str: + return f"" diff --git a/backend/app/models/project.py b/backend/app/models/project.py new file mode 100644 index 0000000..e952247 --- /dev/null +++ b/backend/app/models/project.py @@ -0,0 +1,21 @@ +from datetime import datetime +from typing import Optional +from sqlalchemy import String, Text, DateTime, Integer, JSON +from sqlalchemy.orm import Mapped, mapped_column +from app.database import Base + +class Project(Base): + """项目配置""" + __tablename__ = "projects" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(255), unique=True, index=True) + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + git_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) + local_path: Mapped[str] = mapped_column(String(500)) + config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # 项目特定配置 + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self) -> str: + return f"" From d8ea66412f917e3b61be78f9b26cf8008415d1d6 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 10:42:53 +0000 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=E5=85=8B=E9=9A=86=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E9=85=8D=E7=BD=AE=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- backend/app/utils/__init__.py | 1 + backend/app/utils/logger.py | 47 +++++++++++++++++++++++++ backend/app/utils/validator.py | 63 ++++++++++++++++++++++++++++++++++ backend/data/assistant.db | 0 4 files changed, 111 insertions(+) create mode 100644 backend/app/utils/__init__.py create mode 100644 backend/app/utils/logger.py create mode 100644 backend/app/utils/validator.py create mode 100644 backend/data/assistant.db diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..490b22e --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1 @@ +"""工具函数模块""" diff --git a/backend/app/utils/logger.py b/backend/app/utils/logger.py new file mode 100644 index 0000000..73dfc67 --- /dev/null +++ b/backend/app/utils/logger.py @@ -0,0 +1,47 @@ +"""日志工具""" +import logging +import sys +from pathlib import Path +from typing import Optional +from logging.handlers import RotatingFileHandler +from app.config import settings + + +def setup_logger(name: str, log_file: Optional[str] = None) -> logging.Logger: + """配置日志记录器""" + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG if settings.debug else logging.INFO) + + # 控制台输出 + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.DEBUG) + console_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) + + # 文件输出(可选) + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = RotatingFileHandler( + log_path, + maxBytes=10*1024*1024, # 10MB + backupCount=5, + encoding='utf-8' + ) + file_handler.setLevel(logging.DEBUG) + file_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s' + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + return logger + + +# 默认日志记录器 +logger = setup_logger("ai_dev_assistant", "logs/app.log") diff --git a/backend/app/utils/validator.py b/backend/app/utils/validator.py new file mode 100644 index 0000000..13abe0e --- /dev/null +++ b/backend/app/utils/validator.py @@ -0,0 +1,63 @@ +"""参数校验工具""" +from pathlib import Path +from typing import Optional + + +class ValidationError(Exception): + """参数校验异常""" + pass + + +def validate_file_path(path: str) -> Path: + """校验文件路径合法性""" + if not path or not path.strip(): + raise ValidationError("文件路径不能为空") + + # 检查危险字符 + dangerous_chars = ['..', '~', '$', '`', '|', '&', ';'] + for char in dangerous_chars: + if char in path: + raise ValidationError(f"文件路径包含非法字符: {char}") + + try: + return Path(path).resolve() + except Exception as e: + raise ValidationError(f"无效的文件路径: {e}") + + +def validate_git_url(url: str) -> str: + """校验 Git 仓库 URL""" + if not url or not url.strip(): + raise ValidationError("Git URL 不能为空") + + # 支持 HTTPS 和 SSH 格式 + https_pattern = r'^https?://[\w\-\.]+(:\d+)?/[\w\-\.]+/[\w\-\.]+(\.git)?$' + ssh_pattern = r'^git@[\w\-\.]+:[\w\-\.]+/[\w\-\.]+(\.git)?$' + + import re + if not (re.match(https_pattern, url) or re.match(ssh_pattern, url)): + raise ValidationError(f"无效的 Git URL 格式: {url}") + + return url.strip() + + +def validate_model_config(provider: str, model: str) -> None: + """校验大模型配置""" + valid_providers = ['openai', 'ollama'] + if provider not in valid_providers: + raise ValidationError(f"不支持的模型提供商: {provider},支持: {valid_providers}") + + if not model or not model.strip(): + raise ValidationError("模型名称不能为空") + + +def validate_category(category: Optional[str]) -> Optional[str]: + """校验分类名称""" + if category is None: + return None + + category = category.strip() + if len(category) > 100: + raise ValidationError("分类名称不能超过 100 个字符") + + return category diff --git a/backend/data/assistant.db b/backend/data/assistant.db new file mode 100644 index 0000000..e69de29 From 18c57e4a6ccf3a90d10596c55af9b09987ed49c1 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 10:53:17 +0000 Subject: [PATCH 05/16] feat: add parameter validation and logging utilities --- .../utils/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 175 bytes .../utils/__pycache__/validator.cpython-314.pyc | Bin 0 -> 3581 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/app/utils/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/utils/__pycache__/validator.cpython-314.pyc diff --git a/backend/app/utils/__pycache__/__init__.cpython-314.pyc b/backend/app/utils/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a80bbbf9e4fb5d9dc2dd92ce62382ee39145c23 GIT binary patch literal 175 zcmdPq}zQ`1q9!pFz5BY3P^d7iAY0BqpcoCnY9lr{<;TCl(aw zmzHGa6zj*wXXa&=#K-FuRNmsS$<0qG%}KQ@VgqUf*O!XV9OX>t0oq0oCbg4TbdX%5Sw18GtXOSCP2&pvGRqwP1A(TI3c8Iq0=;Iz)9ghP;q8}i#y|KEE#^PUBl36`$OOE-Pwv^ z!VjJ9*t>6c-|oGA`}yp9YVwqoA}D=-Yx-fzfzTJUV-=ez%tb*+BN1t+ABlJy?&37A zmde~VzDv-AT2zJVkjPgfQRuU^@WwjLF4|zkUX5bqnnQGe>}0Z2ECIQc$t9u-WH*yb z#j<+j_oOOdeSY|b!m0Cx)9>8+;6(nyyZIv*{8$%uhO{WO&Yq;EB;ui1*tC!}*zC+b z3_=?9Aq|7cT)$o8MXU)Tr`bdvJ|O7c-cU@5gys^` zQ<|(w60w7cbJSklA9y}N_NvKHSPtwCh4;$wNFbC<22z?5Qv>@ALldMqd4SOSU=>}h zI-xp>ZU`$r7o4QbnhH}dSe_6-rk63d4yrWjMg83R!w5xSYz6u*j$#o9Va6yT7vwBC zz=5CmTePnWNpQMuC$g3zaX+W?szwN%fs3&3NsxUZO{E`PMLbj;921Ji6L4m56RCnB z7yPTno!mtpykVABZ$2|ao6%jY$LwdOgDxxdtYD@v=Z3wh>0dV|BGuIRUbNFXq&ZHT z7E`u#2i#OQNL|2+h+E86QQV9~TMtg+UR;hkVb3J@tgjWc2AFfUgV9CnxL(eUzQrx9 zEj&`B0GF8!L7cSxW6o@s1ZzL2SkVzt?O5tdT zsJcKK9m^_3ZG+F}Gw@VlQa}cV0rMyrNj+#VRQ(KuG@5pKGup`DD}y5kUp+YCS(&R@ zKkjMGIa~i>N7e1=p3lo`hlJ}c@5>z%6?M7QPhQ@2xihzF`*_8UoO{Qt4K4A_ICW39ymf!^){T4XbI$sIPPmrdMi}N`SKs4Vd|u7fw2yl_a?Xx_&+stuFO_=x zihJ3XHR#G3*AqN{Wu0fsM*hl18_53?8)09g7;Gfv(9`)8i4pohp2egR8b2lgB2>gc z#yM84pc5AH%5e28g6hrs8QP52v);YLquyu4`ws~J58{bj5{rD2gYY42-Zynpr-eZ35#`L-&WZ|c%(`HQdlz9;TT zQJ>QxkbTAU8hpGzs%c4eQ(GYLT=4neBf;jrrpC5lq{+W&VPHXO3^orant#*k!KQu? zHqWnVo0smR+?}GgI!{n$9JQoydMrOS+~xz0tpo)|M4cnDZi7cU9EIUEFszp`6DgH6 znZ$LMsz$92X#|Bu3@o2#AXq3-{H+Bg2Ce}RDE~Z}@QCt?GP3v8y^Mb?~Y2taxB_11+P7yeI@{CSWr-SR5Z4N)ivYhNY+2`o#EN296j%@vtUwcm&}DJM42pOs^~a(@y+ezoYX-%I z-E9jyigxgdqX_r;(KChj-^ssq{MM0^)1Ulawc3{dNA-Q3xgUo&NjS2}1+=EI2W(eCFizne)Z$>GpF*@SxlJdD09G2|%TL z83rZmqEwY0w4uR_5moaDW%nnA6Y)LD0HG&mm$V&(G@7igJ-saV&^OXO*B#!^91RR` z8?w>t_b>k>_ss5`7|zv1#yxV*DNnn~e`$N$^()ugWp9)*M4J(1mPcP}xEG3Mwu0tp zI`yDLYaz{AOKEHtNWUx*-Cv_P3^>D(UkxP40Tv6uZPXtR3<%!_C3O{46tN|E=`5{A zogY@dTQ)<*j~d4w{nI0Cbm5GaRrCw+FPOgudeI)}t$^PH^$*C#l0})+LY~wgS^qjf z-N?}9^C+<>$Ld8i31zvt`8#0f==2{h8L;V2QF`W!Mhqa`{P@VtKfmPr=DKz8E`MMo zG=L-uD4mbVafUAln#>iH0}V!+v1A@X^PZ*IAX78mmq~~Bs-yabw{p^3J$&fcp>c0* zx@*EwI<)@9pJvq091k;`bdGzr=A2t6U6tt`=EH8ki%<^e4w|~el-(?M*6rGXq!J%6 z?yVN)4dB;PiDH(4mrxVfZw0oMVX3pj_z|*^bigb94^Ty bw-GdVN+Hp{vkHR#9UnYRcbCFVv;F=H#TP^G literal 0 HcmV?d00001 From 4f2316469be6d8674f48f599304d7b517ba81d82 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 10:56:29 +0000 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=E5=85=8B=E9=9A=86=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E9=85=8D=E7=BD=AE=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- backend/app/core/__init__.py | 1 + backend/app/core/context.py | 41 +++++++++++++ backend/app/core/llm.py | 113 +++++++++++++++++++++++++++++++++++ backend/tests/test_llm.py | 82 +++++++++++++++++++++++++ 4 files changed, 237 insertions(+) create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/context.py create mode 100644 backend/app/core/llm.py create mode 100644 backend/tests/test_llm.py diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..946f6c7 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ +"""核心业务逻辑模块""" diff --git a/backend/app/core/context.py b/backend/app/core/context.py new file mode 100644 index 0000000..70b8dbe --- /dev/null +++ b/backend/app/core/context.py @@ -0,0 +1,41 @@ +"""对话上下文管理""" +from typing import List, Dict, Any +from dataclasses import dataclass, field +from datetime import datetime + + +@dataclass +class Message: + """消息""" + role: str # user, assistant, system + content: str + timestamp: datetime = field(default_factory=datetime.utcnow) + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class ConversationContext: + """对话上下文""" + session_id: str + messages: List[Message] = field(default_factory=list) + max_history: int = 20 + + def add_message(self, role: str, content: str, **metadata): + """添加消息""" + msg = Message(role=role, content=content, metadata=metadata) + self.messages.append(msg) + + # 保持历史长度 + if len(self.messages) > self.max_history: + self.messages = self.messages[-self.max_history:] + + def get_messages_for_llm(self) -> List[Dict[str, str]]: + """获取发送给 LLM 的消息格式""" + return [ + {"role": msg.role, "content": msg.content} + for msg in self.messages + ] + + def clear(self): + """清空上下文""" + self.messages.clear() diff --git a/backend/app/core/llm.py b/backend/app/core/llm.py new file mode 100644 index 0000000..131cc96 --- /dev/null +++ b/backend/app/core/llm.py @@ -0,0 +1,113 @@ +"""大模型统一调度模块""" +from typing import Optional, AsyncIterator, List, Dict, Any +from langchain_core.language_models import BaseChatModel +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage +from app.config import settings +from app.utils.logger import logger +from app.utils.validator import validate_model_config + + +class LLMManager: + """大模型统一管理器""" + + def __init__(self): + self._llm: Optional[BaseChatModel] = None + self._provider: Optional[str] = None + self._model: Optional[str] = None + + def _create_openai_llm(self) -> BaseChatModel: + """创建 OpenAI 模型""" + from langchain_openai import ChatOpenAI + + if not settings.openai_api_key: + raise ValueError("OpenAI API Key 未配置") + + return ChatOpenAI( + model=settings.openai_model, + api_key=settings.openai_api_key, + base_url=settings.openai_base_url, + temperature=0.7, + max_tokens=2000, + ) + + def _create_ollama_llm(self) -> BaseChatModel: + """创建 Ollama 模型""" + from langchain_community.chat_models import ChatOllama + + return ChatOllama( + model=settings.ollama_model, + base_url=settings.ollama_base_url, + temperature=0.7, + num_predict=2000, + ) + + def get_llm(self) -> BaseChatModel: + """获取大模型实例""" + current_model = settings.openai_model if settings.llm_provider == "openai" else settings.ollama_model + validate_model_config(settings.llm_provider, current_model) + + # 如果配置没变,返回缓存的实例 + if (self._llm and + self._provider == settings.llm_provider and + self._model == current_model): + return self._llm + + # 创建新实例 + if settings.llm_provider == "openai": + self._llm = self._create_openai_llm() + elif settings.llm_provider == "ollama": + self._llm = self._create_ollama_llm() + else: + raise ValueError(f"不支持的模型提供商: {settings.llm_provider}") + + self._provider = settings.llm_provider + self._model = current_model + + logger.info(f"已切换到大模型: {self._provider}/{self._model}") + return self._llm + + def _convert_messages(self, messages: List[Dict[str, str]]) -> List[BaseMessage]: + """转换消息格式为 LangChain 格式""" + converted = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "user": + converted.append(HumanMessage(content=content)) + elif role == "assistant": + converted.append(AIMessage(content=content)) + elif role == "system": + converted.append(SystemMessage(content=content)) + else: + converted.append(HumanMessage(content=content)) + + return converted + + async def chat(self, messages: List[Dict[str, str]], **kwargs) -> str: + """同步对话""" + llm = self.get_llm() + converted_messages = self._convert_messages(messages) + + try: + response = await llm.ainvoke(converted_messages, **kwargs) + return response.content + except Exception as e: + logger.error(f"大模型调用失败: {e}") + raise + + async def stream_chat(self, messages: List[Dict[str, str]], **kwargs) -> AsyncIterator[str]: + """流式对话""" + llm = self.get_llm() + converted_messages = self._convert_messages(messages) + + try: + async for chunk in llm.astream(converted_messages, **kwargs): + yield chunk.content + except Exception as e: + logger.error(f"大模型流式调用失败: {e}") + raise + + +# 全局实例 +llm_manager = LLMManager() diff --git a/backend/tests/test_llm.py b/backend/tests/test_llm.py new file mode 100644 index 0000000..a52b4b8 --- /dev/null +++ b/backend/tests/test_llm.py @@ -0,0 +1,82 @@ +"""大模型模块测试""" +import pytest +from unittest.mock import Mock, patch, AsyncMock +from app.core.llm import LLMManager +from app.core.context import ConversationContext + + +def test_context_add_message(): + """测试上下文添加消息""" + ctx = ConversationContext(session_id="test") + ctx.add_message("user", "hello") + ctx.add_message("assistant", "hi") + + assert len(ctx.messages) == 2 + assert ctx.messages[0].role == "user" + assert ctx.messages[1].content == "hi" + + +def test_context_max_history(): + """测试上下文历史长度限制""" + ctx = ConversationContext(session_id="test", max_history=3) + + for i in range(5): + ctx.add_message("user", f"msg {i}") + + assert len(ctx.messages) == 3 + assert ctx.messages[0].content == "msg 2" + + +def test_context_get_messages_for_llm(): + """测试获取 LLM 格式消息""" + ctx = ConversationContext(session_id="test") + ctx.add_message("system", "You are a helpful assistant") + ctx.add_message("user", "hello") + + messages = ctx.get_messages_for_llm() + assert len(messages) == 2 + assert messages[0]["role"] == "system" + assert messages[1]["role"] == "user" + + +def test_context_clear(): + """测试清空上下文""" + ctx = ConversationContext(session_id="test") + ctx.add_message("user", "hello") + ctx.clear() + + assert len(ctx.messages) == 0 + + +@pytest.mark.asyncio +async def test_llm_chat(): + """测试对话功能""" + manager = LLMManager() + + # Mock 掉实际的 LLM 调用 + with patch.object(manager, 'get_llm') as mock_get_llm: + mock_llm = AsyncMock() + mock_response = Mock() + mock_response.content = "test response" + mock_llm.ainvoke.return_value = mock_response + mock_get_llm.return_value = mock_llm + + result = await manager.chat([{"role": "user", "content": "hello"}]) + assert result == "test response" + + +def test_convert_messages(): + """测试消息格式转换""" + manager = LLMManager() + + messages = [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"} + ] + + converted = manager._convert_messages(messages) + assert len(converted) == 3 + assert converted[0].__class__.__name__ == "SystemMessage" + assert converted[1].__class__.__name__ == "HumanMessage" + assert converted[2].__class__.__name__ == "AIMessage" From b3ec440eae6f217d1e4d4db22a6af7fef4ce0043 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:03:35 +0000 Subject: [PATCH 07/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Pydantic=20Co?= =?UTF-8?q?nfig=20=E5=BC=83=E7=94=A8=E8=AD=A6=E5=91=8A=E5=92=8C=20datetime?= =?UTF-8?q?.utcnow=20=E5=BC=83=E7=94=A8=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/config.py | 11 ++++++----- backend/app/core/context.py | 4 ++-- backend/app/models/file.py | 6 +++--- backend/app/models/operation.py | 4 ++-- backend/app/models/project.py | 6 +++--- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index ae34760..cd435dd 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,12 +1,17 @@ from pathlib import Path from typing import Optional from pydantic_settings import BaseSettings -from pydantic import Field +from pydantic import Field, ConfigDict class Settings(BaseSettings): """应用配置""" + model_config = ConfigDict( + env_file=".env", + env_file_encoding="utf-8" + ) + # 应用基础配置 app_name: str = "AI Dev Assistant" app_version: str = "0.1.0" @@ -33,10 +38,6 @@ class Settings(BaseSettings): # 插件配置 plugins_dir: Path = Field(default=Path("./plugins")) - - class Config: - env_file = ".env" - env_file_encoding = "utf-8" settings = Settings() diff --git a/backend/app/core/context.py b/backend/app/core/context.py index 70b8dbe..d7238cd 100644 --- a/backend/app/core/context.py +++ b/backend/app/core/context.py @@ -1,7 +1,7 @@ """对话上下文管理""" from typing import List, Dict, Any from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone @dataclass @@ -9,7 +9,7 @@ class Message: """消息""" role: str # user, assistant, system content: str - timestamp: datetime = field(default_factory=datetime.utcnow) + timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) metadata: Dict[str, Any] = field(default_factory=dict) diff --git a/backend/app/models/file.py b/backend/app/models/file.py index fe2ba7f..323f073 100644 --- a/backend/app/models/file.py +++ b/backend/app/models/file.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Optional from sqlalchemy import String, Text, DateTime, Integer, Boolean from sqlalchemy.orm import Mapped, mapped_column @@ -18,8 +18,8 @@ class FileMetadata(Base): project_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) size_bytes: Mapped[int] = mapped_column(Integer, default=0) is_archived: Mapped[bool] = mapped_column(Boolean, default=False) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) def __repr__(self) -> str: return f"" diff --git a/backend/app/models/operation.py b/backend/app/models/operation.py index 7d3b407..857462f 100644 --- a/backend/app/models/operation.py +++ b/backend/app/models/operation.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Optional from sqlalchemy import String, Text, DateTime, Integer, JSON from sqlalchemy.orm import Mapped, mapped_column @@ -14,7 +14,7 @@ class OperationLog(Base): status: Mapped[str] = mapped_column(String(20)) # success, failed, pending details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) def __repr__(self) -> str: return f"" diff --git a/backend/app/models/project.py b/backend/app/models/project.py index e952247..12c1077 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from typing import Optional from sqlalchemy import String, Text, DateTime, Integer, JSON from sqlalchemy.orm import Mapped, mapped_column @@ -14,8 +14,8 @@ class Project(Base): git_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) local_path: Mapped[str] = mapped_column(String(500)) config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # 项目特定配置 - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) def __repr__(self) -> str: return f"" From 82b691212cc381b304b9bf964737506dbdb2a61c Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:15:45 +0000 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20=E5=85=8B=E9=9A=86=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E9=85=8D=E7=BD=AE=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- .../app/__pycache__/config.cpython-314.pyc | Bin 2203 -> 2081 bytes backend/app/core/file_organizer.py | 162 ++++++++++ backend/app/core/git_manager.py | 215 ++++++++++++ backend/tests/test_file_organizer.py | 306 ++++++++++++++++++ backend/tests/test_git.py | 173 ++++++++++ 5 files changed, 856 insertions(+) create mode 100644 backend/app/core/file_organizer.py create mode 100644 backend/app/core/git_manager.py create mode 100644 backend/tests/test_file_organizer.py create mode 100644 backend/tests/test_git.py diff --git a/backend/app/__pycache__/config.cpython-314.pyc b/backend/app/__pycache__/config.cpython-314.pyc index 62d9463acbc9c9aecb190d1af8fd95dbeae4f7c0..5415f9eb0edf0d52f63b0ceb2db3e18b5d4f4b4b 100644 GIT binary patch delta 914 zcmYk4OK1~O6o$_vlVpLP53d_K@s&55A_i*6^M`ei9iFyPg_WU21_C4 zZ6QGtI`)Q1m`2zgB@r4UQ5q*Pnm}JkWZrm@JLpLgV~1~4I?jqLd&9~9mzHN?gd>$58T&Ci20WMgG2YV`hr?BKE849bLLHVhxyu5SpLG2f1kiA*l~!&n|!yO0FJXo zh)Yer-Ih?u5R5kYq@6_3=Gby}7Dc-wQg#kSCnV%1f64Ab(dUS1yC20#kg^*y>(`rn zuRVZ)L6F-w%Il@(3}3K^Ff#1;2kcWQiXf+({D^%T#i)}xVUMAh05R3%C+u@5E^`oP zD~|u3aJQSZVQ{v+T{jTa2;JXYX4FaJzrH)F}oOn%Sf7QHLic7vwotBKCnJ_5b> zsBpT_V($AU=dpp)&Jv8nA<+vMbDX%CB}}kCQP-;H^>HR~7e)`muAAeyEl6!a>lXC= bfbKnSo;&mO;U2JuJ;gy#e0FwKa2ovsJpR>l delta 1037 zcmZvaOK1~O6o$`DC&}d1ykC7LO<(CtF~zo0MJaS=7q`%bkPabDOC+7h9CD#KmmY`{!*$H_zvDg_Ug88?1irs!$U%(>7+I z60=e>lc|N-sFm5%jyO=6+NgcX)Oz1Z9n2-}cIsqq>S7++&b-){_K9BiBx&|h&r~a7 z{=Zf4f2x5X#OHzf!a)6#XC}cVND&$cLqTRC8f4)mU=iv_Kr*Dui#f*f>RPGn6jonf z?H=wv`}Sbx@Xd?bck;f1GuW_AoV!&hujSQJw)s0vw*Cl?dxYMEG%!MenU)7w8zoHo zHw9s2*4<}D6LUp%HNUnYP2|+u8|E@w9iM&<3rVXkD~g&~&nb#-SCsYQO1V(`VNTRO zkgnQ}C2ZH+!si&hYp=~T3BSXsKCp2rc+|p zA|k9cq>;*75&V7xTfk*-`|8plw*evC4!&Sr>fsI)PH=kaQkc6?_>8t*?neD$s$N3P75eP-= zQi_kF7&9s{K91r71R`~5mZwlmgHISv|C|w`xk8*vGOBsRL-Qhh2)}@CRg{%tR#CJr zOm!o~ZdE|*#W045BP@tML;}%|z@ch`h#^E9Vi=J`;8=C9qGVLH$TBRatd>W{ZX*S_j>+-B|igH)986nZSgiASUBZM>{(tyqe cj5MI*#1bR@+jmbuJor2TXW;RrE!p_cAJ~EESO5S3 diff --git a/backend/app/core/file_organizer.py b/backend/app/core/file_organizer.py new file mode 100644 index 0000000..8200d21 --- /dev/null +++ b/backend/app/core/file_organizer.py @@ -0,0 +1,162 @@ +"""文件自动归档管理器""" +from pathlib import Path +from typing import Dict, List, Optional +from datetime import datetime, timezone +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.models.file import FileMetadata +from app.utils.logger import logger +from app.utils.validator import validate_file_path, validate_category + + +class FileOrganizer: + """文件自动归档管理器""" + + # 默认分类规则 + DEFAULT_CATEGORIES = { + "code": [".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".cpp", ".c", ".go", ".rs"], + "document": [".md", ".txt", ".doc", ".docx", ".pdf", ".rst"], + "data": [".json", ".xml", ".yaml", ".yml", ".csv", ".sql"], + "image": [".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"], + "config": [".env", ".toml", ".ini", ".cfg", ".conf"], + "archive": [".zip", ".tar", ".gz", ".rar", ".7z"], + } + + def __init__(self, db: AsyncSession): + self.db = db + + def classify_file(self, file_path: Path) -> str: + """根据文件扩展名自动分类""" + ext = file_path.suffix.lower() + + for category, extensions in self.DEFAULT_CATEGORIES.items(): + if ext in extensions: + return category + + return "other" + + async def scan_directory(self, directory: Path, recursive: bool = True) -> List[FileMetadata]: + """扫描目录,创建文件元数据""" + dir_path = validate_file_path(str(directory)) + + if not dir_path.exists(): + raise ValueError(f"目录不存在: {dir_path}") + + files = [] + pattern = "**/*" if recursive else "*" + + for file_path in dir_path.glob(pattern): + if file_path.is_file(): + metadata = await self.create_file_metadata(file_path) + files.append(metadata) + + logger.info(f"扫描完成,找到 {len(files)} 个文件") + return files + + async def create_file_metadata(self, file_path: Path) -> FileMetadata: + """创建文件元数据""" + path = validate_file_path(str(file_path)) + + if not path.exists(): + raise ValueError(f"文件不存在: {path}") + + # 自动分类 + category = self.classify_file(path) + + # 获取文件信息 + stat = path.stat() + + metadata = FileMetadata( + file_path=str(path), + file_name=path.name, + file_type=path.suffix.lower(), + category=category, + size_bytes=stat.st_size, + created_at=datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc), + updated_at=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc), + ) + + return metadata + + async def save_metadata(self, metadata: FileMetadata) -> FileMetadata: + """保存文件元数据到数据库""" + # 检查是否已存在 + stmt = select(FileMetadata).where(FileMetadata.file_path == metadata.file_path) + result = await self.db.execute(stmt) + existing = result.scalar_one_or_none() + + if existing: + # 更新现有记录 + for key, value in metadata.__dict__.items(): + if key != '_sa_instance_state' and key != 'id': + setattr(existing, key, value) + await self.db.flush() + return existing + else: + # 创建新记录 + self.db.add(metadata) + await self.db.flush() + return metadata + + async def organize_file(self, source: Path, target_dir: Path, category: Optional[str] = None) -> FileMetadata: + """归档文件到目标目录""" + source_path = validate_file_path(str(source)) + target_base = validate_file_path(str(target_dir)) + + if not source_path.exists(): + raise ValueError(f"源文件不存在: {source_path}") + + # 确定分类 + if not category: + category = self.classify_file(source_path) + + category = validate_category(category) + + # 创建分类目录 + category_dir = target_base / category + category_dir.mkdir(parents=True, exist_ok=True) + + # 移动文件 + target_path = category_dir / source_path.name + + # 处理文件名冲突 + counter = 1 + original_target = target_path + while target_path.exists(): + stem = original_target.stem + suffix = original_target.suffix + target_path = category_dir / f"{stem}_{counter}{suffix}" + counter += 1 + + import shutil + shutil.move(str(source_path), str(target_path)) + logger.info(f"文件已归档: {source_path} -> {target_path}") + + # 创建并保存元数据 + metadata = await self.create_file_metadata(target_path) + metadata.category = category + return await self.save_metadata(metadata) + + async def search_files( + self, + keyword: Optional[str] = None, + category: Optional[str] = None, + file_type: Optional[str] = None, + limit: int = 100 + ) -> List[FileMetadata]: + """搜索文件""" + stmt = select(FileMetadata) + + if keyword: + stmt = stmt.where(FileMetadata.file_name.contains(keyword)) + + if category: + stmt = stmt.where(FileMetadata.category == category) + + if file_type: + stmt = stmt.where(FileMetadata.file_type == file_type) + + stmt = stmt.limit(limit) + + result = await self.db.execute(stmt) + return result.scalars().all() diff --git a/backend/app/core/git_manager.py b/backend/app/core/git_manager.py new file mode 100644 index 0000000..788948c --- /dev/null +++ b/backend/app/core/git_manager.py @@ -0,0 +1,215 @@ +"""Git 仓库管理器""" +from pathlib import Path +from typing import Optional, List, Dict, Any +from dataclasses import dataclass +from datetime import datetime, timezone +import pygit2 +from app.utils.logger import logger +from app.utils.validator import validate_git_url, validate_file_path + + +@dataclass +class GitCommit: + """Git 提交记录""" + hash: str + message: str + author: str + timestamp: datetime + + +@dataclass +class GitStatus: + """Git 状态""" + branch: str + is_clean: bool + modified_files: List[str] + untracked_files: List[str] + + +class GitManager: + """Git 仓库管理器""" + + def __init__(self, workspace: Path): + self.workspace = workspace.resolve() + self.workspace.mkdir(parents=True, exist_ok=True) + + def clone_repository(self, url: str, name: Optional[str] = None) -> Path: + """克隆 Git 仓库""" + validated_url = validate_git_url(url) + + # 生成仓库名称 + if not name: + name = validated_url.split('/')[-1].replace('.git', '') + + repo_path = self.workspace / name + + if repo_path.exists(): + raise ValueError(f"仓库目录已存在: {repo_path}") + + logger.info(f"开始克隆仓库: {validated_url} -> {repo_path}") + + try: + # 使用回调函数显示进度 + class CloneProgress(pygit2.RemoteCallbacks): + def transfer_progress(self, stats): + if stats.received_objects > 0: + pct = stats.received_objects / stats.total_objects * 100 + logger.debug(f"克隆进度: {pct:.1f}%") + + repo = pygit2.clone_repository( + validated_url, + str(repo_path), + callbacks=CloneProgress() + ) + + logger.info(f"仓库克隆成功: {repo_path}") + return repo_path + + except Exception as e: + logger.error(f"克隆仓库失败: {e}") + # 清理失败的克隆 + if repo_path.exists(): + import shutil + shutil.rmtree(repo_path) + raise + + def open_repository(self, path: Path) -> pygit2.Repository: + """打开本地仓库""" + repo_path = validate_file_path(str(path)) + + if not repo_path.exists(): + raise ValueError(f"仓库路径不存在: {repo_path}") + + try: + return pygit2.Repository(str(repo_path)) + except Exception as e: + raise ValueError(f"无法打开 Git 仓库: {e}") + + def get_status(self, repo_path: Path) -> GitStatus: + """获取仓库状态""" + repo = self.open_repository(repo_path) + + # 获取当前分支 + if repo.head_is_unborn: + branch = "HEAD (unborn)" + else: + branch = repo.head.shorthand + + # 获取状态 + status = repo.status() + modified = [] + untracked = [] + + for filepath, flags in status.items(): + if flags & pygit2.GIT_STATUS_WT_MODIFIED: + modified.append(filepath) + if flags & pygit2.GIT_STATUS_WT_NEW: + untracked.append(filepath) + + return GitStatus( + branch=branch, + is_clean=len(status) == 0, + modified_files=modified, + untracked_files=untracked + ) + + def get_recent_commits(self, repo_path: Path, limit: int = 10) -> List[GitCommit]: + """获取最近的提交记录""" + repo = self.open_repository(repo_path) + + if repo.head_is_unborn: + return [] + + commits = [] + for commit in repo.walk(repo.head.target, pygit2.GIT_SORT_TIME): + commits.append(GitCommit( + hash=str(commit.id), + message=commit.message.strip(), + author=commit.author.name, + timestamp=datetime.fromtimestamp(commit.commit_time, tz=timezone.utc) + )) + + if len(commits) >= limit: + break + + return commits + + def pull_repository(self, repo_path: Path) -> Dict[str, Any]: + """拉取远程更新""" + repo = self.open_repository(repo_path) + + if repo.head_is_unborn: + raise ValueError("仓库没有初始提交") + + # 获取当前分支的远程跟踪 + branch = repo.head.shorthand + remote_name = repo.branches[branch].upstream.remote_name if repo.branches[branch].upstream else "origin" + + remote = repo.remotes[remote_name] + remote.fetch() + + # 合并远程分支 + remote_ref = f"refs/remotes/{remote_name}/{branch}" + remote_commit = repo.references[remote_ref].target + + # 执行快进合并 + repo.merge(remote_commit) + + if repo.state == pygit2.GIT_REPO_STATE_MERGE: + # 如果有冲突,需要手动解决 + raise ValueError("合并冲突,需要手动解决") + + return { + "status": "success", + "branch": branch, + "remote": remote_name + } + + def commit_changes(self, repo_path: Path, message: str, files: Optional[List[str]] = None) -> str: + """提交更改""" + repo = self.open_repository(repo_path) + + # 添加文件到索引 + if files: + for file in files: + repo.index.add(file) + else: + # 添加所有更改 + repo.index.add_all() + + repo.index.write() + + # 创建提交 + tree = repo.index.write_tree() + + if repo.head_is_unborn: + # 首次提交 + sig = repo.default_signature + commit = repo.create_commit( + "HEAD", + sig, + sig, + message, + tree, + [] + ) + else: + # 正常提交 + parent = repo.head.peel(pygit2.Commit) + sig = repo.default_signature + commit = repo.create_commit( + "HEAD", + sig, + sig, + message, + tree, + [parent.id] + ) + + logger.info(f"提交成功: {commit}") + return str(commit) + + +# 全局实例(使用配置的工作空间) +from app.config import settings +git_manager = GitManager(settings.git_workspace) diff --git a/backend/tests/test_file_organizer.py b/backend/tests/test_file_organizer.py new file mode 100644 index 0000000..69d034a --- /dev/null +++ b/backend/tests/test_file_organizer.py @@ -0,0 +1,306 @@ +"""文件归档管理器测试""" +import pytest +import tempfile +from pathlib import Path +from app.core.file_organizer import FileOrganizer + + +@pytest.fixture +def temp_workspace(): + """创建临时工作空间""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +def test_file_organizer_init(temp_workspace): + """测试 FileOrganizer 初始化""" + organizer = FileOrganizer(temp_workspace) + assert organizer.workspace == temp_workspace + + +def test_classify_file(temp_workspace): + """测试文件分类""" + organizer = FileOrganizer(temp_workspace) + + # 测试代码文件 + assert organizer.classify_file(Path("test.py")) == "code" + assert organizer.classify_file(Path("app.js")) == "code" + assert organizer.classify_file(Path("main.go")) == "code" + + # 测试文档文件 + assert organizer.classify_file(Path("readme.md")) == "document" + assert organizer.classify_file(Path("notes.txt")) == "document" + assert organizer.classify_file(Path("report.pdf")) == "document" + + # 测试数据文件 + assert organizer.classify_file(Path("config.json")) == "data" + assert organizer.classify_file(Path("data.csv")) == "data" + assert organizer.classify_file(Path("settings.yaml")) == "data" + + # 测试图片文件 + assert organizer.classify_file(Path("photo.jpg")) == "image" + assert organizer.classify_file(Path("logo.png")) == "image" + + # 测试配置文件 + assert organizer.classify_file(Path(".env")) == "config" + assert organizer.classify_file(Path("config.toml")) == "config" + + # 测试未知类型 + assert organizer.classify_file(Path("unknown.xyz")) == "other" + + +def test_scan_directory(temp_workspace): + """测试扫描目录""" + organizer = FileOrganizer(temp_workspace) + + # 创建测试文件 + (temp_workspace / "test1.py").write_text("print('hello')") + (temp_workspace / "test2.md").write_text("# Test") + (temp_workspace / "test3.json").write_text("{}") + + # 创建子目录和文件 + subdir = temp_workspace / "subdir" + subdir.mkdir() + (subdir / "test4.txt").write_text("test") + + # 扫描目录 + files = organizer.scan_directory(temp_workspace) + + assert len(files) == 4 + file_names = [f.name for f in files] + assert "test1.py" in file_names + assert "test2.md" in file_names + assert "test3.json" in file_names + assert "test4.txt" in file_names + + +def test_scan_directory_non_recursive(temp_workspace): + """测试非递归扫描目录""" + organizer = FileOrganizer(temp_workspace) + + # 创建测试文件 + (temp_workspace / "test1.py").write_text("print('hello')") + + # 创建子目录和文件 + subdir = temp_workspace / "subdir" + subdir.mkdir() + (subdir / "test2.py").write_text("print('world')") + + # 非递归扫描 + files = organizer.scan_directory(temp_workspace, recursive=False) + + assert len(files) == 1 + assert files[0].name == "test1.py" + + +def test_create_file_metadata(temp_workspace): + """测试创建文件元数据""" + organizer = FileOrganizer(temp_workspace) + + test_file = temp_workspace / "test.py" + test_file.write_text("print('hello')") + + metadata = organizer.create_file_metadata(test_file) + + assert metadata.name == "test.py" + assert metadata.path == str(test_file) + assert metadata.category == "code" + assert metadata.extension == ".py" + assert metadata.size > 0 + + +def test_save_metadata(temp_workspace): + """测试保存元数据到数据库""" + organizer = FileOrganizer(temp_workspace) + + test_file = temp_workspace / "test.py" + test_file.write_text("print('hello')") + + metadata = organizer.create_file_metadata(test_file) + saved = organizer.save_metadata(metadata) + + assert saved.id is not None + assert saved.name == "test.py" + + # 验证数据库中有记录 + files = organizer.search_files() + assert len(files) == 1 + assert files[0].name == "test.py" + + +def test_save_metadata_update(temp_workspace): + """测试更新已存在的元数据""" + organizer = FileOrganizer(temp_workspace) + + test_file = temp_workspace / "test.py" + test_file.write_text("print('hello')") + + # 第一次保存 + metadata1 = organizer.create_file_metadata(test_file) + saved1 = organizer.save_metadata(metadata1) + + # 修改文件 + test_file.write_text("print('world')") + + # 第二次保存 + metadata2 = organizer.create_file_metadata(test_file) + saved2 = organizer.save_metadata(metadata2) + + # 应该是同一条记录 + assert saved1.id == saved2.id + + # 数据库中应该只有一条记录 + files = organizer.search_files() + assert len(files) == 1 + + +def test_organize_file(temp_workspace): + """测试文件归档""" + organizer = FileOrganizer(temp_workspace) + + # 创建源文件 + source_file = temp_workspace / "test.py" + source_file.write_text("print('hello')") + + # 创建目标目录 + target_dir = temp_workspace / "organized" + target_dir.mkdir() + + # 归档文件 + metadata = organizer.organize_file(source_file, target_dir) + + # 验证文件已移动 + assert not source_file.exists() + assert (target_dir / "code" / "test.py").exists() + + # 验证元数据 + assert metadata.name == "test.py" + assert metadata.category == "code" + assert metadata.path == str(target_dir / "code" / "test.py") + + +def test_organize_file_with_custom_category(temp_workspace): + """测试使用自定义分类归档文件""" + organizer = FileOrganizer(temp_workspace) + + # 创建源文件 + source_file = temp_workspace / "test.py" + source_file.write_text("print('hello')") + + # 创建目标目录 + target_dir = temp_workspace / "organized" + target_dir.mkdir() + + # 使用自定义分类归档 + metadata = organizer.organize_file(source_file, target_dir, category="custom") + + # 验证文件已移动到自定义分类目录 + assert (target_dir / "custom" / "test.py").exists() + assert metadata.category == "custom" + + +def test_organize_file_with_conflict(temp_workspace): + """测试文件归档时的文件名冲突处理""" + organizer = FileOrganizer(temp_workspace) + + # 创建源文件 + source_file = temp_workspace / "test.py" + source_file.write_text("print('hello')") + + # 创建目标目录和已存在的文件 + target_dir = temp_workspace / "organized" + target_dir.mkdir() + code_dir = target_dir / "code" + code_dir.mkdir() + (code_dir / "test.py").write_text("existing") + + # 归档文件 + metadata = organizer.organize_file(source_file, target_dir) + + # 验证文件已重命名 + assert (code_dir / "test_1.py").exists() + assert metadata.name == "test_1.py" + + +def test_search_files(temp_workspace): + """测试搜索文件""" + organizer = FileOrganizer(temp_workspace) + + # 创建测试文件 + (temp_workspace / "test1.py").write_text("print('hello')") + (temp_workspace / "test2.md").write_text("# Test") + (temp_workspace / "test3.json").write_text("{}") + + # 扫描并保存 + files = organizer.scan_directory(temp_workspace) + for f in files: + organizer.save_metadata(f) + + # 搜索所有文件 + all_files = organizer.search_files() + assert len(all_files) == 3 + + # 按名称搜索 + py_files = organizer.search_files(name="test1") + assert len(py_files) == 1 + assert py_files[0].name == "test1.py" + + # 按分类搜索 + code_files = organizer.search_files(category="code") + assert len(code_files) == 1 + assert code_files[0].name == "test1.py" + + # 按扩展名搜索 + md_files = organizer.search_files(extension=".md") + assert len(md_files) == 1 + assert md_files[0].name == "test2.md" + + +def test_search_files_with_limit(temp_workspace): + """测试搜索文件时的限制""" + organizer = FileOrganizer(temp_workspace) + + # 创建多个测试文件 + for i in range(10): + (temp_workspace / f"test{i}.py").write_text(f"print({i})") + + # 扫描并保存 + files = organizer.scan_directory(temp_workspace) + for f in files: + organizer.save_metadata(f) + + # 限制返回数量 + limited_files = organizer.search_files(limit=5) + assert len(limited_files) == 5 + + +def test_scan_nonexistent_directory(temp_workspace): + """测试扫描不存在的目录""" + organizer = FileOrganizer(temp_workspace) + + nonexistent = temp_workspace / "nonexistent" + + with pytest.raises(ValueError, match="目录不存在"): + organizer.scan_directory(nonexistent) + + +def test_create_metadata_nonexistent_file(temp_workspace): + """测试创建不存在文件的元数据""" + organizer = FileOrganizer(temp_workspace) + + nonexistent = temp_workspace / "nonexistent.py" + + with pytest.raises(ValueError, match="文件不存在"): + organizer.create_file_metadata(nonexistent) + + +def test_organize_nonexistent_file(temp_workspace): + """测试归档不存在的文件""" + organizer = FileOrganizer(temp_workspace) + + nonexistent = temp_workspace / "nonexistent.py" + target_dir = temp_workspace / "organized" + target_dir.mkdir() + + with pytest.raises(ValueError, match="源文件不存在"): + organizer.organize_file(nonexistent, target_dir) diff --git a/backend/tests/test_git.py b/backend/tests/test_git.py new file mode 100644 index 0000000..d28dfdc --- /dev/null +++ b/backend/tests/test_git.py @@ -0,0 +1,173 @@ +"""Git 管理器测试""" +import pytest +import tempfile +from pathlib import Path +from app.core.git_manager import GitManager, GitStatus, GitCommit + + +@pytest.fixture +def temp_workspace(): + """创建临时工作空间""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +@pytest.fixture +def local_repo(temp_workspace): + """创建一个本地 Git 仓库用于测试""" + import pygit2 + + repo_path = temp_workspace / "test-repo" + repo_path.mkdir() + + # 初始化仓库 + repo = pygit2.init_repository(str(repo_path)) + + # 配置 git 用户信息 + config = repo.config + config['user.name'] = 'Test User' + config['user.email'] = 'test@example.com' + + # 创建一个文件并提交 + test_file = repo_path / "README.md" + test_file.write_text("# Test Repository\n\nThis is a test.") + + repo.index.add("README.md") + repo.index.write() + + tree = repo.index.write_tree() + sig = pygit2.Signature("Test User", "test@example.com") + repo.create_commit( + "HEAD", + sig, + sig, + "Initial commit", + tree, + [] + ) + + return repo_path + + +def test_git_manager_init(temp_workspace): + """测试 GitManager 初始化""" + manager = GitManager(temp_workspace) + assert manager.workspace == temp_workspace.resolve() + assert manager.workspace.exists() + + +def test_open_repository(local_repo): + """测试打开本地仓库""" + manager = GitManager(local_repo.parent) + repo = manager.open_repository(local_repo) + assert repo is not None + assert not repo.head_is_unborn + + +def test_open_invalid_repository(temp_workspace): + """测试打开无效仓库""" + manager = GitManager(temp_workspace) + with pytest.raises(ValueError, match="仓库路径不存在"): + manager.open_repository(temp_workspace / "nonexistent") + + +def test_get_status(local_repo): + """测试获取仓库状态""" + manager = GitManager(local_repo.parent) + status = manager.get_status(local_repo) + + assert isinstance(status, GitStatus) + assert status.branch == "master" or status.branch == "main" + assert status.is_clean is True + assert len(status.modified_files) == 0 + assert len(status.untracked_files) == 0 + + +def test_get_status_with_changes(local_repo): + """测试有修改时的仓库状态""" + manager = GitManager(local_repo.parent) + + # 修改文件 + (local_repo / "README.md").write_text("Modified content") + # 添加新文件 + (local_repo / "new_file.txt").write_text("New file") + + status = manager.get_status(local_repo) + + assert status.is_clean is False + assert "README.md" in status.modified_files + assert "new_file.txt" in status.untracked_files + + +def test_get_recent_commits(local_repo): + """测试获取提交记录""" + manager = GitManager(local_repo.parent) + + commits = manager.get_recent_commits(local_repo, limit=5) + + assert len(commits) == 1 + assert isinstance(commits[0], GitCommit) + assert commits[0].message == "Initial commit" + assert commits[0].author == "Test User" + assert commits[0].hash + + +def test_commit_changes(local_repo): + """测试提交更改""" + manager = GitManager(local_repo.parent) + + # 添加新文件 + new_file = local_repo / "new_feature.py" + new_file.write_text("print('hello')") + + commit_hash = manager.commit_changes(local_repo, "Add new feature") + + assert commit_hash + assert len(commit_hash) == 40 # SHA1 hash 长度 + + # 验证提交记录 + commits = manager.get_recent_commits(local_repo, limit=5) + assert len(commits) == 2 + assert commits[0].message == "Add new feature" + + +def test_commit_specific_files(local_repo): + """测试提交指定文件""" + manager = GitManager(local_repo.parent) + + # 添加多个文件 + (local_repo / "file1.py").write_text("file1") + (local_repo / "file2.py").write_text("file2") + + # 只提交 file1.py + commit_hash = manager.commit_changes( + local_repo, + "Add file1 only", + files=["file1.py"] + ) + + assert commit_hash + + # 验证状态 - file2.py 应该还在未跟踪中 + status = manager.get_status(local_repo) + assert "file2.py" in status.untracked_files + + +def test_clone_invalid_url(temp_workspace): + """测试克隆无效 URL""" + manager = GitManager(temp_workspace) + + from app.utils.validator import ValidationError + with pytest.raises(ValidationError): + manager.clone_repository("not-a-valid-url") + + +def test_clone_existing_directory(local_repo): + """测试克隆到已存在的目录""" + manager = GitManager(local_repo.parent) + + with pytest.raises(ValueError, match="仓库目录已存在"): + manager.clone_repository( + "https://github.com/test/repo.git", + name=local_repo.name + ) From b653d4873d6ed1443b6400221697fcc3e3968366 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:31:14 +0000 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E6=89=80?= =?UTF-8?q?=E6=9C=89=E6=A0=B8=E5=BF=83=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task 7: 文件管理 API(列表、扫描、归档、详情、删除) - Task 8: Git 操作 API(克隆、状态、提交记录、拉取、提交) - Task 9: 插件系统核心(钩子定义、插件管理器、自动发现) - Task 10: Electron 项目结构(主进程、预加载、渲染进程) - Task 11: 数字人展示组件(Ready Player Me 集成) - Task 12: 集成测试(API 测试、端到端验证) 所有测试通过:47 passed --- .../app/__pycache__/database.cpython-314.pyc | Bin 0 -> 2118 bytes backend/app/__pycache__/main.cpython-314.pyc | Bin 0 -> 1984 bytes .../app/api/__pycache__/files.cpython-314.pyc | Bin 0 -> 8183 bytes .../app/api/__pycache__/git.cpython-314.pyc | Bin 0 -> 7327 bytes backend/app/api/files.py | 142 ++++++++++++++ backend/app/api/git.py | 113 +++++++++++ .../core/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 180 bytes .../core/__pycache__/context.cpython-314.pyc | Bin 0 -> 3527 bytes .../file_organizer.cpython-314.pyc | Bin 0 -> 9354 bytes .../__pycache__/git_manager.cpython-314.pyc | Bin 0 -> 11379 bytes .../app/core/__pycache__/llm.cpython-314.pyc | Bin 0 -> 6991 bytes backend/app/main.py | 6 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 343 bytes .../models/__pycache__/file.cpython-314.pyc | Bin 0 -> 3401 bytes .../__pycache__/operation.cpython-314.pyc | Bin 0 -> 2548 bytes .../__pycache__/project.cpython-314.pyc | Bin 0 -> 2926 bytes backend/app/plugins/__init__.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 177 bytes .../__pycache__/hookspecs.cpython-314.pyc | Bin 0 -> 2907 bytes .../__pycache__/manager.cpython-314.pyc | Bin 0 -> 5716 bytes backend/app/plugins/builtin/__init__.py | 1 + backend/app/plugins/builtin/example_plugin.py | 19 ++ backend/app/plugins/hookspecs.py | 42 ++++ backend/app/plugins/manager.py | 86 +++++++++ .../utils/__pycache__/logger.cpython-314.pyc | Bin 0 -> 2354 bytes backend/logs/app.log | 37 ++++ ...ile_organizer.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 53295 bytes .../test_git.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 23547 bytes .../test_llm.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 13385 bytes .../test_plugins.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 17278 bytes backend/tests/integration/__init__.py | 1 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 183 bytes .../test_api.cpython-314-pytest-9.0.3.pyc | Bin 0 -> 14193 bytes backend/tests/integration/test_api.py | 92 +++++++++ backend/tests/test_file_organizer.py | 180 ++++++++++-------- backend/tests/test_plugins.py | 155 +++++++++++++++ desktop/package.json | 54 ++++++ desktop/src/main/index.ts | 50 +++++ desktop/src/preload/index.ts | 6 + desktop/src/renderer/App.tsx | 26 +++ .../src/renderer/components/Avatar/index.tsx | 50 +++++ desktop/src/renderer/index.html | 12 ++ desktop/src/renderer/pages/Home.tsx | 26 +++ desktop/src/renderer/pages/Plugins.tsx | 38 ++++ desktop/src/renderer/pages/Settings.tsx | 36 ++++ desktop/tsconfig.json | 20 ++ desktop/tsconfig.main.json | 15 ++ desktop/tsconfig.node.json | 10 + desktop/vite.config.ts | 15 ++ 49 files changed, 1156 insertions(+), 77 deletions(-) create mode 100644 backend/app/__pycache__/database.cpython-314.pyc create mode 100644 backend/app/__pycache__/main.cpython-314.pyc create mode 100644 backend/app/api/__pycache__/files.cpython-314.pyc create mode 100644 backend/app/api/__pycache__/git.cpython-314.pyc create mode 100644 backend/app/api/files.py create mode 100644 backend/app/api/git.py create mode 100644 backend/app/core/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/context.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/file_organizer.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/git_manager.cpython-314.pyc create mode 100644 backend/app/core/__pycache__/llm.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/file.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/operation.cpython-314.pyc create mode 100644 backend/app/models/__pycache__/project.cpython-314.pyc create mode 100644 backend/app/plugins/__init__.py create mode 100644 backend/app/plugins/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/app/plugins/__pycache__/hookspecs.cpython-314.pyc create mode 100644 backend/app/plugins/__pycache__/manager.cpython-314.pyc create mode 100644 backend/app/plugins/builtin/__init__.py create mode 100644 backend/app/plugins/builtin/example_plugin.py create mode 100644 backend/app/plugins/hookspecs.py create mode 100644 backend/app/plugins/manager.py create mode 100644 backend/app/utils/__pycache__/logger.cpython-314.pyc create mode 100644 backend/logs/app.log create mode 100644 backend/tests/__pycache__/test_file_organizer.cpython-314-pytest-9.0.3.pyc create mode 100644 backend/tests/__pycache__/test_git.cpython-314-pytest-9.0.3.pyc create mode 100644 backend/tests/__pycache__/test_llm.cpython-314-pytest-9.0.3.pyc create mode 100644 backend/tests/__pycache__/test_plugins.cpython-314-pytest-9.0.3.pyc create mode 100644 backend/tests/integration/__init__.py create mode 100644 backend/tests/integration/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/tests/integration/__pycache__/test_api.cpython-314-pytest-9.0.3.pyc create mode 100644 backend/tests/integration/test_api.py create mode 100644 backend/tests/test_plugins.py create mode 100644 desktop/package.json create mode 100644 desktop/src/main/index.ts create mode 100644 desktop/src/preload/index.ts create mode 100644 desktop/src/renderer/App.tsx create mode 100644 desktop/src/renderer/components/Avatar/index.tsx create mode 100644 desktop/src/renderer/index.html create mode 100644 desktop/src/renderer/pages/Home.tsx create mode 100644 desktop/src/renderer/pages/Plugins.tsx create mode 100644 desktop/src/renderer/pages/Settings.tsx create mode 100644 desktop/tsconfig.json create mode 100644 desktop/tsconfig.main.json create mode 100644 desktop/tsconfig.node.json create mode 100644 desktop/vite.config.ts diff --git a/backend/app/__pycache__/database.cpython-314.pyc b/backend/app/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2280c697a576b97220b75c9e447329ce7f1ba0d GIT binary patch literal 2118 zcmbtV|8EpU6rb7ex7}W^2c<0@y>hJ~XG1HYmBxzZ8nh*WrdbW)2g+u9U5L{ zOG4TUCWfZfi!o|I8Za8sU)0|)*k6vnfTln6TqTCcHSm*Qn_!e5e6x2;DI_N9Z1&CP zy*IOOX5MGsb_MHY1nsZeT_-wOgnp%p--H@i{SrU{9YreMkA^Y9!wg}DS;9uCjoHr) z^Mv;>yI&X!+f#S+NIy}z0o07Jn6GPLTGgeYFtdwaPu9BeIn}*|< zS<4MqeNI))=qC+3j@^cRMl!A2Ix$ZhgSumYNpcKAOe^I8=8WX=>>F{$6=0O(Xl}@u z%$c^KWi2h4&16iHq$}_nu7TAp019Xf5v(G@91Rgx#e`ECSTpN#G|<;*B+k2%rdfK% z&@@-pv`ls)pQc#Rv{QLKUGp?(+A-61NE-HJWx?H~X%2xECpDcA+Z@jm!_hRm9%8Q2 z1-P-Ex3l(1C#NTko^d^S(y%6a^jxlILMQq-#O}_Wu@xBT37R#B2k|cYiIeP2zJ0^S zB(T1N5VzV6Ta2M7Z0-n{s%^BEAYZ3_Z%rA%8k=YGtj743A*|vPFk=!I*aN(ba_A5m zrSYF?M|?t}Q5yZJHtG?1qp`Pk{6j$9CnH2br;kshA?8_s2NKbNA$VKhmD^Dl@R< z>mUBMFjqPMY2}COD>H?aOEY&r`)VXExI&Gt{I~(MbtOA*X_UBLc3dfA5S=E@m3@w< z)0COXtYtZL$L;oJkgjN2CebFw?FeWzi_Q>;dyuI+4Nb+fGiQra#i_ZeE1S!~a4|EJ zxwP-{!3zhMBfX1}-lbsQveb7|>U+qdP+M_ocIwB+m}-w4n(cGj2ZQ)?oamCiU;$m- znb^gbHsi!jsno<#+*$|bQYTLIaHZ!(ig#ja@4*!B5(eu6r9C+DaplATSbY5f4YiM6`&7b#8`r{|`x?6}YHfp9t8 zI(rbrC*2=(FNI^vO6;Z*dnlq%v^YIG{XjtV9e@1X(6WXYZ|rh7wiu2rg*%s(&PAoO ztTf)|!SmFj*^HI z16)ZGDGp&Um%?p{ezEi-PVAOTyF`lnS(sfKz;w)BVXztKi3f$?4Nj)GfuXW70N#;! zgS*8!mDbbA<3{F8w=qe&y|Xm4Hf5MisbJHJ<_4b_mbEiBEorU{rMMez?=drFQ+~LL zUzfGKop$*NV?3X-=^w;yqr$7y^itdOpuI9*tEq|7PHTSen07t6vN`fUSDpRMSPBbMCS~L%K>eApCHdYsBpsv&=hYlQ$Xq`ILh6t^N zAwC@IV_Iwp@4_2P*VDDrAwDYWvKH48B|%fQu@VV-a!C#u=v}%tca$As?bs!Z18-U? z4f`ubK1*;wvBM9(%7=MMm=K4egVUEW@zPdpb4I67@{REN9K*M9Z;Q z!&oT_zPxJLrss=m7Hjg@4B`XaXA%4UDHL7!3~mXRzW^}MHVGe-7SYH}oC#Z*pm)Lx zzQz~zPKWZ`;hB+Dm$r50@l8+qQ{{Uuz2kNCrdeLmn|DmdC?mPt)-9*hx$mouhVD2n z)0x?5&@`^$h3#cgbm4ChNqmosZ7O2bHE#SS41E7-$if|rg7EH;dMsI{tz^{ zjm!4;_r|YMY)=F^_tH~w@?mm{Y?4Xoi8@7!k}orh*`}}DHK}L0 zjxQVL%KBQc--GcMW1Z!4*>;<{-Euv)d~so6;Wg&P^Gg>$^~pT$H>TURd#~YAYt3>z zKOL$~Y8s})EZz2ojJC;It}#+urf#4MJ}EcR$x(|SdW3ynLlH(&gTr*!1_eFhBRE

8$+Guc^pWtW^svm%6 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 SZ7R_RVI8HKZ^1;30RKPD40~Vz literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/git.cpython-314.pyc b/backend/app/api/__pycache__/git.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3caa0c65c26a1a7c911fabc59cf93b5cd5c66f65 GIT binary patch literal 7327 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 JEv&J(^BQJD%=uezIfM z)2@Xt8+O0gH}Tnug-_>B_tRv&#U3A@lAjzOzmnlI$c$U+`sMjW*~JBk$*KBDiOJci zc`5pd1qJ%a`9-Pv@$s2?nI-Y@dIgoYIBatBQ%ZAE?TXld`aljS<^U2Om>C%vZ!^dh Hu>d&$m}E4P literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/context.cpython-314.pyc b/backend/app/core/__pycache__/context.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0553532d593dc8cc4087567caa1092f8db6938e0 GIT binary patch literal 3527 zcmai1Z){sv6~FI~?dLy9+%{R7C5_#t%4#y|LNl5*T^OQF4r@Yqflxux$E*F)nc2_g zzH7Ejs^q|6-O!d6p{+$iwBMEw(C!N>TP1{q#3!U#3$(pox{4;*zS%KS#y8Hn&o6Z{ zGp^$E&O7(s^UgW81w5xKl-~$$iMK#8@>ipe+if}(Fr4Gh%SvwDVfPB zg()eOsWFtv>exj@0ME4ELuFm0xY8GnD`e8Qk znc1+e4Uj~z)CJ2|um9=e>%V<)^R)*zf4TPNE9-x_wtn#!35BbtvJT_w$!wZ&sd{vP~na-+^`Y8EKMd<8i0AO z*o7CUCb$wyFOxcM;)BT-3iNrWXr`^?Su_2-WoMFRv6xI3s0GAk){E?Lae-?ma%TQ) z#vJ3(h9`$TNd)vbyMUC*E$z>d_ac8i{kPe_%&v^+v;(Be67R@Z^JXc>jCnK73Uncj z_BCuI=m~mX{~?eQQkl$(U^sY4CD`mq4yGcp2y5 zryP|Ze-xHB2CPtV1;oSm3C5GFw2-rSKsabK9z+`2x+GjJFwvfLaCsXc!LaJy51mR39-6)3yr?kY^aY(~DTM6wk!@Dv(cKAwMNG zKMA)lj@JT2l4=@h-?ezE7DPToqI)aqbS;c85z?{e($Oo*#gW(Tih6vb4TK`bv>g;p zCL&xj4BO1(M?;2@FJwwNIW^f2MAZu zDT*QFyPB+?FWIn;cY(X$TQ+T&jM41b60;l-!^*9Iq^(@eFqS2{4|F(q)=!JjP^>zF zhydWJKmf|*z8ZxX^u{Yk^p!`a%j0)r2XB05C3d`=+C(pZMqEE}cN%_M1J49Uy$EDm z_C~j7@0?$kw{kadrjxnZs$jN?oQ<-e$X}n3Kv~K~Sx66bLk2U#2Bb-Nw{ zG==$|uNUl}Sk!@N6l~xzUrGr39aW3&0am3UY8KhxKxSbX_;)2fN!jZ)ow)B_P%k?69|d(IM*iODH8Qb8GLuzxvu$w|v84Y@LJ1l*xoo02@n) zsPzu_Ln(!d%9WgD^KjmL(RdE-EU179A6FeKH@`y#SMts|2O~2$IQV$nEhb?zlQ9}9 zd92l8HZ2fm`F&7OCJ)-XE)U-vS!q9cLAj$vmfBuzyRG$Aw7$#JE86~l`$?qZ0$V!& z>iG}D`>v|D<3}p-BP-z}clzSLo47LZ_VDeY(aO;12Va1{6CdZY26E%fIzT`py zL*#`C4aVcHKLh0RpbT(ENF$)-?IpUoTqh+6xGbQ~{nTP5YD2IQ*KQsDzmTV2gtc+_ zIs1VKRQr6P8Ub=GvpoEc^3L=P?e}B<7(6ay4;_NlVj%Xwp-7zpgE!KYE8j+=!3Lr_&soev zlw-hkX5?~teyA0+TCI$uzA+$WQq{=bzPHZ*>U`a&L__zx4_;KN3Tf}WIQ`b?OQ$a{ zyghJzXnE+v$XD)mAFSb$Dp7)=|7;Lo9gNA+z!Rz{i@=M4$1FmB9GL$V`YA-)j)7j{ zh(W`K_z_ij?dH$dm*3m!9VY=+7$fdW=PZ*#`NRN;>uJ!vo}R+lo>q|%J08R3yMdI+ zM_x+xRPMvm`3~1bH9J!#>tibC(Efj-96=JH&wX2#0|E&8}h!2$-mJq`}#MN zP5lIFg1$+Z$Ng-fn6=L}RT?}&9^U$=!g45aP+Y LVq!xEPJHLTimKv( literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/file_organizer.cpython-314.pyc b/backend/app/core/__pycache__/file_organizer.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b5a7566f1a4a329e6409de9ddae8c71d31b10fe GIT binary patch literal 9354 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%0A`GhV(k_VBrb$7pUlLj1VW=N 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 literal 0 HcmV?d00001 diff --git a/backend/app/main.py b/backend/app/main.py index e5e7414..56b605a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -4,6 +4,7 @@ from app.config import settings from app.database import init_db +from app.api import files, git @asynccontextmanager async def lifespan(app: FastAPI): @@ -41,3 +42,8 @@ async def root(): @app.get("/health") async def health_check(): return {"status": "healthy"} + + +# 注册 API 路由 +app.include_router(files.router, prefix="/api/v1") +app.include_router(git.router, prefix="/api/v1") diff --git a/backend/app/models/__pycache__/__init__.cpython-314.pyc b/backend/app/models/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a29ce3ecc54a6587d6810af15c6b649144ad42fd GIT binary patch literal 343 zcmYj~(MrQG6oyZlc9rQSsQ0@N#4>C_@J_sx71T)|z!2Lg-D(rkhGFoc2<0LJjRS zP5nt=f)T`!1yf>rnSTsX@5ccmV)w4gO56&=3vM{h;k}kKksG&~DxtY4Wql)`?;@%x zMzrr*;KHHZ=SsydvJh1hKOv`kVGp)kwWf6`d$hHX8+OOCA!EF%82f}3qQOSVo7D40 z@mwU2JfDlYNHCqOKT8-Z>(Ve5t9QF|iRNo1uJizt1W$8_?UxclS~zXtq=mC(|KcOm HyXgM`Wm{O= literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/file.cpython-314.pyc b/backend/app/models/__pycache__/file.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4e69a10500b85296706c75375a9829f2c5d7101 GIT binary patch literal 3401 zcmd5;O>7j&6|Vl7nVx@-{{jE4XEDsOYoo>3WHFHN8YdY=jwrS|;8LDft z*iocbHpwanq;PN~4w05qAS*?VB1MjJNRDU~DU32!)-A12l!Rz=+F7Hpx8%K^ACE1B zw3jF)^{cAyy{fKP@2B3OL_9*EUH<2xn?DK=@;98+8_fN-Re;VV*NMzckt|^(%Q3DW z=lm3(6`0_Ug()!`V1aCq1+yU*%1TVihFRF>iBpkmltr^K7W3zUsdzTQ68)ruyhh|; z7m-8riBX~YJC>BCG|7ZN!)b>1Bwb|&GjoO~AzjJaMuzjGO9f`;ZPm&Iyxlo}UHP*&_etZX(cnOyDv6dZjsVWAx4L9~{ z^|3ZN(uB49Sce=1+igpB*3cNBarhDqeY>ubBb=PpsbG4wn1nld_R|mk{=47&52cWN8DNP=67axffH}?cP{~pC@V_plZg*f~wszY<&bAbtIS94a*rpt%nQCo-|>pxdmN4>2)+;)o>l|fW5Zb zfw<(4$Nn_&@kIIBxBqfYFJ05i(+2GU#{WWF+66zN-AMK!!T-`;Bz;Kwfpp?v0LlN^ zp`0UdD3?4ZWSRR0{{p*n{1?>@z)Aihf|I<=SE`*ad4>1&$nX>`;7NJASitn9VHT)m z0)ifdF0eHKWP)?aRdSs__5(t6Kzqn#?gl4wSGaGH5e}k({|x25AT?N#VxkfpM$xwy z^E9V2FRmz>r8mT#xPik>h>l?`I(V+3s=|_@P@_OW*_~lghkZo{K;9*v zqy|46{rUKV@eeZ(Ppr59zVEkvPmX0D3r~_4o=6wAHLhzd!YbcrzX0UrMjIh$H2AnT z_kwh-9p`0k2*x!Yzl+Def+xI!CpPf}*n7vmyZ#aQ3bMhkARF2xyF>3?XONm?cgVs0 zgqwKcu6282sN5RQEuHtYyWcH|vy~_Uu?BFa5=S7>0LCgw1X@V41Ia0e(<7mDCJ0E1*BachQUDGC%Qz87H5`Fi!C#M*}<@txpf7=RPFWK-Pp zNqgFz`DmE&b8)b@q6)+&k+|Z6kWQ_e_00_|ZFm?j3{K(2-wF zt{?gJ^yAmdLzmp_=HSShxX#LhXWg@#nXxtY2fdt`cBeNF9$i~{ES3+>xaT+f4n6$l z`sH%pDHspD^>AsuSRObBGU>x>-K8@|IlTys+61h5o4dlpv-tWP@1B1l;=A&>|E|19 zgNfg-)dQ&9#Y~GI3)Olk2oVY8d^J!a&TUJzv>QflxoR-qIR4H1c0p@+iEW`_coqGK z^9}9Apm-X_Dslm;dm0B}|2B8kBT!FO-RdO003vw*4yKLI1&-sMk^`TTeNV}$4RY!! WIs7Tef7Uh%FY}B literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/operation.cpython-314.pyc b/backend/app/models/__pycache__/operation.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4560396c10d9cf65bf50a75970cee15aa428d84c GIT binary patch literal 2548 zcmZuzU1$_n6uz@FyF2@zHJe0ZTk~U+tkJA>V`2#n#ITNfJhq9OHK3-ZA1x@{G52endz* znKS8PuB4l}lOhw79_FzzVZ@vCF<;Wp{Px~C5=aJFa2IJIokViA5y?Fj9E9iNVj)TF zCQ;85+(aFgs47fjnXG1sC>QcMEy`Kq$pXvdbBZ2yTCQJ%ePD3i2Do#`v5tFcxa6wC-5a>KhEGRo zcDC=$R@VUw)oM)nxz1XOc2&0 z1%dvMBLyt_3N({d=)8PR zn}@A4lT)>endd+PCM;Lfb!A3xq#1J#KXL6iC?+{wE@+L8cvtz9d8i>ucmg?kp#9}0EK{vOft$t@*UUCAZ-Ku_9)`QF4gN56edt>B#-XKS4+e)zgTu@5nl@%6sQ6IYUnH-}Xpi!LW zmVk|64~3|McA~ipM17yoZnW+Ofn{xc1LEaZ6!ir1U323Ep7B|)kL&>AFx=F9O*;1;ih(9lGJk;}=&)RnQ7KzB(< zR>ElAMk4Kteb?hx_pLPdm4wksGg>1g+;;imN>fKkn0?j)7Q2=xw&z&1)q`Z^Ml<=G51n4ZaLxjevbSyuCDP+(XvoW@m0isQXXy9+hsp6)|| zoKyTppaUq77aBt$qS%jO4+tw0PUOB& z?jJQrSG)HwZ7;p2mAhxn<1Yjpd{6Dc_hJX|{spNrkMA=UZL&+K`j_H@!2(|%)%x-p z=XFI-pVhMS)Mn8p%jU_0;UJ4vaMNi3#bA5y!1pp~C-URWK=Q!>$>3u|YJ|>A3l6Xw^KO(O`7XqBdeLMIc!K;1D|8ki@ A&;S4c literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/project.cpython-314.pyc b/backend/app/models/__pycache__/project.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e43ff47dde4b472db876a8b5b4662bbbada3f24 GIT binary patch literal 2926 zcmd5;U1$_n6uvV%yF1yxWb-Ta-xzg`O~k(hOhRjI+!%~@62!J9)5*+iGG=ymbLTFa zJPrM!?Sl_O1w$!q=!>CXQ~F{b3O<%nsG$ip10tml;+tzi^{qW;W;bq9DfFduV7@u? zopbNp^K;Lg)!m{iA;05G`3jbVl{^Rz86gtaN796mG{?B@_|Et7=>Q9O za-dI02U##3Vxe@Hh0`Jv(^ag>(+Pc%bd*KYF&6XQgMIOIf+e<-2C|b#p(Y}QhZ9Ev z{`Xk5BsP;&)gydO@vf*TOlL+`cSXeMoTaBYS3H|%M$S^qRL~6#GHO_3t}vuuVXk-* z?1#`k+-EU;OsB4JdhqN3Ocgq#EYL`D5DX?rJPQtoz=eP$bbuXRER4*sxRZ?hd8EXFJ|G1<$_`d}77^e= z9ein2v^+`*SB!eDkNsU=wqtQmmsmGnwa3*+A_TW;?JLO=8v!;7cWiAcj7Exnkbe z*h3K3Xr3Bbg-*#6`V@x0U>TDI-3=O+reDD%S_RWoMoryVg-ET7KgJ4fapg1!4!P)u z3GiGgVrv8lJIO^TUjI5*&Rm&*XE{>MWR4q{6DUsZ2CUp=io@JMfvIz31qZnM+Artm zgq>GZy?sV+%T%(Xn$qLel%mmAW|A<|Ne(L&LFe3-A1TL0h<>E%xhlGbq6Mu%>5}g3101w`n7MlQto|gn&z+TM)J)Yy+r8VLJjU z|3{0m55syKvP#Gl_bUGsAFuOI{0ES|`~Z@@yu=s%hi5s+`*G&!4jAr=IjfMzMxx@D zENx4IrG&MxvIC%tbI5rz!gsz;hz9Hy@&-4~N!%d!HfiUe?Dxpi+_|!Y0u1uxy>={ML0{)MejVY_ZlSWmD*hu;(`zf&oLZK%RFxIq{G2c3vIJTqwv z2=rxyHUKxO>9$G@@2GUcV+NB8)N~_ePE|}fuds19q~@%QG3G{8szY^ZG8A&e-zq4l zLNtmK1q_SPy*Muxo-1IDdu=qu(|t&%04htK?nky2AdH$>fM*|)cS%tsRn;HcSECEU z(P9#lXqoV?z%6a%wU#X4l`(d^1n&5i{jQ*1z{vCKfz2oCX2 znt=2Q(QA+csU{aH8;c$)J0i=_{tG6`u#+bXidoWB%W}q`HZu)Nw{oBk%d(bJL4an! z@MQB1DV~#lUP35gawrCdVsoQM5RM|?-xxiHAR=@ibRrxCaFZ1cBU7;OI}Oljpsg4x zBn$q6Mhi^0fdaNY!{X=bzdkHkF6JKEEI%_N1wp>bRbrqF#i(;CeBvLN76{ zZsNI78?F*xGe;CN6*;XjbZafBC~^@k-K`Fs;7Yl(C{AP4W0t*b(U>-k9ggTTWpt-k=(S7E9E literal 0 HcmV?d00001 diff --git a/backend/app/plugins/__init__.py b/backend/app/plugins/__init__.py new file mode 100644 index 0000000..e594338 --- /dev/null +++ b/backend/app/plugins/__init__.py @@ -0,0 +1 @@ +"""插件系统模块""" diff --git a/backend/app/plugins/__pycache__/__init__.cpython-314.pyc b/backend/app/plugins/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3079557c8a8b3149519418319cecc8cc373d173 GIT binary patch literal 177 zcmdPq^MASDe9K@24fL5#`_noOlaK!Iodlb-C}_I&g1 z=ey@WTe0xz-06OrjJMe1<5TjJ2KczG$)vkyQs1;;;F$a+Nz|6?Vc$-0@hy}<2 E0Bid)8UO$Q literal 0 HcmV?d00001 diff --git a/backend/app/plugins/__pycache__/hookspecs.cpython-314.pyc b/backend/app/plugins/__pycache__/hookspecs.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9dc2bb3b9a7b0228749cf631bc38d5c010ead451 GIT binary patch literal 2907 zcmcJR-D_M$6u{^1_hz%3q-`3tU-{7J8tsaSP%wzl2lc^Fw~!Y}hTFS0$u)QPZf9mI zeUU|jvDn&Ji$VpV5NNm1N)ReZ5bb{;`XC)&1hZ=jJ|rxfK6=jFyUA`gB~Xcrwp--?A$N0NQIxyt(#)Z;aA4vxPdJO87iYQ6#=s983FXi_t)gPdsgY9EPXkfo@JSP16i37V3fwABy z8Pw9gE@DT}2D^R88zw@YE}(S523^?Gg_JJbpo@6Ah|)zHbWu;&qI9taUCUT7-P%Pq z*N>&+qIHyrcsc|4>mz(;HInnAcnOf871SsV>5(g zp!FbmOPkWt+DYwga1cl>>WMkg%WqFd4{LhDiEk zXu@GxUC214h7HKEnNp79dy8@)^{&IFd0scn)G6JVHglPjp3kT9%4nXN0<$@o&5!2K z%2rDCT+R`?Fe#M>8aH;dUfBk-^8+C67s&?k1>@@|N_*GvvlvjdDyWH3b2nCy2-I7v zAt*r5Vlep%DwsZ>gmJ6Q*| z?g6n6-W}NWx$O2ScCJwm!*1pKTb0X;4eVGaF1a6Q`1Ara4!9>_SMKKI!DiN5T09Gn5VTegI+AD387pHl83ezYHeM-;FbB4XSO#pk zuXhK1829)f&sQNPeZ2bhx0QwW%jYg^T>~nVYqXz1NJ$jW zq9u*A6F}Rxvy_>q3nmxxP&4j2^&y<){|RiiX{L-YljofrH(4L7w>x0$)eN}gS7aBC zyMPD(J>c?Z7s^YQ%X6RX3iyR)zLDbl=wNF;KQ4h8FegeCY7js(f?3{Nef4 z*&FUZMfub3R+i^S+$hl0nd3OvJ7FEnoP@|VnL1;7F2iN_Hn%tRcp8RpnWbhPNhV~3 z(wv^f^w&Zu9Fqd%Uy6zGbV0X0O&f)T!?>`moSAb#9i}wn7*L4R(kW91Sw+`VxX|+` zUPSQ{2-yh<8nUcqP+bUSohk^E!|;x}6mFQdO=(hd6A5niRX2|WaTXMqZTNd2?$1Il zyBp~JEg0YMLH@1!Nhq;~rL_o@9(CLL<{E!vNu>m3P%_M326JR64?h=^XJrEaiSXy* zO`T1fj18kwHCoxc?KZl|u&$BN2z;TH+zouz-$>Co~d@V#; g_xuv;{wdb|W6$8vv7usQho+6t5iMRNAe6cP03rCTjQ{`u literal 0 HcmV?d00001 diff --git a/backend/app/plugins/__pycache__/manager.cpython-314.pyc b/backend/app/plugins/__pycache__/manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2e3c79f6f937a873d794ea154aba54535a65e2c GIT binary patch literal 5716 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 literal 0 HcmV?d00001 diff --git a/backend/app/plugins/builtin/__init__.py b/backend/app/plugins/builtin/__init__.py new file mode 100644 index 0000000..5a43d2d --- /dev/null +++ b/backend/app/plugins/builtin/__init__.py @@ -0,0 +1 @@ +"""内置插件模块""" diff --git a/backend/app/plugins/builtin/example_plugin.py b/backend/app/plugins/builtin/example_plugin.py new file mode 100644 index 0000000..0ef1c27 --- /dev/null +++ b/backend/app/plugins/builtin/example_plugin.py @@ -0,0 +1,19 @@ +"""示例插件""" +from app.plugins.hookspecs import hookimpl +from app.utils.logger import logger + + +class ExamplePlugin: + """示例插件""" + + @hookimpl + def on_file_created(self, file_path: str, metadata: dict): + logger.info(f"[ExamplePlugin] 文件创建: {file_path}") + + @hookimpl + def on_git_commit(self, repo_path: str, commit_hash: str, message: str): + logger.info(f"[ExamplePlugin] Git 提交: {commit_hash[:8]} - {message}") + + @hookimpl + def on_llm_request(self, provider: str, model: str, messages: list): + logger.info(f"[ExamplePlugin] LLM 请求: {provider}/{model}") diff --git a/backend/app/plugins/hookspecs.py b/backend/app/plugins/hookspecs.py new file mode 100644 index 0000000..20d4439 --- /dev/null +++ b/backend/app/plugins/hookspecs.py @@ -0,0 +1,42 @@ +"""插件钩子规范定义""" +import pluggy + +# 创建 hookspec 标记器 +hookspec = pluggy.HookspecMarker("ai_dev_assistant") + +# 创建 hook 实现器标记 +hookimpl = pluggy.HookimplMarker("ai_dev_assistant") + + +class PluginHookSpec: + """插件钩子规范定义""" + + @hookspec + def on_file_created(self, file_path: str, metadata: dict): + """文件创建时的钩子""" + pass + + @hookspec + def on_file_modified(self, file_path: str, metadata: dict): + """文件修改时的钩子""" + pass + + @hookspec + def on_git_commit(self, repo_path: str, commit_hash: str, message: str): + """Git 提交时的钩子""" + pass + + @hookspec + def on_llm_request(self, provider: str, model: str, messages: list): + """大模型请求前的钩子""" + pass + + @hookspec + def on_llm_response(self, provider: str, model: str, response: str): + """大模型响应后的钩子""" + pass + + @hookspec + def register_commands(self): + """注册自定义命令""" + pass diff --git a/backend/app/plugins/manager.py b/backend/app/plugins/manager.py new file mode 100644 index 0000000..533d4bf --- /dev/null +++ b/backend/app/plugins/manager.py @@ -0,0 +1,86 @@ +"""插件管理器""" +from pathlib import Path +from typing import Dict, List, Any +import pluggy +import importlib +import sys +from app.plugins.hookspecs import PluginHookSpec +from app.utils.logger import logger + + +class PluginManager: + """插件管理器""" + + def __init__(self, plugins_dir: Path): + self.plugins_dir = plugins_dir + self.pm = pluggy.PluginManager("ai_dev_assistant") + self.pm.add_hookspecs(PluginHookSpec) + self._loaded_plugins: Dict[str, Any] = {} + + def discover_plugins(self): + """发现并加载插件""" + if not self.plugins_dir.exists(): + logger.warning(f"插件目录不存在: {self.plugins_dir}") + return + + # 添加插件目录到 Python 路径 + sys.path.insert(0, str(self.plugins_dir)) + + # 扫描 Python 文件 + for plugin_file in self.plugins_dir.glob("*.py"): + if plugin_file.name.startswith("_"): + continue + + module_name = plugin_file.stem + try: + module = importlib.import_module(module_name) + self.load_plugin(module_name, module) + except Exception as e: + logger.error(f"加载插件失败 {module_name}: {e}") + + def load_plugin(self, name: str, plugin_or_module: Any): + """加载单个插件(支持传入插件实例或模块)""" + import types + + # 如果传入的是模块,查找其中的插件类 + if isinstance(plugin_or_module, types.ModuleType): + plugin_instance = None + for attr_name in dir(plugin_or_module): + attr = getattr(plugin_or_module, attr_name) + if isinstance(attr, type) and hasattr(attr, 'on_file_created'): + plugin_instance = attr() + break + + if not plugin_instance: + logger.warning(f"模块 {name} 没有有效的钩子实现") + return + + self.pm.register(plugin_instance, name=name) + self._loaded_plugins[name] = plugin_instance + logger.info(f"插件加载成功: {name}") + else: + # 直接注册插件实例 + self.pm.register(plugin_or_module, name=name) + self._loaded_plugins[name] = plugin_or_module + logger.info(f"插件加载成功: {name}") + + def unload_plugin(self, name: str): + """卸载插件""" + if name in self._loaded_plugins: + self.pm.unregister(self._loaded_plugins[name]) + del self._loaded_plugins[name] + logger.info(f"插件已卸载: {name}") + + def list_plugins(self) -> List[str]: + """列出所有已加载的插件""" + return list(self._loaded_plugins.keys()) + + def call_hook(self, hook_name: str, **kwargs): + """调用插件钩子""" + hook = getattr(self.pm.hook, hook_name) + return hook(**kwargs) + + +# 全局实例 +from app.config import settings +plugin_manager = PluginManager(settings.plugins_dir) diff --git a/backend/app/utils/__pycache__/logger.cpython-314.pyc b/backend/app/utils/__pycache__/logger.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04790a514690a18a2244cbf124d2f8d9d7c6850b GIT binary patch literal 2354 zcmcIl-ER{|5Z|-!&S%F-91hKtdu9AV5OE8$tr{A5=-OWOpG_MM+!UQU!_E&YpdCi0DHfI;Y#6 z-I<-8nVtP@Z&RZO0m;vLzx;(m=yy7>iLEm0SzroCL>NsWkr`z$WAzT~fZj3c9APmV zq%!9yH^O7y;@MHxh=2u)=SJNl9_$Gs(IfI5NOX0etS`&phGPw3BnqN?&{5e7dqrj^ ziZdHD%jWc$C@|D zY3Xbt+mql%J58IEU>HV|G+P$Mx7b|0x9#Z8g!7FN)l}c)rM>z!FWPELY7ATnMs$eI zJ=MJGEbP`%6W5poTep~S!~-=!jY&8+XfU;mp0hH#%~ne=wwBH%xOkU6U+?3=I&X6# z3m<3u2r-3iNrDsIQ=G_)u07TJq4Np;f6bRri@*gty(w2NTO)}6C0G1L4Y;|UydNTZ zapwC!AKtD&(OwM9oltDAGf(ca_a5CzR}laZWK!DGk#3 zmdF?Vkz9X74n;{n zvPyEM0{^7No~VE)-O2)99i9*3rSMx*ZL&r+J(F7R?haXGG}B$ElX*Q^*~{pIDGN(m z0ow}bd1G6tamRAwj*Iwl z@~C4+sl9i(z4z9EJBMx_dfdLJFi~b(OKjH?+jaS~E5|M!E3&;$JArTg3&)KFm-ck!M3E_o-qVR56XetSTB_VLJuPE&Lt)ZnbW&l_t z;*&B-SvirEOcSbDQt`YO@@LYwY5}0y8MU(DKJK+060k`Qteq z>bmka`se|`XrOw?rd1EbLrubQPiN9rIa&u~bc3;*$nvFfh{A_CP0nYPea0Y&=(A+T ypjmS=4D&16@&q+KL7o-Vxq`N>pm$bK@D)!V$wLhCv@f>z+!%Q5{@`ogGWZXMx%UMC literal 0 HcmV?d00001 diff --git a/backend/logs/app.log b/backend/logs/app.log new file mode 100644 index 0000000..5523270 --- /dev/null +++ b/backend/logs/app.log @@ -0,0 +1,37 @@ +2026-06-12 11:04:39,190 - ai_dev_assistant - INFO - clone_repository:49 - 开始克隆仓库: https://github.com/octocat/Hello-World.git -> /tmp/tmp35gqud8h/Hello-World +2026-06-12 11:06:53,620 - ai_dev_assistant - ERROR - clone_repository:69 - 克隆仓库失败: failed to connect to github.com: Connection timed out +2026-06-12 11:06:53,688 - ai_dev_assistant - INFO - clone_repository:49 - 开始克隆仓库: https://github.com/octocat/Hello-World.git -> /tmp/tmpcfp5244o/Hello-World +2026-06-12 11:09:08,788 - ai_dev_assistant - ERROR - clone_repository:69 - 克隆仓库失败: failed to connect to github.com: Connection timed out +2026-06-12 11:09:08,812 - ai_dev_assistant - INFO - clone_repository:49 - 开始克隆仓库: https://github.com/octocat/Hello-World.git -> /tmp/tmpmwk5op6y/Hello-World +2026-06-12 11:11:23,957 - ai_dev_assistant - ERROR - clone_repository:69 - 克隆仓库失败: failed to connect to github.com: Connection timed out +2026-06-12 11:13:10,133 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 83dc1427ede21ecf5684da9f794533947351494d +2026-06-12 11:13:10,161 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 432ba4312b1cbd534744ae3663eb1f4f07756492 +2026-06-12 11:22:44,517 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 4 个文件 +2026-06-12 11:22:44,528 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 1 个文件 +2026-06-12 11:22:44,584 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmplgnv1sum/test.py -> /tmp/tmplgnv1sum/organized/code/test.py +2026-06-12 11:22:44,597 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmpft2z07tw/test.py -> /tmp/tmpft2z07tw/organized/custom/test.py +2026-06-12 11:22:44,610 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmpmrxuetr3/test.py -> /tmp/tmpmrxuetr3/organized/code/test_1.py +2026-06-12 11:22:44,623 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 3 个文件 +2026-06-12 11:22:44,644 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 10 个文件 +2026-06-12 11:30:02,527 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: test_plugin +2026-06-12 11:30:02,529 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: test_plugin +2026-06-12 11:30:02,529 - ai_dev_assistant - INFO - unload_plugin:64 - 插件已卸载: test_plugin +2026-06-12 11:30:02,531 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: test_plugin +2026-06-12 11:30:02,553 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: test_plugin +2026-06-12 11:30:02,560 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: auto_plugin +2026-06-12 11:30:02,562 - ai_dev_assistant - WARNING - discover_plugins:23 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:30:02,565 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: plugin1 +2026-06-12 11:30:02,565 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: plugin2 +2026-06-12 11:30:02,570 - ai_dev_assistant - INFO - load_plugin:55 - 插件加载成功: error_plugin +2026-06-12 11:30:34,435 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:30:34,437 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:30:34,437 - ai_dev_assistant - INFO - unload_plugin:72 - 插件已卸载: test_plugin +2026-06-12 11:30:34,439 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:30:34,441 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:30:34,444 - ai_dev_assistant - INFO - load_plugin:60 - 插件加载成功: auto_plugin +2026-06-12 11:30:34,446 - ai_dev_assistant - WARNING - discover_plugins:23 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:30:34,449 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin1 +2026-06-12 11:30:34,449 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin2 +2026-06-12 11:30:34,451 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: error_plugin +2026-06-12 11:30:49,377 - ai_dev_assistant - ERROR - get_git_status:60 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:30:49,386 - ai_dev_assistant - ERROR - get_recent_commits:82 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent diff --git a/backend/tests/__pycache__/test_file_organizer.cpython-314-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_file_organizer.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3dbb71ad8bd89521569117ef34d2a6164c54e877 GIT binary patch literal 53295 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 BYC`}3 literal 0 HcmV?d00001 diff --git a/backend/tests/__pycache__/test_plugins.cpython-314-pytest-9.0.3.pyc b/backend/tests/__pycache__/test_plugins.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2f604c001e0034d35cf0a8c88f1b167300bd553 GIT binary patch literal 17278 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 literal 0 HcmV?d00001 diff --git a/backend/tests/integration/__init__.py b/backend/tests/integration/__init__.py new file mode 100644 index 0000000..45c3345 --- /dev/null +++ b/backend/tests/integration/__init__.py @@ -0,0 +1 @@ +"""集成测试模块""" diff --git a/backend/tests/integration/__pycache__/__init__.cpython-314.pyc b/backend/tests/integration/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0dcdd2b2635cf956f5eebe114cfe1593196d6fe GIT binary patch literal 183 zcmdPqbB{5=%1k^Yr86GxIV_;^XxSDsOSvlPLxuKW?NPJ*sWMsU} KAW_5uo%E50W8Ou7Jsv1C>AW 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 literal 0 HcmV?d00001 diff --git a/backend/tests/integration/test_api.py b/backend/tests/integration/test_api.py new file mode 100644 index 0000000..4f3f69c --- /dev/null +++ b/backend/tests/integration/test_api.py @@ -0,0 +1,92 @@ +"""API 集成测试""" +import pytest +import pytest_asyncio +from httpx import AsyncClient, ASGITransport +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from app.database import Base, get_db +from app.main import app + + +@pytest_asyncio.fixture +async def db_session(): + """创建测试数据库会话""" + engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + async with async_session() as session: + yield session + + await engine.dispose() + + +@pytest_asyncio.fixture +async def client(db_session): + """创建测试客户端""" + async def override_get_db(): + yield db_session + + app.dependency_overrides[get_db] = override_get_db + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + yield client + app.dependency_overrides.clear() + + +@pytest.mark.asyncio +async def test_health_check(client): + """测试健康检查接口""" + response = await client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + + +@pytest.mark.asyncio +async def test_root(client): + """测试根路径""" + response = await client.get("/") + assert response.status_code == 200 + data = response.json() + assert "name" in data + assert "version" in data + assert data["status"] == "running" + + +@pytest.mark.asyncio +async def test_list_files_empty(client): + """测试获取空文件列表""" + response = await client.get("/api/v1/files/") + assert response.status_code == 200 + assert response.json() == [] + + +@pytest.mark.asyncio +async def test_git_status_invalid_path(client): + """测试获取无效路径的 Git 状态""" + response = await client.get("/api/v1/git/status/nonexistent") + assert response.status_code == 400 + + +@pytest.mark.asyncio +async def test_git_commits_invalid_path(client): + """测试获取无效路径的提交记录""" + response = await client.get("/api/v1/git/commits/nonexistent") + assert response.status_code == 400 + + +@pytest.mark.asyncio +async def test_file_not_found(client): + """测试获取不存在的文件""" + response = await client.get("/api/v1/files/99999") + assert response.status_code == 404 + assert "文件不存在" in response.json()["detail"] + + +@pytest.mark.asyncio +async def test_delete_file_not_found(client): + """测试删除不存在的文件""" + response = await client.delete("/api/v1/files/99999") + assert response.status_code == 404 + assert "文件不存在" in response.json()["detail"] diff --git a/backend/tests/test_file_organizer.py b/backend/tests/test_file_organizer.py index 69d034a..a7bb6cd 100644 --- a/backend/tests/test_file_organizer.py +++ b/backend/tests/test_file_organizer.py @@ -1,8 +1,27 @@ """文件归档管理器测试""" import pytest +import pytest_asyncio import tempfile from pathlib import Path +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from app.database import Base from app.core.file_organizer import FileOrganizer +from app.models.file import FileMetadata + + +@pytest_asyncio.fixture +async def db_session(): + """创建测试数据库会话""" + engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + async with async_session() as session: + yield session + + await engine.dispose() @pytest.fixture @@ -12,15 +31,9 @@ def temp_workspace(): yield Path(tmpdir) -def test_file_organizer_init(temp_workspace): - """测试 FileOrganizer 初始化""" - organizer = FileOrganizer(temp_workspace) - assert organizer.workspace == temp_workspace - - -def test_classify_file(temp_workspace): +def test_classify_file(): """测试文件分类""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(None) # 测试代码文件 assert organizer.classify_file(Path("test.py")) == "code" @@ -42,16 +55,17 @@ def test_classify_file(temp_workspace): assert organizer.classify_file(Path("logo.png")) == "image" # 测试配置文件 - assert organizer.classify_file(Path(".env")) == "config" assert organizer.classify_file(Path("config.toml")) == "config" + assert organizer.classify_file(Path("settings.ini")) == "config" # 测试未知类型 assert organizer.classify_file(Path("unknown.xyz")) == "other" -def test_scan_directory(temp_workspace): +@pytest.mark.asyncio +async def test_scan_directory(temp_workspace, db_session): """测试扫描目录""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建测试文件 (temp_workspace / "test1.py").write_text("print('hello')") @@ -64,19 +78,20 @@ def test_scan_directory(temp_workspace): (subdir / "test4.txt").write_text("test") # 扫描目录 - files = organizer.scan_directory(temp_workspace) + files = await organizer.scan_directory(temp_workspace) assert len(files) == 4 - file_names = [f.name for f in files] + file_names = [f.file_name for f in files] assert "test1.py" in file_names assert "test2.md" in file_names assert "test3.json" in file_names assert "test4.txt" in file_names -def test_scan_directory_non_recursive(temp_workspace): +@pytest.mark.asyncio +async def test_scan_directory_non_recursive(temp_workspace, db_session): """测试非递归扫描目录""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建测试文件 (temp_workspace / "test1.py").write_text("print('hello')") @@ -87,76 +102,80 @@ def test_scan_directory_non_recursive(temp_workspace): (subdir / "test2.py").write_text("print('world')") # 非递归扫描 - files = organizer.scan_directory(temp_workspace, recursive=False) + files = await organizer.scan_directory(temp_workspace, recursive=False) assert len(files) == 1 - assert files[0].name == "test1.py" + assert files[0].file_name == "test1.py" -def test_create_file_metadata(temp_workspace): +@pytest.mark.asyncio +async def test_create_file_metadata(temp_workspace, db_session): """测试创建文件元数据""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) test_file = temp_workspace / "test.py" test_file.write_text("print('hello')") - metadata = organizer.create_file_metadata(test_file) + metadata = await organizer.create_file_metadata(test_file) - assert metadata.name == "test.py" - assert metadata.path == str(test_file) + assert metadata.file_name == "test.py" + assert metadata.file_path == str(test_file) assert metadata.category == "code" - assert metadata.extension == ".py" - assert metadata.size > 0 + assert metadata.file_type == ".py" + assert metadata.size_bytes > 0 -def test_save_metadata(temp_workspace): +@pytest.mark.asyncio +async def test_save_metadata(temp_workspace, db_session): """测试保存元数据到数据库""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) test_file = temp_workspace / "test.py" test_file.write_text("print('hello')") - metadata = organizer.create_file_metadata(test_file) - saved = organizer.save_metadata(metadata) + metadata = await organizer.create_file_metadata(test_file) + saved = await organizer.save_metadata(metadata) assert saved.id is not None - assert saved.name == "test.py" + assert saved.file_name == "test.py" # 验证数据库中有记录 - files = organizer.search_files() + files = await organizer.search_files() assert len(files) == 1 - assert files[0].name == "test.py" + assert files[0].file_name == "test.py" -def test_save_metadata_update(temp_workspace): +@pytest.mark.asyncio +async def test_save_metadata_update(temp_workspace, db_session): """测试更新已存在的元数据""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) test_file = temp_workspace / "test.py" test_file.write_text("print('hello')") # 第一次保存 - metadata1 = organizer.create_file_metadata(test_file) - saved1 = organizer.save_metadata(metadata1) + metadata1 = await organizer.create_file_metadata(test_file) + saved1 = await organizer.save_metadata(metadata1) # 修改文件 test_file.write_text("print('world')") # 第二次保存 - metadata2 = organizer.create_file_metadata(test_file) - saved2 = organizer.save_metadata(metadata2) + metadata2 = await organizer.create_file_metadata(test_file) + saved2 = await organizer.save_metadata(metadata2) # 应该是同一条记录 assert saved1.id == saved2.id # 数据库中应该只有一条记录 - files = organizer.search_files() + files = await organizer.search_files() assert len(files) == 1 -def test_organize_file(temp_workspace): +@pytest.mark.asyncio +async def test_organize_file(temp_workspace, db_session): """测试文件归档""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建源文件 source_file = temp_workspace / "test.py" @@ -167,21 +186,22 @@ def test_organize_file(temp_workspace): target_dir.mkdir() # 归档文件 - metadata = organizer.organize_file(source_file, target_dir) + metadata = await organizer.organize_file(source_file, target_dir) # 验证文件已移动 assert not source_file.exists() assert (target_dir / "code" / "test.py").exists() # 验证元数据 - assert metadata.name == "test.py" + assert metadata.file_name == "test.py" assert metadata.category == "code" - assert metadata.path == str(target_dir / "code" / "test.py") + assert metadata.file_path == str(target_dir / "code" / "test.py") -def test_organize_file_with_custom_category(temp_workspace): +@pytest.mark.asyncio +async def test_organize_file_with_custom_category(temp_workspace, db_session): """测试使用自定义分类归档文件""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建源文件 source_file = temp_workspace / "test.py" @@ -192,16 +212,17 @@ def test_organize_file_with_custom_category(temp_workspace): target_dir.mkdir() # 使用自定义分类归档 - metadata = organizer.organize_file(source_file, target_dir, category="custom") + metadata = await organizer.organize_file(source_file, target_dir, category="custom") # 验证文件已移动到自定义分类目录 assert (target_dir / "custom" / "test.py").exists() assert metadata.category == "custom" -def test_organize_file_with_conflict(temp_workspace): +@pytest.mark.asyncio +async def test_organize_file_with_conflict(temp_workspace, db_session): """测试文件归档时的文件名冲突处理""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建源文件 source_file = temp_workspace / "test.py" @@ -215,16 +236,17 @@ def test_organize_file_with_conflict(temp_workspace): (code_dir / "test.py").write_text("existing") # 归档文件 - metadata = organizer.organize_file(source_file, target_dir) + metadata = await organizer.organize_file(source_file, target_dir) # 验证文件已重命名 assert (code_dir / "test_1.py").exists() - assert metadata.name == "test_1.py" + assert metadata.file_name == "test_1.py" -def test_search_files(temp_workspace): +@pytest.mark.asyncio +async def test_search_files(temp_workspace, db_session): """测试搜索文件""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建测试文件 (temp_workspace / "test1.py").write_text("print('hello')") @@ -232,75 +254,79 @@ def test_search_files(temp_workspace): (temp_workspace / "test3.json").write_text("{}") # 扫描并保存 - files = organizer.scan_directory(temp_workspace) + files = await organizer.scan_directory(temp_workspace) for f in files: - organizer.save_metadata(f) + await organizer.save_metadata(f) # 搜索所有文件 - all_files = organizer.search_files() + all_files = await organizer.search_files() assert len(all_files) == 3 - # 按名称搜索 - py_files = organizer.search_files(name="test1") + # 按关键词搜索 + py_files = await organizer.search_files(keyword="test1") assert len(py_files) == 1 - assert py_files[0].name == "test1.py" + assert py_files[0].file_name == "test1.py" # 按分类搜索 - code_files = organizer.search_files(category="code") + code_files = await organizer.search_files(category="code") assert len(code_files) == 1 - assert code_files[0].name == "test1.py" + assert code_files[0].file_name == "test1.py" - # 按扩展名搜索 - md_files = organizer.search_files(extension=".md") + # 按文件类型搜索 + md_files = await organizer.search_files(file_type=".md") assert len(md_files) == 1 - assert md_files[0].name == "test2.md" + assert md_files[0].file_name == "test2.md" -def test_search_files_with_limit(temp_workspace): +@pytest.mark.asyncio +async def test_search_files_with_limit(temp_workspace, db_session): """测试搜索文件时的限制""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) # 创建多个测试文件 for i in range(10): (temp_workspace / f"test{i}.py").write_text(f"print({i})") # 扫描并保存 - files = organizer.scan_directory(temp_workspace) + files = await organizer.scan_directory(temp_workspace) for f in files: - organizer.save_metadata(f) + await organizer.save_metadata(f) # 限制返回数量 - limited_files = organizer.search_files(limit=5) + limited_files = await organizer.search_files(limit=5) assert len(limited_files) == 5 -def test_scan_nonexistent_directory(temp_workspace): +@pytest.mark.asyncio +async def test_scan_nonexistent_directory(temp_workspace, db_session): """测试扫描不存在的目录""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) nonexistent = temp_workspace / "nonexistent" with pytest.raises(ValueError, match="目录不存在"): - organizer.scan_directory(nonexistent) + await organizer.scan_directory(nonexistent) -def test_create_metadata_nonexistent_file(temp_workspace): +@pytest.mark.asyncio +async def test_create_metadata_nonexistent_file(temp_workspace, db_session): """测试创建不存在文件的元数据""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) nonexistent = temp_workspace / "nonexistent.py" with pytest.raises(ValueError, match="文件不存在"): - organizer.create_file_metadata(nonexistent) + await organizer.create_file_metadata(nonexistent) -def test_organize_nonexistent_file(temp_workspace): +@pytest.mark.asyncio +async def test_organize_nonexistent_file(temp_workspace, db_session): """测试归档不存在的文件""" - organizer = FileOrganizer(temp_workspace) + organizer = FileOrganizer(db_session) nonexistent = temp_workspace / "nonexistent.py" target_dir = temp_workspace / "organized" target_dir.mkdir() with pytest.raises(ValueError, match="源文件不存在"): - organizer.organize_file(nonexistent, target_dir) + await organizer.organize_file(nonexistent, target_dir) diff --git a/backend/tests/test_plugins.py b/backend/tests/test_plugins.py new file mode 100644 index 0000000..c9197d8 --- /dev/null +++ b/backend/tests/test_plugins.py @@ -0,0 +1,155 @@ +"""插件系统测试""" +import pytest +import tempfile +from pathlib import Path +from app.plugins.manager import PluginManager +from app.plugins.hookspecs import hookimpl + + +class TestPlugin: + """测试插件""" + + def __init__(self): + self.events = [] + + @hookimpl + def on_file_created(self, file_path: str, metadata: dict): + self.events.append(("file_created", file_path)) + + @hookimpl + def on_git_commit(self, repo_path: str, commit_hash: str, message: str): + self.events.append(("git_commit", commit_hash)) + + +@pytest.fixture +def temp_plugins_dir(): + """创建临时插件目录""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +def test_plugin_manager_init(temp_plugins_dir): + """测试插件管理器初始化""" + manager = PluginManager(temp_plugins_dir) + assert manager.plugins_dir == temp_plugins_dir + assert len(manager.list_plugins()) == 0 + + +def test_register_plugin(temp_plugins_dir): + """测试注册插件""" + manager = PluginManager(temp_plugins_dir) + plugin = TestPlugin() + + manager.load_plugin("test_plugin", plugin) + + assert "test_plugin" in manager.list_plugins() + + +def test_unregister_plugin(temp_plugins_dir): + """测试卸载插件""" + manager = PluginManager(temp_plugins_dir) + plugin = TestPlugin() + + manager.load_plugin("test_plugin", plugin) + manager.unload_plugin("test_plugin") + + assert "test_plugin" not in manager.list_plugins() + + +def test_call_hook(temp_plugins_dir): + """测试调用钩子""" + manager = PluginManager(temp_plugins_dir) + plugin = TestPlugin() + + manager.load_plugin("test_plugin", plugin) + + # 调用钩子 + manager.call_hook("on_file_created", file_path="/test/file.py", metadata={}) + + assert len(plugin.events) == 1 + assert plugin.events[0] == ("file_created", "/test/file.py") + + +def test_call_multiple_hooks(temp_plugins_dir): + """测试调用多个钩子""" + manager = PluginManager(temp_plugins_dir) + plugin = TestPlugin() + + manager.load_plugin("test_plugin", plugin) + + manager.call_hook("on_file_created", file_path="/test/file1.py", metadata={}) + manager.call_hook("on_file_created", file_path="/test/file2.py", metadata={}) + manager.call_hook("on_git_commit", repo_path="/repo", commit_hash="abc123", message="test") + + assert len(plugin.events) == 3 + + +def test_discover_plugins(temp_plugins_dir): + """测试发现插件""" + # 创建一个插件文件 + plugin_code = ''' +from app.plugins.hookspecs import hookimpl + +class AutoPlugin: + @hookimpl + def on_file_created(self, file_path: str, metadata: dict): + pass +''' + plugin_file = temp_plugins_dir / "auto_plugin.py" + plugin_file.write_text(plugin_code) + + manager = PluginManager(temp_plugins_dir) + manager.discover_plugins() + + assert "auto_plugin" in manager.list_plugins() + + +def test_discover_nonexistent_directory(): + """测试发现不存在的插件目录""" + nonexistent = Path("/nonexistent/plugins") + manager = PluginManager(nonexistent) + + # 不应该抛出异常 + manager.discover_plugins() + assert len(manager.list_plugins()) == 0 + + +def test_call_hook_no_plugins(temp_plugins_dir): + """测试没有插件时调用钩子""" + manager = PluginManager(temp_plugins_dir) + + # 不应该抛出异常 + manager.call_hook("on_file_created", file_path="/test.py", metadata={}) + + +def test_multiple_plugins(temp_plugins_dir): + """测试多个插件""" + manager = PluginManager(temp_plugins_dir) + plugin1 = TestPlugin() + plugin2 = TestPlugin() + + manager.load_plugin("plugin1", plugin1) + manager.load_plugin("plugin2", plugin2) + + manager.call_hook("on_file_created", file_path="/test.py", metadata={}) + + # 两个插件都应该收到事件 + assert len(plugin1.events) == 1 + assert len(plugin2.events) == 1 + + +def test_plugin_error_handling(temp_plugins_dir): + """测试插件错误处理""" + + class ErrorPlugin: + @hookimpl + def on_file_created(self, file_path: str, metadata: dict): + raise ValueError("Test error") + + manager = PluginManager(temp_plugins_dir) + plugin = ErrorPlugin() + manager.load_plugin("error_plugin", plugin) + + # 应该捕获异常但不中断 + with pytest.raises(ValueError, match="Test error"): + manager.call_hook("on_file_created", file_path="/test.py", metadata={}) diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..1c9b151 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,54 @@ +{ + "name": "ai-dev-assistant-desktop", + "version": "0.1.0", + "description": "AI Dev Assistant Desktop Client", + "main": "dist/main/index.js", + "scripts": { + "dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"", + "dev:main": "tsc -p tsconfig.main.json --watch", + "dev:renderer": "vite", + "build": "npm run build:main && npm run build:renderer", + "build:main": "tsc -p tsconfig.main.json", + "build:renderer": "vite build", + "start": "electron .", + "package": "electron-builder" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "antd": "^5.13.0", + "@ant-design/icons": "^5.2.6", + "axios": "^1.6.0" + }, + "devDependencies": { + "electron": "^28.0.0", + "electron-builder": "^24.9.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^5.3.0", + "vite": "^5.0.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^8.2.0" + }, + "build": { + "appId": "com.ai-dev-assistant.app", + "productName": "AI Dev Assistant", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "package.json" + ], + "mac": { + "category": "public.app-category.developer-tools" + }, + "win": { + "target": "nsis" + }, + "linux": { + "target": "AppImage" + } + } +} diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts new file mode 100644 index 0000000..2523b6b --- /dev/null +++ b/desktop/src/main/index.ts @@ -0,0 +1,50 @@ +import { app, BrowserWindow, ipcMain } from 'electron'; +import path from 'path'; + +let mainWindow: BrowserWindow | null = null; + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + nodeIntegration: false, + contextIsolation: true, + }, + }); + + // 开发环境加载 Vite 开发服务器 + if (process.env.NODE_ENV === 'development') { + mainWindow.loadURL('http://localhost:5173'); + mainWindow.webContents.openDevTools(); + } else { + // 生产环境加载打包后的文件 + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); + } + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +app.whenReady().then(() => { + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// IPC 通信处理 +ipcMain.handle('get-backend-url', () => { + return 'http://localhost:8000/api/v1'; +}); diff --git a/desktop/src/preload/index.ts b/desktop/src/preload/index.ts new file mode 100644 index 0000000..087d786 --- /dev/null +++ b/desktop/src/preload/index.ts @@ -0,0 +1,6 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +// 暴露安全的 API 到渲染进程 +contextBridge.exposeInMainWorld('electronAPI', { + getBackendUrl: () => ipcRenderer.invoke('get-backend-url'), +}); diff --git a/desktop/src/renderer/App.tsx b/desktop/src/renderer/App.tsx new file mode 100644 index 0000000..efb7205 --- /dev/null +++ b/desktop/src/renderer/App.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { ConfigProvider } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; + +import Home from './pages/Home'; +import Settings from './pages/Settings'; +import Plugins from './pages/Plugins'; + +const App: React.FC = () => { + return ( + + + + } /> + } /> + } /> + + + + ); +}; + +const root = createRoot(document.getElementById('root')!); +root.render(); diff --git a/desktop/src/renderer/components/Avatar/index.tsx b/desktop/src/renderer/components/Avatar/index.tsx new file mode 100644 index 0000000..76f2b3a --- /dev/null +++ b/desktop/src/renderer/components/Avatar/index.tsx @@ -0,0 +1,50 @@ +import React, { useEffect, useRef } from 'react'; +import { Card } from 'antd'; + +interface AvatarProps { + modelUrl?: string; + isSpeaking?: boolean; +} + +const Avatar: React.FC = ({ + modelUrl = 'https://models.readyplayer.me/65a8f3c8b8e3b0f6a7d4e5f6.glb', + isSpeaking = false +}) => { + const containerRef = useRef(null); + + useEffect(() => { + // 这里集成 Ready Player Me 的 WebGL 渲染器 + // 实际实现需要加载 3D 模型和动画 + console.log('Avatar component mounted', { modelUrl, isSpeaking }); + + return () => { + // 清理资源 + }; + }, [modelUrl, isSpeaking]); + + return ( + +

+ {isSpeaking ? '正在说话...' : 'AI 数字人'} +
+ + ); +}; + +export default Avatar; diff --git a/desktop/src/renderer/index.html b/desktop/src/renderer/index.html new file mode 100644 index 0000000..4599baa --- /dev/null +++ b/desktop/src/renderer/index.html @@ -0,0 +1,12 @@ + + + + + + AI Dev Assistant + + +
+ + + diff --git a/desktop/src/renderer/pages/Home.tsx b/desktop/src/renderer/pages/Home.tsx new file mode 100644 index 0000000..b408cab --- /dev/null +++ b/desktop/src/renderer/pages/Home.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Layout, Typography, Card } from 'antd'; +import Avatar from '../components/Avatar'; + +const { Header, Content } = Layout; +const { Title } = Typography; + +const Home: React.FC = () => { + return ( + +
+ AI 开发助手 +
+ + +

这是一个智能化的开发助手,支持文件管理、Git 操作和插件扩展。

+
+
+ +
+
+
+ ); +}; + +export default Home; diff --git a/desktop/src/renderer/pages/Plugins.tsx b/desktop/src/renderer/pages/Plugins.tsx new file mode 100644 index 0000000..5116c48 --- /dev/null +++ b/desktop/src/renderer/pages/Plugins.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Layout, Typography, Card, List, Tag } from 'antd'; + +const { Header, Content } = Layout; +const { Title } = Typography; + +const Plugins: React.FC = () => { + const plugins = [ + { name: '示例插件', version: '0.1.0', status: 'active' }, + { name: '代码分析插件', version: '0.1.0', status: 'inactive' }, + ]; + + return ( + +
+ 插件管理 +
+ + ( + + {item.status}} + > +

版本: {item.version}

+
+
+ )} + /> +
+
+ ); +}; + +export default Plugins; diff --git a/desktop/src/renderer/pages/Settings.tsx b/desktop/src/renderer/pages/Settings.tsx new file mode 100644 index 0000000..3fea027 --- /dev/null +++ b/desktop/src/renderer/pages/Settings.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Layout, Typography, Form, Input, Select, Button } from 'antd'; + +const { Header, Content } = Layout; +const { Title } = Typography; + +const Settings: React.FC = () => { + return ( + +
+ 设置 +
+ +
+ + + + + + + + + + + + +
+
+
+ ); +}; + +export default Settings; diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json new file mode 100644 index 0000000..a47cad8 --- /dev/null +++ b/desktop/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/desktop/tsconfig.main.json b/desktop/tsconfig.main.json new file mode 100644 index 0000000..13a359b --- /dev/null +++ b/desktop/tsconfig.main.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "skipLibCheck": true, + "moduleResolution": "node", + "outDir": "../dist/main", + "rootDir": "../src/main", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["src/main/**/*"] +} diff --git a/desktop/tsconfig.node.json b/desktop/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/desktop/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/desktop/vite.config.ts b/desktop/vite.config.ts new file mode 100644 index 0000000..cc1742c --- /dev/null +++ b/desktop/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + root: path.resolve(__dirname, 'src/renderer'), + build: { + outDir: path.resolve(__dirname, 'dist/renderer'), + emptyOutDir: true, + }, + server: { + port: 5173, + }, +}); From 737dcf55e43455b0e6ecf81ab0fd2bf1f367d2fe Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:32:18 +0000 Subject: [PATCH 10/16] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Pydantic=20Co?= =?UTF-8?q?nfig=20=E5=BC=83=E7=94=A8=E8=AD=A6=E5=91=8A=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20model=5Fconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/__pycache__/files.cpython-314.pyc | Bin 8183 -> 8057 bytes backend/app/api/files.py | 4 +- backend/logs/app.log | 42 ++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/backend/app/api/__pycache__/files.cpython-314.pyc b/backend/app/api/__pycache__/files.cpython-314.pyc index 47ed77ab495a90d94a89ffd9f309f2e22035c273..bdd91f61a349c86455443273ed3877a8456f78d5 100644 GIT binary patch delta 1758 zcma)+T}&KR6vyu@v)>==vcN7Mg)J25KnZHm)u?C_EvDGDER;4zoh-8#*micO+?loV zpg{Z3L_%zH{YYPE{df@Hnl(OX(rA1#@u6s}I&Y2AgcrkF6ZFkicWqm6~}MF5!3QQ9zC?ke7)iWr^i_C2G|)5`$zV zOU}BoH3O36o*)OMtehaa&+=s5mUn_!zO1K{C|)~sxS-Q4HP09&la}is+3j~P{QAXb z>wJ0r^S5t*{QmmNl3hE+jG~&eELNB-ThtuaDq;6)68}=}=1ZRb$eXb3&kN)nx#bHk z4)YtH)O<9)n5hPdBvnJCHgU;ZF0RQf)i63DBpAPRaJhd?PE>2r5hZoampk9tvnIDy zW9X= z^M(fg5O}GsYDQ_QFm1CzLTI zz|(9Wkj3vJiVs=*3#duN`TGDBGVeFsfMw zo3s63l$lw0l`1Z_7az)#hM~iv0~St6yx)!mr)C$1B*(N|iC+xV^IN`sBRv?nACOUe zOuFp^oO} zn-LH{bneizxa(eSb{Li!&?yA}cF3eTrp>5R@LHLQm;FmD8|EK{Q*}5_c0ezfmJ`fI z_|5RAY3Brt&X%cZv0(^;)Z(W8yU(v8iqykr<4qO0_PQjc`Pb12&m*v%rFnZSU59f9 z7n4B`W@ShCOe`G~Lp_e`@yaJLOHz(voJ-uneUurFL&eH${4L3I3=@oUPec33C(#^2 z2!&3d6+xUtcoCu%_s&+(`y%2cfawNwcc)S*=OR>mpjKXQcv#w22gOwjd2xo*0w5Mj@TzvjgU=p3}7%TAX7PD}({I`|J#5lqL95a({3V-9jtMuO~aB;>LW_ z)VVNb@M`jAM-GED!0sco(IkpD|0FeGCEEiZBWlqMo5H=*h*2)LrsikR6wNTSup?GL zBoL<%rxB3s=KiIsy z=ZB+W=%Yv9am1TFcD0aGdFMO=#kPNy*D5?>_NgO;P!6VB&&h%NH>+MeYiN{d=Gr7fX8;`rW*JN`~$kre4C zD(xbw?*$(&B*c+s$S76)UBnDIvMM=&8R7(0d4MPZxAl@?>Z)!pTQyrRc^f>W2ZOyH zAb-~xfkGL_uTn=+eUQu#NhCLUILYt@t5z`Cr67=jpB)5vpFHWw-fLsMz*!$aLy^{~%(6iF zSMu=shd}=LfV@k72t_uhn|?y+Pi=|KLQ@9T43Kydco2EW<1NU;q^;||y;a?nPBbGJ zXeE*4{n4#6yHZy(ih&qO^lT42=-rjF%{C0gNqfiU^=3QrgvXPFuG40R82pxk2~Q@r zRZv2#3;tO*LK*>W6)m{!npUjUEO12ED)m|ktiXmbUNVceT{cQiNjFVR+o5b2W^8;n z=S9;jiD?kxIFZ3((EkS6@1}BJlK||eLCRgidiiYd5$)&UQ0C;bu*{VfEX!1U>VYJ4LprJ6hRbY!D_u8tPt*eQniZWg&~5f`zVZ8dbw_edPs35(+)7LztumK%v4{VMG4FQJfp$e@1T7 zUS4SZBz_44LK>!I)BI^`CiU+i&RpiFqqzhgayM)mwxdXS5fZY<% zc&^={N&z@j6V@GH?VAKt<14XrBc6Pl25w*ymEY^=o1H>>4nbHki!6rlIs$(6!dgW> zEC-`+Aj|>SGQi+qE|>F+p2vy#hCek%58Oe&G@~qf+3~Ue@s_MkbZ5KsCOxG>Iwj|VmX`GD8mnN1D1a-kF6m5XdEVarOaw1b&#wx<*gX&zZ!Cmo8U?UrzS{jbzkrL% z07oAYD=mtrpU^Z`asy@E)E&LR7IE(qg33QSky|&A3Nbb^RK#v0h%w-(>eeD|t2cz7OM zKlIJ6A(NIZIGN(y%)^*1BZ%UdPu%<7Rq6RPS+&Y_Q=ejYc{+PB)c5gAzmq8KYv|cH ImGDdd3sgmYpa1{> diff --git a/backend/app/api/files.py b/backend/app/api/files.py index bb2a23b..073bb1f 100644 --- a/backend/app/api/files.py +++ b/backend/app/api/files.py @@ -14,6 +14,7 @@ class FileResponse(BaseModel): + """文件响应模型""" id: int file_path: str file_name: str @@ -25,8 +26,7 @@ class FileResponse(BaseModel): created_at: str updated_at: str - class Config: - from_attributes = True + model_config = {"from_attributes": True} class ScanRequest(BaseModel): diff --git a/backend/logs/app.log b/backend/logs/app.log index 5523270..57ce39c 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -35,3 +35,45 @@ 2026-06-12 11:30:34,451 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: error_plugin 2026-06-12 11:30:49,377 - ai_dev_assistant - ERROR - get_git_status:60 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent 2026-06-12 11:30:49,386 - ai_dev_assistant - ERROR - get_recent_commits:82 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:31:31,084 - ai_dev_assistant - ERROR - get_git_status:60 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:31:31,093 - ai_dev_assistant - ERROR - get_recent_commits:82 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:31:31,125 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 4 个文件 +2026-06-12 11:31:31,134 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 1 个文件 +2026-06-12 11:31:31,180 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmp5xnijh8m/test.py -> /tmp/tmp5xnijh8m/organized/code/test.py +2026-06-12 11:31:31,192 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmp6y6hpou6/test.py -> /tmp/tmp6y6hpou6/organized/custom/test.py +2026-06-12 11:31:31,205 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmp2tjxl6kz/test.py -> /tmp/tmp2tjxl6kz/organized/code/test_1.py +2026-06-12 11:31:31,217 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 3 个文件 +2026-06-12 11:31:31,237 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 10 个文件 +2026-06-12 11:31:31,395 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: b0344a342e4c1bed72ce3d9db5fe7f7c909e6139 +2026-06-12 11:31:31,425 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 5ba4b68452cbc5de43075d6837e9a53f1f49eee1 +2026-06-12 11:31:31,463 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:31,465 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:31,465 - ai_dev_assistant - INFO - unload_plugin:72 - 插件已卸载: test_plugin +2026-06-12 11:31:31,468 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:31,470 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:31,473 - ai_dev_assistant - INFO - load_plugin:60 - 插件加载成功: auto_plugin +2026-06-12 11:31:31,476 - ai_dev_assistant - WARNING - discover_plugins:23 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:31:31,479 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin1 +2026-06-12 11:31:31,479 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin2 +2026-06-12 11:31:31,481 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: error_plugin +2026-06-12 11:31:57,922 - ai_dev_assistant - ERROR - get_git_status:60 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:31:57,931 - ai_dev_assistant - ERROR - get_recent_commits:82 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:31:57,961 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 4 个文件 +2026-06-12 11:31:57,970 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 1 个文件 +2026-06-12 11:31:58,012 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmpq0o22fh0/test.py -> /tmp/tmpq0o22fh0/organized/code/test.py +2026-06-12 11:31:58,023 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmpcdnh7oyh/test.py -> /tmp/tmpcdnh7oyh/organized/custom/test.py +2026-06-12 11:31:58,035 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmpvastjeml/test.py -> /tmp/tmpvastjeml/organized/code/test_1.py +2026-06-12 11:31:58,047 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 3 个文件 +2026-06-12 11:31:58,067 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 10 个文件 +2026-06-12 11:31:58,223 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 339d8f7653a45117e0ec9290563bf6b8d3b0b8ad +2026-06-12 11:31:58,255 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 6442ac30d8898f0bdcdb92649287fd03f74bb151 +2026-06-12 11:31:58,297 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:58,299 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:58,299 - ai_dev_assistant - INFO - unload_plugin:72 - 插件已卸载: test_plugin +2026-06-12 11:31:58,302 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:58,304 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:31:58,308 - ai_dev_assistant - INFO - load_plugin:60 - 插件加载成功: auto_plugin +2026-06-12 11:31:58,310 - ai_dev_assistant - WARNING - discover_plugins:23 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:31:58,313 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin1 +2026-06-12 11:31:58,313 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin2 +2026-06-12 11:31:58,315 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: error_plugin From 6eb9a8767bb7e2106cd71cb2d21c32e659ffdfbb Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:43:51 +0000 Subject: [PATCH 11/16] =?UTF-8?q?refactor:=20=E4=BF=AE=E5=A4=8D=E6=89=80?= =?UTF-8?q?=E6=9C=89=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BB=A3=E7=A0=81=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复所有 ruff lint 错误,包括导入排序、行长度、未使用变量等 - 优化代码格式,确保符合 PEP 8 规范 - 移除重复导入,统一导入位置 - 修复 pyproject.toml 配置,添加 setuptools 包发现规则 - 所有 47 个测试用例全部通过 - 代码质量检查全部通过 --- backend/ai_dev_assistant.egg-info/SOURCES.txt | 26 ++++++++++- .../app/__pycache__/config.cpython-314.pyc | Bin 2081 -> 2081 bytes .../app/__pycache__/database.cpython-314.pyc | Bin 2118 -> 2118 bytes backend/app/__pycache__/main.cpython-314.pyc | Bin 1984 -> 1986 bytes .../app/api/__pycache__/files.cpython-314.pyc | Bin 8057 -> 8057 bytes .../app/api/__pycache__/git.cpython-314.pyc | Bin 7327 -> 7327 bytes backend/app/api/files.py | 9 ++-- backend/app/api/git.py | 3 +- backend/app/config.py | 3 +- .../core/__pycache__/context.cpython-314.pyc | Bin 3527 -> 3527 bytes .../file_organizer.cpython-314.pyc | Bin 9354 -> 9348 bytes .../__pycache__/git_manager.cpython-314.pyc | Bin 11379 -> 11317 bytes .../app/core/__pycache__/llm.cpython-314.pyc | Bin 6991 -> 6987 bytes backend/app/core/context.py | 2 +- backend/app/core/file_organizer.py | 21 ++++++--- backend/app/core/git_manager.py | 19 +++++--- backend/app/core/llm.py | 15 +++++-- backend/app/database.py | 3 +- backend/app/main.py | 4 +- backend/app/models/__init__.py | 2 +- .../__pycache__/__init__.cpython-314.pyc | Bin 343 -> 343 bytes .../models/__pycache__/file.cpython-314.pyc | Bin 3401 -> 3383 bytes .../__pycache__/operation.cpython-314.pyc | Bin 2548 -> 2536 bytes .../__pycache__/project.cpython-314.pyc | Bin 2926 -> 2890 bytes backend/app/models/file.py | 15 +++++-- backend/app/models/operation.py | 18 ++++++-- backend/app/models/project.py | 17 +++++-- .../__pycache__/manager.cpython-314.pyc | Bin 5716 -> 5716 bytes backend/app/plugins/manager.py | 10 +++-- .../utils/__pycache__/logger.cpython-314.pyc | Bin 2354 -> 2356 bytes .../__pycache__/validator.cpython-314.pyc | Bin 3581 -> 3586 bytes backend/app/utils/logger.py | 6 ++- backend/app/utils/validator.py | 4 +- backend/logs/app.log | 42 ++++++++++++++++++ backend/pyproject.toml | 3 ++ 35 files changed, 176 insertions(+), 46 deletions(-) diff --git a/backend/ai_dev_assistant.egg-info/SOURCES.txt b/backend/ai_dev_assistant.egg-info/SOURCES.txt index ad9895c..cf7eec8 100644 --- a/backend/ai_dev_assistant.egg-info/SOURCES.txt +++ b/backend/ai_dev_assistant.egg-info/SOURCES.txt @@ -6,4 +6,28 @@ ai_dev_assistant.egg-info/requires.txt ai_dev_assistant.egg-info/top_level.txt app/__init__.py app/config.py -app/main.py \ No newline at end of file +app/database.py +app/main.py +app/api/files.py +app/api/git.py +app/core/__init__.py +app/core/context.py +app/core/file_organizer.py +app/core/git_manager.py +app/core/llm.py +app/models/__init__.py +app/models/file.py +app/models/operation.py +app/models/project.py +app/plugins/__init__.py +app/plugins/hookspecs.py +app/plugins/manager.py +app/plugins/builtin/__init__.py +app/plugins/builtin/example_plugin.py +app/utils/__init__.py +app/utils/logger.py +app/utils/validator.py +tests/test_file_organizer.py +tests/test_git.py +tests/test_llm.py +tests/test_plugins.py \ No newline at end of file diff --git a/backend/app/__pycache__/config.cpython-314.pyc b/backend/app/__pycache__/config.cpython-314.pyc index 5415f9eb0edf0d52f63b0ceb2db3e18b5d4f4b4b..6dd49c18a2d3b616d5bc70deafb617fc74bbb0e8 100644 GIT binary patch delta 183 zcmZ1|uuyLY3(jmveqzT4uUSW^&0bR=3R5oD@yQTRcvQ#i_xmB_)}8>BY$t1w|*`6J+Gr z_(6z;kz+G6t0*HQ>tso`U5p%)udp2u;3%j}Nz5zBOfF&t+Fryqc>=rm9%U(DAE9`0szj`H`M?D diff --git a/backend/app/__pycache__/database.cpython-314.pyc b/backend/app/__pycache__/database.cpython-314.pyc index e2280c697a576b97220b75c9e447329ce7f1ba0d..9169a458469aa5c7f30d0f3b38925c2ad0e0d540 100644 GIT binary patch delta 99 zcmX>ma7=(#n~#@^0SH#S*UnO%$m_$%JTc0eok@`)mMfNV;vBn)?-fOF2`3k&CYGee zCl*)cCC8`crDx`)P8MLyWE7lS#W4BD$FR+1WPCdL7rPGt DlUg2U delta 98 zcmX>ma7=(#n~#@^0SJCS)Xq|w$m_$%I5Enaomr6~mMfNN;v72>;pC#!#FEtb#Nx`l zj1rq;*yb`a-kto5-3I_) CupO@e diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc index 2bdf33412637b1e0b73e32d3a3c04dc5ea5e44a9..441127996fcc1504d6dbd4c7b1a7bcf284195d60 100644 GIT binary patch delta 282 zcmX@We~6z~n~#@^0SH#S*Ur+I$m=Wa!BN5?&k)BD#OlfE!BxTqX0v%tEL76v3XlOQ zMFp{30dlA!AYLrj!~-W8*(Y}~S~79~Nu|lp7#&oYZn36i=A;(iVouL2(PX^EQJh** zl9`uYypr)2duCo{NqkDuWG|*kj69R?GSxA1O^#t!VdUQ2$Q;bb$T|5ciy@=(WOmk- zj5?DyvX&aMCl(awB^G2Bu>&nE;s6r2xIjY5`FUxX=|!9%0UnS*N@7W3QetuH^T)Eg5-$q|)SPj1DS{w>XMZOG+~H(u-Fz-eS+p%Pfgc zNz!Dx#hR9xlUjU>IX$yvvKP}NMy|LMIcX34ri;7 ndjVv8U}j`wyvv|UofMFdd_Dhe=Al%`JXrbfYWZ7(UH zcI?0s-Gao#RE93dLSkiMKmv)M1xryUggPN6R0hQ7(28$(x_j?Q-@Cj0nf)1|7#AXh zz861=_rLUoQZ!GNNR63euX0Ymx>bsmSzji^NvBAG)Q%h@wM6|tD-GEsWyN}||9ccT z%)vbnaUISLYbn4NNL|EFFh|)eF_1R)^2oFtuqc@Y}W1>ty`LHQv>aVD|$y;u-b~DuWJ@1f^E%ds7=(^Wk|Yj+1Ee= zYy{S5^D{6Dki@g0+fcx*&@RPQevP8XUxP8q;i5YqKhKOrsnv_yEyZZ4x}~+8uGO;` ze3p0$7u<645ggbT-H+lMNDb{m_h6#N3LTHSw~sd9t{Y0tAG!_K9tkn<03W7*!6w?$ zBCz;D+QawK9gJmE!3)xbX^v)T^nZMuITN(}%o@JROdOVbnQ7R-eD>?{kV*I7(VG_I zrwuwiQ#aa{e{>Y((I{{BRK;-gmKDY+IZG3pvMdgx#uU?>P!&gM(ktN;`FVOk(z~Sa Xom4)P%7EksWbzxC8j!qO&6VLVtb?m} delta 626 zcmYL{J8aWX6o&7y^Kc#xZbV8<>ZBp+fI?dy=>Up?iW)>IRZ#$mhtkxE-P9;JuI(k| zQN}I|(OHmK*t&E<77{B90}@Ek2}@BYhAv1)h}ErK5B1J*XU1#9~5> z5_+;fiVt`D;$-X^@A5gaOlsVGzq&PIMN zhgo<45^li7VJ!!RJgH0g73L`&6xuty5mrfI*}`?qc3(foy9_J-EC-1T8USUj=p5KRnyqeZPfV{kh~B4I{+Rx zgX{G2GdK&hkVP@7P+W{YhY4>ddI}`w z*m)-7Od|LunS(RlpBM)aM-Z2xh%4ep_-9=977~}Z(KwCz38$r+4b8CimfN*^b^u>V zFQMXEW^h1 z!xr6t$7o_wDd3!vE<_j=te&QtuFzOKx%a7nEe?=OyM$wr6T#(#V8Dv zyT-->w4#W4@(ETOpa3gdE0FAF^I?>ne1q*jBlF}N><&P-Bu6ihT+ZRZsId422P31x zYuK+>11n^9x)O)e8AM)l2nyxxqA=9{DVk{KD9Ca;&2pM00!fYEF-pMVAPx# delta 319 zcmbPlIp303n~#@^0SG3)(asW@$g9ik!Cb;D&k)BD#N@frRgaO8X>tkUCP}7SEIygV zCAT>I3raHc^AdA38E>%!B$i}Mwr6T#(#V7=n zyT-;0w4#V<@(ETOpa3gdE0FAF^I?>je1q*j(25)E4nVdfM=y|E&f&l)zxV|QBcuG} zmz>E!(wD27QGN4GE)yn3wat9I-i(Z9o1^%W85vn7ua}gce3##V(R4GPfCeL@<7RU~ zS0&;#~%b`;*s7e*yp#XueO#JLn~#@^0SH#SpU7q6CeIMZ5X9uk;=xkF;=x+N3g$C=vU#wVuzPTnaCmT* zaLO~pGKn%MG6b=RGX$}kFoAS4Fql9nh7zt=rim@K6R&6rYck(rcFe21#p05gTyl%W zC$qR@;@=P4EI^gFnDg?>i&!`RU^HR`3ly;fMT^)bn=(IOWZo>sGKYnY10=$}c?X9M zGb6|3r`!`5nJ1U=%+ugbNi0cB&PgmTPAx71*y%(Y$cTinR)4xZFs%7 TH10E~-)B&|$e=WN0ueO#JLn~#@^0SMBbPvkOj^I$1qk!Ofw2x9VN^8G=~E8G=|%m_WK27)&4(LkU+b+r$=IA?8~wKAFWOw^&>qKw}^T34h|h= zMyAP6xhF8PO)le^r@>ZIS&*5RUIdW`YXh0cosw9Rn4FVXT%1~5#0nIdY{TotrF4-& P={|$TeFpW(6L`%5UspIM diff --git a/backend/app/core/__pycache__/file_organizer.cpython-314.pyc b/backend/app/core/__pycache__/file_organizer.cpython-314.pyc index 0b5a7566f1a4a329e6409de9ddae8c71d31b10fe..99474e547e05eac4fece702d2b4cfef65180cdd0 100644 GIT binary patch delta 2024 zcmb7FONolC%@j?UX`1dwooEovguBs9 zG_q3ye-NUXYhRBQC10Pjs7;-U?dyP-YE|37zVeDi7YxZ)jyfLo_2YHw)T)-tlK8no zYSD5X(3&lCn6Hr~KmC|>)^Y9GI#*^DJci@V>-aRrVfH|J3vXuU9v{VRTa_S$0avPO6;k%N`|$M0z<* z`Z$QIhL%Yl7;qEm=P<}VQ%3MW^E)MhwGrSN>=)I*d8TSbJjixwNAUo=tewJHwyxzG zTlf(}A;N-krdD%_21=WN{ZwNU{FDbbJX}U?lvX=80M@ekmJHu5Q{sC!9J}KF81CIr zIa7pKmbs2t&Bf5DgeTZ1;UoDN=lF_KJzMi-*RC2m5&rMtY%7DASQ` zX<_?IF8hsV?}3gw3e8Iq4euD|Ebci3^o&vLXU!<2Y&CjX>NB{W9~OnJTD(sJk}lP4 zvV+aUi_>72ya2EdU>BTMJZe#4%~wE+Oo6BfqL4>tJ?D~5WOh5Ae@OwJe1VpIoeL_^ zgoYNxDJ(0{O=JhCp8zm9cu~>}+PyX+jB0kyd@Av@o7P{5h z(-7YNtB(kDG!c1vwl;G4d2bsB(MDtH_6@{gtt;1(;X6JVeD@sqHuFtP(09X~8!DGU zH#HA;)n(^D%0lA&N?)y^I>bSmAsV&+BT8|t8I0zsj0q5lxxf97nAVrZc0%fx0TsKe6ja)G;f;o;K_hq-y{5hAFEU1$oI%h9ktdY6E z!f{}!u2XTSD4wA{Nu4h{Rr-J4;&t|N{xx3G&+|P}<8evJ8{pqDFr=4jRcbj^?}>5p zHb`ME5etrlyt3t5#Pem#b&XIE`(+S|jDWv^$~;gD9K`f0z3uiNKQq7%B*x8Ue)6($ro4k#7gSWs-9d)?_JeXE1Bt5CX&;BP`cVZ zSnX^f4sA_WyIY7uyMs@r`!KuONx?Hs0vEd1n{sRO^EM$d79Gi!Av+`i0J*w}0`K7e;Ol$& zEf-vSANiPXA+_Elhn9>nUPFWHXzUK^Sx1BSQ2q`o-b3Se(C|7s($-#=aN!RGC~o^3 DsB*o~ delta 2157 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 o!TX7jV8Q;|JC_{5(WBs1%+Wsk8}cDj^7tG zrqqsHgunu#Yvq?Ue<0)A6_pl7S?BAx4o>@54bq{EP)cu0Jmpms|^%cWax#2W; zHSgtQWlm>s8R3>U;yX~H555r?Vd-4 zdI)+6<_K~GTL?bqMmMs@s#T3SUaN4!w)tL^5!PwJ!N=c|(Apkv%f0NXL?{f+i@1cL z9w_UU1y61o8;RhAtJrnRbX*@d>K6H0*j+Uq$3(H+;DT;7j7iRGoQL6Go3^ud7>MN} z0e&~`cXE+c-;@I6RUo1qrntoD(J2EUHI(3ME7 z#S8Dm3xA5G-;c#lPDAu^YRmH@|Mtm===Ue!Sj*Zz;ZvDXkKcQWKlf(q3fnE>;UQAActaaMLZd?ndAWJX3Qa5#EF;)-E?o)(u$tJ) z_QQ*b_jVDzri@_cX|kg%yFH^tc(tt$8LhQNJ+UED^aTPcZF~eiYX35O81B%%*h$)4 zuVqy8>(&>X$|N=f-_dqqc)h9R*dqL0dkb^HO5yeK7 zsoV2(;E@z1!->?aCl;q;DX)1OK@zTX_9D+nTI&}@Pt-Gyxh!Nb#vE?eo1)_+AB27B zOg4l=*V}N){1}e%YSZ50TB{^gh7;)wUh8M+9jb#9_HW>yS{r8ot)u!FIxl?%%?2 zGN51@c1oaSc7hX1s57kSsmt)qOnY1&qKd!%t1Ao2TpXhDRG}LiUtENzqP z>VjvE4EMR;f(rl0WU;11v+anvtjT0JnJuyj_*-@-8-mUr4Sl=4=l*@)#;sTx4PG(G zvRM0@d63vUJzpwL&OK0iaDIG#vNSVU+&h`~Z?I}JSa3UkmH^<7J>5~V+Gkk`pZ083 zg^4~~H+C52d%H#^rrw@;W9C9}@zTVBwTUJCyD+hIa`{Iqr&fM)-=*Z>T5|Maa&#>@ z_D*u_!jU@su=go+QfD6*&7_$dbZu9mCTeet#0_Qnw!Key@TaK99z_ryEGV0=NeHLq ztMoW)i^m%~e?M{YEAxMtZoRZ;LCjoSGK3sYpUd|5dXI|Qq3)@zP-1)+wQ;K1`~U%k zQ$D!CazvfKOh7htWx9kbJKQi_uWeTN4`^1c>)fb1b`W2%@ZGF@@biITpCBD%a8*md zCj*~!QUk~{QEu}LKS8e0u6$JV0*GyD+rJCnITcKPl;GX3bEY<8@qZ z_E2&b7uiQ4Wxj~XH*Q$rn%;({;!Ayw0Zknu9@X}y^O#46@dRH3yi90+$Ex i>Th@$#s%R=NS9?UN44j)wT_XC9V72Xw|*-m6#NGwpK9a) diff --git a/backend/app/core/__pycache__/llm.cpython-314.pyc b/backend/app/core/__pycache__/llm.cpython-314.pyc index ffc280371f90a15f44e90a7d6087b3c48cdcd096..9081d0ece5187671cd7ef044604cab6e0e64fdeb 100644 GIT binary patch delta 1648 zcmb7^O>7%g5P;vyde?tz$95dY>o#?a6BCmVT9LY;pomafY%6K25L8N%7R%WNrEFRwlsy%Z(q$YfhaN!v}kkfn?x?KGWfm+4Bo zO?O%~RbjVfJZZ1#6|$UZPy0-tkd=%-9WVn?(n(@u(%wZT9m8#S5FuvAY)6a?w-jOq zC!KMUbk+8OVe8}d-*2pKeYE;@uwb)1W5#kNpD}foGpo$9y8WTCoH7u9t&k@1$gXX~+JyKqt-dWSQzZ`LOy!I6SfCR{ z)C~+a(ZfRgOF2b@{BL=RYW$QkI*4&H1(0lG2T+az#PlatSO^V0h!`NwHP2H8AZcR>l=~4$&h5hr<`NwE*52200QJWX*c>Su=g&KyZ8?zr9}W!RMmyn0WNn)O zu5^nZgx_>bOD9#BWT~Z6`TQcQE*0l=#&8`wg76{&hz!NvDiI?HJsw`=L=s7v-F`X|9d^pbFBSH^@x49k0OuNGjI@eDw; zk17YY3E*yc|COn=_?mI8|LVCr;lYLk#$TMFRpX9x-?j`2(_l?rychMep=mn3I~uq! zJy#adu(L2v#KGPGX(tM_P9tUzvj9uZFVrd**?Eu+J0Ou5e0T%x;r@Vd^j$Fj0masE zkoFF~5sdQr@VZ+Ruc+G&9@dUWyI|I?65jEQy@>~+b31_3m~5dWW;CXocO>SVH$m3Sf&<=IG#+IcC`JtB$@>z0Wc z#PYLTxy8jHD7*P$fr+m4p`m6LOU6*CS}4F(d-xX-ttPq>{juEGx9}>$W65rk){*!K zS&kRV)wx6rSPk-o3laU0_*Yca<*N(xuCvZiSUvB}GcTYgts(fJLA;%k8puf5_as zq?jtiC!=X1nJ1&s7n{mMlNuh3CO(*$_!l*35xtRAqcuJmco7q0;yJT~;#2iLeD|9( zXJ)=L=bZUG`Bl=_;Zr>XzS7N(XMV0(_66xH%WaWGO}69+nIfZADVL^Lir`Ypt$8eu z=C!<9wNtvoVkt05LNSxF45nKpW;=#+re!-$ z<}#Ko4HOqvsI5Mzo5tX*Zly~zMn3MatF>}LFQyIC)U$@|9!T*7ZIo><`tcKVl6qF( zpm|DF?v;wv#h;a0X%l}_YNvkwnbb_ftG`JLRBi=DOyybUfWHwbj!g_A*20?j`_3e| zZ#$o$Vcsb{(8caV9~VHpibasO00{qsORNbEQG|m4QGQK^WB8Zy<)*`Ei~+XCbqs{A9@;fewr-%{|GD@mjLsB%w5!Rh~_pc8ir0Q|W`c1V*mw2km;v-q*_ z>hH>gw5Y3PF1(O|sAzZWre$0te zl4&tkjn>;{ZHTY{T)HKarOf3r>BJj3&WC3 z<+)gWs__h+*dGlf%+L!W8g>R8MI3Ab*j^N9eHvjBVG6)@W@gL9^Xx2e6Pq*^>)g$N zFL*>2LND^GfjG|wuXsgSi;52LP-rL;ga!MGdruA1aU#o~3*8-V0cui!6P0GK!zlz0 z|2Wjq3*EJaEw*#C78$@!E5t2=-w6Wy1Fu0uE>?}i8(D$3g(Fo==7Zr@s_@xxbKk!U zP86=~(dXv6q1w*nvP`sWEgH&JE^l_{OW7=B*vCHzhsvU5QIhS&riM2X-bc1VDyQNT zWh?hXXO{CuKf3@!4983YY)O>T?@9CqIlfMgZ<4+nWb7L str: return "other" - async def scan_directory(self, directory: Path, recursive: bool = True) -> List[FileMetadata]: + async def scan_directory( + self, directory: Path, recursive: bool = True + ) -> List[FileMetadata]: """扫描目录,创建文件元数据""" dir_path = validate_file_path(str(directory)) @@ -98,7 +105,9 @@ async def save_metadata(self, metadata: FileMetadata) -> FileMetadata: await self.db.flush() return metadata - async def organize_file(self, source: Path, target_dir: Path, category: Optional[str] = None) -> FileMetadata: + async def organize_file( + self, source: Path, target_dir: Path, category: Optional[str] = None + ) -> FileMetadata: """归档文件到目标目录""" source_path = validate_file_path(str(source)) target_base = validate_file_path(str(target_dir)) diff --git a/backend/app/core/git_manager.py b/backend/app/core/git_manager.py index 788948c..404e93f 100644 --- a/backend/app/core/git_manager.py +++ b/backend/app/core/git_manager.py @@ -1,11 +1,14 @@ """Git 仓库管理器""" -from pathlib import Path -from typing import Optional, List, Dict, Any from dataclasses import dataclass from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional + import pygit2 + +from app.config import settings from app.utils.logger import logger -from app.utils.validator import validate_git_url, validate_file_path +from app.utils.validator import validate_file_path, validate_git_url @dataclass @@ -56,7 +59,7 @@ def transfer_progress(self, stats): pct = stats.received_objects / stats.total_objects * 100 logger.debug(f"克隆进度: {pct:.1f}%") - repo = pygit2.clone_repository( + pygit2.clone_repository( validated_url, str(repo_path), callbacks=CloneProgress() @@ -143,7 +146,8 @@ def pull_repository(self, repo_path: Path) -> Dict[str, Any]: # 获取当前分支的远程跟踪 branch = repo.head.shorthand - remote_name = repo.branches[branch].upstream.remote_name if repo.branches[branch].upstream else "origin" + upstream = repo.branches[branch].upstream + remote_name = upstream.remote_name if upstream else "origin" remote = repo.remotes[remote_name] remote.fetch() @@ -165,7 +169,9 @@ def pull_repository(self, repo_path: Path) -> Dict[str, Any]: "remote": remote_name } - def commit_changes(self, repo_path: Path, message: str, files: Optional[List[str]] = None) -> str: + def commit_changes( + self, repo_path: Path, message: str, files: Optional[List[str]] = None + ) -> str: """提交更改""" repo = self.open_repository(repo_path) @@ -211,5 +217,4 @@ def commit_changes(self, repo_path: Path, message: str, files: Optional[List[str # 全局实例(使用配置的工作空间) -from app.config import settings git_manager = GitManager(settings.git_workspace) diff --git a/backend/app/core/llm.py b/backend/app/core/llm.py index 131cc96..24c85af 100644 --- a/backend/app/core/llm.py +++ b/backend/app/core/llm.py @@ -1,7 +1,9 @@ """大模型统一调度模块""" -from typing import Optional, AsyncIterator, List, Dict, Any +from typing import AsyncIterator, Dict, List, Optional + from langchain_core.language_models import BaseChatModel -from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage + from app.config import settings from app.utils.logger import logger from app.utils.validator import validate_model_config @@ -43,7 +45,10 @@ def _create_ollama_llm(self) -> BaseChatModel: def get_llm(self) -> BaseChatModel: """获取大模型实例""" - current_model = settings.openai_model if settings.llm_provider == "openai" else settings.ollama_model + current_model = ( + settings.openai_model if settings.llm_provider == "openai" + else settings.ollama_model + ) validate_model_config(settings.llm_provider, current_model) # 如果配置没变,返回缓存的实例 @@ -96,7 +101,9 @@ async def chat(self, messages: List[Dict[str, str]], **kwargs) -> str: logger.error(f"大模型调用失败: {e}") raise - async def stream_chat(self, messages: List[Dict[str, str]], **kwargs) -> AsyncIterator[str]: + async def stream_chat( + self, messages: List[Dict[str, str]], **kwargs + ) -> AsyncIterator[str]: """流式对话""" llm = self.get_llm() converted_messages = self._convert_messages(messages) diff --git a/backend/app/database.py b/backend/app/database.py index a2e2bc3..387bcd9 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -1,5 +1,6 @@ -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase + from app.config import settings engine = create_async_engine( diff --git a/backend/app/main.py b/backend/app/main.py index 56b605a..54683d7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,10 +1,12 @@ from contextlib import asynccontextmanager + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from app.api import files, git from app.config import settings from app.database import init_db -from app.api import files, git + @asynccontextmanager async def lifespan(app: FastAPI): diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 9b83909..08ee5f0 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,5 +1,5 @@ from app.models.file import FileMetadata -from app.models.project import Project from app.models.operation import OperationLog +from app.models.project import Project __all__ = ["FileMetadata", "Project", "OperationLog"] diff --git a/backend/app/models/__pycache__/__init__.cpython-314.pyc b/backend/app/models/__pycache__/__init__.cpython-314.pyc index a29ce3ecc54a6587d6810af15c6b649144ad42fd..47f8c4620ca2fd7f4b97eb5080ccaee38e121efb 100644 GIT binary patch delta 107 zcmcc4be)Myn~#@^0SH#SpU9P?&f{N@T9jClnV;vApRUPxi#?zyKPxr4#7~pCh#9D= zh!sSzOq?q-@sgMX3y^b5D6ybGFE>9WHK$mw0HS*0^CUjqy9{cNxJ;Yai@1TR0EtN- AkpKVy delta 109 zcmcc4be)Myn~#@^0SNdWP2|c^XAdaK&q_@$(PX^E<6n?klvt9PpXZaG?x)FI#0*qb z!~!B%C(f0bctu1C#JMGsSWuvso1c=JQ>>Q{(=zdS5}(>#2Hi(orcLZc+(2~zjmaO# diff --git a/backend/app/models/__pycache__/file.cpython-314.pyc b/backend/app/models/__pycache__/file.cpython-314.pyc index b4e69a10500b85296706c75375a9829f2c5d7101..e455b8aeb6b7b4c13a21b73b394918c3b93cdfc8 100644 GIT binary patch delta 522 zcmX>pwOxu=n~#@^0SJ6QXlHGm$h%XM-6=mmCp9te7Kck>Noq)DZt5*|&%Bb<^wgqT zY{4Z(nR)5ASVB@ON+!PdVC33t!MK1)N)l*5u^^CWU|6EDTxXHanu_%`D{HPO*-Y+Y z>5-B}NYCMz&ohf>NyPG)g)vtojKI=zNYWDXWoF4N$ylDVFz1S-@#G-ZEsTnj+1Xw& zDsKM3_JvVQ1*o=22T0uFOv}tkjW0+n$tcpD{EMTMQEhSor# zB2Ext1R``ngfWOP0TCdBicCR-8HnHj5#}Jm0z|NZ2ul#b4kD~Tgf)oJpX|r&EDv@c zBarU^aUMTN3PgaNcY5*$?wJNRI0PUSBWpEKBBWw5``VDOYd_6wT? Rqc7tGlWz&cF(+$ z)b!M%TkKBx`8lbHc@y7zFtTs9U|hf?D+)B=vj~uAU^ruO-sY6em57Av5h)iVQm%NX zPM*Qi!zl)oEK&dxAf63~S8NO<8W_GXFjO)+GJOY8u1udG6j0EO$qyvx#uUW#5ki4g z1~W~zW8K0iG5Is=3r2~}ci6r#im3t(D$)fKw>Z->b5i3A5=%0Q^d>*!D78@sNwI>Y zSdxIQE@B2UHJOSuK*E|pqL>LtC^Zzxf&{ccgf@_12jb#alc#fDkh;i{_EDCBk@2G( zgP{1timL(&K-aK_FnyAr+{Bd}2C}t?4Mc!^Q=|tXOhAMwhya;WWCkM4K?Em=umBO3 zAOaNZMOGk!14LMZ2pbS#Fxi^hnNf3cJ+}lK*eRzb&*h$}zd_=nbLti4v<9!6$_8K# z&?$wC!A!t_Q3+vc@cPce$m+}ZfdNE*SDu{4GmFt@G9$0J{Cx)ds|@z{84T_+$bMlF SV)SL4VDgOtL>I{bEd&60afE6B diff --git a/backend/app/models/__pycache__/operation.cpython-314.pyc b/backend/app/models/__pycache__/operation.cpython-314.pyc index 4560396c10d9cf65bf50a75970cee15aa428d84c..c5cb195c7a1b269d3c5c45d5dd79026eb7ab0dfb 100644 GIT binary patch delta 824 zcmXAn-%ry}6vuDZuIoBlH#X=5m%@f@gE7H@yr7bZWMG)d;L#tctbv&g@UA8hNLa#) zi7}yRRQ5mu#so}EFyYld!bTUdiNObrKKQU2B0hRg>BHyte0%P>=X+02g1>@duPE?{ zJa-zs5BnKi3{xdNK0GL+dnnILc(c65XMLK_iQN-TSwRzI)Pg!uo|RDEb5|IoVLL*a zsJ}?djKxe&-_BXwn4+mO#j@q9a%`3iLV^Hons-xed}n1plh&6fthz2GnBCLNYHiCdp*nU87FskW=uR zDDN1VGZKP&=GiTi$=E>< zLP!kh+1kwWTwREnOxA7zwH1ZhYKh&-W9c<-6I>I20w%)uMD#wkD?zykUJUO*b)0bBwkjdAXV>{z8pJbLC-giip+>4ve- zMOwaa;uHLo-|9M{T`{(rvsu(LY}C0cy+@qiY#SkR7h9b?<;ZXwFyYfq((VR2YZ!{< zlIM&e@9?@MRvwj<((JriUP&+FGKNzyxUia(rKNO1(Ub>DMa4~^JH3G4vqCTliyJ8~ y79N+>Yxp_|q45gLh5QQ?b&TR)QTtDn`G_*dsQ(9woUkE^-yHml$U=&ck^Tn}P{Jnw delta 840 zcmXAnK};G!6ozMIfkoH_1S__&C|Cup)CM9ZrHzTEESk~;X~Lz&Ev!l-NQY@+4~-C6l^XBFK^Y)MD!Xr!zd_SU3 z;rH~jb5ld`G1bOod|tE>n;$(v88+1~+KF9s5Qh<)Q@kh;!H)*f7|K|NP{#ULh?$@S zLd4m49|@T?%PPU8%7(^f2V2lith;|KBm@Td91rIT1vjU`)A@teJs&7EZPYBt|mU2ClsTFzpN zD`MZ#BXfiqxlRw`3BV+UW+xKfkV(EOkxd*@_AOKQ?Vy`V6@o`0%?u%1EENcbO9}Vk zF^~fkwLXeK9S?&L1dLNy>05iFxU7L)l}+k)#4x%84fvYefoG5JxF=LLqw}B&i1#$2 zZ&y?{syjh&A;H@ao7oTP4j)umQFntnfV@|mp@Zn&Ove*a*$27@R4;N5HFi3#QI%ak zAEXNQK?iV?>F^be%jYY~7i;RS160GX1y>QzvWz45zP@J#NJH*>Z@F`oE611Ic>&S9nf2=H{jjed?mz4zAaiFIgDunK9VQ zVcP6#)N2>Yq{A*{))wD2n$d*X^DSwsuql^cMz(Po!#FUQ&>T{=8YxOddMedq%!6*U z0+xH#U=SLYC~p^EY{~cV0u7<>+6{^`6T>iFG&5?wTVibgJ2o{^b%vA@U$ H3!~0|ia62_ diff --git a/backend/app/models/__pycache__/project.cpython-314.pyc b/backend/app/models/__pycache__/project.cpython-314.pyc index 8e43ff47dde4b472db876a8b5b4662bbbada3f24..d37757e8d0a25417d2983e8d822664e3b15f45d0 100644 GIT binary patch delta 724 zcmZ9J&ubGw6vy8ryPHk28xt@|vuTW`t!YSzwn^+C8O ziUvGX&`TKfP=wMFDE2jykH3>ea&osFA&$>7>U{X{oG^tI2zCz-c9YK z-mJY{f4RON%TbNpvm0*QKm&*n`)LnGLVpb}JesC$`%P{m(L> z{bT9d>fh{dE$-H~Yx|)Lw*Vw;^b9)^9*{7rh;b5OPsIq)*ej87B`-y}GA|7iZEr)G zChh@@l0>A~7e~R3Q?u;VWs?rE3vyuSJTApK3>!zRLBl8vBN80A-PmDE^2SgHY@*ch z10fv^e8J}0C-2z~2%_{W%(ZSberLb4H0efn_e>eTQV1tv1Tl)pAS6T<;XsTbE+NwF zwexbcn~Lyyq8G}8ONef$XH0eJ6Q5=82Q7DOw+%{->5c@0BV0M~jvn|j#|{C-6NQNR zHgKH8dCo`BONxDF5~tWZ*Z4!rW8A5gYqu8l`u+3@ty4UJ;w>ztyt(12fs$=aFll=rCK@ZiXADD3TKOiQC1Y%r6dgP=5M7jFj){8ec+0V>;-@MuHdpq0VAK}oL9_j#m z9vqB4yW9+&W3^^{a?14E{un%hoRaA<19rd++Cg`%WoMqs3y||hAm@9iPk87A z0K2ofnM|mTH*br@>Vl&btTo%w?i6jSuvoDiU#4nX3zl${Z2taivt=^hR)P)kST`pB z>_If()70nGc4~L-@t3*M&Rl8to+S)f_SBPM42uY%k6;k`2~om%#2K2z2nNU(yu2a) zFQy$ zmsYqPq-6J#UYaEdbdI2AhzY_~0$mPqji3=G2~&h|`PM%jb4^jTH`|^%DMWBRy_4Vl z){PHmcc!h~p>iYpb!gmOpr;j<=h(;L0&iqb6{e;S0dZ=e&6j`wE*&#F+9fwTCO2Fk zj$U6`DlW}GwJPh$Wl<5-Cqb1s!D6kJ#P%1T6zi4KU9!@`#4Y5z lK7=^*FveOC-v^@wsXa)wV0<5r{_p!9wygx1Cu@L|O diff --git a/backend/app/models/file.py b/backend/app/models/file.py index 323f073..c779cff 100644 --- a/backend/app/models/file.py +++ b/backend/app/models/file.py @@ -1,9 +1,12 @@ from datetime import datetime, timezone from typing import Optional -from sqlalchemy import String, Text, DateTime, Integer, Boolean + +from sqlalchemy import Boolean, DateTime, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column + from app.database import Base + class FileMetadata(Base): """文件元数据""" __tablename__ = "file_metadata" @@ -18,8 +21,14 @@ class FileMetadata(Base): project_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) size_bytes: Mapped[int] = mapped_column(Integer, default=0) is_archived: Mapped[bool] = mapped_column(Boolean, default=False) - created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + created_at: Mapped[datetime] = mapped_column( + DateTime, default=lambda: datetime.now(timezone.utc) + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc) + ) def __repr__(self) -> str: return f"" diff --git a/backend/app/models/operation.py b/backend/app/models/operation.py index 857462f..efb7a33 100644 --- a/backend/app/models/operation.py +++ b/backend/app/models/operation.py @@ -1,20 +1,30 @@ from datetime import datetime, timezone from typing import Optional -from sqlalchemy import String, Text, DateTime, Integer, JSON + +from sqlalchemy import JSON, DateTime, String, Text from sqlalchemy.orm import Mapped, mapped_column + from app.database import Base + class OperationLog(Base): """操作记录""" __tablename__ = "operation_logs" id: Mapped[int] = mapped_column(primary_key=True) - operation_type: Mapped[str] = mapped_column(String(50), index=True) # git_clone, file_move, etc. + operation_type: Mapped[str] = mapped_column( + String(50), index=True + ) # git_clone, file_move, etc. target_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) status: Mapped[str] = mapped_column(String(20)) # success, failed, pending details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) + created_at: Mapped[datetime] = mapped_column( + DateTime, default=lambda: datetime.now(timezone.utc) + ) def __repr__(self) -> str: - return f"" + return ( + f"" + ) diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 12c1077..6e6b53d 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -1,9 +1,12 @@ from datetime import datetime, timezone from typing import Optional -from sqlalchemy import String, Text, DateTime, Integer, JSON + +from sqlalchemy import JSON, DateTime, String, Text from sqlalchemy.orm import Mapped, mapped_column + from app.database import Base + class Project(Base): """项目配置""" __tablename__ = "projects" @@ -13,9 +16,15 @@ class Project(Base): description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) git_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) local_path: Mapped[str] = mapped_column(String(500)) - config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # 项目特定配置 - created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc)) - updated_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + config: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime, default=lambda: datetime.now(timezone.utc) + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc) + ) def __repr__(self) -> str: return f"" diff --git a/backend/app/plugins/__pycache__/manager.cpython-314.pyc b/backend/app/plugins/__pycache__/manager.cpython-314.pyc index e2e3c79f6f937a873d794ea154aba54535a65e2c..a39002983a69b3a8e13034580fe3e6102886ef35 100644 GIT binary patch delta 1184 zcma)5O-vI(6rS1L?sf~iv`be&#R_O;rJ%9+6HV}+v{4iaMx#+|u+)ky&Fm26qC^u< z8pXNjO%5h<@j_xujEQGY8cj@=5MulxosFlM_ z$k7+8xcB|_n{OXIn7=(md}nBi({k`Hd1-mH}a=W^`9OY8S=`70gveHj$Cg* zs0TWeNn)|5@@h`8Aa`JUdY$r2G6SZY(Kh0m=`UI@R!#mt zX=VrY+EfzE5C=yhQLtkiM1sa~Vu7O?AkHDhn7)c9dZ^DyxS8FmJhln?L!e5ej)Na2 zg(A;fAY>VAm6AI+;%HPI`|4>Uza-QTK&++6|dZ&81$*&AJlJK5#vF|K7U+Pw|5 z_$n#TQQ~qP8gTxfrY?3S)&Mt(vAUoXLxnNV!J-r%=0woFEmkY;VuRM|;O>V+J{>yV z@8*XKBoiq8M_YP$ln4F!Hjcg`Ike&vz+_r1#AXfE*U}yQI4Fk#^o|nrd?77 zDSG4x@F*OT$<#`&?sfGV+{5&Eiv_iHWYJRs14imtXMFF{dNA7oS^@R}7g1oJgsjA^^RE)==ZW--So1rkO$8nR delta 1216 zcma)5OH30%7@pbPZeMQMzCuBzJfvi$BC!Qyd*UOBsnuadZleNLe|fP$lD{u8c}mu_kBT)Xh0L>!BVoda0L>O4gV0 zQ@@CDW!Ggis>M+hwV@HU5skRh5~M(g26(I|+mO+z9!DnwZRn&v)L!R)E04Av?-}(l~XLBPE+U@_uV(kg0e8`{#0a z6ypH9D=jAiT)(D_A2`sTgju9sk*C7THnF}|>a2ov7J^E_anoD7SKc{$>HIsh|p4YeH0d_+l zn3f0XJT;&XkJeG^mNE68PQ!<$_g!_+!8bKhTVhXIGhhk8aqz zh+<2q{WWS|K-vP*7m#le^)8~0uj(O;TfQK`)lmuI*7w1d*$eODsn_w;n_&7%K#2VU DFL@AE diff --git a/backend/app/plugins/manager.py b/backend/app/plugins/manager.py index 533d4bf..edae0bf 100644 --- a/backend/app/plugins/manager.py +++ b/backend/app/plugins/manager.py @@ -1,9 +1,12 @@ """插件管理器""" -from pathlib import Path -from typing import Dict, List, Any -import pluggy import importlib import sys +from pathlib import Path +from typing import Any, Dict, List + +import pluggy + +from app.config import settings from app.plugins.hookspecs import PluginHookSpec from app.utils.logger import logger @@ -82,5 +85,4 @@ def call_hook(self, hook_name: str, **kwargs): # 全局实例 -from app.config import settings plugin_manager = PluginManager(settings.plugins_dir) diff --git a/backend/app/utils/__pycache__/logger.cpython-314.pyc b/backend/app/utils/__pycache__/logger.cpython-314.pyc index 04790a514690a18a2244cbf124d2f8d9d7c6850b..e16c5672834a40054891e21a66b5e7f98441b6fd 100644 GIT binary patch delta 247 zcmdlav_*(ln~#@^0SGETXlLD<$h%9M?W_YzZkAxGXAxxvYPiK# zkXV$OS8|IrH#;S>sEBFu8nzBLuz8}Bt=M1kePCkbHTlfUEXCEq_km-wK1aNcKu&&o zdS+g_UPfYGN=|A~aS;>9B=!QJ`*JdqiXi5eR2Bf$Oy0#2qGE8DLE9M?W_YzZkAxGXAxxrYPiK#kXV$OS8|IrH#;S>sEB3q z8nzBLuzA9ht=M1k+~A4%%*-Uk)xr0HZL%RpybgOoVo63$W>OJEdr4(MW?p&`GsrA~ soc#23AWttNF)t-2wWxUV9*z(}iMtHacNq-sGRS{t1G&v>vLdH201YBTX#fBK diff --git a/backend/app/utils/__pycache__/validator.cpython-314.pyc b/backend/app/utils/__pycache__/validator.cpython-314.pyc index 5cd42365076ac4866726ae6f0d645485a3d32d85..2fa55ee5ef7fc7c6ab2ecb178a8ac79719dd24da 100644 GIT binary patch delta 144 zcmew>-6X@S&Bx2d00eCxw6nT5^4f4pb23_eWB?LNoIil5EzDqQ2MbVea|&k@qh$b4 zc>}`(arp%j7sRw0{BN-HUtw3f!zX)#PkV*O3abs#2jnj}1YY0?`pnG4&-H}`(arp%j7sRw0{BN-HUtw3f!zX(|!FET+j;I6G7lM;61SMaPOS#OKdVwSLIk&)M hdG0HWW}Clo8!|GQO;+PQ&kHpAy9g(vND(j4H~ logging.Logger: ) file_handler.setLevel(logging.DEBUG) file_formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s' + '%(asctime)s - %(name)s - %(levelname)s - ' + '%(funcName)s:%(lineno)d - %(message)s' ) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) diff --git a/backend/app/utils/validator.py b/backend/app/utils/validator.py index 13abe0e..33676af 100644 --- a/backend/app/utils/validator.py +++ b/backend/app/utils/validator.py @@ -45,7 +45,9 @@ def validate_model_config(provider: str, model: str) -> None: """校验大模型配置""" valid_providers = ['openai', 'ollama'] if provider not in valid_providers: - raise ValidationError(f"不支持的模型提供商: {provider},支持: {valid_providers}") + raise ValidationError( + f"不支持的模型提供商: {provider},支持: {valid_providers}" + ) if not model or not model.strip(): raise ValidationError("模型名称不能为空") diff --git a/backend/logs/app.log b/backend/logs/app.log index 57ce39c..e129bd6 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -77,3 +77,45 @@ 2026-06-12 11:31:58,313 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin1 2026-06-12 11:31:58,313 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin2 2026-06-12 11:31:58,315 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: error_plugin +2026-06-12 11:37:03,007 - ai_dev_assistant - ERROR - get_git_status:60 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:37:03,016 - ai_dev_assistant - ERROR - get_recent_commits:82 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:37:03,048 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 4 个文件 +2026-06-12 11:37:03,058 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 1 个文件 +2026-06-12 11:37:03,101 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmp7eyqnnec/test.py -> /tmp/tmp7eyqnnec/organized/code/test.py +2026-06-12 11:37:03,113 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmplf08nzmg/test.py -> /tmp/tmplf08nzmg/organized/custom/test.py +2026-06-12 11:37:03,125 - ai_dev_assistant - INFO - organize_file:133 - 文件已归档: /tmp/tmpj4a6qwsf/test.py -> /tmp/tmpj4a6qwsf/organized/code/test_1.py +2026-06-12 11:37:03,137 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 3 个文件 +2026-06-12 11:37:03,157 - ai_dev_assistant - INFO - scan_directory:53 - 扫描完成,找到 10 个文件 +2026-06-12 11:37:03,315 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 3e7ed3a0a1132d24a7790e57c31523814fa91d62 +2026-06-12 11:37:03,345 - ai_dev_assistant - INFO - commit_changes:209 - 提交成功: 70c80e8e0c077a748d702065d47a50dc8dc88fa3 +2026-06-12 11:37:03,388 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:37:03,390 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:37:03,390 - ai_dev_assistant - INFO - unload_plugin:72 - 插件已卸载: test_plugin +2026-06-12 11:37:03,392 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:37:03,394 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: test_plugin +2026-06-12 11:37:03,398 - ai_dev_assistant - INFO - load_plugin:60 - 插件加载成功: auto_plugin +2026-06-12 11:37:03,400 - ai_dev_assistant - WARNING - discover_plugins:23 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:37:03,403 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin1 +2026-06-12 11:37:03,404 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: plugin2 +2026-06-12 11:37:03,406 - ai_dev_assistant - INFO - load_plugin:65 - 插件加载成功: error_plugin +2026-06-12 11:43:15,525 - ai_dev_assistant - ERROR - get_git_status:61 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:43:15,534 - ai_dev_assistant - ERROR - get_recent_commits:83 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:43:15,567 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 4 个文件 +2026-06-12 11:43:15,576 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 1 个文件 +2026-06-12 11:43:15,621 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpr1gkqgjn/test.py -> /tmp/tmpr1gkqgjn/organized/code/test.py +2026-06-12 11:43:15,634 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpbdvlduql/test.py -> /tmp/tmpbdvlduql/organized/custom/test.py +2026-06-12 11:43:15,646 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpdgs6q_e2/test.py -> /tmp/tmpdgs6q_e2/organized/code/test_1.py +2026-06-12 11:43:15,659 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 3 个文件 +2026-06-12 11:43:15,680 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 10 个文件 +2026-06-12 11:43:15,830 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 8ef2b869b35532c0ab0f43c2b6a2572da82da9c5 +2026-06-12 11:43:15,860 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 27f46ab177285cf5bdc32f2add987dc2fd3dd3f4 +2026-06-12 11:43:15,897 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:43:15,899 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:43:15,899 - ai_dev_assistant - INFO - unload_plugin:75 - 插件已卸载: test_plugin +2026-06-12 11:43:15,902 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:43:15,904 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:43:15,908 - ai_dev_assistant - INFO - load_plugin:63 - 插件加载成功: auto_plugin +2026-06-12 11:43:15,910 - ai_dev_assistant - WARNING - discover_plugins:26 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:43:15,913 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 +2026-06-12 11:43:15,913 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 +2026-06-12 11:43:15,915 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1aac911..16f8498 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -30,6 +30,9 @@ dev = [ "mypy>=1.8.0", ] +[tool.setuptools.packages.find] +include = ["app*"] + [tool.black] line-length = 88 From 32ae638b2e5f3904105c186dfc77f85b9ec8eec1 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:49:46 +0000 Subject: [PATCH 12/16] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=AE=A1=E7=90=86=20API=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/__init__.py | 1 + backend/app/api/plugins.py | 93 +++++++++++++++++++++++++++++++++++++ backend/app/main.py | 3 +- backend/logs/app.log | 42 +++++++++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/plugins.py diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..80ca775 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ +"""API 路由模块""" diff --git a/backend/app/api/plugins.py b/backend/app/api/plugins.py new file mode 100644 index 0000000..f988a2a --- /dev/null +++ b/backend/app/api/plugins.py @@ -0,0 +1,93 @@ +"""插件管理 API""" +from typing import List + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from app.plugins.manager import plugin_manager +from app.utils.logger import logger + +router = APIRouter(prefix="/plugins", tags=["plugins"]) + + +class PluginResponse(BaseModel): + """插件响应""" + name: str + status: str + + +class LoadPluginRequest(BaseModel): + """加载插件请求""" + name: str + module_path: str + + +@router.get("/", response_model=List[PluginResponse]) +async def list_plugins(): + """获取所有已加载的插件列表""" + plugins = plugin_manager.list_plugins() + return [ + PluginResponse(name=name, status="loaded") + for name in plugins + ] + + +@router.post("/load") +async def load_plugin(request: LoadPluginRequest): + """加载插件""" + try: + import importlib.util + import sys + + # 动态加载模块 + spec = importlib.util.spec_from_file_location( + request.name, + request.module_path + ) + if spec is None or spec.loader is None: + raise ValueError(f"无法加载模块: {request.module_path}") + + module = importlib.util.module_from_spec(spec) + sys.modules[request.name] = module + spec.loader.exec_module(module) + + # 加载插件 + plugin_manager.load_plugin(request.name, module) + + return { + "status": "success", + "message": f"插件 {request.name} 加载成功" + } + except Exception as e: + logger.error(f"加载插件失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/unload/{plugin_name}") +async def unload_plugin(plugin_name: str): + """卸载插件""" + try: + plugin_manager.unload_plugin(plugin_name) + return { + "status": "success", + "message": f"插件 {plugin_name} 已卸载" + } + except Exception as e: + logger.error(f"卸载插件失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/discover") +async def discover_plugins(): + """发现并加载所有插件""" + try: + plugin_manager.discover_plugins() + plugins = plugin_manager.list_plugins() + return { + "status": "success", + "message": f"发现 {len(plugins)} 个插件", + "plugins": plugins + } + except Exception as e: + logger.error(f"发现插件失败: {e}") + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/app/main.py b/backend/app/main.py index 54683d7..d014d77 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,7 +3,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api import files, git +from app.api import files, git, plugins from app.config import settings from app.database import init_db @@ -49,3 +49,4 @@ async def health_check(): # 注册 API 路由 app.include_router(files.router, prefix="/api/v1") app.include_router(git.router, prefix="/api/v1") +app.include_router(plugins.router, prefix="/api/v1") diff --git a/backend/logs/app.log b/backend/logs/app.log index e129bd6..9142c7a 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -119,3 +119,45 @@ 2026-06-12 11:43:15,913 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 2026-06-12 11:43:15,913 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 2026-06-12 11:43:15,915 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin +2026-06-12 11:46:47,157 - ai_dev_assistant - ERROR - get_git_status:61 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:46:47,166 - ai_dev_assistant - ERROR - get_recent_commits:83 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:46:47,197 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 4 个文件 +2026-06-12 11:46:47,206 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 1 个文件 +2026-06-12 11:46:47,249 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmp8wyew02m/test.py -> /tmp/tmp8wyew02m/organized/code/test.py +2026-06-12 11:46:47,260 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmp76bc3i58/test.py -> /tmp/tmp76bc3i58/organized/custom/test.py +2026-06-12 11:46:47,272 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpevqi53h3/test.py -> /tmp/tmpevqi53h3/organized/code/test_1.py +2026-06-12 11:46:47,283 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 3 个文件 +2026-06-12 11:46:47,303 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 10 个文件 +2026-06-12 11:46:47,456 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 479b3a1587252fd9ef43e19c67111446e0d0d847 +2026-06-12 11:46:47,484 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 8c139780ea2b9d08676282affdc5b9e6cd3a3003 +2026-06-12 11:46:47,521 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:46:47,523 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:46:47,523 - ai_dev_assistant - INFO - unload_plugin:75 - 插件已卸载: test_plugin +2026-06-12 11:46:47,528 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:46:47,530 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:46:47,534 - ai_dev_assistant - INFO - load_plugin:63 - 插件加载成功: auto_plugin +2026-06-12 11:46:47,536 - ai_dev_assistant - WARNING - discover_plugins:26 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:46:47,539 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 +2026-06-12 11:46:47,539 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 +2026-06-12 11:46:47,541 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin +2026-06-12 11:48:11,143 - ai_dev_assistant - ERROR - get_git_status:61 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:48:11,151 - ai_dev_assistant - ERROR - get_recent_commits:83 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:48:11,182 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 4 个文件 +2026-06-12 11:48:11,191 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 1 个文件 +2026-06-12 11:48:11,233 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpngbxj1hu/test.py -> /tmp/tmpngbxj1hu/organized/code/test.py +2026-06-12 11:48:11,244 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpjjioq1_g/test.py -> /tmp/tmpjjioq1_g/organized/custom/test.py +2026-06-12 11:48:11,256 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpl52eo59k/test.py -> /tmp/tmpl52eo59k/organized/code/test_1.py +2026-06-12 11:48:11,268 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 3 个文件 +2026-06-12 11:48:11,290 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 10 个文件 +2026-06-12 11:48:11,441 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 797e48aa9a59b27104a3de2986dcdbff788857fa +2026-06-12 11:48:11,470 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 442677382cde600a8048bf7610db7f80f5fb84f7 +2026-06-12 11:48:11,509 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:48:11,511 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:48:11,511 - ai_dev_assistant - INFO - unload_plugin:75 - 插件已卸载: test_plugin +2026-06-12 11:48:11,516 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:48:11,518 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:48:11,521 - ai_dev_assistant - INFO - load_plugin:63 - 插件加载成功: auto_plugin +2026-06-12 11:48:11,523 - ai_dev_assistant - WARNING - discover_plugins:26 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:48:11,527 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 +2026-06-12 11:48:11,527 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 +2026-06-12 11:48:11,529 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin From f047d5cba8294e9520630dd7bb3e225d9552ad44 Mon Sep 17 00:00:00 2001 From: badhope Date: Fri, 12 Jun 2026 11:53:35 +0000 Subject: [PATCH 13/16] =?UTF-8?q?feat:=20=E5=85=8B=E9=9A=86=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B9=B6=E9=85=8D=E7=BD=AE=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- backend/app/__pycache__/main.cpython-314.pyc | Bin 1986 -> 2088 bytes backend/logs/app.log | 63 +++++++++++ .../plans/2026-06-12-ai-dev-assistant.md | 106 +++++++++--------- 3 files changed, 119 insertions(+), 50 deletions(-) diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc index 441127996fcc1504d6dbd4c7b1a7bcf284195d60..60edea10247a254ed467f086a6dc2673fded8549 100644 GIT binary patch delta 641 zcmZut!EVz)5S^{#B#z^>7`Y)75mKO*sYF^Rid3P9V{*dE-Hn!blNcO3TDzbgxNzox zwFkZcsT`2_0VKp7iAtz|rQV=$1MvZfUHd?VI)}G!X5QPKop+1-OWJx(s{oJw@AdoR zL~EcwiP^DxN~|l#U9f#p3jE?o{>})>>;6%7X~kZp5@*A zjsgwCn65EN-rH;mPiac9A5Zt_RiVk?pSqeihodPOJ)jnQC2yk!`ytyqR1xM1f9Coq z494DsW)oCJ!NoDYha*C(;z~onWdT?N%6w= zf3uWt&q8@txLg)yteS?)FxbYp#12Eaw;$ib6Y_dtktp0)Mk$3A8PN9lS-LC?lo)&-sErA-WicwINqQH*qvO?OhQsdH~ChLjG{4c4R{5zk-R837$m1>~u zr+R>PtkzULHyCYks-7oFSmZS-gqV+Z|DSaS*~GSbYcaA=v%^NwOopNpD~ivD5wl>*{dM diff --git a/backend/logs/app.log b/backend/logs/app.log index 9142c7a..16d202b 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -161,3 +161,66 @@ 2026-06-12 11:48:11,527 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 2026-06-12 11:48:11,527 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 2026-06-12 11:48:11,529 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin +2026-06-12 11:49:35,051 - ai_dev_assistant - ERROR - get_git_status:61 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:49:35,059 - ai_dev_assistant - ERROR - get_recent_commits:83 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:49:35,090 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 4 个文件 +2026-06-12 11:49:35,098 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 1 个文件 +2026-06-12 11:49:35,144 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpzwsibjcf/test.py -> /tmp/tmpzwsibjcf/organized/code/test.py +2026-06-12 11:49:35,155 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmp_c0xsv22/test.py -> /tmp/tmp_c0xsv22/organized/custom/test.py +2026-06-12 11:49:35,167 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpmxmhxz3u/test.py -> /tmp/tmpmxmhxz3u/organized/code/test_1.py +2026-06-12 11:49:35,178 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 3 个文件 +2026-06-12 11:49:35,198 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 10 个文件 +2026-06-12 11:49:35,327 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 913d73a142190fddd075c74bf56cb200f95a654e +2026-06-12 11:49:35,351 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 3453f17c85f3f513eca02f67ca24cd4239398d1a +2026-06-12 11:49:35,383 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:49:35,385 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:49:35,385 - ai_dev_assistant - INFO - unload_plugin:75 - 插件已卸载: test_plugin +2026-06-12 11:49:35,387 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:49:35,388 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:49:35,392 - ai_dev_assistant - INFO - load_plugin:63 - 插件加载成功: auto_plugin +2026-06-12 11:49:35,393 - ai_dev_assistant - WARNING - discover_plugins:26 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:49:35,396 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 +2026-06-12 11:49:35,397 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 +2026-06-12 11:49:35,398 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin +2026-06-12 11:50:04,600 - ai_dev_assistant - ERROR - get_git_status:61 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:50:04,608 - ai_dev_assistant - ERROR - get_recent_commits:83 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:50:04,642 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 4 个文件 +2026-06-12 11:50:04,650 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 1 个文件 +2026-06-12 11:50:04,693 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpdqk0d7ms/test.py -> /tmp/tmpdqk0d7ms/organized/code/test.py +2026-06-12 11:50:04,704 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpu3ubp7u9/test.py -> /tmp/tmpu3ubp7u9/organized/custom/test.py +2026-06-12 11:50:04,715 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpfz6fwg1b/test.py -> /tmp/tmpfz6fwg1b/organized/code/test_1.py +2026-06-12 11:50:04,726 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 3 个文件 +2026-06-12 11:50:04,747 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 10 个文件 +2026-06-12 11:50:04,872 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 445c26bf3ca098e61f06528d21b4ac0d99a73a81 +2026-06-12 11:50:04,892 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: 412175dbef977cd86e5863d62e0fd612cc27c0cb +2026-06-12 11:50:04,924 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:50:04,926 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:50:04,926 - ai_dev_assistant - INFO - unload_plugin:75 - 插件已卸载: test_plugin +2026-06-12 11:50:04,927 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:50:04,929 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:50:04,932 - ai_dev_assistant - INFO - load_plugin:63 - 插件加载成功: auto_plugin +2026-06-12 11:50:04,934 - ai_dev_assistant - WARNING - discover_plugins:26 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:50:04,937 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 +2026-06-12 11:50:04,937 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 +2026-06-12 11:50:04,939 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin +2026-06-12 11:52:23,371 - ai_dev_assistant - ERROR - get_git_status:61 - 获取仓库状态失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:52:23,407 - ai_dev_assistant - ERROR - get_recent_commits:83 - 获取提交记录失败: 仓库路径不存在: /workspace/backend/nonexistent +2026-06-12 11:52:23,488 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 4 个文件 +2026-06-12 11:52:23,521 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 1 个文件 +2026-06-12 11:52:23,622 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpchz_wqab/test.py -> /tmp/tmpchz_wqab/organized/code/test.py +2026-06-12 11:52:23,653 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmp400xzqdr/test.py -> /tmp/tmp400xzqdr/organized/custom/test.py +2026-06-12 11:52:23,681 - ai_dev_assistant - INFO - organize_file:142 - 文件已归档: /tmp/tmpts4v70ix/test.py -> /tmp/tmpts4v70ix/organized/code/test_1.py +2026-06-12 11:52:23,722 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 3 个文件 +2026-06-12 11:52:23,770 - ai_dev_assistant - INFO - scan_directory:60 - 扫描完成,找到 10 个文件 +2026-06-12 11:52:24,100 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: c91d56e3eba42022232e3fa1381caa34564c57f7 +2026-06-12 11:52:24,162 - ai_dev_assistant - INFO - commit_changes:215 - 提交成功: cda3473cefd4db3e129905d39de66856d4468080 +2026-06-12 11:52:24,261 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:52:24,267 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:52:24,269 - ai_dev_assistant - INFO - unload_plugin:75 - 插件已卸载: test_plugin +2026-06-12 11:52:24,274 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:52:24,282 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: test_plugin +2026-06-12 11:52:24,425 - ai_dev_assistant - INFO - load_plugin:63 - 插件加载成功: auto_plugin +2026-06-12 11:52:24,429 - ai_dev_assistant - WARNING - discover_plugins:26 - 插件目录不存在: /nonexistent/plugins +2026-06-12 11:52:24,433 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin1 +2026-06-12 11:52:24,434 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: plugin2 +2026-06-12 11:52:24,439 - ai_dev_assistant - INFO - load_plugin:68 - 插件加载成功: error_plugin diff --git a/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md b/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md index 09a18c8..187a75b 100644 --- a/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md +++ b/docs/superpowers/plans/2026-06-12-ai-dev-assistant.md @@ -90,7 +90,7 @@ ai-dev-assistant/ - Create: `backend/app/main.py` - Create: `backend/app/config.py` -- [ ] **Step 1: 创建项目目录** +- [x] **Step 1: 创建项目目录** ```bash mkdir -p backend/app/{api,core,models,plugins/builtin,utils} @@ -98,7 +98,7 @@ mkdir -p backend/tests cd backend ``` -- [ ] **Step 2: 创建 pyproject.toml** +- [x] **Step 2: 创建 pyproject.toml** ```toml [build-system] @@ -145,7 +145,7 @@ python_version = "3.11" strict = true ``` -- [ ] **Step 3: 创建 requirements.txt(备用)** +- [x] **Step 3: 创建 requirements.txt(备用)** ```txt fastapi>=0.109.0 @@ -161,7 +161,7 @@ aiosqlite>=0.19.0 python-dotenv>=1.0.0 ``` -- [ ] **Step 4: 创建配置管理模块** +- [x] **Step 4: 创建配置管理模块** ```python # backend/app/config.py @@ -207,7 +207,7 @@ class Settings(BaseSettings): settings = Settings() ``` -- [ ] **Step 5: 创建 FastAPI 主入口** +- [x] **Step 5: 创建 FastAPI 主入口** ```python # backend/app/main.py @@ -244,7 +244,7 @@ async def health_check(): return {"status": "healthy"} ``` -- [ ] **Step 6: 创建 .env 示例文件** +- [x] **Step 6: 创建 .env 示例文件** ```bash # backend/.env.example @@ -268,7 +268,7 @@ GIT_WORKSPACE=./workspace PLUGINS_DIR=./plugins ``` -- [ ] **Step 7: 验证项目结构** +- [x] **Step 7: 验证项目结构** ```bash cd backend @@ -280,7 +280,7 @@ python -c "from app.config import settings; print(settings.app_name)" Expected: `AI Dev Assistant` -- [ ] **Step 8: 提交代码** +- [x] **Step 8: 提交代码** ```bash git init @@ -299,7 +299,7 @@ git commit -m "feat: initialize Python project structure with FastAPI" - Create: `backend/app/models/project.py` - Create: `backend/app/models/operation.py` -- [ ] **Step 1: 创建数据库连接模块** +- [x] **Step 1: 创建数据库连接模块** ```python # backend/app/database.py @@ -337,7 +337,7 @@ async def get_db(): raise ``` -- [ ] **Step 2: 创建文件元数据模型** +- [x] **Step 2: 创建文件元数据模型** ```python # backend/app/models/file.py @@ -368,7 +368,7 @@ class FileMetadata(Base): return f"" ``` -- [ ] **Step 3: 创建项目配置模型** +- [x] **Step 3: 创建项目配置模型** ```python # backend/app/models/project.py @@ -395,7 +395,7 @@ class Project(Base): return f"" ``` -- [ ] **Step 4: 创建操作记录模型** +- [x] **Step 4: 创建操作记录模型** ```python # backend/app/models/operation.py @@ -421,7 +421,7 @@ class OperationLog(Base): return f"" ``` -- [ ] **Step 5: 更新 models/__init__.py** +- [x] **Step 5: 更新 models/__init__.py** ```python # backend/app/models/__init__.py @@ -432,7 +432,7 @@ from app.models.operation import OperationLog __all__ = ["FileMetadata", "Project", "OperationLog"] ``` -- [ ] **Step 6: 更新 main.py 添加数据库初始化** +- [x] **Step 6: 更新 main.py 添加数据库初始化** ```python # backend/app/main.py (添加这部分) @@ -454,7 +454,7 @@ app = FastAPI( ) ``` -- [ ] **Step 7: 测试数据库初始化** +- [x] **Step 7: 测试数据库初始化** ```bash cd backend @@ -468,7 +468,7 @@ print('Database initialized successfully') Expected: `Database initialized successfully` -- [ ] **Step 8: 提交代码** +- [x] **Step 8: 提交代码** ```bash git add . @@ -483,7 +483,7 @@ git commit -m "feat: add database models for files, projects, and operations" - Create: `backend/app/utils/validator.py` - Create: `backend/app/utils/logger.py` -- [ ] **Step 1: 创建参数校验工具** +- [x] **Step 1: 创建参数校验工具** ```python # backend/app/utils/validator.py @@ -546,7 +546,7 @@ def validate_category(category: Optional[str]) -> Optional[str]: return category ``` -- [ ] **Step 2: 创建日志工具** +- [x] **Step 2: 创建日志工具** ```python # backend/app/utils/logger.py @@ -595,7 +595,7 @@ def setup_logger(name: str, log_file: Optional[str] = None) -> logging.Logger: logger = setup_logger("ai_dev_assistant", "logs/app.log") ``` -- [ ] **Step 3: 测试校验工具** +- [x] **Step 3: 测试校验工具** ```bash cd backend @@ -643,7 +643,7 @@ git commit -m "feat: add parameter validation and logging utilities" - Create: `backend/app/core/context.py` - Create: `backend/tests/test_llm.py` -- [ ] **Step 1: 创建上下文管理模块** +- [x] **Step 1: 创建上下文管理模块** ```python # backend/app/core/context.py @@ -687,7 +687,7 @@ class ConversationContext: self.messages.clear() ``` -- [ ] **Step 2: 创建大模型统一调度模块** +- [x] **Step 2: 创建大模型统一调度模块** ```python # backend/app/core/llm.py @@ -786,7 +786,7 @@ class LLMManager: llm_manager = LLMManager() ``` -- [ ] **Step 3: 编写测试** +- [x] **Step 3: 编写测试** ```python # backend/tests/test_llm.py @@ -843,7 +843,7 @@ async def test_llm_chat(): assert result == "test response" ``` -- [ ] **Step 4: 运行测试** +- [x] **Step 4: 运行测试** ```bash cd backend @@ -867,7 +867,7 @@ git commit -m "feat: implement unified LLM manager with OpenAI and Ollama suppor - Create: `backend/app/core/git_manager.py` - Create: `backend/tests/test_git.py` -- [ ] **Step 1: 创建 Git 管理器** +- [x] **Step 1: 创建 Git 管理器** ```python # backend/app/core/git_manager.py @@ -1083,7 +1083,7 @@ from app.config import settings git_manager = GitManager(settings.git_workspace) ``` -- [ ] **Step 2: 编写测试** +- [x] **Step 2: 编写测试** ```python # backend/tests/test_git.py @@ -1134,7 +1134,7 @@ def test_get_recent_commits(temp_workspace): assert commits[0].message ``` -- [ ] **Step 3: 运行测试** +- [x] **Step 3: 运行测试** ```bash cd backend @@ -1158,7 +1158,7 @@ git commit -m "feat: implement Git repository manager with clone, pull, commit" - Create: `backend/app/core/file_organizer.py` - Create: `backend/tests/test_file_organizer.py` -- [ ] **Step 1: 创建文件归档器** +- [x] **Step 1: 创建文件归档器** ```python # backend/app/core/file_organizer.py @@ -1325,7 +1325,7 @@ class FileOrganizer: return result.scalars().all() ``` -- [ ] **Step 2: 编写测试** +- [x] **Step 2: 编写测试** ```python # backend/tests/test_file_organizer.py @@ -1416,7 +1416,7 @@ async def test_search_files(temp_dir, db_session): assert results[0].file_name == "test1.py" ``` -- [ ] **Step 3: 运行测试** +- [x] **Step 3: 运行测试** ```bash cd backend @@ -1441,7 +1441,7 @@ git commit -m "feat: implement file organizer with auto-classification" **Files:** - Create: `backend/app/api/files.py` -- [ ] **Step 1: 创建文件管理 API 路由** +- [x] **Step 1: 创建文件管理 API 路由** ```python # backend/app/api/files.py @@ -1580,7 +1580,7 @@ async def delete_file( return {"message": "删除成功"} ``` -- [ ] **Step 2: 更新 main.py 注册路由** +- [x] **Step 2: 更新 main.py 注册路由** ```python # backend/app/main.py (添加这部分) @@ -1589,7 +1589,7 @@ from app.api import files app.include_router(files.router, prefix="/api/v1") ``` -- [ ] **Step 3: 测试 API** +- [x] **Step 3: 测试 API** ```bash cd backend @@ -1612,7 +1612,7 @@ git commit -m "feat: add file management REST API endpoints" **Files:** - Create: `backend/app/api/git.py` -- [ ] **Step 1: 创建 Git API 路由** +- [x] **Step 1: 创建 Git API 路由** ```python # backend/app/api/git.py @@ -1721,7 +1721,7 @@ async def commit_changes(request: CommitRequest): raise HTTPException(status_code=400, detail=str(e)) ``` -- [ ] **Step 2: 更新 main.py 注册路由** +- [x] **Step 2: 更新 main.py 注册路由** ```python # backend/app/main.py (添加这部分) @@ -1748,7 +1748,7 @@ git commit -m "feat: add Git operations REST API endpoints" - Create: `backend/app/plugins/manager.py` - Create: `backend/app/plugins/builtin/example_plugin.py` -- [ ] **Step 1: 定义插件钩子规范** +- [x] **Step 1: 定义插件钩子规范** ```python # backend/app/plugins/hookspecs.py @@ -1794,7 +1794,7 @@ class PluginHookSpec: pass ``` -- [ ] **Step 2: 创建插件管理器** +- [x] **Step 2: 创建插件管理器** ```python # backend/app/plugins/manager.py @@ -1875,7 +1875,7 @@ from app.config import settings plugin_manager = PluginManager(settings.plugins_dir) ``` -- [ ] **Step 3: 创建示例插件** +- [x] **Step 3: 创建示例插件** ```python # backend/app/plugins/builtin/example_plugin.py @@ -1918,7 +1918,7 @@ git commit -m "feat: implement plugin system with pluggy" - Create: `desktop/src/renderer/index.html` - Create: `desktop/src/renderer/App.tsx` -- [ ] **Step 1: 初始化 Electron 项目** +- [x] **Step 1: 初始化 Electron 项目** ```bash mkdir -p desktop/src/{main,renderer,preload} @@ -1926,7 +1926,7 @@ cd desktop npm init -y ``` -- [ ] **Step 2: 创建 package.json** +- [x] **Step 2: 创建 package.json** ```json { @@ -1965,7 +1965,7 @@ npm init -y } ``` -- [ ] **Step 3: 创建 TypeScript 配置** +- [x] **Step 3: 创建 TypeScript 配置** ```json { @@ -1990,7 +1990,7 @@ npm init -y } ``` -- [ ] **Step 4: 创建主进程入口** +- [x] **Step 4: 创建主进程入口** ```typescript // desktop/src/main/index.ts @@ -2046,7 +2046,7 @@ ipcMain.handle('get-backend-url', () => { }); ``` -- [ ] **Step 5: 创建渲染进程入口** +- [x] **Step 5: 创建渲染进程入口** ```html @@ -2064,7 +2064,7 @@ ipcMain.handle('get-backend-url', () => { ``` -- [ ] **Step 6: 创建 React 应用** +- [x] **Step 6: 创建 React 应用** ```tsx // desktop/src/renderer/App.tsx @@ -2110,7 +2110,7 @@ git commit -m "feat: initialize Electron desktop application" **Files:** - Create: `desktop/src/renderer/components/Avatar/index.tsx` -- [ ] **Step 1: 创建数字人组件** +- [x] **Step 1: 创建数字人组件** ```tsx // desktop/src/renderer/components/Avatar/index.tsx @@ -2182,7 +2182,7 @@ git commit -m "feat: add AI avatar component with Ready Player Me integration" **Files:** - Create: `backend/tests/integration/test_api.py` -- [ ] **Step 1: 创建 API 集成测试** +- [x] **Step 1: 创建 API 集成测试** ```python # backend/tests/integration/test_api.py @@ -2212,7 +2212,7 @@ def test_git_status_invalid_path(client): assert response.status_code == 400 ``` -- [ ] **Step 2: 运行集成测试** +- [x] **Step 2: 运行集成测试** ```bash cd backend @@ -2254,10 +2254,16 @@ git commit -m "test: add integration tests for API endpoints" --- -**Plan complete and saved to `docs/superpowers/plans/2026-06-12-ai-dev-assistant.md`. Two execution options:** +**Plan complete and saved to `docs/superpowers/plans/2026-06-12-ai-dev-assistant.md`.** -**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration +**Status: ALL TASKS COMPLETED ✓** -**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints +所有 12 个任务已全部完成: +- ✓ Task 1-3: Python 项目结构、数据库模型、工具函数 +- ✓ Task 4-6: 大模型调度、Git 管理、文件归档 +- ✓ Task 7-8: 文件管理 API、Git 操作 API +- ✓ Task 9: 插件系统核心(含插件管理 API) +- ✓ Task 10-11: Electron 项目、数字人组件 +- ✓ Task 12: 集成测试 -**Which approach?** +测试结果:47 passed, 1 warning (PytestCollectionWarning) From e448748840653387454239e38df032a4ea2fa377 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:02:24 +0000 Subject: [PATCH 14/16] chore(deps)(deps): bump the production-dependencies group with 5 updates Bumps the production-dependencies group with 5 updates: | Package | From | To | | --- | --- | --- | | [@expo/metro-runtime](https://github.com/expo/expo) | `56.0.14` | `56.0.15` | | [@react-navigation/bottom-tabs](https://github.com/react-navigation/react-navigation/tree/HEAD/packages/bottom-tabs) | `7.17.2` | `7.18.2` | | [@react-navigation/native](https://github.com/react-navigation/react-navigation/tree/HEAD/packages/native) | `7.3.0` | `7.3.3` | | [@react-navigation/native-stack](https://github.com/react-navigation/react-navigation/tree/HEAD/packages/native-stack) | `7.17.2` | `7.17.5` | | [expo](https://github.com/expo/expo/tree/HEAD/packages/expo) | `56.0.9` | `56.0.12` | Updates `@expo/metro-runtime` from 56.0.14 to 56.0.15 - [Changelog](https://github.com/expo/expo/blob/main/CHANGELOG.md) - [Commits](https://github.com/expo/expo/commits) Updates `@react-navigation/bottom-tabs` from 7.17.2 to 7.18.2 - [Release notes](https://github.com/react-navigation/react-navigation/releases) - [Changelog](https://github.com/react-navigation/react-navigation/blob/@react-navigation/bottom-tabs@7.18.2/packages/bottom-tabs/CHANGELOG.md) - [Commits](https://github.com/react-navigation/react-navigation/commits/@react-navigation/bottom-tabs@7.18.2/packages/bottom-tabs) Updates `@react-navigation/native` from 7.3.0 to 7.3.3 - [Release notes](https://github.com/react-navigation/react-navigation/releases) - [Changelog](https://github.com/react-navigation/react-navigation/blob/@react-navigation/native@7.3.3/packages/native/CHANGELOG.md) - [Commits](https://github.com/react-navigation/react-navigation/commits/@react-navigation/native@7.3.3/packages/native) Updates `@react-navigation/native-stack` from 7.17.2 to 7.17.5 - [Release notes](https://github.com/react-navigation/react-navigation/releases) - [Changelog](https://github.com/react-navigation/react-navigation/blob/@react-navigation/native-stack@7.17.5/packages/native-stack/CHANGELOG.md) - [Commits](https://github.com/react-navigation/react-navigation/commits/@react-navigation/native-stack@7.17.5/packages/native-stack) Updates `expo` from 56.0.9 to 56.0.12 - [Changelog](https://github.com/expo/expo/blob/main/CHANGELOG.md) - [Commits](https://github.com/expo/expo/commits/HEAD/packages/expo) --- updated-dependencies: - dependency-name: "@expo/metro-runtime" dependency-version: 56.0.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies - dependency-name: "@react-navigation/bottom-tabs" dependency-version: 7.18.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: production-dependencies - dependency-name: "@react-navigation/native" dependency-version: 7.3.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies - dependency-name: "@react-navigation/native-stack" dependency-version: 7.17.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies - dependency-name: expo dependency-version: 56.0.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 616 +++++++++++++++++++--------------------------- package.json | 8 +- 2 files changed, 251 insertions(+), 373 deletions(-) diff --git a/package-lock.json b/package-lock.json index 641a7aa..0f23e18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,13 @@ "version": "1.1.0", "license": "MIT", "dependencies": { - "@expo/metro-runtime": "~56.0.14", + "@expo/metro-runtime": "~56.0.15", "@react-native-async-storage/async-storage": "3.1.1", "@react-native-community/datetimepicker": "9.1.0", - "@react-navigation/bottom-tabs": "^7.17.2", + "@react-navigation/bottom-tabs": "^7.18.2", "@react-navigation/native": "^7.3.0", - "@react-navigation/native-stack": "^7.17.2", - "expo": "~56.0.9", + "@react-navigation/native-stack": "^7.17.5", + "expo": "~56.0.12", "expo-status-bar": "~56.0.4", "react": "19.2.7", "react-dom": "19.2.7", @@ -1238,6 +1238,99 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@expo/cli": { + "version": "56.1.16", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.16.tgz", + "integrity": "sha512-VBQn0mqAwc67b9Cn0RVXyeodghomAx5xGRhA/bXaQzuxDjMQk0zIOb6pXMZX7yiIwJW66UZt/zQiJNSv6aWJYw==", + "license": "MIT", + "dependencies": { + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~56.0.9", + "@expo/config-plugins": "~56.0.9", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.3.0", + "@expo/image-utils": "^0.10.1", + "@expo/inline-modules": "^0.0.12", + "@expo/json-file": "^10.2.0", + "@expo/log-box": "^56.0.13", + "@expo/metro": "~56.0.0", + "@expo/metro-config": "~56.0.14", + "@expo/metro-file-map": "^56.0.3", + "@expo/osascript": "^2.6.0", + "@expo/package-manager": "^1.12.1", + "@expo/plist": "^0.7.0", + "@expo/prebuild-config": "^56.0.16", + "@expo/require-utils": "^56.1.3", + "@expo/router-server": "^56.0.14", + "@expo/schema-utils": "^56.0.0", + "@expo/spawn-async": "^1.8.0", + "@expo/ws-tunnel": "^2.0.0", + "@expo/xcpretty": "^4.4.4", + "@react-native/dev-middleware": "0.85.3", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "dnssd-advertise": "^1.1.4", + "expo-server": "^56.0.5", + "fetch-nodeshim": "^0.4.10", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.2.1", + "multitars": "^1.0.0", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^4.0.4", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "terminal-link": "^2.1.1", + "toqr": "^0.1.1", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1", + "zod": "^3.25.76" + }, + "bin": { + "expo-internal": "main.js" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/cli/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -1266,12 +1359,12 @@ } }, "node_modules/@expo/config-plugins": { - "version": "56.0.8", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-56.0.8.tgz", - "integrity": "sha512-phTuyBhgVLfqUHMjQkAfRtbyoY6yTxoKja1awtpVnEkoJDxPJuXx1KX5uvq1eZtt4bJQ08OBJ6P95INqRSHpRg==", + "version": "56.0.9", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-56.0.9.tgz", + "integrity": "sha512-/6a/S9USwx8OC9tGjHxbviLFiBHyueN3aoNWMLvWDEJoZ1CIVW800ZBzwXq/FYNK2qzcN1LxFmQtzD1zeFQKNA==", "license": "MIT", "dependencies": { - "@expo/config-types": "^56.0.5", + "@expo/config-types": "^56.0.6", "@expo/json-file": "~10.2.0", "@expo/plist": "^0.7.0", "@expo/require-utils": "^56.1.3", @@ -1299,9 +1392,9 @@ } }, "node_modules/@expo/config-types": { - "version": "56.0.5", - "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-56.0.5.tgz", - "integrity": "sha512-GsAHO/MwW9ZRdgnmyfRXqVGLCP/zejD6rWnp5OROp8mBGRObKm4HfrjlUyT1skjMwCj1OrURx9ZfIc6yeBAkIA==", + "version": "56.0.6", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-56.0.6.tgz", + "integrity": "sha512-4Y6Aum5J4Re5NnxGVofRNe1aDwUBOmWhQYkynZsqzRtX/zEA1ADUeyHXuEckv9YD9djiyT7bKtLt5gKL3mA6VQ==", "license": "MIT" }, "node_modules/@expo/config/node_modules/semver": { @@ -1382,9 +1475,9 @@ } }, "node_modules/@expo/expo-modules-macros-plugin": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@expo/expo-modules-macros-plugin/-/expo-modules-macros-plugin-0.0.9.tgz", - "integrity": "sha512-odai6D7ng/gA7At8ukFcWcauNEeDdyVqzVPbQxDkyU2NTJ4kgphA4I5iigS5C4LXFicSIzEt2nzdlLM8sjsTdA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@expo/expo-modules-macros-plugin/-/expo-modules-macros-plugin-0.2.2.tgz", + "integrity": "sha512-4IMzPDIo/VOXREQjsJtliSfqYVZvfzU2SLFS/9sKMWF848S8CHx+e/E+Vf0TcMvpWCCKX5umyqxb13KJJ+YUzg==", "license": "MIT" }, "node_modules/@expo/fingerprint": { @@ -1485,12 +1578,12 @@ } }, "node_modules/@expo/inline-modules": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.0.11.tgz", - "integrity": "sha512-ZlIfKL61DPnW8YUTdMEjMA31xrDDV6p7Xi8rWYyhd5qXBV8MwGwjuJ7vKeaVaMjRqxJk1N9lv7zlfyvQpRCNNw==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@expo/inline-modules/-/inline-modules-0.0.12.tgz", + "integrity": "sha512-SNIZr/HWfIQPTZBwmukItxpc7ws1SgMUywYq1dnQvDknQDjJcuWAasIRFUjsK15yQ1xb4G5CP7VHtbN3V4lENg==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "~56.0.8" + "@expo/config-plugins": "~56.0.9" } }, "node_modules/@expo/json-file": { @@ -1514,9 +1607,9 @@ } }, "node_modules/@expo/log-box": { - "version": "56.0.12", - "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-56.0.12.tgz", - "integrity": "sha512-budE6AGmJbpOJfGSOz+JVP3+FevElT82IEIg+ukQ4gZpW/dGO7QX1unFjanKdSaYgudBwJ4FCFGMwWhW/1tXVQ==", + "version": "56.0.13", + "resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-56.0.13.tgz", + "integrity": "sha512-QWRZSpWPyjkDLVQio4R7oAzg/Av2MOt/DciFkfjr8qQ3qxGVn1Rt1oHP/80hvcWDcHFV7N6PqpyxRXw6nbxzKQ==", "license": "MIT", "dependencies": { "@expo/dom-webview": "^56.0.5", @@ -1553,9 +1646,9 @@ } }, "node_modules/@expo/metro-config": { - "version": "56.0.13", - "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-56.0.13.tgz", - "integrity": "sha512-OPyNYiex/6Ms8zT2POdIZsLhcAZYk7O+yJvpz5uG/4QRA7aiESfCy1I+0YHewMlR4P1YQeyxIrfTurs6m9xfZA==", + "version": "56.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-56.0.14.tgz", + "integrity": "sha512-O3CIHruaTJhswPAf/nf3i8QQ3f2jl+mEwSea1eb3khuplabdy/wTQz+JvHN8VGUFyg7JKwUGU1QfO6T3JiSQqA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -1578,7 +1671,6 @@ "hermes-parser": "^0.33.3", "jsc-safe-url": "^0.2.4", "lightningcss": "^1.30.1", - "msgpackr": "^2.0.1", "picomatch": "^4.0.4", "postcss": "^8.5.14", "resolve-from": "^5.0.0" @@ -1607,19 +1699,19 @@ } }, "node_modules/@expo/metro-runtime": { - "version": "56.0.14", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.14.tgz", - "integrity": "sha512-xqSWX7W1jd/B8MzDOJkc/iHAtIsHOMYrDya/jJkEj8A6XdN4XqtmxqfAQ2oWcpYi47vH97lECDp8aoP7jO6v0Q==", + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.15.tgz", + "integrity": "sha512-WIWeVsL6kCSB57oYZdUA4MTkH7c67UFMIjdNoQzKXwxZYwBFE/xL2cGPDC3z8RWt0femzJTVxAVZUOW/hiqRzA==", "license": "MIT", "dependencies": { - "@expo/log-box": "^56.0.12", + "@expo/log-box": "^56.0.13", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { - "@expo/log-box": "^56.0.12", + "@expo/log-box": "^56.0.13", "expo": "*", "react": "*", "react-dom": "*", @@ -1669,19 +1761,19 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "56.0.15", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-56.0.15.tgz", - "integrity": "sha512-6GC+QjdCkzp/5wjsqgfu/B2+2yf5MyZMtzf9szIPrLt9uKhzV2PdyM0vU0kvbj1YT8weHCtO7bsrzimman0sjA==", + "version": "56.0.16", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-56.0.16.tgz", + "integrity": "sha512-ce9ENfPWO4WUWUVQz0OaqL3KYZ7YofP8O35ncnn7CHCaKwQ7BqxcCGJbh+qvP1UjlWeNB3CjHPrXXJ3bnZwlJw==", "license": "MIT", "dependencies": { "@expo/config": "~56.0.9", - "@expo/config-plugins": "~56.0.8", - "@expo/config-types": "^56.0.5", + "@expo/config-plugins": "~56.0.9", + "@expo/config-types": "^56.0.6", "@expo/image-utils": "^0.10.1", "@expo/json-file": "^10.2.0", "@react-native/normalize-colors": "0.85.3", "debug": "^4.3.1", - "expo-modules-autolinking": "~56.0.15", + "expo-modules-autolinking": "~56.0.16", "resolve-from": "^5.0.0", "semver": "^7.6.0" } @@ -1717,6 +1809,40 @@ } } }, + "node_modules/@expo/router-server": { + "version": "56.0.14", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.14.tgz", + "integrity": "sha512-2UCTtZfcq1ZPgp3wk8/+sq9DvFI9UxrPr1jcEKMAF2DGAJLosnpc8GWNNg2hkjt6SHUOdFHIPxujWPYyho2y3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "@expo/metro-runtime": "^56.0.15", + "expo": "*", + "expo-constants": "^56.0.18", + "expo-font": "^56.0.6", + "expo-router": "*", + "expo-server": "^56.0.5", + "react": "*", + "react-dom": "*", + "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" + }, + "peerDependenciesMeta": { + "@expo/metro-runtime": { + "optional": true + }, + "expo-router": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, "node_modules/@expo/schema-utils": { "version": "56.0.1", "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-56.0.1.tgz", @@ -1748,10 +1874,13 @@ "license": "MIT" }, "node_modules/@expo/ws-tunnel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", - "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-2.0.0.tgz", + "integrity": "sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==", + "license": "MIT", + "peerDependencies": { + "ws": "^8.0.0" + } }, "node_modules/@expo/xcpretty": { "version": "4.4.4", @@ -1926,84 +2055,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", - "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", - "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", - "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", - "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", - "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", - "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@react-native-async-storage/async-storage": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-3.1.1.tgz", @@ -2307,17 +2358,17 @@ } }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.17.2.tgz", - "integrity": "sha512-Ojo0LTUTqQhrjIwlA2yGpof0mSjdIri1tdIaqKnsFX8yAEyvMxy64Dkgp57sO/+Jy6Xzc/1yzcORYLx59W3cYA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.18.2.tgz", + "integrity": "sha512-ZzeBTo0HPyqMQfbX7PIPm6z5lDS+UM+cHWxP/0I1RlzCAJnUa+MELWOHGTo/IVsajsnfs8h4laFoQaeXZRX5WA==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.22", + "@react-navigation/elements": "^2.9.25", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { - "@react-navigation/native": "^7.3.0", + "@react-navigation/native": "^7.3.3", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -2325,12 +2376,12 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.19.0.tgz", - "integrity": "sha512-OEXT9sSx+md4bqBVvg0sfFfVjRWHK+E7YHXobB/IQ/l8jqP3ThCwZk0DhxYmjwEGmUcCCkquXMxDG+hgMDeDSg==", + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.21.1.tgz", + "integrity": "sha512-9eOK71VFEyJjm1bP8o4Gf7YYppWPZ+RdNIjTc+WbSM7AFEH0XXEIHkkhpIBuB416sDlRZ1CRxHk2frh1HPBZqw==", "license": "MIT", "dependencies": { - "@react-navigation/routers": "^7.5.6", + "@react-navigation/routers": "^7.6.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -2350,9 +2401,9 @@ "license": "MIT" }, "node_modules/@react-navigation/elements": { - "version": "2.9.22", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.22.tgz", - "integrity": "sha512-n2u2ls34mvUVWLUpNYOWvE9agP5rGt/Fp7x8lo/oZnh03Bydr2HnGeovIWrt4OOI/p1gHor3PhWT+xhf10Xl7w==", + "version": "2.9.25", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.25.tgz", + "integrity": "sha512-cV7Hny55aE/uge6f4UB0pbGQbFl3XjXLMW+lTBNuJs8P7a9tuf7dkkPHxxgnLIvxE+KJVI2K8CGabfDk4Ht0Sg==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -2361,7 +2412,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.3.0", + "@react-navigation/native": "^7.3.3", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -2373,12 +2424,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.0.tgz", - "integrity": "sha512-ExKP2Y0d6e72JLEWEQFXmJe9QObjVWHYeKBUxaQm8ep+RbQbcBrbNOLrtN9Eb6tbGVGTVdzvAdQZbAYPh4dQww==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.3.tgz", + "integrity": "sha512-VjZngfeiyJestZPT99CKHRi3qOR6XwnEEPWb6uHF/L7fBI6RBVP842iJf61MUHRTzsWPUT+epJqdf1wm8N5YkQ==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.19.0", + "@react-navigation/core": "^7.21.1", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -2391,18 +2442,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.17.2.tgz", - "integrity": "sha512-a87vGNbQRCBLvudQELr16iYE7LO1lr828nkjdlkmVnvp16LDUb6cUXyCcC11bjEq0Dsf+azD9FL6zNnzIn52aQ==", + "version": "7.17.5", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.17.5.tgz", + "integrity": "sha512-jS67nzFlpuzNSDzAsL09hk59sNrp6zxViqKW+RzC5ia26VMxZBw2QFLBZN6rOnO3nOcpAKDpS8/Mx6vicurDfw==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.9.22", + "@react-navigation/elements": "^2.9.25", "color": "^4.2.3", "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.3.0", + "@react-navigation/native": "^7.3.3", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -2410,9 +2461,9 @@ } }, "node_modules/@react-navigation/routers": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.6.tgz", - "integrity": "sha512-tV/vSXX1va9xCXcqmLqr8qn56ygkwiC9tYoEEWfkqvTCh8LR9FEG7ZVzHBkzrQ/2hJQz+j/3KoglGSHXYrOK4w==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.0.tgz", + "integrity": "sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow==", "license": "MIT", "dependencies": { "nanoid": "^3.3.11" @@ -3182,9 +3233,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "56.0.14", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-56.0.14.tgz", - "integrity": "sha512-+JKVMYf3HajO3tPRA9DlKd/VhZOPTHyTzUo2yZajfMAoQ3l5VEdGVxm2MzX4DXMNKXwsC8GOeTRx7CrO/5dBDA==", + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-56.0.15.tgz", + "integrity": "sha512-0MqbQoM6nBUbKvgu2xJ4VixZnUTGTq3HB2WwvOikdO4CiPxbQ+wGA25fOoHHSni5iEFW39wy6y1ookTWlq3wVw==", "license": "MIT", "dependencies": { "@babel/generator": "^7.20.5", @@ -3233,7 +3284,7 @@ "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", - "expo-widgets": "^56.0.16", + "expo-widgets": "^56.0.18", "react-refresh": ">=0.14.0 <1.0.0" }, "peerDependenciesMeta": { @@ -3972,9 +4023,9 @@ } }, "node_modules/dnssd-advertise": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.4.tgz", - "integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.6.tgz", + "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, "node_modules/doctrine": { @@ -4500,31 +4551,31 @@ } }, "node_modules/expo": { - "version": "56.0.9", - "resolved": "https://registry.npmjs.org/expo/-/expo-56.0.9.tgz", - "integrity": "sha512-Zd/fhhyC600PO4cA14r+K+DlhhUZLNaDNF6dYg+hgne2kLvg9HMnkZ902sTPZYLkW56JOXLJ5dk7hsIoH26N2A==", + "version": "56.0.12", + "resolved": "https://registry.npmjs.org/expo/-/expo-56.0.12.tgz", + "integrity": "sha512-FxgdI/Yqva6iJOThZIHfvxlKPxs4EC4uScUnEswwSArR/Fj9k430O13R590LcOQTsdNsjIs+GBHwjfoAY6vmAQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^56.1.14", + "@expo/cli": "^56.1.16", "@expo/config": "~56.0.9", - "@expo/config-plugins": "~56.0.8", + "@expo/config-plugins": "~56.0.9", "@expo/devtools": "~56.0.2", "@expo/dom-webview": "~56.0.5", "@expo/fingerprint": "^0.19.4", "@expo/local-build-cache-provider": "^56.0.8", - "@expo/log-box": "^56.0.12", + "@expo/log-box": "^56.0.13", "@expo/metro": "~56.0.0", - "@expo/metro-config": "~56.0.13", + "@expo/metro-config": "~56.0.14", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~56.0.14", - "expo-asset": "~56.0.16", - "expo-constants": "~56.0.17", - "expo-file-system": "~56.0.7", - "expo-font": "~56.0.5", + "babel-preset-expo": "~56.0.15", + "expo-asset": "~56.0.17", + "expo-constants": "~56.0.18", + "expo-file-system": "~56.0.8", + "expo-font": "~56.0.7", "expo-keep-awake": "~56.0.3", - "expo-modules-autolinking": "~56.0.15", - "expo-modules-core": "~56.0.15", + "expo-modules-autolinking": "~56.0.16", + "expo-modules-core": "~56.0.17", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -4562,13 +4613,13 @@ } }, "node_modules/expo-asset": { - "version": "56.0.16", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-56.0.16.tgz", - "integrity": "sha512-iIxPo6C6+/d8JxGV74ZKZbIcCz2s8//dVl7oBAj124NcPMFhzdwycFBpMqq5LUxin+lVy5cCoEjv2LD8ulnkiQ==", + "version": "56.0.17", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-56.0.17.tgz", + "integrity": "sha512-GFN5j+8SPkyv0nfsiFHewmdB/D0tL237TsBE/gSfFOFy/J3a52py7IulcSqkA3sQE/u/UlD5BmvP5ssS4//nUg==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.10.1", - "expo-constants": "~56.0.17" + "expo-constants": "~56.0.18" }, "peerDependencies": { "expo": "*", @@ -4577,9 +4628,9 @@ } }, "node_modules/expo-constants": { - "version": "56.0.17", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.17.tgz", - "integrity": "sha512-bU8iU1+7cI7QzfGQVnz2C1nlbXD08YPwD6h8ZEuNspgUuD2prXfmrhrdLe1GjCPYGw4hB3BNjWPjpenNyyymfQ==", + "version": "56.0.18", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.18.tgz", + "integrity": "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw==", "license": "MIT", "dependencies": { "@expo/env": "~2.3.0" @@ -4590,9 +4641,9 @@ } }, "node_modules/expo-file-system": { - "version": "56.0.7", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-56.0.7.tgz", - "integrity": "sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA==", + "version": "56.0.8", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-56.0.8.tgz", + "integrity": "sha512-NrH41/8snGIBSbYicwVLB4txPdgCATd7ZYhMAGS3YJZ9GbnduhlAoV4/YCbGayjrbpE9bJb/6wegPL/zmvRMnQ==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -4600,9 +4651,9 @@ } }, "node_modules/expo-font": { - "version": "56.0.5", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-56.0.5.tgz", - "integrity": "sha512-WLoDu9hlEgPRKXJRR01HFLJ6Z2tFcORX/WFPRYBndmYc5kjQrFGH/j4BRaF3aBRPyYEAUXiUJybNLXkKCwEXQw==", + "version": "56.0.7", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-56.0.7.tgz", + "integrity": "sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg==", "license": "MIT", "dependencies": { "fontfaceobserver": "^2.1.0" @@ -4624,9 +4675,9 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "56.0.15", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.15.tgz", - "integrity": "sha512-WqpBFwLzn7DsrUkWltIjVmAjwuI1VdQ2jRMlvk1nh2kVadwdJBkSjUBQWRifsEePNhiMT/rFOovBolUU/ARt5w==", + "version": "56.0.16", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.16.tgz", + "integrity": "sha512-9JnL4N46P8ubDpDIfWolDn7nxU2j1rY67xY/dNVuyH0m+HG+r/JI16VYtjIf4COpZtEuFo4D3h3MBeFzGucMnw==", "license": "MIT", "dependencies": { "@expo/require-utils": "^56.1.3", @@ -4638,31 +4689,10 @@ "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, - "node_modules/expo-modules-core": { - "version": "56.0.15", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.15.tgz", - "integrity": "sha512-XOXuWjtUA/xF8VjMHoRTRxuAmrAeUv8QyASX3h/CpTNS58fOt3stV8EYW7BinJPJyqwV7BZoYV83iN0p2FzyZw==", - "license": "MIT", - "dependencies": { - "@expo/expo-modules-macros-plugin": "~0.0.9", - "expo-modules-jsi": "~56.0.8", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "react": "*", - "react-native": "*", - "react-native-worklets": "^0.7.4 || ^0.8.0" - }, - "peerDependenciesMeta": { - "react-native-worklets": { - "optional": true - } - } - }, "node_modules/expo-modules-jsi": { - "version": "56.0.8", - "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-56.0.8.tgz", - "integrity": "sha512-tXqFU1MHrf7Ctq+Pw0qOeIPDFl1W51p9nRRZy9vVUn4GNuAk1Av0vrj0SGLvcxJvDf3aGwSzr8o8dgUsX5sG0g==", + "version": "56.0.10", + "resolved": "https://registry.npmjs.org/expo-modules-jsi/-/expo-modules-jsi-56.0.10.tgz", + "integrity": "sha512-fHZcFpYO/o62GYa6fJyAQJZcAShzhoN0iMMDzbr7vD3ewET6e1vAlTonbEakN9F0VHEgBFJ4NREy87uwVcpCuA==", "license": "MIT", "peerDependencies": { "react-native": "*" @@ -4688,133 +4718,27 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/@expo/cli": { - "version": "56.1.14", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.14.tgz", - "integrity": "sha512-rSH3ygjEPipEYG6dgiJ116J8KqCQ/BYKcwQDipStSh4IFWJ10RZaYP4u5B74jxfeIWjWrOeqvwB6NZfQBjaQ4Q==", - "license": "MIT", - "dependencies": { - "@expo/code-signing-certificates": "^0.0.6", - "@expo/config": "~56.0.9", - "@expo/config-plugins": "~56.0.8", - "@expo/devcert": "^1.2.1", - "@expo/env": "~2.3.0", - "@expo/image-utils": "^0.10.1", - "@expo/inline-modules": "^0.0.11", - "@expo/json-file": "^10.2.0", - "@expo/log-box": "^56.0.12", - "@expo/metro": "~56.0.0", - "@expo/metro-config": "~56.0.13", - "@expo/metro-file-map": "^56.0.3", - "@expo/osascript": "^2.6.0", - "@expo/package-manager": "^1.12.1", - "@expo/plist": "^0.7.0", - "@expo/prebuild-config": "^56.0.15", - "@expo/require-utils": "^56.1.3", - "@expo/router-server": "^56.0.13", - "@expo/schema-utils": "^56.0.0", - "@expo/spawn-async": "^1.8.0", - "@expo/ws-tunnel": "^1.0.1", - "@expo/xcpretty": "^4.4.4", - "@react-native/dev-middleware": "0.85.3", - "accepts": "^1.3.8", - "arg": "^5.0.2", - "bplist-creator": "0.1.0", - "bplist-parser": "^0.3.1", - "chalk": "^4.0.0", - "ci-info": "^3.3.0", - "compression": "^1.7.4", - "connect": "^3.7.0", - "debug": "^4.3.4", - "dnssd-advertise": "^1.1.4", - "expo-server": "^56.0.5", - "fetch-nodeshim": "^0.4.10", - "getenv": "^2.0.0", - "glob": "^13.0.0", - "lan-network": "^0.2.1", - "multitars": "^1.0.0", - "node-forge": "^1.3.3", - "npm-package-arg": "^11.0.0", - "ora": "^3.4.0", - "picomatch": "^4.0.4", - "pretty-format": "^29.7.0", - "progress": "^2.0.3", - "prompts": "^2.3.2", - "resolve-from": "^5.0.0", - "semver": "^7.6.0", - "send": "^0.19.0", - "slugify": "^1.3.4", - "stacktrace-parser": "^0.1.10", - "structured-headers": "^0.4.1", - "terminal-link": "^2.1.1", - "toqr": "^0.1.1", - "wrap-ansi": "^7.0.0", - "ws": "^8.12.1", - "zod": "^3.25.76" - }, - "bin": { - "expo-internal": "main.js" - }, - "peerDependencies": { - "expo": "*", - "expo-router": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "expo-router": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/expo/node_modules/@expo/router-server": { - "version": "56.0.13", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.13.tgz", - "integrity": "sha512-M2H2zHlRBKIPENCWV8Gqo3/9WANCS9vvOMCcdWfS9wD8XXMnDASFniS0bBoGwwS1qq1LIpYzX8m8wdv7Awy88g==", + "node_modules/expo/node_modules/expo-modules-core": { + "version": "56.0.17", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.17.tgz", + "integrity": "sha512-5J8whnT7Ccp+BrFClLmpF76omBqn95VZExroTm01Dgjm4vpty1Rb7U3we+ZUceNHtRd07Lw30u7FNfDgIhEbRQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.4" + "@expo/expo-modules-macros-plugin": "0.2.2", + "expo-modules-jsi": "~56.0.10", + "invariant": "^2.2.4" }, "peerDependencies": { - "@expo/metro-runtime": "^56.0.14", - "expo": "*", - "expo-constants": "^56.0.17", - "expo-font": "^56.0.5", - "expo-router": "*", - "expo-server": "^56.0.5", "react": "*", - "react-dom": "*", - "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" + "react-native": "*", + "react-native-worklets": "^0.7.4 || ^0.8.0" }, "peerDependenciesMeta": { - "@expo/metro-runtime": { - "optional": true - }, - "expo-router": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-server-dom-webpack": { + "react-native-worklets": { "optional": true } } }, - "node_modules/expo/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -7158,37 +7082,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/msgpackr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", - "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.4" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", - "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" - } - }, "node_modules/multitars": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.0.tgz", @@ -7277,21 +7170,6 @@ "node": ">= 6.13.0" } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8446,9 +8324,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", - "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" diff --git a/package.json b/package.json index a891292..2617c6c 100644 --- a/package.json +++ b/package.json @@ -51,13 +51,13 @@ "node": ">=18" }, "dependencies": { - "@expo/metro-runtime": "~56.0.14", + "@expo/metro-runtime": "~56.0.15", "@react-native-async-storage/async-storage": "3.1.1", "@react-native-community/datetimepicker": "9.1.0", - "@react-navigation/bottom-tabs": "^7.17.2", + "@react-navigation/bottom-tabs": "^7.18.2", "@react-navigation/native": "^7.3.0", - "@react-navigation/native-stack": "^7.17.2", - "expo": "~56.0.9", + "@react-navigation/native-stack": "^7.17.5", + "expo": "~56.0.12", "expo-status-bar": "~56.0.4", "react": "19.2.7", "react-dom": "19.2.7", From 670b22c42876fabc99a1df52573432c562ead6d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:03:09 +0000 Subject: [PATCH 15/16] chore(deps)(deps-dev): bump the development-dependencies group with 6 updates Bumps the development-dependencies group with 6 updates: | Package | From | To | | --- | --- | --- | | [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.4` | `10.0.1` | | [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.5.0` | | [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `5.2.0` | `7.1.1` | | [globals](https://github.com/sindresorhus/globals) | `15.15.0` | `17.6.0` | | [typescript](https://github.com/microsoft/TypeScript) | `5.9.3` | `6.0.3` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.1` | `8.61.1` | Updates `@eslint/js` from 9.39.4 to 10.0.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js) Updates `eslint` from 9.39.4 to 10.5.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.5.0) Updates `eslint-plugin-react-hooks` from 5.2.0 to 7.1.1 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/react/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks) Updates `globals` from 15.15.0 to 17.6.0 - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v15.15.0...v17.6.0) Updates `typescript` from 5.9.3 to 6.0.3 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3) Updates `typescript-eslint` from 8.60.1 to 8.61.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@eslint/js" dependency-version: 10.0.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: eslint dependency-version: 10.5.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: eslint-plugin-react-hooks dependency-version: 7.1.1 dependency-type: direct:development update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: globals dependency-version: 17.6.0 dependency-type: direct:development update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: typescript dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 511 +++++++++++++++++++++++----------------------- package.json | 12 +- 2 files changed, 267 insertions(+), 256 deletions(-) diff --git a/package-lock.json b/package-lock.json index 641a7aa..85610d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,14 +28,14 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@eslint/js": "^9.39.4", + "@eslint/js": "^10.0.1", "@types/react": "~19.2.17", - "eslint": "^9.39.4", + "eslint": "^10.5.0", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "globals": "^15.15.0", - "typescript": "^5.1.3", - "typescript-eslint": "^8.60.1" + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.6.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.1" }, "engines": { "node": ">=18" @@ -1084,158 +1084,128 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.5" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.15" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@eslint/eslintrc/node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@eslint/core": "^1.2.1" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@expo/code-signing-certificates": { @@ -2424,6 +2394,13 @@ "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2497,17 +2474,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz", - "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/type-utils": "8.60.1", - "@typescript-eslint/utils": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2520,7 +2497,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.60.1", + "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2536,16 +2513,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "engines": { @@ -2561,14 +2538,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "engines": { @@ -2583,14 +2560,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz", - "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2601,9 +2578,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", "dev": true, "license": "MIT", "engines": { @@ -2618,15 +2595,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2643,9 +2620,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", "dev": true, "license": "MIT", "engines": { @@ -2657,16 +2634,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.60.1", - "@typescript-eslint/tsconfig-utils": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1", + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2724,9 +2701,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", "dev": true, "license": "ISC", "bin": { @@ -2737,16 +2714,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2761,13 +2738,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2778,19 +2755,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@ungap/structured-clone": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", @@ -4245,33 +4209,33 @@ } }, "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", - "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -4281,8 +4245,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4290,7 +4253,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -4338,16 +4301,40 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react-hooks/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" } }, "node_modules/eslint-plugin-react/node_modules/resolve": { @@ -4375,35 +4362,60 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4417,19 +4429,35 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -5236,9 +5264,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -6503,13 +6531,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -7664,29 +7685,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parent-module/node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/parse-png": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", @@ -9485,9 +9483,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -9499,16 +9497,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", - "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.60.1", - "@typescript-eslint/parser": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1" + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10069,6 +10067,19 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "5.0.14", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", diff --git a/package.json b/package.json index a891292..dc04a17 100644 --- a/package.json +++ b/package.json @@ -70,14 +70,14 @@ }, "devDependencies": { "@babel/core": "^7.20.0", - "@eslint/js": "^9.39.4", + "@eslint/js": "^10.0.1", "@types/react": "~19.2.17", - "eslint": "^9.39.4", + "eslint": "^10.5.0", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", - "globals": "^15.15.0", - "typescript": "^5.1.3", - "typescript-eslint": "^8.60.1" + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.6.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.1" }, "private": true } From 6e3f986a712c0e22a7af840c17369f7dc11d153b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:03:44 +0000 Subject: [PATCH 16/16] ci(deps): bump pnpm/action-setup in the production-dependencies group Bumps the production-dependencies group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup). Updates `pnpm/action-setup` from 6.0.8 to 6.0.9 - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/0e279bb959325dab635dd2c09392533439d90093...0ebf47130e4866e96fce0953f49152a61190b271) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: 6.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6d9831..60fb564 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: - name: Install pnpm if: steps.pm.outputs.pm == 'pnpm' - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v4.1.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v4.1.0 with: version: 9 @@ -116,7 +116,7 @@ jobs: - name: Install pnpm if: steps.pm.outputs.pm == 'pnpm' - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v4.1.0 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v4.1.0 with: version: 9