Initial commit: snapAna 截图智能整理工具
包含 FastAPI 后端、React 前端、队列/OCR/标签/待办等完整功能。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""SQLAlchemy 模型集中注册入口。"""
|
||||
|
||||
|
||||
def register_all() -> None:
|
||||
"""显式导入以触发模型注册到 Base.metadata。"""
|
||||
from . import screenshot # noqa: F401
|
||||
from . import meta # noqa: F401
|
||||
from . import tag # noqa: F401
|
||||
from . import category # noqa: F401
|
||||
from . import todo # noqa: F401
|
||||
from . import job # noqa: F401
|
||||
from . import watch_folder # noqa: F401
|
||||
from . import setting # noqa: F401
|
||||
@@ -0,0 +1,32 @@
|
||||
"""截图分类。预置常见类目,AI 命中即可写回。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class Category(Base):
|
||||
"""截图分类。"""
|
||||
|
||||
__tablename__ = "categories"
|
||||
__table_args__ = (UniqueConstraint("name", name="uq_categories_name"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
color: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
prompt_hint: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
|
||||
# 首次启动时灌入的默认分类
|
||||
DEFAULT_CATEGORIES: list[dict[str, str | None]] = [
|
||||
{"name": "知识技术", "color": "#3b82f6", "prompt_hint": "技术文章、代码、教程、文档截图"},
|
||||
{"name": "梗图幽默", "color": "#f59e0b", "prompt_hint": "搞笑图、表情包、梗图"},
|
||||
{"name": "小说文字", "color": "#8b5cf6", "prompt_hint": "长段文字、小说阅读、电子书"},
|
||||
{"name": "聊天记录", "color": "#10b981", "prompt_hint": "微信/QQ/Slack 等聊天截图"},
|
||||
{"name": "UI 设计", "color": "#ec4899", "prompt_hint": "界面设计、网页/App 灵感参考"},
|
||||
{"name": "生活记录", "color": "#22c55e", "prompt_hint": "日常照片、生活记录、票据"},
|
||||
{"name": "购物商品", "color": "#ef4444", "prompt_hint": "商品截图、价格、订单"},
|
||||
{"name": "其他", "color": "#6b7280", "prompt_hint": "无法明确归类"},
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""分析任务队列:持久化到 SQLite,断电可恢复。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class JobKind(str, Enum):
|
||||
"""任务种类。"""
|
||||
|
||||
OCR = "ocr"
|
||||
VLM = "vlm"
|
||||
FULL = "full" # OCR + VLM 一条龙
|
||||
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
"""任务运行状态。"""
|
||||
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
DONE = "done"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class Job(Base):
|
||||
"""单条分析任务记录。"""
|
||||
|
||||
__tablename__ = "jobs"
|
||||
__table_args__ = (
|
||||
Index("ix_jobs_status", "status"),
|
||||
Index("ix_jobs_kind_status", "kind", "status"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
screenshot_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("screenshots.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
kind: Mapped[str] = mapped_column(String(16), default=JobKind.FULL.value)
|
||||
status: Mapped[str] = mapped_column(String(16), default=JobStatus.PENDING.value)
|
||||
retries: Mapped[int] = mapped_column(Integer, default=0)
|
||||
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), nullable=False
|
||||
)
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
@@ -0,0 +1,27 @@
|
||||
"""截图的 OCR / AI 元信息。与 screenshot 1:1。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class ScreenshotMeta(Base):
|
||||
"""OCR 文本 + AI 结构化结果。"""
|
||||
|
||||
__tablename__ = "screenshot_meta"
|
||||
|
||||
screenshot_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("screenshots.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
|
||||
ocr_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_title: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_suggestion: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
ai_raw_json: Mapped[str | None] = mapped_column(Text, nullable=True) # 完整原始 JSON
|
||||
|
||||
screenshot = relationship("Screenshot", back_populates="meta")
|
||||
@@ -0,0 +1,86 @@
|
||||
"""截图主表与处理状态。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class ProcessStatus(str, Enum):
|
||||
"""处理流水线的状态枚举。"""
|
||||
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
DONE = "done"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
class Screenshot(Base):
|
||||
"""截图文件主记录。"""
|
||||
|
||||
__tablename__ = "screenshots"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("file_hash", name="uq_screenshots_file_hash"),
|
||||
Index("ix_screenshots_captured_at", "captured_at"),
|
||||
Index("ix_screenshots_ai_status", "ai_status"),
|
||||
Index("ix_screenshots_category_id", "category_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
path: Mapped[str] = mapped_column(String(1024), nullable=False)
|
||||
file_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
width: Mapped[int] = mapped_column(Integer, default=0)
|
||||
height: Mapped[int] = mapped_column(Integer, default=0)
|
||||
size: Mapped[int] = mapped_column(BigInteger, default=0)
|
||||
|
||||
captured_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
||||
imported_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), nullable=False
|
||||
)
|
||||
|
||||
thumb_path: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
|
||||
ocr_status: Mapped[str] = mapped_column(String(16), default=ProcessStatus.PENDING.value)
|
||||
ai_status: Mapped[str] = mapped_column(String(16), default=ProcessStatus.PENDING.value)
|
||||
|
||||
# AI 写回的分类:外键 + SET NULL,删除分类时自动把引用置空
|
||||
category_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("categories.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
is_favorite: Mapped[int] = mapped_column(Integer, default=0) # 0/1,便于 SQLite 索引
|
||||
is_hidden: Mapped[int] = mapped_column(Integer, default=0)
|
||||
|
||||
meta = relationship(
|
||||
"ScreenshotMeta",
|
||||
back_populates="screenshot",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
tags = relationship(
|
||||
"Tag",
|
||||
secondary="screenshot_tags",
|
||||
back_populates="screenshots",
|
||||
lazy="selectin",
|
||||
)
|
||||
todos = relationship(
|
||||
"Todo",
|
||||
back_populates="screenshot",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""键值设置:Provider 配置等以 JSON 形式存储。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class Setting(Base):
|
||||
"""通用键值设置。"""
|
||||
|
||||
__tablename__ = "settings"
|
||||
|
||||
key: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
value_json: Mapped[str] = mapped_column(Text, nullable=False, default="null")
|
||||
|
||||
|
||||
# 设置键名常量
|
||||
KEY_OCR_PROVIDER = "ocr_provider"
|
||||
KEY_VLM_PROVIDER = "vlm_provider"
|
||||
KEY_RECOGNITION_MODE = "recognition_mode" # ocr | vision | hybrid
|
||||
KEY_CATEGORY_HINT = "category_hint"
|
||||
|
||||
# 默认识别模式:混合(OCR 文本 + 视觉 AI 联合分析)
|
||||
DEFAULT_RECOGNITION_MODE = "hybrid"
|
||||
@@ -0,0 +1,42 @@
|
||||
"""标签与多对多关联。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Table, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
screenshot_tags = Table(
|
||||
"screenshot_tags",
|
||||
Base.metadata,
|
||||
Column(
|
||||
"screenshot_id",
|
||||
Integer,
|
||||
ForeignKey("screenshots.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
Column(
|
||||
"tag_id",
|
||||
Integer,
|
||||
ForeignKey("tags.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Tag(Base):
|
||||
"""用户/AI 共享的自由标签。"""
|
||||
|
||||
__tablename__ = "tags"
|
||||
__table_args__ = (UniqueConstraint("name", name="uq_tags_name"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
color: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
|
||||
screenshots = relationship(
|
||||
"Screenshot",
|
||||
secondary=screenshot_tags,
|
||||
back_populates="tags",
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""AI 抽取的待办(待看/待读/待办)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class TodoStatus(str, Enum):
|
||||
"""待办状态。"""
|
||||
|
||||
PENDING = "pending"
|
||||
DOING = "doing"
|
||||
DONE = "done"
|
||||
DROPPED = "dropped"
|
||||
|
||||
|
||||
class Todo(Base):
|
||||
"""AI 从截图中抽取的待办项。"""
|
||||
|
||||
__tablename__ = "todos"
|
||||
__table_args__ = (
|
||||
Index("ix_todos_status", "status"),
|
||||
Index("ix_todos_screenshot_id", "screenshot_id"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
screenshot_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("screenshots.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
kind: Mapped[str | None] = mapped_column(String(32), nullable=True) # 待看/待读/待办等
|
||||
status: Mapped[str] = mapped_column(String(16), default=TodoStatus.PENDING.value)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), nullable=False
|
||||
)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
|
||||
screenshot = relationship("Screenshot", back_populates="todos")
|
||||
@@ -0,0 +1,25 @@
|
||||
"""被监听的截图目录列表。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, UniqueConstraint, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base
|
||||
|
||||
|
||||
class WatchFolder(Base):
|
||||
"""监听的截图目录。"""
|
||||
|
||||
__tablename__ = "watch_folders"
|
||||
__table_args__ = (UniqueConstraint("path", name="uq_watch_folders_path"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
path: Mapped[str] = mapped_column(String(1024), nullable=False)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
recursive: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_sensitive: Mapped[bool] = mapped_column(Boolean, default=False) # 是否禁止上传云端
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), nullable=False
|
||||
)
|
||||
Reference in New Issue
Block a user