add imrpvoed
This commit is contained in:
parent
0eb918c893
commit
beb64e2ed0
@ -27,6 +27,7 @@ Notes:
|
||||
- The server listens on `http://127.0.0.1:8765/screenshot`.
|
||||
- If you omit `--ai`, it will only save files (no OpenAI call).
|
||||
- If you set `--ai`, it will generate reply suggestions and return them back to the extension (and also save `*.ai.json`).
|
||||
- If you're debugging, add `--log-level debug` (or set `AIEA_LOG_LEVEL=debug`) for more detail.
|
||||
- Optional: you can still use `--run ...` as a post-save hook.
|
||||
|
||||
## 2) Load the extension (unpacked)
|
||||
|
||||
@ -65,6 +65,14 @@ Without OpenAI (save files only):
|
||||
python3 tools/local_screenshot_bridge.py --port 8765 --out-dir screenshots
|
||||
```
|
||||
|
||||
More logging (includes AI request/response ids + parse details):
|
||||
|
||||
```bash
|
||||
python3 tools/local_screenshot_bridge.py --port 8765 --out-dir screenshots --ai --log-level debug
|
||||
|
||||
python3 tools/local_screenshot_bridge.py --port 8765 --out-dir screenshots --ai --log-level debug --ai-max-output-tokens 2500
|
||||
```
|
||||
|
||||
Health check:
|
||||
|
||||
```bash
|
||||
|
||||
@ -46,7 +46,14 @@ function renderResult(entryOrResp) {
|
||||
if (resp.ai_result) {
|
||||
if (resp.ai_result.ok && resp.ai_result.ai && Array.isArray(resp.ai_result.ai.posts)) {
|
||||
lines.push("");
|
||||
lines.push(`AI (${resp.ai_result.took_ms || "?"}ms):`);
|
||||
{
|
||||
const ms = resp.ai_result.took_ms || "?";
|
||||
const model = resp.ai_result.model ? ` model=${resp.ai_result.model}` : "";
|
||||
const rid = resp.ai_result.response_id ? ` id=${resp.ai_result.response_id}` : "";
|
||||
const status = resp.ai_result.status && resp.ai_result.status !== "completed" ? ` status=${resp.ai_result.status}` : "";
|
||||
const inc = resp.ai_result.incomplete_reason ? ` incomplete=${resp.ai_result.incomplete_reason}` : "";
|
||||
lines.push(`AI (${ms}ms${model}${rid}${status}${inc}):`);
|
||||
}
|
||||
for (const p of resp.ai_result.ai.posts) {
|
||||
const idx = typeof p.index === "number" ? p.index : "?";
|
||||
const postText = (p.post_text || "").replace(/\s+/g, " ").trim();
|
||||
@ -84,10 +91,40 @@ function renderResult(entryOrResp) {
|
||||
}
|
||||
}
|
||||
if (resp.ai_result.ai_path) lines.push(`\nAI file: ${resp.ai_result.ai_path}`);
|
||||
if (resp.ai_result.usage && typeof resp.ai_result.usage === "object") {
|
||||
const u = resp.ai_result.usage;
|
||||
const ins = typeof u.input_tokens === "number" ? u.input_tokens : null;
|
||||
const outs = typeof u.output_tokens === "number" ? u.output_tokens : null;
|
||||
const tots = typeof u.total_tokens === "number" ? u.total_tokens : null;
|
||||
const parts = [];
|
||||
if (ins != null) parts.push(`in=${ins}`);
|
||||
if (outs != null) parts.push(`out=${outs}`);
|
||||
if (tots != null) parts.push(`total=${tots}`);
|
||||
if (parts.length) lines.push(`AI usage: ${parts.join(" ")}`);
|
||||
}
|
||||
} else {
|
||||
lines.push("");
|
||||
lines.push(`AI error: ${resp.ai_result.error || "unknown"}`);
|
||||
if (resp.ai_result.detail) lines.push(`Detail: ${resp.ai_result.detail}`);
|
||||
const ms = resp.ai_result.took_ms || "?";
|
||||
const model = resp.ai_result.model ? ` model=${resp.ai_result.model}` : "";
|
||||
const rid = resp.ai_result.response_id ? ` id=${resp.ai_result.response_id}` : "";
|
||||
const status = resp.ai_result.status ? ` status=${resp.ai_result.status}` : "";
|
||||
const inc = resp.ai_result.incomplete_reason ? ` incomplete=${resp.ai_result.incomplete_reason}` : "";
|
||||
const err = resp.ai_result.error || (resp.ai_result.ai && resp.ai_result.ai.error) || "unknown";
|
||||
lines.push(`AI error (${ms}ms${model}${rid}${status}${inc}): ${err}`);
|
||||
if (resp.ai_result.detail) lines.push(`Detail: ${clampString(resp.ai_result.detail, 600)}`);
|
||||
if (resp.ai_result.raw_preview) lines.push(`Raw: ${clampString(resp.ai_result.raw_preview, 600)}`);
|
||||
if (resp.ai_result.usage && typeof resp.ai_result.usage === "object") {
|
||||
const u = resp.ai_result.usage;
|
||||
const ins = typeof u.input_tokens === "number" ? u.input_tokens : null;
|
||||
const outs = typeof u.output_tokens === "number" ? u.output_tokens : null;
|
||||
const tots = typeof u.total_tokens === "number" ? u.total_tokens : null;
|
||||
const parts = [];
|
||||
if (ins != null) parts.push(`in=${ins}`);
|
||||
if (outs != null) parts.push(`out=${outs}`);
|
||||
if (tots != null) parts.push(`total=${tots}`);
|
||||
if (parts.length) lines.push(`AI usage: ${parts.join(" ")}`);
|
||||
}
|
||||
if (resp.ai_result.ai_path) lines.push(`AI file: ${resp.ai_result.ai_path}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -222,7 +222,7 @@ def main(argv: list[str]) -> int:
|
||||
"definition_of_post": "A single feed item / post / story / comment root visible on-screen right now. If it's a single-article page, treat the main article as one post.",
|
||||
"output_requirements": {
|
||||
#"ironic": "Lightly ironic, laughing at us humans (not cruel).",
|
||||
"improved": "Proofreading-style improvements, preserving the original words as much as possible. Improving in medium version.",
|
||||
"improved": "Proofread whatever is given in extra_instructions [EXTRA_INSTRUCTIONS]!!! Proofreading-style improvements, preserving the original words as much as possible. Improving in medium version.",
|
||||
"critical": "Bold/critical: politely questions the premise or assumptions.",
|
||||
"suggested": "Best style you think fits (helpful/witty/clarifying/etc).",
|
||||
"short": "1-2 sentences, direct, useful, no fluff.",
|
||||
|
||||
@ -7,9 +7,33 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
_LOG_LEVELS = {"debug": 10, "info": 20, "error": 40, "quiet": 100}
|
||||
|
||||
|
||||
def _log(server, level: str, msg: str) -> None:
|
||||
"""
|
||||
Minimal structured-ish logging to stderr.
|
||||
|
||||
server.log_level: debug|info|error|quiet (default: info)
|
||||
"""
|
||||
try:
|
||||
configured = getattr(server, "log_level", "info") if server is not None else "info"
|
||||
configured_rank = _LOG_LEVELS.get(str(configured).lower(), _LOG_LEVELS["info"])
|
||||
level_rank = _LOG_LEVELS.get(str(level).lower(), _LOG_LEVELS["info"])
|
||||
if level_rank < configured_rank:
|
||||
return
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
print(f"[{ts}] {level.upper()}: {msg}", file=sys.stderr)
|
||||
except Exception:
|
||||
# Never let logging break the handler.
|
||||
return
|
||||
|
||||
|
||||
def _slug(s: str, max_len: int = 80) -> str:
|
||||
@ -58,6 +82,91 @@ def _safe_json_dump(obj: object, max_chars: int) -> str:
|
||||
s = json.dumps(obj, ensure_ascii=True, separators=(",", ":"), sort_keys=False)
|
||||
return _truncate(s, max_chars)
|
||||
|
||||
|
||||
def _maybe_dump_model(obj: object) -> Optional[dict]:
|
||||
"""
|
||||
Best-effort conversion of SDK model objects (pydantic-ish) to plain dicts.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
for attr in ("model_dump", "dict", "to_dict"):
|
||||
fn = getattr(obj, attr, None)
|
||||
if callable(fn):
|
||||
try:
|
||||
out = fn()
|
||||
return out if isinstance(out, dict) else None
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _extract_json_text(raw: str) -> Optional[str]:
|
||||
"""
|
||||
Best-effort extraction of a JSON object from a text response.
|
||||
|
||||
Some providers occasionally wrap JSON in code fences or add preamble text.
|
||||
We keep this conservative; if it can't be extracted, we return None.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
# ```json ... ``` or any fenced block: ```...```
|
||||
# We capture the whole fenced body (not just {...}) so nested braces don't break extraction.
|
||||
m = re.search(r"```(?:[a-z0-9_+-]+)?\s*(.*?)\s*```", raw, flags=re.DOTALL | re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
|
||||
# First {...last} span
|
||||
start = raw.find("{")
|
||||
end = raw.rfind("}")
|
||||
if start != -1 and end != -1 and end > start:
|
||||
return raw[start : end + 1].strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _ai_error_payload(
|
||||
*,
|
||||
error: str,
|
||||
detail: str = "",
|
||||
raw: str = "",
|
||||
model: str = "",
|
||||
response_id: str = "",
|
||||
status: str = "",
|
||||
incomplete_reason: str = "",
|
||||
usage: Optional[dict] = None,
|
||||
took_ms: Optional[int] = None,
|
||||
ai_path: Optional[Path] = None,
|
||||
) -> dict:
|
||||
payload: dict = {"ok": False, "error": error}
|
||||
if detail:
|
||||
payload["detail"] = detail
|
||||
if raw:
|
||||
payload["raw_preview"] = _truncate(raw, 2000)
|
||||
if model:
|
||||
payload["model"] = model
|
||||
if response_id:
|
||||
payload["response_id"] = response_id
|
||||
if status:
|
||||
payload["status"] = status
|
||||
if incomplete_reason:
|
||||
payload["incomplete_reason"] = incomplete_reason
|
||||
if usage:
|
||||
payload["usage"] = usage
|
||||
if isinstance(took_ms, int):
|
||||
payload["took_ms"] = took_ms
|
||||
if ai_path is not None:
|
||||
payload["ai_path"] = str(ai_path)
|
||||
return payload
|
||||
|
||||
|
||||
def _is_valid_ai_json(parsed: object) -> bool:
|
||||
if not isinstance(parsed, dict):
|
||||
return False
|
||||
posts = parsed.get("posts", None)
|
||||
return isinstance(posts, list)
|
||||
|
||||
|
||||
def _ea_sanitize_text(text: object) -> str:
|
||||
"""
|
||||
Port of fl_geo_sanitize_text(), plus lowercase output (no capitals).
|
||||
@ -209,11 +318,13 @@ def _maybe_generate_ai(server, png_path: Path, meta: dict, content: object) -> d
|
||||
_load_dotenv_if_present(project_root)
|
||||
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
_log(server, "error", "AI disabled: missing OPENAI_API_KEY")
|
||||
return {"ok": False, "error": "missing_openai_api_key"}
|
||||
|
||||
try:
|
||||
from openai import OpenAI # type: ignore
|
||||
except Exception as e:
|
||||
_log(server, "error", f"AI disabled: missing openai sdk ({type(e).__name__}: {e})")
|
||||
return {"ok": False, "error": "missing_openai_sdk", "detail": str(e)}
|
||||
|
||||
instructions_text = getattr(server, "ai_instructions", "")
|
||||
@ -227,6 +338,12 @@ def _maybe_generate_ai(server, png_path: Path, meta: dict, content: object) -> d
|
||||
page_title = str(meta.get("title") or "")
|
||||
extra_instructions = str(meta.get("extra_instructions") or "").strip()
|
||||
|
||||
_log(
|
||||
server,
|
||||
"debug",
|
||||
f"AI request: model={model} max_posts={max_posts} image_detail={image_detail} max_output_tokens={max_output_tokens} url={_truncate(page_url, 140)}",
|
||||
)
|
||||
|
||||
user_payload = {
|
||||
"page_url": page_url,
|
||||
"page_title": page_title,
|
||||
@ -237,7 +354,7 @@ def _maybe_generate_ai(server, png_path: Path, meta: dict, content: object) -> d
|
||||
"definition_of_post": "A single feed item / post / story / comment root visible on-screen right now. If it's a single-article page, treat the main article as one post.",
|
||||
"output_requirements": {
|
||||
# "ironic": "Lightly ironic, laughing at us humans (not cruel).",
|
||||
"improved": "Proofreading-style improvements, preserving the original words as much as possible. Improving in medium version.",
|
||||
"improved": "Proofread whatever is given in extra_instructions [EXTRA_INSTRUCTIONS]. Proofreading-style improvements, preserving the original words as much as possible. Improving in medium version.",
|
||||
"critical": "Bold/critical: politely questions the premise or assumptions.",
|
||||
"suggested": "Best style you think fits (helpful/witty/clarifying/etc).",
|
||||
"short": "1-2 sentences, direct, useful, no fluff.",
|
||||
@ -266,49 +383,196 @@ def _maybe_generate_ai(server, png_path: Path, meta: dict, content: object) -> d
|
||||
|
||||
t0 = time.monotonic()
|
||||
client = OpenAI()
|
||||
resp = client.responses.create(
|
||||
model=model,
|
||||
instructions=instructions_text,
|
||||
input=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": prompt_text},
|
||||
{"type": "input_image", "image_url": image_data_url, "detail": image_detail},
|
||||
],
|
||||
}
|
||||
],
|
||||
text={
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "ea_post_responses",
|
||||
"description": "Draft short and medium replies for each visible post on the page.",
|
||||
"schema": _response_schema(max_posts),
|
||||
"strict": True,
|
||||
resp = None
|
||||
took_ms = None
|
||||
try:
|
||||
resp = client.responses.create(
|
||||
model=model,
|
||||
instructions=instructions_text,
|
||||
input=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "input_text", "text": prompt_text},
|
||||
{"type": "input_image", "image_url": image_data_url, "detail": image_detail},
|
||||
],
|
||||
}
|
||||
],
|
||||
text={
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "ea_post_responses",
|
||||
"description": "Draft short and medium replies for each visible post on the page.",
|
||||
"schema": _response_schema(max_posts),
|
||||
"strict": True,
|
||||
},
|
||||
"verbosity": "low",
|
||||
},
|
||||
"verbosity": "low",
|
||||
},
|
||||
max_output_tokens=max_output_tokens,
|
||||
)
|
||||
took_ms = int((time.monotonic() - t0) * 1000)
|
||||
max_output_tokens=max_output_tokens,
|
||||
)
|
||||
took_ms = int((time.monotonic() - t0) * 1000)
|
||||
except Exception as e:
|
||||
took_ms = int((time.monotonic() - t0) * 1000)
|
||||
tb = traceback.format_exc(limit=8)
|
||||
detail = f"{type(e).__name__}: {e}"
|
||||
# Some OpenAI exceptions have request id / status in attrs; include if present.
|
||||
rid = getattr(e, "request_id", "") or getattr(getattr(e, "response", None), "headers", {}).get("x-request-id", "")
|
||||
sc = getattr(e, "status_code", None)
|
||||
if rid:
|
||||
detail = f"{detail} (request_id={rid})"
|
||||
if isinstance(sc, int):
|
||||
detail = f"{detail} (status={sc})"
|
||||
_log(server, "error", f"AI exception after {took_ms}ms: {detail}")
|
||||
_log(server, "debug", f"AI traceback:\n{tb}")
|
||||
# Still write a debug file for the screenshot for later inspection.
|
||||
ai_path = png_path.with_suffix(".ai.json")
|
||||
debug_obj = {
|
||||
"error": "ai_exception",
|
||||
"detail": detail,
|
||||
"took_ms": took_ms,
|
||||
"model": str(model),
|
||||
"traceback": tb if getattr(server, "log_level", "info") == "debug" else "",
|
||||
}
|
||||
try:
|
||||
ai_path.write_text(json.dumps(debug_obj, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
return _ai_error_payload(
|
||||
error="ai_exception",
|
||||
detail=detail,
|
||||
model=str(model),
|
||||
response_id="",
|
||||
status="",
|
||||
incomplete_reason="",
|
||||
usage=None,
|
||||
took_ms=took_ms,
|
||||
ai_path=ai_path,
|
||||
)
|
||||
|
||||
raw = resp.output_text or ""
|
||||
response_id = str(getattr(resp, "id", "") or "")
|
||||
status = str(getattr(resp, "status", "") or "")
|
||||
incomplete_reason = ""
|
||||
try:
|
||||
incomplete_details = getattr(resp, "incomplete_details", None)
|
||||
if incomplete_details is not None:
|
||||
incomplete_reason = str(getattr(incomplete_details, "reason", "") or "")
|
||||
except Exception:
|
||||
incomplete_reason = ""
|
||||
usage = _maybe_dump_model(getattr(resp, "usage", None))
|
||||
|
||||
_log(
|
||||
server,
|
||||
"debug",
|
||||
f"AI response meta: status={status or '?'} incomplete_reason={incomplete_reason or '(none)'} usage={_safe_json_dump(usage or {}, 800)} response_id={response_id}",
|
||||
)
|
||||
|
||||
raw = ""
|
||||
try:
|
||||
raw = resp.output_text or ""
|
||||
except Exception:
|
||||
raw = ""
|
||||
_log(server, "debug", f"AI output_text chars={len(raw)} response_id={response_id}")
|
||||
|
||||
parsed: object
|
||||
parse_error = ""
|
||||
parse_detail = ""
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except Exception:
|
||||
parsed = {"error": "non_json_output", "raw": raw}
|
||||
except Exception as e:
|
||||
# Try a couple conservative extraction heuristics.
|
||||
candidate = _extract_json_text(raw)
|
||||
if candidate:
|
||||
try:
|
||||
parsed = json.loads(candidate)
|
||||
except Exception as e2:
|
||||
parse_error = "non_json_output"
|
||||
parse_detail = f"{type(e2).__name__}: {e2}"
|
||||
parsed = {"error": "non_json_output", "raw": raw}
|
||||
else:
|
||||
parse_error = "non_json_output"
|
||||
parse_detail = f"{type(e).__name__}: {e}"
|
||||
parsed = {"error": "non_json_output", "raw": raw}
|
||||
|
||||
if parse_error:
|
||||
_log(server, "error", f"AI output parse failed: {parse_error} ({parse_detail}) response_id={response_id}")
|
||||
|
||||
if isinstance(parsed, dict) and "posts" in parsed:
|
||||
parsed = _sanitize_ai_payload(parsed, page_url=page_url, page_title=page_title)
|
||||
|
||||
ai_path = png_path.with_suffix(".ai.json")
|
||||
ai_path.write_text(json.dumps(parsed, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
||||
return {"ok": True, "ai": parsed, "ai_path": str(ai_path), "took_ms": took_ms}
|
||||
try:
|
||||
# When generation fails, persist helpful metadata along with the raw text.
|
||||
if parse_error and isinstance(parsed, dict) and "raw" in parsed:
|
||||
parsed = {
|
||||
**parsed,
|
||||
"response_id": response_id,
|
||||
"model": str(model),
|
||||
"status": status,
|
||||
"incomplete_reason": incomplete_reason,
|
||||
"usage": usage or {},
|
||||
}
|
||||
ai_path.write_text(json.dumps(parsed, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
||||
except Exception as e:
|
||||
_log(server, "error", f"Failed writing AI file {ai_path}: {type(e).__name__}: {e}")
|
||||
|
||||
# If we didn't get schema-conformant output, treat it as an error so the popup doesn't show "unknown".
|
||||
if not _is_valid_ai_json(parsed):
|
||||
err = parse_error or (parsed.get("error", "") if isinstance(parsed, dict) else "") or "invalid_ai_output"
|
||||
detail = parse_detail or "AI returned JSON that does not match the expected schema (missing posts[])."
|
||||
if incomplete_reason:
|
||||
err = incomplete_reason if incomplete_reason in ("max_output_tokens", "content_filter") else err
|
||||
if incomplete_reason == "max_output_tokens":
|
||||
detail = (
|
||||
f"AI response was truncated (incomplete_reason=max_output_tokens). "
|
||||
f"Try increasing --ai-max-output-tokens (currently {max_output_tokens}) or decreasing --ai-max-posts (currently {max_posts}). "
|
||||
f"Original parse error: {parse_detail or '(none)'}"
|
||||
)
|
||||
else:
|
||||
detail = f"AI response incomplete (incomplete_reason={incomplete_reason}). Parse error: {parse_detail or '(none)'}"
|
||||
_log(server, "error", f"AI output invalid: error={err} response_id={response_id} ai_path={ai_path}")
|
||||
return _ai_error_payload(
|
||||
error=str(err),
|
||||
detail=str(detail),
|
||||
raw=raw,
|
||||
model=str(model),
|
||||
response_id=response_id,
|
||||
status=status,
|
||||
incomplete_reason=incomplete_reason,
|
||||
usage=usage,
|
||||
took_ms=took_ms,
|
||||
ai_path=ai_path,
|
||||
)
|
||||
|
||||
posts_count = len(parsed.get("posts", [])) if isinstance(parsed, dict) else 0
|
||||
_log(server, "info", f"AI ok: posts={posts_count} took_ms={took_ms} response_id={response_id} ai_path={ai_path}")
|
||||
return {
|
||||
"ok": True,
|
||||
"ai": parsed,
|
||||
"ai_path": str(ai_path),
|
||||
"took_ms": took_ms,
|
||||
"response_id": response_id,
|
||||
"model": str(model),
|
||||
"status": status,
|
||||
"incomplete_reason": incomplete_reason,
|
||||
"usage": usage,
|
||||
}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "LocalScreenshotBridge/0.1"
|
||||
|
||||
def log_message(self, fmt: str, *args): # noqa: N802
|
||||
# Route default HTTP request logs through our logger (and respect --log-level).
|
||||
try:
|
||||
msg = fmt % args
|
||||
except Exception:
|
||||
msg = fmt
|
||||
try:
|
||||
ip = self.client_address[0] if getattr(self, "client_address", None) else "?"
|
||||
except Exception:
|
||||
ip = "?"
|
||||
_log(self.server, "debug", f"HTTP {ip} {msg}") # type: ignore[arg-type]
|
||||
|
||||
def _send_json(self, status: int, payload: dict):
|
||||
body = json.dumps(payload, ensure_ascii=True).encode("utf-8")
|
||||
self.send_response(status)
|
||||
@ -368,6 +632,18 @@ class Handler(BaseHTTPRequestHandler):
|
||||
content = req.get("content", None)
|
||||
extra_instructions = req.get("extra_instructions") or ""
|
||||
|
||||
extra_s = str(extra_instructions).strip()
|
||||
if extra_s:
|
||||
extra_s = _truncate(extra_s.replace("\r", " ").replace("\n", " "), 140)
|
||||
extra_part = f" extra={extra_s}"
|
||||
else:
|
||||
extra_part = ""
|
||||
_log(
|
||||
self.server, # type: ignore[arg-type]
|
||||
"info",
|
||||
f"Capture received: title={_truncate(str(title), 80)} url={_truncate(str(page_url), 140)} content={'yes' if content is not None else 'no'}{extra_part}",
|
||||
)
|
||||
|
||||
m = re.match(r"^data:image/png;base64,(.*)$", data_url)
|
||||
if not m:
|
||||
self._send_json(400, {"ok": False, "error": "expected_png_data_url"})
|
||||
@ -407,6 +683,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
content_path.write_text(raw_content, encoding="utf-8")
|
||||
wrote_content = True
|
||||
except Exception:
|
||||
_log(self.server, "error", f"Failed writing content file {content_path}") # type: ignore[arg-type]
|
||||
# Don't fail the whole request if content writing fails.
|
||||
wrote_content = False
|
||||
|
||||
@ -442,6 +719,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"content_path": final_content_path,
|
||||
"extra_instructions": extra_instructions,
|
||||
}
|
||||
_log(
|
||||
self.server, # type: ignore[arg-type]
|
||||
"info",
|
||||
f"Saved: png={png_path} meta={meta_path} content={final_content_path or '(none)'}",
|
||||
)
|
||||
|
||||
run = getattr(self.server, "run_cmd", None) # type: ignore[attr-defined]
|
||||
ran = None
|
||||
@ -464,15 +746,21 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"stdout": proc.stdout[-4000:],
|
||||
"stderr": proc.stderr[-4000:],
|
||||
}
|
||||
if proc.returncode != 0:
|
||||
_log(self.server, "error", f"Hook failed: exit={proc.returncode} cmd={' '.join(run)}") # type: ignore[arg-type]
|
||||
except Exception as e:
|
||||
ran = {"cmd": run, "error": str(e)}
|
||||
_log(self.server, "error", f"Hook exception: {type(e).__name__}: {e}") # type: ignore[arg-type]
|
||||
|
||||
ai_result = None
|
||||
if getattr(self.server, "ai_enabled", False): # type: ignore[attr-defined]
|
||||
try:
|
||||
ai_result = _maybe_generate_ai(self.server, png_path, meta_obj, content)
|
||||
except Exception as e:
|
||||
ai_result = {"ok": False, "error": "ai_exception", "detail": str(e)}
|
||||
detail = f"{type(e).__name__}: {e}"
|
||||
_log(self.server, "error", f"AI exception (outer): {detail}") # type: ignore[arg-type]
|
||||
_log(self.server, "debug", f"AI outer traceback:\n{traceback.format_exc(limit=8)}") # type: ignore[arg-type]
|
||||
ai_result = {"ok": False, "error": "ai_exception", "detail": detail}
|
||||
|
||||
self._send_json(
|
||||
200,
|
||||
@ -493,6 +781,12 @@ def main(argv: list[str]) -> int:
|
||||
p.add_argument("--bind", default="127.0.0.1", help="Bind address (default: 127.0.0.1)")
|
||||
p.add_argument("--out-dir", default="screenshots", help="Output directory relative to project root")
|
||||
p.add_argument("--ai", action="store_true", help="Run OpenAI to generate reply suggestions and return them to the extension")
|
||||
p.add_argument(
|
||||
"--log-level",
|
||||
default=os.getenv("AIEA_LOG_LEVEL", "info"),
|
||||
choices=sorted(_LOG_LEVELS.keys()),
|
||||
help="Logging verbosity: debug|info|error|quiet (env: AIEA_LOG_LEVEL)",
|
||||
)
|
||||
p.add_argument("--ai-model", default=os.getenv("AI_EA_MODEL", "gpt-5.2"))
|
||||
p.add_argument("--ai-max-posts", type=int, default=int(os.getenv("AI_EA_MAX_POSTS", "12")))
|
||||
p.add_argument("--ai-content-max-chars", type=int, default=int(os.getenv("AI_EA_CONTENT_MAX_CHARS", "120000")))
|
||||
@ -518,6 +812,7 @@ def main(argv: list[str]) -> int:
|
||||
httpd.out_dir = out_dir # type: ignore[attr-defined]
|
||||
httpd.run_cmd = args.run # type: ignore[attr-defined]
|
||||
httpd.ai_enabled = bool(args.ai) # type: ignore[attr-defined]
|
||||
httpd.log_level = args.log_level # type: ignore[attr-defined]
|
||||
httpd.ai_model = args.ai_model # type: ignore[attr-defined]
|
||||
httpd.ai_max_posts = args.ai_max_posts # type: ignore[attr-defined]
|
||||
httpd.ai_content_max_chars = args.ai_content_max_chars # type: ignore[attr-defined]
|
||||
@ -527,6 +822,7 @@ def main(argv: list[str]) -> int:
|
||||
|
||||
print(f"Listening on http://{args.bind}:{args.port}/screenshot", file=sys.stderr)
|
||||
print(f"Saving screenshots to {out_dir}", file=sys.stderr)
|
||||
print(f"Log level: {args.log_level}", file=sys.stderr)
|
||||
if args.ai:
|
||||
print(f"OpenAI enabled: model={args.ai_model} max_posts={args.ai_max_posts}", file=sys.stderr)
|
||||
if args.run:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user