feat: 完成配置与SQLite地基
- 新增 appconfig 配置读写、默认值、AI 参数和敏感字段拦截 - 新增 SQLite batches/accounts/tasks schema 与阶段写库函数 - 校准本地配置、DB、图片和登录态文件的 gitignore 规则 - 更新任务看板、模块合约、当前状态和进度记录
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""Application-level configuration for cmshopee.
|
||||
|
||||
This module owns `config.json`, which stores local app settings and AI role/
|
||||
generation parameters. AI provider definitions and API keys are intentionally
|
||||
kept out of this file; T-005 will own `config/ai_models.json`.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
CONFIG_PATH = "config.json"
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"chrome_path": r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
"user_data_root": "chrome_user_data_dir",
|
||||
"image_dir": "images",
|
||||
"db_path": "cmshopee.db",
|
||||
"default_debug_port": 9222,
|
||||
"debug_port_range": [9222, 9260],
|
||||
"cdp_ready_timeout": 60,
|
||||
"ai": {
|
||||
"default_text_model": "GPT-5.5 文本",
|
||||
"default_image_model": "Nano Banana 2",
|
||||
"title_concurrency": 4,
|
||||
"image_concurrency": 4,
|
||||
"retry": 2,
|
||||
"jpg_quality": 90,
|
||||
"resolution": "1k",
|
||||
"resolution_timeouts": {
|
||||
"512": 180,
|
||||
"1k": 240,
|
||||
"2k": 360,
|
||||
"4k": 600,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SECRET_FIELD_NAMES = {"api_key", "apikey", "key", "token", "password"}
|
||||
|
||||
|
||||
class ConfigError(RuntimeError):
|
||||
"""Raised when app configuration is missing or malformed."""
|
||||
|
||||
|
||||
def default_config() -> dict:
|
||||
"""Return a new copy of the default config."""
|
||||
|
||||
return copy.deepcopy(DEFAULT_CONFIG)
|
||||
|
||||
|
||||
def _deep_merge(defaults, loaded):
|
||||
if not isinstance(defaults, dict):
|
||||
return copy.deepcopy(loaded) if loaded is not None else copy.deepcopy(defaults)
|
||||
merged = copy.deepcopy(defaults)
|
||||
if not isinstance(loaded, dict):
|
||||
return merged
|
||||
for key, value in loaded.items():
|
||||
if isinstance(merged.get(key), dict) and isinstance(value, dict):
|
||||
merged[key] = _deep_merge(merged[key], value)
|
||||
else:
|
||||
merged[key] = copy.deepcopy(value)
|
||||
return merged
|
||||
|
||||
|
||||
def _assert_no_secrets(config):
|
||||
def visit(value, path):
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
lowered = str(key).lower()
|
||||
if lowered in SECRET_FIELD_NAMES or lowered.endswith("_key"):
|
||||
raise ConfigError(
|
||||
f"config.json 不允许保存敏感字段: {'.'.join(path + [str(key)])}"
|
||||
)
|
||||
visit(child, path + [str(key)])
|
||||
elif isinstance(value, list):
|
||||
for index, child in enumerate(value):
|
||||
visit(child, path + [str(index)])
|
||||
|
||||
visit(config, [])
|
||||
|
||||
|
||||
def save_config(config, path=CONFIG_PATH) -> dict:
|
||||
"""Persist config to JSON and return the normalized config."""
|
||||
|
||||
normalized = _deep_merge(DEFAULT_CONFIG, config)
|
||||
_assert_no_secrets(normalized)
|
||||
directory = os.path.dirname(os.path.abspath(path))
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(normalized, fh, ensure_ascii=False, indent=2)
|
||||
fh.write("\n")
|
||||
return normalized
|
||||
|
||||
|
||||
def load_config(path=CONFIG_PATH) -> dict:
|
||||
"""Load config, writing defaults first if the file does not exist."""
|
||||
|
||||
if not os.path.exists(path):
|
||||
return save_config(default_config(), path=path)
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
try:
|
||||
loaded = json.load(fh)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ConfigError(f"配置文件不是有效 JSON: {path}") from exc
|
||||
normalized = _deep_merge(DEFAULT_CONFIG, loaded)
|
||||
_assert_no_secrets(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def update_config(updates, path=CONFIG_PATH) -> dict:
|
||||
"""Merge updates into the persisted config."""
|
||||
|
||||
config = load_config(path)
|
||||
return save_config(_deep_merge(config, updates), path=path)
|
||||
|
||||
|
||||
def _config_or_load(config):
|
||||
return load_config() if config is None else config
|
||||
|
||||
|
||||
def chrome_path(config=None) -> str:
|
||||
return _config_or_load(config).get("chrome_path", "")
|
||||
|
||||
|
||||
def user_data_root(config=None) -> str:
|
||||
return _config_or_load(config).get("user_data_root", "chrome_user_data_dir")
|
||||
|
||||
|
||||
def image_dir(config=None) -> str:
|
||||
return _config_or_load(config).get("image_dir", "images")
|
||||
|
||||
|
||||
def db_path(config=None) -> str:
|
||||
return _config_or_load(config).get("db_path", "cmshopee.db")
|
||||
|
||||
|
||||
def default_debug_port(config=None) -> int:
|
||||
return int(_config_or_load(config).get("default_debug_port", 9222))
|
||||
|
||||
|
||||
def debug_port_range(config=None) -> tuple:
|
||||
values = _config_or_load(config).get("debug_port_range", [9222, 9260])
|
||||
if not isinstance(values, list) or len(values) != 2:
|
||||
raise ConfigError("debug_port_range 必须是 [start, end]")
|
||||
return int(values[0]), int(values[1])
|
||||
|
||||
|
||||
def cdp_ready_timeout(config=None) -> int:
|
||||
return int(_config_or_load(config).get("cdp_ready_timeout", 60))
|
||||
|
||||
|
||||
def ai_config(config=None) -> dict:
|
||||
return copy.deepcopy(_config_or_load(config).get("ai", DEFAULT_CONFIG["ai"]))
|
||||
|
||||
|
||||
def response_timeout(config=None) -> int:
|
||||
ai = ai_config(config)
|
||||
resolution = str(ai.get("resolution", DEFAULT_CONFIG["ai"]["resolution"]))
|
||||
timeouts = ai.get("resolution_timeouts", {})
|
||||
if resolution not in timeouts:
|
||||
raise ConfigError(f"未配置分辨率 {resolution} 的返回超时")
|
||||
return int(timeouts[resolution])
|
||||
@@ -0,0 +1,564 @@
|
||||
"""SQLite persistence for accounts, import batches, and tasks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from . import appconfig
|
||||
|
||||
|
||||
DEFAULT_BUSY_TIMEOUT_MS = 5000
|
||||
VALID_BATCH_FIELDS = {"source_files_json", "status", "note"}
|
||||
VALID_ACCOUNT_FIELDS = {
|
||||
"account_name",
|
||||
"region_host",
|
||||
"slug",
|
||||
"user_data_dir",
|
||||
"debug_port",
|
||||
"password",
|
||||
"note",
|
||||
"last_login_at",
|
||||
}
|
||||
PHASE_ATTEMPT_FIELDS = {
|
||||
"collect": "collect_attempts",
|
||||
"collected": "collect_attempts",
|
||||
"generate": "generate_attempts",
|
||||
"generated": "generate_attempts",
|
||||
"apply": "apply_attempts",
|
||||
"applied": "apply_attempts",
|
||||
"update": "apply_attempts",
|
||||
"updated": "apply_attempts",
|
||||
}
|
||||
|
||||
|
||||
class DbError(RuntimeError):
|
||||
"""Raised when SQLite persistence cannot complete an operation."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Batch:
|
||||
id: str
|
||||
source_files_json: str
|
||||
status: str
|
||||
note: Optional[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
@property
|
||||
def source_files(self) -> list[str]:
|
||||
return json.loads(self.source_files_json)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Account:
|
||||
id: int
|
||||
account_name: str
|
||||
alias: str
|
||||
region_host: str
|
||||
slug: str
|
||||
user_data_dir: str
|
||||
debug_port: int
|
||||
password: Optional[str]
|
||||
note: Optional[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
last_login_at: Optional[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
id: int
|
||||
batch_id: str
|
||||
source_file: str
|
||||
source_file_abs: str
|
||||
source_sheet: str
|
||||
source_row: int
|
||||
row_key: str
|
||||
account_name: Optional[str]
|
||||
alias: str
|
||||
item_id: str
|
||||
old_title: Optional[str]
|
||||
old_cover_path: Optional[str]
|
||||
new_title: Optional[str]
|
||||
new_cover_path: Optional[str]
|
||||
committed: int
|
||||
stage: str
|
||||
status: str
|
||||
last_error: Optional[str]
|
||||
collect_attempts: int
|
||||
generate_attempts: int
|
||||
apply_attempts: int
|
||||
imported_at: str
|
||||
collected_at: Optional[str]
|
||||
generated_at: Optional[str]
|
||||
applied_at: Optional[str]
|
||||
updated_at: str
|
||||
|
||||
|
||||
SCHEMA_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS batches (
|
||||
id TEXT PRIMARY KEY,
|
||||
source_files_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_name TEXT NOT NULL,
|
||||
alias TEXT UNIQUE NOT NULL,
|
||||
region_host TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
user_data_dir TEXT NOT NULL,
|
||||
debug_port INTEGER NOT NULL,
|
||||
password TEXT,
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_login_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
batch_id TEXT NOT NULL REFERENCES batches(id),
|
||||
source_file TEXT NOT NULL,
|
||||
source_file_abs TEXT NOT NULL,
|
||||
source_sheet TEXT NOT NULL,
|
||||
source_row INTEGER NOT NULL,
|
||||
row_key TEXT NOT NULL UNIQUE,
|
||||
account_name TEXT,
|
||||
alias TEXT NOT NULL,
|
||||
item_id TEXT NOT NULL,
|
||||
old_title TEXT,
|
||||
old_cover_path TEXT,
|
||||
new_title TEXT,
|
||||
new_cover_path TEXT,
|
||||
committed INTEGER NOT NULL DEFAULT 0,
|
||||
stage TEXT NOT NULL DEFAULT 'imported',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
last_error TEXT,
|
||||
collect_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
generate_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
apply_attempts INTEGER NOT NULL DEFAULT 0,
|
||||
imported_at TEXT NOT NULL,
|
||||
collected_at TEXT,
|
||||
generated_at TEXT,
|
||||
applied_at TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(batch_id, source_file_abs, source_sheet, source_row)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_batch_stage_status
|
||||
ON tasks(batch_id, stage, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_alias ON tasks(alias);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_item ON tasks(item_id);
|
||||
"""
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def _db_path(path=None) -> str:
|
||||
return path or appconfig.db_path()
|
||||
|
||||
|
||||
def connect(path=None) -> sqlite3.Connection:
|
||||
"""Open a SQLite connection with the project concurrency pragmas."""
|
||||
|
||||
db_path = _db_path(path)
|
||||
if db_path != ":memory:":
|
||||
directory = os.path.dirname(os.path.abspath(db_path))
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
conn = sqlite3.connect(db_path, timeout=DEFAULT_BUSY_TIMEOUT_MS / 1000)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute(f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}")
|
||||
conn.execute("PRAGMA synchronous=NORMAL")
|
||||
return conn
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _connection(conn=None, path=None):
|
||||
owned = conn is None
|
||||
database = connect(path) if owned else conn
|
||||
try:
|
||||
yield database
|
||||
finally:
|
||||
if owned:
|
||||
database.close()
|
||||
|
||||
|
||||
def _as_dataclass(row, cls):
|
||||
return None if row is None else cls(**dict(row))
|
||||
|
||||
|
||||
def _fetch_one(conn, sql, params, cls):
|
||||
return _as_dataclass(conn.execute(sql, params).fetchone(), cls)
|
||||
|
||||
|
||||
def _fetch_all(conn, sql, params, cls):
|
||||
return [_as_dataclass(row, cls) for row in conn.execute(sql, params).fetchall()]
|
||||
|
||||
|
||||
def _validate_fields(fields, allowed):
|
||||
unknown = sorted(set(fields) - allowed)
|
||||
if unknown:
|
||||
raise DbError(f"不支持更新字段: {', '.join(unknown)}")
|
||||
|
||||
|
||||
def _slug_from_alias(alias: str) -> str:
|
||||
base = re.sub(r"[^a-z0-9_]+", "_", alias.lower()).strip("_")
|
||||
suffix = hashlib.sha1(alias.encode("utf-8")).hexdigest()[:8]
|
||||
return f"{base}_{suffix}" if base else f"account_{suffix}"
|
||||
|
||||
|
||||
def _row_key(batch_id, source_file_abs, source_sheet, source_row) -> str:
|
||||
return f"{batch_id}:{source_file_abs}:{source_sheet}:{source_row}"
|
||||
|
||||
|
||||
def _attempt_field(phase: str) -> str:
|
||||
key = str(phase).lower()
|
||||
if key not in PHASE_ATTEMPT_FIELDS:
|
||||
raise DbError(f"未知阶段: {phase}")
|
||||
return PHASE_ATTEMPT_FIELDS[key]
|
||||
|
||||
|
||||
def init_db(path=None, conn=None) -> None:
|
||||
"""Create tables and indexes if they do not already exist."""
|
||||
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.executescript(SCHEMA_SQL)
|
||||
|
||||
|
||||
def create_batch(file_paths: Iterable[str], note=None, path=None, conn=None) -> str:
|
||||
batch_id = datetime.now().strftime("%Y%m%d_%H%M%S_") + uuid.uuid4().hex[:8]
|
||||
files = [os.path.abspath(file_path) for file_path in file_paths]
|
||||
now = _now()
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
"""
|
||||
INSERT INTO batches
|
||||
(id, source_files_json, status, note, created_at, updated_at)
|
||||
VALUES (?, ?, 'active', ?, ?, ?)
|
||||
""",
|
||||
(batch_id, json.dumps(files, ensure_ascii=False), note, now, now),
|
||||
)
|
||||
return batch_id
|
||||
|
||||
|
||||
def get_batch(batch_id, path=None, conn=None):
|
||||
with _connection(conn, path) as database:
|
||||
return _fetch_one(
|
||||
database,
|
||||
"SELECT * FROM batches WHERE id = ?",
|
||||
(batch_id,),
|
||||
Batch,
|
||||
)
|
||||
|
||||
|
||||
def list_batches(status=None, path=None, conn=None):
|
||||
sql = "SELECT * FROM batches"
|
||||
params = []
|
||||
if status is not None:
|
||||
sql += " WHERE status = ?"
|
||||
params.append(status)
|
||||
sql += " ORDER BY created_at DESC, id DESC"
|
||||
with _connection(conn, path) as database:
|
||||
return _fetch_all(database, sql, params, Batch)
|
||||
|
||||
|
||||
def update_batch(batch_id, path=None, conn=None, **fields) -> None:
|
||||
_validate_fields(fields, VALID_BATCH_FIELDS)
|
||||
if not fields:
|
||||
return
|
||||
fields["updated_at"] = _now()
|
||||
assignments = ", ".join(f"{field} = ?" for field in fields)
|
||||
params = list(fields.values()) + [batch_id]
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
f"UPDATE batches SET {assignments} WHERE id = ?",
|
||||
params,
|
||||
)
|
||||
|
||||
|
||||
def list_accounts(path=None, conn=None):
|
||||
with _connection(conn, path) as database:
|
||||
return _fetch_all(
|
||||
database,
|
||||
"SELECT * FROM accounts ORDER BY alias",
|
||||
(),
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
def get_account_by_alias(alias, path=None, conn=None):
|
||||
with _connection(conn, path) as database:
|
||||
return _fetch_one(
|
||||
database,
|
||||
"SELECT * FROM accounts WHERE alias = ?",
|
||||
(alias,),
|
||||
Account,
|
||||
)
|
||||
|
||||
|
||||
def add_account(
|
||||
account_name,
|
||||
alias,
|
||||
region_host,
|
||||
debug_port,
|
||||
password=None,
|
||||
note=None,
|
||||
slug=None,
|
||||
user_data_dir=None,
|
||||
path=None,
|
||||
conn=None,
|
||||
):
|
||||
slug = slug or _slug_from_alias(alias)
|
||||
user_data_dir = user_data_dir or os.path.join("chrome_user_data_dir", slug)
|
||||
now = _now()
|
||||
with _connection(conn, path) as database:
|
||||
try:
|
||||
with database:
|
||||
database.execute(
|
||||
"""
|
||||
INSERT INTO accounts
|
||||
(account_name, alias, region_host, slug, user_data_dir,
|
||||
debug_port, password, note, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
account_name,
|
||||
alias,
|
||||
region_host,
|
||||
slug,
|
||||
user_data_dir,
|
||||
int(debug_port),
|
||||
password,
|
||||
note,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
except sqlite3.IntegrityError as exc:
|
||||
raise DbError(f"账号别名或 slug 已存在: {alias}") from exc
|
||||
return get_account_by_alias(alias, conn=database)
|
||||
|
||||
|
||||
def update_account(alias, path=None, conn=None, **fields) -> None:
|
||||
_validate_fields(fields, VALID_ACCOUNT_FIELDS)
|
||||
if not fields:
|
||||
return
|
||||
fields["updated_at"] = _now()
|
||||
assignments = ", ".join(f"{field} = ?" for field in fields)
|
||||
params = list(fields.values()) + [alias]
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
f"UPDATE accounts SET {assignments} WHERE alias = ?",
|
||||
params,
|
||||
)
|
||||
|
||||
|
||||
def delete_account(alias, path=None, conn=None) -> None:
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute("DELETE FROM accounts WHERE alias = ?", (alias,))
|
||||
|
||||
|
||||
def insert_tasks(batch_id, rows, path=None, conn=None) -> int:
|
||||
now = _now()
|
||||
values = []
|
||||
for row in rows:
|
||||
source_file_abs = os.path.abspath(row["source_file_abs"])
|
||||
source_sheet = row["source_sheet"]
|
||||
source_row = int(row["source_row"])
|
||||
values.append(
|
||||
(
|
||||
batch_id,
|
||||
row.get("source_file") or source_file_abs,
|
||||
source_file_abs,
|
||||
source_sheet,
|
||||
source_row,
|
||||
row.get("row_key") or _row_key(batch_id, source_file_abs, source_sheet, source_row),
|
||||
row.get("account_name"),
|
||||
row["alias"],
|
||||
str(row["item_id"]),
|
||||
now,
|
||||
now,
|
||||
)
|
||||
)
|
||||
with _connection(conn, path) as database:
|
||||
try:
|
||||
with database:
|
||||
database.executemany(
|
||||
"""
|
||||
INSERT INTO tasks
|
||||
(batch_id, source_file, source_file_abs, source_sheet,
|
||||
source_row, row_key, account_name, alias, item_id,
|
||||
imported_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values,
|
||||
)
|
||||
except sqlite3.IntegrityError as exc:
|
||||
raise DbError("任务行重复或批次不存在") from exc
|
||||
return len(values)
|
||||
|
||||
|
||||
def list_tasks(batch_id=None, stage=None, status=None, alias=None, path=None, conn=None):
|
||||
clauses = []
|
||||
params = []
|
||||
filters = {
|
||||
"batch_id": batch_id,
|
||||
"stage": stage,
|
||||
"status": status,
|
||||
"alias": alias,
|
||||
}
|
||||
for field, value in filters.items():
|
||||
if value is not None:
|
||||
clauses.append(f"{field} = ?")
|
||||
params.append(value)
|
||||
sql = "SELECT * FROM tasks"
|
||||
if clauses:
|
||||
sql += " WHERE " + " AND ".join(clauses)
|
||||
sql += " ORDER BY id"
|
||||
with _connection(conn, path) as database:
|
||||
return _fetch_all(database, sql, params, Task)
|
||||
|
||||
|
||||
def mark_running(task_id, phase, path=None, conn=None) -> None:
|
||||
_attempt_field(phase)
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET status = 'running', last_error = NULL, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(_now(), int(task_id)),
|
||||
)
|
||||
|
||||
|
||||
def mark_failed(task_id, phase, error, path=None, conn=None) -> None:
|
||||
attempt_field = _attempt_field(phase)
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
f"""
|
||||
UPDATE tasks
|
||||
SET status = 'failed',
|
||||
last_error = ?,
|
||||
{attempt_field} = {attempt_field} + 1,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(str(error), _now(), int(task_id)),
|
||||
)
|
||||
|
||||
|
||||
def mark_skipped(task_id, reason, path=None, conn=None) -> None:
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET status = 'skipped', last_error = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(str(reason), _now(), int(task_id)),
|
||||
)
|
||||
|
||||
|
||||
def set_collected(task_id, old_title, old_cover_path, path=None, conn=None) -> None:
|
||||
now = _now()
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET old_title = ?,
|
||||
old_cover_path = ?,
|
||||
stage = 'collected',
|
||||
status = 'success',
|
||||
last_error = NULL,
|
||||
collect_attempts = collect_attempts + 1,
|
||||
collected_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(old_title, old_cover_path, now, now, int(task_id)),
|
||||
)
|
||||
|
||||
|
||||
def set_generated(task_id, new_title, new_cover_path, path=None, conn=None) -> None:
|
||||
now = _now()
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET new_title = ?,
|
||||
new_cover_path = ?,
|
||||
stage = 'generated',
|
||||
status = 'success',
|
||||
last_error = NULL,
|
||||
generate_attempts = generate_attempts + 1,
|
||||
generated_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(new_title, new_cover_path, now, now, int(task_id)),
|
||||
)
|
||||
|
||||
|
||||
def set_applied(task_id, committed, error=None, path=None, conn=None) -> None:
|
||||
now = _now()
|
||||
success = bool(committed) and error is None
|
||||
with _connection(conn, path) as database:
|
||||
with database:
|
||||
if success:
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET committed = 1,
|
||||
stage = 'applied',
|
||||
status = 'success',
|
||||
last_error = NULL,
|
||||
apply_attempts = apply_attempts + 1,
|
||||
applied_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(now, now, int(task_id)),
|
||||
)
|
||||
else:
|
||||
database.execute(
|
||||
"""
|
||||
UPDATE tasks
|
||||
SET committed = 0,
|
||||
status = 'failed',
|
||||
last_error = ?,
|
||||
apply_attempts = apply_attempts + 1,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(str(error or "未提交更新"), now, int(task_id)),
|
||||
)
|
||||
Reference in New Issue
Block a user