Skip to content

fix(layout): page-header 的 registration inputs 不再宣告 description - #3265

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-3226-page-header-inputs
Aug 3, 2026
Merged

fix(layout): page-header 的 registration inputs 不再宣告 description#3265
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-3226-page-header-inputs

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes#3226

⚠️ 本 PR 只做原 issue 的一半(objectui 本地那一半)

按 issue 上两条 PM 裁定(#3226 的 comment):本单走路线 B(ADR-0087 D2 conversion 条目)而非直接删除,而 conversion 机制在 objectstack 仓,不在这里。因此本 PR 的范围被明确收窄为无条件成立、且跨仓依赖为零的那一半:

原 issue 原文:「两种路线都要求 inputs 不再宣告 description —— 这一点无论哪条路都成立。」

本 PR 做的:registerLayout()page-headerinputs[title, description] 收窄为 [title, subtitle]

本 PR 刻意不做的:不删 PageHeader.tsxsubtitle ?? description,不删 description prop。这两项被本 PR 的测试主动钉住了(见下),删它们等于走回被否决的删除路线。

剩余部分(阻塞在上游):conversion 条目 page-header-subtitle-alias(descriptionsubtitle,加载时改写成 canonical 键)+ 随后删除消费端裸 ??description prop。这部分归主 backlog PM,在 objectstack 立单;本单挂 Blocked-by


为什么这一半可以现在就做,而且必须现在做

inputs声明面,不是文档。设计器用它生成属性面板,framework 的 check:react-declaration-parity 拿它跟 spec schema 对差。所以宣告 description 不是「容忍一个遗留拼写」,而是对外发布了第二套方言,并且在作者(尤其是读 registry 学形状的 AI 作者)写一个 spec 里根本不存在的键:

  • @objectstack/spec/uiPageHeaderProps 的 shape 是 title / subtitle / icon / breadcrumb / actions / aria —— 没有description(已实测)。
  • canonical 的 page:header(@object-ui/componentscontainers.tsx)声明的是 subtitle
  • 于是同一份 metadata:写 descriptionpage-header 下渲染出副标题,换到 page:header静默丢失。同一份 JSON,两个结果 —— 这正是「单一契约」要消灭的东西。

收窄声明面不改变任何运行时行为(?? 还在),因此对仓外消费者零破坏;而把声明面留在错的状态,只会持续生产出将来 conversion 层还得去吸收的错误 metadata。

为什么运行时的 ?? 现在不能删

这个别名存在的全部理由就是仓外的消费者 schema(佐证:registerLayout() 在本仓没有任何调用点,它纯粹是 @object-ui/layout 对外的公开导出)。所以「仓内 grep 零命中」(我复核过,确实为零)不构成「没人在写」的证据。按删除路线走,外部写 description 的页面会静默丢副标题 —— 标题照常渲染,只是第二行没了,是最难被报障的失效形态。

预警的门禁冲突:核实过,不存在

PM 预警了 check:react-declaration-parity 可能校验「声明的 inputs ↔ 组件实际读取的 props」。查了 framework 侧实现,不会冲突,两个独立理由:

  1. 它比对的是两份声明(spec zod props vs registry inputs),其文件头明确写着「It never looks at a renderer」。组件读不读 description 完全不在它视野内。
  2. 它的 ratchet 只对 NEW registry-only inputs(registry 声明了而 spec 没有的)报警。description 恰恰就是一个 registry-only input,删掉它只会减少 divergence,不可能触发 ratchet
  3. 另外,它的覆盖面是 REACT_BLOCKS(ObjectForm / ListView / ObjectChart / Block),baseline 里也只有这三个 block;page-header 根本不在其中。

测试:两半都钉住

新增 packages/layout/src/__tests__/page-header-authorable-keys.test.tsx(7 个用例):

  • 声明面:page-headerlayout:page-header 两个键都不得description;必须subtitle;并且 —— 这条是从 spec 自己的 shape 推出来的,不是手写白名单 —— 声明的每一个 input 都必须是 PageHeaderProps 的键。将来再往这里加一个 spec 没有的 input,会因为和 description 完全相同的理由变红,不需要谁记得这个 issue。
  • 顺序守卫:运行时仍把 legacy description 渲染成副标题、且 subtitle 在两者都在时胜出 —— 这不是为别名背书,而是钉住顺序:在 conversion 条目落地前删掉这个读取,就是被否决的删除路线。注释里写明:conversion 落地时,同一个 commit 里删掉 fallback 和这两个用例。

验证

pnpm exec vitest run --maxWorkers=2 packages/layout
Test Files 6 passed (6)
Tests 78 passed (78)
pnpm exec turbo run type-check --filter=@object-ui/layout --concurrency=2
Tasks: 9 successful, 9 total
pnpm --filter @object-ui/layout lint → 0 errors(42 warnings,全部为既有)
pnpm run check:spec-symbols → ✅ 1192 files scanned
pnpm run changeset:check → ✅ fixed group ok / no `major`

Changeset

有 —— patch(@object-ui/layout)。虽然运行时行为不变,但声明面是对外发布的:设计器属性面板会从 "Description" 变成 "Subtitle",生成的 sdui.manifest.json 也随之改变,对使用设计器的人是可见的。按 AGENTS.md 未标 major


顺带发现(未在本 PR 修,已另开 issue)

文档里同样在教 description,而且比 inputs 教得更直接 —— 但它在 content/docs/,超出本单的 packages/layout 范围,且牵扯到别的 doc drift(breadcrumbs 这个 prop 组件根本不读),不适合搭车:

  • content/docs/guide/layout.md「PageHeader Component」的 authored JSON 示例写 "type": "page-header" + "description",Schema API 块声明 description?: string
  • content/docs/layout/page-header.mdx「Component Props」块列 description?: string,且完全没提 subtitle

https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa


Generated by Claude Code

…s an input
`registerLayout()` declared `inputs: [title, description]` for the legacy
kebab alias, while `@objectstack/spec/ui`'s `PageHeaderProps` — and the
canonical `page:header` renderer in `@object-ui/components` — declare
`subtitle`. `inputs` is a DECLARATION surface (the designer palette, the
framework's `check:react-declaration-parity`), so this did not tolerate a
legacy spelling, it published a second dialect and taught authors a
non-spec key. Metadata that took the offer renders a subtitle under
`page-header` and loses it under `page:header`.
The runtime `subtitle ?? description` read in `PageHeader.tsx` stays on
purpose: the alias exists for out-of-repo consumer schemas, so zero in-repo
hits is not evidence nobody writes it, and dropping the read would silently
delete an external page's second line. It is retired with the ADR-0087 D2
conversion entry `page-header-subtitle-alias`, which lives in the framework
repo. Narrowing the declaration is unconditional and changes no runtime
behaviour.
New tests pin both halves: the registration may not declare `description`,
must declare `subtitle`, and may declare nothing the spec's own shape does
not; the runtime fallback is pinned as a sequencing guard, to be deleted
together with the conversion entry.
Refs #3226
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa
@vercel

vercelBot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 3, 2026 9:05am

Request Review

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 09:06
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queueAug 3, 2026
Merged via the queue into main with commit d2363e7Aug 3, 2026
17 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-3226-page-header-inputs branch August 3, 2026 09:07
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-CfHWHxqB.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.47KB3.09KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.12KB3.41KB
auth (LoginForm.js)17.86KB5.29KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.43KB2.09KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)476.24KB104.56KB
core (index.js)2.25KB0.80KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.23KB34.75KB
fields (index.js)223.53KB54.78KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)3.26KB1.44KB
layout (index.js)37.96KB10.54KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)60.54KB17.13KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.01KB28.86KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)230.54KB56.80KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.49KB26.95KB
plugin-gantt (index.js)162.25KB39.55KB
plugin-grid (index.js)185.08KB49.04KB
plugin-kanban (index.js)47.89KB13.18KB
plugin-list (index.js)104.94KB25.32KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.55KB10.59KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.34KB2.82KB
plugin-view (index.js)83.67KB20.43KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…bjectstack-ai#3785)
* docs(layout): page-header 两处文档按组件实读收敛到 subtitle,删掉不存在的 breadcrumbs
`content/docs/guide/layout.md` 的 authored JSON 示例与 Schema API 教的是非 spec 的
`description`,还声明了 `breadcrumbs?: Array<{label,href?,icon?}>` 与
`actions?: ComponentSchema[]` —— 前者 PageHeader.tsx 一处都不读,后者类型是错的。
`content/docs/layout/page-header.mdx` 的 Component Props 块只列 `description`、
一字未提 `subtitle`,并漏了 icon/actions/showBack/schema 四个真实 prop。
objectstack-ai#3226 / PR objectstack-ai#3265 只收窄了机器读的 registration `inputs`;这一单让人和 AI 作者读的
文档面跟上,每条声明都从 origin/main 的代码读出:
- layout.md:示例与 Schema API 改 `subtitle`;`actions` 改为 action id / ActionDef
列表(交给 `record:quick_actions`,不是 ComponentSchema);补 `showBack`/`children`;
两段 blockquote 写清「写 subtitle 不写 description」与「没有 breadcrumbs 数组」
(spec 的 `breadcrumb` 是单数 boolean,canonical `page:header` 的开关)。
- page-header.mdx:Component Props 块补齐真实 prop 全集;`description` 保留但标为
legacy alias,注明随上游 ADR-0087 D2 conversion `page-header-subtitle-alias`
一并移除(现在删会让仓外页面静默丢副标题)。同文件 PageHeader 示例的 prop 拼写
一并改 `subtitle`,否则新加的注解会被下面 5 个 `description=` 示例当场推翻;
`<Page description=…>` 那一处**不动** —— 它是 `page` renderer 自己的真 prop,
并就地加注防止后来者误改。
无 changeset:`scripts/check-changeset-presence.mjs` 判定「0 of them under the src/
of a package the release covers … no changeset is owed」,与近 15 个 docs-only PR 的
先例一致。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
* docs(layout): 按上游实读改写 description 退役段 —— conversion 已 live,退役改由 objectstack-ai#3789 门控
PM 复核代查后指出:conversion `page-header-subtitle-alias` 已在 objectstack main
落地并标 live,因此上一版 page-header.mdx 里「It is scheduled for removal … Until
that conversion ships」是落地即陈旧的事实断言 —— 正是本单要消灭的缺陷类。
独立复核(objectstack origin/main = d42a92fc6):
- docs/protocol-upgrade-guide.md:273 —— `page-header-subtitle-alias` |
`page.component.page-header.description` | 'description' → 'subtitle' |
**live — protocol 17 loader accepts the old shape**
- packages/spec/src/conversions/registry.ts:4616-4633 —— toMajor: 17,
apply() 对 PAGE_HEADER_COMPONENT_TYPES 的节点 renameKey(properties,
'description', 'subtitle') 并 emit ConversionNotice
- registry.ts:4607-4612 —— canonical 优先:`subtitle` 在场时不改写,被遮蔽的
`description` 原样留下;只动 header 节点(element:text_input 的 description
是它自己的活属性)
- packages/spec/CHANGELOG.md:3499(089767f)—— 加载期为 defineStack /
validate / lint,以及 applyConversionsToStoredItem 覆盖的 sys_metadata 存量行
- packages/lint/src/authoring-rules.ts:550 已按该改写实现
page-header.mdx:退役段改为如实三段式 —— (a) 上游 conversion 已 live 且做什么;
(b) 本仓 prop 退役由 objectui#3789 门控,门槛是核实每条作者路径都经过执行改写的
loader;(c) objectstack-ai#3789 落地前 prop 保留、新页面写 subtitle。不预告时间,不替 objectstack-ai#3789
下结论。
layout.md:PM 说该文件未做「未落地」断言、不用动 —— 确认无landing-state 断言,
但按 PM 要求自查两文件一致性时发现另一处同类失真:原 blockquote 断言「写
description 的 metadata 在 canonical page:header 下**什么都不出**」。conversion
落地后该结论变成路径相关 —— 经 loader 的元数据会被改写成 subtitle 而正常渲染
(registry fixture:4645-4647 明确把 type: 'page:header' 也一并转换),只有绕过
loader 的裸 JSON 才仍然不出。故按真实情况改写为「两处仍接受 description(加载期
改写 + 本渲染器直读),但 subtitle 是唯一在每条路径上都渲染的拼写」,并指向
objectstack-ai#3789。这是 PM 点 2 自查要抓的东西,不是扩范围。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
---------
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
objectstack-ai#3797) (objectstack-ai#3806)
把 PR objectstack-ai#3795 在 `record:highlights` 上落的单块检查(「不得声明 spec 不接受的顶层
input」)推广到所有「`ComponentPropsMap` 有条目且本仓注册了非空 `inputs`」的 block,
期望值运行时从 spec 自身 shape 推导(不复述键表),现有偏离全部进**显式豁免名单**,
每条带理由 + 追踪单号。
门放在 `apps/console/src/__tests__/`,因为它需要的是产出发布物那份完整注册图:
`dev/manifest-dump.tsx` 用 `src/register-plugins.ts` + `@object-ui/components` 搭
manifest,本文件就导入这一对(与隔壁 `public-contract.test.ts` 同法),不自己攒一份
会自说自话的清单。覆盖面不限于 public tier —— `renderers/layout/page.tsx:462` 用
`getKnownTypes()` 现搭运行时 JSX 页面校验 manifest,所以非 public 的 block(如
`element:record_picker`)的 `inputs` 同样是活的 prop 白名单。
四个 OFF-SPEC block 逐块以代码判定,结论都落在 spec 一侧,因此**本仓四个 block 的
`inputs` 一个字节都没动**:
- `page:header` 的 `recordChrome`/`showStar`/`showCopyId` —— 渲染器实读
(`containers.tsx:979/980/981`),消费于 `:1453` 的布局分支与 `:1531/:1532` 的
RecordTitleChip;作者可达且非宿主注入(宿主注入走 RecordContext 且故意不声明),
`preview-samples.ts:68` 与 `buildDefaultPageSchema.ts:413` 今天就在写。
(issue 猜「可能是 objectstack-ai#3226/PR objectstack-ai#3265 有意保留的 renderer-only prop」已证否:objectstack-ai#3265
动的是 `packages/layout` 的遗留别名 `page-header`,canonical 这个没被碰过。)
- `page:accordion.variant` —— 渲染器实读 `containers.tsx:734`,`:735` 据此改每个
面板的边框类,spec 完全没有对应键。
- `page:tabs.tabStyle` —— spec 已用 `type` 声明同一概念,所以是「一义两拼」;但两条
本地动作都被证否:撤掉它会删掉扁平 SDUI 载体唯一能表达的拼法
(`SchemaRenderer.tsx:251-270` 故意不上提 `properties.type`),改成发布 `type` 又
会声明一个自家 parser 结构上无法校验的键(`validate.ts` 的 `BASE_PROPS` 含
`'type'`)。收敛方向是 spec 侧的契约决定,不在这里猜。
- `element:record_picker` 的 `labelField`/`valueField`/`label` —— 上游
objectstack#5775 已经解决,且正是往本仓渲染器实读的 `labelField` 收敛
(`displayField`/`searchFields`/`multiple` 已转 `retiredKey()` 墓碑)。本仓这三条
flag 纯粹是 pin 落后:npm 上最新已发布的 `@objectstack/spec` 是 `17.0.0-rc.5`,
早于 #5775。
前三项转 objectstack#6776(跨仓转移协议,`pm:queue`,带 `Part of objectui#3797`);
八条豁免全部挂单号。豁免会自己过期:一旦 spec 声明了某个被豁免的键,
`carries no stale exemption` 就红,强制删条目,名单不会烂成永久 allowlist。
Fixesobjectstack-ai#3797
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

page-headerpage:header 对外声明了两套 authorable 键:description vs subtitle,消费端用 ?? 兜底

2 participants

@xuyushun441-sys@claude