新增官方 inline Agent「发票整理助手」(`invoice-organizer`)。
Adds an official inline Agent, **Invoice Organizer**, that turns
invoices scattered across
mailboxes and local folders into a reconcilable, reusable ledger.
## 它做什么 / What it does
七步固定流程,每一步幂等:**接入检查 → 收集 → 解析 → 去重 → 归档 → 台账 → 报告**。
- **收集**:从已接入的邮箱(Gmail / Outlook / IMAP)找出候选发票邮件,附件用 `MailOperations` 的
`save_to` 直接落盘,base64 不进模型上下文
- **解析**:OFD(三个结构化来源分别处理)、PDF 文字层、扫描件走视觉;每个字段带 `extractedBy` 与置信度
- **去重**:发票号码为主键,跨格式识别同一张票(PDF 与照片、重复下载);近重复不自动合并
- **归档**:`归档/年/月/YYYYMMDD_销售方_金额_发票号码.ext`,原件一个不动
- **台账**:xlsx 四张表(明细 / 月度汇总 / 按销售方汇总 / 异常),发票号码强制文本格式;
装不上 openpyxl 时降级为 UTF-8 BOM CSV
- **报告**:Markdown 月度报告,收尾固定报五个计数(范围 / 候选 / 成功 / 待复核 / 失败)
- **自动化**:邮件规则 `agent_handle` + `ManageSchedule` 定时出账
## 明确的能力边界 / Explicit boundaries
- **不做发票真伪查验**,也不暗示做过——只做形式校验与勾稽校验,给出官方查验平台入口让用户自己核验
- **不删除、不移动、不转发用户的邮件**
- **票面内容不外流**:不外发、不代发、不上传第三方接口或在线查验站点
- **不做汇率换算**;不承诺「已找全」,只报「在给定范围内找到 N 封候选、成功解析 M 张」
## 结构 / Structure
```
agents/invoice-organizer/
├── agent.json persona.md principles.md LICENSE
├── USAGE.zh-CN.md USAGE.en-US.md
├── catalog-metadata.v1.json # availability: listing-only(可安装化见下)
├── assets/avatar.webp
└── skills/
├── invoice-workflow/ # 七步总纲、目录布局、落盘顺序、去重主键、幂等
├── invoice-extract/ # 三种载体的解析细则、置信度分档、特殊票据
│ └── references/票面文本形态.md
├── invoice-ledger/ # 台账结构、人民币约定(覆盖 xlsx 技能的默认口径)
│ └── references/月度报告模板.md
└── invoice-automation/ # 邮件规则与定时调度的参数模板与排查
```
`manifest.json` 的 `stats.totalAgents` 3 → 4。
## 事实性核对 / Fact-checking
技能里引用的**每一个**端点 / 工具名 / 参数名 / 返回字段都对着 DesireCore 主仓库实现逐条核对过,
并经过一轮对抗式 review。review 抓到的、已修正的主要事实错误:
- OFD 一节原本只覆盖 2020 年式样;已改为按**三个来源**分述
(内嵌附件 / 2024 数电票的 `Tags/CustomTag.xml` 标引 / `DocInfo/CustomDatas`),
键名从 `Buyer/BuyerName` 改为真实的点号路径 `Buyer.BuyerName`,置信度按来源分档
- **`DocInfo/CustomDatas` 的「合计金额」是不含税金额**,误当 `totalAmount` 会让每张 2024
数电票少记税额
——已写成硬规则
- Gmail 本地缓存搜索的 `q` **实际只按主题过滤**(正文过滤在主题收窄之后才跑),
原文写成「搜主题与正文」会导致静默漏邮件
- 「轮询只覆盖收件箱」只对 IMAP 成立,Gmail / Outlook 是整个邮箱
- `POST /rules/{id}/test` 走另一份内联实现、**没有 `matches_regex` 分支**,
不能用它验证正则规则
- 邮件列表项里 Gmail / IMAP **是带** `attachments[]` 的,只有 Outlook 不带;
`labelIds` 是 Gmail 专有
## 真机验证 / Verified on a live instance
在 dev 实例上以一句话指令处理 31 个混合文件(数电票 / 旧版票 / OFD / 扫描件 / 行程单,
外加重复、近重复、作废、零额与 4 个非发票负样本):
- 23 张归档,字段与夹具 ground truth **逐条吻合**
- 4 个非发票**全部正确拒绝**并写明理由(施工许可证 / 对账单 / 技术服务合同 / 邮件通知)
- 4 个跨格式重复(3 张扫描件 + 1 次重复下载)**全部靠发票号码主键识破**,隔离而非删除
- 近重复正确未合并;作废票归档但不计入合计;低置信度定额发票标为待复核
- 台账 xlsx 四张表、报告含五个计数、`SendUserMessage` 带附件交付
- `待整理/` 31 个原件一个未动
## 校验 / Validation
```
validate_catalog_metadata.py --require-complete 0 error, agents=4, sidecars=74 exit=0
validate-i18n.py 0 error exit=0
translate.py --check exit=0
gen-collection-children.py --check exit=0
```
129 条 warning 全部来自 `skills/*` 的存量条目,改前改后一字不差,`invoice-organizer` 零命中。
另核实:`agent.json` 过 `marketAgentSchema`(详情页)与收窄后过
`agentConfigSchema`(安装),
persona / principles 的 6 个 canonical key 用平台真实解析器全部解得出,全树敏感信息扫描通过
(所有公司名 / 税号 / 银行账号均为 `示例`/`示范`/`虚构`/`样例` 前缀的合成值)。
## 后续 / Follow-up
本 PR 为 `availability: listing-only`。可安装化需要把 `agent.json#contentSource` 与
sidecar 的 `provenance.content` **逐字一致地** pin 到本 PR 的合并 commit,
并补 `governance.compliance` 与 `timestamps.reviewedAt`——那是紧接着的第二个 PR。
DesireCore Market
DesireCore 官方市场仓库,存放官方维护的 Agent/Team/Skill 定义,以及经过整理的第三方 Skill 入口。
Repository Shape
.
├── manifest.json # Market metadata, supported locales, aggregate stats
├── categories.json # Category registry and localized labels
├── builtin-skills.json # Built-in local SKILL.md skills
├── agents/
│ ├── desirecore/
│ │ └── agent.json
│ └── <agent-listing>/
│ ├── agent.json
│ ├── catalog-metadata.v1.json
│ └── USAGE.md # Optional usage notes (USAGE.<locale>.md for variants)
├── teams/
│ └── <team>/
│ └── entry.json
└── skills/
├── <local-skill>/
│ ├── SKILL.md
│ └── SKILL.<locale>.md
└── <external-entry>/
└── entry.json
The market currently contains:
2Agents:desirecore,dingtalk-workspace1Team:contract-review-team40local built-in skills withSKILL.md29external skill entries withentry.json69publishable skills in total (SKILL.md+entry.json)
Skill Sources
Local built-in skills are installable from this repository and must be listed in builtin-skills.json:
baidu-poi-search, ccgp-gov-procurement, clone-agent, cnipa-patent-search, code-intelligence,
configuring-compute, create-agent, creditchina-query, dashscope-image-gen, delete-agent,
dev-environment-setup, discover-agent, docx, frontend-design, guizang-ppt,
image-to-image, mail-operations, manage-skills, manage-teams, markdown,
minimax-music-gen, minimax-video-gen, multi-source-sentiment, nodejs-runtime, pdf, pptx,
presentation-forge, python-runtime, registering-services, s3-storage-operations, skill-creator,
tech-diagram, tianyancha-risk, update-agent, using-services, web-access, workflow, workforce-optimization,
xiaomi-tts, xlsx
builtin-skills.json#retired lists old built-in Skill IDs that clients may safely retire during
startup. Clients only remove copies tracked in skills.lock as market/bundled content whose
SKILL.md hash still matches the installed record; manually installed or locally modified copies
are preserved. An ID must not appear in both skills and retired.
External entries are marketplace pointers to Git/Web/ZIP sources:
agent-reach, ai-news-radar, amap-jsapi-skill, baoyu-skills, dingtalk-api,
dingtalk-cli, flyai-skill, follow-builders, humanizer, humanizer-zh,
ian-xiaohei-illustrations, impeccable, karpathy-guidelines, khazix-skills,
larksuite-cli, last30days, luckin-my-coffee, marketingskills,
mattpocock-skills, minimax-image-gen, minimax-tts, mt-paotui-for-client,
netease-skills, nuwa-skill, taste-skill, watch, watchless,
wechatpay-skills, wecom-cli
Data Formats
Local Skill (skills/<id>/SKILL.md)
Local skills use YAML frontmatter plus Markdown body. The top-level name must equal the directory slug. Display strings live in metadata.i18n.
---
name: web-access
description: >-
Use this skill when ...
version: 2.0.1
type: procedural
risk_level: low
status: enabled
metadata:
author: desirecore
updated_at: '2026-05-05'
i18n:
default_locale: en-US
source_locale: zh-CN
locales: [zh-CN, en-US]
zh-CN:
name: 联网访问
short_desc: 联网搜索、网页抓取、登录态浏览器访问
body: ./SKILL.zh-CN.md
translated_by: human
en-US:
name: Web Access
short_desc: Web search, page fetching, logged-in browser access
body: ./SKILL.md
source_hash: sha256:...
translated_by: human
market:
category: research
channel: latest
maintainer:
name: DesireCore Official
verified: true
---
External Entry (skills/<id>/entry.json)
External entries point to upstream packages or repositories. They are counted in manifest.stats.totalSkills but are not included in builtin-skills.json.
{
"id": "example-skill",
"name": "Example Skill",
"category": "development",
"icon": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">...</svg>",
"tags": ["example"],
"maintainer": {
"name": "Example",
"verified": false,
"account": "example",
"url": "https://github.com/example/example-skill"
},
"stewardship": "community",
"license": "MIT",
"redistribution": "allowed",
"source": {
"kind": "git",
"repoUrl": "https://github.com/example/example-skill.git",
"repoBranch": "main"
}
}
Team Listing (teams/<id>/entry.json)
A Team is a group of Agents with a supervisor. It is published as a fork pointer
only: the team body (team.json, members.json, shared/) always stays in the
upstream team repository, and the catalog registers just "what it is and where to
fork it from". Installation forks that repository and installs the declared members;
updates are a git pull on the fork. Because both actions are Git actions,
source.kind must be git and source.repoUrl is required — zip and web cannot
express either one — and a Team deliberately has no installPolicy /
updatePolicy pair: it is always market-initiated fork plus repository-driven update.
teams/<id>/ therefore holds exactly entry.json plus the sidecar. There is no
inline form; a team.json in the catalog is rejected.
{
"id": "example-team",
"name": "Example Team",
"category": "development",
"tags": ["example"],
"latestVersion": "0.1.0",
"maintainer": {
"name": "Example",
"verified": false,
"account": "example",
"url": "https://github.com/example"
},
"stewardship": "community",
"license": "MIT",
"redistribution": "source-pointer-only",
"source": {
"kind": "git",
"repoUrl": "https://github.com/example/example-team.git",
"repoBranch": "main",
"ref": "0123456789abcdef0123456789abcdef01234567"
},
"requiredClientVersion": "10.0.0",
"avatar": { "t": "示", "bg": "linear-gradient(135deg, #5856D6, #3634A3)" },
"supervisorName": "Example Supervisor",
"supervisorAgentId": "example-lead",
"memberCount": 3,
"memberNames": ["Example Member One", "Example Member Two"]
}
A Team card is rendered from avatar, not from icon. The client's runtime
projection marketTeamSchema requires avatar and has no icon property at all —
the same is true of marketAgentSchema, while only marketSkillSchema exposes
icon. The entry contract inherits icon from the shared common properties, so it
is accepted, but it can never reach a card. The validator therefore requires icon
on Skill listings only, and warns when an Agent or Team listing declares one.
redistribution stays source-pointer-only for a Team even under a permissive
license: the market never ships the team body, it only points at the repository the
client forks. The license governs what a fork may do; redistribution describes how
the content is delivered, and for teams that is always "fetch from upstream".
supervisorName, supervisorAgentId, memberCount, memberNames and
requiredSkills are display metadata declared by the publisher. They may drift from
the upstream repository, so installation, permissions and member resolution must read
the forked team.json / members.json instead. Teams are counted in
manifest.stats.totalTeams, which the client keeps optional: a catalog with no teams
may omit it, and once the key is present it must be exact.
Catalog metadata sidecar (catalog-metadata.v1.json)
The versioned catalog metadata contract is stored at one fixed path next to each legacy item:
agents/<id>/catalog-metadata.v1.json
teams/<id>/catalog-metadata.v1.json
skills/<id>/catalog-metadata.v1.json
Legacy agent.json, SKILL.md, and entry.json files remain the compatibility
surface for older clients. New clients merge the sidecar through a deterministic
adapter. Any field repeated in both files must have the same value; the validator
rejects drift rather than choosing one copy silently.
Agent listings support exactly one of agents/<slug>/agent.json (inline metadata)
or agents/<slug>/entry.json (an external pointer), alongside the sidecar. Missing
or simultaneous primary files are rejected. For a pointer, entry.id and sidecar
identity.id use the catalog directory slug and identity.kind is agent; the
upstream AgentFS agent.json.id remains its own UUID and must not be rewritten.
An agent listing may also carry an optional USAGE.md next to agent.json. The
client renders it as a separate "Usage" section on the agent detail page, kept
apart from fullDesc (which is persona text and also enters the agent's runtime
context). Localized variants use USAGE.<locale>.md, resolved through the same
fallback chain as the i18n block: requested locale, then i18n.source_locale,
then i18n.default_locale, then the unsuffixed file. Its scope is what a reader
needs before installing — prerequisites, authorization steps, capability and
safety boundaries — not full documentation; clients truncate overlong content.
Longer material belongs in a skill's references/ directory, which the agent
loads on demand. Listings without the file are unaffected and render no section.
Relative image references do not render on the detail page, so keep USAGE.md
text-only. See ADR-143 in the DesireCore repository.
Agent pointers first pass the complete raw client contract in
schemas/market-agent-entry.client.schema.json,
exported from marketAgentEntrySchema in the DesireCore repository at commit
18bbb86f62e1288b1f945209bed74ec72620a9d4. The schema's $comment records the
source blob as well. Refresh this generated snapshot from the TypeScript export
when changing client compatibility; do not replace it with permissive sidecar
validation. Version fields keep their original types and the client's supported
format. Installation/update policies must either both be absent (effective
market/market) or form a complete supported pair; the sidecar must preserve
that effective pair.
Agent pointer latestVersion maps to sidecar release.version; optional
requiredClientVersion, installPolicy, and updatePolicy must agree with the
sidecar compatibility/spec fields. Pointer source fields must describe the same
artifact as provenance.content, and maintainer maps to upstreamMaintainer.
An installable Agent pointer must itself pin source.ref (Git) or source.sha256
(Web/ZIP); an immutable ref supplied only by the sidecar cannot pin a mutable
entry. Existing immutable-source, license, governance-review and complete-coverage
checks still apply. Agent pointers do not receive the built-in Skill exceptions.
Agent 目录必须在 agent.json 内联元数据和 entry.json 外部指针中二选一,并提供 sidecar。
Pointer 原始 JSON 先通过固定客户端提交导出的完整 Schema;版本类型与格式、来源路径和策略组合不能由 sidecar 掩盖。
Pointer 的目录 slug、entry.id、sidecar identity.id 必须一致;上游 AgentFS 的 UUID 不改写。
安装/更新策略双缺省时有效值仍是 market/market,sidecar 不得将其改成系统条目。
latestVersion、最低客户端版本和安装/更新策略须与 sidecar 对齐;来源必须是同一个制品。
可安装指针自身必须固定 Git ref 或 Web/ZIP 摘要,不能只在 sidecar 宣称不可变版本。
现有许可、治理审查、不可变来源和完整覆盖门禁继续有效,不适用内置 Skill 的宽松例外。
Team listings are pointer-only, so teams/<slug>/ carries exactly entry.json plus
the sidecar; an inline team.json is rejected. Team pointers first pass the complete
raw client contract in
schemas/market-team-entry.client.schema.json,
exported from marketTeamEntrySchema the same way as the Agent snapshot; its
$comment records the source commit and blob. entry.id, the directory slug and
sidecar identity.id must agree, identity.kind is team, and latestVersion maps
to release.version. supervisorName, supervisorAgentId, memberCount,
memberNames, requiredSkills and requiredClientVersion are compared symmetrically:
the sidecar may neither drop a fact the pointer declares nor invent one it omits,
because the client reads the pointer and a version gate that exists only in the
sidecar would not gate anything. Pointer source fields must describe the same
artifact as provenance.content, and an installable Team pointer must itself pin a
full-SHA source.ref — a tag is not a reproducible pin, because a tag can be moved
to a different commit after the listing is reviewed.
团队条目只有指针形态:teams/<slug>/ 仅放 entry.json 与 sidecar,目录内出现 team.json 直接判非法。
Pointer 原始 JSON 先通过由 marketTeamEntrySchema 导出的完整客户端 Schema;
source.kind 恒为 git 且必须有 repoUrl,团队没有 installPolicy / updatePolicy 组合。
目录 slug、entry.id、sidecar identity.id 必须一致,identity.kind 为 team。
展示字段与最低客户端版本双向比对:sidecar 既不得丢弃指针声明的事实,也不得凭空补上指针没有的事实。
可安装团队指针自身必须固定完整 SHA 的 source.ref,tag 或分支不算可复现锁定。
The sidecar records source-owned presentation, release, timestamp, content
provenance, governance, compatibility, and type-specific facts. It deliberately
cannot declare catalogSourceId, catalog commit/path/trust, effective official
status, installation state, device state, health, URLs discovered at runtime, or
syncedAt. DesireCore injects trusted catalog provenance and runtime facts.
license.evidencePath, compliance.licenseEvidencePath and compliance.noticePath
resolve differently by item shape, because the schema constrains only the string
form. Vendored content (built-in Skills, inline Agents) ships inside this repository,
so the path is relative to the catalog item directory and the file must actually be
there — a missing file is an error. A pointer distributes nothing, so its evidence
can only be inside the upstream snapshot at the pinned revision; the validator cannot
read that offline, so it warns when such a claim is made against an unpinned pointer.
Pin source.ref to a full commit SHA and the claim becomes falsifiable by anyone who
fetches it.
Time facts are explicit known/unknown values. A known day uses
YYYY-MM-DD with precision: "day"; a known second uses an RFC 3339 UTC value
ending in Z with precision: "second". Never use the current date, clone time,
or synchronization time to fill an unknown catalog or release timestamp.
Collection children stay in their parent's sidecar. Each child declares the
canonical skill + parentId + id identity and its own release fact; a collection
parent may have an unknown version, and a child version must not be inferred from
the parent.
The strict source schema is
schemas/catalog-metadata.v1.schema.json.
Categories
Valid category slugs are declared in categories.json:
productivity, development, business, creative, design, media,
communication, research, data, management
Validation
Run these checks before submitting changes:
# Full market + i18n validation
uv run scripts/i18n/validate-i18n.py
# Catalog sidecar validator unit tests and standalone validation
uv run scripts/catalog/test_validate_catalog_metadata.py
uv run scripts/catalog/test_collection_generator.py
uv run scripts/catalog/validate_catalog_metadata.py
# Translation freshness check
uv run scripts/i18n/translate.py --check
# Verify pinned collection children without changing entry.json (network required;
# mutable collections are reported and skipped because their output is not reproducible)
uv run scripts/gen-collection-children.py --check
# Optional network check for entry.json source URLs
uv run scripts/i18n/validate-i18n.py --online
The validators check market stats, category references, builtin-skills.json,
entry.json structure, sidecar schema and legacy consistency, immutable source
evidence, collection identity, i18n completeness, and translation freshness.
Human-locked translations (translated_by: human) must keep source_hash
aligned after manual review. During a data migration,
scripts/catalog/validate_catalog_metadata.py --require-complete additionally
requires one sidecar for every top-level Agent, Team and Skill.
Detailed i18n guidance is in docs/I18N.md.
License
MIT License. See LICENSE.