Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad930da685 | ||
|
|
ace24fa9d2 |
40
PROJECT_OVERVIEW.md
Normal file
40
PROJECT_OVERVIEW.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Проект: Screenshot AI Bridge
|
||||||
|
|
||||||
|
Этот проект представляет собой систему для автоматизации сбора, обработки и анализа снимков экрана с использованием ИИ. Система разработана для работы в среде виртуальной машины Flatlogic и обеспечивает интеграцию между браузерным расширением и бэкенд-обработкой.
|
||||||
|
|
||||||
|
## Основные компоненты
|
||||||
|
|
||||||
|
1. **`tools/local_screenshot_bridge.py`**:
|
||||||
|
- Основной сервис (бэкенд), написанный на Python.
|
||||||
|
- Запускается как процесс PM2 на порту 3001.
|
||||||
|
- Обеспечивает следующие функции:
|
||||||
|
- **API эндпоинты**:
|
||||||
|
- `/`: Основной эндпоинт для данных (health check и JSON-ответы).
|
||||||
|
- `/health`: Проверка состояния системы.
|
||||||
|
- `/guide`: Человекочитаемая страница с инструкциями.
|
||||||
|
- `/download/chrome-extension.zip`: Динамическая генерация и загрузка архива с расширением для Chrome.
|
||||||
|
- **ИИ интеграция**:
|
||||||
|
- Автоматический сбор и анализ контента со скриншотов.
|
||||||
|
- Конфигурируемые параметры ИИ (напр. `--ai-max-output-tokens 2500`).
|
||||||
|
- Автоматическое сохранение результатов в директории `screenshots/` (PNG, JSON, .ai.json).
|
||||||
|
|
||||||
|
2. **`chrome_screenshot_ext/`**:
|
||||||
|
- Расширение для браузера Chrome.
|
||||||
|
- Используется для захвата содержимого экрана и отправки его в `local_screenshot_bridge` для последующей обработки.
|
||||||
|
|
||||||
|
3. **`screenshots/`**:
|
||||||
|
- Директория для хранения результатов обработки (снимки экрана, метаданные, контент, результаты анализа ИИ).
|
||||||
|
|
||||||
|
4. **`scripts/`**:
|
||||||
|
- Вспомогательные скрипты для подготовки ответов и обработки событий (напр. `on_screenshot.sh`).
|
||||||
|
|
||||||
|
## Развертывание и эксплуатация
|
||||||
|
|
||||||
|
- **Порт**: Приложение слушает порт 3001.
|
||||||
|
- **Прокси**: Доступ снаружи осуществляется через Apache (reverse proxy), который перенаправляет запросы на локальный порт 3001.
|
||||||
|
- **PM2**: Управление процессом `screenshot-bridge` осуществляется через PM2.
|
||||||
|
- **Конфигурация**: Параметры работы ИИ (такие как `ai-max-output-tokens`) задаются при запуске процесса через аргументы командной строки.
|
||||||
|
|
||||||
|
## Как использовать
|
||||||
|
Для получения расширения используйте эндпоинт: `https://<ваша-доменная-зона>/download/chrome-extension.zip`
|
||||||
|
Документация и статус системы доступны по адресу: `https://<ваша-доменная-зона>/guide`
|
||||||
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@ -8,10 +9,12 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import zipfile
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
_LOG_LEVELS = {"debug": 10, "info": 20, "error": 40, "quiet": 100}
|
_LOG_LEVELS = {"debug": 10, "info": 20, "error": 40, "quiet": 100}
|
||||||
@ -585,20 +588,172 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
def do_GET(self): # noqa: N802
|
def _send_html(self, status: int, html: str):
|
||||||
if self.path not in ("/", "/health"):
|
body = html.encode("utf-8")
|
||||||
self._send_json(404, {"ok": False, "error": "not_found"})
|
self.send_response(status)
|
||||||
return
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
self._send_json(
|
self.send_header("Content-Length", str(len(body)))
|
||||||
200,
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
{
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def _send_bytes(self, status: int, body: bytes, content_type: str, content_disposition: Optional[str] = None):
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", content_type)
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
if content_disposition:
|
||||||
|
self.send_header("Content-Disposition", content_disposition)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def _build_extension_zip(self) -> tuple[Optional[bytes], Optional[str]]:
|
||||||
|
project_root: Path = self.server.project_root # type: ignore[attr-defined]
|
||||||
|
ext_dir = project_root / "chrome_screenshot_ext"
|
||||||
|
if not ext_dir.exists() or not ext_dir.is_dir():
|
||||||
|
return None, f"missing extension directory: {ext_dir}"
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for p in sorted(ext_dir.rglob("*")):
|
||||||
|
if p.is_file():
|
||||||
|
zf.write(p, arcname=p.relative_to(project_root))
|
||||||
|
return buf.getvalue(), None
|
||||||
|
|
||||||
|
def _health_payload(self) -> dict:
|
||||||
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"service": "local_screenshot_bridge",
|
"service": "local_screenshot_bridge",
|
||||||
"out_dir": str(self.server.out_dir), # type: ignore[attr-defined]
|
"out_dir": str(self.server.out_dir), # type: ignore[attr-defined]
|
||||||
"has_run_cmd": bool(getattr(self.server, "run_cmd", None)), # type: ignore[attr-defined]
|
"has_run_cmd": bool(getattr(self.server, "run_cmd", None)), # type: ignore[attr-defined]
|
||||||
"ai_enabled": bool(getattr(self.server, "ai_enabled", False)), # type: ignore[attr-defined]
|
"ai_enabled": bool(getattr(self.server, "ai_enabled", False)), # type: ignore[attr-defined]
|
||||||
},
|
}
|
||||||
|
|
||||||
|
def _home_html(self) -> str:
|
||||||
|
health = self._health_payload()
|
||||||
|
ai_text = "Enabled" if health["ai_enabled"] else "Disabled"
|
||||||
|
run_cmd_text = "Configured" if health["has_run_cmd"] else "Not configured"
|
||||||
|
return f"""<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Local Screenshot Bridge</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card: #ffffff;
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #475569;
|
||||||
|
--line: #e2e8f0;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--good: #16a34a;
|
||||||
|
}}
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
background: linear-gradient(180deg, #f8fafc, #eef2ff);
|
||||||
|
color: var(--text);
|
||||||
|
font: 16px/1.5 Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
|
||||||
|
padding: 36px 16px;
|
||||||
|
}}
|
||||||
|
.wrap {{ max-width: 820px; margin: 0 auto; }}
|
||||||
|
.card {{
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 26px;
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}}
|
||||||
|
h1 {{ margin: 0 0 8px; font-size: 1.8rem; }}
|
||||||
|
p {{ margin: 0 0 14px; color: var(--muted); }}
|
||||||
|
.status {{
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin: 18px 0 8px;
|
||||||
|
}}
|
||||||
|
.pill {{
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #fff;
|
||||||
|
}}
|
||||||
|
.pill strong {{ color: var(--good); }}
|
||||||
|
ol {{
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}}
|
||||||
|
code {{
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: .92em;
|
||||||
|
}}
|
||||||
|
a {{ color: var(--accent); text-decoration: none; }}
|
||||||
|
a:hover {{ text-decoration: underline; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="wrap">
|
||||||
|
<section class="card">
|
||||||
|
<h1>✅ Screenshot Bridge is running</h1>
|
||||||
|
<p>This server accepts screenshot payloads from the Chrome extension and can generate AI response suggestions.</p>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div class="pill">Server: <strong>Online</strong></div>
|
||||||
|
<div class="pill">AI: <strong>{ai_text}</strong></div>
|
||||||
|
<div class="pill">Run command: <strong>{run_cmd_text}</strong></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>How to use</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Download the extension zip from <a href="/download/chrome-extension.zip"><code>/download/chrome-extension.zip</code></a> (or copy <code>chrome_screenshot_ext</code> manually).</li>
|
||||||
|
<li>Open <code>chrome://extensions</code> in Chrome and enable <strong>Developer mode</strong>.</li>
|
||||||
|
<li>Click <strong>Load unpacked</strong> and choose the <code>chrome_screenshot_ext</code> folder.</li>
|
||||||
|
<li>Open any page, run the extension, and send a screenshot to this server.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p style="margin-top:16px;">Need JSON health output? Use <a href="/health"><code>/health</code></a>.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
def do_GET(self): # noqa: N802
|
||||||
|
parsed = urlparse(self.path)
|
||||||
|
path = parsed.path or "/"
|
||||||
|
|
||||||
|
if path == "/":
|
||||||
|
# Keep root JSON-compatible for extension and existing integrations.
|
||||||
|
self._send_json(200, self._health_payload())
|
||||||
|
return
|
||||||
|
|
||||||
|
if path in ("/guide", "/docs"):
|
||||||
|
self._send_html(200, self._home_html())
|
||||||
|
return
|
||||||
|
|
||||||
|
if path in ("/download/chrome-extension.zip", "/chrome_screenshot_ext.zip"):
|
||||||
|
zip_bytes, err = self._build_extension_zip()
|
||||||
|
if err:
|
||||||
|
_log(self.server, "error", f"Extension zip failed: {err}") # type: ignore[arg-type]
|
||||||
|
self._send_json(500, {"ok": False, "error": "extension_zip_failed", "detail": err})
|
||||||
|
return
|
||||||
|
self._send_bytes(
|
||||||
|
200,
|
||||||
|
zip_bytes or b"",
|
||||||
|
"application/zip",
|
||||||
|
'attachment; filename="chrome_screenshot_ext.zip"',
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if path == "/health":
|
||||||
|
self._send_json(200, self._health_payload())
|
||||||
|
return
|
||||||
|
|
||||||
|
self._send_json(404, {"ok": False, "error": "not_found"})
|
||||||
|
|
||||||
def do_OPTIONS(self): # noqa: N802
|
def do_OPTIONS(self): # noqa: N802
|
||||||
self.send_response(204)
|
self.send_response(204)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user