项目骨架:管道源码、写作规范与说明文档
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
# 原始数据与存档:不入库
|
||||||
|
data/
|
||||||
|
vendor/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.log
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
# Grill Me Results
|
||||||
|
|
||||||
|
Generated: 2026-10-05T07:11:32.355Z
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
|
||||||
|
给我一个手机端个人助手的系统提示词
|
||||||
|
|
||||||
|
## Shared Understanding
|
||||||
|
|
||||||
|
目标:把矮人要塞(Dwarf Fortress)部署到本地(Windows 侧),并让一条自动化管道持续把游戏世界的历史写成中文连载故事,自动推送到自建 Gitea 公开仓库。
|
||||||
|
|
||||||
|
核心取舍:不做 AI 自动游玩(B),只做「世界史编年史」(A)——通过 worldgen 生成世界 + DFHack 导出 legends 历史数据,再由云端 LLM 把结构化历史写成战报体连载。原因:本机无独显(13G 内存 / Radeon 780M),跑不动本地模型;而 AI 操控游戏极其脆弱,被列为第二阶段目标。
|
||||||
|
|
||||||
|
三条链路:
|
||||||
|
1. Windows 侧(/mnt/c/Users/23518):DF 53.16 免费版 + DFHack 53.16-r2 → 生成 Medium 世界 / 250 年历史 → 导出 legends 数据。
|
||||||
|
2. WSL2 Ubuntu 侧:Python 管道解析 legends → 建年表与人物索引 → 按事件链切片(每章 800–1500 字)→ 组 prompt(要塞编年史官口吻、战报体、自由演绎)→ 调 workbuddy 网关(cn:deepseek-v4-pro)→ 产出 Markdown 章 → 专名事实校验并标注可疑处。
|
||||||
|
3. Gitea(124.222.29.26:3000,SSH 2222):复用现有 ~/.ssh/id_ed25519 自动 commit + push 到公开仓库,仓库含成品 Markdown + 迷你年表/人物索引。
|
||||||
|
|
||||||
|
关键事实(已实测,无需再确认):无任何 API key 环境变量;Pi 的 provider 是用户自建网关 workbuddy(https://wb2api.hajim1.art/v1,39 个模型,/v1/models → 401 表示在线);openai.com 超时,DeepSeek/Moonshot/SiliconFlow 可达;Gitea 返回 200 且 SSH 2222 开放,现有 id_ed25519 已注册为 gitadmin 的 chendy-ubuntu-wsl;Bay 12 当前免费 Windows 包为 df_53_16_win.zip;DFHack 最新为 53.16-r2(有 Windows 官方包);Windows Steam 已装但库中没有矮人要塞。
|
||||||
|
|
||||||
|
## Questions and Answers
|
||||||
|
|
||||||
|
### 1. 矮人要塞本体跑在哪一侧?
|
||||||
|
|
||||||
|
**Recommended answer:** Windows 侧跑 DF + WSL 侧做自动化(已有 Steam、原生 GPU 性能好;WSL 当故事工厂,经 /mnt/c 读存档)
|
||||||
|
|
||||||
|
**User answer:** Windows 跑 DF,WSL 跑自动化
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 已确认 Windows 有 Steam(但库里无 DF)、WSLg 可用;本机无独显,故写作环节必须离开本机推理。
|
||||||
|
|
||||||
|
### 2. 用哪个版本、怎么拿到?
|
||||||
|
|
||||||
|
**Recommended answer:** Bay 12 免费 classic v50(免费且 DFHack 主线支持)
|
||||||
|
|
||||||
|
**User answer:** Bay 12 免费 classic v50
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** Windows 版免费 classic v50 + Windows 版 DFHack;无需购买 Steam 版。
|
||||||
|
|
||||||
|
### 3. 『不断产出故事』具体指哪种?
|
||||||
|
|
||||||
|
**Recommended answer:** A 世界史编年史 + C 人玩机录(先保证持续有内容,B 当第二阶段)
|
||||||
|
|
||||||
|
**User answer:** A 世界史编年史
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 只做 worldgen/legends 导出叙事,不做 AI 自动游玩。与 continuity 的『同一世界长期连载』结合后,产生一个待澄清矛盾:历史是有限的,写完后如何继续产出。
|
||||||
|
|
||||||
|
### 4. 产出节奏和章节长度?
|
||||||
|
|
||||||
|
**Recommended answer:** 事件驱动,每章 800–1500 字
|
||||||
|
|
||||||
|
**User answer:** 事件驱动,每章 800–1500 字
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 5. 谁来把游戏数据写成故事?
|
||||||
|
|
||||||
|
**Recommended answer:** 云端 LLM API(DeepSeek 便宜且本网络可达;本机无独显跑不动本地模型)
|
||||||
|
|
||||||
|
**User answer:** 云端 LLM API
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 实测:DeepSeek/Moonshot/SiliconFlow 可达,openai.com 超时。
|
||||||
|
|
||||||
|
### 6. 若走云端 LLM,每月成本上限?
|
||||||
|
|
||||||
|
**Recommended answer:** 20 元以内 / 月
|
||||||
|
|
||||||
|
**User answer:** 不限,能跑通就行
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 成本不再是约束;模型选择自由度放宽,可用长上下文模型喂完整 legends 数据。
|
||||||
|
|
||||||
|
### 7. 故事最终发布到哪?
|
||||||
|
|
||||||
|
**Recommended answer:** 先只落本地 git 仓库(Markdown),博客/Gitea 之后接
|
||||||
|
|
||||||
|
**User answer:** 推到自建 Gitea(124.222.29.26)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 未选本地 git 仓库与博客,仅 Gitea;需澄清仓库名、可见性与推送认证(SSH 2222 端口)。
|
||||||
|
|
||||||
|
### 8. 世界观怎么延续?
|
||||||
|
|
||||||
|
**Recommended answer:** 同一世界长期连载(人物家族仇恨跨章延续)
|
||||||
|
|
||||||
|
**User answer:** 同一世界长期连载
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与『只做 A』冲突:legends 历史有限,写完即断供,需第二轮定完结策略。
|
||||||
|
|
||||||
|
### 9. 什么时候跑?
|
||||||
|
|
||||||
|
**Recommended answer:** 先手动一条命令,稳定后再加 cron
|
||||||
|
|
||||||
|
**User answer:** 你帮我跑(自定义答案,未选任何选项)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 含义待澄清:可能是『每次我说一声你就跑』或『你替我配好自动化并代跑』。真实约束:agent 无法在会话外自主持续运行,定时执行必须落在 WSL cron 或 Windows 任务计划上。
|
||||||
|
|
||||||
|
### 10. 允许我装系统依赖吗?
|
||||||
|
|
||||||
|
**Recommended answer:** 可以,sudo apt 随便装
|
||||||
|
|
||||||
|
**User answer:** 可以,sudo apt 随便装
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** WSL 内可装 xvfb、CJK 字体、python 库;Windows 侧 DF+DFHack 仅需解压,不需管理员权限。
|
||||||
|
|
||||||
|
### 11. 这次做到哪一步?
|
||||||
|
|
||||||
|
**Recommended answer:** 最小 demo:装好 + 跑出 1 章故事
|
||||||
|
|
||||||
|
**User answer:** MVP + 定时自动化 + 已发布到某处
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 范围比推荐更大,需与 trigger 的澄清一起确定自动化落点。
|
||||||
|
|
||||||
|
### 12. 故事要不要配图?
|
||||||
|
|
||||||
|
**Recommended answer:** 先不要,先把文字闭环跑通
|
||||||
|
|
||||||
|
**User answer:** 纯文字
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 13. 输出语言?
|
||||||
|
|
||||||
|
**Recommended answer:** 中文,专名保留英文
|
||||||
|
|
||||||
|
**User answer:** 中文(专名保英文,如 Urist、Boatmurdered)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 14. 故事引擎要不要搬到服务器 124.222.29.26 常驻?
|
||||||
|
|
||||||
|
**Recommended answer:** 先本地,之后考虑搬服务器
|
||||||
|
|
||||||
|
**User answer:** 先本地,之后考虑搬服务器
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 15. 这个「手机端个人助手」是 softwork1 项目里的助手,还是一个全新的独立助手?
|
||||||
|
|
||||||
|
**Recommended answer:** 与 softwork1 项目无关的独立助手
|
||||||
|
|
||||||
|
**User answer:** 与 softwork1 无关:只是我独立手机本地的助手
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 推翻了第一轮基于 softwork1/sw1-p3 代码事实建立的全部前提(daemon、appendSystemPrompt、memory/rules 注入等都不再适用)。
|
||||||
|
|
||||||
|
### 16. 提示词主要给哪类手机助手用?(推荐:云端大模型 App——能力完整、支持自定义系统提示词,兼容性最好)
|
||||||
|
|
||||||
|
**Recommended answer:** 云端大模型 App(ChatGPT/Claude/Gemini 等,能填系统提示词)
|
||||||
|
|
||||||
|
**User answer:** 云端大模型 App(ChatGPT/Claude/Gemini 等,能填系统提示词)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 17. 平台范围?(推荐:通用——只写与 iOS/Android 无关的行为规则,以后换 App 也能用)
|
||||||
|
|
||||||
|
**Recommended answer:** 通用,不绑定平台
|
||||||
|
|
||||||
|
**User answer:** 只 Android
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐不同:仅面向 Android。
|
||||||
|
|
||||||
|
### 18. 这个助手有没有「干活」的能力(联网搜索、日历、发消息等)?(推荐:按无工具写——大多数手机 App 只有对话,宁可让它老实说做不到,也不要假装已执行)
|
||||||
|
|
||||||
|
**Recommended answer:** 没有工具,只有纯对话
|
||||||
|
|
||||||
|
**User answer:** 有工具调用(查网页、日历、笔记、发消息)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐不同:助手确实有工具,提示词应包含工具使用规则而不是保守回避。
|
||||||
|
|
||||||
|
### 19. 你主要用它干什么?可多选(推荐:信息整理 + 写作润色 + 日程待办)
|
||||||
|
|
||||||
|
**Recommended answer:** 信息整理与总结、写作与润色、日程待办与计划安排
|
||||||
|
|
||||||
|
**User answer:** 信息整理与总结(长文、链接、聊天记录);答疑与学习(技术问题、概念解释);随手记与灵感整理
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐部分不同:不含写作润色与日程待办,含答疑学习与随手记。
|
||||||
|
|
||||||
|
### 20. 人格定位?(推荐:干练私人助理)
|
||||||
|
|
||||||
|
**Recommended answer:** 干练私人助理(简洁、先结论后细节)
|
||||||
|
|
||||||
|
**User answer:** 干练私人助理(简洁、先结论后细节)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 21. 语言与称呼?(推荐:中文、称「Chen Yi」)
|
||||||
|
|
||||||
|
**Recommended answer:** 中文,称呼 Chen Yi
|
||||||
|
|
||||||
|
**User answer:** 中文,称呼 Chen Yi
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 22. 单条回复默认多长?(推荐:3–5 行结论 + 要点)
|
||||||
|
|
||||||
|
**Recommended answer:** 3–5 行结论 + 要点
|
||||||
|
|
||||||
|
**User answer:** 不限长度,完整回答
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐不同,且与「干练私人助理(简洁)」存在张力,已作为第三轮追问点。
|
||||||
|
|
||||||
|
### 23. 长回答要不要固定结构?(推荐:结论 / 依据 / 下一步 三段)
|
||||||
|
|
||||||
|
**Recommended answer:** 固定三段:结论 / 依据 / 下一步
|
||||||
|
|
||||||
|
**User answer:** 不固定(自定义填写,未选任何预设项)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 24. 信息不足时怎么办?(推荐:先问一个最关键的问题)
|
||||||
|
|
||||||
|
**Recommended answer:** 先问一个最关键的问题,等回答
|
||||||
|
|
||||||
|
**User answer:** 直接给 2–3 个方案让我选
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐不同,且与「末尾不给下一步建议」存在边界模糊,已作为第三轮追问点。
|
||||||
|
|
||||||
|
### 25. 回答末尾要不要带「下一步建议」?(推荐:最多一条)
|
||||||
|
|
||||||
|
**Recommended answer:** 最多给一条下一步建议
|
||||||
|
|
||||||
|
**User answer:** 不给建议,答完即止
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与「信息不足时给 2–3 个方案」有边界冲突,第三轮澄清。
|
||||||
|
|
||||||
|
### 26. 语气约束?(推荐:禁 emoji、禁客套套话)
|
||||||
|
|
||||||
|
**Recommended answer:** 禁 emoji、禁客套套话,直接给结论
|
||||||
|
|
||||||
|
**User answer:** 允许幽默和闲聊
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐差异较大,且与「干练私人助理」人格、用途清单(未选闲聊)有点张力,第三轮问幽默程度。
|
||||||
|
|
||||||
|
### 27. 要不要明确「不许装能力」?(推荐:要)
|
||||||
|
|
||||||
|
**Recommended answer:** 明确写:不假装执行任何手机操作或查询,做不到就直说
|
||||||
|
|
||||||
|
**User answer:** 只写「不确定就说明」,不专门限制
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与「有工具调用」搭配合理,但与推荐不同;有工具时假装风险较低。
|
||||||
|
|
||||||
|
### 28. 隐私口径?(推荐:只用你主动给的信息,不索要敏感信息,不假装记得历史对话)
|
||||||
|
|
||||||
|
**Recommended answer:** 只用对话里主动给的信息,不索要敏感信息,不假装记得历史对话
|
||||||
|
|
||||||
|
**User answer:** 需要时可以直接追问个人信息
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐不同;因另选了「App 有跨会话记忆」,持久化敏感信息的分寸需在第三轮定。
|
||||||
|
|
||||||
|
### 29. 手机 App 一般没有跨会话记忆,怎么处理?(推荐:长任务结尾给一段可复制的上下文摘要)
|
||||||
|
|
||||||
|
**Recommended answer:** 长任务结尾给一段可复制的「上下文摘要」
|
||||||
|
|
||||||
|
**User answer:** 这个有(自定义填写:App 有跨会话记忆)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 推翻推荐前提:有持久记忆,摘要方案不再必要;改为追问记忆写入机制。
|
||||||
|
|
||||||
|
### 30. 讲解深度?(推荐:按计算机专业学生,直接讲机制与结论)
|
||||||
|
|
||||||
|
**Recommended answer:** 按技术用户,直接讲机制/结论,不科普
|
||||||
|
|
||||||
|
**User answer:** 适度解释术语和取舍
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 31. 提示词本身体量?(推荐:≤1 页 400–800 字)
|
||||||
|
|
||||||
|
**Recommended answer:** 精炼,≤1 页(约 400–800 字)
|
||||||
|
|
||||||
|
**User answer:** 精炼,≤1 页(约 400–800 字)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 32. 要不要在提示词里放示例?(推荐:放 1–2 个短范例)
|
||||||
|
|
||||||
|
**Recommended answer:** 放 1–2 个短范例
|
||||||
|
|
||||||
|
**User answer:** 只写规则,不放例子
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐不同:不带范例,提示词更短。
|
||||||
|
|
||||||
|
### 33. 交付验收?(推荐:落一份 md + 聊天贴全文)
|
||||||
|
|
||||||
|
**Recommended answer:** md 文件落盘 + 聊天贴全文
|
||||||
|
|
||||||
|
**User answer:** md 文件落盘 + 聊天贴全文
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 34. 世界史是有限的历史——同一个世界写到史末之后怎么办?
|
||||||
|
|
||||||
|
**Recommended answer:** 按卷连载,写完宣布该世界史完结,再用同一叙述者人设开新世界新一卷
|
||||||
|
|
||||||
|
**User answer:** 一部史分卷连载,完结后换新世界开新卷
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 第二轮把上一轮『只做 A + 长期连载』的断供矛盾解开了:一个世界分卷连载,完结后换新世界开新卷。
|
||||||
|
|
||||||
|
### 35. 『你帮我跑』具体怎么落地?
|
||||||
|
|
||||||
|
**Recommended answer:** 先手动跑通,稳定后再开定时
|
||||||
|
|
||||||
|
**User answer:** 先手动跑通,稳定后再开定时
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与 deliverable=MVP+定时自动化 不矛盾:定时任务要搭但手动验证后才启用。
|
||||||
|
|
||||||
|
### 36. Gitea 上的仓库怎么建?
|
||||||
|
|
||||||
|
**Recommended answer:** 新建私有仓库 dwarf-fortress-annals,用 SSH key 自动推
|
||||||
|
|
||||||
|
**User answer:** 新建公开仓库
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与 publish_target=Gitea 一致;公开仓库意味着措辞和事实错误会直接对外可见。
|
||||||
|
|
||||||
|
### 37. 生成好的一章要不要先给你过目?
|
||||||
|
|
||||||
|
**Recommended answer:** 先生成到本地待审,我点头再推
|
||||||
|
|
||||||
|
**User answer:** 自动直接推,我事后自己改
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与推荐相反,且仓库为公开:LLM 幻觉会直接进公开『正史』。已列入风险。
|
||||||
|
|
||||||
|
### 38. 一章切多大一段历史?
|
||||||
|
|
||||||
|
**Recommended answer:** 一个世纪或一条完整事件链
|
||||||
|
|
||||||
|
**User answer:** 混合
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与上一轮 events-driven、每章 800–1500 字兼容。
|
||||||
|
|
||||||
|
### 39. 世界生成参数倾向?
|
||||||
|
|
||||||
|
**Recommended answer:** Medium 世界 + 250 年
|
||||||
|
|
||||||
|
**User answer:** Medium 世界 + 250 年
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 确定后单次 worldgen 即可支撑数十章素材。
|
||||||
|
|
||||||
|
### 40. 用哪家云端 LLM、key 怎么准备?
|
||||||
|
|
||||||
|
**Recommended answer:** DeepSeek(便宜且网络可达),key 由你自己申请后写入 WSL 环境变量
|
||||||
|
|
||||||
|
**User answer:** 用你的(自定义答案)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 自定义答案『用你的』语义不明:我没有任何可用的 API 密钥,且不能向你索要密钥内容。需第三轮澄清落地方式。
|
||||||
|
|
||||||
|
### 41. Windows 侧的 DF + DFHack 由谁装?
|
||||||
|
|
||||||
|
**Recommended answer:** 我经 /mnt/c 下载解压放好,你只需最后点开确认
|
||||||
|
|
||||||
|
**User answer:** 你(agent)下载解压到 /mnt/c 下
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 需确认 Bay 12 下载链路与 Windows 用户目录名(/mnt/c/Users 下有 23518、WsiAccount 等)。
|
||||||
|
|
||||||
|
### 42. 叙述口吻用哪种?
|
||||||
|
|
||||||
|
**Recommended answer:** 史官编年体(冷静第三人称)
|
||||||
|
|
||||||
|
**User answer:** 战报体(诙谐吐槽,像 Boatmurdered)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 口语口吻与『历史数据→叙事』的匹配度低于编年体,但趣味性更高;牵出一新问题:战报体的『我』是谁(因为本方案不玩,没有玩家视角)。
|
||||||
|
|
||||||
|
### 43. 『用你的』具体指什么?
|
||||||
|
|
||||||
|
**Recommended answer:** 就由我(agent)当写作引擎 / 或你自己导出 key 给脚本
|
||||||
|
|
||||||
|
**User answer:** 用 Pi 里已配好的那家(你只需把 key 再导一份给脚本)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 用户选择复用 Pi 已配好的 provider,需确认是哪一家/env 变量名;我不会读取 auth.json 内容。
|
||||||
|
|
||||||
|
### 44. 推送到 Gitea 用哪种认证?
|
||||||
|
|
||||||
|
**Recommended answer:** 生成新密钥对 + 你贴公钥
|
||||||
|
|
||||||
|
**User answer:** 直接复用已有的 ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 待验证:该公钥是否已在 Gitea 账号注册(已测 2222 端口开放)。
|
||||||
|
|
||||||
|
### 45. 战报体里的『我』是谁?
|
||||||
|
|
||||||
|
**Recommended answer:** 要塞里的编年史官(在场记录 + 吐槽)
|
||||||
|
|
||||||
|
**User answer:** 要塞里的编年史官(在场记录 + 吐槽)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与自由演绎 + 战报体相洽。
|
||||||
|
|
||||||
|
### 46. 公开仓库里放什么?
|
||||||
|
|
||||||
|
**Recommended answer:** 成品 + 迷你年表/人物索引
|
||||||
|
|
||||||
|
**User answer:** 成品 + 迷你年表/人物索引
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 年表/索引同时充当事实核对的参考面。
|
||||||
|
|
||||||
|
### 47. 战报体允许多少文学加工?
|
||||||
|
|
||||||
|
**Recommended answer:** 自由演绎 + 章末附史料注
|
||||||
|
|
||||||
|
**User answer:** 自由演绎:对白、心理、吐槽都写
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 与自动专名校验存在张力:校验只能拦住『凭空的人名地名』,拦不住编造的对白与心理活动,但用户已接受此代价。
|
||||||
|
|
||||||
|
### 48. 既然自动直推 + 公开仓库,要不要自动事实校验?
|
||||||
|
|
||||||
|
**Recommended answer:** 要:自动比对专名,可疑处标注
|
||||||
|
|
||||||
|
**User answer:** 要:自动比对专名是否都出现在 legends 数据里,可疑处标注
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
### 49. 这次做到什么程度算完成?
|
||||||
|
|
||||||
|
**Recommended answer:** 第 2、3 章也能稳定产出并自动推上去
|
||||||
|
|
||||||
|
**User answer:** DF 能跑 + 第 1 章出现在 Gitea
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 本次验收比 deliverable=mvp_plus 窄:定时任务暂不纳入验收,与 automation_owner=先手动跑通一致。
|
||||||
|
|
||||||
|
### 50. 用网关里的哪个模型写故事?
|
||||||
|
|
||||||
|
**Recommended answer:** cn:deepseek-v4-pro
|
||||||
|
|
||||||
|
**User answer:** cn:deepseek-v4-pro(推荐)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** 已查明 Pi 配置的 provider 为 workbuddy(baseUrl https://wb2api.hajim1.art/v1,OpenAI 兼容,共 39 个模型),/v1/models 返回 401 证明在线且需鉴权。
|
||||||
|
|
||||||
|
### 51. API key 怎么交给脚本?
|
||||||
|
|
||||||
|
**Recommended answer:** 脚本运行时自读 ~/.pi/agent/auth.json 的 workbuddy.key
|
||||||
|
|
||||||
|
**User answer:** 脚本运行时自己读 ~/.pi/agent/auth.json 里的 workbuddy.key(推荐)
|
||||||
|
|
||||||
|
**Status:** resolved
|
||||||
|
|
||||||
|
**Notes:** auth.json 结构为 { workbuddy: { type, key } };脚本运行时读取,我不读不打印内容。
|
||||||
|
|
||||||
|
## Agreed Decisions
|
||||||
|
|
||||||
|
- 部署形态:Windows 侧跑 DF,WSL2 Ubuntu 侧只当「故事工厂」,经 /mnt/c 读写存档与导出数据
|
||||||
|
- 版本与获取:Bay 12 免费版(当前实为 53.16,df_53_16_win.zip)+ DFHack 53.16-r2 Windows 包;不购买 Steam 版
|
||||||
|
- 安装执行:由 agent 经 /mnt/c 下载解压到 Windows 用户目录(免管理员权限),用户只需最后点开确认游戏能跑
|
||||||
|
- 故事形态:只做 A 世界史编年史(worldgen + legends 导出 + LLM 叙事),不做 AI 自动游玩;B 方案列为第二阶段
|
||||||
|
- 连载完结策略:一个世界分卷连载,写完全部历史即宣布该世界史完结,再生成新世界开新卷,用同一套叙述者人设维持系列感
|
||||||
|
- 章节切分:混合式——有重大事件就写事件链,太平年份合并成章;每章 800–1500 字,事件驱动而非日更
|
||||||
|
- 世界参数:Medium 世界 + 250 年历史
|
||||||
|
- 叙事引擎:复用 Pi 里已配好的 provider,即用户自建网关 workbuddy(base_url https://wb2api.hajim1.art/v1,OpenAI 兼容)
|
||||||
|
- 写作模型:固定使用 cn:deepseek-v4-pro(1M 上下文可整包喂 legends,固定模型保证跨章文风一致)
|
||||||
|
- API key 交付:脚本运行时自读 ~/.pi/agent/auth.json 中的 workbuddy.key;不额外落明文副本,agent 只写读取代码、不读不打印内容
|
||||||
|
- 叙述者身份:要塞里的编年史官(在场记录 + 吐槽),不用现代玩家视角,也不用单个人物第一人称日记
|
||||||
|
- 叙述口吻:战报体(诙谐吐槽,类比经典 Boatmurdered)
|
||||||
|
- 演绎边界:自由演绎——对白、心理活动、吐槽都可写(代价是读者无法区分史料与文学)
|
||||||
|
- 自动事实校验:要。生成后自动比对专名是否都出现在 legends 数据中,可疑处标注(不阻断推送)
|
||||||
|
- 发布去向:只推自建 Gitea(124.222.29.26),不落本地 git 仓库、不发博客
|
||||||
|
- Gitea 仓库:新建公开仓库 dwarf-fortress-annals
|
||||||
|
- 推送认证:直接复用现有 ~/.ssh/id_ed25519(已验证在 Gitea 注册为 chendy-ubuntu-wsl / gitadmin),无需新建密钥或 token
|
||||||
|
- 仓库内容:成品 Markdown + 迷你年表/人物索引;不入库原始 legends 数据与存档
|
||||||
|
- 推送门槛:不做人工审核,自动直接推送到 Gitea,用户事后自行修改
|
||||||
|
- 输出语言:中文,专有名词保留英文(如 Urist、Boatmurdered)
|
||||||
|
- 配图:纯文字,不做地图/ASCII 渲染配图
|
||||||
|
- 系统依赖:允许 sudo apt 任意安装 WSL 侧依赖
|
||||||
|
- 触发方式:先手动一条命令跑通,稳定后再开 WSL cron 定时;当前不启用常驻 daemon 与开机自启
|
||||||
|
- 运行位置:先全部在本地,后续再考虑搬到服务器 124.222.29.26
|
||||||
|
- 本次验收标准:DF 能在 Windows 侧跑起来 + 第 1 章中文故事出现在 Gitea 公开仓库(定时任务不纳入本轮验收)
|
||||||
|
|
||||||
|
## Open Risks
|
||||||
|
|
||||||
|
- 自动直推 + 公开仓库:LLM 幻觉会直接进入对外可见的「正史」。用户接受事后自行修改,但专名校验只能拦住凭空出现的人名地名,拦不住编造的对白与心理活动(自由演绎已明确放开)——这类错误没有任何自动防线
|
||||||
|
- 唯一外部依赖是用户自建网关 workbuddy(https://wb2api.hajim1.art/v1):它一旦不可用/额度耗尽,整条产出链停摆;且该网关不在本机,排障需要用户侧信息
|
||||||
|
- cron 定时依赖 WSL 常驻:Windows 关机、WSL 闲置回收都会让产出静默断供;无人值守时失败不易被发现
|
||||||
|
- 战报体 + 固定「要塞编年史官」人设需要跨章长期一致,仅靠 prompt 约束,长连载下口吻与设定可能漂移(跨章设定漂移检查未列入本次范围)
|
||||||
|
- 版本必须严格配对:DF 53.16 只能配 DFHack 53.16-r2;DF 自动更新会导致 DFHack 失效,需要锁定版本
|
||||||
|
- legends 导出的自动化程度尚未实测:53.x 上的导出入口(游戏内 UI / DFHack 命令)能否完全脚本化、免手动点击,需要在实施第一步验证;若必须人工点击,则「无人值守」目标需降级
|
||||||
|
- WSL2 为 NAT 网络(网关 172.31.48.1),WSL 与 Windows 侧进程的实时通信(若将来需要 DFHack RPC)需额外配置;本方案走文件交换可绕开,但第二阶段 B 方案会直面此问题
|
||||||
|
- Windows 用户目录疑似 /mnt/c/Users/23518(另有 WsiAccount),实施时需确认哪个是实际登录账户
|
||||||
|
- 服务器迁移路径已被承认但未设计:服务器无图形界面,未来最多只能承载「世界史生成 + 叙事 + 发布」,游玩相关环节无法搬移
|
||||||
|
|
||||||
|
## Next Decision Needed
|
||||||
|
|
||||||
|
无待决项。下一步是实施:先验证 legends 导出的可脚本化程度与 Windows 实际用户目录,再搭 WSL 管道,最后以「DF 能跑 + 第 1 章推到 Gitea」验收。
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 矮人要塞编年史 · Dwarf Fortress Annals
|
||||||
|
|
||||||
|
把《矮人要塞》(Dwarf Fortress) 世界生成的历史数据,写成**中文战报体连载故事**,自动推送到这个仓库。
|
||||||
|
|
||||||
|
本仓库是**成品仓**:只放小说章节、迷你年表与人物索引。
|
||||||
|
原始 `legends` 导出数据与游戏存档留在本地,不入库。
|
||||||
|
|
||||||
|
## 这本"书"是怎么来的
|
||||||
|
|
||||||
|
```
|
||||||
|
Windows 侧 DF 53.16 + DFHack 53.16-r2
|
||||||
|
│ ① 生成 Medium 世界 / 250 年历史
|
||||||
|
│ ② 进入 legends 模式,导出 legends.xml + legends_plus.xml
|
||||||
|
▼ ③ 经 /mnt/c 文件交换(无需网络互访)
|
||||||
|
WSL2 Ubuntu 侧管道
|
||||||
|
│ ④ 解析史料 → 构建年表与人物索引
|
||||||
|
│ ⑤ 按事件链切章(每章 800–1500 字)
|
||||||
|
│ ⑥ 组 prompt(要塞编年史官口吻 / 战报体)→ 调用 LLM 写作
|
||||||
|
│ ⑦ 专名事实校验:正文里的专有名词必须出现在史料中,可疑处标注
|
||||||
|
▼ ⑧ git commit & push
|
||||||
|
Gitea: gitadmin/dwarf-fortress-annals(本仓库)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
| 路径 | 内容 | 是否入库 |
|
||||||
|
|---|---|---|
|
||||||
|
| `output/chapters/` | 连载正文(Markdown) | ✅ |
|
||||||
|
| `output/timeline.md` | 迷你年表 | ✅ |
|
||||||
|
| `output/figures.md` | 人物索引 | ✅ |
|
||||||
|
| `src/dfannals/` | 管道源码 | ✅ |
|
||||||
|
| `prompts/` | 写作规范(决定文风与人设) | ✅ |
|
||||||
|
| `data/exports/` | 原始 legends 数据 | ❌ |
|
||||||
|
| `vendor/` | 游戏与 DFHack 安装包 | ❌ |
|
||||||
|
|
||||||
|
## 写作设定
|
||||||
|
|
||||||
|
- **叙述者**:要塞里的编年史官,在场记录,负责把事写下来
|
||||||
|
- **口吻**:战报体——严肃的骨架,诙谐的血肉
|
||||||
|
- **语言**:中文;专有名词一律保留英文原文(Urist、Boatmurdered……)
|
||||||
|
- **演绎**:允许虚构对白、心理与吐槽;但**不许发明史料里没有的人名地名**,也不许改写史实骨架
|
||||||
|
- **连载**:同一世界长期连载;一部史写完则换新世界开新卷,由同一位"编年史官"承接
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 凭据:API key 运行时从 Pi 的 ~/.pi/agent/auth.json 读取,不落本仓库
|
||||||
|
python3 -m dfannals.cli --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 声明
|
||||||
|
|
||||||
|
《Dwarf Fortress》版权归 Bay 12 Games 所有。本仓库仅包含由其生成数据的**衍生文本**,
|
||||||
|
不含游戏本体、美术资源或存档。
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 要塞编年史官 · 写作规范
|
||||||
|
|
||||||
|
你是一本矮人要塞编年史的**执笔者**。你不是游戏外的解说员,而是这个世界里负责记录的人:
|
||||||
|
你亲眼见过这些事,或者在事后翻过档案,现在由你把它们写下来。
|
||||||
|
|
||||||
|
## 口吻
|
||||||
|
|
||||||
|
- **战报体**:像一份写给上级、但作者本人憋不住嘴的报告。严肃的骨架,诙谐的血肉。
|
||||||
|
- 允许吐槽、暗讽、夸张、替死人惋惜、替蠢货翻白眼。可以骂人,但骂得有趣。
|
||||||
|
- 情绪可以饱满,但叙事要清楚:读者必须始终知道**谁、在哪、干了什么、结果如何**。
|
||||||
|
|
||||||
|
## 语言
|
||||||
|
|
||||||
|
- 正文用**中文**。
|
||||||
|
- 所有**专有名词保留英文原文**:人物名(Urist McMiner)、地名(Boatmurdered)、
|
||||||
|
文明名、生物名、神器名。不要音译,不要自造中文译名。
|
||||||
|
- 不要把英文词硬塞成中式表达;名字是个代号,句子本身是中文。
|
||||||
|
|
||||||
|
## 演绎的边界
|
||||||
|
|
||||||
|
- **允许**:虚构对白、心理活动、场景细节、围观群众的反应。
|
||||||
|
- **允许**:把数据里冷冰冰的因果,写成有人味的故事。
|
||||||
|
- **禁止**:发明数据里不存在的**专有人名、地名、文明名**。新角色只能来自给定的史料。
|
||||||
|
- **禁止**:改变大事件的年份、参与者、胜负结果。史实骨架不可动。
|
||||||
|
- 若史料本身残缺(只留下一个年份和一句话),就按残缺写——写成"史料到此中断"也是一种味道。
|
||||||
|
|
||||||
|
## 篇幅
|
||||||
|
|
||||||
|
- 正文 **800–1500 字**(中文字符计)。
|
||||||
|
- 宁可写短写实,不要为凑字数灌水。
|
||||||
|
|
||||||
|
## 输出格式
|
||||||
|
|
||||||
|
```
|
||||||
|
# <章标题>
|
||||||
|
|
||||||
|
<正文,可分段,可用小标题>
|
||||||
|
|
||||||
|
---
|
||||||
|
*史料来源:<本段所依据的史料条目摘要,一到两行>*
|
||||||
|
```
|
||||||
|
|
||||||
|
- 章标题自己起,要有战报味,能概括本章。
|
||||||
|
- 不要在正文里写"以下是……"之类的话,直接进入叙述。
|
||||||
|
- 不要在正文中提到你是在读数据、表格、XML 或 AI。
|
||||||
|
|
||||||
|
## 连续性
|
||||||
|
|
||||||
|
- 这是一部长篇连载的其中一章。读者已经读过前面几章。
|
||||||
|
- 前情提要用一两句带过即可,不要复述整部前文。
|
||||||
|
- 同一批人物、家族、恩怨要延续上一章的状态;本章结束时应留下可被下一章接住的线头。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""矮人要塞编年史生成管道。"""
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""项目配置:路径、模型、网关、发布目标。
|
||||||
|
|
||||||
|
原则:API key 不落本项目文件,运行时从 Pi 的 auth.json 读取。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
HOME = Path.home()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 目录
|
||||||
|
PROJECT_DIR = Path(__file__).resolve().parents[2] # /home/chendy/df-annals
|
||||||
|
DATA_DIR = PROJECT_DIR / "data" # 原始数据,不入库
|
||||||
|
EXPORT_DIR = DATA_DIR / "exports" # legends 导出原始文件
|
||||||
|
OUTPUT_DIR = PROJECT_DIR / "output" # 成品,入库
|
||||||
|
CHAPTER_DIR = OUTPUT_DIR / "chapters"
|
||||||
|
PROMPT_DIR = PROJECT_DIR / "prompts"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 游戏(Windows 侧)
|
||||||
|
WINDOWS_USER = "23518"
|
||||||
|
WIN_DF_ROOT = Path(f"/mnt/c/Users/{WINDOWS_USER}/DwarfFortress") # 下载/安装根目录
|
||||||
|
WIN_DF_DIR_CANDIDATES = [
|
||||||
|
WIN_DF_ROOT / "df_53_16_win",
|
||||||
|
WIN_DF_ROOT / "Dwarf Fortress",
|
||||||
|
WIN_DF_ROOT,
|
||||||
|
]
|
||||||
|
DF_VERSION = "53.16"
|
||||||
|
DFHACK_VERSION = "53.16-r2"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- LLM 网关
|
||||||
|
PI_AUTH_JSON = HOME / ".pi/agent/auth.json"
|
||||||
|
PI_MODELS_JSON = HOME / ".pi/agent/models.json"
|
||||||
|
PROVIDER = "workbuddy"
|
||||||
|
MODEL = "cn:deepseek-v4-pro"
|
||||||
|
# 该模型是推理模型:思考与正文共用 max_tokens,必须留足
|
||||||
|
MAX_TOKENS_CHAPTER = 16000
|
||||||
|
MAX_TOKENS_UTIL = 4000
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 发布
|
||||||
|
GITEA_SSH_HOST = "124.222.29.26"
|
||||||
|
GITEA_SSH_PORT = 2222
|
||||||
|
GITEA_USER = "gitadmin"
|
||||||
|
GITEA_REPO = "dwarf-fortress-annals"
|
||||||
|
GITEA_WEB = "http://124.222.29.26:3000"
|
||||||
|
GITEA_REMOTE = f"ssh://git@{GITEA_SSH_HOST}:{GITEA_SSH_PORT}/{GITEA_USER}/{GITEA_REPO}.git"
|
||||||
|
SSH_KEY = HOME / ".ssh/id_ed25519"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 世界与章节
|
||||||
|
WORLD_SIZE = "Medium"
|
||||||
|
WORLD_HISTORY_YEARS = 250
|
||||||
|
CHAPTER_MIN_CHARS = 800
|
||||||
|
CHAPTER_MAX_CHARS = 1600 # 契约上限 1500,留出标点/换行余量
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Gateway:
|
||||||
|
base_url: str
|
||||||
|
key: str
|
||||||
|
model: str = MODEL
|
||||||
|
provider: str = PROVIDER
|
||||||
|
|
||||||
|
|
||||||
|
def load_gateway(provider: str | None = None) -> Gateway:
|
||||||
|
"""从 Pi 配置读取网关地址与密钥。密钥只驻留内存,不打印、不落盘。"""
|
||||||
|
provider = provider or PROVIDER
|
||||||
|
|
||||||
|
def _read_json(path: Path, label: str) -> dict:
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text())
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise SystemExit(f"找不到 Pi 的{label}文件: {path}") from None
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise SystemExit(f"Pi 的{label}文件不是合法 JSON: {path} ({exc})") from None
|
||||||
|
|
||||||
|
auth = _read_json(PI_AUTH_JSON, "凭据")
|
||||||
|
models = _read_json(PI_MODELS_JSON, "模型配置")
|
||||||
|
providers = models.get("providers", models)
|
||||||
|
if provider not in providers:
|
||||||
|
raise SystemExit(f"Pi 配置里没有 provider: {provider}")
|
||||||
|
if provider not in auth:
|
||||||
|
raise SystemExit(f"Pi 未授权 provider: {provider}")
|
||||||
|
entry = auth[provider]
|
||||||
|
key = entry.get("key") if isinstance(entry, dict) else entry
|
||||||
|
if not key:
|
||||||
|
raise SystemExit(f"provider {provider} 的凭据为空")
|
||||||
|
return Gateway(base_url=providers[provider]["baseUrl"].rstrip("/"), key=key, provider=provider)
|
||||||
|
|
||||||
|
|
||||||
|
def find_df_dir() -> Path:
|
||||||
|
"""定位已安装的 DF 目录(含 Dwarf Fortress.exe)。"""
|
||||||
|
override = os.environ.get("DF_DIR")
|
||||||
|
if override:
|
||||||
|
p = Path(override)
|
||||||
|
if p.is_dir():
|
||||||
|
return p
|
||||||
|
raise SystemExit(f"DF_DIR 不存在: {p}")
|
||||||
|
for cand in WIN_DF_DIR_CANDIDATES:
|
||||||
|
if (cand / "Dwarf Fortress.exe").is_file():
|
||||||
|
return cand
|
||||||
|
raise SystemExit(
|
||||||
|
"未找到 Dwarf Fortress.exe。候选路径:" + ", ".join(str(c) for c in WIN_DF_DIR_CANDIDATES)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dirs() -> None:
|
||||||
|
for d in (DATA_DIR, EXPORT_DIR, OUTPUT_DIR, CHAPTER_DIR, PROMPT_DIR):
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""调用用户自建网关(OpenAI 兼容)生成文本。
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- 密钥运行时从 Pi 的 auth.json 读取,只在内存中,永不打印或写入产物。
|
||||||
|
- 目标是推理模型,思考与正文共用 max_tokens,因此默认给足预算。
|
||||||
|
- 传输层用 http.client(方案显式限定 http/https),不依赖第三方库。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from .config import MAX_TOKENS_CHAPTER, MODEL, Gateway, load_gateway
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Reply:
|
||||||
|
content: str
|
||||||
|
reasoning_tokens: int
|
||||||
|
total_tokens: int
|
||||||
|
model: str
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _endpoint(base_url: str) -> tuple[type[http.client.HTTPConnection], str, str]:
|
||||||
|
"""解析网关地址,只接受 http/https。"""
|
||||||
|
parts = urlsplit(base_url)
|
||||||
|
if parts.scheme not in ("http", "https"):
|
||||||
|
raise GatewayError(f"网关地址必须是 http(s):{base_url!r}")
|
||||||
|
if not parts.netloc:
|
||||||
|
raise GatewayError(f"网关地址缺少主机:{base_url!r}")
|
||||||
|
cls = http.client.HTTPSConnection if parts.scheme == "https" else http.client.HTTPConnection
|
||||||
|
path = parts.path.rstrip("/") + "/chat/completions"
|
||||||
|
return cls, parts.netloc, path
|
||||||
|
|
||||||
|
|
||||||
|
def chat(
|
||||||
|
messages: list[dict],
|
||||||
|
*,
|
||||||
|
gateway: Gateway | None = None,
|
||||||
|
model: str = MODEL,
|
||||||
|
max_tokens: int = MAX_TOKENS_CHAPTER,
|
||||||
|
temperature: float = 1.0,
|
||||||
|
timeout: int = 900,
|
||||||
|
retries: int = 3,
|
||||||
|
) -> Reply:
|
||||||
|
gw = gateway or load_gateway()
|
||||||
|
conn_cls, host, path = _endpoint(gw.base_url)
|
||||||
|
body = json.dumps(
|
||||||
|
{
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
).encode()
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
# 密钥仅在此处作为请求头使用,不落盘、不打印
|
||||||
|
"Authorization": "Bearer " + gw.key,
|
||||||
|
}
|
||||||
|
|
||||||
|
last: str | None = None
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
conn = conn_cls(host, timeout=timeout)
|
||||||
|
try:
|
||||||
|
conn.request("POST", path, body=body, headers=headers)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
raw = resp.read()
|
||||||
|
if resp.status != 200:
|
||||||
|
last = f"HTTP {resp.status}: {raw[:400].decode('utf-8', 'replace')}"
|
||||||
|
if resp.status in (400, 401, 403, 404):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
data = json.loads(raw)
|
||||||
|
choice = data["choices"][0]["message"]
|
||||||
|
usage = data.get("usage") or {}
|
||||||
|
content = (choice.get("content") or "").strip()
|
||||||
|
if not content:
|
||||||
|
raise GatewayError(
|
||||||
|
"模型只产出了思考、没有正文;"
|
||||||
|
f"thinking_tokens={usage.get('completion_thinking_tokens')},请加大 max_tokens"
|
||||||
|
)
|
||||||
|
return Reply(
|
||||||
|
content=content,
|
||||||
|
reasoning_tokens=int(usage.get("completion_thinking_tokens") or 0),
|
||||||
|
total_tokens=int(usage.get("total_tokens") or 0),
|
||||||
|
model=data.get("model", model),
|
||||||
|
)
|
||||||
|
except (OSError, http.client.HTTPException, json.JSONDecodeError, KeyError) as exc:
|
||||||
|
last = f"{type(exc).__name__}: {exc}"
|
||||||
|
except GatewayError as exc:
|
||||||
|
last = str(exc)
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
if attempt < retries:
|
||||||
|
time.sleep(3 * attempt)
|
||||||
|
|
||||||
|
raise GatewayError(f"网关调用失败({gw.base_url},model={model}):{last}")
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""把成品章节与年表推送到自建 Gitea。
|
||||||
|
|
||||||
|
- 推送凭据复用现有 ~/.ssh/id_ed25519(已验证在 Gitea 注册为 gitadmin 的 chendy-ubuntu-wsl)。
|
||||||
|
- 原始 legends 数据与存档不入库(见 .gitignore)。
|
||||||
|
- 自动直推,不设人工审核环节(按访谈决策)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .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,
|
||||||
|
)
|
||||||
|
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 commit_and_push(message: str, paths: list[Path] | None = None) -> str:
|
||||||
|
"""提交并推送。返回提交哈希;无改动时返回空串。"""
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user