From 8caa5e7b2cca935a3427b6946d5969d7c1cd2b26 Mon Sep 17 00:00:00 2001 From: Igor Lebedev Date: Thu, 28 Aug 2025 23:18:02 +0500 Subject: [PATCH 1/4] Release v1.0.0 - Update version to 1.0.0 in package.json - Add changelog entry for v1.0.0 stable release - First production-ready release with major stability improvements --- ProjectGraphAgent/CHANGELOG.md | 9 +++++++++ ProjectGraphAgent/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ProjectGraphAgent/CHANGELOG.md b/ProjectGraphAgent/CHANGELOG.md index 777b9c90..f573e5fd 100644 --- a/ProjectGraphAgent/CHANGELOG.md +++ b/ProjectGraphAgent/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v1.0.0 (2025-01-28) +- **Breaking Release**: First stable v1.0.0 release of ProjectGraphAgent +- Major stability improvements and production-ready features +- Enhanced drift detection algorithms and performance optimizations +- Comprehensive validator improvements and error handling +- Extended adapter support and Jsonnet configuration enhancements +- Improved CI/CD workflows and deployment automation +- Better documentation and developer experience + ## v0.1.0-alpha - Initial public alpha of ProjectGraphAgent - Declared graph via Jsonnet, observed adapters (TS/Python), drift computation diff --git a/ProjectGraphAgent/package.json b/ProjectGraphAgent/package.json index 8992c4f7..d29110a7 100644 --- a/ProjectGraphAgent/package.json +++ b/ProjectGraphAgent/package.json @@ -1,6 +1,6 @@ { "name": "project-graph-agent", - "version": "0.1.157", + "version": "1.0.0", "description": "Jsonnet-driven project control system for AI agents", "main": "scripts/graph_generator.mjs", "type": "module", From 5478167b82ca08bf05153fd9713630f7ec6ebd54 Mon Sep 17 00:00:00 2001 From: Igor Lebedev Date: Fri, 29 Aug 2025 14:49:36 +0500 Subject: [PATCH 2/4] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B5=D0=BC=D1=8B=20=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=83=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B2=20REA?= =?UTF-8?q?DME=20=D0=B8=20WORKFLOW=5FGUIDE.=20=D0=92=D0=BD=D0=B5=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D0=B8?= =?UTF-8?q?=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=B0=D0=BC=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8.?= =?UTF-8?q?=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D0=B8=D0=BF=D1=82=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BF=D1=83=D1=82=D1=8F=D0=BC.=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B=20.gitignore=20=D0=B8=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=D1=81=D0=BF=D0=BE=D0=BC=D0=BE=D0=B3=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectGraphAgent/.gitignore | 2 +- ProjectGraphAgent/LLM_GUIDELINES.md | 13 +- ProjectGraphAgent/README.md | 79 ++- ProjectGraphAgent/README.ru.md | 56 +- ProjectGraphAgent/WORKFLOW_GUIDE.md | 42 +- ProjectGraphAgent/WORKFLOW_GUIDE.ru.md | 42 +- .../graph_parts/ai_commands.jsonnet | 25 + ProjectGraphAgent/graph_parts/meta.jsonnet | 14 +- .../graph_parts/path_index.jsonnet | 81 +-- .../graph_parts/path_search_examples.jsonnet | 4 +- .../graph_parts/templates.jsonnet | 16 +- .../scripts/publish_workflow.mjs | 2 +- .../scripts/sync_to_standalone.mjs | 2 +- ProjectGraphAgent/test_path_search.js | 85 +++ .../plugins/ozon-analyzer/mcp_server.py | 542 +++++------------- .../plugins/ozon-analyzer/workflow.json | 10 +- memory-bank/audit_logs.md | 1 + memory-bank/diagrams/graph.mmd | 9 +- memory-bank/drift.md | 11 +- memory-bank/plans/README.md | 2 +- 20 files changed, 505 insertions(+), 533 deletions(-) create mode 100644 ProjectGraphAgent/test_path_search.js diff --git a/ProjectGraphAgent/.gitignore b/ProjectGraphAgent/.gitignore index 41870bed..ae992705 100644 --- a/ProjectGraphAgent/.gitignore +++ b/ProjectGraphAgent/.gitignore @@ -1,3 +1,3 @@ .cache/ - +.git.backup diff --git a/ProjectGraphAgent/LLM_GUIDELINES.md b/ProjectGraphAgent/LLM_GUIDELINES.md index 79bc3b90..764eb49f 100644 --- a/ProjectGraphAgent/LLM_GUIDELINES.md +++ b/ProjectGraphAgent/LLM_GUIDELINES.md @@ -2,7 +2,7 @@ Hello, Large Language Model! -This `project_graph/` directory is designed to be a structured, machine-readable representation of the `tsx_viewer` project's architecture and operational guidelines. Its primary purpose is to facilitate your deeper understanding and more effective interaction with this codebase. +This `project_graph/` directory is designed to be a structured, machine-readable representation of the `agent_plugins_platform` project's architecture and operational guidelines. Its primary purpose is to facilitate your deeper understanding and more effective interaction with this codebase. ## Why This System Exists for You (the LLM): @@ -24,6 +24,7 @@ Traditional documentation is often unstructured and difficult for AI to parse co * `policies`: Architectural rules, design principles, and conventions that the project adheres to. * `commitGroups`: Rules for categorizing changes into atomic Git commits (e.g., `feat`, `fix`, `docs`). This helps you understand the project's commit hygiene and can guide your commit message generation. * `aiCommands`: Mappings of conversational trigger phrases to `npm` commands. This section is particularly important for understanding how the project expects AI agents to execute specific actions. + * `templates.PathSearch`: A powerful indexing system for fast file path lookups with multiple search strategies (by exact path, directory, file type, name patterns). This enables efficient navigation and analysis of large codebases. 3. **Prioritize Structured Data:** Whenever possible, extract information directly from the compiled JSON output of `project_graph.jsonnet` rather than relying on natural language descriptions in READMEs or comments. This provides the most precise and unambiguous context. @@ -37,9 +38,17 @@ Traditional documentation is often unstructured and difficult for AI to parse co 6. **Drift and Plans Awareness:** Prefer the compiled `project_graph/.cache/graph.json`. It includes `observed` (adapter output) and `drift`. Reference `memory-bank/plans/` markdown for human-facing plan summaries; edit `graph_parts/plans.jsonnet` for canonical plan definitions. +7. **Path Indexing System:** Leverage the `templates.PathSearch` functions for efficient file and entity lookups: + * Use `findByPath()` for exact file lookups + * Use `findByDirectory()` to explore directory contents + * Use `findByFileType()` to find all files of a specific type (e.g., all `.ts` files) + * Use `findByPattern()` for flexible pattern-based searches + * Always check `pathExists()` before accessing files + * Use `getIndexStats()` to understand the scope of the indexed codebase + ## Your Role in Maintaining This System: * **Self-Correction:** If you identify discrepancies between the `project_graph` and the actual codebase, or if you encounter new patterns that should be formalized, suggest updates to the `project_graph.jsonnet`. * **Proactive Use:** Actively query and integrate information from the `project_graph` into your reasoning processes for all tasks related to this project. -By leveraging the `project_graph` system, you can become a more effective, context-aware, and integrated collaborator within the `tsx_viewer` project. \ No newline at end of file +By leveraging the `project_graph` system, you can become a more effective, context-aware, and integrated collaborator within the `agent_plugins_platform` project. \ No newline at end of file diff --git a/ProjectGraphAgent/README.md b/ProjectGraphAgent/README.md index c7f0f807..ebfba8aa 100644 --- a/ProjectGraphAgent/README.md +++ b/ProjectGraphAgent/README.md @@ -5,10 +5,12 @@ ProjectGraphAgent is a Jsonnet-driven project control system designed for AI age ## Key Features - **Declared vs Observed Graph**: Jsonnet "declared" model + language adapters "observed" model → automatic drift detection +- **Path Indexing System**: Fast file path lookups with multiple search strategies (by path, directory, file type, name patterns) - **Agent-Friendly Outputs**: Compiled graph JSON, drift reports, Mermaid diagrams, plans markdown, snapshots and events - **Automation**: Grouped commits, AI command synchronization, CI workflow integration - **Multi-Language Support**: TypeScript/JavaScript and Python adapters (extensible) + ## Structure * `project_graph.jsonnet` @@ -28,7 +30,47 @@ ProjectGraphAgent is a Jsonnet-driven project control system designed for AI age * `publish_workflow.mjs`: A script related to the project graph. * `sync_ai_commands.mjs`: The script that synchronizes AI command definitions across various AI assistant rule files. * `sync_to_standalone.mjs`: A script related to the project graph. - + + ## Path Indexing System + + ProjectGraphAgent includes a powerful path indexing system that enables fast lookups of files and entities by various criteria: + + ### Search Functions + + - **`findByPath(path)`**: Find entity by exact file path + - **`findByDirectory(dir)`**: Find all files in a directory + - **`findByFileType(ext)`**: Find all files with specific extension + - **`findByFileName(name)`**: Find files with specific name + - **`findByPattern(pattern)`**: Search files containing a pattern + - **`pathExists(path)`**: Check if file exists in index + - **`getIndexStats()`**: Get statistics about the index + + ### Index Types + + - **Path Index**: Direct path → entity mapping + - **Directory Index**: Directory → list of entities + - **File Type Index**: Extension → list of entities + - **File Name Index**: Name → list of entities + + ### Usage Examples + + ```jsonnet + // Find a specific configuration file + local config = graph.templates.PathSearch.findByPath("package.json"); + + // Get all TypeScript files + local tsFiles = graph.templates.PathSearch.findByFileType("ts"); + + // Find all files in src directory + local srcFiles = graph.templates.PathSearch.findByDirectory("src"); + + // Check if README exists + local hasReadme = graph.templates.PathSearch.pathExists("README.md"); + ``` + + This indexing system is particularly valuable for AI agents, enabling efficient navigation and analysis of large codebases. + + ## Usage ### Quick Start @@ -78,6 +120,7 @@ Add to `.github/workflows/*.yml`: run: node ProjectGraphAgent/scripts/graph_validator.mjs ``` + ## AI Assistant Command Mapping To streamline interaction with AI assistants, you can configure them to trigger `npm run graph:audit` and `npm run graph:commit` using simpler, more conversational commands. Below are examples of how to set this up for various AI assistants, based on the definitions in `graph_parts/ai_commands.jsonnet`. @@ -97,6 +140,24 @@ To streamline interaction with AI assistants, you can configure them to trigger - **Action:** Run `npm run graph:commit` - **Description:** Executes the AI Committer script to automatically categorize and commit staged changes based on project_graph.jsonnet rules. +## Sync Ai Commands +- **Trigger Phrase:** "sync-ai-commands" +- **Action:** Run `npm run sync:ai-commands` +- **Description:** Synchronizes AI command definitions across various AI assistant rule files. + +``` + + +## Graph Audit +- **Trigger Phrase:** "graph-audit" +- **Action:** Run `node project_graph/scripts/graph_generator.mjs` +- **Description:** Executes the project graph audit script to check for discrepancies between the graph definition and actual project files. + +## Graph Commit +- **Trigger Phrase:** "graph-commit" +- **Action:** Run `npm run graph:commit` +- **Description:** Executes the AI Committer script to automatically categorize and commit staged changes based on project_graph.jsonnet rules. + ## Sync Ai Commands - **Trigger Phrase:** "sync-ai-commands" - **Action:** Run `npm run sync:ai-commands` @@ -130,10 +191,12 @@ To streamline interaction with AI assistants, you can configure them to trigger - **Description:** Synchronizes AI command definitions across various AI assistant rule files. ``` + ## Drift - observedNotDeclared: 0 -- declaredNotObserved: 10 +- declaredNotObserved: 17 + ## Development Workflow @@ -141,10 +204,10 @@ To streamline interaction with AI assistants, you can configure them to trigger This ProjectGraphAgent is designed to work in two modes: -1. **Parent Project Mode** (`/home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/`) - - Contains project-specific data (TSX-viewer entities, settings) +1. **Parent Project Mode** (`/home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/`) + - Contains project-specific data (Agent Plugins Platform entities, settings) - Used for active development and testing - - Manages the parent project (TSX-viewer) + - Manages the parent project (Agent Plugins Platform) 2. **Standalone Mode** (`/home/igor/Документы/Проекты/ProjectGraphAgent/`) - Clean, universal template @@ -155,13 +218,13 @@ This ProjectGraphAgent is designed to work in two modes: 1. **Develop in Parent Project**: ```bash - # Work in tsx_viewer/ProjectGraphAgent/ + # Work in agent_plugins_platform/ProjectGraphAgent/ # Make changes to scripts, graph_parts, adapters, etc. ``` 2. **Sync to Standalone**: ```bash - # From tsx_viewer/ProjectGraphAgent/ + # From agent_plugins_platform/ProjectGraphAgent/ npm run sync ``` @@ -184,7 +247,7 @@ This ProjectGraphAgent is designed to work in two modes: For convenience, use the automated publish workflow: ```bash -# From tsx_viewer/ProjectGraphAgent/ +# From agent_plugins_platform/ProjectGraphAgent/ npm run publish ``` diff --git a/ProjectGraphAgent/README.ru.md b/ProjectGraphAgent/README.ru.md index bc5b8776..fa6c0bbd 100644 --- a/ProjectGraphAgent/README.ru.md +++ b/ProjectGraphAgent/README.ru.md @@ -5,6 +5,7 @@ ProjectGraphAgent — это система управления проекта ## Ключевые особенности - **Заявленный и наблюдаемый граф**: Модель "заявленного" состояния в Jsonnet + модель "наблюдаемого" состояния от языковых адаптеров → автоматическое обнаружение расхождений. +- **Система индексации путей**: Быстрый поиск файлов с множественными стратегиями (по пути, директории, типу файла, именам с паттернами). - **Выходные данные для агентов**: Скомпилированный граф в JSON, отчеты о расхождениях, диаграммы Mermaid, markdown-файлы с планами, снимки состояния и события. - **Автоматизация**: Группировка коммитов, синхронизация AI-команд, интеграция с CI-воркфлоу. - **Поддержка нескольких языков**: Адаптеры для TypeScript/JavaScript и Python (с возможностью расширения). @@ -28,8 +29,47 @@ ProjectGraphAgent — это система управления проекта * `publish_workflow.mjs`: Скрипт, связанный с графом проекта. * `sync_ai_commands.mjs`: Скрипт, который синхронизирует определения AI-команд между различными файлами правил AI-ассистентов. * `sync_to_standalone.mjs`: Скрипт, связанный с графом проекта. - -## Использование + + ## Система индексации путей + + ProjectGraphAgent включает мощную систему индексации путей, которая обеспечивает быстрый поиск файлов и сущностей по различным критериям: + + ### Функции поиска + + - **`findByPath(path)`**: Найти сущность по точному пути файла + - **`findByDirectory(dir)`**: Найти все файлы в директории + - **`findByFileType(ext)`**: Найти все файлы с определенным расширением + - **`findByFileName(name)`**: Найти файлы с определенным именем + - **`findByPattern(pattern)`**: Поиск файлов по паттерну + - **`pathExists(path)`**: Проверить существование файла в индексе + - **`getIndexStats()`**: Получить статистику индекса + + ### Типы индексов + + - **Индекс путей**: Прямое сопоставление путь → сущность + - **Индекс директорий**: Директория → список сущностей + - **Индекс типов файлов**: Расширение → список сущностей + - **Индекс имен файлов**: Имя → список сущностей + + ### Примеры использования + + ```jsonnet + // Найти конкретный конфигурационный файл + local config = graph.templates.PathSearch.findByPath("package.json"); + + // Получить все TypeScript файлы + local tsFiles = graph.templates.PathSearch.findByFileType("ts"); + + // Найти все файлы в директории src + local srcFiles = graph.templates.PathSearch.findByDirectory("src"); + + // Проверить существование README + local hasReadme = graph.templates.PathSearch.pathExists("README.md"); + ``` + + Эта система индексации особенно ценна для AI-агентов, обеспечивая эффективную навигацию и анализ больших кодовых баз. + + ## Использование ### Быстрый старт @@ -160,10 +200,10 @@ ProjectGraphAgent — это система управления проекта ProjectGraphAgent разработан для работы в двух режимах: -1. **Режим родительского проекта** (`/home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/`) - - Содержит данные, специфичные для проекта (сущности и настройки TSX-viewer). +1. **Режим родительского проекта** (`/home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/`) + - Содержит данные, специфичные для проекта (сущности и настройки Agent Plugins Platform). - Используется для активной разработки и тестирования. - - Управляет родительским проектом (TSX-viewer). + - Управляет родительским проектом (Agent Plugins Platform). 2. **Автономный режим** (`/home/igor/Документы/Проекты/ProjectGraphAgent/`) - Чистый, универсальный шаблон. @@ -174,13 +214,13 @@ ProjectGraphAgent разработан для работы в двух режи 1. **Разработка в родительском проекте**: ```bash - # Работайте в tsx_viewer/ProjectGraphAgent/ + # Работайте в agent_plugins_platform/ProjectGraphAgent/ # Вносите изменения в скрипты, graph_parts, адаптеры и т.д. ``` 2. **Синхронизация с автономной версией**: ```bash - # Из директории tsx_viewer/ProjectGraphAgent/ + # Из директории agent_plugins_platform/ProjectGraphAgent/ npm run sync ``` @@ -203,7 +243,7 @@ ProjectGraphAgent разработан для работы в двух режи Для удобства используйте автоматизированный воркфлоу публикации: ```bash -# Из директории tsx_viewer/ProjectGraphAgent/ +# Из директории agent_plugins_platform/ProjectGraphAgent/ npm run publish ``` diff --git a/ProjectGraphAgent/WORKFLOW_GUIDE.md b/ProjectGraphAgent/WORKFLOW_GUIDE.md index c275d5a0..f4647f50 100644 --- a/ProjectGraphAgent/WORKFLOW_GUIDE.md +++ b/ProjectGraphAgent/WORKFLOW_GUIDE.md @@ -8,10 +8,10 @@ This guide explains the dual-directory workflow for ProjectGraphAgent developmen ``` /home/igor/Документы/Проекты/ -├── tsx_viewer/ProjectGraphAgent/ # Parent project mode -│ ├── project_graph.jsonnet # Contains TSX-viewer data -│ ├── graph_parts/entities.jsonnet # TSX-viewer specific entities -│ ├── settings.json # TSX-viewer settings +├── agent_plugins_platform/ProjectGraphAgent/ # Parent project mode +│ ├── project_graph.jsonnet # Contains Agent Plugins Platform data +│ ├── graph_parts/entities.jsonnet # Agent Plugins Platform specific entities +│ ├── settings.json # Agent Plugins Platform settings │ └── ... (all other files) └── ProjectGraphAgent/ # Standalone mode ├── project_graph.jsonnet # Clean template @@ -23,10 +23,10 @@ This guide explains the dual-directory workflow for ProjectGraphAgent developmen ### 1. Development Phase (Parent Project) -Work in `/home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/`: +Work in `/home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/`: ```bash -cd /home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/ +cd /home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/ # Make changes to: # - scripts/ (new automation features) @@ -44,7 +44,7 @@ npm run graph:validate Sync changes to the standalone directory: ```bash -# From tsx_viewer/ProjectGraphAgent/ +# From agent_plugins_platform/ProjectGraphAgent/ npm run sync ``` @@ -91,7 +91,7 @@ git push origin main For convenience, use the automated publish workflow: ```bash -# From tsx_viewer/ProjectGraphAgent/ +# From agent_plugins_platform/ProjectGraphAgent/ npm run publish ``` @@ -128,19 +128,19 @@ npm run graph:commit # Grouped commits (planned) ## File Management -### Parent Project Files (tsx_viewer/ProjectGraphAgent/) +### Parent Project Files (agent_plugins_platform/ProjectGraphAgent/) **Contains project-specific data:** -- `project_graph.jsonnet` - TSX-viewer configuration -- `graph_parts/entities.jsonnet` - TSX-viewer entities -- `settings.json` - TSX-viewer settings -- `.cache/` - Generated artifacts for TSX-viewer -- `memory-bank/` - TSX-viewer memory bank +- `project_graph.jsonnet` - Agent Plugins Platform configuration +- `graph_parts/entities.jsonnet` - Agent Plugins Platform entities +- `settings.json` - Agent Plugins Platform settings +- `.cache/` - Generated artifacts for Agent Plugins Platform +- `memory-bank/` - Agent Plugins Platform memory bank **Used for:** - Active development - Testing new features -- Managing TSX-viewer project +- Managing Agent Plugins Platform project - Debugging and experimentation ### Standalone Files (/home/igor/Документы/Проекты/ProjectGraphAgent/) @@ -176,12 +176,18 @@ npm run graph:commit # Grouped commits (planned) 3. **Monitor Git status** - Check for unexpected changes 4. **Update documentation** - Keep README.md current +### Path Indexing +1. **Use path search for large projects** - Leverage `graph.templates.PathSearch` functions for efficient file lookups +2. **Check file existence before access** - Always use `pathExists()` before working with files +3. **Utilize different search strategies** - Combine directory, file type, and pattern searches for precise targeting +4. **Monitor index statistics** - Use `getIndexStats()` to understand codebase scope and performance + ## Troubleshooting ### Sync Issues ```bash # Check if source exists -ls -la /home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/ +ls -la /home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/ # Check if destination exists ls -la /home/igor/Документы/Проекты/ProjectGraphAgent/ @@ -215,9 +221,9 @@ git clean -fd | Command | Location | Purpose | |---------|----------|---------| -| `npm run sync` | tsx_viewer/ProjectGraphAgent/ | Sync to standalone | +| `npm run sync` | agent_plugins_platform/ProjectGraphAgent/ | Sync to standalone | | `npm run clean` | ProjectGraphAgent/ | Clean for publication | -| `npm run publish` | tsx_viewer/ProjectGraphAgent/ | Full workflow | +| `npm run publish` | agent_plugins_platform/ProjectGraphAgent/ | Full workflow | | `npm run graph:audit` | Any | Generate graph | | `npm run graph:validate` | Any | Validate graph | diff --git a/ProjectGraphAgent/WORKFLOW_GUIDE.ru.md b/ProjectGraphAgent/WORKFLOW_GUIDE.ru.md index c8d46b83..2ba6f57c 100644 --- a/ProjectGraphAgent/WORKFLOW_GUIDE.ru.md +++ b/ProjectGraphAgent/WORKFLOW_GUIDE.ru.md @@ -8,10 +8,10 @@ ``` /home/igor/Документы/Проекты/ -├── tsx_viewer/ProjectGraphAgent/ # Режим родительского проекта -│ ├── project_graph.jsonnet # Содержит данные TSX-viewer -│ ├── graph_parts/entities.jsonnet # Сущности, специфичные для TSX-viewer -│ ├── settings.json # Настройки TSX-viewer +├── agent_plugins_platform/ProjectGraphAgent/ # Режим родительского проекта +│ ├── project_graph.jsonnet # Содержит данные Agent Plugins Platform +│ ├── graph_parts/entities.jsonnet # Сущности, специфичные для Agent Plugins Platform +│ ├── settings.json # Настройки Agent Plugins Platform │ └── ... (все остальные файлы) └── ProjectGraphAgent/ # Автономный режим ├── project_graph.jsonnet # Чистый шаблон @@ -23,10 +23,10 @@ ### 1. Этап разработки (Родительский проект) -Работайте в `/home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/`: +Работайте в `/home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/`: ```bash -cd /home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/ +cd /home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/ # Вносите изменения в: # - scripts/ (новые функции автоматизации) @@ -44,7 +44,7 @@ npm run graph:validate Синхронизируйте изменения в автономную директорию: ```bash -# Из директории tsx_viewer/ProjectGraphAgent/ +# Из директории agent_plugins_platform/ProjectGraphAgent/ npm run sync ``` @@ -91,7 +91,7 @@ git push origin main Для удобства используйте автоматизированный воркфлоу публикации: ```bash -# Из директории tsx_viewer/ProjectGraphAgent/ +# Из директории agent_plugins_platform/ProjectGraphAgent/ npm run publish ``` @@ -128,19 +128,19 @@ npm run graph:commit # Групповые коммиты (в планах) ## Управление файлами -### Файлы родительского проекта (tsx_viewer/ProjectGraphAgent/) +### Файлы родительского проекта (agent_plugins_platform/ProjectGraphAgent/) **Содержат данные, специфичные для проекта:** -- `project_graph.jsonnet` - Конфигурация TSX-viewer -- `graph_parts/entities.jsonnet` - Сущности TSX-viewer -- `settings.json` - Настройки TSX-viewer -- `.cache/` - Сгенерированные артефакты для TSX-viewer -- `memory-bank/` - Memory bank для TSX-viewer +- `project_graph.jsonnet` - Конфигурация Agent Plugins Platform +- `graph_parts/entities.jsonnet` - Сущности Agent Plugins Platform +- `settings.json` - Настройки Agent Plugins Platform +- `.cache/` - Сгенерированные артефакты для Agent Plugins Platform +- `memory-bank/` - Memory bank для Agent Plugins Platform **Используются для:** - Активной разработки - Тестирования новых функций -- Управления проектом TSX-viewer +- Управления проектом Agent Plugins Platform - Отладки и экспериментов ### Файлы автономной версии (/home/igor/Документы/Проекты/ProjectGraphAgent/) @@ -176,12 +176,18 @@ npm run graph:commit # Групповые коммиты (в планах) 3. **Следите за статусом Git** - Проверяйте на наличие неожиданных изменений 4. **Обновляйте документацию** - Поддерживайте README.md в актуальном состоянии +### Индексация путей +1. **Используйте поиск по путям для больших проектов** - Используйте функции `graph.templates.PathSearch` для эффективного поиска файлов +2. **Проверяйте существование файлов перед доступом** - Всегда используйте `pathExists()` перед работой с файлами +3. **Используйте различные стратегии поиска** - Комбинируйте поиск по директории, типу файла и паттернам для точного targeting +4. **Следите за статистикой индекса** - Используйте `getIndexStats()` для понимания охвата кодовой базы и производительности + ## Устранение неполадок ### Проблемы с синхронизацией ```bash # Проверьте, существует ли источник -ls -la /home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent/ +ls -la /home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent/ # Проверьте, существует ли назначение ls -la /home/igor/Документы/Проекты/ProjectGraphAgent/ @@ -215,9 +221,9 @@ git clean -fd | Команда | Расположение | Назначение | |---------|----------|---------| -| `npm run sync` | tsx_viewer/ProjectGraphAgent/ | Синхронизировать с автономной версией | +| `npm run sync` | agent_plugins_platform/ProjectGraphAgent/ | Синхронизировать с автономной версией | | `npm run clean` | ProjectGraphAgent/ | Очистить для публикации | -| `npm run publish` | tsx_viewer/ProjectGraphAgent/ | Полный рабочий процесс | +| `npm run publish` | agent_plugins_platform/ProjectGraphAgent/ | Полный рабочий процесс | | `npm run graph:audit` | Любое | Сгенерировать граф | | `npm run graph:validate` | Любое | Проверить граф | diff --git a/ProjectGraphAgent/graph_parts/ai_commands.jsonnet b/ProjectGraphAgent/graph_parts/ai_commands.jsonnet index 28c31341..21b82694 100644 --- a/ProjectGraphAgent/graph_parts/ai_commands.jsonnet +++ b/ProjectGraphAgent/graph_parts/ai_commands.jsonnet @@ -40,6 +40,31 @@ 'sync assistant commands', ], }, + { + name: 'path-search', + npmCommand: 'node project_graph/scripts/graph_generator.mjs --path-search', + description: 'Search for files and entities using the path indexing system. Supports various search strategies.', + implemented: true, + triggerPhrases: [ + 'path-search', + 'search files', + 'find files', + 'index search', + 'path index', + ], + }, + { + name: 'show-index-stats', + npmCommand: 'node project_graph/scripts/graph_generator.mjs --index-stats', + description: 'Display statistics about the path indexing system including number of indexed files and search performance.', + implemented: true, + triggerPhrases: [ + 'index-stats', + 'show stats', + 'index statistics', + 'search stats', + ], + }, ], // Define how these commands should be presented for different AI platforms diff --git a/ProjectGraphAgent/graph_parts/meta.jsonnet b/ProjectGraphAgent/graph_parts/meta.jsonnet index 350a205f..e5e8c1d9 100644 --- a/ProjectGraphAgent/graph_parts/meta.jsonnet +++ b/ProjectGraphAgent/graph_parts/meta.jsonnet @@ -34,7 +34,19 @@ }, 'graph_parts/templates.jsonnet': { type: 'MetaTemplateFile', - purpose: 'Defines reusable helper functions (templates) for creating entities within the graph, ensuring consistency.', + purpose: 'Defines reusable helper functions (templates) for creating entities within the graph, ensuring consistency. Includes PathSearch system for efficient file path indexing and lookup operations.', + }, + 'graph_parts/path_index.jsonnet': { + type: 'MetaIndexFile', + purpose: 'Creates searchable indexes for path-based lookups without changing the core entity structure. Enables fast file and entity searches by path, directory, file type, and patterns.', + }, + 'graph_parts/path_search_examples.jsonnet': { + type: 'MetaExamplesFile', + purpose: 'Contains practical examples and scenarios for using the PathSearch system in AI agent workflows.', + }, + 'graph_parts/README_path_indexing.md': { + type: 'MetaDocumentationFile', + purpose: 'Comprehensive documentation for the path indexing system, including usage examples and best practices.', }, 'metadata_block': { type: 'MetaConcept', diff --git a/ProjectGraphAgent/graph_parts/path_index.jsonnet b/ProjectGraphAgent/graph_parts/path_index.jsonnet index 39e471a8..7ddd6b72 100644 --- a/ProjectGraphAgent/graph_parts/path_index.jsonnet +++ b/ProjectGraphAgent/graph_parts/path_index.jsonnet @@ -12,81 +12,62 @@ local entities = import 'entities.jsonnet'; if std.objectHas(entity, 'path') }, + // Вспомогательные функции + local getDir(path) = std.join('/', std.slice(std.split(path, '/'), 0, std.length(std.split(path, '/')) - 1, 1)), + local getFileExtension(path) = ( + local parts = std.split(path, '.'); + if std.length(parts) > 1 then parts[std.length(parts) - 1] else 'no_extension' + ), + local getFileName(path) = ( + local parts = std.split(path, '/'); + if std.length(parts) > 0 then parts[std.length(parts) - 1] else path + ), + + // Предварительные вычисления + local allDirs = std.set([ + getDir(entity.path) + for entity in std.objectValues(entities) + if std.objectHas(entity, 'path') && entity.path != '' + ]), + local allExtensions = std.set([ + getFileExtension(entity.path) + for entity in std.objectValues(entities) + if std.objectHas(entity, 'path') + ]), + local allFileNames = std.set([ + getFileName(entity.path) + for entity in std.objectValues(entities) + if std.objectHas(entity, 'path') + ]), + // Индекс: директория -> список entities directoryIndex: { - local getDir(path) = std.join('/', std.slice(std.split(path, '/'), 0, std.length(std.split(path, '/')) - 1)), - local dirs = std.set([ - getDir(entity.path) - for entity in std.objectValues(entities) - if std.objectHas(entity, 'path') && entity.path != '' - ]), - [dir]: [ entity for entity in std.objectValues(entities) if std.objectHas(entity, 'path') && std.startsWith(entity.path, dir + '/') ] - for dir in dirs + for dir in allDirs }, // Индекс: тип файла -> список entities fileTypeIndex: { - local getFileExtension(path) = ( - local parts = std.split(path, '.'); - if std.length(parts) > 1 then parts[std.length(parts) - 1] else 'no_extension' - ), - local extensions = std.set([ - getFileExtension(entity.path) - for entity in std.objectValues(entities) - if std.objectHas(entity, 'path') - ]), - [ext]: [ entity for entity in std.objectValues(entities) if std.objectHas(entity, 'path') && getFileExtension(entity.path) == ext ] - for ext in extensions + for ext in allExtensions }, // Индекс: базовое имя файла -> список entities (для поиска файлов с одинаковыми именами) fileNameIndex: { - local getFileName(path) = ( - local parts = std.split(path, '/'); - if std.length(parts) > 0 then parts[std.length(parts) - 1] else path - ), - local fileNames = std.set([ - getFileName(entity.path) - for entity in std.objectValues(entities) - if std.objectHas(entity, 'path') - ]), - [fileName]: [ entity for entity in std.objectValues(entities) if std.objectHas(entity, 'path') && getFileName(entity.path) == fileName ] - for fileName in fileNames + for fileName in allFileNames }, - // Вспомогательные функции для работы с индексами - utils: { - // Получить все уникальные директории - getAllDirectories(): std.objectFields(self.directoryIndex), - - // Получить все уникальные расширения файлов - getAllFileTypes(): std.objectFields(self.fileTypeIndex), - - // Проверить существование пути в индексе - pathExists(path): std.objectHas(self.pathIndex, path), - - // Получить статистику индексов - getStats(): { - totalEntities: std.length(std.objectValues(entities)), - indexedPaths: std.length(std.objectFields(self.pathIndex)), - directories: std.length(std.objectFields(self.directoryIndex)), - fileTypes: std.length(std.objectFields(self.fileTypeIndex)), - fileNames: std.length(std.objectFields(self.fileNameIndex)), - }, - }, } \ No newline at end of file diff --git a/ProjectGraphAgent/graph_parts/path_search_examples.jsonnet b/ProjectGraphAgent/graph_parts/path_search_examples.jsonnet index da7c3e31..86c14c3e 100644 --- a/ProjectGraphAgent/graph_parts/path_search_examples.jsonnet +++ b/ProjectGraphAgent/graph_parts/path_search_examples.jsonnet @@ -19,8 +19,8 @@ local PathSearch = graph.templates.PathSearch; // 2. Поиск всех файлов в директории findFilesInDirectory: { description: 'Найти все файлы в указанной директории', - example: PathSearch.findByDirectory('packages'), - usage: 'graph.templates.PathSearch.findByDirectory("packages")', + example: PathSearch.findByDirectory('src'), + usage: 'graph.templates.PathSearch.findByDirectory("src")', }, // 3. Поиск файлов по типу (расширению) diff --git a/ProjectGraphAgent/graph_parts/templates.jsonnet b/ProjectGraphAgent/graph_parts/templates.jsonnet index 2ab5b823..083b67bf 100644 --- a/ProjectGraphAgent/graph_parts/templates.jsonnet +++ b/ProjectGraphAgent/graph_parts/templates.jsonnet @@ -109,16 +109,22 @@ findByFileName(fileName):: pathIndex.fileNameIndex[fileName], // Проверить существование файла в индексе - pathExists(path):: pathIndex.utils.pathExists(path), + pathExists(path):: std.objectHas(pathIndex.pathIndex, path), // Получить статистику индексов - getIndexStats():: pathIndex.utils.getStats(), + getIndexStats():: { + totalEntities: std.length(std.objectValues(pathIndex.pathIndex)), + indexedPaths: std.length(std.objectFields(pathIndex.pathIndex)), + directories: std.length(std.objectFields(pathIndex.directoryIndex)), + fileTypes: std.length(std.objectFields(pathIndex.fileTypeIndex)), + fileNames: std.length(std.objectFields(pathIndex.fileNameIndex)), + }, // Получить все доступные директории - getAllDirectories():: pathIndex.utils.getAllDirectories(), + getAllDirectories():: std.objectFields(pathIndex.directoryIndex), // Получить все доступные типы файлов - getAllFileTypes():: pathIndex.utils.getAllFileTypes(), + getAllFileTypes():: std.objectFields(pathIndex.fileTypeIndex), // Поиск с использованием паттернов (простая реализация) findByPattern(pattern):: [ @@ -135,7 +141,7 @@ ], // Получить родительскую директорию для пути - getParentDirectory(path):: std.join('/', std.slice(std.split(path, '/'), 0, std.length(std.split(path, '/')) - 1)), + getParentDirectory(path):: std.join('/', std.slice(std.split(path, '/'), 0, std.length(std.split(path, '/')) - 1, 1)), // Получить расширение файла getFileExtension(path):: ( diff --git a/ProjectGraphAgent/scripts/publish_workflow.mjs b/ProjectGraphAgent/scripts/publish_workflow.mjs index 04368dc5..63271cab 100644 --- a/ProjectGraphAgent/scripts/publish_workflow.mjs +++ b/ProjectGraphAgent/scripts/publish_workflow.mjs @@ -19,7 +19,7 @@ const __dirname = path.dirname(__filename); const PROJECT_ROOT = path.resolve(__dirname, '..'); // Paths -const PARENT_PROJECT = '/home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent'; +const PARENT_PROJECT = '/home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent'; const STANDALONE_PROJECT = '/home/igor/Документы/Проекты/ProjectGraphAgent'; function log(message, type = 'info') { diff --git a/ProjectGraphAgent/scripts/sync_to_standalone.mjs b/ProjectGraphAgent/scripts/sync_to_standalone.mjs index b41cf5af..cc891429 100644 --- a/ProjectGraphAgent/scripts/sync_to_standalone.mjs +++ b/ProjectGraphAgent/scripts/sync_to_standalone.mjs @@ -16,7 +16,7 @@ const __dirname = path.dirname(__filename); const PROJECT_ROOT = path.resolve(__dirname, '..'); // Paths -const PARENT_PROJECT = '/home/igor/Документы/Проекты/tsx_viewer/ProjectGraphAgent'; +const PARENT_PROJECT = '/home/igor/Документы/Проекты/agent_plugins_platform/ProjectGraphAgent'; const STANDALONE_PROJECT = '/home/igor/Документы/Проекты/ProjectGraphAgent'; // Files to sync (relative to ProjectGraphAgent root) diff --git a/ProjectGraphAgent/test_path_search.js b/ProjectGraphAgent/test_path_search.js new file mode 100644 index 00000000..2494ef6c --- /dev/null +++ b/ProjectGraphAgent/test_path_search.js @@ -0,0 +1,85 @@ +// Тестовый скрипт для проверки работы функций поиска по путям +import fs from 'fs'; + +// Загружаем сгенерированный граф +const graph = JSON.parse(fs.readFileSync('./.cache/graph.json', 'utf8')); + +console.log('=== Тестирование функций поиска по путям ===\n'); + +// Функции поиска (имитация PathSearch из templates.jsonnet) +const PathSearch = { + findByPath(path) { + return graph.pathIndex.pathIndex[path]; + }, + + findByDirectory(dir) { + return graph.pathIndex.directoryIndex[dir] || []; + }, + + findByFileType(fileType) { + return graph.pathIndex.fileTypeIndex[fileType] || []; + }, + + findByFileName(fileName) { + return graph.pathIndex.fileNameIndex[fileName] || []; + }, + + pathExists(path) { + return !!graph.pathIndex.pathIndex[path]; + } +}; + +// Тест 1: Поиск по точному пути +console.log('1. Тест findByPath():'); +const packageJson = PathSearch.findByPath('package.json'); +console.log(' Поиск package.json:', packageJson ? '✓ Найден' : '✗ Не найден'); +if (packageJson) { + console.log(' Тип:', packageJson.type); + console.log(' Назначение:', packageJson.purpose); +} + +// Тест 2: Поиск по директории +console.log('\n2. Тест findByDirectory():'); +const srcFiles = PathSearch.findByDirectory('src'); +console.log(' Файлы в директории src:', srcFiles.length, 'шт.'); +srcFiles.forEach(file => { + console.log(' -', file.path, '(' + file.type + ')'); +}); + +// Тест 3: Поиск по типу файла +console.log('\n3. Тест findByFileType():'); +const jsonFiles = PathSearch.findByFileType('json'); +console.log(' JSON файлы:', jsonFiles.length, 'шт.'); +jsonFiles.slice(0, 3).forEach(file => { + console.log(' -', file.path); +}); + +// Тест 4: Поиск по имени файла +console.log('\n4. Тест findByFileName():'); +const readmeFiles = PathSearch.findByFileName('README.md'); +console.log(' Файлы README.md:', readmeFiles.length, 'шт.'); +readmeFiles.forEach(file => { + console.log(' -', file.path); +}); + +// Тест 5: Проверка существования +console.log('\n5. Тест pathExists():'); +console.log(' package.json существует:', PathSearch.pathExists('package.json') ? '✓' : '✗'); +console.log(' nonexistent.json существует:', PathSearch.pathExists('nonexistent.json') ? '✓' : '✗'); + +// Статистика индексов +console.log('\n6. Статистика индексов:'); +const stats = { + totalEntities: Object.keys(graph.entities).length, + indexedPaths: Object.keys(graph.pathIndex.pathIndex).length, + directories: Object.keys(graph.pathIndex.directoryIndex).length, + fileTypes: Object.keys(graph.pathIndex.fileTypeIndex).length, + fileNames: Object.keys(graph.pathIndex.fileNameIndex).length +}; +console.log(' Всего сущностей:', stats.totalEntities); +console.log(' Индексированных путей:', stats.indexedPaths); +console.log(' Директорий:', stats.directories); +console.log(' Типов файлов:', stats.fileTypes); +console.log(' Имен файлов:', stats.fileNames); + +console.log('\n=== Тестирование завершено ==='); \ No newline at end of file diff --git a/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py b/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py index 3906f5c0..d05b86ec 100644 --- a/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py +++ b/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py @@ -1,455 +1,179 @@ -import sys import json -import asyncio -import re from typing import Any, Dict, List -# from bs4 import BeautifulSoup # Может не работать в Pyodide -# Простой HTML парсер для Pyodide -class SimpleHTMLParser: - def __init__(self, html): - self.html = html - - def find(self, tag, attrs=None): - # Простая реализация поиска тега - return SimpleHTMLElement(self.html, tag, attrs) - - def find_all(self, tag, attrs=None): - # Простая реализация поиска всех тегов - return [SimpleHTMLElement(self.html, tag, attrs)] - -class SimpleHTMLElement: - def __init__(self, html, tag, attrs): - self.html = html - self.tag = tag - self.attrs = attrs or {} - - def get(self, attr, default=''): - return self.attrs.get(attr, default) - - def find(self, tag, attrs=None): - return SimpleHTMLElement(self.html, tag, attrs) - - def find_all(self, tag, attrs=None): - return [SimpleHTMLElement(self.html, tag, attrs)] - - def get_text(self, strip=False): - # Простая реализация извлечения текста - return "Sample text" if strip else "Sample text" - - @property - def text(self): - return "Sample text" - -# Глобальная переменная для доступа к JavaScript API -js = None - -# Конфигурация нейросетей -AI_MODELS = { - "basic_analysis": "gemini-flash", - "detailed_comparison": "gemini-pro", - "deep_analysis": "gemini-25", - "scraping_fallback": "gemini-flash" -} - -async def main(): - """Основная функция MCP сервера для анализатора Ozon""" - global js - +# --- "КОНТРАКТ" ДЛЯ АНАЛИЗАТОРА ТИПОВ --- +# Этот блок помогает редактору кода понимать, какие JS-функции доступны. +# В реальной среде он не выполняется, так как `js` предоставляется Pyodide. +try: + from typing import Protocol, runtime_checkable + + @runtime_checkable + class JsBridge(Protocol): + def sendMessageToChat(self, message: Dict[str, Any]) -> None: ... + def llm_call(self, model_alias: str, params: Dict[str, Any]) -> Any: ... + def get_setting(self, setting_name: str) -> Any: ... + + js: JsBridge +except ImportError: + # В среде Pyodide `Protocol` может отсутствовать, это нормально. + pass + +# --- ГЛАВНЫЕ ИНСТРУМЕНТЫ (вызываются из workflow.json) --- + +async def analyze_ozon_product(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Главная точка входа. Анализирует HTML-контент страницы товара Ozon. + """ try: - while True: - line = sys.stdin.readline() - if not line: - break - - request = json.loads(line) - response = await process_request(request) - - sys.stdout.write(json.dumps(response) + '\n') - sys.stdout.flush() - - except Exception as e: - error_response = { - "error": { - "code": -32603, - "message": f"Internal error: {str(e)}" - } - } - sys.stdout.write(json.dumps(error_response) + '\n') - sys.stdout.flush() - -async def process_request(request: Dict[str, Any]) -> Dict[str, Any]: - """Обработка MCP запросов""" - method = request.get('method') - params = request.get('params', {}) - - if method == 'analyze_product': - return await analyze_ozon_product(params) - elif method == 'deep_analysis': - return await perform_deep_analysis(params.get('description', ''), params.get('composition', '')) - elif method == 'ping': - return {"result": "pong"} - else: - return { - "error": { - "code": -32601, - "message": f"Method not found: {method}" - } - } - -async def analyze_ozon_product(params: Dict[str, Any]) -> Dict[str, Any]: - """Анализ товара на Ozon""" - try: - # Получаем HTML страницы - page_html = params.get('page_html', '') + page_html = input_data.get('page_html', '') if not page_html: - return { - "error": { - "code": -32602, - "message": "HTML страницы не предоставлен" - } - } + raise ValueError("HTML страницы не предоставлен") - # Парсим HTML (используем встроенный парсер) + # В будущем здесь будет использоваться `beautifulsoup4` # soup = BeautifulSoup(page_html, 'html.parser') - # Временно используем простой парсинг - soup = SimpleHTMLParser(page_html) - - # Проверяем, что это страница товара - if not page_html.startswith('https://www.ozon.ru/product/'): + soup = SimpleHTMLParser(page_html) # Временная заглушка + + # Валидация (оставлено для примера, в реальности может быть сложнее) + is_product_page = soup.find('div', {'data-widget': 'webProductHeading'}) is not None + if not is_product_page: return { - "result": { - "message": "Это не страница товара Ozon. Перейдите на страницу товара для анализа." - } + "status": "info", + "message": "Это не страница товара Ozon. Перейдите на страницу товара для анализа." } - # Извлекаем категории из breadcrumbs - categories = extract_categories(soup) - - # Извлекаем описание и состав - description, composition = extract_description_and_composition(soup) - - # Анализируем соответствие описания и состава - analysis_result = await analyze_composition_vs_description(description, composition) + js.sendMessageToChat({"content": "Python: Начинаю анализ страницы товара..."}) + + categories = _extract_categories(soup) + description, composition = _extract_description_and_composition(soup) - # Ищем аналоги - analogs = await find_similar_products(categories, composition) + js.sendMessageToChat({"content": f"Python: Описание и состав извлечены. Анализирую соответствие с помощью AI..."}) + analysis_result = await _analyze_composition_vs_description(description, composition) - # Проверяем, нужен ли глубокий анализ - deep_analysis_available = await check_deep_analysis_availability() + analogs = await _find_similar_products(categories, composition) + # Проверяем, разрешен ли "глубокий анализ" в настройках плагина + enable_deep_analysis = await js.get_setting("enable_deep_analysis").to_py() + result = { "categories": categories, "description": description, "composition": composition, "analysis": analysis_result, "analogs": analogs, - "message": f"Анализ завершен. Оценка соответствия: {analysis_result['score']}/10" + "message": f"Анализ завершен. Оценка соответствия: {analysis_result.get('score', 'N/A')}/10" } - # Если доступен глубокий анализ, предлагаем его - if deep_analysis_available and analysis_result['score'] < 7: + # Предлагаем глубокий анализ, только если он включен и оценка низкая + if enable_deep_analysis and analysis_result.get('score', 10) < 7: result["deep_analysis_offer"] = { "available": True, - "message": "Хотите провести более глубокий анализ с помощью Gemini 2.5 Pro?", - "model": AI_MODELS["deep_analysis"] + "message": "Обнаружены несоответствия. Хотите провести более глубокий анализ?", } - return {"result": result} + return result except Exception as e: - return { - "error": { - "code": -32603, - "message": f"Ошибка анализа товара: {str(e)}" - } - } - -def extract_categories(soup: SimpleHTMLParser) -> List[str]: - """Извлекает категории из breadcrumbs""" - categories = [] - - breadcrumbs = soup.find('div', {'data-widget': 'breadCrumbs'}) - if breadcrumbs: - links = breadcrumbs.find_all('a') - for link in links: - href = link.get('href', '') - if '/category/' in href: - # Извлекаем название категории - span = link.find('span') - if span: - categories.append(span.text.strip()) - - return categories - -def extract_description_and_composition(soup: SimpleHTMLParser) -> tuple: - """Извлекает описание и состав товара""" - description = "" - composition = "" + js.sendMessageToChat({"content": f"Python: Ошибка при анализе - {e}"}) + return { "status": "error", "message": f"Ошибка анализа товара: {str(e)}" } + +async def perform_deep_analysis(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Выполняет глубокий анализ с помощью самой мощной модели. + """ + description = input_data.get('description', '') + composition = input_data.get('composition', '') + + js.sendMessageToChat({"content": "Python: Запускаю глубокий анализ с помощью Gemini Pro..."}) + + prompt = f""" + Проведи глубокий анализ товара с медицинской и научной точки зрения. + Описание: {description} + Состав: {composition} + Проанализируй: + 1. Научную обоснованность заявленных свойств. + 2. Потенциальные побочные эффекты и противопоказания. + 3. Эффективность по сравнению с аналогами. + Верни детальный анализ в структурированном виде (Markdown). + """ - # Ищем div с описанием - description_sections = soup.find_all('div', {'id': 'section-description'}) - - for section in description_sections: - h2 = section.find('h2') - if h2: - h2_text = h2.text.strip().lower() - - if 'описание' in h2_text: - # Извлекаем описание - desc_div = section.find('div') - if desc_div: - description = desc_div.get_text(strip=True) - - elif 'состав' in h2_text or 'характеристики' in h2_text: - # Извлекаем состав - comp_div = section.find('div') - if comp_div: - composition = comp_div.get_text(strip=True) - - return description, composition - -async def get_ai_api_key(model_name: str) -> str: - """Получает API ключ для указанной нейросети""" try: - # В реальной реализации здесь будет обращение к background script - # для получения сохраненных ключей - return "demo_key" # Заглушка - except Exception as e: - print(f"Ошибка получения API ключа для {model_name}: {e}") - return "" - -async def call_ai_model(model_name: str, prompt: str) -> str: - """Вызывает указанную нейросеть с промптом с обработкой лимитов""" - try: - api_key = await get_ai_api_key(model_name) - if not api_key: - return f"Ошибка: API ключ для {model_name} не настроен" - - # Проверяем лимиты перед вызовом - rate_limit_info = await check_rate_limit(model_name) - if rate_limit_info['limited']: - return await handle_rate_limit(model_name, rate_limit_info, prompt) + # "deep_analysis" - это псевдоним из manifest.json плагина. + # Наша платформа сама подставит нужную модель (gemini-pro или gemini-25). + result = await _call_ai_model("deep_analysis", prompt) - # В реальной реализации здесь будет вызов API нейросети - # Пока возвращаем заглушку - result = f"Ответ от {model_name}: {prompt[:50]}..." - - # Обновляем статистику использования - await update_usage_stats(model_name) - - return result - - except Exception as e: - return f"Ошибка вызова {model_name}: {str(e)}" - -async def check_rate_limit(model_name: str) -> Dict[str, Any]: - """Проверяет лимиты для указанной модели""" - try: - # В реальной реализации здесь будет проверка лимитов API - # Пока возвращаем заглушку return { - 'limited': False, - 'reset_time': None, - 'remaining_requests': 1000 + "deep_analysis_report": result, } except Exception as e: - print(f"Ошибка проверки лимитов для {model_name}: {e}") - return {'limited': False, 'reset_time': None, 'remaining_requests': 0} + return { "status": "error", "message": f"Ошибка глубокого анализа: {str(e)}" } -async def handle_rate_limit(model_name: str, rate_limit_info: Dict[str, Any], prompt: str) -> str: - """Обрабатывает ситуацию с лимитами API""" - try: - # Получаем доступные альтернативные модели - alternative_models = await get_alternative_models(model_name) - - # Пытаемся использовать альтернативную модель - for alt_model in alternative_models: - alt_rate_limit = await check_rate_limit(alt_model) - if not alt_rate_limit['limited']: - print(f"Переключаемся на альтернативную модель: {alt_model}") - return await call_ai_model(alt_model, prompt) - - # Если альтернативы недоступны, возвращаем информацию о лимите - reset_time = rate_limit_info.get('reset_time') - if reset_time: - return f"Лимит API для {model_name} превышен. Повторить запрос после {reset_time} или использовать другую модель." - else: - return f"Лимит API для {model_name} превышен. Попробуйте позже или используйте другую модель." - - except Exception as e: - return f"Ошибка обработки лимита для {model_name}: {str(e)}" -async def get_alternative_models(model_name: str) -> List[str]: - """Возвращает список альтернативных моделей""" - # Определяем альтернативы для каждой модели - alternatives = { - 'gemini-flash': ['gemini-25'], - 'gemini-25': ['gemini-flash'], - 'gemini-pro': ['gemini-flash', 'gemini-25'] - } - - return alternatives.get(model_name, []) +# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ (приватная логика плагина) --- +# Начинаются с `_`, чтобы показать, что они не предназначены для вызова извне. -async def update_usage_stats(model_name: str): - """Обновляет статистику использования модели""" +async def _analyze_composition_vs_description(description: str, composition: str) -> Dict[str, Any]: + if not description or not composition: + return { "score": 0, "reasoning": "Не удалось извлечь описание или состав товара." } + + prompt = f""" + Проанализируй соответствие описания товара и его состава. + Описание: {description} + Состав: {composition} + Оцени по шкале от 1 до 10, где 1 - полное несоответствие, 10 - полное соответствие. + Верни JSON в формате: {{"score": число, "reasoning": "краткое объяснение оценки"}} + """ + try: - # В реальной реализации здесь будет обновление статистики - print(f"Обновлена статистика использования для {model_name}") + # Используем "basic_analysis", псевдоним для gemini-flash + result_str = await _call_ai_model("basic_analysis", prompt) + # Убираем "обертку" Markdown, если она есть + cleaned_str = result_str.strip().replace('```json', '').replace('```', '') + return json.loads(cleaned_str) except Exception as e: - print(f"Ошибка обновления статистики для {model_name}: {e}") + return { "score": 0, "reasoning": f"Ошибка анализа AI: {str(e)}" } -async def check_deep_analysis_availability() -> bool: - """Проверяет доступность глубокого анализа""" +async def _call_ai_model(model_alias: str, prompt: str) -> str: + """Обертка для вызова AI через Host-API.""" try: - api_key = await get_ai_api_key(AI_MODELS["deep_analysis"]) - return bool(api_key and api_key != "demo_key") - except Exception as e: - print(f"Ошибка проверки доступности глубокого анализа: {e}") - return False + # Мы больше не управляем ключами или лимитами в Python. + # Мы просто просим платформу выполнить запрос с псевдонимом модели. + # Платформа сама обработает ключи, лимиты и разрешения. + params = {"prompt": prompt} + response_proxy = await js.llm_call(model_alias, params) + result = response_proxy.to_py() -async def perform_deep_analysis(description: str, composition: str) -> Dict[str, Any]: - """Выполняет глубокий анализ с помощью Gemini 2.5 Pro""" - try: - prompt = f""" - Проведи глубокий анализ товара с медицинской и научной точки зрения. - - Описание: {description} - Состав: {composition} - - Проанализируй: - 1. Научную обоснованность заявленных свойств - 2. Потенциальные побочные эффекты и противопоказания - 3. Взаимодействие с другими препаратами - 4. Эффективность по сравнению с аналогами - 5. Рекомендации по применению - 6. Альтернативные варианты - - Верни детальный анализ в структурированном виде. - """ + if result.get("error"): + raise Exception(result.get("error_message", "Неизвестная ошибка от AI API")) - result = await call_ai_model(AI_MODELS["deep_analysis"], prompt) - - return { - "deep_analysis": result, - "model_used": AI_MODELS["deep_analysis"], - "timestamp": asyncio.get_event_loop().time() - } - - except Exception as e: - return { - "error": f"Ошибка глубокого анализа: {str(e)}" - } + return result.get("response", "Нет ответа от модели.") -async def analyze_composition_vs_description(description: str, composition: str) -> Dict[str, Any]: - """Анализирует соответствие описания и состава с помощью нейросетей""" - - if not description or not composition: - return { - "score": 0, - "reasoning": "Не удалось извлечь описание или состав товара", - "details": [] - } - - try: - # Базовый анализ с помощью Gemini Flash - basic_prompt = f""" - Проанализируй соответствие описания товара и его состава. - - Описание: {description} - Состав: {composition} - - Оцени по шкале от 1 до 10, где: - 1 - полное несоответствие - 10 - полное соответствие - - Верни JSON в формате: - {{ - "score": число, - "reasoning": "объяснение оценки", - "details": ["деталь 1", "деталь 2"] - }} - """ - - basic_result = await call_ai_model(AI_MODELS["basic_analysis"], basic_prompt) - - # Детальное сравнение с помощью Gemini Pro - detailed_prompt = f""" - Проведи детальный анализ соответствия описания и состава товара. - - Описание: {description} - Состав: {composition} - - Проанализируй: - 1. Соответствие заявленных свойств составу - 2. Качество и полезность ингредиентов - 3. Потенциальные риски или преимущества - 4. Рекомендации по использованию - - Верни структурированный анализ. - """ - - detailed_result = await call_ai_model(AI_MODELS["detailed_comparison"], detailed_prompt) - - # Парсим результат базового анализа - try: - basic_data = json.loads(basic_result) - score = basic_data.get("score", 5) - reasoning = basic_data.get("reasoning", "Анализ не удался") - details = basic_data.get("details", []) - except: - score = 5 - reasoning = "Ошибка парсинга результата анализа" - details = [] - - return { - "score": score, - "reasoning": reasoning, - "details": details, - "detailed_analysis": detailed_result, - "ai_models_used": [AI_MODELS["basic_analysis"], AI_MODELS["detailed_comparison"]] - } - except Exception as e: - return { - "score": 0, - "reasoning": f"Ошибка анализа: {str(e)}", - "details": [] - } - score = max(1, min(10, score)) - - reasoning = f"Оценка {score}/10: " - if score >= 8: - reasoning += "Отличное соответствие описания и состава" - elif score >= 6: - reasoning += "Хорошее соответствие с небольшими расхождениями" - elif score >= 4: - reasoning += "Среднее соответствие, есть расхождения" - else: - reasoning += "Плохое соответствие, описание не отражает реальный состав" - - return { - "score": score, - "reasoning": reasoning, - "details": details - } + # Пробрасываем ошибку выше, чтобы вызывающая функция могла ее обработать. + raise RuntimeError(f"Ошибка при вызове модели '{model_alias}': {e}") from e -async def find_similar_products(categories: List[str], composition: str) -> List[Dict[str, Any]]: - """Ищет аналогичные товары (заглушка)""" - # В реальном проекте здесь был бы поиск по API Ozon - analogs = [] - - if categories: - # Симулируем поиск аналогов - for i, category in enumerate(categories[:3]): - analogs.append({ - "name": f"Аналог в категории {category}", - "price": f"{1000 + i * 200} ₽", - "url": f"https://www.ozon.ru/search?text={category}", - "similarity": f"{80 - i * 10}%" - }) - - return analogs +def _extract_categories(soup: 'SimpleHTMLParser') -> List[str]: + # Ваша логика извлечения категорий (без изменений) + return ["Пример", "Категории"] -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file +def _extract_description_and_composition(soup: 'SimpleHTMLParser') -> tuple: + # Ваша логика извлечения описания (без изменений) + return "Пример описания", "Пример состава" + +async def _find_similar_products(categories: List[str], composition: str) -> List[Dict[str, Any]]: + # Ваша логика поиска аналогов (без изменений) + return [{"name": "Пример аналога", "price": "1000 ₽"}] + + +# --- ЗАГЛУШКА ДЛЯ ПАРСЕРА (временно) --- +class SimpleHTMLParser: + def __init__(self, html: str): self.html = html + def find(self, tag: str, attrs: Dict = None) -> 'SimpleHTMLElement': return SimpleHTMLElement(self.html, tag, attrs) + def find_all(self, tag: str, attrs: Dict = None) -> List['SimpleHTMLElement']: return [SimpleHTMLElement(self.html, tag, attrs)] +class SimpleHTMLElement: + def __init__(self, html: str, tag: str, attrs: Dict): self.html, self.tag, self.attrs = html, tag, attrs or {} + def get(self, attr: str, default: str = '') -> str: return self.attrs.get(attr, default) + def find(self, tag: str, attrs: Dict = None) -> 'SimpleHTMLElement': return SimpleHTMLElement(self.html, tag, attrs) + def find_all(self, tag: str, attrs: Dict = None) -> List['SimpleHTMLElement']: return [SimpleHTMLElement(self.html, tag, attrs)] + def get_text(self, strip: bool = False) -> str: return "Пример текста" + @property + def text(self) -> str: return "Пример текста" \ No newline at end of file diff --git a/chrome-extension/public/plugins/ozon-analyzer/workflow.json b/chrome-extension/public/plugins/ozon-analyzer/workflow.json index 01ae4482..663ac44c 100644 --- a/chrome-extension/public/plugins/ozon-analyzer/workflow.json +++ b/chrome-extension/public/plugins/ozon-analyzer/workflow.json @@ -1,12 +1,12 @@ { "name": "ozon_analyzer", - "description": "Анализатор товаров Ozon с проверкой соответствия описания и состава", + "description": "Анализатор товаров Ozon...", "steps": [ { - "name": "analyze_product", - "description": "Анализирует товар на странице Ozon", - "method": "analyze_product", - "params": { + "id": "analyze_page", + "tool": "python.analyze_ozon_product", + "description": "Анализирует товар на текущей странице Ozon", + "input": { "page_html": "{{page_html}}" } } diff --git a/memory-bank/audit_logs.md b/memory-bank/audit_logs.md index b6ecc36c..4ebb8f56 100644 --- a/memory-bank/audit_logs.md +++ b/memory-bank/audit_logs.md @@ -7,3 +7,4 @@ Audit performed on 2025-08-21T20:14:46.591Z. Scope: Full Project. Status: WARNIN Audit performed on 2025-08-21T20:15:19.433Z. Scope: Full Project. Status: WARNINGS. Audit performed on 2025-08-21T20:20:46.565Z. Scope: Full Project. Status: WARNINGS. Audit performed on 2025-08-21T20:53:12.052Z. Scope: Full Project. Status: WARNINGS. +Audit performed on 2025-08-27T23:25:36.367Z. Scope: Full Project. Status: WARNINGS. diff --git a/memory-bank/diagrams/graph.mmd b/memory-bank/diagrams/graph.mmd index 096d98b7..33e91140 100644 --- a/memory-bank/diagrams/graph.mmd +++ b/memory-bank/diagrams/graph.mmd @@ -1 +1,8 @@ -graph LR \ No newline at end of file +graph LR + memory-bank/projects/chrome-extension-chat-recovery/architecture/chat-architecture.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/docs/code-changes-summary.md + memory-bank/projects/chrome-extension-chat-recovery/README.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/docs/project-overview.md + memory-bank/projects/chrome-extension-chat-recovery/docs/code-changes-summary.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/testing/testing-results.md + memory-bank/projects/chrome-extension-chat-recovery/docs/lessons-learned.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/docs/project-overview.md + memory-bank/projects/chrome-extension-chat-recovery/docs/problems-solved.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/architecture/chat-architecture.md + memory-bank/projects/chrome-extension-chat-recovery/docs/project-overview.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/docs/problems-solved.md + memory-bank/projects/chrome-extension-chat-recovery/testing/testing-results.md -- references --> memory-bank/projects/chrome-extension-chat-recovery/docs/lessons-learned.md \ No newline at end of file diff --git a/memory-bank/drift.md b/memory-bank/drift.md index 6cdf3ed1..ea9d6895 100644 --- a/memory-bank/drift.md +++ b/memory-bank/drift.md @@ -1,9 +1,9 @@ # Graph Drift -Generated: 2025-08-21T20:53:12.058Z +Generated: 2025-08-27T23:25:36.395Z - observedNotDeclared: 0 -- declaredNotObserved: 10 +- declaredNotObserved: 17 ## Samples @@ -13,6 +13,13 @@ Generated: 2025-08-21T20:53:12.058Z - declared only: memory-bank/development/README.md - declared only: memory-bank/diagrams/graph.mmd - declared only: memory-bank/drift.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/README.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/architecture/chat-architecture.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/docs/code-changes-summary.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/docs/lessons-learned.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/docs/problems-solved.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/docs/project-overview.md +- declared only: memory-bank/projects/chrome-extension-chat-recovery/testing/testing-results.md - declared only: package.json - declared only: packages/dev-utils/package.json - declared only: packages/hmr/package.json diff --git a/memory-bank/plans/README.md b/memory-bank/plans/README.md index d75893ec..6b2b2e42 100644 --- a/memory-bank/plans/README.md +++ b/memory-bank/plans/README.md @@ -1,3 +1,3 @@ # Plans Digest -Generated: 2025-08-21T20:53:12.072Z +Generated: 2025-08-27T23:25:36.477Z From a51c8bb345e9a5fed34ad608c7dd99e9c7d0abbd Mon Sep 17 00:00:00 2001 From: Igor Lebedev Date: Fri, 29 Aug 2025 16:28:19 +0500 Subject: [PATCH 3/4] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20MCP=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BF=D0=BB=D0=B0=D0=B3=D0=B8=D0=BD=D0=B0=20"Oz?= =?UTF-8?q?on=20Analyzer":=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=20?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2,=20=D1=83=D0=BB?= =?UTF-8?q?=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=B7=D0=B0=D0=B3=D0=BB=D1=83=D1=88=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=D0=B0.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20wo?= =?UTF-8?q?rkflow.json=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D1=88=D0=B0=D0=B3=D0=BE=D0=B2=20=D0=B0=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20API=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=20AI=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B0=D0=B9=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20=D0=B2=20host=20AP?= =?UTF-8?q?I=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B?= =?UTF-8?q?=20=D1=81=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=B8=20=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20AI.=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B2=D1=85?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=B8=20=D1=83=D1=81=D0=BB=D0=BE=D0=B2=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=88=D0=B0=D0=B3=D0=BE=D0=B2=20=D0=B2=20workflow.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/ozon-analyzer/mcp_server.py | 203 ++++++++++----- .../plugins/ozon-analyzer/workflow.json | 17 +- .../src/background/ai-api-client.ts | 236 ++++++++++++++++++ chrome-extension/src/background/host-api.ts | 44 +++- chrome-extension/src/background/index.ts | 136 +++++++++- .../src/background/pyodide-worker.js | 29 +++ core/workflow-engine.js | 94 +++++-- 7 files changed, 660 insertions(+), 99 deletions(-) create mode 100644 chrome-extension/src/background/ai-api-client.ts diff --git a/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py b/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py index d05b86ec..0dc1978e 100644 --- a/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py +++ b/chrome-extension/public/plugins/ozon-analyzer/mcp_server.py @@ -1,90 +1,138 @@ +# ============================================================================== +# MCP Server (Библиотека Инструментов) для плагина "Ozon Analyzer" +# ============================================================================== +# Этот скрипт НЕ является самостоятельным сервером. Он представляет собой +# набор Python-функций ("инструментов"), которые вызываются по требованию +# движком `workflow-engine.js` нашей платформы. +# Вся коммуникация с внешним миром (UI, Browser API, LLM) происходит +# опосредованно, через вызовы JavaScript-функций, доступных в глобальном +# объекте `js`. +# ------------------------------------------------------------------------------ + +# Стандартные импорты import json -from typing import Any, Dict, List - -# --- "КОНТРАКТ" ДЛЯ АНАЛИЗАТОРА ТИПОВ --- -# Этот блок помогает редактору кода понимать, какие JS-функции доступны. -# В реальной среде он не выполняется, так как `js` предоставляется Pyodide. +from typing import Any, Dict, List, Protocol, runtime_checkable + +# --- "Контракт" с JavaScript: Объявление типов для `js` моста --- +# Этот блок кода критически важен для статических анализаторов (Pyright, MyPy) +# и для автодополнения в IDE (Cursor, VS Code). Он "объясняет" анализатору, +# какие методы существуют у глобального объекта `js`, который предоставляет +# среда Pyodide. В реальной среде выполнения этот блок не создает новых +# переменных, так как `js` уже будет определен. try: - from typing import Protocol, runtime_checkable - @runtime_checkable class JsBridge(Protocol): + """Описывает "контракт" API, который предоставляет JavaScript-хост.""" def sendMessageToChat(self, message: Dict[str, Any]) -> None: ... def llm_call(self, model_alias: str, params: Dict[str, Any]) -> Any: ... def get_setting(self, setting_name: str) -> Any: ... - js: JsBridge except ImportError: - # В среде Pyodide `Protocol` может отсутствовать, это нормально. + # В минимальной среде Python `Protocol` может отсутствовать. + # В Pyodide это не вызовет проблем. pass -# --- ГЛАВНЫЕ ИНСТРУМЕНТЫ (вызываются из workflow.json) --- +# ============================================================================== +# Секция 1: "Публичные" Инструменты +# ------------------------------------------------------------------------------ +# Эти функции являются точками входа для `workflow-engine.js`. +# Имя каждой функции соответствует значению `tool` в `workflow.json`, +# например, "python.analyze_ozon_product". +# ============================================================================== async def analyze_ozon_product(input_data: Dict[str, Any]) -> Dict[str, Any]: """ - Главная точка входа. Анализирует HTML-контент страницы товара Ozon. + Главная точка входа для анализа страницы товара Ozon. + Эта функция оркестрирует весь процесс: парсинг, анализ, поиск аналогов + и формирование итогового отчета. + + Args: + input_data: Словарь, содержащий `page_html` текущей страницы. + + Returns: + Словарь с полным отчетом. Ключевые поля `description` и `composition` + возвращаются на верхнем уровне, чтобы быть доступными для последующих + шагов в `workflow.json` (например, для `perform_deep_analysis`). """ try: page_html = input_data.get('page_html', '') if not page_html: - raise ValueError("HTML страницы не предоставлен") + raise ValueError("HTML страницы не предоставлен для анализа.") - # В будущем здесь будет использоваться `beautifulsoup4` - # soup = BeautifulSoup(page_html, 'html.parser') - soup = SimpleHTMLParser(page_html) # Временная заглушка - - # Валидация (оставлено для примера, в реальности может быть сложнее) - is_product_page = soup.find('div', {'data-widget': 'webProductHeading'}) is not None - if not is_product_page: + # Временная заглушка для парсера. В будущем здесь будет использоваться + # библиотека `beautifulsoup4`, которая будет установлена как зависимость + # плагина через `micropip`. + soup = SimpleHTMLParser(page_html) + + # Простая проверка, что мы находимся на странице продукта. + # Более надежная проверка потребует реального парсинга. + if 'ozon.ru/product' not in page_html: return { "status": "info", - "message": "Это не страница товара Ozon. Перейдите на страницу товара для анализа." + "message": "Это не страница товара Ozon. Плагин работает только на страницах товаров." } js.sendMessageToChat({"content": "Python: Начинаю анализ страницы товара..."}) + # Шаг 1: Извлечение структурированных данных со страницы categories = _extract_categories(soup) description, composition = _extract_description_and_composition(soup) + # Шаг 2: Анализ соответствия с помощью "быстрой" AI-модели js.sendMessageToChat({"content": f"Python: Описание и состав извлечены. Анализирую соответствие с помощью AI..."}) analysis_result = await _analyze_composition_vs_description(description, composition) + # Шаг 3: Поиск аналогов (в данной версии - заглушка) analogs = await _find_similar_products(categories, composition) - # Проверяем, разрешен ли "глубокий анализ" в настройках плагина + # Шаг 4: Проверяем настройки плагина, заданные пользователем в UI enable_deep_analysis = await js.get_setting("enable_deep_analysis").to_py() - + + # Шаг 5: Формируем условное предложение для глубокого анализа + # Это поле будет использоваться в `workflow.json` в условии `run_if`. + offer_deep_analysis = enable_deep_analysis and analysis_result.get('score', 10) < 7 + + # Шаг 6: Собираем финальный результат result = { - "categories": categories, + # Эти два поля дублируются на верхнем уровне специально для того, + # чтобы следующий шаг в воркфлоу (`perform_deep_analysis`) + # мог легко получить к ним доступ через `{{steps.analyze.output.description}}`. "description": description, "composition": composition, + # Вся остальная информация для отображения в UI + "categories": categories, "analysis": analysis_result, "analogs": analogs, - "message": f"Анализ завершен. Оценка соответствия: {analysis_result.get('score', 'N/A')}/10" - } - - # Предлагаем глубокий анализ, только если он включен и оценка низкая - if enable_deep_analysis and analysis_result.get('score', 10) < 7: - result["deep_analysis_offer"] = { - "available": True, - "message": "Обнаружены несоответствия. Хотите провести более глубокий анализ?", + "message": f"Анализ завершен. Оценка соответствия: {analysis_result.get('score', 'N/A')}/10", + # Этот объект используется `workflow-engine` для принятия решения, + # запускать ли следующий шаг. + "deep_analysis_offer": { + "available": offer_deep_analysis, + "message": "Обнаружены несоответствия. Хотите провести более глубокий анализ?" if offer_deep_analysis else "" } + } return result except Exception as e: - js.sendMessageToChat({"content": f"Python: Ошибка при анализе - {e}"}) + js.sendMessageToChat({"content": f"Python: Критическая ошибка при анализе - {e}"}) + # Возвращаем стандартизированный объект ошибки return { "status": "error", "message": f"Ошибка анализа товара: {str(e)}" } async def perform_deep_analysis(input_data: Dict[str, Any]) -> Dict[str, Any]: """ - Выполняет глубокий анализ с помощью самой мощной модели. + Выполняет глубокий, ресурсоемкий анализ с помощью самой мощной + AI-модели, доступной платформе. """ description = input_data.get('description', '') composition = input_data.get('composition', '') - js.sendMessageToChat({"content": "Python: Запускаю глубокий анализ с помощью Gemini Pro..."}) + if not description or not composition: + return { "status": "error", "message": "Описание или состав не были переданы для глубокого анализа."} + + js.sendMessageToChat({"content": "Python: Запускаю глубокий анализ..."}) + # Промпт для "экспертного" анализа. prompt = f""" Проведи глубокий анализ товара с медицинской и научной точки зрения. Описание: {description} @@ -93,78 +141,103 @@ async def perform_deep_analysis(input_data: Dict[str, Any]) -> Dict[str, Any]: 1. Научную обоснованность заявленных свойств. 2. Потенциальные побочные эффекты и противопоказания. 3. Эффективность по сравнению с аналогами. - Верни детальный анализ в структурированном виде (Markdown). + Верни детальный анализ в структурированном виде (используй Markdown). """ try: - # "deep_analysis" - это псевдоним из manifest.json плагина. - # Наша платформа сама подставит нужную модель (gemini-pro или gemini-25). + # "deep_analysis" - это псевдоним из `manifest.json` этого плагина. + # Платформа сама определит, какую реальную модель (например, gemini-pro) + # использовать, и подставит соответствующий API-ключ. result = await _call_ai_model("deep_analysis", prompt) - - return { - "deep_analysis_report": result, - } + return { "deep_analysis_report": result } except Exception as e: return { "status": "error", "message": f"Ошибка глубокого анализа: {str(e)}" } - -# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ (приватная логика плагина) --- -# Начинаются с `_`, чтобы показать, что они не предназначены для вызова извне. +# ============================================================================== +# Секция 2: "Приватные" Вспомогательные Функции +# ------------------------------------------------------------------------------ +# Эти функции не предназначены для прямого вызова из `workflow.json`. +# Они инкапсулируют внутреннюю логику плагина. +# ============================================================================== async def _analyze_composition_vs_description(description: str, composition: str) -> Dict[str, Any]: + """Использует "быструю" AI-модель для базовой оценки соответствия.""" if not description or not composition: return { "score": 0, "reasoning": "Не удалось извлечь описание или состав товара." } - + prompt = f""" Проанализируй соответствие описания товара и его состава. Описание: {description} Состав: {composition} Оцени по шкале от 1 до 10, где 1 - полное несоответствие, 10 - полное соответствие. - Верни JSON в формате: {{"score": число, "reasoning": "краткое объяснение оценки"}} + Верни ТОЛЬКО JSON в формате: {{"score": число, "reasoning": "краткое объяснение оценки"}} """ - + try: - # Используем "basic_analysis", псевдоним для gemini-flash + # Используем псевдоним "basic_analysis", который в манифесте + # сопоставлен с быстрой и дешевой моделью типа `gemini-flash`. result_str = await _call_ai_model("basic_analysis", prompt) - # Убираем "обертку" Markdown, если она есть + + # Очистка и парсинг ответа от AI. Модели часто "оборачивают" + # JSON в Markdown, который нужно удалить. cleaned_str = result_str.strip().replace('```json', '').replace('```', '') - return json.loads(cleaned_str) + + # Используем стандартный и безопасный `json.loads` для парсинга. + try: + parsed = json.loads(cleaned_str) + # Простая валидация формата ответа + if isinstance(parsed, dict) and 'score' in parsed: + return parsed + else: + return {"score": 5, "reasoning": "Неверный формат ответа от AI (отсутствует 'score')."} + except json.JSONDecodeError: + return {"score": 5, "reasoning": f"Не удалось распарсить JSON от AI: {cleaned_str[:100]}..."} + except Exception as e: return { "score": 0, "reasoning": f"Ошибка анализа AI: {str(e)}" } async def _call_ai_model(model_alias: str, prompt: str) -> str: - """Обертка для вызова AI через Host-API.""" + """ + Централизованная обертка для всех вызовов LLM. + Делегирует всю сложную работу (управление ключами, лимитами, разрешениями) + платформе через `js.llm_call`. + """ try: - # Мы больше не управляем ключами или лимитами в Python. - # Мы просто просим платформу выполнить запрос с псевдонимом модели. - # Платформа сама обработает ключи, лимиты и разрешения. - params = {"prompt": prompt} - response_proxy = await js.llm_call(model_alias, params) + # Вызываем функцию хоста, передавая псевдоним модели и параметры. + response_proxy = await js.llm_call(model_alias, {"prompt": prompt}) + # `await` дожидается выполнения JS Promise. `.to_py()` конвертирует + # результат (JS-объект) в Python-словарь. result = response_proxy.to_py() - if result.get("error"): - raise Exception(result.get("error_message", "Неизвестная ошибка от AI API")) - + # Стандартизированная обработка ошибок от хоста + if result is None or result.get("error"): + error_msg = result.get("error_message", "Неизвестная ошибка") if result else "Пустой ответ от хоста" + raise Exception(f"Ошибка вызова API: {error_msg}") + return result.get("response", "Нет ответа от модели.") except Exception as e: - # Пробрасываем ошибку выше, чтобы вызывающая функция могла ее обработать. + # Пробрасываем ошибку выше, чтобы вызывающая функция могла ее перехватить + # и обработать в своей бизнес-логике. raise RuntimeError(f"Ошибка при вызове модели '{model_alias}': {e}") from e def _extract_categories(soup: 'SimpleHTMLParser') -> List[str]: - # Ваша логика извлечения категорий (без изменений) + """Заглушка для извлечения категорий.""" return ["Пример", "Категории"] def _extract_description_and_composition(soup: 'SimpleHTMLParser') -> tuple: - # Ваша логика извлечения описания (без изменений) + """Заглушка для извлечения описания и состава.""" return "Пример описания", "Пример состава" async def _find_similar_products(categories: List[str], composition: str) -> List[Dict[str, Any]]: - # Ваша логика поиска аналогов (без изменений) + """Заглушка для поиска аналогов.""" return [{"name": "Пример аналога", "price": "1000 ₽"}] - -# --- ЗАГЛУШКА ДЛЯ ПАРСЕРА (временно) --- +# ============================================================================== +# Секция 3: Временные Заглушки +# ------------------------------------------------------------------------------ +# Этот код будет заменен, когда мы добавим поддержку установки `beautifulsoup4`. +# ============================================================================== class SimpleHTMLParser: def __init__(self, html: str): self.html = html def find(self, tag: str, attrs: Dict = None) -> 'SimpleHTMLElement': return SimpleHTMLElement(self.html, tag, attrs) diff --git a/chrome-extension/public/plugins/ozon-analyzer/workflow.json b/chrome-extension/public/plugins/ozon-analyzer/workflow.json index 663ac44c..08110273 100644 --- a/chrome-extension/public/plugins/ozon-analyzer/workflow.json +++ b/chrome-extension/public/plugins/ozon-analyzer/workflow.json @@ -1,13 +1,20 @@ { "name": "ozon_analyzer", - "description": "Анализатор товаров Ozon...", + "description": "Анализатор товаров Ozon с проверкой соответствия описания и состава", "steps": [ { - "id": "analyze_page", + "id": "analyze", + "description": "Анализ продукта Ozon", "tool": "python.analyze_ozon_product", - "description": "Анализирует товар на текущей странице Ozon", - "input": { - "page_html": "{{page_html}}" + "inputs": { "page_html": "{{input.page_html}}" } + }, + { + "id": "deep-analysis", + "tool": "python.perform_deep_analysis", + "run_if": "{{steps.analyze.output.deep_analysis_offer.available}} == true", + "inputs": { + "description": "{{steps.analyze.output.description}}", + "composition": "{{steps.analyze.output.composition}}" } } ] diff --git a/chrome-extension/src/background/ai-api-client.ts b/chrome-extension/src/background/ai-api-client.ts new file mode 100644 index 00000000..108dd3da --- /dev/null +++ b/chrome-extension/src/background/ai-api-client.ts @@ -0,0 +1,236 @@ +/** + * AI API Client for Agent-Plugins-Platform + * Handles communication with various AI providers (OpenAI, Google Gemini, etc.) + */ + +export interface AiModelResponse { + response: string; + usage?: { + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + }; + model?: string; +} + +// Доступные модели +export type ModelAlias = 'gemini-flash' | 'gemini-pro' | 'gemini-25' | 'gpt-3.5-turbo' | 'gpt-4'; + +// Конфигурация модели +interface ModelConfig { + provider: string; + model_name: string; + endpoint: string; + api_key_env: string; +} + +// Поддерживаемые модели и их конфигурации с типизированными индексами +const MODEL_CONFIGS: Record = { + 'gemini-flash': { + provider: 'google', + model_name: 'gemini-1.5-flash-latest', + endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/', + api_key_env: 'GOOGLE_AI_API_KEY' + }, + 'gemini-pro': { + provider: 'google', + model_name: 'gemini-1.5-pro-latest', + endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/', + api_key_env: 'GOOGLE_AI_API_KEY' + }, + 'gemini-25': { + provider: 'google', + model_name: 'gemini-1.5-flash-8b-latest', + endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/', + api_key_env: 'GOOGLE_AI_API_KEY' + }, + 'gpt-3.5-turbo': { + provider: 'openai', + model_name: 'gpt-3.5-turbo', + endpoint: 'https://api.openai.com/v1/chat/completions', + api_key_env: 'OPENAI_API_KEY' + }, + 'gpt-4': { + provider: 'openai', + model_name: 'gpt-4', + endpoint: 'https://api.openai.com/v1/chat/completions', + api_key_env: 'OPENAI_API_KEY' + } +}; + +/** + * Получает API ключ для указанной модели из хранилища расширения + */ +export async function getApiKeyForModel(modelAlias: string): Promise { + try { + // Проверяем, является ли modelAlias допустимым ключом MODEL_CONFIGS + if (!Object.keys(MODEL_CONFIGS).includes(modelAlias)) { + throw new Error(`Неизвестная модель: ${modelAlias}`); + } + const config = MODEL_CONFIGS[modelAlias as ModelAlias]; + if (!config) { + throw new Error(`Неизвестная модель: ${modelAlias}`); + } + + // В будущем здесь можно добавить логику получения API ключей из безопасного хранилища + // Пока используем переменные окружения или настройки расширения + const apiKeyName = config.api_key_env; + + // Пробуем получить из chrome.storage (local) + const storageResult = await chrome.storage.local.get([apiKeyName]); + if (storageResult[apiKeyName]) { + return storageResult[apiKeyName]; + } + + // Запасной вариант - проверяем переменные окружения (хотя в расширениях они ограничены) + // Это больше для разработки и тестирования + const envApiKey = process.env[apiKeyName]; + if (envApiKey) { + return envApiKey; + } + + console.warn(`[AI Client] API key not found for model ${modelAlias} (${apiKeyName})`); + return null; + } catch (error) { + console.error('[AI Client] Error getting API key:', error); + return null; + } +} + +/** + * Выполняет запрос к AI API в зависимости от провайдера + */ +export async function callAiModel(modelAlias: string, apiKey: string, prompt: string): Promise { + try { + // Проверяем, является ли modelAlias допустимым ключом MODEL_CONFIGS + if (!Object.keys(MODEL_CONFIGS).includes(modelAlias)) { + throw new Error(`Неизвестная модель: ${modelAlias}`); + } + const config = MODEL_CONFIGS[modelAlias as ModelAlias]; + if (!config) { + throw new Error(`Неизвестная модель: ${modelAlias}`); + } + + switch (config.provider) { + case 'google': + return await callGoogleGemini(config, apiKey, prompt); + case 'openai': + return await callOpenAI(config, apiKey, prompt); + default: + throw new Error(`Неподдерживаемый провайдер: ${config.provider}`); + } + } catch (error) { + console.error('[AI Client] Error calling AI model:', error); + throw new Error(`Ошибка при вызове модели ${modelAlias}: ${(error as Error).message}`); + } +} + +/** + * Выполняет запрос к Google Gemini API + */ +async function callGoogleGemini(config: any, apiKey: string, prompt: string): Promise { + const url = `${config.endpoint}${config.model_name}:generateContent?key=${apiKey}`; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + contents: [{ + parts: [{ + text: prompt + }] + }], + generationConfig: { + temperature: 0.7, + topK: 40, + topP: 0.95, + maxOutputTokens: 2048, + } + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Google Gemini API error ${response.status}: ${errorText}`); + } + + const data = await response.json(); + + if (!data.candidates || !data.candidates[0] || !data.candidates[0].content) { + throw new Error('Неверный формат ответа от Google Gemini API'); + } + + const generatedText = data.candidates[0].content.parts[0].text; + return generatedText || 'Нет ответа от модели'; +} + +/** + * Выполняет запрос к OpenAI API + */ +async function callOpenAI(config: any, apiKey: string, prompt: string): Promise { + const response = await fetch(config.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model: config.model_name, + messages: [ + { + role: 'user', + content: prompt + } + ], + temperature: 0.7, + max_tokens: 2048 + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API error ${response.status}: ${errorText}`); + } + + const data = await response.json(); + + if (!data.choices || !data.choices[0] || !data.choices[0].message) { + throw new Error('Неверный формат ответа от OpenAI API'); + } + + return data.choices[0].message.content || 'Нет ответа от модели'; +} + +/** + * Устанавливает API ключ для модели + */ +export async function setApiKeyForModel(modelAlias: string, apiKey: string): Promise { + try { + // Проверяем, является ли modelAlias допустимым ключом MODEL_CONFIGS + if (!Object.keys(MODEL_CONFIGS).includes(modelAlias)) { + throw new Error(`Неизвестная модель: ${modelAlias}`); + } + const config = MODEL_CONFIGS[modelAlias as ModelAlias]; + if (!config) { + throw new Error(`Неизвестная модель: ${modelAlias}`); + } + + const apiKeyName = config.api_key_env; + await chrome.storage.local.set({ [apiKeyName]: apiKey }); + + console.log(`[AI Client] API key set for model ${modelAlias}`); + } catch (error) { + console.error('[AI Client] Error setting API key:', error); + throw error; + } +} + +/** + * Проверяет доступность API ключа для модели + */ +export async function checkApiKeyAvailability(modelAlias: string): Promise { + const apiKey = await getApiKeyForModel(modelAlias); + return apiKey !== null && apiKey.length > 0; +} \ No newline at end of file diff --git a/chrome-extension/src/background/host-api.ts b/chrome-extension/src/background/host-api.ts index a156e496..600085a9 100644 --- a/chrome-extension/src/background/host-api.ts +++ b/chrome-extension/src/background/host-api.ts @@ -90,6 +90,48 @@ export const hostApi = { }); }, + async llm_call(modelAlias: string, options: any) { + try { + // Получаем информацию о плагине из контекста, если доступен + const currentPlugin = (window as any).currentPlugin || 'ozon-analyzer'; + + // Направляем через background script + return sendMessageToBackground({ + command: "llm_call", + data: { + modelAlias, + options, + pluginId: currentPlugin + } + }); + } catch (error) { + console.error('[HOST API] llm_call error:', error); + throw error; + } + }, + + async get_setting(settingName: string, defaultValue?: any, category?: string) { + try { + // Получаем информацию о плагине из контекста + const currentPlugin = (window as any).currentPlugin || 'ozon-analyzer'; + + // Направляем через background script + return sendMessageToBackground({ + command: "get_setting", + data: { + settingName, + defaultValue, + category, + pluginId: currentPlugin + } + }); + } catch (error) { + console.error('[HOST API] get_setting error:', error); + // Возвращаем значение по умолчанию в случае ошибки + return defaultValue; + } + }, + sendMessageToChat(message: { content: string }) { if ((window as any).activeWorkflowLogger) { (window as any).activeWorkflowLogger.addMessage('PYTHON', message.content); @@ -97,4 +139,4 @@ export const hostApi = { console.warn("[Python Message] Логгер не активен:", message.content); } } -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/chrome-extension/src/background/index.ts b/chrome-extension/src/background/index.ts index 42ea53e0..e74aad58 100644 --- a/chrome-extension/src/background/index.ts +++ b/chrome-extension/src/background/index.ts @@ -2,6 +2,7 @@ import 'webextension-polyfill'; import { pluginChatApi } from './plugin-chat-api'; import { getAvailablePlugins } from './plugin-manager'; import { getPageKey } from '../../../packages/shared/lib/utils/helpers'; +import { getApiKeyForModel, callAiModel } from './ai-api-client'; import { exampleThemeStorage, pluginSettingsStorage, getPluginSettings } from '@extension/storage'; import type { ChatMessage } from './plugin-chat-api'; import type { Plugin } from './plugin-manager'; @@ -21,6 +22,9 @@ interface ExtensionMessage { level?: 'info' | 'success' | 'error' | 'warning' | 'debug'; stepId?: string; logData?: unknown; + // Для идентификации сообщений и запросов: + requestId?: string; + messageId?: string; } // Только стандартное поведение: панель открывается/закрывается глобально по клику на иконку @@ -247,7 +251,7 @@ chrome.runtime.onMessage.addListener( console.log('[background] Processing RUN_WORKFLOW request for:', msg.pluginId); (async () => { try { - const result = await runPluginIfEnabled(msg.pluginId); + const result = await runPluginIfEnabled(msg.pluginId as string); sendResponse(result); } catch (error) { console.error('[background] Error in RUN_WORKFLOW:', error); @@ -735,6 +739,136 @@ const handleHostApiMessage = async ( sendResponse({ data }); break; } + case 'llm_call': { + try { + const { modelAlias, options, pluginId } = message.data as { + modelAlias: string; + options: any; + pluginId?: string + }; + + console.log('[HOST API] LLM call requested:', { modelAlias, pluginId }); + + // Загружаем manifest плагина для получения маппинга моделей + const currentPlugin = pluginId || 'ozon-analyzer'; + const manifestUrl = chrome.runtime.getURL(`public/plugins/${currentPlugin}/manifest.json`); + + let manifestResponse; + try { + manifestResponse = await fetch(manifestUrl); + if (!manifestResponse.ok) { + throw new Error(`Failed to load manifest: ${manifestResponse.status}`); + } + } catch (error) { + console.error('[HOST API] Error loading manifest:', error); + sendResponse({ + error: true, + error_message: `Не удалось загрузить настройки плагина ${currentPlugin}: ${(error as Error).message}` + }); + return true; + } + + const manifest = await manifestResponse.json(); + const aiModels = manifest.ai_models || {}; + + // Определяем реальную модель на основе алиаса + const actualModel = aiModels[modelAlias]; + if (!actualModel) { + sendResponse({ + error: true, + error_message: `Модель с алиасом '${modelAlias}' не найдена в манифесте плагина` + }); + return true; + } + + console.log('[HOST API] Using model:', actualModel, 'for alias:', modelAlias); + + // Получаем API ключ для модели + const apiKey = await getApiKeyForModel(actualModel); + if (!apiKey) { + sendResponse({ + error: true, + error_message: `API ключ для модели ${actualModel} не найден` + }); + return true; + } + + // Выполняем запрос к AI API + try { + const aiResponse = await callAiModel(actualModel, apiKey, options.prompt || ''); + sendResponse({ + response: aiResponse + }); + } catch (aiError) { + console.error('[HOST API] AI API error:', aiError); + sendResponse({ + error: true, + error_message: `Ошибка вызова AI API: ${(aiError as Error).message}` + }); + } + } catch (error) { + console.error('[HOST API] llm_call error:', error); + sendResponse({ + error: true, + error_message: (error as Error).message + }); + } + break; + } + case 'get_setting': { + try { + const { settingName, defaultValue, category, pluginId } = message.data as { + settingName: string; + defaultValue?: any; + category?: string; + pluginId?: string + }; + + console.log('[HOST API] Get setting requested:', { settingName, pluginId }); + + // Загружаем manifest плагина для получения настроек + const currentPlugin = pluginId || 'ozon-analyzer'; + const manifestUrl = chrome.runtime.getURL(`public/plugins/${currentPlugin}/manifest.json`); + + let manifestResponse; + try { + manifestResponse = await fetch(manifestUrl); + if (!manifestResponse.ok) { + throw new Error(`Failed to load manifest: ${manifestResponse.status}`); + } + } catch (error) { + console.error('[HOST API] Error loading manifest:', error); + sendResponse({ + error: true, + error_message: `Не удалось загрузить настройки плагина ${currentPlugin}: ${(error as Error).message}` + }); + return true; + } + + const manifest = await manifestResponse.json(); + const settings = manifest.settings || {}; + + // Получаем значение настройки + let settingValue = settings[settingName]; + + if (settingValue === undefined) { + // Если настройка не найдена, используем значение по умолчанию + settingValue = defaultValue; + console.log(`[HOST API] Setting '${settingName}' not found, using default:`, defaultValue); + } + + console.log(`[HOST API] Returning setting '${settingName}':`, settingValue); + sendResponse({ value: settingValue }); + + } catch (error) { + console.error('[HOST API] get_setting error:', error); + sendResponse({ + error: true, + error_message: (error as Error).message + }); + } + break; + } default: sendResponse({ error: `Unknown command: ${message.command}` }); } diff --git a/chrome-extension/src/background/pyodide-worker.js b/chrome-extension/src/background/pyodide-worker.js index aba22a48..e481afbb 100644 --- a/chrome-extension/src/background/pyodide-worker.js +++ b/chrome-extension/src/background/pyodide-worker.js @@ -35,6 +35,35 @@ async function initializePyodide() { hostCallPromises.set(callId, { resolve, reject }); self.postMessage({ type: 'host_call', func: 'host_fetch', callId, args: [url] }); }); + }, + llm_call: (modelAlias, options) => { + const callId = `host_call_${Date.now()}_${Math.random()}`; + return new Promise((resolve, reject) => { + hostCallPromises.set(callId, { resolve, reject }); + // Преобразуем options из PyProxy в обычный JS объект + const jsOptions = options.toJs ? options.toJs({ dict_converter: Object.fromEntries }) : options; + self.postMessage({ + type: 'host_call', + func: 'llm_call', + callId, + args: [modelAlias, jsOptions] + }); + }); + }, + get_setting: (settingName, defaultValue, category) => { + const callId = `host_call_${Date.now()}_${Math.random()}`; + return new Promise((resolve, reject) => { + hostCallPromises.set(callId, { resolve, reject }); + // Преобразуем параметры из PyProxy в обычные JS значения + const jsDefaultValue = defaultValue?.toJs ? defaultValue.toJs({ dict_converter: Object.fromEntries }) : defaultValue; + const jsCategory = category?.toJs ? category.toJs() : category; + self.postMessage({ + type: 'host_call', + func: 'get_setting', + callId, + args: [settingName, jsDefaultValue, jsCategory] + }); + }); } }); } diff --git a/core/workflow-engine.js b/core/workflow-engine.js index aa934f25..6da09b9c 100644 --- a/core/workflow-engine.js +++ b/core/workflow-engine.js @@ -1,45 +1,45 @@ /** * core/workflow-engine.js * - * Движок для выполнения декларативных воркфлоу. + * Движок для выполнения декларативных воркфлоу с поддержкой условных шагов. */ import { runPythonTool } from '../bridge/mcp-bridge.js'; import { createRunLogger } from '../ui/log-manager.js'; export async function runWorkflow(pluginId) { - // --- ▼▼▼ ИСПРАВЛЕНИЕ ОПЕЧАТКИ ▼▼▼ --- window.activeWorkflowLogger = createRunLogger(`Воркфлоу плагина: ${pluginId}`); - const logger = window.activeWorkflowLogger; // Используем правильное имя - // --- ▲▲▲ КОНЕЦ ИСПРАВЛЕНИЯ ▲▲▲ --- - + const logger = window.activeWorkflowLogger; logger.addMessage('ENGINE', `▶️ Запуск воркфлоу...`); - document.querySelector('.tab-button[data-tab="logs"]')?.click(); const workflow = await loadWorkflowDefinition(pluginId, logger); if (!workflow) return; - const context = { steps: {}, logger: logger }; + const context = { steps: {}, input: workflow.initialInput || {}, logger: logger }; for (const step of workflow.steps) { + const shouldRun = evaluateRunIf(step.run_if, context); + if (!shouldRun) { + logger.addMessage('ENGINE', `Пропущен шаг: ${step.id} (условие run_if не выполнено)`); + continue; + } + logger.addMessage('ENGINE', `➡️ Выполнение шага: ${step.id} (инструмент: ${step.tool})`); try { - const toolInput = resolveInputs(step.input, context); + // ИСПОЛЬЗУЕМ `step.inputs`, а не `step.input` + const toolInput = resolveInputs(step.inputs, context); let output; const [toolType, toolName] = step.tool.split('.'); if (toolType === 'host') { if (window.hostApi && typeof window.hostApi[toolName] === 'function') { output = await window.hostApi[toolName](toolInput, context); - } else { - throw new Error(`Host tool "${toolName}" не найден.`); - } + } else { throw new Error(`Host tool "${toolName}" не найден.`); } } else if (toolType === 'python') { - output = await runPythonTool(pluginId, toolName, toolInput); - } else { - throw new Error(`Неизвестный тип инструмента: ${step.tool}`); - } + output = await runPythonTool(pluginId, toolName, toolInput, context); + } else { throw new Error(`Неизвестный тип инструмента: ${step.tool}`); } + context.steps[step.id] = { output }; logger.addMessage('ENGINE', `✅ Шаг ${step.id} выполнен.`); } catch (error) { @@ -49,20 +49,60 @@ export async function runWorkflow(pluginId) { } } - // Отображаем финальный результат - const lastStep = workflow.steps[workflow.steps.length - 1]; - if (lastStep && context.steps[lastStep.id]) { - const finalResult = context.steps[lastStep.id].output; - logger.renderResult(lastStep.id, finalResult); + const lastExecutedStepId = Object.keys(context.steps).pop(); + if (lastExecutedStepId) { + const finalResult = context.steps[lastExecutedStepId].output; + logger.renderResult(lastExecutedStepId, finalResult); } logger.addMessage('ENGINE', `🏁 Воркфлоу успешно завершен.`); } -// ... вспомогательные функции, но с исправленными путями ... +// --- Вспомогательные функции --- + +function evaluateRunIf(condition, context) { + if (condition === undefined || condition === null) return true; + + const parts = condition.match(/^{{(.*?)}} *(==|!=|>|<|>=|<=) *(.*)$/); + if (!parts) { + console.warn(`[WorkflowEngine] Некорректный формат run_if: "${condition}"`); + return false; + } + + const [, path, operator, expectedValueStr] = parts; + const actualValue = getContextValue(path.trim(), context); + + // --- ▼▼▼ УМНОЕ ПРЕОБРАЗОВАНИЕ ТИПОВ ▼▼▼ --- + let expectedValue; + const trimmedExpected = expectedValueStr.trim(); + + if (trimmedExpected === 'true') { + expectedValue = true; + } else if (trimmedExpected === 'false') { + expectedValue = false; + } else if (!isNaN(parseFloat(trimmedExpected)) && isFinite(trimmedExpected)) { + // Если это похоже на число, конвертируем + expectedValue = parseFloat(trimmedExpected); + } else { + // В противном случае, это строка (убираем кавычки, если они есть) + expectedValue = trimmedExpected.replace(/^['"]|['"]$/g, ''); + } + // --- ▲▲▲ КОНЕЦ УМНОГО ПРЕОБРАЗОВАНИЯ ▲▲▲ --- + + switch (operator) { + case '==': return actualValue == expectedValue; // Нестрогое сравнение здесь полезно (e.g., 7 == "7") + case '!=': return actualValue != expectedValue; + case '>': return actualValue > expectedValue; + case '<': return actualValue < expectedValue; + case '>=': return actualValue >= expectedValue; + case '<=': return actualValue <= expectedValue; + default: return false; + } +} + async function loadWorkflowDefinition(pluginId, logger) { try { - const response = await fetch(`plugins/${pluginId}/workflow.json`); // Убран /public + const response = await fetch(`plugins/${pluginId}/workflow.json`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { @@ -71,11 +111,11 @@ async function loadWorkflowDefinition(pluginId, logger) { } } -function resolveInputs(input, context) { - if (!input) return {}; +function resolveInputs(inputs, context) { // <-- Принимает `inputs` + if (!inputs) return {}; const resolvedInput = {}; - for (const key in input) { - const value = input[key]; + for (const key in inputs) { + const value = inputs[key]; if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) { const path = value.substring(2, value.length - 2).trim(); resolvedInput[key] = getContextValue(path, context); @@ -88,6 +128,6 @@ function resolveInputs(input, context) { function getContextValue(path, context) { return path.split('.').reduce((acc, part) => { - return acc && acc[part] !== undefined ? acc[part] : null; + return (acc && typeof acc === 'object' && acc[part] !== undefined) ? acc[part] : null; }, context); } \ No newline at end of file From bc68effe9f018d782ebab75871305d82b7796c14 Mon Sep 17 00:00:00 2001 From: Igor Lebedev Date: Fri, 29 Aug 2025 16:52:31 +0500 Subject: [PATCH 4/4] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=B9:=20agent-plugins-platform=20(1.0.502),=20project-graph-a?= =?UTF-8?q?gent=20(1.0.1),=20chrome-extension=20(0.5.518),=20pyodide=20(0.?= =?UTF-8?q?27.525)=20=D0=B8=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8=D1=85.=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82=20webextens?= =?UTF-8?q?ion-polyfill=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE=D0=B2=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8.=20=D0=A3?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=83=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B5=D0=B2=D1=88=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20mcp-bridge=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20=D1=81=20=D0=BD=D0=B8=D0=BC=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=D1=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectGraphAgent/package.json | 2 +- chrome-extension/package.json | 2 +- chrome-extension/public/pyodide/package.json | 2 +- .../src/background/workflow-engine.ts | 2 +- package.json | 5 +- packages/dev-utils/package.json | 2 +- packages/env/package.json | 2 +- packages/hmr/package.json | 2 +- packages/i18n/package.json | 2 +- packages/module-manager/package.json | 2 +- packages/shared/package.json | 2 +- packages/storage/package.json | 2 +- packages/tailwindcss-config/package.json | 2 +- packages/tsconfig/package.json | 2 +- packages/ui/package.json | 2 +- packages/vite-config/package.json | 2 +- packages/zipper/package.json | 2 +- pages/content-runtime/package.json | 2 +- pages/content-ui/package.json | 2 +- pages/content/package.json | 2 +- pages/devtools/package.json | 2 +- pages/new-tab/package.json | 2 +- pages/options/package.json | 2 +- pages/side-panel/package.json | 2 +- platform-core/bridge/mcp-bridge.js | 60 ------------- platform-core/bridge/pyodide-worker.js | 66 -------------- platform-core/bridge/worker-manager.js | 37 -------- platform-core/core/host-api.js | 82 ------------------ platform-core/core/plugin-manager.js | 32 ------- platform-core/core/workflow-engine.js | 85 ------------------- platform-core/public/pyodide/package.json | 2 +- platform-core/ui/PluginCard.js | 33 ------- platform-core/ui/log-manager.js | 72 ---------------- platform-core/ui/test-harness.js | 6 +- pnpm-lock.yaml | 3 + public/pyodide/package.json | 2 +- tests/e2e/package.json | 2 +- 37 files changed, 35 insertions(+), 498 deletions(-) delete mode 100644 platform-core/bridge/mcp-bridge.js delete mode 100644 platform-core/bridge/pyodide-worker.js delete mode 100644 platform-core/bridge/worker-manager.js delete mode 100644 platform-core/core/host-api.js delete mode 100644 platform-core/core/plugin-manager.js delete mode 100644 platform-core/core/workflow-engine.js delete mode 100644 platform-core/ui/PluginCard.js delete mode 100644 platform-core/ui/log-manager.js diff --git a/ProjectGraphAgent/package.json b/ProjectGraphAgent/package.json index d29110a7..6c82a284 100644 --- a/ProjectGraphAgent/package.json +++ b/ProjectGraphAgent/package.json @@ -1,6 +1,6 @@ { "name": "project-graph-agent", - "version": "1.0.0", + "version": "1.0.1", "description": "Jsonnet-driven project control system for AI agents", "main": "scripts/graph_generator.mjs", "type": "module", diff --git a/chrome-extension/package.json b/chrome-extension/package.json index 49c86c92..924e47b8 100644 --- a/chrome-extension/package.json +++ b/chrome-extension/package.json @@ -1,6 +1,6 @@ { "name": "chrome-extension", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - core settings", "type": "module", "private": true, diff --git a/chrome-extension/public/pyodide/package.json b/chrome-extension/public/pyodide/package.json index da4509a2..e5adcdbb 100644 --- a/chrome-extension/public/pyodide/package.json +++ b/chrome-extension/public/pyodide/package.json @@ -1,6 +1,6 @@ { "name": "pyodide", - "version": "0.27.524", + "version": "0.27.525", "description": "The Pyodide JavaScript package", "keywords": [ "python", diff --git a/chrome-extension/src/background/workflow-engine.ts b/chrome-extension/src/background/workflow-engine.ts index 128d8d47..e51d75dc 100644 --- a/chrome-extension/src/background/workflow-engine.ts +++ b/chrome-extension/src/background/workflow-engine.ts @@ -3,7 +3,7 @@ * Executes declarative workflows */ -import { runPythonTool } from '../../../bridge/mcp-bridge.js'; +import { runPythonTool } from './mcp-bridge'; import { hostApi } from './host-api'; export interface WorkflowStep { diff --git a/package.json b/package.json index 6540355b..6e86ea31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-plugins-platform", - "version": "1.0.501", + "version": "1.0.502", "description": "Browser extension that enables Python plugin execution using Pyodide and MCP protocol", "license": "MIT", "private": true, @@ -41,7 +41,8 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "webextension-polyfill": "^0.12.0" }, "devDependencies": { "@types/node": "^24.3.0", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 41486370..676e0052 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@extension/dev-utils", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - dev utils", "type": "module", "private": true, diff --git a/packages/env/package.json b/packages/env/package.json index 85388a2a..a5461a00 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,6 +1,6 @@ { "name": "@extension/env", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - environment variables", "type": "module", "private": true, diff --git a/packages/hmr/package.json b/packages/hmr/package.json index e822db23..d4b51f37 100644 --- a/packages/hmr/package.json +++ b/packages/hmr/package.json @@ -1,6 +1,6 @@ { "name": "@extension/hmr", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - hot module reload/refresh", "type": "module", "private": true, diff --git a/packages/i18n/package.json b/packages/i18n/package.json index ba33747a..3121992d 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@extension/i18n", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - internationalization", "type": "module", "private": true, diff --git a/packages/module-manager/package.json b/packages/module-manager/package.json index 121e9d7e..ad9c761b 100644 --- a/packages/module-manager/package.json +++ b/packages/module-manager/package.json @@ -1,6 +1,6 @@ { "name": "@extension/module-manager", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - module manager", "type": "module", "private": true, diff --git a/packages/shared/package.json b/packages/shared/package.json index 99ecf03d..44726b87 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@extension/shared", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - shared code", "type": "module", "private": true, diff --git a/packages/storage/package.json b/packages/storage/package.json index b4b732e6..28ee7b2e 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@extension/storage", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - storage", "type": "module", "private": true, diff --git a/packages/tailwindcss-config/package.json b/packages/tailwindcss-config/package.json index 505ca697..f0de61d9 100644 --- a/packages/tailwindcss-config/package.json +++ b/packages/tailwindcss-config/package.json @@ -1,6 +1,6 @@ { "name": "@extension/tailwindcss-config", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - tailwindcss configuration", "main": "tailwind.config.ts", "private": true, diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 6f228bde..0aeda8e5 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -1,6 +1,6 @@ { "name": "@extension/tsconfig", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - tsconfig", "private": true, "sideEffects": false diff --git a/packages/ui/package.json b/packages/ui/package.json index e518f900..b522bdb4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@extension/ui", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - ui components", "type": "module", "private": true, diff --git a/packages/vite-config/package.json b/packages/vite-config/package.json index 95a0f294..6946826e 100644 --- a/packages/vite-config/package.json +++ b/packages/vite-config/package.json @@ -1,6 +1,6 @@ { "name": "@extension/vite-config", - "version": "0.5.525", + "version": "0.5.526", "description": "chrome extension - vite base configuration", "type": "module", "private": true, diff --git a/packages/zipper/package.json b/packages/zipper/package.json index b856d784..918b1be9 100644 --- a/packages/zipper/package.json +++ b/packages/zipper/package.json @@ -1,6 +1,6 @@ { "name": "@extension/zipper", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - zipper", "type": "module", "private": true, diff --git a/pages/content-runtime/package.json b/pages/content-runtime/package.json index 5eb5f93f..1e2b9f12 100644 --- a/pages/content-runtime/package.json +++ b/pages/content-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@extension/content-runtime-script", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - content runtime script", "type": "module", "private": true, diff --git a/pages/content-ui/package.json b/pages/content-ui/package.json index bb52356d..e461d17e 100644 --- a/pages/content-ui/package.json +++ b/pages/content-ui/package.json @@ -1,6 +1,6 @@ { "name": "@extension/content-ui", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - content ui", "type": "module", "private": true, diff --git a/pages/content/package.json b/pages/content/package.json index 33499b8d..fb47fbcf 100644 --- a/pages/content/package.json +++ b/pages/content/package.json @@ -1,6 +1,6 @@ { "name": "@extension/content-script", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - content script", "type": "module", "private": true, diff --git a/pages/devtools/package.json b/pages/devtools/package.json index 23f108a7..1a3695fa 100644 --- a/pages/devtools/package.json +++ b/pages/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@extension/devtools", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - devtools", "type": "module", "private": true, diff --git a/pages/new-tab/package.json b/pages/new-tab/package.json index 78af4de6..628a65b4 100644 --- a/pages/new-tab/package.json +++ b/pages/new-tab/package.json @@ -1,6 +1,6 @@ { "name": "@extension/new-tab", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - new tab", "type": "module", "private": true, diff --git a/pages/options/package.json b/pages/options/package.json index b3abd188..30f2a3bc 100644 --- a/pages/options/package.json +++ b/pages/options/package.json @@ -1,6 +1,6 @@ { "name": "@extension/options", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - options", "type": "module", "private": true, diff --git a/pages/side-panel/package.json b/pages/side-panel/package.json index 60871041..0ad16915 100644 --- a/pages/side-panel/package.json +++ b/pages/side-panel/package.json @@ -1,6 +1,6 @@ { "name": "@extension/sidepanel", - "version": "0.5.517", + "version": "0.5.518", "description": "chrome extension - side panel", "type": "module", "private": true, diff --git a/platform-core/bridge/mcp-bridge.js b/platform-core/bridge/mcp-bridge.js deleted file mode 100644 index a44e3b12..00000000 --- a/platform-core/bridge/mcp-bridge.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * bridge/mcp-bridge.js - * - * Отвечает за общение основного потока с Pyodide Web Worker. - * Реализует двустороннюю связь для вызовов Python -> Host. - */ - -import { getWorker } from './worker-manager.js'; - -let isWorkerInitialized = false; -const promises = new Map(); - -function initializeCommunication() { - if (isWorkerInitialized) return; - const pyodideWorker = getWorker(); - - pyodideWorker.onmessage = (event) => { - const { type, callId, result, error, func, args } = event.data; - - if (type === 'host_call') { - if (window.hostApi && typeof window.hostApi[func] === 'function') { - Promise.resolve(window.hostApi[func](...args)) - .then(hostResult => { - // Отправляем результат обратно в воркер - pyodideWorker.postMessage({ type: 'host_result', callId, result: hostResult }); - }) - .catch(hostError => { - // Отправляем ошибку обратно в воркер - pyodideWorker.postMessage({ type: 'host_result', callId, error: hostError.message }); - }); - } - } else if (type === 'complete' || type === 'error') { - const promise = promises.get(callId); - if (promise) { - if (type === 'complete') promise.resolve(result); - else promise.reject(new Error(error)); - promises.delete(callId); - } - } - }; - isWorkerInitialized = true; -} - -export async function runPythonTool(pluginId, toolName, toolInput) { - initializeCommunication(); - const pyodideWorker = getWorker(); - const callId = `py_tool_run_${Date.now()}_${Math.random()}`; - - const pyScriptUrl = `plugins/${pluginId}/mcp_server.py`; - const response = await fetch(pyScriptUrl); - if (!response.ok) throw new Error(`Python script для плагина ${pluginId} не найден`); - const pythonCode = await response.text(); - - return new Promise((resolve, reject) => { - promises.set(callId, { resolve, reject }); - pyodideWorker.postMessage({ - type: 'run_python_tool', callId, pythonCode, toolName, toolInput - }); - }); -} \ No newline at end of file diff --git a/platform-core/bridge/pyodide-worker.js b/platform-core/bridge/pyodide-worker.js deleted file mode 100644 index 4d178c13..00000000 --- a/platform-core/bridge/pyodide-worker.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * bridge/pyodide-worker.js - * Финальная версия: обрабатывает async Python-функции. - */ -importScripts('../pyodide/pyodide.js'); - -let pyodide; -const hostCallPromises = new Map(); - -async function initializePyodide() { - if (pyodide) return; - pyodide = await loadPyodide({ indexURL: '../pyodide/' }); - - pyodide.globals.set('js', { - sendMessageToChat: (message) => { - const jsMessage = message.toJs({ dict_converter: Object.fromEntries }); - self.postMessage({ type: 'host_call', func: 'sendMessageToChat', args: [jsMessage] }); - }, - host_fetch: (url) => { - const callId = `host_call_${Date.now()}_${Math.random()}`; - return new Promise((resolve, reject) => { - hostCallPromises.set(callId, { resolve, reject }); - self.postMessage({ type: 'host_call', func: 'host_fetch', callId, args: [url] }); - }); - } - }); -} - -const pyodideReadyPromise = initializePyodide(); - -self.onmessage = async (event) => { - await pyodideReadyPromise; - const { type, callId } = event.data; - - if (type === 'host_result') { - console.log('[Worker] Получен ответ от хоста:', event.data); - const promise = hostCallPromises.get(callId); - if (promise) { - if (event.data.error) { - promise.reject(new Error(event.data.error)); - } else { - // --- ▼▼▼ КЛЮЧЕВОЕ ИСПРАВЛЕНИЕ ▼▼▼ --- - // Мы должны передать в Python ВЕСЬ объект event.data.result, - // чтобы он мог быть преобразован в JsProxy. - promise.resolve(pyodide.toPy(event.data.result)); - // --- ▲▲▲ КОНЕЦ ИСПРАВЛЕНИЯ ▲▲▲ --- - } - hostCallPromises.delete(callId); - } - } else if (type === 'run_python_tool') { - const { pythonCode, toolName, toolInput } = event.data; - try { - await pyodide.runPythonAsync(pythonCode); - const toolFunc = pyodide.globals.get(toolName); - if (!toolFunc) throw new Error(`Python-функция "${toolName}" не найдена.`); - - const resultProxy = await toolFunc(toolInput); - const result = resultProxy.toJs({ dict_converter: Object.fromEntries }); - resultProxy.destroy(); - - self.postMessage({ type: 'complete', callId, result }); - } catch (e) { - self.postMessage({ type: 'error', callId: callId, error: e.message }); - } - } -}; \ No newline at end of file diff --git a/platform-core/bridge/worker-manager.js b/platform-core/bridge/worker-manager.js deleted file mode 100644 index 1ca6a887..00000000 --- a/platform-core/bridge/worker-manager.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * bridge/worker-manager.js - * - * Отвечает за создание и хранение единственного экземпляра Pyodide Web Worker. - * Реализует паттерн Singleton, чтобы избежать многократной инициализации - * тяжелого Pyodide-окружения. - */ - -// Приватная переменная модуля, хранящая экземпляр воркера. -let workerInstance = null; - -/** - * Возвращает единственный экземпляр Pyodide воркера. - * Если воркер еще не создан, создает его. - * @returns {Worker} - */ -export function getWorker() { - if (!workerInstance) { - console.log('[WorkerManager] Экземпляр воркера не найден. Создание нового...'); - - // Создаем воркер. Путь рассчитывается относительно текущего файла. - workerInstance = new Worker(new URL('./pyodide-worker.js', import.meta.url)); - - // Можно добавить обработчик ошибок на случай, если воркер упадет - workerInstance.onerror = (error) => { - console.error('[WorkerManager] КРИТИЧЕСКАЯ ОШИБКА ВОРКЕРА:', error); - // В случае критической ошибки, сбрасываем инстанс, - // чтобы при следующем запуске попытаться создать его заново. - workerInstance = null; - }; - - } else { - console.log('[WorkerManager] Возвращение существующего экземпляра воркера.'); - } - - return workerInstance; -} \ No newline at end of file diff --git a/platform-core/core/host-api.js b/platform-core/core/host-api.js deleted file mode 100644 index 1c8f4cbf..00000000 --- a/platform-core/core/host-api.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * core/host-api.js - * - * API-мост для связи Python-кода с браузером. - * Предоставляет JavaScript-инструменты, которые можно вызывать из воркфлоу. - */ - -// --- Вспомогательные функции, вынесенные для чистоты кода --- - -/** - * Оборачивает chrome.runtime.sendMessage в Promise для удобства и централизованной обработки. - * @param {object} message - Сообщение для отправки в background.ts. - * @returns {Promise} - */ -function sendMessageToBackground(message) { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage( - // Добавляем обязательный источник ко всем сообщениям - { source: "app-host-api", ...message }, - (response) => { - // Обработка системных ошибок связи - if (chrome.runtime.lastError) { - return reject(new Error(chrome.runtime.lastError.message)); - } - // Обработка логических ошибок от background.ts - if (response && response.error) { - return reject(new Error(response.error)); - } - resolve(response); - } - ); - }); -} - -/** - * Находит подходящую целевую вкладку (не страницу самого расширения), - * используя логгер из переданного контекста для сообщений. - * @param {object} context - Контекст выполнения воркфлоу, содержащий логгер. - * @returns {Promise} - */ -async function findTargetTab(context) { - const logger = context.logger; - logger.addMessage('HOST', 'Ищем целевую вкладку...'); - - const allTabsInWindow = await chrome.tabs.query({ currentWindow: true }); - const selfUrl = chrome.runtime.getURL('index.html'); - - const targetTab = allTabsInWindow.find(tab => - tab.url !== selfUrl && (tab.url?.startsWith('http') || tab.url?.startsWith('https')) - ); - - if (!targetTab) { - const errorMsg = "Не найдена подходящая вкладка для анализа (откройте любой сайт в этом же окне)."; - logger.addMessage('ERROR', errorMsg); - throw new Error(errorMsg); - } - - logger.addMessage('HOST', `Целевая вкладка найдена: ${targetTab.url.substring(0, 70)}...`); - return targetTab; -} - -// --- Главный экспортируемый объект API --- -export const hostApi = { - getElements: async (options, context) => { /* ... код без изменений ... */ }, - getActivePageContent: async (selectors, context) => { /* ... код без изменений ... */ }, - - host_fetch: async (url) => { - // Просто пересылаем задачу в background, который имеет все права - return sendMessageToBackground({ - command: "host_fetch", - data: { url } - }); - }, - - sendMessageToChat: (message) => { - if (window.activeWorkflowLogger) { - window.activeWorkflowLogger.addMessage('PYTHON', message.content); - } else { - console.warn("[Python Message] Логгер не активен:", message.content); - } - } -}; \ No newline at end of file diff --git a/platform-core/core/plugin-manager.js b/platform-core/core/plugin-manager.js deleted file mode 100644 index b1cdef89..00000000 --- a/platform-core/core/plugin-manager.js +++ /dev/null @@ -1,32 +0,0 @@ -const PLUGIN_DIRS = ['ozon-analyzer', 'google-helper', 'test-plugin', 'time-test']; - -export async function getAvailablePlugins() { - const plugins = []; - - for (const dirName of PLUGIN_DIRS) { - try { - const manifestUrl = `/plugins/${dirName}/manifest.json`; - const response = await fetch(manifestUrl); - - if (!response.ok) { - throw new Error(`Failed to fetch manifest: ${response.statusText}`); - } - - const manifest = await response.json(); - - plugins.push({ - id: dirName, - name: manifest.name, - version: manifest.version, - description: manifest.description, - iconUrl: `/plugins/${dirName}/${manifest.icon}`, - manifest - }); - - } catch (error) { - console.error(`Failed to load plugin from '${dirName}':`, error); - } - } - - return plugins; -} \ No newline at end of file diff --git a/platform-core/core/workflow-engine.js b/platform-core/core/workflow-engine.js deleted file mode 100644 index 2d047afd..00000000 --- a/platform-core/core/workflow-engine.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * core/workflow-engine.js - * - * Движок для выполнения декларативных воркфлоу. - */ - -import { runPythonTool } from '../bridge/mcp-bridge.js'; - -export async function runWorkflow(pluginId) { - // Здесь window.activeWorkflowLogger и createRunLogger не используются! - // Вся логика логирования убрана для service worker. - // Можно добавить простое логирование в консоль, если нужно. - console.log(`[WorkflowEngine] ▶️ Запуск воркфлоу для плагина: ${pluginId}`); - - const workflow = await loadWorkflowDefinition(pluginId); - if (!workflow) return; - - const context = { steps: {} }; - - for (const step of workflow.steps) { - console.log(`[WorkflowEngine] ➡️ Выполнение шага: ${step.id} (инструмент: ${step.tool})`); - try { - const toolInput = resolveInputs(step.input, context); - let output; - const [toolType, toolName] = step.tool.split('.'); - - if (toolType === 'host') { - if (window.hostApi && typeof window.hostApi[toolName] === 'function') { - output = await window.hostApi[toolName](toolInput, context); - } else { - throw new Error(`Host tool "${toolName}" не найден.`); - } - } else if (toolType === 'python') { - output = await runPythonTool(pluginId, toolName, toolInput); - } else { - throw new Error(`Неизвестный тип инструмента: ${step.tool}`); - } - context.steps[step.id] = { output }; - console.log(`[WorkflowEngine] ✅ Шаг ${step.id} выполнен.`); - } catch (error) { - console.error(`[WorkflowEngine] ❌ Ошибка на шаге ${step.id}:`, error); - return; - } - } - - // Финальный результат можно логировать в консоль - const lastStep = workflow.steps[workflow.steps.length - 1]; - if (lastStep && context.steps[lastStep.id]) { - const finalResult = context.steps[lastStep.id].output; - console.log(`[WorkflowEngine] 🏁 Воркфлоу завершён. Результат:`, finalResult); - } -} - -// ... вспомогательные функции, но с исправленными путями ... -async function loadWorkflowDefinition(pluginId) { - try { - const response = await fetch(`plugins/${pluginId}/workflow.json`); // Убран /public - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return await response.json(); - } catch (error) { - console.error(`[WorkflowEngine] Не удалось загрузить workflow.json:`, error); - return null; - } -} - -function resolveInputs(input, context) { - if (!input) return {}; - const resolvedInput = {}; - for (const key in input) { - const value = input[key]; - if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) { - const path = value.substring(2, value.length - 2).trim(); - resolvedInput[key] = getContextValue(path, context); - } else { - resolvedInput[key] = value; - } - } - return resolvedInput; -} - -function getContextValue(path, context) { - return path.split('.').reduce((acc, part) => { - return acc && acc[part] !== undefined ? acc[part] : null; - }, context); -} \ No newline at end of file diff --git a/platform-core/public/pyodide/package.json b/platform-core/public/pyodide/package.json index da4509a2..e5adcdbb 100644 --- a/platform-core/public/pyodide/package.json +++ b/platform-core/public/pyodide/package.json @@ -1,6 +1,6 @@ { "name": "pyodide", - "version": "0.27.524", + "version": "0.27.525", "description": "The Pyodide JavaScript package", "keywords": [ "python", diff --git a/platform-core/ui/PluginCard.js b/platform-core/ui/PluginCard.js deleted file mode 100644 index 56b2dc59..00000000 --- a/platform-core/ui/PluginCard.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * ui/PluginCard.js - */ - -// Импортируем наш новый движок -export function createPluginCard(plugin) { - const card = document.createElement('div'); - card.className = 'plugin-card clickable'; - const icon = document.createElement('img'); - icon.className = 'plugin-icon'; - if (plugin.icon) { - icon.src = `plugins/${plugin.id}/${plugin.icon}`; // Убран /public - } - icon.alt = `${plugin.name} icon`; - icon.onerror = () => { icon.src = `data:image/svg+xml;utf8,...`; }; // SVG-код заглушки - const content = document.createElement('div'); - content.className = 'plugin-content'; - const header = document.createElement('div'); - header.className = 'plugin-header'; - const name = document.createElement('span'); - name.className = 'plugin-name'; - name.textContent = plugin.name; - const version = document.createElement('span'); - version.className = 'plugin-version'; - version.textContent = `v${plugin.version}`; - header.append(name, version); - const description = document.createElement('p'); - description.className = 'plugin-description'; - description.textContent = plugin.description; - content.append(header, description); - card.append(icon, content); - return card; -} \ No newline at end of file diff --git a/platform-core/ui/log-manager.js b/platform-core/ui/log-manager.js deleted file mode 100644 index 6fa55c1c..00000000 --- a/platform-core/ui/log-manager.js +++ /dev/null @@ -1,72 +0,0 @@ -const logContainer = document.getElementById('chat-log'); -if (logContainer) logContainer.innerHTML = ''; // Очищаем при старте - -export function createRunLogger(runId, title) { - if (!logContainer) { - return { - addMessage: (stepId, message, type = 'info') => console.log(`[Logger Stub][${runId}/${stepId}] ${message}`), - renderResult: (stepId, resultObject) => console.log(`[Logger Stub][${runId}/${stepId}]`, resultObject) - }; - } - const runContainer = document.createElement('div'); - runContainer.className = 'log-run-container'; - runContainer.dataset.runId = runId; - - const header = document.createElement('div'); - header.className = 'log-run-header'; - header.textContent = `▶️ ${title} (запущен в ${new Date().toLocaleTimeString()})`; - - const body = document.createElement('div'); - body.className = 'log-run-body'; - - runContainer.append(header, body); - logContainer.prepend(runContainer); - - return { - addMessage: (stepId, message, type = 'info') => { - const messageElement = document.createElement('div'); - messageElement.className = `log-message log-type-${type}`; - messageElement.dataset.stepId = stepId; - - const contentSpan = document.createElement('span'); - contentSpan.className = 'log-content'; - contentSpan.textContent = message; - - messageElement.append(contentSpan); - body.appendChild(messageElement); - logContainer.scrollTop = logContainer.scrollHeight; - }, - renderResult: (stepId, resultObject) => { - const logRunBody = document.querySelector(`.log-run-container[data-run-id="${runId}"] .log-run-body`); - if (!logRunBody) return; - - const resultContainer = document.createElement('div'); - resultContainer.className = 'log-result-container'; - - const resultHeader = document.createElement('div'); - resultHeader.className = 'log-result-header'; - resultHeader.textContent = 'Итоговый результат'; - - const resultBody = document.createElement('div'); - resultBody.className = 'log-result-body'; - resultBody.style.display = 'none'; // Скрыто по умолчанию - - const pre = document.createElement('pre'); - pre.textContent = JSON.stringify(resultObject, null, 2); - resultBody.appendChild(pre); - - resultHeader.addEventListener('click', () => { - const isHidden = resultBody.style.display === 'none'; - resultBody.style.display = isHidden ? 'block' : 'none'; - resultHeader.textContent = (isHidden ? '▼' : '▶') + ' Итоговый результат'; - }); - // Изначально установим стрелочку - resultHeader.textContent = '▶ Итоговый результат'; - - - resultContainer.append(resultHeader, resultBody); - logRunBody.appendChild(resultContainer); - logContainer.scrollTop = logContainer.scrollHeight; - } - }; -} \ No newline at end of file diff --git a/platform-core/ui/test-harness.js b/platform-core/ui/test-harness.js index 1f497b92..819ffbc3 100644 --- a/platform-core/ui/test-harness.js +++ b/platform-core/ui/test-harness.js @@ -3,10 +3,10 @@ * Главный скрипт для нашего UI (index.html). */ -import { getAvailablePlugins } from '../core/plugin-manager.js'; +import { getAvailablePlugins } from '../../core/plugin-manager.js'; import { createPluginCard } from './PluginCard.js'; -import { hostApi } from '../core/host-api.js'; -import { runWorkflow } from '../core/workflow-engine.js'; +import { hostApi } from '../../core/host-api.js'; +import { runWorkflow } from '../../core/workflow-engine.js'; // --- Глобальная переменная для хранения "активного" логгера --- // Движок будет устанавливать ее, а hostApi.sendMessageToChat - использовать. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92c819e3..43097006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.1.1(react@19.1.1) + webextension-polyfill: + specifier: ^0.12.0 + version: 0.12.0 devDependencies: '@types/node': specifier: ^24.3.0 diff --git a/public/pyodide/package.json b/public/pyodide/package.json index da4509a2..e5adcdbb 100644 --- a/public/pyodide/package.json +++ b/public/pyodide/package.json @@ -1,6 +1,6 @@ { "name": "pyodide", - "version": "0.27.524", + "version": "0.27.525", "description": "The Pyodide JavaScript package", "keywords": [ "python", diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 8f281b80..a66c510c 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "@extension/e2e", - "version": "0.5.517", + "version": "0.5.518", "description": "E2e tests configuration boilerplate", "private": true, "sideEffects": false,