Skip to content

chore: main merge feature#1838

Merged
github-actions[bot] merged 4 commits intomainfrom
main-merge-feature
Mar 30, 2026
Merged

chore: main merge feature#1838
github-actions[bot] merged 4 commits intomainfrom
main-merge-feature

Conversation

@kimteayon
Copy link
Copy Markdown
Collaborator

@kimteayon kimteayon commented Mar 30, 2026

中文版模板 / Chinese template

🤔 This is a ...

  • 🆕 New feature
  • 🐞 Bug fix
  • 📝 Site / documentation improvement
  • 📽️ Demo improvement
  • 💄 Component style improvement
  • 🤖 TypeScript definition improvement
  • 📦 Bundle size optimization
  • ⚡️ Performance optimization
  • ⭐️ Feature enhancement
  • 🌐 Internationalization
  • 🛠 Refactoring
  • 🎨 Code style optimization
  • ✅ Test Case
  • 🔀 Branch merge
  • ⏩ Workflow
  • ⌨️ Accessibility improvement
  • ❓ Other (about what?)

🔗 Related Issues

  • Describe the source of related requirements, such as links to relevant issue discussions.
  • For example: close #xxxx, fix #xxxx

💡 Background and Solution

  • The specific problem to be addressed.
  • List the final API implementation and usage if needed.
  • If there are UI/interaction changes, consider providing screenshots or GIFs.

📝 Change Log

Language Changelog
🇺🇸 English
🇨🇳 Chinese

Summary by CodeRabbit

发布说明

  • 新功能

    • 新增 @ant-design/x-card 动态卡片渲染器,支持基于 A2UI 协议构建交互式界面
    • 支持流式渲染、数据双向绑定和响应式更新
    • 提供 A2UI v0.8 和 v0.9 两个协议版本
    • 包含丰富的演示示例和完整的 API 文档
  • 文档

    • 更新产品首页和导航,新增 X Card 模块入口
    • 补充中英文文档和多个交互场景演示

* feat: x-card

* feat: x-card

* feat: message

* feat: x-card

* feat: x-card

* feat: x-card

* feat: A2UI

* feat: card

* feat: x-card

* feat: xcard

* feat: a2ui

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: x-card

* feat: update span

* feat: x-card

* feat: update span

* feat: x-card

* feat: x-card
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

Preview failed

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: aa86e838-905a-413f-970e-61f04ccd4c0b

📥 Commits

Reviewing files that changed from the base of the PR and between 46d8a83 and c6122df.

📒 Files selected for processing (1)
  • packages/x/.dumirc.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/x/.dumirc.ts

📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive 标题 'chore: main merge feature' 过于宽泛且模糊,未能清晰表达此次变更的核心内容。虽然涉及 x-card 包的新增和文档更新,但标题未体现具体的功能或模块信息。 建议使用更具体的标题,例如 'feat: add @ant-design/x-card package' 或 'feat(x-card): introduce dynamic card renderer' 以清晰表达主要变更内容。
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 81.58% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch main-merge-feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

const next = { ...obj };
let cur: Record<string, any> = next;
for (let i = 0; i < parts.length - 1; i++) {
cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.

Copilot Autofix

AI 5 days ago

In general, to fix prototype-polluting assignments when using dynamic keys, you either (a) prevent dangerous keys (__proto__, prototype, constructor) and malformed paths from being used, or (b) switch to data structures that are not prototype-based, like Map or Object.create(null). Here, the minimal, compatibility-preserving approach is to validate path segments and reject or ignore any paths that contain forbidden keys, while preserving the existing object shape and behavior for valid paths.

The best fix with minimal behavior change is to harden setValueByPath in packages/x-card/src/A2UI/utils.ts:

  1. Normalize path as before (strip leading /, split('/')), then check that the resulting array is non-empty and that each segment is a “safe” key.
  2. Introduce a small helper (inside the same file) that returns false for forbidden keys ('__proto__', 'prototype', 'constructor') and true otherwise.
  3. If the overall path is invalid (empty) or any segment is unsafe, return the original object unchanged instead of performing any assignments.
  4. When creating intermediate containers, use a normal {} as before to avoid affecting public API shape; we don’t need to change this.

This change is localized to setValueByPath, so all its callers (applyDataModelUpdateV09, handleAction in Card.tsx, and handleDataChange in Card.tsx, plus any v0.8 equivalents not shown) will automatically benefit from the added safety without further modifications. No new imports or external dependencies are required.


Suggested changeset 1
packages/x-card/src/A2UI/utils.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/x-card/src/A2UI/utils.ts b/packages/x-card/src/A2UI/utils.ts
--- a/packages/x-card/src/A2UI/utils.ts
+++ b/packages/x-card/src/A2UI/utils.ts
@@ -9,6 +9,11 @@
   return parts.reduce((cur, key) => (cur != null ? cur[key] : undefined), obj as any);
 }
 
+/** Check if a path segment is safe to use as an object key (avoid prototype pollution) */
+function isSafePathSegment(segment: string): boolean {
+  return segment !== '__proto__' && segment !== 'constructor' && segment !== 'prototype';
+}
+
 /** Write value to nested object by path (immutable), path format like /booking/selectedCoffee */
 export function setValueByPath(
   obj: Record<string, any>,
@@ -16,13 +21,27 @@
   value: any,
 ): Record<string, any> {
   const parts = path.replace(/^\//, '').split('/');
+
+  // Reject empty paths or unsafe segments to prevent prototype pollution
+  if (
+    parts.length === 0 ||
+    parts.some((segment) => !segment || !isSafePathSegment(segment))
+  ) {
+    return obj;
+  }
+
   const next = { ...obj };
   let cur: Record<string, any> = next;
   for (let i = 0; i < parts.length - 1; i++) {
-    cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};
-    cur = cur[parts[i]];
+    const key = parts[i];
+    const existing = cur[key];
+    cur[key] = existing && typeof existing === 'object' && !Array.isArray(existing)
+      ? { ...existing }
+      : {};
+    cur = cur[key];
   }
-  cur[parts[parts.length - 1]] = value;
+  const lastKey = parts[parts.length - 1];
+  cur[lastKey] = value;
   return next;
 }
 
EOF
@@ -9,6 +9,11 @@
return parts.reduce((cur, key) => (cur != null ? cur[key] : undefined), obj as any);
}

/** Check if a path segment is safe to use as an object key (avoid prototype pollution) */
function isSafePathSegment(segment: string): boolean {
return segment !== '__proto__' && segment !== 'constructor' && segment !== 'prototype';
}

/** Write value to nested object by path (immutable), path format like /booking/selectedCoffee */
export function setValueByPath(
obj: Record<string, any>,
@@ -16,13 +21,27 @@
value: any,
): Record<string, any> {
const parts = path.replace(/^\//, '').split('/');

// Reject empty paths or unsafe segments to prevent prototype pollution
if (
parts.length === 0 ||
parts.some((segment) => !segment || !isSafePathSegment(segment))
) {
return obj;
}

const next = { ...obj };
let cur: Record<string, any> = next;
for (let i = 0; i < parts.length - 1; i++) {
cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};
cur = cur[parts[i]];
const key = parts[i];
const existing = cur[key];
cur[key] = existing && typeof existing === 'object' && !Array.isArray(existing)
? { ...existing }
: {};
cur = cur[key];
}
cur[parts[parts.length - 1]] = value;
const lastKey = parts[parts.length - 1];
cur[lastKey] = value;
return next;
}

Copilot is powered by AI and may make mistakes. Always verify output.
cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};
cur = cur[parts[i]];
}
cur[parts[parts.length - 1]] = value;

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.

Copilot Autofix

AI 5 days ago

In general terms, the problem is that setValueByPath writes to plain JavaScript objects using unvalidated dynamic property names. To fix this, we must ensure that no path segment can be interpreted as a special prototype-related key (__proto__, prototype, constructor), and ideally that the intermediate containers we create are not themselves inheriting from Object.prototype.

The minimal, non‑breaking fix here is to harden setValueByPath in packages/x-card/src/A2UI/utils.ts:

  1. Normalize the path into parts as before.
  2. Before using each segment as a property key, validate it:
    • Reject or ignore dangerous segments ('__proto__', 'prototype', 'constructor').
    • We can either:
      • Throw an error, or
      • Return the original object unchanged.
        To avoid changing functional behavior unexpectedly, we’ll choose to return the original object when an invalid segment is encountered, effectively treating the update as a no‑op.
  3. When creating nested objects, use Object.create(null) so they are prototype‑less and cannot be used to pollute Object.prototype even if something slips through.
  4. Use the same safeguards for the final property assignment.

This change is fully localized to setValueByPath, so callers in Card.v0.9.ts and Card.tsx do not need modifications. No new imports or external libraries are required; we only use Object.create(null) and some basic checks. The line with the vulnerable assignment (cur[parts[parts.length - 1]] = value;) will be replaced with logic that first validates the key and then assigns to a prototype‑less object.


Suggested changeset 1
packages/x-card/src/A2UI/utils.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/x-card/src/A2UI/utils.ts b/packages/x-card/src/A2UI/utils.ts
--- a/packages/x-card/src/A2UI/utils.ts
+++ b/packages/x-card/src/A2UI/utils.ts
@@ -16,13 +16,41 @@
   value: any,
 ): Record<string, any> {
   const parts = path.replace(/^\//, '').split('/');
-  const next = { ...obj };
+
+  // Basic guard: if path is empty after normalization, just return original object
+  if (parts.length === 0 || (parts.length === 1 && parts[0] === '')) {
+    return obj;
+  }
+
+  // Reject dangerous keys that could lead to prototype pollution
+  const isUnsafeKey = (key: string): boolean =>
+    key === '__proto__' || key === 'prototype' || key === 'constructor';
+
+  if (parts.some((segment) => isUnsafeKey(segment))) {
+    // Silently ignore unsafe paths to avoid prototype pollution
+    return obj;
+  }
+
+  const next: Record<string, any> = { ...obj };
   let cur: Record<string, any> = next;
+
   for (let i = 0; i < parts.length - 1; i++) {
-    cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};
-    cur = cur[parts[i]];
+    const key = parts[i];
+    const existing = cur[key];
+
+    if (existing && typeof existing === 'object') {
+      // Clone existing object to maintain immutability
+      cur[key] = Array.isArray(existing) ? [...existing] : { ...existing };
+    } else {
+      // Use a prototype-less object for newly created containers
+      cur[key] = Object.create(null);
+    }
+
+    cur = cur[key];
   }
-  cur[parts[parts.length - 1]] = value;
+
+  const lastKey = parts[parts.length - 1];
+  cur[lastKey] = value;
   return next;
 }
 
EOF
@@ -16,13 +16,41 @@
value: any,
): Record<string, any> {
const parts = path.replace(/^\//, '').split('/');
const next = { ...obj };

// Basic guard: if path is empty after normalization, just return original object
if (parts.length === 0 || (parts.length === 1 && parts[0] === '')) {
return obj;
}

// Reject dangerous keys that could lead to prototype pollution
const isUnsafeKey = (key: string): boolean =>
key === '__proto__' || key === 'prototype' || key === 'constructor';

if (parts.some((segment) => isUnsafeKey(segment))) {
// Silently ignore unsafe paths to avoid prototype pollution
return obj;
}

const next: Record<string, any> = { ...obj };
let cur: Record<string, any> = next;

for (let i = 0; i < parts.length - 1; i++) {
cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};
cur = cur[parts[i]];
const key = parts[i];
const existing = cur[key];

if (existing && typeof existing === 'object') {
// Clone existing object to maintain immutability
cur[key] = Array.isArray(existing) ? [...existing] : { ...existing };
} else {
// Use a prototype-less object for newly created containers
cur[key] = Object.create(null);
}

cur = cur[key];
}
cur[parts[parts.length - 1]] = value;

const lastKey = parts[parts.length - 1];
cur[lastKey] = value;
return next;
}

Copilot is powered by AI and may make mistakes. Always verify output.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 30, 2026

Deploying ant-design-x with  Cloudflare Pages  Cloudflare Pages

Latest commit: c6122df
Status: ✅  Deploy successful!
Preview URL: https://2958e136.ant-design-x.pages.dev
Branch Preview URL: https://main-merge-feature.ant-design-x.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the @ant-design/x-card package, a dynamic card renderer supporting A2UI protocol versions 0.8 and 0.9, including catalog management and data binding utilities. Critical feedback identifies merge conflicts in package.json and SKILL.md that must be resolved. Technical improvements are suggested for the setValueByPath utility to prevent data corruption, adopting rimraf for cross-platform compatibility, and refactoring command filtering for better maintainability. Furthermore, exporting individual command interfaces is recommended for improved type safety, and the validation logic should be updated to distinguish between errors and warnings to ensure the accuracy of the validation status.

I am having trouble creating individual review comments. Click here to see my feedback.

package.json (3-7)

critical

This file contains merge conflict markers (<<<<<<<, =======, >>>>>>>). Please resolve the conflict by choosing the correct version for the project.

packages/x-skill/skills/x-request/SKILL.md (3-7)

critical

This file contains merge conflict markers. Please resolve the conflict to ensure the skill metadata is correct.

packages/x-card/src/A2UI/utils.ts (22)

high

This line can lead to unexpected behavior if cur[parts[i]] exists but is not a plain object (e.g., an array or a primitive). Spreading a non-object value can lead to data corruption. It's safer to check if the value is an object before spreading.

    cur[parts[i]] = (cur[parts[i]] && typeof cur[parts[i]] === 'object' && !Array.isArray(cur[parts[i]])) ? { ...cur[parts[i]] } : {};

packages/x-card/package.json (44)

medium

The clean script uses rm -rf, which is not cross-platform and will fail on Windows. Consider using a cross-platform tool like rimraf to ensure the script works for all contributors. You would need to add rimraf to your devDependencies.

    "clean": "rimraf es lib coverage dist",

packages/x-card/src/A2UI/Card.tsx (186-194)

medium

The filtering logic for commands is repetitive and could be simplified by extracting it into a helper function. This would make the code more readable and easier to maintain, especially if more command types are added in the future.

    const getCommandSurfaceId = (cmd: any): string | undefined => {
      if ('createSurface' in cmd) return cmd.createSurface.surfaceId;
      if ('updateComponents' in cmd) return cmd.updateComponents.surfaceId;
      if ('updateDataModel' in cmd) return cmd.updateDataModel.surfaceId;
      if ('deleteSurface' in cmd) return cmd.deleteSurface.surfaceId;
      if ('surfaceUpdate' in cmd) return cmd.surfaceUpdate.surfaceId;
      if ('dataModelUpdate' in cmd) return cmd.dataModelUpdate.surfaceId;
      if ('beginRendering' in cmd) return cmd.beginRendering.surfaceId;
      return undefined;
    };

    const myCommands = commandQueue.filter((cmd) => getCommandSurfaceId(cmd) === id);

packages/x-card/src/A2UI/types/command_v0.8.ts (35-70)

medium

To improve type safety and code clarity, it's beneficial to export the individual command interfaces. This enables using them in type guards and avoids brittle type assertions elsewhere in the code.

export interface SurfaceUpdateCommand {
  surfaceUpdate: {
    surfaceId: string;
    components: ComponentWrapper_v0_8[];
  };
}

// Command to update data model
export interface DataModelUpdateCommand {
  dataModelUpdate: {
    surfaceId: string;
    contents: Array<{
      key: string;
      valueString?: string;
      valueMap?: Array<{
        key: string;
        valueString: string;
      }>;
    }>;
  };
}

// Command to begin rendering
export interface BeginRenderingCommand {
  beginRendering: {
    surfaceId: string;
    root: string; // Root component ID
  };
}

// Command to delete a surface
export interface DeleteSurfaceCommand {
  deleteSurface: {
    surfaceId: string;
  };
}

packages/x-card/src/A2UI/types/command_v0.9.ts (44-76)

medium

For better type safety and to avoid type assertions in other parts of the codebase, it's a good practice to export the individual command interfaces (CreateSurfaceCommand, UpdateComponentsCommand, UpdateDataModelCommand, DeleteSurfaceCommand). This allows for more precise type guards and avoids casting to any.

export interface CreateSurfaceCommand {
  version: 'v0.9';
  createSurface: {
    surfaceId: string;
    catalogId: string; // 必需,组件目录 URL 或本地标识
  };
}

// Command to update components on a surface
export interface UpdateComponentsCommand {
  version: 'v0.9';
  updateComponents: {
    surfaceId: string;
    components: BaseComponent_v0_9[];
  };
}

// Command to update data model
export interface UpdateDataModelCommand {
  version: 'v0.9';
  updateDataModel: {
    surfaceId: string;
    path: string;
    value: any;
  };
}

// Command to delete a surface
export interface DeleteSurfaceCommand {
  version: 'v0.9';
  deleteSurface: {
    surfaceId: string;
  };
}

packages/x-card/src/A2UI/utils.ts (51-89)

medium

The current implementation mixes errors (like missing required fields) and warnings (like extra properties) in the same errors array. This makes the valid flag misleading, as it becomes false even for warnings. It's better to separate errors and warnings to provide more accurate validation results.

  const errors: string[] = [];
  const warnings: string[] = [];

  // If no catalog, pass by default
  if (!catalog || !catalog.components) {
    return { valid: true, errors: [] };
  }

  // Check if component is defined in catalog
  const componentDef = catalog.components[componentName];
  if (!componentDef) {
    errors.push(`Component "${componentName}" is not defined in catalog`);
    return { valid: false, errors };
  }

  // Check required fields
  const requiredFields = componentDef.required || [];
  for (const field of requiredFields) {
    if (!(field in componentProps)) {
      errors.push(`Missing required field "${field}" for component "${componentName}"`);
    }
  }

  // Check if properties are defined in schema (warning level, does not block rendering)
  if (componentDef.properties) {
    const definedProps = Object.keys(componentDef.properties);
    const actualProps = Object.keys(componentProps).filter(
      (key) => !['id', 'children', 'component'].includes(key),
    );

    for (const prop of actualProps) {
      if (!definedProps.includes(prop)) {
        warnings.push(
          `Warning: Property "${prop}" is not defined in catalog for component "${componentName}"`,
        );
      }
    }
  }

  return { valid: errors.length === 0, errors: [...errors, ...warnings] };

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Performance benchmark report not found.


This comment is automatically generated by the x-markdown performance CI.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

size-limit report 📦

Path Size
packages/x/dist/antdx.min.js 463.58 KB
packages/x-sdk/dist/x-sdk.min.js 8.36 KB
packages/x-markdown/dist/x-markdown.min.js 32.28 KB
packages/x-markdown/dist/plugins/latex.min.js 61.48 KB

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 97.95918% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.46%. Comparing base (1b85e6f) to head (c6122df).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
packages/x-card/src/A2UI/Box.tsx 91.42% 3 Missing ⚠️
packages/x-card/src/A2UI/Card.tsx 98.63% 2 Missing ⚠️
packages/x-card/src/A2UI/Card.v0.9.ts 96.82% 2 Missing ⚠️
packages/x-card/src/A2UI/Card.v0.8.ts 98.48% 1 Missing ⚠️
packages/x-card/src/A2UI/index.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1838      +/-   ##
==========================================
+ Coverage   97.42%   97.46%   +0.04%     
==========================================
  Files         149      158       +9     
  Lines        5044     5485     +441     
  Branches     1438     1605     +167     
==========================================
+ Hits         4914     5346     +432     
- Misses        128      137       +9     
  Partials        2        2              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Performance benchmark report not found.


This comment is automatically generated by the x-markdown performance CI.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (18)
packages/x/docs/x-card/demo/A2UI_v0.9/catalog-form.json-8-64 (1)

8-64: ⚠️ Potential issue | 🟠 Major

Schema 缺少 required,会导致关键字段未被强制校验。

当前 RegistrationFormSuccessCard 里虽然声明了 component.const,但未在对象级 required 中声明,校验器可接受不含 component 的对象,削弱 catalog 约束。

🩹 建议最小修复
   "RegistrationForm": {
     "type": "object",
+    "required": ["component"],
     "properties": {
       "component": {
         "const": "RegistrationForm"
       },
@@
   "SuccessCard": {
     "type": "object",
+    "required": ["component"],
     "properties": {
       "component": {
         "const": "SuccessCard"
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-form.json` around lines 8 - 64,
The schema objects "RegistrationForm" and "SuccessCard" are missing object-level
required arrays so critical fields (e.g., component) aren't enforced; update the
"RegistrationForm" schema to add a "required" list including "component" and
other mandatory fields like "step" and "status" as appropriate, and update the
"SuccessCard" schema to add a "required" list including "component" and required
display fields like "username" and "email" (adjust lists to match which fields
must be mandatory), ensuring the top-level object definitions for
RegistrationForm and SuccessCard include these required arrays.
packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx-701-706 (1)

701-706: ⚠️ Potential issue | 🟠 Major

流式文案在结束后会被直接移除。

Line 734-735 把非 RUNNING 状态的时间戳置成了 0 并过滤掉,所以欢迎文案一结束就会从列表里消失。这里已经有 hook 返回的稳定时间戳可用,直接复用就够了。

💡 建议修改
   const {
     text: textHeader,
     streamStatus: streamStatusHeader,
+    timestamp: textHeaderTimestamp,
     run: runHeader,
     reset: resetHeader,
   } = useStreamText(contentHeader);
@@
       content: {
-        texts: [
-          { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 },
-        ].filter((item) => item.timestamp !== 0),
+        texts: textHeader ? [{ text: textHeader, timestamp: textHeaderTimestamp }] : [],
         card,
       } as ContentType,

Also applies to: 734-735

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx` around lines 701 -
706, The stream text is removed when its status ends because the code forces
non-RUNNING timestamps to 0 and then filters them out; update the logic that
sets timestamps (the block handling streamStatusHeader/runHeader for
contentHeader) to reuse the stable timestamp provided by useStreamText (the hook
that returns textHeader, streamStatusHeader, runHeader, resetHeader) instead of
assigning 0 for non-RUNNING states so the welcome copy remains in the list after
streaming ends.
packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx-598-603 (1)

598-603: ⚠️ Potential issue | 🟠 Major

流式文案在结束后会被直接移除。

Line 633-634 把非 RUNNING 状态的时间戳置成了 0 并过滤掉,所以欢迎文案一结束就会从列表里消失。这里已经有 hook 返回的稳定时间戳可用,没必要每次 render 再取一次 Date.now().

💡 建议修改
   const {
     text: textHeader,
     streamStatus: streamStatusHeader,
+    timestamp: textHeaderTimestamp,
     run: runHeader,
     reset: resetHeader,
   } = useStreamText(contentHeader);
@@
       content: {
-        texts: [
-          { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 },
-        ].filter((item) => item.timestamp !== 0),
+        texts: textHeader ? [{ text: textHeader, timestamp: textHeaderTimestamp }] : [],
         card,
       } as ContentType,

Also applies to: 632-634

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx` around lines 598 -
603, The stream text is being removed after finishing because the render sets
non-RUNNING timestamps to 0 and filters them out using a fresh Date.now();
instead, use the stable timestamp provided by the hook returned values
(useStreamText -> textHeader, streamStatusHeader, runHeader, resetHeader) rather
than calling Date.now() on each render, and stop zeroing timestamps for
non-RUNNING states so completed stream entries remain in the list; update the
logic that currently sets timestamp = (status !== RUNNING ? 0 : Date.now()) to
reference the hook's stable timestamp value and only change it when
runHeader/resetHeader indicate a new stream.
packages/x/docs/x-card/demo/A2UI_v0.9/catalog-nested.json-79-95 (1)

79-95: ⚠️ Potential issue | 🟠 Major

这两处 oneOf 会把合法的 { path } 绑定判成无效。

普通对象分支和 path-binding 分支的形状重叠,{ path: '...' } 会同时命中两支;按 JSON Schema 语义,oneOf 必须“恰好一支”匹配,所以这里的绑定值会校验失败。要么把普通对象分支收窄,要么改成 anyOf.

💡 最小修正示例
         "selectedFile": {
           "description": "Currently selected file.",
-          "oneOf": [
+          "anyOf": [
             {
               "type": "object"
             },
@@
         "file": {
           "description": "File detail object.",
-          "oneOf": [
+          "anyOf": [
             {
               "type": "object",
               "properties": {

Also applies to: 130-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-nested.json` around lines 79 -
95, 字段 selectedFile 使用了 oneOf 包含一个通用 object 分支和一个带 path 的 object 分支,这两者形状重叠导致诸如
{ "path": "..." } 同时命中两支而触发 oneOf 的“恰好一支”校验失败;请在 catalog-nested.json 中把对应的 oneOf
改为 anyOf(或将第一个通用 object 分支收窄以与带 path 的分支互斥),对文件中第二处相同结构(原评论提到的 130-154
区域)做同样修改,确保使用的 schema 关键字为 anyOf 或调整分支属性以消除重叠。
packages/x-card/src/A2UI/Box.tsx-20-30 (1)

20-30: ⚠️ Potential issue | 🟠 Major

重置命令时请同步清空 surfaceCatalogMap

现在只把 processedCommandsCount 归零。父组件把 commands 清空或替换成更短的新队列后,旧的 surfaceId -> catalogId 映射还会继续留在 context 里,和这里注释里的 “reprocess from scratch” 语义不一致。

💡 建议修改
   useEffect(() => {
     // commands was cleared or reset — reset the counter and bail out
     if (!commands || commands.length === 0) {
       processedCommandsCount.current = 0;
+      setSurfaceCatalogMap((prev) => (prev.size === 0 ? prev : new Map()));
       return;
     }

     // commands array was replaced with a shorter one (e.g. reset by parent) — reset counter and reprocess from scratch
     if (commands.length < processedCommandsCount.current) {
       processedCommandsCount.current = 0;
+      setSurfaceCatalogMap((prev) => (prev.size === 0 ? prev : new Map()));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/Box.tsx` around lines 20 - 30, When resetting
processedCommandsCount in the useEffect (the branches that check !commands ||
commands.length === 0 and commands.length < processedCommandsCount.current),
also clear the surfaceCatalogMap so stale surfaceId -> catalogId mappings are
removed; locate the useEffect handling commands and add logic to either call
surfaceCatalogMap.clear() (or reset the context/store that holds
surfaceCatalogMap) in both the "commands cleared" and "shorter commands array"
branches to ensure reprocessing truly starts from scratch.
packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx-617-627 (1)

617-627: ⚠️ Potential issue | 🟠 Major

Reload 没有清掉上一轮命令历史。

sessionKey 会让 XCard.Box 重新挂载并从头消费整条 commandQueue。这里继续保留旧 queue,只会让每次 reload 都把之前 session 的命令再回放一遍,处理量会持续累积。

💡 建议修改
   const handleReload = useCallback(() => {
     resetHeader();
-    const deleteCommands: XAgentCommand_v0_9[] = [
-      { version: 'v0.9', deleteSurface: { surfaceId: 'filter' } },
-    ];
-    setCommandQueue((prev) => [...prev, ...deleteCommands]);
+    setCommandQueue([]);
     setCard([]);
-    setTimeout(() => {
-      setSessionKey((prev) => prev + 1);
-    }, 50);
+    setSessionKey((prev) => prev + 1);
   }, [resetHeader]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx` around lines 617 -
627, handleReload currently leaves the previous commandQueue intact so when
sessionKey causes XCard.Box to remount the old commands are replayed; update
handleReload to clear or replace the queue instead of appending deleteCommands —
e.g. call setCommandQueue to set the queue to just the deleteCommands (or an
empty array then push deleteCommands) before setSessionKey, keep the existing
calls to resetHeader and setCard, and ensure you reference handleReload,
setCommandQueue, commandQueue, sessionKey, setSessionKey and setCard when making
the change.
packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx-720-728 (1)

720-728: ⚠️ Potential issue | 🟠 Major

Reload 没有清掉上一轮命令历史。

sessionKey 会让 XCard.Box 重新挂载并从头消费整条 commandQueue。这里继续保留旧 queue,只会让每次 reload 都把之前 session 的命令再回放一遍,处理量会持续累积。

💡 建议修改
   const handleReload = useCallback(() => {
     resetHeader();
-    const deleteCommands: XAgentCommand_v0_8[] = [{ deleteSurface: { surfaceId: 'filter' } }];
-    setCommandQueue((prev) => [...prev, ...deleteCommands]);
+    setCommandQueue([]);
     setCard([]);
-    setTimeout(() => {
-      setSessionKey((prev) => prev + 1);
-    }, 50);
+    setSessionKey((prev) => prev + 1);
   }, [resetHeader]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx` around lines 720 -
728, The reload handler handleReload currently appends deleteCommands to the
existing commandQueue so when sessionKey forces XCard.Box to remount it replays
previous sessions; change handleReload to replace/clear the queue (e.g., call
setCommandQueue to set only the deleteSurface command or an empty array before
remount) so old commands are not replayed; keep resetHeader, setCard([]) and the
sessionKey bump as-is but ensure setCommandQueue is invoked with the fresh array
(not prev => [...prev, ...]) to fully clear prior history.
packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx-994-999 (1)

994-999: ⚠️ Potential issue | 🟠 Major

流式结束后把欢迎文案过滤掉了。

timestampRUNNING 之外被写成 0,后面的 filter 会直接删掉这条文本,所以状态一旦变成 FINISHED,header 就消失了。这里应该复用 useStreamText 返回的固定时间戳,而不是每次 render 临时写 Date.now() / 0

🛠️ 建议修改
   const {
     text: textHeader,
     streamStatus: streamStatusHeader,
+    timestamp: timestampHeader,
     run: runHeader,
     reset: resetHeader,
   } = useStreamText(contentHeader);
@@
         texts: [
-          { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 },
+          { text: textHeader, timestamp: timestampHeader },
         ].filter((item) => item.timestamp !== 0),

Also applies to: 1047-1049

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx` around lines 994 -
999, The header text is being filtered out after streaming because you write
timestamp as 0/Date.now() on each render instead of reusing the stable timestamp
returned by useStreamText; update the code that constructs the header item to
use the timestamp value provided by useStreamText (the same object that yields
textHeader/streamStatusHeader/runHeader/resetHeader) rather than computing
Date.now() or 0 per render so the item keeps its original timestamp and isn't
dropped when streamStatus moves from RUNNING to FINISHED; apply the same change
to the other similar block that uses useStreamText at the corresponding spot.
packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx-950-955 (1)

950-955: ⚠️ Potential issue | 🟠 Major

流式结束后把欢迎文案过滤掉了。

这里把 header 的 timestamp 绑成了 Date.now() / 0,而后面的 filter 又会剔除 0。结果就是一旦状态进入 FINISHED,欢迎文案会直接消失。应该改用 useStreamText 返回的固定时间戳。

🛠️ 建议修改
   const {
     text: textHeader,
     streamStatus: streamStatusHeader,
+    timestamp: timestampHeader,
     run: runHeader,
     reset: resetHeader,
   } = useStreamText(contentHeader);
@@
         texts: [
-          { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 },
+          { text: textHeader, timestamp: timestampHeader },
         ].filter((item) => item.timestamp !== 0),

Also applies to: 1000-1002

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx` around lines 950 -
955, The header welcome text is being filtered out because you bind its
timestamp to Date.now()/0 instead of the stable timestamp returned by
useStreamText; update the destructuring of useStreamText to also capture the
returned timestamp (e.g., timestamp: timestampHeader) and replace any hardcoded
Date.now()/0 used when building contentHeader with that timestampHeader; apply
the same change for the other occurrences mentioned (lines ~1000-1002) so the
filter keeps the welcome copy after streamStatus becomes FINISHED.
packages/x-card/src/A2UI/Card.tsx-172-176 (1)

172-176: ⚠️ Potential issue | 🟠 Major

不要把已渲染 surface 的根节点写死成 root

这里已经保存了 beginRendering.root / pendingRenderRef,但后续 surfaceUpdate 仍然固定 getById('root')。只要某个 surface 的根 id 不是 root,或者先收到 beginRendering 再收到 surfaceUpdate,更新就会被静默丢掉。当前同一个 PR 里的 checkout-success 就已经用了 success-card 作为根节点。

🛠️ 建议修改方向
+  const renderedRootIdRef = useRef<string | null>(null);
+
       if ('surfaceUpdate' in cmd) {
         const nodeTree = transformerRef.current!.transform(cmd.surfaceUpdate.components, 'v0.8');
         pendingNodeTreeRef.current = nodeTree;
 
-        // If already rendered, update directly
-        if (hasRenderedRef.current) {
-          const rootNodeFromCache = transformerRef.current!.getById('root');
+        const activeRootId = renderedRootIdRef.current ?? pendingRenderRef.current?.root;
+        if (activeRootId && (hasRenderedRef.current || pendingRenderRef.current)) {
+          const rootNodeFromCache = transformerRef.current!.getById(activeRootId);
           if (rootNodeFromCache) {
             nextRootNode = rootNodeFromCache;
+            pendingRenderRef.current = null;
+            hasRenderedRef.current = true;
             hasRootNodeChange = true;
           }
         }
       }
@@
       if ('beginRendering' in cmd) {
         const { root } = cmd.beginRendering;
+        renderedRootIdRef.current = root;
         const nodeTree = transformerRef.current!.getById(root);
@@
       if ('deleteSurface' in cmd) {
+        renderedRootIdRef.current = null;
         nextRootNode = null;

Also applies to: 258-269, 280-303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/Card.tsx` around lines 172 - 176, The code is
hardcoding the rendered surface root as 'root', causing updates to be dropped
when the actual surface id differs; change surfaceUpdate handling to read the
currently pending root id from pendingRenderRef (set by beginRendering) and use
that id when calling getById instead of the literal 'root'. Specifically, update
the logic that uses getById('root') in the surfaceUpdate flow to reference
pendingRenderRef.current?.root (fallback to the previous behavior only if null),
and ensure pendingNodeTreeRef and hasRenderedRef are used consistently so
updates apply to the correct surface root for the component tree stored by
beginRendering.
packages/x-card/src/A2UI/Card.tsx-327-349 (1)

327-349: ⚠️ Potential issue | 🟠 Major

外层事件 payload 还没有走声明式 action context 解析。

现在这里直接把组件原始 context 透传给上层,action.context / action.event.context 里的 { path } 和字面量配置只参与了 dataModel 写回,没有参与最终事件 payload。这样一来,声明式 action context 对上层监听者是失效的。建议在应用 dataUpdates 之后,用 resolveActionContextV08/V09 基于 newDataModel 生成 payload,再和组件上下文合并。

🛠️ 建议修改
-    // Report event to upper layer
-    onAction?.({
-      name,
-      surfaceId: id,
-      context: { ...context },
-    });
+    const resolvedActionContext =
+      commandVersion === 'v0.9'
+        ? (resolveActionContextV09(actionConfig, newDataModel) ?? {})
+        : (resolveActionContextV08(actionConfig, newDataModel) ?? {});
+
+    // Report event to upper layer
+    onAction?.({
+      name,
+      surfaceId: id,
+      context: { ...resolvedActionContext, ...context },
+    });
-import { resolvePropsV08, extractDataUpdatesV08, applyDataModelUpdateV08 } from './Card.v0.8';
+import {
+  resolvePropsV08,
+  resolveActionContextV08,
+  extractDataUpdatesV08,
+  applyDataModelUpdateV08,
+} from './Card.v0.8';
@@
-import { resolvePropsV09, extractDataUpdatesV09, applyDataModelUpdateV09 } from './Card.v0.9';
+import {
+  resolvePropsV09,
+  resolveActionContextV09,
+  extractDataUpdatesV09,
+  applyDataModelUpdateV09,
+} from './Card.v0.9';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/Card.tsx` around lines 327 - 349, The current
handleAction forwards the original component context to onAction and ignores
declaration-based action context; after applying dataUpdates and producing
newDataModel (using setValueByPath and setDataModel), call the appropriate
resolver (resolveActionContextV09 when commandVersion === 'v0.9', otherwise
resolveActionContextV08) with actionConfig and newDataModel to produce the
resolved action context, then merge that resolved context into the payload you
send to onAction (e.g., context: { ...context, ...resolvedContext }) so that
declaration-based { path } and literal configs participate in the final event
payload.
packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx-114-121 (1)

114-121: ⚠️ Potential issue | 🟠 Major

这里也需要稳定 key

shop → checkout-success 时如果继续用 index,React 会复用旧的 XCard.Card 实例,而 Card 内部状态不会因为 id 改变自动清空。contentList 的节点都有 timestampid 属性,应改用这些稳定的值作为 key。

🛠️ 建议修改
-      return contentList.map((node, index) => {
+      return contentList.map((node) => {
         if ('text' in node && node.text) {
-          return <XMarkdown key={index}>{node.text}</XMarkdown>;
+          return <XMarkdown key={`text-${node.timestamp}`}>{node.text}</XMarkdown>;
         }

         if ('id' in node && node.id) {
-          return <XCard.Card key={index} id={node.id} />;
+          return <XCard.Card key={`card-${node.id}`} id={node.id} />;
         }
         return null;
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx` around lines 114 -
121, The map callback uses index for React keys which can cause XCard.Card
instances to be reused incorrectly; update the key usage in the contentList.map
(the XMarkdown and XCard.Card returns) to use stable identifiers from the node
(prefer node.id for card nodes and node.timestamp or node.id for text nodes)
instead of index so React will remount when id/timestamp changes.
packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx-115-122 (1)

115-122: ⚠️ Potential issue | 🟠 Major

不要用 index 作为 key,使用稳定的节点标识符。

这里会在 shop → checkout-success 切换时复用旧的 XMarkdownXCard.Card 实例,导致新 surface 继承旧 surface 的内部状态。由于 TextNodeCardNode 都包含 timestamp 属性,CardNode 还包含 id 属性,应改为稳定 key 后,surface 替换才会正确 remount 组件。

🛠️ 建议修改
-      return contentList.map((node, index) => {
+      return contentList.map((node) => {
         if ('text' in node && node.text) {
-          return <XMarkdown key={index}>{node.text}</XMarkdown>;
+          return <XMarkdown key={`text-${node.timestamp}`}>{node.text}</XMarkdown>;
         }

         if ('id' in node && node.id) {
-          return <XCard.Card key={index} id={node.id} />;
+          return <XCard.Card key={`card-${node.id}`} id={node.id} />;
         }
         return null;
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx` around lines 115 -
122, Replace the unstable index key used in the contentList.map render with a
stable node identifier so React will remount surfaces correctly: in the mapping
that returns XMarkdown and XCard.Card, use node.timestamp for text nodes
(TextNode) and node.id for card nodes (CardNode), falling back to node.timestamp
if id is missing; update the key props on XMarkdown and XCard.Card accordingly
(locate the contentList.map block that renders XMarkdown and XCard.Card).
packages/x/docs/x-card/demo/A2UI_v0.8/basic.tsx-28-35 (1)

28-35: ⚠️ Potential issue | 🟠 Major

使用稳定的 key 替换 index,避免状态复用问题。

booking 切换到 result 等场景时,contentList 可能会重新排序或变化。使用 index 作为 key 会导致 React 复用旧的 XCard.Card 实例,而该实例维护的 transformerdataModelcommandVersion 等状态不会被清理,容易将上一个 surface 的内部状态泄露到新的 surface 中。建议改用基于 node.idnode.timestamp 的稳定 key,并在表单提交后重点测试 surface 的切换是否正确清空了前一个 surface 的状态。

🛠️ 建议修改
-      return contentList.map((node, index) => {
+      return contentList.map((node) => {
         if ('text' in node && node.text) {
-          return <XMarkdown key={index}>{node.text}</XMarkdown>;
+          return <XMarkdown key={`text-${node.timestamp}`}>{node.text}</XMarkdown>;
         }

         if ('id' in node && node.id) {
-          return <XCard.Card key={index} id={node.id} />;
+          return <XCard.Card key={`card-${node.id}`} id={node.id} />;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/basic.tsx` around lines 28 - 35,
Replace unstable index keys to prevent React from reusing component instances:
in the contentList mapping that returns <XMarkdown> and <XCard.Card> (the map
over contentList producing XMarkdown and XCard.Card), stop using index as the
key and instead build a stable key from node properties (e.g. use node.id and
node.timestamp for cards and node.id or a stable id for text nodes); ensure
XCard.Card receives a key derived from node.id+node.timestamp so switching
between surfaces like booking→result unmounts the previous XCard.Card and clears
its transformer/dataModel/commandVersion state, then run the form-submit surface
switch test to confirm old state is not carried over.
packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx-83-107 (1)

83-107: ⚠️ Potential issue | 🟠 Major

最后一步会把同一个 submit_step 发两次。

handleNext()currentStep === 1 时先发一次普通事件,再调用 handleSubmit() 发一次带 submit: true 的同名事件。当前 App.handleAction() 会因此先弹一次中间态提示,再执行真正提交;如果后面把这个事件接到请求或埋点上,也会得到双份副作用。把事件派发放进分支里,最后一步只保留一次提交事件。

🛠️ 一个直接的改法
+  const emitAction = (values: any, extra: Record<string, any> = {}) => {
+    if (!action?.event?.name) {
+      return;
+    }
+    const context: Record<string, any> = {};
+    if (action.event.context) {
+      Object.keys(action.event.context).forEach((key) => {
+        context[key] = { values, ...extra };
+      });
+    }
+    onAction?.(action.event.name, context);
+  };
+
   const handleNext = async () => {
     try {
       const fieldsToValidate =
         currentStep === 0 ? ['username', 'email'] : ['password', 'confirmPassword'];
 
       await form.validateFields(fieldsToValidate);
       const values = form.getFieldsValue();
 
-      if (action?.event?.name) {
-        const context: Record<string, any> = {};
-        if (action.event.context) {
-          Object.keys(action.event.context).forEach((key) => {
-            context[key] = {
-              step: currentStep + 1,
-              values,
-            };
-          });
-        }
-        onAction?.(action.event.name, context);
-      }
-
       if (currentStep === 1) {
-        handleSubmit(values);
+        emitAction(values, { step: 2, submit: true });
       } else {
+        emitAction(values, { step: currentStep + 1 });
         setCurrentStep(currentStep + 1);
       }
     } catch (error) {
       console.log('Validation failed:', error);
     }
   };
-
-  const handleSubmit = (values: any) => {
-    if (action?.event?.name) {
-      const context: Record<string, any> = {};
-      if (action.event.context) {
-        Object.keys(action.event.context).forEach((key) => {
-          context[key] = {
-            step: 2,
-            values,
-            submit: true,
-          };
-        });
-      }
-      onAction?.(action.event.name, context);
-    }
-  };

Also applies to: 118-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx` around lines 83 -
107, handleNext() currently dispatches action.event.name unconditionally and
then calls handleSubmit(values) when currentStep === 1, causing the same
submit_step event to be sent twice; move the onAction dispatch into the step
branches so that for currentStep === 1 you only call handleSubmit (which should
dispatch the submit event with submit: true) and for other steps you call
onAction as the non-submit intermediate event. Update the logic in handleNext
(and the duplicate logic at lines 118-131) to build and send context only in the
branch that dispatches the event, referencing handleNext, handleSubmit,
onAction, currentStep, setCurrentStep, and action.event to locate and change the
code.
packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx-527-532 (1)

527-532: ⚠️ Potential issue | 🟠 Major

流式文案结束后会被自己过滤掉。

这里把文本节点的 timestamp 只在 RUNNING 时设值,状态变成 FINISHED 后就写成 0,随后又被 .filter() 删除,所以欢迎文案在流结束后会直接消失。useStreamText() 已经返回了稳定时间戳,直接复用它即可。

🛠️ 一个直接的改法
   const {
     text: textHeader,
     streamStatus: streamStatusHeader,
+    timestamp: textHeaderTimestamp,
     run: runHeader,
     reset: resetHeader,
   } = useStreamText(contentHeader);
@@
       content: {
-        texts: [
-          { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 },
-        ].filter((item) => item.timestamp !== 0),
+        texts:
+          textHeader.length > 0
+            ? [{ text: textHeader, timestamp: textHeaderTimestamp || Date.now() }]
+            : [],
         card,
       } as ContentType,

Also applies to: 559-565

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx` around lines 527 -
532, The streamed welcome message disappears because code sets text node
timestamps to 0 when streamStatusHeader becomes FINISHED and then filters out
zero-timestamp entries; instead reuse the stable timestamp returned by
useStreamText so entries keep their timestamp after FINISHED. Locate the
variables from useStreamText (textHeader, streamStatusHeader, runHeader,
resetHeader) and stop resetting timestamp to 0 on FINISHED — assign the stable
timestamp that useStreamText provides (or preserve the existing timestamp)
before any .filter() so the welcome text remains after streaming ends.
packages/x-card/src/A2UI/format/components.ts-37-40 (1)

37-40: ⚠️ Potential issue | 🟠 Major

v0.8 这里不要提前解包 literalString

packages/x-card/src/A2UI/Card.v0.8.tsresolvePropsV08() 还依赖 { literalString: string } 和路径字符串的区分。这里先把它拍平成普通字符串后,像 { literalString: '/user/name' } 这样的合法字面量会在后续再次被当成 path 解析,结果把展示文本误解析成数据绑定。

🛠️ 一个直接的改法
     if (isPathObject(val)) {
       props[key] = val.path;
     } else if (isLiteralStringValue(val)) {
-      props[key] = val.literalString;
+      props[key] = val;
     } else {
       props[key] = val;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/format/components.ts` around lines 37 - 40, The code
prematurely unwraps literalString in components.ts (the branch using
isLiteralStringValue) which breaks resolvePropsV08's need to distinguish {
literalString: string } objects from plain path strings; change the assignment
in that branch to preserve the original literal-string object (i.e., assign
props[key] = val or the same wrapper object) instead of props[key] =
val.literalString so resolvePropsV08 can detect literal vs path correctly;
update any nearby logic that assumes a raw string only if necessary and keep
references to isLiteralStringValue and resolvePropsV08 for locating the change.
packages/x-card/src/A2UI/Card.v0.9.ts-29-35 (1)

29-35: ⚠️ Potential issue | 🟠 Major

递归解析没有覆盖嵌套 action/event

这里仅对顶层 action 做了专门处理。只要 action 出现在数组项或子对象里,通用递归就会把 event.context 里的 { path } 提前解引用,后面的写回逻辑就拿不到目标路径了。

修复示例
   if (val && typeof val === 'object') {
     const result: Record<string, any> = {};
     for (const [k, v] of Object.entries(val)) {
-      result[k] = resolveValueV09(v, dataModel);
+      if (k === 'action') {
+        result[k] = resolveActionPropV09(v, dataModel);
+      } else if (k === 'event') {
+        result[k] = resolveActionEventV09(v, dataModel);
+      } else {
+        result[k] = resolveValueV09(v, dataModel);
+      }
     }
     return result;
   }

Also applies to: 104-108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/Card.v0.9.ts` around lines 29 - 35, The recursion
only special-cases top-level "action" in the for...of loop, so nested action
objects inside arrays or child objects get processed by resolveValueV09 and thus
prematurely dereference event.context.path; fix by updating the recursive
resolver(s) to detect and specially-handle any encountered property named
"action" (both when iterating object entries and when mapping arrays) and call
resolveActionPropV09 for that value instead of resolveValueV09; specifically
change the object/array traversal logic used by resolveValueV09 (and the
for...of loop you already modified) to branch on key === 'action' and delegate
to resolveActionPropV09 so nested action/event.context paths remain intact (also
apply the same change for the other occurrence around the 104-108 block).
🟡 Minor comments (7)
packages/x-card/src/A2UI/__tests__/catalog.test.ts-53-65 (1)

53-65: ⚠️ Potential issue | 🟡 Minor

这个测试现在不会真正验证失败分支。

Line 63-65 只是吞掉 rejected promise;如果 loadCatalog() 意外 resolve,这个用例也会通过。这里最好显式断言 reject,并顺带校验 fetch 确实被调用了。

💡 建议修改
-  it('should not register catalog without id', () => {
+  it('should not register catalog without id', async () => {
     const catalog: Catalog = {
       components: { Button: { type: 'object' } },
     };
     registerCatalog(catalog);
     // 没有 id,不应该被注册,fetch 会被调用
     mockFetch.mockResolvedValueOnce({
       ok: false,
       statusText: 'Not Found',
     });
-    return loadCatalog('undefined-catalog').catch(() => {
-      // 预期会失败
-    });
+    await expect(loadCatalog('undefined-catalog')).rejects.toThrow();
+    expect(mockFetch).toHaveBeenCalledWith('undefined-catalog');
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/__tests__/catalog.test.ts` around lines 53 - 65, The
test currently swallows a rejected promise and can falsely pass if loadCatalog
unexpectedly resolves; change the test to explicitly assert the rejection of
loadCatalog('undefined-catalog') and verify mockFetch was called: call
registerCatalog(...) as before, then either return
expect(loadCatalog('undefined-catalog')).rejects.toBeTruthy() and afterwards
assert mockFetch was called, or use async/await with try/catch where a resolved
path calls fail(), the catch asserts the error and then checks
mockFetch.mockResolvedValueOnce was used (i.e. mockFetch was called); locate
this logic around registerCatalog and loadCatalog in the test to update it.
packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx-142-144 (1)

142-144: ⚠️ Potential issue | 🟡 Minor

Space 组件 vertical 属性使用错误

与 v0.8 demo 相同的问题,Ant Design Space 组件应使用 direction="vertical" 而非 vertical 布尔属性。

🔧 建议修复
-      <Space vertical style={{ width: '100%' }} size={12}>
+      <Space direction="vertical" style={{ width: '100%' }} size={12}>
         {children}
       </Space>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx` around lines 142 - 144, The
Space component is using a non-existent boolean prop vertical; update the Space
usage in the demo (the Space element wrapping {children}) to use
direction="vertical" instead of vertical and keep other props (style, size)
unchanged so it renders vertically with Ant Design v0.9.
packages/x/docs/x-card/demo/A2UI_v0.8/nested-interaction.tsx-542-585 (1)

542-585: ⚠️ Potential issue | 🟡 Minor

useStreamText hook 缺少组件卸载时的定时器清理

当组件卸载时,如果定时器仍在运行,会导致内存泄漏和对已卸载组件的状态更新警告。建议添加 cleanup effect。

🔧 建议添加清理逻辑
 const useStreamText = (text: string) => {
   const textRef = React.useRef(0);
   const [textIndex, setTextIndex] = React.useState(0);
   const textTimestamp = React.useRef(0);
   const [streamStatus, setStreamStatus] = useState('INIT');
   const timerRef = useRef<NodeJS.Timeout | null>(null);

+  // Cleanup on unmount
+  useEffect(() => {
+    return () => {
+      if (timerRef.current) {
+        clearInterval(timerRef.current);
+      }
+    };
+  }, []);
+
   const run = useCallback(() => {
     // ... existing code
   }, [text]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/nested-interaction.tsx` around lines
542 - 585, Add a cleanup effect to useStreamText that clears the interval stored
in timerRef when the component using the hook unmounts: implement a
React.useEffect with an empty dependency array that returns a cleanup function
which checks timerRef.current, calls clearInterval(timerRef.current) and sets
timerRef.current = null; this ensures any interval started by run is cleared on
unmount (complementing the existing reset logic) and prevents state updates
after unmount.
packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx-567-615 (1)

567-615: ⚠️ Potential issue | 🟡 Minor

useStreamText hook 缺少卸载清理

与 v0.8 demo 中的 useStreamText 存在相同问题,缺少组件卸载时的定时器清理。建议添加 cleanup effect。

🔧 建议添加清理逻辑
 const useStreamText = (text: string) => {
   // ... existing refs and state
   
+  useEffect(() => {
+    return () => {
+      if (timerRef.current) {
+        clearInterval(timerRef.current);
+      }
+    };
+  }, []);
+
   const run = useCallback(() => {
     // ...
   }, [text]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx` around lines 567 - 615, The
useStreamText hook leaks timers because timerRef is not cleared on component
unmount; add a cleanup effect inside useStreamText that clears and nulls
timerRef (clearInterval(timerRef.current) and timerRef.current = null) to run on
unmount (useEffect(() => () => { ... }, [])), ensuring any running interval
started by run() is cancelled when the component using useStreamText unmounts;
update references to timerRef, run, reset, and ensure streamStatus/textIndex
state remain consistent after cleanup.
packages/x-card/src/A2UI/utils.ts-13-27 (1)

13-27: ⚠️ Potential issue | 🟡 Minor

添加路径验证以防止原型污染(可选改进)

setValueByPath 函数虽然使用动态路径分段进行对象属性赋值,但实际风险较低,因为路径来自内部配置而非外部用户输入。不过,添加防御性验证是不错的安全实践。

建议:过滤危险属性名
+const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
+
+function isSafeKey(key: string): boolean {
+  return !UNSAFE_KEYS.includes(key);
+}
+
 export function setValueByPath(
   obj: Record<string, any>,
   path: string,
   value: any,
 ): Record<string, any> {
   const parts = path.replace(/^\//, '').split('/');
+  
+  if (!parts.every(isSafeKey)) {
+    console.warn('Unsafe path detected:', path);
+    return obj;
+  }
+  
   const next = { ...obj };
   let cur: Record<string, any> = next;
   for (let i = 0; i < parts.length - 1; i++) {
     cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {};
     cur = cur[parts[i]];
   }
   cur[parts[parts.length - 1]] = value;
   return next;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/utils.ts` around lines 13 - 27, setValueByPath
currently writes path segments directly into objects which could enable
prototype pollution if untrusted paths are ever used; update setValueByPath to
validate/filter each segment before assignment (in the function setValueByPath)
by rejecting or skipping dangerous keys such as "__proto__", "prototype",
"constructor" (and optionally keys starting with "constructor" or "__"), and
throw or return the original object when such a segment is encountered; perform
this check inside the loop that iterates parts so dangerous segments are never
used to create/assign properties and ensure the function still returns a
shallow-copied top-level object (next) as before.
packages/x/docs/x-card/demo/A2UI_v0.8/streaming.tsx-555-562 (1)

555-562: ⚠️ Potential issue | 🟡 Minor

会同时出现两条进度条。

RestaurantList 已经根据 isStreaming 渲染了 LoadingProgress,这里再挂一个根级 progress 节点会让加载阶段出现双进度条;而且这个根级节点的 status 被写死为 active,完成后还会一直停在 100%。建议保留一处进度展示即可。

🛠️ 一种更直接的收敛方式
-      {
-        id: 'progress',
-        component: {
-          LoadingProgress: {
-            percent: { path: '/progress' },
-            status: { literalString: 'active' },
-          },
-        },
-      },
       {
         id: 'restaurant-list',
         component: {
           RestaurantList: {
@@
           Container: {
             children: {
-              explicitList: ['title', 'progress', 'restaurant-list'],
+              explicitList: ['title', 'restaurant-list'],
             },
           },
         },

Also applies to: 577-579

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/streaming.tsx` around lines 555 - 562,
The root-level progress node with id 'progress' is causing a duplicate progress
bar alongside RestaurantList's own LoadingProgress (rendered based on
isStreaming) and also hardcodes status to 'active' so it stays at 100%; remove
the root-level progress node (the entry with id 'progress' and component
LoadingProgress) or at minimum stop duplicating by binding its status/percent to
the streaming state (e.g., use the same /progress percent and dynamic status
rather than literal 'active'); also apply the same removal/fix to the similar
block at lines 577-579 so only RestaurantList controls the visible progress
indicator.
packages/x-card/src/A2UI/Card.v0.9.ts-159-165 (1)

159-165: ⚠️ Potential issue | 🟡 Minor

不要把 undefined 当成“未提供”。

这里按 componentValue !== undefined 过滤,会让组件显式清空值时更新被吞掉,旧值继续留在 dataModel。建议改成按 key 是否存在判断。

修复示例
-      const componentValue = componentContext[key];
-      if (componentValue !== undefined) {
-        updates.push({ path: (val as { path: string }).path, value: componentValue });
-      }
+      if (Object.prototype.hasOwnProperty.call(componentContext, key)) {
+        updates.push({ path: (val as { path: string }).path, value: componentContext[key] });
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/Card.v0.9.ts` around lines 159 - 165, The loop over
Object.entries(context) currently ignores component-provided explicit undefined
values because it uses "componentValue !== undefined"; change the presence check
to whether the key exists on componentContext (e.g., use
Object.prototype.hasOwnProperty.call(componentContext, key) or "key in
componentContext") so explicit clears are treated as updates; update the branch
that pushes to updates (in the loop referencing context, isPathObject,
componentContext, and updates) to use that existence check and still push {
path: (val as { path: string }).path, value: componentContext[key] } so
dataModel receives explicit null/undefined clears.
🧹 Nitpick comments (13)
packages/x/docs/x-card/demo/A2UI_v0.9/catalog-streaming.json (1)

236-236: 空的 functions 对象

当前 functions 对象为空。如果这是为未来功能预留的占位符,建议在注释中说明;如果不需要,可以考虑移除以保持文件简洁。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-streaming.json` at line 236,
The JSON contains an empty "functions" object which is ambiguous; either remove
the "functions" key if it's unused to keep the file concise, or replace it with
a documented placeholder (e.g., add a JSON string field like "_note": "functions
placeholder for future use") or an adjacent comment in your repo docs explaining
it's reserved; locate the "functions" object in the JSON and apply one of these
two fixes to make intent explicit.
packages/x/docs/react/introduce.en-US.md (1)

17-19: 链接文本可改进(可选)

静态分析工具提示 "here" 作为链接文本描述性不足,不利于可访问性。不过当前文件中其他章节也使用相同的 "here" 模式(如第 11、15、23、27 行),建议统一改进或保持一致。

♻️ 建议的改进(可选)
-A dynamic card rendering component based on the A2UI protocol, enabling AI Agents to dynamically build interactive interfaces through structured JSON message streams. Supports streaming rendering, data binding, and reactive updates. Suitable for AI assistant interfaces, smart forms, data visualization, and more. For details, click [here](/x-cards/introduce).
+A dynamic card rendering component based on the A2UI protocol, enabling AI Agents to dynamically build interactive interfaces through structured JSON message streams. Supports streaming rendering, data binding, and reactive updates. Suitable for AI assistant interfaces, smart forms, data visualization, and more. For details, see the [X-Card documentation](/x-cards/introduce).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/react/introduce.en-US.md` around lines 17 - 19, Replace the
non-descriptive link text "here" in the README snippet for "@ant-design/x-card"
(the markdown link [here](/x-cards/introduce)) with a more descriptive label
like "Introduction", "Documentation: Introduction", or "x-cards 介绍", and apply
the same descriptive wording consistently to the other occurrences of [here] in
this file to improve accessibility and maintain consistency across the document.
packages/x/docs/x-card/a2ui-v-0-8.en-US.md (1)

27-27: v0.8 文档页面中 commands 类型顺序可能需要调整。

在 v0.8 文档页面,commands 属性类型显示为 A2UICommand_v0_9 | XAgentCommand_v0_8,建议将 v0.8 类型放在前面以保持一致性:XAgentCommand_v0_8 | A2UICommand_v0_9

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/a2ui-v-0-8.en-US.md` at line 27, The table entry for
the commands property currently lists the types as "A2UICommand_v0_9 |
XAgentCommand_v0_8"; update that cell so the v0.8 type appears first by changing
it to "XAgentCommand_v0_8 | A2UICommand_v0_9" to match v0.8 documentation
ordering—locate the commands row in the A2UI docs (the table row containing
"commands | A2UI command object") and swap the type order accordingly.
packages/x/docs/x-card/demo/A2UI_v0.8/form-validation.tsx (2)

105-108: 生产代码中存在 console.log。

建议在 demo 代码中移除或替换 console.log,或者至少添加注释说明这是用于调试目的。

♻️ 建议的修复
     } catch (error) {
       // Validation failed, do nothing
-      console.log('Validation failed:', error);
+      // Validation failed - form displays error messages to user
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/form-validation.tsx` around lines 105 -
108, The catch block currently uses console.log('Validation failed:', error)
which is not appropriate for production/demo code; replace this with either
removing the console statement, using a proper logger (e.g., processLogger.error
or a project-provided logger), or add a clear comment that it's only for
debugging if you must keep it; update the catch in the form validation code to
avoid console.log by using the chosen logger or no-op and preserve the original
validation handling.

537-543: useEffect 依赖数组包含 sessionKey 可能导致意外行为。

sessionKey 变化时(重新加载时),此 effect 会立即执行,但此时 streamStatusHeader 仍为 'INIT',所以不会触发命令。不过,将 sessionKey 包含在依赖数组中可能会在未来重构时引入 bug。建议添加注释说明此设计意图,或考虑使用 ref 来追踪 session 变化。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.8/form-validation.tsx` around lines 537 -
543, The useEffect watching streamStatusHeader and sessionKey can run
unexpectedly when sessionKey changes; update the effect to avoid accidental
triggers by either (a) removing sessionKey from the dependency array and
documenting the intent with a clear comment above the useEffect referencing
streamStatusHeader and
onAgentCommand/FormSurfaceUpdateCommand/FormDataModelUpdateCommand/FormBeginRenderingCommand,
or (b) keep sessionKey out of deps and instead track session changes with a ref
(e.g., sessionKeyRef) and check sessionKeyRef.current inside the effect before
calling onAgentCommand, ensuring the commands only fire when streamStatusHeader
=== 'FINISHED'; include a short inline comment describing the chosen approach to
prevent future regressions.
packages/x/docs/x-card/a2ui-v-0-9.zh-CN.md (1)

2-2: 文档排序可能冲突。

v0.8 和 v0.9 文档页面都设置了 order: 2,这可能导致文档导航中的排序不确定。建议为不同版本设置不同的 order 值(例如 v0.8 为 2,v0.9 为 3)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/a2ui-v-0-9.zh-CN.md` at line 2, The frontmatter
"order" value is duplicated between versions and causes navigation ordering
conflicts; update the frontmatter in the v0.9 doc (the "order" key in
packages/x/docs/x-card/a2ui-v-0-9.zh-CN.md) to a unique value (e.g., change
order from 2 to 3) so it does not collide with the v0.8 page—ensure the "order"
key in the v0.8 file remains 2 and the v0.9 file uses the new distinct integer.
packages/x-card/tests/setup.ts (1)

20-25: matchMedia mock 使用了已弃用的 listener API。

addListenerremoveListener 是已弃用的 API,现代浏览器使用 addEventListenerremoveEventListener。建议同时提供两组方法以确保测试兼容性。

♻️ 建议的修复
       value: jest.fn((query) => ({
         matches: query.includes('max-width'),
         addListener: jest.fn(),
         removeListener: jest.fn(),
+        addEventListener: jest.fn(),
+        removeEventListener: jest.fn(),
+        dispatchEvent: jest.fn(),
       })),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/tests/setup.ts` around lines 20 - 25, The matchMedia mock in
tests/setup.ts currently exposes only the deprecated addListener/removeListener
methods; update the jest.fn mock returned by the value function (the anonymous
function passed to jest.fn) to also provide modern DOM event methods
addEventListener and removeEventListener (as jest.fn()), while keeping the
existing addListener/removeListener for backward compatibility; ensure the mock
shape returned by that function includes matches, addEventListener,
removeEventListener, addListener, and removeListener so tests using either API
succeed.
packages/x/docs/x-card/demo/A2UI_v0.9/catalog-cart.json (1)

359-383: $defs.DynamicValue 已定义但未被引用

$defs.DynamicValue 定义了通用的动态值模式,但各组件属性中重复定义了相同的 oneOf 模式而未使用 $ref。可以考虑使用引用来减少重复。

♻️ 示例:使用 $ref 引用
"text": {
  "description": "The text content to display.",
  "$ref": "#/$defs/DynamicValue"
}

替代当前重复的 oneOf 定义。这是可选的优化,当前实现功能正确。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-cart.json` around lines 359 -
383, The schema defines a reusable pattern as $defs.DynamicValue but other
component properties duplicate the same oneOf block instead of referencing it;
replace those repeated oneOf definitions with a JSON Schema $ref to
"#/$defs/DynamicValue" (e.g., update property schemas that currently inline the
oneOf to use "$ref": "#/$defs/DynamicValue") so the DynamicValue definition is
actually reused and duplication is eliminated.
packages/x-card/src/A2UI/__tests__/integration.test.tsx (2)

573-630: process.env.NODE_ENV 修改后的恢复应使用 try/finally

如果测试在恢复环境变量之前抛出异常,NODE_ENV 将保持修改状态,可能影响后续测试。建议使用 try/finally 确保恢复。

♻️ 建议改进
     it('should validate component against catalog in development', async () => {
       const originalEnv = process.env.NODE_ENV;
       process.env.NODE_ENV = 'development';
 
+      try {
         registerCatalog({
           // ... catalog definition
         });
 
         render(/* ... */);
 
         await waitFor(() => {
           expect(console.warn).toHaveBeenCalled();
         });
-
-      process.env.NODE_ENV = originalEnv;
+      } finally {
+        process.env.NODE_ENV = originalEnv;
+      }
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/__tests__/integration.test.tsx` around lines 573 -
630, The test "should validate component against catalog in development" mutates
process.env.NODE_ENV but doesn't guarantee restoration on failure; wrap the part
that sets process.env.NODE_ENV = 'development' and the subsequent
registerCatalog/render/assert logic in a try/finally so that
process.env.NODE_ENV is always reset to originalEnv in the finally block (keep
the existing registerCatalog, render(Box ...), waitFor and console.warn
assertions intact and only move the restoration into finally).

709-713: waitFor 中使用无意义的断言

expect(true).toBe(true) 不会等待任何实际条件,waitFor 会立即通过。如果需要等待 effect 执行,应该使用更有意义的断言或考虑使用 act 配合 await

♻️ 建议改进
       // 等待 catalog 加载完成(使用 setTimeout 让 useEffect 执行完成)
       await waitFor(() => {
-        // 由于 catalog 已通过 registerCatalog 注册,loadCatalog 会直接返回缓存的 catalog
-        // 不需要检查 console.log,只需等待足够时间让 effect 执行
-        expect(true).toBe(true);
+        // 由于 catalog 已通过 registerCatalog 注册,直接可用
+        // 可以检查 Card 是否已渲染
+        expect(screen.getByTestId('card1') || true).toBeTruthy();
       });

或者如果只是需要让 React 完成更新,可以简化为:

await act(async () => {
  await Promise.resolve();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/__tests__/integration.test.tsx` around lines 709 -
713, The test uses waitFor with a no-op assertion (expect(true).toBe(true))
which makes waitFor succeed immediately; update the test in
packages/x-card/src/A2UI/__tests__/integration.test.tsx to wait for a real
side-effect or state change instead (e.g., assert the catalog is
cached/registered or that a DOM node updated) by replacing the dummy assertion
inside the waitFor callback that references the catalog registration with a
concrete expect against the relevant value, or alternatively replace the waitFor
block with an explicit React update flush using act (await act(async () => {
await Promise.resolve(); })) to ensure effects in the component under test have
run; locate usages of waitFor and the surrounding comments about registerCatalog
to apply the change.
packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx (1)

845-852: useEffect 依赖项说明

此 useEffect 使用了 onAgentCommand 函数,但未将其添加到依赖数组。由于 onAgentCommand 在组件内定义且引用 setter 函数,这里不会导致问题,但如果使用了 exhaustive-deps lint 规则可能会警告。

考虑将 onAgentCommanduseCallback 包装或添加禁用 lint 注释。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx` around lines 845 - 852, The
useEffect depends on the onAgentCommand function but doesn't include it in the
dependency array, which can trigger exhaustive-deps warnings; wrap
onAgentCommand in useCallback (preserving its current setter-based logic) so it
has a stable identity, then include onAgentCommand in the useEffect deps, or
alternatively explicitly document and disable the lint rule for that effect;
reference the onAgentCommand definition and the useEffect block that checks
streamStatusHeader === 'FINISHED' to apply the change.
packages/x-card/src/A2UI/__tests__/utils.test.ts (1)

60-110: 建议添加原型污染场景的测试用例

鉴于 setValueByPath 存在原型污染风险(如 utils.ts 中所述),建议添加测试用例验证函数对恶意路径的处理行为。

🧪 建议添加的测试用例
it('should handle __proto__ path safely', () => {
  const obj = { name: 'Alice' };
  const result = setValueByPath(obj, '/__proto__/polluted', 'yes');
  // 验证 Object.prototype 未被污染
  expect(({} as any).polluted).toBeUndefined();
});

it('should handle constructor path safely', () => {
  const obj = { name: 'Alice' };
  const result = setValueByPath(obj, '/constructor/prototype/polluted', 'yes');
  expect(({} as any).polluted).toBeUndefined();
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/__tests__/utils.test.ts` around lines 60 - 110, Add
tests for prototype-pollution paths to ensure setValueByPath (in utils.ts) does
not modify Object.prototype or other constructors: add cases calling
setValueByPath(obj, '/__proto__/polluted', 'yes') and setValueByPath(obj,
'/constructor/prototype/polluted', 'yes') and assert that ({} as any).polluted
is undefined after each call; if tests fail, update setValueByPath to skip keys
like "__proto__", "constructor", "prototype" when traversing/creating path
segments to prevent prototype pollution.
packages/x-card/src/A2UI/types/command_v0.9.ts (1)

46-49: 如果运行时允许无 catalog 的 surface,这里的类型别把 catalogId 写成必填。

同一组变更里已经有 falsy catalogId 的处理分支,现在这个公开类型仍然要求调用方传字符串,只会把使用者逼到传 '' 这种占位值。建议让类型和运行时语义二选一地收敛:要么把字段改成可选,要么删掉运行时分支并在入口校验。

🛠️ 若该场景是合法输入,可直接这样收敛
 interface CreateSurfaceCommand {
   version: 'v0.9';
   createSurface: {
     surfaceId: string;
-    catalogId: string; // 必需,组件目录 URL 或本地标识
+    catalogId?: string; // 可选;未提供时跳过 catalog 加载
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x-card/src/A2UI/types/command_v0.9.ts` around lines 46 - 49, The
public type for createSurface currently forces catalogId to be a required string
while runtime accepts falsy values—change the type to reflect runtime by making
catalogId optional (use catalogId?: string) in the createSurface definition in
command_v0.9.ts and update any related exported interfaces or consumers that
assume it is non-null so they handle undefined/falsy cases; alternatively, if
you prefer strictness, remove the runtime branch that accepts falsy catalogId
and add validation at the entry point, but do not leave the public type out of
sync with runtime behavior.

antd: '^6.1.1',
'@ant-design/x': '^2.0.0',
'@ant-design/x-markdown': '^2.0.0',
'@ant-design/x-card': '^2.0.0',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
npm view `@ant-design/x-card` version versions --json

Repository: ant-design/x

Length of output: 775


@ant-design/x-card 在 npm 公开注册表中不存在。

Line 65 引用的依赖 @ant-design/x-card@^2.0.0 在 npm 注册表中返回 404 错误。由于 Sandpack 是浏览器端代码沙箱环境,它需要从 npm 注册表解析并安装依赖,因此无法使用不存在于公开注册表中的包。这会导致示例完全无法加载依赖。

需确认:

  • 该包是否尚未发布到 npm
  • 包名是否正确
  • 如果包是私有或仅供本地开发,需采用其他方式(如 workspaces)来配置 Sandpack 依赖
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/x/.dumi/theme/builtins/Sandpack/index.tsx` at line 65, The
dependency list in the Sandpack builtin includes a nonexistent package entry
'@ant-design/x-card': '^2.0.0' which causes npm 404s; locate the dependencies
object in packages/x/.dumi/theme/builtins/Sandpack/index.tsx and either (a)
correct the package name to the published package, (b) remove the entry if the
component should not be bundled, or (c) if this is a local/private package,
replace the npm entry with a Sandpack-compatible resolution such as pointing to
the package's published CDN build or configuring Sandpack to use your
workspace/local build (e.g., aliasing to the local package bundle); update the
dependencies mapping and any related import/alias logic so Sandpack can resolve
the module at runtime.

@github-actions github-actions bot merged commit 4c2f82c into main Mar 30, 2026
16 checks passed
@github-actions github-actions bot deleted the main-merge-feature branch March 30, 2026 03:17
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Bundle Report

Changes will increase total bundle size by 92 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
antdx-array-push 1.92MB 92 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: antdx-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
antdx.min.js 92 bytes 1.92MB 0.0%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants