@antglobal/copilot-cards-mini-program 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,32 @@
7
7
  * Mirrors the web-side `createWebActionContext` but routes calls
8
8
  * through the adapter instead of browser globals.
9
9
  */
10
+ function abortedRequestError() {
11
+ return new DOMException('The request was aborted', 'AbortError');
12
+ }
13
+ function awaitMiniProgramRequest(request, signal) {
14
+ if (!signal)
15
+ return request;
16
+ if (signal.aborted) {
17
+ // Observe a request implementation that has already started so a later
18
+ // rejection cannot become unhandled.
19
+ void request.catch(() => undefined);
20
+ return Promise.reject(abortedRequestError());
21
+ }
22
+ return new Promise((resolve, reject) => {
23
+ const onAbort = () => {
24
+ reject(abortedRequestError());
25
+ };
26
+ signal.addEventListener('abort', onAbort, { once: true });
27
+ request.then(response => {
28
+ signal.removeEventListener('abort', onAbort);
29
+ resolve(response);
30
+ }, error => {
31
+ signal.removeEventListener('abort', onAbort);
32
+ reject(error);
33
+ });
34
+ });
35
+ }
10
36
  // ─── Factory ─────────────────────────────────────────────────────
11
37
  /**
12
38
  * Create an ActionRunnerContext wired to mini-program platform APIs.
@@ -24,13 +50,18 @@
24
50
  function createMiniProgramActionContext(adapter, options = {}) {
25
51
  return {
26
52
  fetch: async (url, init) => {
53
+ if (init?.signal?.aborted) {
54
+ throw abortedRequestError();
55
+ }
27
56
  // Convert standard fetch interface to mini-program adapter request
28
- const response = await adapter.request({
57
+ const requestOptions = {
29
58
  url,
30
59
  method: init?.method ?? 'GET',
31
60
  headers: init?.headers,
32
61
  data: init?.body ? JSON.parse(init.body) : undefined,
33
- });
62
+ ...(init?.signal ? { signal: init.signal } : {}),
63
+ };
64
+ const response = await awaitMiniProgramRequest(adapter.request(requestOptions), init?.signal);
34
65
  // Wrap as a fetch-compatible Response
35
66
  return {
36
67
  ok: response.statusCode >= 200 && response.statusCode < 300,
@@ -1,4 +1,4 @@
1
- import { ExpressionContext, CardSchemaInput } from '@antglobal/copilot-cards-core';
1
+ import { ExpressionContext, CardSchemaInput, MaterializedCard } from '@antglobal/copilot-cards-core';
2
2
  import { MiniProgramAdapter } from '../adapter/index.js';
3
3
  export { transformNode, transformTree } from './transform.js';
4
4
 
@@ -46,6 +46,11 @@ interface MiniProgramCardInstance {
46
46
  /** Dispose the card instance and clean up resources */
47
47
  dispose(): void;
48
48
  }
49
+ interface MiniProgramRendererSnapshot {
50
+ tree: MiniProgramRenderNode;
51
+ materialized: MaterializedCard | null;
52
+ variables: Record<string, any>;
53
+ }
49
54
  /**
50
55
  * Create a mini-program card instance from a schema.
51
56
  *
@@ -62,6 +67,8 @@ interface MiniProgramCardInstance {
62
67
  * ```
63
68
  */
64
69
  declare function renderForMiniProgram(schemaInput: CardSchemaInput, options: MiniProgramRendererOptions): MiniProgramCardInstance;
70
+ declare function getMiniProgramRendererSnapshot(instance: MiniProgramCardInstance): MiniProgramRendererSnapshot;
71
+ declare function syncMiniProgramAuthoritativeVariables(instance: MiniProgramCardInstance, nextRoot: Record<string, any>): MiniProgramRendererSnapshot;
65
72
 
66
- export { renderForMiniProgram };
67
- export type { MiniProgramCardInstance, MiniProgramRenderNode, MiniProgramRendererOptions };
73
+ export { getMiniProgramRendererSnapshot, renderForMiniProgram, syncMiniProgramAuthoritativeVariables };
74
+ export type { MiniProgramCardInstance, MiniProgramRenderNode, MiniProgramRendererOptions, MiniProgramRendererSnapshot };
@@ -1,6 +1,6 @@
1
- import { normalizeSchema, validateSchema, parseSchema, createLifecycleManager, runActionSteps, resolveActionRef } from '@antglobal/copilot-cards-core';
1
+ import { normalizeSchema, validateSchema, requiresBindingMaterialization, parseSchema, createLifecycleManager, cloneJsonData, runActionSteps, materializeCard, resolveActionRef, createA2UIParameterResolver, createExpressionContext, findAffectedRepeatOwners } from '@antglobal/copilot-cards-core';
2
2
  import { createMiniProgramActionContext } from '../action-context/index.js';
3
- import { transformTree } from './transform.js';
3
+ import { transformTree, transformBoundTree } from './transform.js';
4
4
  export { transformNode } from './transform.js';
5
5
 
6
6
  /**
@@ -11,6 +11,8 @@ export { transformNode } from './transform.js';
11
11
  * this renderer doesn't produce DOM. Instead, it produces a normalised
12
12
  * data tree that can be consumed by mini-program templates.
13
13
  */
14
+ const rendererSnapshots = new WeakMap();
15
+ const boundRendererControllers = new WeakMap();
14
16
  // ─── Main API ────────────────────────────────────────────────────
15
17
  /**
16
18
  * Create a mini-program card instance from a schema.
@@ -35,6 +37,12 @@ function renderForMiniProgram(schemaInput, options) {
35
37
  if (errors.length > 0) {
36
38
  throw new Error(`[renderForMiniProgram] Invalid schema:\n${errors.join('\n')}`);
37
39
  }
40
+ if (!requiresBindingMaterialization(schema)) {
41
+ return renderStaticForMiniProgram(schema, options);
42
+ }
43
+ return renderBoundForMiniProgram(schema, options);
44
+ }
45
+ function renderStaticForMiniProgram(schema, options) {
38
46
  // 2. Parse into render tree
39
47
  const tree = parseSchema(schema);
40
48
  // 3. Reactive variables store
@@ -72,7 +80,7 @@ function renderForMiniProgram(schemaInput, options) {
72
80
  // 9. Compute initial data
73
81
  let currentTree = transformTree(tree, variables);
74
82
  // ── Instance API ───────────────────────────────────────────────
75
- return {
83
+ const instance = {
76
84
  getData() {
77
85
  return { tree: currentTree };
78
86
  },
@@ -80,6 +88,11 @@ function renderForMiniProgram(schemaInput, options) {
80
88
  variables = { ...variables, ...newVars };
81
89
  rebuildActionContext();
82
90
  currentTree = transformTree(tree, variables);
91
+ rendererSnapshots.set(instance, {
92
+ tree: currentTree,
93
+ materialized: null,
94
+ variables,
95
+ });
83
96
  return { tree: currentTree };
84
97
  },
85
98
  async handleEvent(nodeId, eventName, eventDetail) {
@@ -104,12 +117,327 @@ function renderForMiniProgram(schemaInput, options) {
104
117
  // Re-compute tree after action execution (variables may have changed)
105
118
  rebuildActionContext();
106
119
  currentTree = transformTree(tree, variables);
120
+ rendererSnapshots.set(instance, {
121
+ tree: currentTree,
122
+ materialized: null,
123
+ variables,
124
+ });
107
125
  },
108
126
  dispose() {
109
127
  abortController.abort();
110
128
  lifecycleManager.dispose(actionContext);
111
129
  },
112
130
  };
131
+ rendererSnapshots.set(instance, {
132
+ tree: currentTree,
133
+ materialized: null,
134
+ variables,
135
+ });
136
+ return instance;
137
+ }
138
+ function renderBoundForMiniProgram(schema, options) {
139
+ const schemaActions = schema.actions ?? {};
140
+ const variables = {};
141
+ const lifecycleManager = createLifecycleManager();
142
+ const abortController = new AbortController();
143
+ const inflightRequests = new Map();
144
+ let revision = 0;
145
+ let currentTree;
146
+ let currentMaterialized;
147
+ let nodeEventsMap = new Map();
148
+ let instance;
149
+ let actionQueue = Promise.resolve();
150
+ let disposed = false;
151
+ const lifecycleMounts = new Map();
152
+ function transactionConflict() {
153
+ return new Error('[MiniProgramRenderer] BOUND_TRANSACTION_CONFLICT: '
154
+ + (disposed
155
+ ? 'renderer instance is disposed'
156
+ : 'renderer state changed before the candidate could commit'));
157
+ }
158
+ function assertCurrentRevision(baseRevision) {
159
+ if (disposed || baseRevision !== revision) {
160
+ throw transactionConflict();
161
+ }
162
+ }
163
+ function installDraftRoot(draft) {
164
+ for (const key of Object.keys(variables)) {
165
+ if (!Reflect.deleteProperty(variables, key)) {
166
+ throw new Error(`[MiniProgramRenderer] Cannot remove bound variable "${key}"`);
167
+ }
168
+ }
169
+ for (const key of Object.keys(draft)) {
170
+ Object.defineProperty(variables, key, {
171
+ value: draft[key],
172
+ enumerable: true,
173
+ configurable: true,
174
+ writable: true,
175
+ });
176
+ }
177
+ }
178
+ function rebindMaterializedRoot(root) {
179
+ const visitedScopes = new WeakSet();
180
+ function walk(node) {
181
+ if (!visitedScopes.has(node.scope)) {
182
+ node.scope.root = variables;
183
+ visitedScopes.add(node.scope);
184
+ }
185
+ node.children.forEach(walk);
186
+ }
187
+ walk(root);
188
+ }
189
+ function publishSnapshot() {
190
+ const snapshot = {
191
+ tree: currentTree,
192
+ materialized: currentMaterialized,
193
+ variables,
194
+ };
195
+ if (instance) {
196
+ rendererSnapshots.set(instance, snapshot);
197
+ }
198
+ return snapshot;
199
+ }
200
+ function prepareState(draft, changedPaths, baseRevision) {
201
+ assertCurrentRevision(baseRevision);
202
+ // Dependency selection is intentionally computed from the last committed
203
+ // metadata. Mini's public API still publishes a complete transformed tree.
204
+ if (currentMaterialized) {
205
+ for (const changedPath of changedPaths) {
206
+ findAffectedRepeatOwners(currentMaterialized, changedPath);
207
+ }
208
+ }
209
+ const nextMaterialized = materializeCard(schema, draft);
210
+ const nextTree = transformBoundTree(nextMaterialized.root);
211
+ // Committed event contexts must terminate at the stable variables object.
212
+ // The nested local objects are values from `draft`; those same references
213
+ // become reachable from the stable root in the final synchronous commit.
214
+ rebindMaterializedRoot(nextMaterialized.root);
215
+ const nextNodeEventsMap = buildBoundNodeEventsMap(nextMaterialized.root, schemaActions);
216
+ assertCurrentRevision(baseRevision);
217
+ installDraftRoot(draft);
218
+ currentTree = nextTree;
219
+ currentMaterialized = nextMaterialized;
220
+ nodeEventsMap = nextNodeEventsMap;
221
+ revision += 1;
222
+ return publishSnapshot();
223
+ }
224
+ function cloneCommittedVariables() {
225
+ return cloneJsonData(variables);
226
+ }
227
+ function defineDraftValues(draft, values) {
228
+ const clonedValues = cloneJsonData(values);
229
+ const changedPaths = [];
230
+ for (const key of Object.keys(clonedValues)) {
231
+ Object.defineProperty(draft, key, {
232
+ value: clonedValues[key],
233
+ enumerable: true,
234
+ configurable: true,
235
+ writable: true,
236
+ });
237
+ changedPaths.push(`/${escapeJsonPointerSegment(key)}`);
238
+ }
239
+ return changedPaths;
240
+ }
241
+ function createLifecycleActionContext(node, allowAfterDispose = false) {
242
+ const platformContext = createMiniProgramActionContext(options.adapter, {
243
+ setVariable: (key, value) => {
244
+ if (allowAfterDispose || !disposed)
245
+ variables[key] = value;
246
+ },
247
+ emit: (eventName, payload) => {
248
+ if (allowAfterDispose || !disposed) {
249
+ options.onEmit?.(eventName, payload);
250
+ }
251
+ },
252
+ variables,
253
+ botId: options.botId,
254
+ abortSignal: abortController.signal,
255
+ });
256
+ return {
257
+ ...platformContext,
258
+ showToast: (...args) => {
259
+ if (allowAfterDispose || !disposed) {
260
+ platformContext.showToast?.(...args);
261
+ }
262
+ },
263
+ navigate: (...args) => {
264
+ if (allowAfterDispose || !disposed) {
265
+ platformContext.navigate?.(...args);
266
+ }
267
+ },
268
+ copyText: (...args) => {
269
+ if (allowAfterDispose || !disposed) {
270
+ platformContext.copyText?.(...args);
271
+ }
272
+ },
273
+ variables,
274
+ ...(node.bindingDialect === 'a2ui'
275
+ ? {
276
+ parameterResolver: createA2UIParameterResolver(variables, node.dataPath),
277
+ }
278
+ : {
279
+ expressionContext: createExpressionContext(node.scope),
280
+ }),
281
+ inflightRequests,
282
+ };
283
+ }
284
+ function registerBoundLifecycles(root) {
285
+ function walk(node) {
286
+ if (node.lifecycle) {
287
+ const context = createLifecycleActionContext(node);
288
+ lifecycleManager.register(node.id, node.lifecycle);
289
+ lifecycleMounts.set(node.id, lifecycleManager.mount(node.id, context));
290
+ }
291
+ node.children.forEach(walk);
292
+ }
293
+ walk(root);
294
+ }
295
+ function disposeBoundLifecycles(root) {
296
+ function walk(node) {
297
+ if (node.lifecycle) {
298
+ const mount = lifecycleMounts.get(node.id) ?? Promise.resolve();
299
+ const destroyContext = createLifecycleActionContext(node, true);
300
+ void mount.then(() => lifecycleManager.destroy(node.id, destroyContext), () => undefined).finally(() => {
301
+ lifecycleManager.unregister(node.id);
302
+ lifecycleMounts.delete(node.id);
303
+ });
304
+ }
305
+ node.children.forEach(walk);
306
+ }
307
+ walk(root);
308
+ }
309
+ const initialVariables = cloneJsonData({
310
+ ...schema.variables,
311
+ ...options.data,
312
+ });
313
+ prepareState(initialVariables, [''], revision);
314
+ registerBoundLifecycles(currentMaterialized.root);
315
+ async function runBoundEvent(nodeId, eventName, eventDetail) {
316
+ assertCurrentRevision(revision);
317
+ if (!nodeEventsMap.has(nodeId)) {
318
+ throw new Error(`[MiniProgramRenderer] Unknown or stale runtime node "${nodeId}"`);
319
+ }
320
+ const baseRevision = revision;
321
+ const draft = cloneCommittedVariables();
322
+ const freshMaterialized = materializeCard(schema, draft);
323
+ const freshNodeEventsMap = buildBoundNodeEventsMap(freshMaterialized.root, schemaActions);
324
+ const nodeInfo = freshNodeEventsMap.get(nodeId);
325
+ if (!nodeInfo) {
326
+ throw new Error(`[MiniProgramRenderer] Unknown or stale runtime node "${nodeId}"`);
327
+ }
328
+ const changedPaths = new Set();
329
+ const writeVariable = (key, value) => {
330
+ Object.defineProperty(draft, key, {
331
+ value,
332
+ enumerable: true,
333
+ configurable: true,
334
+ writable: true,
335
+ });
336
+ changedPaths.add(`/${escapeJsonPointerSegment(key)}`);
337
+ };
338
+ if (eventDetail != null) {
339
+ writeVariable('_event', cloneJsonData(eventDetail));
340
+ }
341
+ if ((eventName === 'onInput' || eventName === 'onChange')
342
+ && nodeInfo.variableKey
343
+ && eventDetail?.value !== undefined) {
344
+ writeVariable(nodeInfo.variableKey, cloneJsonData(eventDetail.value));
345
+ }
346
+ const steps = nodeInfo.events[eventName];
347
+ if (steps) {
348
+ const platformContext = createMiniProgramActionContext(options.adapter, {
349
+ setVariable: writeVariable,
350
+ emit: (eventName, payload) => {
351
+ if (!disposed)
352
+ options.onEmit?.(eventName, payload);
353
+ },
354
+ variables: draft,
355
+ botId: options.botId,
356
+ abortSignal: abortController.signal,
357
+ });
358
+ const actionContext = {
359
+ ...platformContext,
360
+ showToast: (...args) => {
361
+ if (!disposed)
362
+ platformContext.showToast?.(...args);
363
+ },
364
+ navigate: (...args) => {
365
+ if (!disposed)
366
+ platformContext.navigate?.(...args);
367
+ },
368
+ copyText: (...args) => {
369
+ if (!disposed)
370
+ platformContext.copyText?.(...args);
371
+ },
372
+ variables: draft,
373
+ expressionContext: nodeInfo.expressionContext,
374
+ parameterResolver: nodeInfo.parameterResolver,
375
+ variableWriter: (key, value) => {
376
+ writeVariable(key, value);
377
+ },
378
+ inflightRequests,
379
+ };
380
+ await runActionSteps(steps, actionContext);
381
+ }
382
+ prepareState(draft, [...changedPaths], baseRevision);
383
+ }
384
+ instance = {
385
+ getData() {
386
+ return { tree: currentTree };
387
+ },
388
+ updateVariables(newVars) {
389
+ assertCurrentRevision(revision);
390
+ const baseRevision = revision;
391
+ const draft = cloneCommittedVariables();
392
+ const changedPaths = defineDraftValues(draft, newVars);
393
+ const snapshot = prepareState(draft, changedPaths, baseRevision);
394
+ return { tree: snapshot.tree };
395
+ },
396
+ handleEvent(nodeId, eventName, eventDetail) {
397
+ if (disposed) {
398
+ return Promise.reject(transactionConflict());
399
+ }
400
+ const result = actionQueue.then(() => runBoundEvent(nodeId, eventName, eventDetail));
401
+ actionQueue = result.then(() => undefined, () => undefined);
402
+ return result;
403
+ },
404
+ dispose() {
405
+ if (disposed)
406
+ return;
407
+ disposed = true;
408
+ revision += 1;
409
+ abortController.abort();
410
+ disposeBoundLifecycles(currentMaterialized.root);
411
+ },
412
+ };
413
+ boundRendererControllers.set(instance, {
414
+ syncAuthoritativeVariables(nextRoot) {
415
+ assertCurrentRevision(revision);
416
+ const baseRevision = revision;
417
+ const draft = cloneJsonData(nextRoot);
418
+ return prepareState(draft, [''], baseRevision);
419
+ },
420
+ });
421
+ publishSnapshot();
422
+ return instance;
423
+ }
424
+ function escapeJsonPointerSegment(value) {
425
+ return value.replace(/~/g, '~0').replace(/\//g, '~1');
426
+ }
427
+ function getMiniProgramRendererSnapshot(instance) {
428
+ const snapshot = rendererSnapshots.get(instance);
429
+ if (!snapshot) {
430
+ throw new Error('[MiniProgramRenderer] Unknown renderer instance');
431
+ }
432
+ return snapshot;
433
+ }
434
+ function syncMiniProgramAuthoritativeVariables(instance, nextRoot) {
435
+ const controller = boundRendererControllers.get(instance);
436
+ if (controller) {
437
+ return controller.syncAuthoritativeVariables(nextRoot);
438
+ }
439
+ instance.updateVariables(nextRoot);
440
+ return getMiniProgramRendererSnapshot(instance);
113
441
  }
114
442
  /**
115
443
  * Build a map of nodeId → { events, variableKey } for fast event lookup.
@@ -139,6 +467,39 @@ function buildNodeEventsMap(tree, schemaActions) {
139
467
  walk(tree);
140
468
  return map;
141
469
  }
470
+ function buildBoundNodeEventsMap(tree, schemaActions) {
471
+ const map = new Map();
472
+ function walk(node) {
473
+ const events = {};
474
+ if (node.events) {
475
+ for (const [eventName, eventValue] of Object.entries(node.events)) {
476
+ if (!eventValue)
477
+ continue;
478
+ const resolvedSteps = resolveActionRef(eventValue, schemaActions);
479
+ if (resolvedSteps) {
480
+ events[eventName] = resolvedSteps;
481
+ }
482
+ }
483
+ }
484
+ const variableKey = node.props?.variableKey;
485
+ if (Object.keys(events).length > 0 || variableKey) {
486
+ map.set(node.id, {
487
+ events,
488
+ variableKey,
489
+ ...(node.bindingDialect === 'a2ui'
490
+ ? {
491
+ parameterResolver: createA2UIParameterResolver(node.scope.root, node.dataPath),
492
+ }
493
+ : {
494
+ expressionContext: createExpressionContext(node.scope),
495
+ }),
496
+ });
497
+ }
498
+ node.children.forEach(walk);
499
+ }
500
+ walk(tree);
501
+ return map;
502
+ }
142
503
  /**
143
504
  * Register lifecycle hooks for all nodes in the tree.
144
505
  */
@@ -155,4 +516,4 @@ function registerLifecycles(tree, lifecycleManager, actionContext) {
155
516
  walk(tree);
156
517
  }
157
518
 
158
- export { renderForMiniProgram, transformTree };
519
+ export { getMiniProgramRendererSnapshot, renderForMiniProgram, syncMiniProgramAuthoritativeVariables, transformTree };
@@ -1,4 +1,4 @@
1
- import { RenderTreeNode } from '@antglobal/copilot-cards-core';
1
+ import { RenderTreeNode, BoundRenderTreeNode } from '@antglobal/copilot-cards-core';
2
2
  import { MiniProgramRenderNode } from './index.js';
3
3
 
4
4
  /**
@@ -20,5 +20,14 @@ declare function transformNode(node: RenderTreeNode, variables: Record<string, a
20
20
  * Transform an entire render tree into a mini-program data tree.
21
21
  */
22
22
  declare function transformTree(tree: RenderTreeNode, variables: Record<string, any>): MiniProgramRenderNode;
23
+ /**
24
+ * Transform a materialized node against its captured repeat/A2UI scope.
25
+ *
26
+ * The legacy transform above deliberately remains untouched so literal cards
27
+ * retain their exact output and expression behavior.
28
+ */
29
+ declare function transformBoundNode(node: BoundRenderTreeNode): MiniProgramRenderNode;
30
+ /** Transform a fully materialized bound render tree. */
31
+ declare function transformBoundTree(tree: BoundRenderTreeNode): MiniProgramRenderNode;
23
32
 
24
- export { transformNode, transformTree };
33
+ export { transformBoundNode, transformBoundTree, transformNode, transformTree };
@@ -1,4 +1,4 @@
1
- import { hasExpression, resolveExpression, resolveDeep, resolveExpressionValue } from '@antglobal/copilot-cards-core';
1
+ import { hasExpression, resolveExpression, resolveDeep, resolveExpressionValue, createExpressionContext, resolveA2UIDeep } from '@antglobal/copilot-cards-core';
2
2
  import { decorateMiniIconProps } from '../icons.js';
3
3
 
4
4
  /**
@@ -87,5 +87,81 @@ function transformNode(node, variables) {
87
87
  function transformTree(tree, variables) {
88
88
  return transformNode(tree, variables);
89
89
  }
90
+ // ─── Bound Transform ─────────────────────────────────────────────
91
+ function isHidden(value) {
92
+ return value === false || value === 'false' || value === '' || value === 0;
93
+ }
94
+ function isDisabled(value) {
95
+ return value === true || value === 'true' || value === 1;
96
+ }
97
+ function resolveBoundDirective(node, value, expressionContext) {
98
+ if (node.bindingDialect === 'a2ui') {
99
+ return resolveA2UIDeep(value, node.scope.root, node.dataPath);
100
+ }
101
+ return typeof value === 'string' && hasExpression(value)
102
+ ? resolveExpression(value, expressionContext)
103
+ : value;
104
+ }
105
+ /**
106
+ * Transform a materialized node against its captured repeat/A2UI scope.
107
+ *
108
+ * The legacy transform above deliberately remains untouched so literal cards
109
+ * retain their exact output and expression behavior.
110
+ */
111
+ function transformBoundNode(node) {
112
+ const expressionContext = createExpressionContext(node.scope);
113
+ let visible = true;
114
+ if (node.directives?.visible) {
115
+ visible = !isHidden(resolveBoundDirective(node, node.directives.visible, expressionContext));
116
+ }
117
+ if (!visible) {
118
+ return {
119
+ id: node.id,
120
+ type: node.type,
121
+ props: {},
122
+ children: [],
123
+ visible: false,
124
+ disabled: false,
125
+ events: [],
126
+ };
127
+ }
128
+ let resolvedProps = (node.bindingDialect === 'a2ui'
129
+ ? resolveA2UIDeep(node.props, node.scope.root, node.dataPath)
130
+ : resolveDeep(node.props, expressionContext));
131
+ if (resolvedProps.content
132
+ && typeof resolvedProps.content === 'object'
133
+ && resolvedProps.content !== null
134
+ && 'type' in resolvedProps.content) {
135
+ resolvedProps.content = resolveExpressionValue(resolvedProps.content, expressionContext);
136
+ }
137
+ if (node.type === 'Icon') {
138
+ resolvedProps = decorateMiniIconProps(resolvedProps);
139
+ }
140
+ let disabled = false;
141
+ if (node.directives?.disabled) {
142
+ disabled = isDisabled(resolveBoundDirective(node, node.directives.disabled, expressionContext));
143
+ }
144
+ const events = [];
145
+ if (node.events && !disabled) {
146
+ for (const eventName of Object.keys(node.events)) {
147
+ if (node.events[eventName]) {
148
+ events.push(eventName);
149
+ }
150
+ }
151
+ }
152
+ return {
153
+ id: node.id,
154
+ type: node.type,
155
+ props: resolvedProps,
156
+ children: node.children.map(transformBoundNode),
157
+ visible,
158
+ disabled,
159
+ events,
160
+ };
161
+ }
162
+ /** Transform a fully materialized bound render tree. */
163
+ function transformBoundTree(tree) {
164
+ return transformBoundNode(tree);
165
+ }
90
166
 
91
- export { transformNode, transformTree };
167
+ export { transformBoundNode, transformBoundTree, transformNode, transformTree };
@@ -1,5 +1,5 @@
1
- import { StreamingParser, StreamingEngine, normalizeSchema } from '@antglobal/copilot-cards-core';
2
- import { renderForMiniProgram } from '../renderer/index.js';
1
+ import { StreamingParser, StreamingEngine, normalizeSchema, cloneJsonData, replaceRootContents, findAffectedRepeatOwners, bindingTopologyFingerprint, validateSchema } from '@antglobal/copilot-cards-core';
2
+ import { getMiniProgramRendererSnapshot, syncMiniProgramAuthoritativeVariables, renderForMiniProgram } from '../renderer/index.js';
3
3
 
4
4
  /**
5
5
  * Streaming Adapter for Mini-Program — enables progressive card rendering
@@ -39,139 +39,384 @@ import { renderForMiniProgram } from '../renderer/index.js';
39
39
  function createStreamingCardInstance(options) {
40
40
  const parser = new StreamingParser(options.parserOptions);
41
41
  let currentInstance = null;
42
+ let currentSurfaceId = null;
42
43
  let currentTree = null;
43
- let variables = { ...options.data };
44
- // Track node paths for efficient patching
45
- const nodePathMap = new Map(); // elementId → setData path
46
- /**
47
- * Build the node path map by traversing the tree.
48
- */
49
- function buildNodePaths(node, basePath) {
50
- nodePathMap.set(node.id, basePath);
51
- node.children.forEach((child, index) => {
52
- buildNodePaths(child, `${basePath}.children[${index}]`);
44
+ let currentTopologyFingerprint = null;
45
+ let nodePathMap = new Map();
46
+ let nodeMap = new Map();
47
+ let duplicateNodeIds = new Set();
48
+ const incompleteSurfaces = new Set();
49
+ function indexTree(tree) {
50
+ const paths = new Map();
51
+ const nodes = new Map();
52
+ const duplicates = new Set();
53
+ function visit(node, path) {
54
+ if (nodes.has(node.id))
55
+ duplicates.add(node.id);
56
+ paths.set(node.id, path);
57
+ nodes.set(node.id, node);
58
+ node.children.forEach((child, index) => {
59
+ visit(child, `${path}.children[${index}]`);
60
+ });
61
+ }
62
+ visit(tree, 'tree');
63
+ return { paths, nodes, duplicates };
64
+ }
65
+ function createFullPatch(tree = currentTree) {
66
+ return { data: { tree } };
67
+ }
68
+ function pathsOverlap(left, right) {
69
+ return (left === right
70
+ || left.startsWith(`${right}.`)
71
+ || left.startsWith(`${right}[`)
72
+ || right.startsWith(`${left}.`)
73
+ || right.startsWith(`${left}[`));
74
+ }
75
+ function nodeShell(node) {
76
+ return JSON.stringify({
77
+ id: node.id,
78
+ type: node.type,
79
+ props: node.props,
80
+ visible: node.visible,
81
+ disabled: node.disabled,
82
+ events: node.events,
53
83
  });
54
84
  }
55
- /**
56
- * Create a full tree patch (for initial render or full re-render).
57
- */
58
- function createFullPatch() {
59
- return { data: { tree: currentTree } };
85
+ function isStaticPublishedNode(materialized, id) {
86
+ for (const instance of materialized.instances.values()) {
87
+ if (instance.id === id && instance.node.instancePath === '') {
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+ function buildTargetedDataPatch(oldSnapshot, nextSnapshot, updatePath, oldPaths, oldNodes, oldDuplicates, nextIndex) {
94
+ const oldMaterialized = oldSnapshot.materialized;
95
+ const nextMaterialized = nextSnapshot.materialized;
96
+ const fullPatch = () => createFullPatch(nextSnapshot.tree);
97
+ if (!oldMaterialized
98
+ || !nextMaterialized
99
+ || oldMaterialized.unresolvedRepeatOwners.size > 0
100
+ || nextMaterialized.unresolvedRepeatOwners.size > 0) {
101
+ return fullPatch();
102
+ }
103
+ const owners = findAffectedRepeatOwners(oldMaterialized, updatePath);
104
+ if (owners.length === 0)
105
+ return fullPatch();
106
+ const data = {};
107
+ const patchPaths = [];
108
+ for (const owner of owners) {
109
+ const nextOwner = nextMaterialized.repeatOwners.get(owner.key);
110
+ const runtimeId = owner.runtimeOwnerId;
111
+ const oldPath = oldPaths.get(runtimeId);
112
+ const nextPath = nextIndex.paths.get(runtimeId);
113
+ const nextNode = nextIndex.nodes.get(runtimeId);
114
+ if (!nextOwner
115
+ || nextOwner.runtimeOwnerId !== runtimeId
116
+ || !oldPath
117
+ || nextPath !== oldPath
118
+ || !nextNode
119
+ || oldDuplicates.has(runtimeId)
120
+ || nextIndex.duplicates.has(runtimeId)
121
+ || patchPaths.some(path => pathsOverlap(path, oldPath))) {
122
+ return fullPatch();
123
+ }
124
+ data[oldPath] = nextNode;
125
+ patchPaths.push(oldPath);
126
+ }
127
+ // A repeat dependency can also affect static nodes outside every repeat
128
+ // closure (for example, a global currency heading). Publish those nodes in
129
+ // the same patch or fall back before exposing an incomplete host tree.
130
+ for (const [id, oldNode] of oldNodes) {
131
+ if (!isStaticPublishedNode(oldMaterialized, id))
132
+ continue;
133
+ if (oldDuplicates.has(id) || nextIndex.duplicates.has(id)) {
134
+ return fullPatch();
135
+ }
136
+ const oldPath = oldPaths.get(id);
137
+ const nextPath = nextIndex.paths.get(id);
138
+ const nextNode = nextIndex.nodes.get(id);
139
+ if (!oldPath || nextPath !== oldPath || !nextNode) {
140
+ return fullPatch();
141
+ }
142
+ if (nodeShell(oldNode) === nodeShell(nextNode))
143
+ continue;
144
+ if (Object.prototype.hasOwnProperty.call(data, oldPath))
145
+ continue;
146
+ if (patchPaths.some(path => pathsOverlap(path, oldPath))) {
147
+ return fullPatch();
148
+ }
149
+ data[oldPath] = nextNode;
150
+ patchPaths.push(oldPath);
151
+ }
152
+ return patchPaths.length > 0 ? { data } : fullPatch();
153
+ }
154
+ function buildRendererCandidate(schema, authoritativeVariables) {
155
+ const instance = renderForMiniProgram(schema, {
156
+ adapter: options.adapter,
157
+ data: authoritativeVariables,
158
+ botId: options.botId,
159
+ onEmit: options.onEmit,
160
+ });
161
+ const snapshot = getMiniProgramRendererSnapshot(instance);
162
+ return {
163
+ instance,
164
+ snapshot,
165
+ index: indexTree(snapshot.tree),
166
+ topologyFingerprint: bindingTopologyFingerprint(schema),
167
+ };
168
+ }
169
+ function hasOnlyExplicitIncompleteReferences(schema) {
170
+ if (!hasWellFormedReferenceIds(schema))
171
+ return false;
172
+ const errors = validateSchema(schema);
173
+ return errors.length > 0 && errors.every(error => ((error.startsWith('Root element "')
174
+ && error.endsWith('not found in elements'))
175
+ || error.includes(' references unknown child "')
176
+ || error.includes(' group references unknown child "')
177
+ || error.includes(' repeat references unknown template "')
178
+ || error.includes(' dynamic children reference unknown template "')));
179
+ }
180
+ function hasWellFormedReferenceIds(schema) {
181
+ const isReferenceId = (value) => typeof value === 'string' && value.length > 0;
182
+ const ownValue = (target, key) => {
183
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
184
+ if (!descriptor)
185
+ return { found: false };
186
+ if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
187
+ return null;
188
+ }
189
+ return { found: true, value: descriptor.value };
190
+ };
191
+ try {
192
+ if (!isReferenceId(schema.rootID))
193
+ return false;
194
+ if (!schema.elements
195
+ || typeof schema.elements !== 'object'
196
+ || Array.isArray(schema.elements))
197
+ return false;
198
+ for (const elementId of Object.keys(schema.elements)) {
199
+ const elementEntry = ownValue(schema.elements, elementId);
200
+ if (!elementEntry?.found
201
+ || !elementEntry.value
202
+ || typeof elementEntry.value !== 'object')
203
+ return false;
204
+ const propsEntry = ownValue(elementEntry.value, 'props');
205
+ if (!propsEntry?.found
206
+ || !propsEntry.value
207
+ || typeof propsEntry.value !== 'object')
208
+ return false;
209
+ const slotsEntry = ownValue(propsEntry.value, 'slots');
210
+ if (!slotsEntry)
211
+ return false;
212
+ if (!slotsEntry.found || slotsEntry.value === undefined)
213
+ continue;
214
+ if (!slotsEntry.value
215
+ || typeof slotsEntry.value !== 'object'
216
+ || Array.isArray(slotsEntry.value))
217
+ return false;
218
+ for (const slotName of Reflect.ownKeys(slotsEntry.value)) {
219
+ const slotEntry = ownValue(slotsEntry.value, slotName);
220
+ if (!slotEntry?.found
221
+ || !slotEntry.value
222
+ || typeof slotEntry.value !== 'object')
223
+ return false;
224
+ const slot = slotEntry.value;
225
+ const childrenEntry = ownValue(slot, 'children');
226
+ if (!childrenEntry)
227
+ return false;
228
+ if (childrenEntry.found && childrenEntry.value !== undefined) {
229
+ if (!Array.isArray(childrenEntry.value)
230
+ || !childrenEntry.value.every(isReferenceId))
231
+ return false;
232
+ }
233
+ const groupsEntry = ownValue(slot, 'groups');
234
+ if (!groupsEntry)
235
+ return false;
236
+ if (groupsEntry.found && groupsEntry.value !== undefined) {
237
+ if (!Array.isArray(groupsEntry.value)
238
+ || !groupsEntry.value.every(group => Array.isArray(group) && group.every(isReferenceId)))
239
+ return false;
240
+ }
241
+ const repeatEntry = ownValue(slot, 'repeat');
242
+ if (!repeatEntry)
243
+ return false;
244
+ if (repeatEntry.found) {
245
+ if (!repeatEntry.value
246
+ || typeof repeatEntry.value !== 'object'
247
+ || Array.isArray(repeatEntry.value))
248
+ return false;
249
+ const templateEntry = ownValue(repeatEntry.value, 'template');
250
+ if (!templateEntry?.found
251
+ || !isReferenceId(templateEntry.value))
252
+ return false;
253
+ }
254
+ for (const key of Reflect.ownKeys(slot)) {
255
+ if (typeof key !== 'symbol')
256
+ continue;
257
+ const sidecarEntry = ownValue(slot, key);
258
+ if (!sidecarEntry?.found
259
+ || !sidecarEntry.value
260
+ || typeof sidecarEntry.value !== 'object')
261
+ continue;
262
+ const dialectEntry = ownValue(sidecarEntry.value, 'dialect');
263
+ const templateEntry = ownValue(sidecarEntry.value, 'templateId');
264
+ if (dialectEntry?.found
265
+ && dialectEntry.value === 'a2ui'
266
+ && (!templateEntry?.found
267
+ || !isReferenceId(templateEntry.value)))
268
+ return false;
269
+ }
270
+ }
271
+ }
272
+ return true;
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
278
+ function publishCandidate(surfaceId, candidate, patch) {
279
+ const previousInstance = currentInstance;
280
+ currentInstance = candidate.instance;
281
+ currentSurfaceId = surfaceId;
282
+ currentTree = candidate.snapshot.tree;
283
+ currentTopologyFingerprint = candidate.topologyFingerprint;
284
+ nodePathMap = candidate.index.paths;
285
+ nodeMap = candidate.index.nodes;
286
+ duplicateNodeIds = candidate.index.duplicates;
287
+ pendingPatches.push(patch);
288
+ previousInstance?.dispose();
289
+ }
290
+ function publishSnapshot(snapshot, index, patch) {
291
+ currentTree = snapshot.tree;
292
+ nodePathMap = index.paths;
293
+ nodeMap = index.nodes;
294
+ duplicateNodeIds = index.duplicates;
295
+ pendingPatches.push(patch);
60
296
  }
61
297
  // ─── Engine Setup ───────────────────────────────────────────────
62
298
  const pendingPatches = [];
63
299
  const engine = new StreamingEngine({
64
300
  onSurfaceCreated(surfaceId, schemaInput) {
65
- if (schemaInput) {
66
- const schema = normalizeSchema(schemaInput);
67
- variables = { ...schema.variables, ...options.data };
68
- currentInstance = renderForMiniProgram(schema, {
69
- adapter: options.adapter,
70
- data: variables,
71
- botId: options.botId,
72
- onEmit: options.onEmit,
301
+ if (!schemaInput) {
302
+ incompleteSurfaces.add(surfaceId);
303
+ return;
304
+ }
305
+ const schema = engine.getSchema(surfaceId)
306
+ ?? normalizeSchema(schemaInput);
307
+ const authoritativeVariables = engine.getVariables(surfaceId);
308
+ if (!authoritativeVariables)
309
+ return;
310
+ const initialData = options.data ?? {};
311
+ for (const key of Object.keys(initialData)) {
312
+ Object.defineProperty(authoritativeVariables, key, {
313
+ value: initialData[key],
314
+ enumerable: true,
315
+ configurable: true,
316
+ writable: true,
73
317
  });
74
- currentTree = currentInstance.getData().tree;
75
- if (currentTree) {
76
- nodePathMap.clear();
77
- buildNodePaths(currentTree, 'tree');
318
+ }
319
+ let candidate;
320
+ try {
321
+ candidate = buildRendererCandidate(schema, authoritativeVariables);
322
+ }
323
+ catch (error) {
324
+ if (hasOnlyExplicitIncompleteReferences(schema)) {
325
+ incompleteSurfaces.add(surfaceId);
326
+ return;
78
327
  }
79
- pendingPatches.push(createFullPatch());
328
+ throw error;
80
329
  }
330
+ incompleteSurfaces.delete(surfaceId);
331
+ publishCandidate(surfaceId, candidate, createFullPatch(candidate.snapshot.tree));
81
332
  },
82
- onComponentsUpdated(surfaceId, changes) {
83
- // For mini-program, component tree changes require full re-render
84
- // because we can't insert DOM nodes — we need to regenerate the data tree
333
+ onComponentsUpdated(surfaceId) {
85
334
  const schema = engine.getSchema(surfaceId);
86
- if (!schema)
335
+ const authoritativeVariables = engine.getVariables(surfaceId);
336
+ if (!schema || !authoritativeVariables)
87
337
  return;
88
- currentInstance = renderForMiniProgram(schema, {
89
- adapter: options.adapter,
90
- data: variables,
91
- botId: options.botId,
92
- onEmit: options.onEmit,
93
- });
94
- currentTree = currentInstance.getData().tree;
95
- if (currentTree) {
96
- nodePathMap.clear();
97
- buildNodePaths(currentTree, 'tree');
338
+ let candidate;
339
+ try {
340
+ candidate = buildRendererCandidate(schema, authoritativeVariables);
98
341
  }
99
- pendingPatches.push(createFullPatch());
100
- },
101
- onDataModelUpdated(surfaceId, path, value) {
102
- if (!currentInstance)
103
- return;
104
- // Update variables and regenerate tree
105
- const pathParts = path.replace(/^\//, '').split('/').filter(Boolean);
106
- if (pathParts.length === 0) {
107
- if (typeof value === 'object' && value !== null) {
108
- variables = { ...variables, ...value };
342
+ catch (error) {
343
+ if (hasOnlyExplicitIncompleteReferences(schema)) {
344
+ incompleteSurfaces.add(surfaceId);
345
+ return;
109
346
  }
347
+ throw error;
110
348
  }
111
- else {
112
- let current = variables;
113
- for (let i = 0; i < pathParts.length - 1; i++) {
114
- if (current[pathParts[i]] === undefined)
115
- current[pathParts[i]] = {};
116
- current = current[pathParts[i]];
117
- }
118
- current[pathParts[pathParts.length - 1]] = value;
349
+ incompleteSurfaces.delete(surfaceId);
350
+ // Component topology/mode changes must be full patches. Unchanged
351
+ // topology is also conservatively full until every static/repeat target
352
+ // can be proven safe from one candidate.
353
+ if (currentTopologyFingerprint !== null
354
+ && currentTopologyFingerprint
355
+ !== candidate.topologyFingerprint) {
356
+ publishCandidate(surfaceId, candidate, createFullPatch(candidate.snapshot.tree));
357
+ return;
119
358
  }
120
- const result = currentInstance.updateVariables(variables);
121
- currentTree = result.tree;
122
- if (currentTree) {
123
- nodePathMap.clear();
124
- buildNodePaths(currentTree, 'tree');
359
+ publishCandidate(surfaceId, candidate, createFullPatch(candidate.snapshot.tree));
360
+ },
361
+ onDataModelUpdated(surfaceId, path) {
362
+ if (incompleteSurfaces.has(surfaceId)
363
+ || !currentInstance
364
+ || currentSurfaceId !== surfaceId)
365
+ return;
366
+ const authoritativeVariables = engine.getVariables(surfaceId);
367
+ if (!authoritativeVariables)
368
+ return;
369
+ const oldSnapshot = getMiniProgramRendererSnapshot(currentInstance);
370
+ const oldPaths = nodePathMap;
371
+ const oldNodes = nodeMap;
372
+ const oldDuplicates = duplicateNodeIds;
373
+ const nextSnapshot = syncMiniProgramAuthoritativeVariables(currentInstance, authoritativeVariables);
374
+ const nextIndex = indexTree(nextSnapshot.tree);
375
+ let patch;
376
+ try {
377
+ patch = buildTargetedDataPatch(oldSnapshot, nextSnapshot, path, oldPaths, oldNodes, oldDuplicates, nextIndex);
378
+ }
379
+ catch {
380
+ patch = createFullPatch(nextSnapshot.tree);
125
381
  }
126
- pendingPatches.push(createFullPatch());
382
+ publishSnapshot(nextSnapshot, nextIndex, patch);
127
383
  },
128
384
  onContentAppended(surfaceId, elementId, content) {
129
- // Try to do a targeted patch for text content
385
+ if (currentSurfaceId !== surfaceId)
386
+ return;
130
387
  const nodePath = nodePathMap.get(elementId);
131
- if (nodePath && currentTree) {
132
- // Find the node and update its content in-place
133
- const node = findNodeById(currentTree, elementId);
134
- if (node) {
135
- const currentContent = typeof node.props.content === 'string'
136
- ? node.props.content : '';
137
- node.props.content = currentContent + content;
138
- // Generate a path-based setData patch for efficiency
139
- pendingPatches.push({
140
- data: { [`${nodePath}.props.content`]: node.props.content },
141
- });
142
- return;
143
- }
144
- }
145
- // Fallback: full re-render
146
- if (currentInstance) {
147
- const schema = engine.getSchema(surfaceId);
148
- if (schema) {
149
- variables = { ...schema.variables, ...variables };
150
- const result = currentInstance.updateVariables(variables);
151
- currentTree = result.tree;
152
- pendingPatches.push(createFullPatch());
153
- }
388
+ const node = nodeMap.get(elementId);
389
+ if (nodePath
390
+ && node
391
+ && currentTree
392
+ && !duplicateNodeIds.has(elementId)) {
393
+ const currentContent = typeof node.props.content === 'string'
394
+ ? node.props.content : '';
395
+ node.props.content = currentContent + content;
396
+ pendingPatches.push({
397
+ data: { [`${nodePath}.props.content`]: node.props.content },
398
+ });
399
+ return;
154
400
  }
401
+ if (currentInstance)
402
+ pendingPatches.push(createFullPatch());
155
403
  },
156
404
  onSurfaceDeleted(surfaceId) {
405
+ incompleteSurfaces.delete(surfaceId);
406
+ if (currentSurfaceId !== surfaceId)
407
+ return;
157
408
  currentInstance?.dispose();
158
409
  currentInstance = null;
410
+ currentSurfaceId = null;
159
411
  currentTree = null;
160
- nodePathMap.clear();
412
+ currentTopologyFingerprint = null;
413
+ nodePathMap = new Map();
414
+ nodeMap = new Map();
415
+ duplicateNodeIds = new Set();
161
416
  pendingPatches.push({ data: { tree: null } });
162
417
  },
163
418
  });
164
419
  // ─── Helpers ────────────────────────────────────────────────────
165
- function findNodeById(node, id) {
166
- if (node.id === id)
167
- return node;
168
- for (const child of node.children) {
169
- const found = findNodeById(child, id);
170
- if (found)
171
- return found;
172
- }
173
- return null;
174
- }
175
420
  function drainPatches() {
176
421
  const patches = [...pendingPatches];
177
422
  pendingPatches.length = 0;
@@ -208,19 +453,51 @@ function createStreamingCardInstance(options) {
208
453
  async handleEvent(nodeId, eventName, eventDetail) {
209
454
  if (!currentInstance)
210
455
  return null;
211
- await currentInstance.handleEvent(nodeId, eventName, eventDetail);
212
- currentTree = currentInstance.getData().tree;
213
- if (currentTree) {
214
- nodePathMap.clear();
215
- buildNodePaths(currentTree, 'tree');
456
+ const handledInstance = currentInstance;
457
+ const handledSurfaceId = currentSurfaceId;
458
+ if (!handledSurfaceId)
459
+ return null;
460
+ const previousEngineVariables = engine.getVariables(handledSurfaceId);
461
+ if (!previousEngineVariables)
462
+ return null;
463
+ const engineRollback = cloneJsonData(previousEngineVariables);
464
+ await handledInstance.handleEvent(nodeId, eventName, eventDetail);
465
+ if (currentInstance !== handledInstance
466
+ || currentSurfaceId !== handledSurfaceId)
467
+ return null;
468
+ const snapshot = getMiniProgramRendererSnapshot(handledInstance);
469
+ const index = indexTree(snapshot.tree);
470
+ const authoritativeVariables = engine.getVariables(handledSurfaceId);
471
+ if (!authoritativeVariables)
472
+ return null;
473
+ try {
474
+ replaceRootContents(authoritativeVariables, snapshot.variables);
475
+ }
476
+ catch (error) {
477
+ const rollbackSnapshot = syncMiniProgramAuthoritativeVariables(handledInstance, engineRollback);
478
+ const rollbackIndex = indexTree(rollbackSnapshot.tree);
479
+ currentTree = rollbackSnapshot.tree;
480
+ nodePathMap = rollbackIndex.paths;
481
+ nodeMap = rollbackIndex.nodes;
482
+ duplicateNodeIds = rollbackIndex.duplicates;
483
+ throw error;
216
484
  }
217
- return createFullPatch();
485
+ currentTree = snapshot.tree;
486
+ nodePathMap = index.paths;
487
+ nodeMap = index.nodes;
488
+ duplicateNodeIds = index.duplicates;
489
+ return createFullPatch(snapshot.tree);
218
490
  },
219
491
  dispose() {
220
492
  currentInstance?.dispose();
221
493
  currentInstance = null;
494
+ currentSurfaceId = null;
222
495
  currentTree = null;
223
- nodePathMap.clear();
496
+ currentTopologyFingerprint = null;
497
+ nodePathMap = new Map();
498
+ nodeMap = new Map();
499
+ duplicateNodeIds = new Set();
500
+ incompleteSurfaces.clear();
224
501
  engine.dispose();
225
502
  parser.reset();
226
503
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antglobal/copilot-cards-mini-program",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Mini-program native renderer for copilot bot card SDK — adapts to WeChat / Alipay / DingTalk mini-program templates",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@antglobal/copilot-cards-core": "^1.0.1",
33
+ "@antglobal/copilot-cards-core": "^1.0.3",
34
34
  "tslib": "^2.8.1"
35
35
  },
36
36
  "devDependencies": {