- 修复上次提交把 dfannals/cli.py 写成 0 字节的问题(它是唯一入口,导致管道不可运行) - 性别:解析 <caste>,人物表与写作素材带性别(Ral Fastenhatchets 实为女性) - 新增 dfannals/deslop.py:AI 味机械诊断(硬伤词/句式/标点,按千字密度报告) - 新增 dfannals/normalize.py:标点与结构清理(引号配对、重复段落与句子、模型自加的小节标记) - 新增 dfannals/expand.py 与 prompts/literary-expander.md:按年份场景分块大量扩写 - 专名防幻觉:每块附史料专名白名单,事后按段自动修复可疑专名 - episode 命令并进文笔层(骨架稿另存 .skeleton.md),新增 expand 命令做 A/B 对照 - 新增 notes/switched-threads.md 与 test_deslop / test_normalize 回归测试 - 提交前拦截「已跟踪文件为空」,防止上述事故复发
103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""把成品章节与年表推送到自建 Gitea。
|
|
|
|
- 推送凭据复用现有 ~/.ssh/id_ed25519(已验证在 Gitea 注册为 gitadmin 的 chendy-ubuntu-wsl)。
|
|
- 原始 legends 数据与存档不入库(见 .gitignore)。
|
|
- 自动直推,不设人工审核环节(按访谈决策)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from dfannals.config import GITEA_REMOTE, PROJECT_DIR, SSH_KEY
|
|
|
|
GITIGNORE = """\
|
|
# 原始数据与存档:不入库
|
|
data/
|
|
vendor/
|
|
__pycache__/
|
|
*.pyc
|
|
*.log
|
|
"""
|
|
|
|
|
|
def _git(*args: str, check: bool = True) -> subprocess.CompletedProcess:
|
|
env = {
|
|
"GIT_SSH_COMMAND": f"ssh -i {SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new",
|
|
"GIT_TERMINAL_PROMPT": "0",
|
|
}
|
|
proc = subprocess.run(
|
|
["git", *args],
|
|
cwd=PROJECT_DIR,
|
|
env={**_base_env(), **env},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False, # 下面是自定义错误处理,故意不用 check=True
|
|
)
|
|
if check and proc.returncode != 0:
|
|
raise RuntimeError(f"git {' '.join(args)} 失败:{proc.stderr.strip() or proc.stdout.strip()}")
|
|
return proc
|
|
|
|
|
|
def _base_env() -> dict:
|
|
import os
|
|
|
|
env = dict(os.environ)
|
|
env.pop("GIT_DIR", None)
|
|
return env
|
|
|
|
|
|
def ensure_repo() -> None:
|
|
"""初始化仓库、写 .gitignore、设置 remote 与提交身份。"""
|
|
if not (PROJECT_DIR / ".git").is_dir():
|
|
_git("init", "-b", "main")
|
|
(PROJECT_DIR / ".gitignore").write_text(GITIGNORE)
|
|
|
|
remotes = _git("remote").stdout.split()
|
|
if "origin" in remotes:
|
|
_git("remote", "set-url", "origin", GITEA_REMOTE)
|
|
else:
|
|
_git("remote", "add", "origin", GITEA_REMOTE)
|
|
|
|
if not _git("config", "user.name", check=False).stdout.strip():
|
|
_git("config", "user.name", "DF Annals Bot")
|
|
if not _git("config", "user.email", check=False).stdout.strip():
|
|
_git("config", "user.email", "df-annals@hajim1.art")
|
|
|
|
|
|
def _tracked_empty_sources() -> list[str]:
|
|
"""已跟踪的源码/文档里,哪些是 0 字节。"""
|
|
suffixes = (".py", ".sh", ".lua", ".md", ".toml", ".json")
|
|
out: list[str] = []
|
|
for rel in _git("ls-files").stdout.split():
|
|
if not rel.endswith(suffixes):
|
|
continue
|
|
path = PROJECT_DIR / rel
|
|
if path.is_file() and path.stat().st_size == 0:
|
|
out.append(rel)
|
|
return out
|
|
|
|
|
|
def commit_and_push(message: str, paths: list[Path] | None = None) -> str:
|
|
"""提交并推送。返回提交哈希;无改动时返回空串。
|
|
|
|
提交前硬拦空文件:实测发生过一次 cli.py 被提交为 0 字节的事故——
|
|
而 cli.py 是唯一入口,于是远端仓库里的管道完全不可运行,且失败是静默的。
|
|
这种错误不能靠人复核,必须在提交口拦住。
|
|
"""
|
|
empty = _tracked_empty_sources()
|
|
if empty:
|
|
raise RuntimeError("拒绝提交:以下已跟踪文件是空的 → " + "、".join(empty))
|
|
|
|
if paths:
|
|
for p in paths:
|
|
_git("add", "--", str(p.relative_to(PROJECT_DIR)) if p.is_absolute() else str(p))
|
|
else:
|
|
_git("add", "-A")
|
|
if not _git("status", "--porcelain").stdout.strip():
|
|
return ""
|
|
_git("commit", "-m", message)
|
|
head = _git("rev-parse", "--short", "HEAD").stdout.strip()
|
|
_git("push", "-u", "origin", "HEAD:main")
|
|
return head
|