mirror of
https://git.openapi.site/https://github.com/desirecore/market.git
synced 2026-09-06 19:43:51 +08:00
fix(invoice-organizer): 钉死三个索引文件的顶层结构 / pin the index file schemas (#127)
## 问题 / Problem
真机对比同一个 Agent 两个版本产出的索引文件:
```
v1.0.0 ledger.json {schemaVersion, updatedAt, records:[…]} 数组
v1.0.1 ledger.json {schemaVersion, invoices:{…}, suspectedDuplicates, quarantined} 字典
v1.0.0 files.json {schemaVersion, updatedAt, files} emails.json {schemaVersion, updatedAt, emails, note}
v1.0.1 files.json {schemaVersion, files} emails.json {schemaVersion, emails}
```
技能原文只写了一句「`ledger.json` 发票主键 → 记录(主索引)」——**容器键名从未钉死**,于是同一个 Agent
在不同轮次产出了两种不兼容结构,`updatedAt` 也时有时无。
台账、报告、幂等判据**全部从索引派生**,键名一漂,跨轮次和外部工具就都对不上。这与已修的归档路径那条同源:**规格没说死的地方就会漂**。
The skill only said "`ledger.json` maps invoice key to record" — the
container key was never pinned, so the same agent produced two
incompatible shapes across runs. Everything (ledger spreadsheet, report,
idempotency) derives from these index files.
## 改动 / Changes
- 把三个索引文件的顶层形状写成 jsonc 范例,**容器键名与嵌套形态钉死**(`invoices` / `files` /
`emails` 都是字典,外加 `schemaVersion` 与 `updatedAt`;`ledger.json` 另有
`suspectedDuplicates` 与 `quarantined`)
- 明确 **「读到旧形态时不要重写容器」**:顶层出现 `records` 就一路沿用
`records`,只增改里面的条目。理由写进技能——容器一换,旧版写过、当前规格没覆盖的字段会**静默丢掉**,而用户毫无察觉
- 判据给成一句可照做的话:`records` → 沿用 `records`;`invoices` → 用 `invoices`;新建才用
`invoices`
## 为什么不做自动迁移 / Why no auto-migration
升级路径已实测(v1.0.0 目录被 v1.0.1 接手):Agent
**读得懂旧结构并原样保留**,台账记录数不变、幂等成立,还顺手修正了历史上错误的归档路径。既然读兼容已经成立,把容器强行规范化只会带来「丢字段」风险而没有收益。本
PR 把这个已被验证正确的行为**写成规则**,而不是引入新行为。
The upgrade path was verified in a real run: v1.0.1 reads the legacy
shape and preserves it, with zero duplicate ledger entries. This PR
codifies that verified behavior instead of introducing migration.
## 版本与 pin / Version and pin
同 #120 / #121 的两步发布约束:本 PR 只改技能内容,**不动版本号与 `contentSource.ref`**——pin
必须指向已合并的 commit,那个 SHA 在本 PR 合并前不存在。版本号、pin 与 changelog 留到后续 PR。
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -275,6 +275,39 @@ IMAP 的两个坑:`messageId` 用 `"imap:<uid>"` 形式(列表返回的 `id`
|
||||
└── raw/<sha256>.json 单文件原始解析输出
|
||||
```
|
||||
|
||||
### 三个索引文件的顶层结构(硬规则,不许自己另起一套)
|
||||
|
||||
**新建**时必须是下面这个形状。容器键名与嵌套形态都钉死——台账、报告、幂等判据全部从这里
|
||||
派生,键名一漂,跨轮次和外部工具就都对不上:
|
||||
|
||||
```jsonc
|
||||
// .index/ledger.json —— 发票主键 → 记录(是**字典**,不是数组)
|
||||
{ "schemaVersion": 1, "updatedAt": "<ISO8601>",
|
||||
"invoices": { "<发票主键>": { /* 单张发票的记录 */ } },
|
||||
"suspectedDuplicates": [ /* 疑似重复,交人工确认,不并入 invoices */ ],
|
||||
"quarantined": [ /* 隔离项摘要,与 _quarantine/ 里的 .reason.txt 对应 */ ] }
|
||||
|
||||
// .index/files.json —— 文件 sha256 → 解析结果
|
||||
{ "schemaVersion": 1, "updatedAt": "<ISO8601>",
|
||||
"files": { "<sha256>": { /* 该文件的解析结果与去向 */ } } }
|
||||
|
||||
// .index/emails.json —— 已处理邮件 id
|
||||
{ "schemaVersion": 1, "updatedAt": "<ISO8601>",
|
||||
"emails": { "<provider>:<email>:<mailId 原值>": { /* 处理结论 */ } } }
|
||||
// mailId 一律用列表接口返回的 id **原值**,不做任何清洗。IMAP 的 id 本身就带
|
||||
// "imap:" 前缀,所以它的 key 长这样(前缀出现两次是对的,别"修正"):
|
||||
// "imap:me@example.com:imap:123"
|
||||
// "gmail:me@example.com:18f2c9a1b7e4"
|
||||
```
|
||||
|
||||
**读到旧形态时不要重写容器。** 历史目录里 `ledger.json` 可能是
|
||||
`{schemaVersion, updatedAt, records: [ … ]}`(**数组**)。这种目录要**原样读、原样写回**,
|
||||
只增改里面的条目——不要顺手"规范化"成 `invoices`。理由:容器一换,本轮之外的字段
|
||||
(旧版写过、当前规格没覆盖的)就会静默丢掉,而用户毫无察觉。判据很简单:
|
||||
**顶层出现 `records` 就一路沿用 `records`,出现 `invoices` 就用 `invoices`,新建才用 `invoices`。**
|
||||
|
||||
实测踩过:同一个 Agent 在不同轮次分别产出过这两种不兼容结构,`updatedAt` 也时有时无。
|
||||
|
||||
产物必须落在**已登记的工作目录**里。Agent 的 AgentFS 私有目录不在文件工作台的可见范围内,别把台账放那儿。
|
||||
|
||||
用 `Write` 落盘,不要用 Bash 重定向——`Write` 会产生「本轮修改了哪些文件」卡片,用户能直接点开台账;Bash 写的文件不会。
|
||||
@@ -368,7 +401,7 @@ IMAP 的两个坑:`messageId` 用 `"imap:<uid>"` 形式(列表返回的 `id`
|
||||
|
||||
### 多邮箱
|
||||
|
||||
`accounts-with-settings` 返回多个账户时,默认全都扫,收集阶段按账户循环,`emails.json` 的 key 用 `<provider>:<email>:<mailId>` 避免不同账户的 id 撞车。用户明确指定某个邮箱时只扫那个。
|
||||
`accounts-with-settings` 返回多个账户时,默认全都扫,收集阶段按账户循环,`emails.json` 的 key 用 `<provider>:<email>:<mailId 原值>` 避免不同账户的 id 撞车。**`mailId` 用列表接口返回的 id 原值,一个字符都不改**——IMAP 的 id 自带 `imap:` 前缀,拼出来就是 `imap:me@example.com:imap:123`,前缀出现两次是对的。如果这一轮抄原值、下一轮又把前缀剥掉,同一封邮件会产生两个 key,「已处理」判定直接失效、邮件被重复入账。用户明确指定某个邮箱时只扫那个。
|
||||
|
||||
### 多工作目录
|
||||
|
||||
|
||||
Reference in New Issue
Block a user