Spaces:
Sleeping
Sleeping
Download get_notes/web.py from yuziyang/brew-profile-parser: direct link, hf CLI and curl.
- Browser
- Download file 7.02 kB
-
https://huggingface.co/spaces/yuziyang/brew-profile-parser/resolve/main/get_notes/web.py
- Command line
-
hf download hf://spaces/yuziyang/brew-profile-parser/get_notes/web.py
-
curl -L -o web.py https://huggingface.co/spaces/yuziyang/brew-profile-parser/resolve/main/get_notes/web.py
7.02 kB
| """ | |
| Web 服务:为 Get笔记 提供 API 和前端界面。 | |
| 使用 FastAPI + SSE 实现实时进度推送。 | |
| 启动方式: python -m get_notes.web | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import logging | |
| import queue | |
| import threading | |
| import time | |
| import uuid | |
| from pathlib import Path | |
| from typing import Optional | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import HTMLResponse, StreamingResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from get_notes.app import GetNotesApp, _load_dotenv | |
| from get_notes.config import AppConfig | |
| _load_dotenv() | |
| app = FastAPI(title="Get笔记", version="0.1.0") | |
| STATIC_DIR = Path(__file__).parent / "static" | |
| STATIC_DIR.mkdir(exist_ok=True) | |
| notes_app: Optional[GetNotesApp] = None | |
| def get_notes_app() -> GetNotesApp: | |
| global notes_app | |
| if notes_app is None: | |
| notes_app = GetNotesApp() | |
| return notes_app | |
| class ProgressHandler(logging.Handler): | |
| """Captures log messages and forwards them to a queue as SSE events.""" | |
| def __init__(self, q: queue.Queue): | |
| super().__init__() | |
| self.q = q | |
| def emit(self, record: logging.LogRecord): | |
| msg = self.format(record) | |
| step = None | |
| if "Step 1" in msg or "解析链接" in msg: | |
| step = "parse" | |
| elif "Step 2" in msg or "处理内容" in msg: | |
| step = "process" | |
| elif "Step 3" in msg or "聚合" in msg: | |
| step = "aggregate" | |
| elif "Step 4b" in msg or "AI补全" in msg: | |
| step = "infer" | |
| elif "Step 4" in msg or "AI生成" in msg or "AI总结" in msg or "AI精准提取" in msg: | |
| step = "summarize" | |
| elif "笔记生成完成" in msg or "笔记已保存" in msg: | |
| step = "done" | |
| if step: | |
| self.q.put({"type": "progress", "step": step, "message": msg}) | |
| tasks: dict[str, dict] = {} | |
| def _run_task(task_id: str, link: str, instruction: Optional[str], model: Optional[str] = None): | |
| """Run the processing pipeline in a background thread.""" | |
| q = tasks[task_id]["queue"] | |
| handler = ProgressHandler(q) | |
| handler.setLevel(logging.INFO) | |
| root_logger = logging.getLogger("get_notes") | |
| root_logger.addHandler(handler) | |
| root_logger.setLevel(logging.INFO) | |
| try: | |
| q.put({"type": "progress", "step": "start", "message": "开始处理..."}) | |
| app_instance = get_notes_app() | |
| if model: | |
| app_instance.config.llm.model = model | |
| card = app_instance.process_link(link, instruction) | |
| from dataclasses import asdict | |
| result = asdict(card) | |
| result.pop("raw_content", None) | |
| q.put({"type": "result", "data": result}) | |
| except Exception as e: | |
| q.put({"type": "error", "message": str(e)}) | |
| finally: | |
| root_logger.removeHandler(handler) | |
| q.put({"type": "end"}) | |
| async def process_link(request: Request): | |
| body = await request.json() | |
| link = body.get("link", "").strip() | |
| instruction = body.get("instruction", "").strip() or None | |
| model = body.get("model", "").strip() or None | |
| if not link: | |
| return {"error": "请提供链接"} | |
| task_id = uuid.uuid4().hex[:12] | |
| tasks[task_id] = {"queue": queue.Queue(), "created": time.time()} | |
| thread = threading.Thread( | |
| target=_run_task, args=(task_id, link, instruction, model), daemon=True | |
| ) | |
| thread.start() | |
| return {"task_id": task_id} | |
| async def stream_progress(task_id: str): | |
| if task_id not in tasks: | |
| return {"error": "任务不存在"} | |
| q = tasks[task_id]["queue"] | |
| async def event_generator(): | |
| idle_count = 0 | |
| while True: | |
| try: | |
| event = q.get_nowait() | |
| idle_count = 0 | |
| except queue.Empty: | |
| idle_count += 1 | |
| if idle_count % 10 == 0: | |
| yield ": heartbeat\n\n" | |
| await asyncio.sleep(0.5) | |
| continue | |
| yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n" | |
| if event.get("type") in ("end", "error"): | |
| break | |
| tasks.pop(task_id, None) | |
| return StreamingResponse( | |
| event_generator(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| }, | |
| ) | |
| ENV_PATH = Path(__file__).resolve().parent.parent / ".env" | |
| def _try_write_env(key_map: dict[str, str]): | |
| """Best-effort write to .env; silently skip if read-only filesystem.""" | |
| try: | |
| lines = [] | |
| if ENV_PATH.exists(): | |
| lines = ENV_PATH.read_text(encoding="utf-8").splitlines() | |
| found_keys: set[str] = set() | |
| new_lines = [] | |
| for line in lines: | |
| updated = False | |
| for k, v in key_map.items(): | |
| if line.startswith(f"{k}=") or line.startswith(f"{k} ="): | |
| new_lines.append(f"{k}={v}") | |
| found_keys.add(k) | |
| updated = True | |
| break | |
| if not updated: | |
| new_lines.append(line) | |
| for k, v in key_map.items(): | |
| if k not in found_keys: | |
| new_lines.append(f"{k}={v}") | |
| ENV_PATH.write_text("\n".join(new_lines) + "\n", encoding="utf-8") | |
| except OSError: | |
| pass | |
| async def get_settings(): | |
| """返回当前配置(API Key 脱敏显示)""" | |
| app_inst = get_notes_app() | |
| key = app_inst.config.llm.api_key or "" | |
| masked = key[:6] + "***" + key[-4:] if len(key) > 10 else ("*" * len(key) if key else "") | |
| return { | |
| "api_key_masked": masked, | |
| "api_key_set": bool(key and "填入" not in key), | |
| "base_url": app_inst.config.llm.base_url, | |
| "model": app_inst.config.llm.model, | |
| } | |
| async def save_settings(request: Request): | |
| """保存 API Key 并热更新运行时配置""" | |
| global notes_app | |
| body = await request.json() | |
| api_key = body.get("api_key", "").strip() | |
| base_url = body.get("base_url", "").strip() | |
| if not api_key: | |
| return {"error": "请填写 API Key"} | |
| if not base_url: | |
| base_url = "https://api.jiekou.ai/openai/v1" | |
| _try_write_env({"LLM_API_KEY": api_key, "LLM_BASE_URL": base_url}) | |
| app_inst = get_notes_app() | |
| app_inst.config.llm.api_key = api_key | |
| app_inst.config.llm.base_url = base_url | |
| return {"ok": True} | |
| async def index(): | |
| html_path = STATIC_DIR / "index.html" | |
| return HTMLResponse(html_path.read_text(encoding="utf-8")) | |
| async def index_retro(): | |
| html_path = STATIC_DIR / "index-retro.html" | |
| return HTMLResponse(html_path.read_text(encoding="utf-8")) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("get_notes.web:app", host="0.0.0.0", port=8000, reload=True) | |