Source:输入源
Source Adapter 是 Cage 的输入插件。每种输入格式一个 Adapter,统一产出 Canonical Model。
Cage Core
|
+------------+-------------+
| | |
Excel JSON YAML
| | |
+------------+-------------+
|
Canonical Model重要原则:
Source Adapter 只负责读取和解析,不负责游戏业务逻辑。
文本类源在解析期做基础类型推断(如 CSV 里 100 推断为整数、true 推断为布尔),Schema 随后在 L2 按目标类型严格校验(族不匹配即 E1101,不做字符串↔数字重校准——数值文本写进 String 列会报类型错误);Source 不做业务校验、不做转换决策。
第一阶段支持
Excel
CSV
JSON
YAML
MessagePackExcel
Excel 是游戏行业非常常见的 Authoring Source,但它不是 Cage 的核心抽象。
例如:
Item.xlsx
Monster.xlsx
Skill.xlsx
Quest.xlsxExcel Adapter(基于 calamine 读取 .xlsx/.xlsm/.ods)可以处理:
- Workbook
- Worksheet
- Header
- Cell
- Row
- Column
- Merged Cells(合并单元格回填;仅
.xlsx,.ods不做) - Formula(公式缓存值,不重算)
- Cell Type
表级元数据(Sheet Metadata):隐藏 sheet(hidden / veryHidden)是作者的草稿/暂存面,解析前即跳过、不进 Canonical Model(2026-10-12 拍板,design §3.1)。单元格 注释(Comment)明确不做——注释是作者批注不是数据,管线零消费者; 重开条件与理由见同一拍板。
错误定位精确到单元格(诊断 Source 字段渲染形如 monster.xlsx | Sheet: Monster | Row: 27 | Col: G)。
最终转换成:
Document
|
+-- Table: Item
+-- Table: Monster
+-- Table: SkillCSV
- 以表头行定义列名,行为数据行
- 定位到表头名 + 行号
- 解析期做类型推断:
true/false/yes/no→ Bool,整数与浮点文本 → Int/UInt/Float,空单元格 → Null,其余 → String;Schema 在 L2 按目标 类型严格校验(数值文本进 String 列会报E1101,引号是 CSV 语法、 不改变内容类型)
JSON / YAML
- 天然树形结构,直接映射到 Canonical Model
- YAML 根形态:顶层 Sequence(元素为 Mapping)按多表读,顶层 Mapping 按单行表读,null 跳过;支持
<<: *anchor合并键展开 - 保留路径定位:文本格式错误(YAML 语法错误等)精确到行号(错误码
E0001族)
MessagePack
- 文件形态即 msgpack target 的线格式:裸行对象数组
[{"id": 1, "name": "Sword"}, …];二进制不带表名,文件名 stem 命名表(CSV 惯例) - 浮点按位还原、
bin载荷还原为Bytes、超出i64::MAX的整数保留UInt族——JSON 往返无法表达的三类值;非负整数归一为Int(与 JSON 源一致:线格式的正数标记不携带符号性) - 空数组产出空表(不跳过),保证 target → source 往返完整
- 形状违规(根非数组、行非对象、尾随字节、截断帧)按
E0001/E0004报错
源目录装载范围
[source_roots] 目录发现收集 json / yaml / yml / csv / xlsx / xls / msgpack 扩展名;其中 .xls 旧格式被收集后按 E0001 拒绝, .xlsm/.ods 不在目录收集清单内(目录模式下被静默忽略——单文件 schema_path/source 显式指定时适配器本身支持 .xlsx/.xlsm/.ods)。 远程源根(registry:,见 CLI · registry)同样可作 [source_roots] 取值。
HTTP API(远程源,S1)
[source_roots] 直写 http(s) URL 即远程源:GET 响应字节原样落 .cage-cache/source/<URL 指纹>/,再走与本地 JSON 完全相同的解析 ({表名: 行数组} / 单对象 → Root / 行数组 → Data),L0-L7 全量校验、无旁路。连接类失败有界重试;取数失败报 E1901, 401/403 报 E1902,坏 JSON 报 E0001(与本地文件同一诊断)。 四源形态定稿见 design §45。
分页(S7):响应带 RFC 8288 Link: <...>; rel="next" 头时沿链取页 ——每页必须是 JSON 行数组,页序拼接成一份合并文档(裸数组 → Data 表)落缓存;相对 next 目标对页 URL 解析,回环与超过 1000 页 均定错(E1901)。无 next 头的响应保持单 GET 契约逐字节不变。 限流协商:429 带 Retry-After(秒数或 HTTP-date)且 ≤ 30s 时按其 退避重试(与传输重试共用预算),缺失 / 不可解析 / 超上限即时 E1901 定错。分页契约违规与限流定错不参与离线回退——只有传输类 失败(服务器不可达)才回退缓存(E1906 WARNING)。
条件 GET 增量拉取(S8,文档级):响应带 ETag / Last-Modified 头时,验证器随字节一起存进缓存旁的 sidecar meta (.cage-cache/source/<URL 指纹>/<URL 指纹>.meta.json);同 URL 再次取数时先带 If-None-Match / If-Modified-Since 发条件请求, 服务器回 304 则缓存字节原样复用(连 meta 都不重写),200 则新 字节新验证器双双替换。meta 与 payload 必须同时存在才做 revalidation——缺一半即按无缓存走普通 GET 重建。只有首页 GET 带 条件(首页的验证器描述整个集合,304 时分页链不必重走); --no-cache(strict)依然 revalidate——304 是服务器确认缓存, 不是缓存自答。传输失败仍走 S4 离线回退语义,304 不参与。行级 增量(按 revision / updated_at 取变更行)仍留待实现期。
MySQL / PostgreSQL(远程源,S2)
[source_roots] 写 mysql:<表|具名查询> / pg:<表|具名查询>: 具名查询取自 [remote.<scheme>].queries,否则名字必须是安全表名 并展开为 SELECT * FROM <name>。装载期双重只读保险——静态 SELECT 白名单(单条、SELECT 开头,拒分号 / 注释 / 行锁 / INTO / CTE, E1905)先跑,之后才解析 [remote.<scheme>].dsn_env 指名的 环境变量(未声明 / 未设置报 E1904,DSN 永不进 cage.toml), 连接后会话钉只读(MySQL SESSION TRANSACTION READ ONLY、PG default_transaction_read_only)。连接按 DSN 进程级池化(design §45 落地 2026-10-13):同一服务器的 N 个源共享一次握手,每次取到 的会话照旧钉只读,取数失败的会话弃用不回池。类型口径:DECIMAL / NUMERIC 文本保真不走 Float,NULL → Null,二进制列 base64,其余按列类型 映射为 JSON 标量。行集序列化成 canonical JSON 落 .cage-cache/source/<scheme+DSN+SQL 指纹>/,再走标准 JSON 解析 ——与 HTTP 源同一缓存锚与解析链。行序:带 ORDER BY 尊重原序, 否则按行序列化形式排序(构建不依赖服务端返回顺序)。连接 / 语句失败报 E1901。
库表内省 Schema 草稿(S9,design §45)
cage schema-draft <工程> <spec>... [-o <文件>] 把库表的形状 落成一份可审阅的 schema 草稿(见 cli):每个 mysql:<表> / pg:<表> spec 经只读会话内省 information_schema(表名绑定为参数、永不拼接进 SQL),渲染出表、 主键与 NOT NULL → required: true;DECIMAL / 日期时间 / JSON / 未知类型在行内注释里标出「需要作者拍板」,无主键表以空 primary_key: [] 加注释提醒。类型口径与适配器一致——DECIMAL / temporal / JSON 草稿为 String,正是它们在缓存 JSON 里的实际形态。 渲染确定性:同一次内省永远同一段字节(头部无时间戳)。凭据与只读 纪律同 S2:DSN 从 [remote.<scheme>].dsn_env 读(E1904),表找 不到报 E1901。
Google Sheets(远程源,S3)
[source_roots] 写 gsheet:<spreadsheet_id>/<tab>,凭据是 API key, 从 [remote.gsheets].credential_env 指名的环境变量读(未声明 / 未设置报 E1904,key 永不进 cage.toml 也不落任何日志——错误诊断只 引 spec)。取数走 Sheets API v4 values, valueRenderOption=UNFORMATTED_VALUE(公式缓存值,不重算,同 Excel adapter 口径)。映射同 Excel 惯例:tab = 表、首行 = 表头(空表头 单元格退 col<i>)、空行跳过、短行补 null 对齐表头宽,tab 自然行序 = 作者承诺序原样保留。单元格初值为字符串(数字 / 布尔保留 JSON 文本),类型交 Schema 校准。响应形状门(非行集 / 空表头 / majorDimension 非 ROWS)报 E1903;401/403 报 E1902,取数失败报 E1901。canonical JSON 落 .cage-cache/source/<gsheets+id+tab 指纹>/(API key 不进指纹—— 它不改变字节语义),再走标准 JSON 解析。
离线回退与 --no-cache(远程源通用,S4)
三个远程源共享同一条离线语义:传输类取数失败(连接拒绝 / DNS / 超时 / 服务不可达)时,若上一份缓存副本还在 .cage-cache/source/,构建不失败——回退解析那份副本,stderr 打 E1906 WARNING(warning: E1906 remote source unreachable, serving the previous cached copy: <spec> (cache: <路径>))。两类失败永不 回退:404(远端已删除)与 401/403(访问可能已被吊销)——旧字节会 静默出错,保持硬失败(E1901 / E1902)。DB 源的连接 / 语句失败 按传输类回退;凭据(E1904)与查询白名单(E1905)在任何网络触达 之前发生,旧字节不掩盖配置错误。回退出的缓存文件走与在线路径同一条 标准 JSON 解析与全量校验,无旁路。
--no-cache(check / build / gen / inspect 四命令)关闭 回退:远端取不到即失败,即使有缓存副本。适合「必须以远端最新字节 构建」的发布前核对。
后续扩展(非首期)
XML
TOML
SQLite
Custom Binary以上格式不在首期交付范围内;首期四大远程源(HTTP / MySQL / PostgreSQL / Google Sheets)已随 S1–S5 全量交付,详见上文各节。
Rust 落地
| 输入源 | crate | 主要依赖 |
|---|---|---|
| Excel | cage-source-excel | calamine |
| CSV | cage-source-csv | csv |
| JSON | cage-source-json | serde_json |
| YAML | cage-source-yaml | serde_yaml |
| MessagePack | cage-source-msgpack | rmpv |
| HTTP API | cage-source-http | cage_core::remote(取数 / 重试 / 缓存)+ cage-source-json |
| MySQL / PostgreSQL | cage-source-db | mysql / postgres(纯 Rust 协议客户端)+ cage_core::remote(缓存键)+ cage-source-json |
| Google Sheets | cage-source-sheets | cage_core::remote(取数 / 重试 / 缓存)+ cage-source-json |
统一产出 cage-core 的 Canonical Model:Null / Bool / Int / UInt / Float / String / Bytes / Array / Object,并携带 Source Location 与元数据。