面向《桌游模拟器》(Tabletop Simulator)图包作者的命令行工具:通过 TTS 的外部编辑器协议直连运行中的游戏,只读地检查连接状态、执行 Lua、拉取全部脚本与 UI、盘点素材 URL;并支持离线的图包工作区管理(unpack / build / pull / push / diff),所有面向用户的输出支持中文 / 英文双语。
当前进度:阶段 1(基础设施 + 协议层 + 只读 CLI)+ 阶段 2A(图包工作区)+ 2B(卡牌图集)+ 2C(多图包与版本控制)+ 阶段 3(素材导入 / 图床 / 打包分发,窗口 C)+ 阶段 4(hub + MCP,窗口 D)+ 阶段 5(写入路径,窗口 E)。
status / exec / pull / assets / config等命令只读;pack init/unpack/build/export/import/sync-upstream、import / host / fetch / migrate / deck / vcs / review离线或按需联网;pack pull/diff与运行中 TTS 交互;pack push为完整安全流水线(默认 dry-run,--yes才实写,见 §11);tts watch文件监听自动 push;tts hub启动常驻守护进程,tts-mcp为 MCP stdio 服务(见 §10)。
| 项目 | 要求 |
|---|---|
| Node.js | ≥ 24.19.0(package.json 的 engines;开发环境实测 v24.19.0) |
| 操作系统 | Windows 为主要目标平台(数据目录探测的安装目录 / 用户文档分支按 Windows 实现) |
| 游戏 | 使用与游戏通信的命令时,TTS 需正在运行并已加载图包存档(39999 端口才会监听) |
| 其他 | 关闭 VSCode 的 TTS 插件(见下文「与官方 VSCode 插件的互斥」) |
npm install # 网络受限时先看「代理配置」
npm run build # 编译到 dist/(集成测试与 tts 命令都依赖它)
npm link # 可选:把 tts 命令链接到全局不 npm link 时,用 node dist/cli/index.js 代替下文的 tts,例如
node dist/cli/index.js status。
以下输出为 2026-10-04 在本机(251 个对象的图包存档)实测,具体数字随存档变化。
$ tts status
已连接到 TTS 编辑器(端口 39998)
脚本引擎版本:MoonSharp 3.0.0.0
当前存档对象数:251端口 39998 被占用时给出可操作的中文提示并以退出码 1 结束(通常是 VSCode 插件还开着)。
在当前存档的全局脚本中执行一段 Lua,并打印返回值:
$ tts exec 'return 1+1'
2
$ tts exec 'return #getObjects()'
251
$ tts exec 'return {a=1,b="x"}'
{
"a": 1,
"b": "x"
}要点:
- 返回 table / 对象时,
exec自动用return JSON.encode(...)包装再解析,绕开「协议只回传标量、table 静默失败」的坑; - 数字会整型化:TTS 回传
2.0,输出为2; - 默认超时 30 秒;Lua 报错时 stderr 含 TTS 原始错误与行列号(如
(1,0-44)),退出码 1; - 不支持多返回值:
return 1, 2会报错并提示改用JSON.encode。
$ tts pull ./work
输出目录:D:\...\work
拉取完成:脚本 36 个,UI 界面 1 个文件名规则:全局脚本为 Global.lua / Global.xml,其他对象为 <guid>.<对象名>.lua / .xml;
对象没有脚本或 UI 时不生成空文件。目录不存在会自动创建。
$ tts assets
共 130 个不同 URL,249 次引用
按域名分组:
steamusercontent-a.akamaihd.net: 130 URL, 249 引用--check:联网探测每个 URL 的存活(HTTP 2xx),输出有效 / 失效数量与前 20 条死链;--sample <n>:配合--check只随机抽检 N 个 URL;- 走代理:见「代理配置」。
$ tts config datadir
找到 4 个 TTS 数据目录:
1. D:\SteamLibrary\...\Tabletop Simulator_Data\Mods(来源:安装目录,有效子目录 2 个)
2. C:\Program Files (x86)\Steam\...\Tabletop Simulator_Data\Mods(来源:安装目录,有效子目录 0 个)
3. C:\SteamLibrary\...\Tabletop Simulator_Data\Mods(来源:安装目录,有效子目录 0 个)
4. C:\Users\<用户>\Documents\My Games\Tabletop Simulator\Mods(来源:用户文档,有效子目录 2 个)
检测到多个可用的 TTS 数据目录:
请输入编号后回车(1-4,直接回车取消):探测到多个有效目录时必须由用户选择,不会静默挑一个;结果写入
%APPDATA%\tts-toolkit\config.yaml。非交互环境(如脚本、管道)请用
tts config datadir --set <path> 直接写入。
离线地管理图包工作区:从存档 JSON 或 .ttsmod 解包出 scripts/、ui/、decks/、objects/,改完后再合成回 TTS 可加载的存档 JSON。骨架存档 .tts/skeleton.json 是 build 做"按 GUID 定点替换"的依据,绝不入 git(已加入 .gitignore)。
$ tts pack init ./my-pack --name 第七大陆汉化
图包工作区已创建于 D:\...\my-pack
$ tts pack unpack ./save.json --out ./my-pack
已从 ./save.json 解包到 ./my-pack:36 脚本,1 UI,251 对象
$ tts pack unpack ./workshop.ttsmod --out ./my-pack
已从 ./workshop.ttsmod 解包到 ./my-pack:0 脚本,0 UI,11 对象
$ tts pack build --root ./my-pack
已写出 ./my-pack/dist/my-pack.json:替换 0 脚本 / 0 UI / 0 对象
$ tts pack build --root ./my-pack --dry-run
已写出 ./my-pack/dist/my-pack.json:替换 0 脚本 / 0 UI / 0 对象
(dry-run,未写文件)要点:
tts pack init <dir>新建工作区:ensureLayout建 7 个目录(scripts/ ui/ decks/ objects/ sheets/ source/ .tts/)+ 写pack.yaml+git init+ git-lfs 强制三选一(装 / 禁用二次确认 / 取消,约束 10);可用--lfs enabled|disabled跳过交互、--skip-git跳过 git。tts pack unpack <save>离线解包:支持存档 JSON 或.ttsmod(后者是 ZIP,自动找Mods/Workshop/*.json)。落盘.tts/skeleton.json+ 整理scripts/、ui/、decks/、objects/。tts pack pull从运行中的 TTS 拉最新脚本/UI 到工作区,不更新 skeleton(skeleton 只由 unpack 生成)。tts pack push骨架:列出将推回游戏的清单(阶段 5 才实际调 saveAndPlay)。tts pack diff对比工作区与运行中 TTS 的脚本/UI 差异(added/modified/deleted)。tts pack build把工作区合成 TTS 可加载的存档 JSON(约束 8:读 skeleton + 按 GUID 定点替换,未改动对象与骨架逐字节一致);--dry-run只打印摘要。
工作区契约文档见 docs/schemas/(pack.yaml / deck.yaml / assets.yaml / cards.csv / objects.csv / registry.yaml 六份,B2/B3 必读;另加阶段 3 的 import.yaml / host / ttsmod 三份、阶段 4 的 hub-control 一份、阶段 5 的 baseline.json 一份,共十一份)。
离线做卡牌图集的切片 / 重新拼接 / 原位拼回 / URL 替换 dry-run / 一致性校验。卡牌明细的唯一源是 <deck>/cards.csv(10 列严格顺序);deck.yaml 只留元数据(name/guid/shared_with/atlas)。
$ tts deck slice --sheet 第七大陆卡图.png --cols 5 --rows 5 --save save.json -o ./cards
已切片:5 张卡到 ./cards/冒险牌堆/
$ tts deck generate --cards ./cards --cols 10 --rows 7 -o ./rebuilt.png
已重新排版:70 张卡 → 1 张图集
$ tts deck generate --in-place --deck 冒险牌堆 --root ./my-pack
已原位拼回:3 张卡换图,67 张保持原像素
$ tts deck plan --replace https://old.com/=https://new.com/ --root ./my-pack
替换计划:影响 52 处 / 涉及 52 个对象(父牌堆 + 每张 Card 子对象的 CustomDeck 都覆盖)
$ tts deck verify --root ./my-pack
校验完成:0 错误,2 警告要点:
- 1 基 slot 写死:
cards.csv的slot列与CardID % 100一致(%100===0 记 100)。 - slot 是不变量:重新排版时按 card_id 的 slot 连续段切,不改卡序。
- 余数吸收机制:源图集不整除网格时(如 4096/5=819.2),单格尺寸按
floor(源/cols)取整,剩余像素由最后一列/行吸收——切片/拼回/校验四处统一。 - UniqueBack 两种:
true切 70 张背面;false + BackURL==FaceURL降级为无自定义背面(不重复切)。 - 共享图集:同一 URL 被多个 CustomDeck 引用时,
slice列出所有候选牌堆让用户选,写入deck.yaml的shared_with。
多图包统一视图:所有图包注册到 <packs_root>/.registry.yaml(含名称/类型/上游工坊/当前分支/图床/统计/lfs 状态)。
git 版本控制语义封装:把 git status 的文件级输出翻译成图包语言——"冒险牌堆 12 张卡换图"而不是"decks/冒险牌堆/001_正面.png 已修改"。绝不替用户做合并选择。
$ tts pack list --root ./packs
已注册图包(2 个):
第七大陆全扩 第七大陆全扩(脚本汉化) zh-cn true enabled
沉睡的神祇 沉睡的神祇 main false disabled
$ tts pack list --dirty
已注册图包(1 个):
第七大陆全扩 第七大陆全扩(脚本汉化) zh-cn true enabled
$ tts pack status 第七大陆全扩
dir: 第七大陆全扩
name: 第七大陆全扩(脚本汉化)
kind: localization
branch: zh-cn
...
$ tts vcs status
冒险牌堆 2 张卡换图(001_正面.png, 002_正面.png)
素材 tile_01 改动
Global 脚本 +5/-1 行
$ tts vcs commit -m "汉化冒险牌堆"
已提交 32ab68e:汉化冒险牌堆:替换 冒险牌堆 2 张卡图,修改 Global 脚本(+8/-0 行),更新素材 tile_01
$ tts vcs status --conflicts # 有 merge UU 时
冒险牌堆 1 张卡换图(001_正面.png)
发现 1 个合并冲突:
⚠️ 卡牌图片冲突(需人工选择)
牌堆:冒险牌堆 (GUID: abc123)
卡牌:迷路的旅人 (CardID: 101)
文件:decks/冒险牌堆/001_正面.png(正面)
所属图集:sheet_id=1, slot=1
源 URL:https://example.com/sheet1.png
冲突类型:双方都修改 (UU)
选择:
git checkout --ours decks/冒险牌堆/001_正面.png # 保留当前分支版本
git checkout --theirs decks/冒险牌堆/001_正面.png # 用对方分支版本
或手动用图像工具合成后 git add
$ tts vcs verify
校验完成:0 错误,1 警告(存在未提交改动)
$ tts vcs size
工作区体积:1.23 KB
.git 体积:806.03 KB
.git/lfs/objects 体积:768.09 KB
按目录分解:
decks 329 B 4
scripts 171 B 1
...
$ tts vcs lfs status
git-lfs 已安装(版本 3.7.1)
.gitattributes 含 lfs 规则:true
pack.yaml 的 vcs.lfs:enabled
三方一致(lfs 状态正常)
$ tts vcs lfs disable # 强制二次确认(约束 10)
⚠️ 禁用 git-lfs 后:
- 每次修改一张卡图,git 都会存一整份新副本
- 一个图包改 10 轮,仓库可能膨胀到几十 GB
- 推送到远端会非常慢,某些托管商可能拒绝(GitHub 单文件 100MB 上限)
确认禁用?此操作建议仅用于「纯本地、不打算推远端」的场景。
输入 yes 确认:yes
lfs 已禁用(.gitattributes 的 lfs 规则已清空,pack.yaml 已写为 disabled)要点:
.registry.yaml并发写保护:乐观锁(mtime 校验),写冲突抛REGISTRY_CONFLICT(不引入文件锁)。vcs verify是deck verify的超集:先跑 deck verify(网格/卡数/CMYK/CardID/父子一致/共享图集),再加 git 检查(未提交改动 / UU 冲突 / lfs 三方一致性)。vcs size三数字同报:lfs 启用时必须同时给"工作区体积"和"lfs 对象体积"(方案设计 §4.11.1)。- 冲突语义化反查:从
cards.csv/objects.csv反查冲突文件对应的 CardID / sheet_id / slot / sheet_source;绝不替用户选边(不自动跑--ours/--theirs)。 vcs commit自动 message:基于analyzeStatus的语义化结果,按"卡图 > 卡表 > 脚本 > UI > 素材 > 元数据"优先级取前 3 条,超过 3 条末尾加"等 N 项改动"。
阶段 3(窗口 C)补齐"素材从哪来 → 传到哪去 → 怎么打成自包含分发包"这条链路。命令分四组:
| 组 | 命令 | 联网 / 环境 |
|---|---|---|
| 素材导入 | tts import |
全离线 |
| 图床 | tts host list / tts host check / tts assets upload |
list 离线;check / upload 按图床联网 |
| 体检与迁移 | tts fetch / tts migrate |
联网 |
| 打包分发 | tts pack export / tts pack import / tts pack sync-upstream |
离线(export 的扩展名探测可选联网) |
| 审批门禁 | tts review prepare / tts review status / tts review gate |
prepare / gate 离线;status 需审批工具 |
本节示例中
import/host list/pack export/pack import为 2026-10-05 在本机实测 (演示夹具,非真实工坊图包);host check/fetch/migrate/assets upload/review为按 t() 模板展示的用法示例(依赖网络 / 图床 / 审批工具环境)。
按 import.yaml 把用户已备好的成图归位到工作区(切图来源走 tts deck slice,不经过这里)。
清单里的相对路径相对清单文件所在目录解析,--pack 给工作区根(缺省 .)。
$ tts import ./import.yaml --pack ./packs/演示包 --dry-run
导入完成:卡堆 1 个(新增卡 2 张),素材 1 个,复制文件 5 个
卡堆 冒险牌堆:新增 2 张卡,图集 1 张(新建) D:\...\packs\演示包\decks\冒险牌堆
素材 tile 地图板块A:asset_id imp-964dedd7(新建)
(dry-run:未写入任何文件,以上为规划结果)
$ tts import ./import.yaml --pack ./packs/演示包 # 实落盘(去掉最后一行提示)要点:
- 全部校验(清单 schema、文件存在性、图像可读、拒 CMYK)与规划先于任何落盘完成, 失败即报错并指出具体条目,不静默跳过;坏清单的错误码见契约文档。
- 只写
decks/<名>/cards.csv与objects/objects.csv(不写 deck.yaml);已有行的card_id/asset_id永不改变,新增卡从已有最大 key + 1 另开新图集。 - 契约:
docs/schemas/import.yaml.md。
host list 汇总三个来源:内置默认 steamcloud、全局配置声明的图床、~/.tts-toolkit/hosts/*.js
插件。host check 从工作区盘点素材 URL(.tts/skeleton.json + assets.yaml 合并去重)逐个做
存活检测,有死链时退出码 1(可直接当健康检查用)。
$ tts host list
可用图床 1 个:
steamcloud 内置(默认)
可删除:否 单文件上限:不限 格式:-
$ tts host check --pack ./packs/第七大陆
开始检测:130 个 URL(骨架 130 条 / 台账 0 条),图床 steamcloud
死链:https://example.com/gone.png(HTTP 404)
检测完成:共 130 个,存活 129,死链 1要点:
- 图床类型:
steamcloud(默认,上传可能需按提示去游戏内Cloud Manager → Upload All手动完成, 这是可接受结果不是失败)、s3(R2 / AWS / MinIO,可全自动)、local(仅本机测试)、command(调用自定义命令上传,万能逃生口;也可写~/.tts-toolkit/hosts/<名>.js插件)。 - 配置错误绝不静默回退默认图床:未知 id 报
HOST_NOT_FOUND,配置 / 插件损坏报HOST_CONFIG_INVALID/HOST_PLUGIN_*。 - 契约与完整可跑的 rclone 示例:
docs/schemas/host.md。
fetch 做"素材健康报告",把 URL 分成正常 / 可迁移(老 Steam Cloud 域名、Google Drive 转直链、
Dropbox、paste 站 raw)/ 死链 / 需人工四类并给修复动作;migrate 按 --to 指定的图床
下载 → 上传 → 递归改写工作区里全部存档形态 JSON(骨架 + decks/**/data.json +
objects/**/data.json),并同步 assets.yaml 与 objects.csv.source。
$ tts fetch ./packs/第七大陆/.tts/skeleton.json
从清单读取 URL:./packs/第七大陆/.tts/skeleton.json(132 条)
体检完成:共 132 个 URL(正常 100 / 可迁移 20 / 死链 8 / 需人工 4)
可迁移(20 条):
http://cloud-3.steamusercontent.com/ugc/... [老式 Steam Cloud] 修复:迁移域名
建议改为:https://steamusercontent-a.akamaihd.net/ugc/...
...
$ tts migrate --to s3 --pack ./packs/第七大陆
开始迁移:图床 s3,52 个 URL,3 个存档文件
已迁移:https://old.example.com/a.png → https://cdn.example.com/tts/...
跳过(不可下载):file:///C:/Users/...(本地路径无法下载,需重新上传)
迁移完成:成功 50 / 跳过 1 / 失败 1 / 待人工 0;共改写 118 处 URL,涉及 3 个文件要点:仍有失败 URL 时 migrate 退出码 1;file: / {lang} 变体等不可下载形态原样跳过并列出原因;
Steam Cloud 的 pending 结果不算失败,按"待人工上传"报告。迁移只改工作区,改完重新
tts pack build 即可产出引用新 URL 的存档。
扫 decks/、objects/ 下的素材,逐个算 sha256 与 assets.yaml 台账比对,只上传新增或内容
变化的文件,成功后回写台账与 objects.csv 的 source 列。
$ tts assets upload --pack ./packs/第七大陆 --host rclone-cdn
上传目标图床:rclone-cdn(pack.yaml 声明:steamcloud)
扫描完成:待上传 3 个,未变化跳过 12 个
已上传 decks/冒险牌堆/001_正面.png → https://cdn.example.com/tts/decks/冒险牌堆/001_正面.png
已更新素材台账:D:\...\packs\第七大陆\assets.yaml
上传完成:成功 3 / 跳过 12 / 待人工 0 / 失败 0要点:--host 覆盖 pack.yaml.host;pack.yaml.host 的 imgur / gdrive / dropbox / custom
不是注册图床 id,会提示改用 --host。用内置 steamcloud 上传时返回"待人工上传"提示
(文件已备好在 .tts/steamcloud-pending/,去游戏内点 Upload All)。有失败时退出码 1。
导出:把工作区合成存档 JSON 后打成自包含 .ttsmod(素材条目名 = TTS 缓存键,接收方解压到
Mods 的父目录即可离线命中,存档 JSON 里的 URL 一个字不改)。
$ tts pack export ./packs/演示包 -o 演示图包.ttsmod --datadir "D:\...\Tabletop Simulator_Data\Mods"
已导出 D:\...\演示图包.ttsmod:条目 4 个,1.95 KB,素材 1 条,跳过 0 条
随包说明:README.txt包内布局(readZip 实测):
Mods/Workshop/演示图包.json 存档 JSON,逐字节原样(URL 一个字不改)
Mods/Images/httpsexamplecomimageshero.png 素材条目名 = sanitize(url) + 扩展名
manifest.json 工具 / 版本 / 时间 / 源工坊 ID / 素材清单(新增根条目)
README.txt 中英双语随包说明(--readme zh|en|both|none,默认 both)
导入:解压到 --into(Mods 的父目录);非 Mods/ 条目(如 Saves/)解到 --saves
(缺省 <into>/Saves)。已存在的文件不覆盖,逐个列出。
$ tts pack import 演示图包.ttsmod --into D:\tts-restore
已导入 演示图包.ttsmod → D:\tts-restore(写出 3 / 共 4 个文件条目)
包内工坊存档 1 个:
D:\tts-restore\Mods\Workshop\演示图包.json
如需建工作区,可运行:tts pack unpack "D:\tts-restore\Mods\Workshop\演示图包.json"
$ tts pack import 演示图包.ttsmod --into D:\tts-restore # 再导入一次
已导入 演示图包.ttsmod → D:\tts-restore(写出 0 / 共 4 个文件条目)
已存在未覆盖 3 个:
Mods/Workshop/演示图包.json
Mods/Images/httpsexamplecomimageshero.png
README.txt
...上游同步:pack import <工坊ID> --as-upstream 把上游快照落到 upstream 分支;
pack sync-upstream 拉新快照并合并回当前分支,冲突只报告不选边(退出码 1 待人工解决)。
$ tts pack import 379104394 --as-upstream --pack ./packs/第七大陆 --snapshot ./upstream-v2.json
已导入上游快照 ./upstream-v2.json(分支 upstream,提交 3ab12cd)
$ tts pack sync-upstream --pack ./packs/第七大陆
已同步上游快照 ...(当前分支 zh-cn,upstream 提交 9e8f7a6)
已将 upstream 合并回分支 zh-cn(干净合并)要点:--strict 缺任一素材即报错、不产出不完整的包;默认行为是"只打包本地已有的,缺的逐条列出";
-o 缺省按 <图包名> (<工坊ID>).ttsmod 命名;--datadir 提供 TTS 的 Mods 目录后,扩展名推导
才有本地缓存可查。契约:docs/schemas/ttsmod.md。
与「图包审批工具」松耦合联动:prepare 生成 approval.config.json(素材 id = 文件名,
零转换);status 调审批工具总览(服务在跑走 HTTP,否则走 python agent.py);gate 读审批结果做
发布门禁——全 pass 才允许打包 / 上传,不过时逐条列出拦截理由并退出码 1。
$ tts review prepare --pack ./packs/第七大陆 --deck 冒险牌堆 --b ./render
审批配置已写入:D:\...\packs\第七大陆\.tts\approval\approval.config.json
素材集 冒险牌堆:源A D:\...\packs\第七大陆\decks\冒险牌堆 → 源B D:\...\render
审批数据目录:D:\...\packs\第七大陆\.tts\approval\data
$ tts review gate --pack ./packs/第七大陆
门禁评估:素材集 冒险牌堆,共 70 条(pass 69 / reject 1 / flag 0 / 未审 0 / 过期 0)
拦截项 1 条:
003_正面.png:审批未通过(reject)
门禁未通过:请先处理上面的拦截项(重新审批后重跑本命令)(全部 pass 时输出 门禁通过:全部素材已审批通过,可以打包 / 上传 并退出码 0。)
阶段 4(窗口 D)新增常驻 hub 进程与 MCP 服务:hub 独占编辑器端口 39998,把 TTS 推送扇出给下游,并在 127.0.0.1:39995 暴露 HTTP+JSON 控制通道;status / pull / exec 在 hub 在线时自动委托,tts-mcp 通过同一控制通道把 10 个工具接入 MCP 客户端。
本节中 hub 的 JSON Lines 日志、端口监听与 curl 响应为 2026-10-05 在本机实测(TTS 未运行,只验证了协议与错误契约);CLI 中文提示来自
locales的cli.hub.*/cli.status.connectedViaHub/cli.pull.viaHub/cli.exec.viaHub/error.hub.delegateFailed键,按t()模板语义展示。
$ tts hub
{"ts":"2026-10-05T05:21:52.023Z","level":"info","msg":"hub started","editorPort":39998,"tcpPort":39997,"wsPort":39996}
control server listening on 127.0.0.1:39995
{"ts":"2026-10-05T05:21:52.024Z","level":"info","msg":"hub started","port":39995}
hub 已启动:控制通道 http://127.0.0.1:39995/v1;编辑器端口 39998;TCP 扇出 39997;WS 扇出 39996(Ctrl+C 停止)Ctrl+C(或 POST /v1/hub/shutdown)优雅停止,退出码 0:
^C
{"ts":"2026-10-05T05:22:07.494Z","level":"info","msg":"hub stopping"}
control server stopped
{"ts":"2026-10-05T05:22:07.495Z","level":"info","msg":"hub stopped"}
{"ts":"2026-10-05T05:22:07.495Z","level":"info","msg":"hub stopped"}
hub 已停止| 选项 | 缺省 | 说明 |
|---|---|---|
--port <n> |
39995 | 控制通道端口 |
--editor-port <n> |
39998 | 编辑器入站端口(TTS 主动连入;hub 期间独占) |
--tcp-port <n> |
39997 | TCP 扇出端口 |
--ws-port <n> |
39996 | WebSocket 扇出端口 |
--log-file <path> |
仅 stdout | 追加写日志文件;文件不可用时降级为仅 stdout 并记一条 warn |
端口分工:
| 端口 | 归属 | 方向与形态 |
|---|---|---|
| 39998 | 编辑器入站 | TTS → hub,独占绑定(与官方 VSCode 插件互斥,见 10.6) |
| 39997 | TCP 扇出 | hub → 下游,连入后持续收 JSON 串(无分隔符) |
| 39996 | WS 扇出 | hub → 下游,连入后持续收 JSON 文本帧 |
| 39995 | 控制通道 | 双向 HTTP+JSON,只绑 127.0.0.1 |
要点:
- 日志是英文 JSON Lines(
ts/level/msg+ 附加字段)写 stdout;控制通道自身的启停两行为纯英文文本行(control server listening on .../control server stopped),不是 JSON。 - 启动失败(任一端口被占)输出中文原因并退出 1;39998 被占的提示含 PID(文案与「与官方 VSCode 插件的互斥」一节相同),已启动的资源会被回滚。
- 停止顺序固定:控制通道(先拒新连接)→ 编辑器 + 扇出 → 日志文件流;SIGINT / SIGTERM /
/v1/hub/shutdown三路共用同一次停止流程(幂等)。 tts-hubbin 是tts hub的无选项薄入口(端口全取缺省值),适合写进脚本或服务自启。
tts status / tts pull / tts exec 每条命令执行前都会现场探测一次 hub(GET /v1/status,800 ms 超时,不缓存):
- hub 在线:委托执行——
pull/exec首行打印一行「经 hub」提示(分别为cli.pull.viaHub/cli.exec.viaHub的文案),status报告经 hub 的连接(含控制通道地址);实际动作在 hub 进程内完成(它持有 39998 的独占绑定)。pack pull/pack diff/pack push同样支持委托(阶段 5 已修复 hub 运行期 39998 二次绑定问题,见 §10.4)。 - hub 不在线:独立模式,与阶段 3 完全一致(临时独占绑定 39998 后直连 TTS)。
- 委托失败(hub 中途退出、协议错误等):输出
error.hub.delegateFailed并退出 1,不回退独立模式——hub 在线期间 39998 由 hub 持有,回退也绑不上端口。 - 其余命令(
assets / config / pack / deck / vcs / import / host / fetch / migrate / review)不经过 hub,行为不变。
$ tts status
已通过 hub 连接到 TTS 编辑器(控制通道 127.0.0.1:39995)
脚本引擎版本:MoonSharp 3.0.0.0
当前存档对象数:251
$ tts exec 'return 1+1'
hub 在线:本次执行委托 hub 完成
2(hub 在线但 TTS 还没连上 39998 时,status 给出提示并以退出码 1 结束。)
hub 收到 TTS 推送后,同一份 JSON 同时写入三路(单路断开自动摘除,不影响其他路):
| 通道 | 地址 | 数据形态 | 面向 |
|---|---|---|---|
| TCP 扇出 | 127.0.0.1:39997 |
JSON 串原样连写,无换行分隔 | 阶段 6 改版 VSCode 插件(纯字节转发) |
| WS 广播 | ws://127.0.0.1:39996/ |
一条消息 = 一帧文本(JSON) | 自研客户端 / 网页面板 |
| SSE | http://127.0.0.1:39995/v1/events |
data: <JSON> 行事件流 |
curl / 浏览器 EventSource 调试 |
$ curl -N http://127.0.0.1:39995/v1/events
:connected
data: {"messageID":2,"message":"hello from Lua"}- TCP 不写分隔符(S2 约定):接收方按 JSON 流自行增量解析;只服务本机(hub 绑 127.0.0.1)。
- WS 是手写极简 RFC6455:只接受
GET /+Upgrade: websocket,普通 HTTP 请求回 426;下行文本帧;上行只处理 Ping(回 Pong)与 Close,数据帧忽略——下游只收。 - SSE 连接先收到一行
:connected注释;只转发 6 类消息:GameLoaded / Print / Error / CustomMessage / GameSaved / ObjectCreated(不含PushNewObject与 exec 往返的ReturnValue);客户端断开即退订。 - 三路都只绑回环地址(实测 39995 / 39996 / 39997 均为
127.0.0.1)。
基线 http://127.0.0.1:39995/v1,12 条请求 / 响应路由 + 1 条 SSE(GET /v1/events):
| 方法 | 路由 | 作用 |
|---|---|---|
| GET | /v1/status |
hub 健康 + TTS 连接状态 |
| POST | /v1/scripts/pull |
拉全部脚本到工作区 |
| POST | /v1/scripts/save-and-play |
回写 scriptStates(供 push 用) |
| POST | /v1/exec |
执行 Lua 返回 JSON |
| POST | /v1/assets/check |
素材盘点 / 存活检测 |
| GET | /v1/packs |
列出注册表图包 |
| POST | /v1/deck/slice |
切片 |
| POST | /v1/deck/plan |
替换计划 dry-run |
| POST | /v1/import |
按 import.yaml 导入 |
| POST | /v1/diff |
本地 vs 游戏内差异 |
| POST | /v1/push |
写回并重载(必须 confirm:true) |
| POST | /v1/hub/shutdown |
优雅关闭 hub |
| GET | /v1/events |
SSE 事件流 |
统一约定:
- 只监听回环地址;构造时就拒绝
0.0.0.0/::等通配地址(绝不监听所有接口)。 - POST 请求体上限 1 MB(超限 413);
Content-Type必须是application/json(否则 415)。 - 错误统一
{error:{code,message,details?}}+ 4xx/5xx:
| code | HTTP | 触发 |
|---|---|---|
HUB_BAD_REQUEST |
400 | 缺字段 / 非法 JSON / 请求体不是对象 |
HUB_NOT_FOUND |
404 | 未知路由 |
HUB_METHOD_NOT_ALLOWED |
405 | 路径存在但方法不对(带 Allow 头) |
HUB_PAYLOAD_TOO_LARGE |
413 | 请求体超过 1 MB |
HUB_UNSUPPORTED_MEDIA_TYPE |
415 | Content-Type 不是 application/json |
HUB_CONFIRM_REQUIRED |
400 | /v1/push 未带 confirm:true |
HUB_PACK_ERROR |
400 | 图包业务错误(details.packCode 透传业务码) |
HUB_LUA_ERROR |
400 | Lua 运行时错误(details 带 guid / line / col / endCol) |
HUB_INTERNAL_ERROR |
500 | 其余异常(含 TTS 未连接) |
HUB_NOT_RUNNING |
—(客户端合成) | hub 不可达:CLI / MCP 客户端侧网络层失败,不是 hub 返回的 |
$ curl http://127.0.0.1:39995/v1/status
{"ok":true,"hub":{"editor":true,"tcpClients":0,"wsClients":0,"inprocClients":0,"startedAt":1791177712023,"uptimeMs":5020},"tts":{"connected":false}}
$ curl -X POST http://127.0.0.1:39995/v1/exec -H "Content-Type: application/json" -d '{"lua":"return 1+1"}'
{"error":{"code":"HUB_INTERNAL_ERROR","message":"无法连接 TTS(127.0.0.1:39999)。请确认游戏已启动并加载了存档。"}}
$ curl -X POST http://127.0.0.1:39995/v1/push -H "Content-Type: application/json" -d '{"root":"D:/packs/演示包"}'
{"error":{"code":"HUB_CONFIRM_REQUIRED","message":"this operation requires confirm:true in the request body"}}
$ curl -X POST http://127.0.0.1:39995/v1/hub/shutdown
{"ok":true}要点:
- 控制通道有意不暴露
pack export/pack import(.ttsmod)/pack sync-upstream/review(打包、上游合并与审批门禁不进 HTTP 面)。 /v1/deck/slice与/v1/deck/plan的请求体就是 CLI 同名选项(sheetPath/savePath/outDir等绝对路径);HTTP 场景无交互,多候选时 slice 报SLICE_AMBIGUOUS(HUB_PACK_ERROR),用deckKey/deckGuid消歧后重试。/v1/push与 CLItts pack push共用同一条完整安全流水线(阶段 5:备份 → 基线 / 素材检测 → 写回 → 回读校验 → 更新基线,见 §11.2);HTTP 层confirm:true必填。- ✅ 阶段 4 已知问题 #1(已修复,阶段 5):
/v1/scripts/pull与/v1/diff在 hub 运行期不再返回 500——pullFromGame/diffWorkspace增加可选会话注入(server参数复用 hub 已绑定的 39998),hub 路由传入daemon.server;细节见docs/schemas/hub-control.md§4.2 / §4.10。 - 完整契约(字段级)见
docs/schemas/hub-control.md(第 10 份契约文档)。
tts-mcp 是独立的 stdio MCP 进程(不经过 tts 主 CLI,自身不监听任何端口):每个工具调用 = 控制通道上的一次 HTTP 请求,需要先启动 hub。hub 未运行时工具返回 isError:true + {error:{code:"HUB_NOT_RUNNING",...}},不会倒退为本地模式。
$ tts hub # 先起 hub
$ tts-mcp # MCP 客户端按需拉起(stdout 是协议通道,诊断走 stderr)--lang zh-CN | en-US 指定工具标题 / 描述语言(非法值向 stderr 告警后按默认链回退,不阻塞会话)。
10 个工具:
| 工具 | 作用(对应路由) | 副作用 | 参数 |
|---|---|---|---|
tts_status |
hub + TTS 状态(GET /v1/status) |
只读 | 无 |
tts_pull |
拉全部脚本到工作区(POST /v1/scripts/pull) |
写工作区 scripts/、ui/ |
root |
tts_exec |
执行 Lua(POST /v1/exec) |
由 Lua 代码决定(游戏内) | lua;可选 guid、timeoutMs |
tts_assets |
URL 存活检测(POST /v1/assets/check) |
只读(联网探测) | urls(非空字符串数组);可选 timeoutMs |
tts_pack_list |
列出注册表图包(GET /v1/packs) |
只读 | 可选 packsRoot |
tts_deck_slice |
图集切片(POST /v1/deck/slice) |
写切片图到 outDir |
sheetPath、savePath、outDir;可选 deckKey、deckGuid |
tts_deck_plan |
替换计划 dry-run(POST /v1/deck/plan) |
只读 | savePath(路径或已解析对象)、rules(数组) |
tts_import |
按 import.yaml 导入(POST /v1/import) |
dryRun:false 时写工作区 |
root、manifestPath;可选 dryRun |
tts_diff |
本地 vs 游戏内差异(POST /v1/diff) |
只读 | root |
tts_push |
写回并重载:完整 push 流水线(POST /v1/push) |
写入游戏(危险) | root、confirm(必须字面量 true) |
要点:
tts_push是唯一有确认门的工具:调用侧 schema 就要求confirm: true(字面量),hub 侧再挡一次(缺了返回 400HUB_CONFIRM_REQUIRED);写回语义注意——scriptStates缺script/ui字段时 TTS 会删除对应内容。- ✅
tts_pull/tts_diff的 hub 运行期 500 问题已随阶段 5 会话注入修复(§10.4);tts_push现走 §11.2 的完整 push 流水线(备份 / 基线 / 素材检测 / 回读校验)。 - 工具结果都是结构化 JSON(英文键名,不走
t());失败体统一{error:{code,message,details?}}(HUB_LUA_ERROR时 details 带 guid / line / col / endCol)。 - MCP 会话期 stdout 只走协议;诊断 / 致命错误走 stderr JSON Lines(英文)。
ZCode 注册示例:编辑 C:\Users\<用户>\.zcode\cli\config.json(先备份原文件),把下面这段并入文件顶层(plugins / skills 等原字段保持不动):
{
"mcp": {
"servers": {
"tts": {
"command": "node",
"args": ["D:\\Codex\\TTS图包制作维护工具\\tts-toolkit\\dist\\mcp\\main.js"]
}
}
}
}(需要先 npm run build;可选在 args 里追加 "--lang", "zh-CN"。改完重启 ZCode 会话即可看到 10 个 tts_* 工具。)
hub 独占 39998 期间,官方 VSCode 插件(以及未走 hub 的其他工具实例)都连不上游戏;反过来官方插件先占用 39998 时,tts hub 会启动失败并输出含 PID 的中文提示。排查命令与阶段 1 相同:
Get-NetTCPConnection -LocalPort 39998 -State Listen | Select-Object OwningProcess阶段 6 会提供改版 VSCode 插件:它不再抢 39998,而是连 hub 的 TCP 扇出 127.0.0.1:39997(纯字节转发),届时两者可同时使用。在改版插件就绪前,使用 hub(或独立模式的任何命令)都要先关闭官方插件。
阶段 5(窗口 E)把「工作区 → 游戏」的写回链路补全:tts pack push 从只列清单的骨架变为完整安全流水线(默认 dry-run,--yes 才实写),新增 tts watch 文件监听自动 push 与 tts pack diff -u 逐行差异;tts pack pull 落盘后自动维护 .tts/baseline.json 安全基线。
口径来自阶段 5 Run 1 实现(
src/cli/commands/pack.ts、src/cli/commands/watch.ts、src/pack/push.ts、src/safety/*.ts);baseline 字段级契约见docs/schemas/baseline.json.md。本节命令输出为locales/zh-CN.json的实际文案(行内注明文案键;路径 / 时间戳为示意)。
$ tts pack push [--root <dir>] [--dry-run | --yes] [--force-scripts-only] [--no-backup] [--no-baseline-check] [--backup-retention <n>]
$ tts watch [root] [--dry-run | --yes] [--debounce <ms>] [--force-scripts-only] [--backup-retention <n>]
$ tts pack diff [--root <dir>] [-u, --unified]tts pack push 选项:
| 选项 | 缺省 | 说明 |
|---|---|---|
--root <dir> |
. |
图包工作区根目录 |
--dry-run |
开 | 试运行:走完整检测与过滤,只报告"将推送 / 跳过"多少;不备份、不写游戏、不写基线 |
--yes |
关 | 唯一实写入口(给了 --yes 则 dryRun=false);不给就是试运行 |
--force-scripts-only |
关 | 素材有改动时仍强制只推脚本(自担风险;改动清单记入结果的 assetChanges) |
--no-backup |
开(默认备份) | 跳过 push 前自动备份(不推荐) |
--no-baseline-check |
开(默认检测) | 跳过基线冲突检测(冲突记入结果的 baselineConflicts) |
--backup-retention <n> |
20 |
备份保留份数(1..100) |
tts watch:位置参数 root(缺省 .)、--debounce <ms>(缺省 300),其余旗标与 push 同名同义(--yes 打开自动实写)。
tts pack diff --unified(或 -u)在 modified 条目后追加逐行 unified hunks(@@ -本地起始,行数 @@ + - 本地行 / + 游戏侧行,含前后 3 行上下文);added / deleted 与归一化后超过 5000 行的条目不附 hunks。
tts pack push [--yes]
1. 校验 pack.yaml(缺失 / 损坏立即失败,不占端口)
2. 收集 scripts/*.lua + ui/*.xml 清单(空清单直接返回,不连 TTS)
3. 拉游戏侧快照 getScripts(独立模式临时独占 39998 一次;hub 在线走 /v1/push)
4. 素材改动检测 → 有 changed / added / deleted 且未 --force-scripts-only → 拒绝
5. 基线冲突检测 → 游戏侧相对基线被改过且未 --no-baseline-check → 拒绝
6. 过滤无变化对象(归一化后比较,CRLF / 结尾空行不算变化)→ 计入 skipped
7. 强制补齐 script / ui:本地没有的一半用游戏侧现有内容原样带上
(TTS 协议:缺字段 = 删除对应内容),name 一律用游戏内真实显示名
8. dryRun(默认)→ 到此返回:不备份、不发送、不写基线
9. 备份:游戏内全部 scriptStates → .tts/backups/<时间戳>/(--no-backup 可关)
10. 确认门:CLI 交互确认(--yes 直接放行;非 TTY 拒绝——写入路径宁可误拒)
11. saveAndPlay 写回(只发有变化对象)
12. 回读校验:再次 getScripts 与发送内容逐对象比对,不一致 → PUSH_VERIFY_FAILED,基线不更新
13. 更新基线:writeBaseline(回读快照) + touchLastPushAt
- 步骤 4 / 5 在备份之前:被拦截的 push 不产生备份、不动游戏、不动基线。
- 两条通路:hub 在线 → CLI 委托
POST /v1/push(hub 进程持有 39998,注入 server 复用、绝不二次绑定);hub 离线 → 独立模式,withEditorServer一次性包住「拉远端 → 备份 → 发送 → 回读」整段,用完即放。 - MCP 的
tts_push走 hub/v1/push(confirm:true两层门);响应体形{ok:true, dryRun, pushed, skipped, items(=pushed+skipped), backupDir?, baselineConflicts?, assetChanges?}(字段级契约见docs/schemas/hub-control.md§4.11)。 - 常见拦截错误码(stderr + exitCode 1):
PUSH_ASSET_CHANGES_DETECTED(素材改动)、BASELINE_CONFLICT(基线冲突)、PUSH_ABORTED(确认被拒)、PUSH_VERIFY_FAILED(回读不一致);另有PACK_NOT_FOUND/PUSH_FAILED/BASELINE_*/BACKUP_*。
$ tts pack push --root ./packs/第七大陆
推送试运行:将推送 1 个对象,跳过 25 个(未写回游戏) # cli.pack.push.dryRunSummary
$ tts pack push --root ./packs/第七大陆 --yes
推送完成:已写回 1 个对象,跳过 25 个;备份目录:D:\packs\第七大陆\.tts\backups\2026-10-05T13-38-41.252Z
# cli.pack.push.pushedSummary- 位置:
<root>/.tts/baseline.json(2 空格缩进 JSON + 末尾换行);tts pack init已把它写入图包.gitignore,工具内部状态、不入库。 - 内容:
entries[]记录各对象 script / ui 归一化后的 sha256(缺字段不记 hash);assetFiles记录文本素材清单的内容 hash;updatedAt/lastPushAt两个时间戳。 - 写入时机:
tts pack pull落盘后自动全量重写(保留旧lastPushAt);push --yes成功且回读校验通过后用回读快照重写并盖lastPushAt。dry-run / 被拦截 / 校验失败的 push 一律不写。 - 作用:push 前回答两个安全问题——① 游戏内脚本 / UI 是否被别人改过(基线冲突 → 先 pull 对账,避免覆盖他人改动);② 工作区素材是否漂移(素材改动 push 无法生效,必须先走离线回路)。
- 损坏语义:文件不存在 / JSON 损坏 / 结构不符一律按"没有基线"处理(首跑重建),不会卡死写入链路;只有"文件在但读不出"(权限等)才报
BASELINE_READ_FAILED。
{
"version": 1,
"packRoot": "D:\\packs\\第七大陆",
"updatedAt": "2026-10-05T13:38:41.252Z",
"lastPushAt": "2026-10-05T13:40:02.101Z",
"entries": [{ "guid": "-1", "name": "Global", "scriptHash": "336f365e…(64 位 hex)" }],
"assetFiles": { "decks/冒险牌堆/cards.csv": "aca7e28b…(64 位 hex)" }
}(完整字段、冲突判定规则与错误码见 docs/schemas/baseline.json.md。)
只覆盖三类文本清单(图片等二进制不进基线):
| 覆盖 | 路径(相对 root) |
|---|---|
| ✅ | decks/<卡堆目录>/cards.csv |
| ✅ | decks/<卡堆目录>/deck.yaml |
| ✅ | objects/objects.csv |
- 口径:utf8 读出 → 归一化(CRLF / 单独 CR 折成 LF + 去掉结尾空白)→ sha256;因此换行风格 / 结尾空行差异不算改动(避免假拦截)。
- 任一
changed/added/deleted都拦截;没有基线时当前全部素材都算added(首跑口径)。 - 被拦截时的修复四步引导(在错误文案内):
tts assets upload→tts pack build→ 游戏内加载新存档 → 再tts pack push;确要只推脚本用--force-scripts-only(改动清单记入结果,仍不会推素材)。
$ tts pack push --root ./packs/第七大陆 --yes
⚠️ 检测到素材改动(decks/ 或 objects/)共 1 处;push 协议不接收素材字段,此类改动无法通过热重载生效:
- decks/冒险牌堆/cards.csv
(退出码 1;随后是 assets upload → pack build → 加载存档 → 再 push 的四步引导) # error.push.assetChangesDetected- 原因(约束 7):push 协议(出站 messageID 1 / Save & Play)只接收
scriptStates,不接收任何素材字段,素材改动永远无法靠 push 生效。
$ tts watch ./packs/第七大陆 # 默认 dry-run:只报告将推送什么
$ tts watch ./packs/第七大陆 --yes # 静默期结束自动实写(--yes 即确认门)- 只监听
<root>/scripts(.lua)与<root>/ui(.xml)的 add / change / unlink;decks//objects/不在监听列表(素材改动本就该被 push 拦截)。 - 手写防抖:缺省静默 300 ms(
--debounce可调);push 进行中再来的事件合批为"本轮结束后补一轮",保证最后一轮改动不丢。 - 每一轮都是完整的 §11.2 流水线(hub 在线委托
/v1/push,离线独立模式);轮内失败(如素材改动 / TTS 正在加载存档)打印cli.watch.pushFailed后继续监听,不退出。 --yes是启动时给出的唯一确认门;不带给 dry-run,不写游戏、不产生备份。- Ctrl+C 打印退出提示并关闭 watcher。
$ tts watch ./packs/第七大陆 --yes
watch 已启动:监听 D:\packs\第七大陆(dry-run:false) # cli.watch.started
正在监听脚本/UI 变化(Ctrl+C 停止) # cli.watch.watching
检测到改动,开始推送… # cli.watch.pushing
推送完成:已写回 1 个对象,跳过 25 个;备份目录:… # cli.watch.pushOk
watch 已停止 # cli.watch.stopped(Ctrl+C)| 改动类型 | 生效通路 |
|---|---|
脚本 / UI(scripts/*.lua / ui/*.xml) |
tts pack push --yes(Save & Play 热重载;先备份 + 素材 / 基线检测) |
素材(图片、cards.csv / deck.yaml / objects.csv) |
tts pack build 出新存档 → 游戏内加载(push 会拦截素材改动) |
对象 / 牌堆数据(objects|decks/*/data.json) |
同素材:tts pack build(push 只写 scriptStates) |
- 约束 8(离线回路):
tts pack unpack把完整原始存档落成.tts/skeleton.json,tts pack build按 GUID 定点替换工作区改动,未改动部分与骨架逐字节一致——绝不"重新生成"存档。 - 约束 7(push 能力边界):push 只发脚本 / UI;素材改动必须走 build。两条通路互不替代,
tts pack diff -u用于写回前看清脚本 / UI 的逐行差异。
| 选项 | 说明 |
|---|---|
--lang <zh-CN | en-US> |
输出语言;缺省按「配置文件 lang > 系统语言 > 中文」解析。只接受这两个值(施工文档里的 --lang en 不被接受,会报「无效的语言选项」) |
--datadir <path> |
显式指定 TTS 数据目录,跳过自动探测 |
--dev |
开发模式:缺翻译时向 stderr 告警 |
-h, --help / -V, --version |
帮助 / 版本号 |
英文输出示例:tts --lang en-US status → Connected to the TTS editor (port 39998)。
TTS 的外部编辑器协议只认一个 39998 端口监听者。本工具与官方 VSCode 插件都会监听 39998, 同时开启会互相抢端口,表现为工具报错或 VSCode 插件失效。
-
使用本工具前:在 VSCode 中停用 / 卸载 TTS 插件,或关闭 VSCode;
-
报错文案:
端口 39998 已被占用(PID xxxx)。常见原因:VSCode 的 TTS 插件正在运行,请关闭后重试。 -
排查占用进程(PowerShell 7):
Get-NetTCPConnection -LocalPort 39998 -State Listen | Select-Object OwningProcess
本工具对 39998 采用独占绑定(exclusive: true),并会先试绑通配地址再试绑 127.0.0.1,
以避免 Windows 上 SO_REUSEADDR 造成的「绑定成功但收不到消息」假象。
只有联网的探测 / 下载会用到代理:tts assets --check、tts host check、tts fetch、tts migrate,以及 tts pack export 的扩展名 HTTP 探测(src/assets/check.ts / src/assets/fetch.ts / src/archive/detect.ts 在调用时读取
https_proxy,其次 HTTPS_PROXY,空值视为未设置)。TTS 协议本身始终连接
127.0.0.1:39999,不受代理影响。
本机代理为 http://127.0.0.1:7897:
:: cmd.exe
set https_proxy=http://127.0.0.1:7897
set http_proxy=http://127.0.0.1:7897
tts assets --check# PowerShell 7
$env:https_proxy = "http://127.0.0.1:7897"
$env:http_proxy = "http://127.0.0.1:7897"
tts assets --checknpm install 同样受上述环境变量影响,网络受限时先设置再安装。
TTS 数据目录(Mods)的位置由玩家游戏内设置决定,安装目录与用户文档目录都可能存在, 因此本工具绝不写死单一路径(坑 6),而是按优先级列出全部候选:
- 显式指定:
--datadir <path>或tts config datadir --set <path>; - 全局配置文件
%APPDATA%\tts-toolkit\config.yaml的datadir字段; - 注册表 / 游戏内
ConfigMods\Location(本阶段未实现,TODO); - 安装目录
<安装目录>\Tabletop Simulator_Data\Mods(安装目录来自环境变量TTS_INSTALL_DIR,未设置时依次尝试三个常见 Steam 库路径); %USERPROFILE%\Documents\My Games\Tabletop Simulator\Mods;- macOS / Linux 对应路径(本阶段未实现,仅留 hook)。
候选「有效」的判定:目录存在且 Workshop / Images / Saves 中至少一个子目录存在。
有效候选 ≥ 2 时要求用户选择;恰好 1 个时推荐并二次确认;0 个时报错退出。
- VSCode 插件改版未提供:hub(阶段 4)已落地并独占 39998,官方 VSCode 插件在 hub 运行期间不可用;连 hub TCP 扇出(39997)的改版插件属于阶段 6(见 §10.6)。
- push 协议不接收素材字段(约束 7):
tts pack push只写脚本 / UI,素材改动会被 检测并拒绝(除非显式--force-scripts-only,自担风险);素材必须走tts pack build→ 游戏内加载新存档,见 §11.6。 - 注册表探测未实现:无法读取游戏内设置的真实 Mods 路径,只能靠上述候选与用户指定。
- macOS / Linux 探测未实现:仅保留 hook。
tts --help的文案不跟随--lang:命令描述在模块加载时求值,早于--lang生效。--lang只接受zh-CN/en-US:--lang en会以「无效的语言选项」退出 1。- 协议限制:Lua 多返回值不受支持;table 必须经
JSON.encode包装(exec已自动处理); 端口 39998 同时只能有一个监听者。
npm run dev -- status # tsx 直接跑源码(tsx 免构建)
npm test # vitest:单元测试(集成测试默认跳过)
npm run test:integration # 只跑 tests/integration
npm run build # tsc -p . → dist/目录结构:
src/
protocol/ 消息类型、端口与独占检测、编辑器服务器(39998)、TTS 客户端(39999)
session/ Lua 执行(execJson)、脚本快照读写、Lua 片段生成
assets/ snake_case ↔ CamelCase 字段映射、素材盘点、URL 存活检测、下载修复与迁移(阶段 3)
datadir/ TTS 数据目录探测与配置读写
i18n/ 极简 t(key, params) 双语实现
pack/ 图包工作区:layout / packyaml / manifest / init / unpack / pull / push / diff / build / import(阶段 2A / 3 / 5)
safety/ 写入路径安全链:push 前备份 / 基线 hash 与冲突检测 / 交互确认门(阶段 5)
deck/ 卡牌图集切片 / 拼接 / 校验、cards.csv 与 objects.csv(阶段 2B)
vcs/ git 语义化 status / commit / verify / lfs / size(阶段 2C)
archive/ .ttsmod 读写、TTS 缓存键、扩展名三级推导(阶段 3)
host/ 图床统一接口与四种内置实现:steamcloud / s3 / local / command(阶段 3)
review/ 与图包审批工具的联动:prepare 配置 / status 调用 / gate 门禁(阶段 3)
hub/ hub 守护进程:编辑器入站 + TCP/WS 扇出 + S2 控制通道(阶段 4 / 5)
mcp/ MCP stdio 服务与 10 个工具(阶段 4)
cli/ commander 命令注册(status / config / pull / exec / assets / pack / deck / vcs / import / host / fetch / migrate / review / hub / watch)
docs/schemas/ 契约文档:pack.yaml / deck.yaml / assets.yaml / cards.csv / objects.csv / registry.yaml
+ 阶段 3 的 import.yaml / host / ttsmod + 阶段 4 的 hub-control + 阶段 5 的 baseline.json(共十一份)
locales/ zh-CN.json、en-US.json
tests/unit/ 不依赖 TTS 的单元测试
tests/integration/ 验收骨架(phase1 需 TTS;phase2b / 2c / 3 / 4 / 5 为逐用例 it.skip 清单)
约定:TypeScript ESM(.ts,import/export)、target ES2022 / module NodeNext / strict;
面向用户的字符串一律走 t();禁止 any(运行时边界用 unknown + zod);注释用中文。
tests/integration/phase1.acceptance.test.ts 对照《施工流程》阶段 1 验收标准,
通过子进程调用已构建的 CLI。因为需要真实 TTS,默认整组 describe.skip,
npm test 与 npm run test:integration 都会跳过它(输出 10 skipped)。
tests/integration/phase2b.acceptance.test.ts / phase2c.acceptance.test.ts /
phase3.acceptance.test.ts / phase4.acceptance.test.ts /
phase5.acceptance.test.ts 是后续阶段的验收骨架:
逐用例 it.skip(不是 describe.skip),
每个用例体只有 TODO 与一条 todo(...) 守卫(未实现就打开会明确失败,不给假绿)。开启方式:
按用例内 TODO 装配夹具后删掉该用例的 .skip,再跑 npx vitest run tests/integration/<文件>
(或 npm run test:integration)。phase3 的 12 个场景覆盖 import / host / migrate / pack export /
pack import / 扩展名推导 / review 门禁 / sync-upstream,多数场景全离线;
phase5 的 10 个场景覆盖离线 build→unpack 回路 / pull 写 baseline / push dry-run 与
--yes 实写 / 原样回写零副作用 / marker 生效 / 素材改动拦截与强制绕过 / 基线冲突 / watch 自动 push。
手动开启 phase1 的步骤:
- 确保已
npm run build; - 启动 TTS 并加载图包存档;
- 关闭 VSCode 的 TTS 插件;
- 打开
tests/integration/phase1.acceptance.test.ts,把describe.skip(改成describe(; - 执行
npm run test:integration。
基准值(本机实测):36 个 .lua + 1 个 .xml、脚本总量 277,644 字符(断言允许 ±5%)、
251 个对象、130 个不同素材 URL。存档改动后如数字变化,更新文件顶部的基准常量即可。