@cniot/mdd-editor 0.3.6 → 0.3.8

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.
@@ -231,9 +231,388 @@ function PullDiffDetail({ diffSummary }) {
231
231
  );
232
232
  }
233
233
 
234
+ const L4_CODE_PATTERN = /L4:([a-zA-Z0-9_-]+)/g;
235
+ const DEFAULT_LINKED_PAGE_DETAIL_URL = '/l4/api/v1/console/get-by-code';
236
+ const LINKED_PAGE_LOG_PREFIX = '[MDD AI Bridge][L4]';
237
+ const LOCAL_CHANGE_SKIP_REASON = '本地已有未同步修改';
238
+
239
+ const normalizeText = (value) => {
240
+ if (typeof value === 'string') return value;
241
+ if (value == null) return '';
242
+ return JSON.stringify(value, null, 2);
243
+ };
244
+
245
+ const logLinkedPageDebug = (...args) => {
246
+ if (typeof console !== 'undefined' && typeof console.info === 'function') {
247
+ console.info(LINKED_PAGE_LOG_PREFIX, ...args);
248
+ }
249
+ };
250
+
251
+ const collectL4ReferencesFromText = (text = '', source = 'script', path = '$') => {
252
+ const references = [];
253
+ const pattern = new RegExp(L4_CODE_PATTERN);
254
+ let match = pattern.exec(String(text));
255
+ while (match) {
256
+ references.push({
257
+ code: match[1],
258
+ source,
259
+ paths: [path],
260
+ });
261
+ match = pattern.exec(String(text));
262
+ }
263
+ return references;
264
+ };
265
+
266
+ const collectL4ReferencesFromJSON = (value, path = '$', references = []) => {
267
+ if (typeof value === 'string') {
268
+ references.push(...collectL4ReferencesFromText(value, 'schema', path));
269
+ return references;
270
+ }
271
+ if (Array.isArray(value)) {
272
+ value.forEach((item, index) => collectL4ReferencesFromJSON(item, `${path}[${index}]`, references));
273
+ return references;
274
+ }
275
+ if (value && typeof value === 'object') {
276
+ Object.keys(value).forEach((key) => {
277
+ references.push(...collectL4ReferencesFromText(key, 'schema-key', `${path}.${key}`));
278
+ collectL4ReferencesFromJSON(value[key], `${path}.${key}`, references);
279
+ });
280
+ }
281
+ return references;
282
+ };
283
+
284
+ const mergeL4References = (references = [], rootCode) => {
285
+ const referenceMap = new Map();
286
+ references.forEach((reference) => {
287
+ if (!reference?.code || reference.code === rootCode) return;
288
+ const existing = referenceMap.get(reference.code) || {
289
+ code: reference.code,
290
+ source: reference.source,
291
+ paths: [],
292
+ };
293
+ existing.source = existing.source === reference.source ? existing.source : 'mixed';
294
+ existing.paths = [...new Set([...existing.paths, ...(reference.paths || [])])];
295
+ referenceMap.set(reference.code, existing);
296
+ });
297
+ return [...referenceMap.values()];
298
+ };
299
+
300
+ const collectLinkedPageReferences = ({ schemaJson, scriptInfo, rootCode, extraSources = [] }) => {
301
+ const schemaReferences = collectL4ReferencesFromJSON(schemaJson);
302
+ const scriptReferences = collectL4ReferencesFromText(scriptInfo, 'script', 'mdd.script.jsx');
303
+ const extraReferences = extraSources.flatMap((source) =>
304
+ collectL4ReferencesFromText(source?.text || '', source?.source || 'extra', source?.path || '$'),
305
+ );
306
+ return mergeL4References([...schemaReferences, ...scriptReferences, ...extraReferences], rootCode);
307
+ };
308
+
309
+ const createLinkedPageExtraSources = (data = {}, prefix = 'payload') => {
310
+ const sourceFields = [
311
+ 'schemaInfo',
312
+ 'schemaJson',
313
+ 'schema',
314
+ 'scriptInfo',
315
+ 'script',
316
+ 'style',
317
+ 'compileFile',
318
+ 'translateInfo',
319
+ 'serverDataHandle',
320
+ 'metaData',
321
+ 'sqlData',
322
+ 'sqlDataCount',
323
+ 'swaggerInfo',
324
+ 'dataPermissions',
325
+ 'pageMeta',
326
+ 'pageIR',
327
+ ];
328
+ return sourceFields
329
+ .filter((fieldName) => data[fieldName] != null)
330
+ .map((fieldName) => ({
331
+ source: fieldName,
332
+ path: `${prefix}.${fieldName}`,
333
+ text: normalizeText(data[fieldName]),
334
+ }));
335
+ };
336
+
337
+ const enqueueLinkedPageReferences = ({ queue, referenceMap, references, parentCode, depth, rootCode, visitedCodes }) => {
338
+ references.forEach((reference) => {
339
+ if (!reference?.code || reference.code === rootCode) return;
340
+ const nextReference = {
341
+ ...reference,
342
+ parentCode,
343
+ depth,
344
+ };
345
+ const existing = referenceMap.get(reference.code);
346
+ if (existing) {
347
+ existing.source = existing.source === nextReference.source ? existing.source : 'mixed';
348
+ existing.paths = [...new Set([...(existing.paths || []), ...(nextReference.paths || [])])];
349
+ existing.parents = [...new Set([...(existing.parents || []), parentCode])];
350
+ existing.depth = Math.min(existing.depth || depth, depth);
351
+ return;
352
+ }
353
+ referenceMap.set(reference.code, {
354
+ ...nextReference,
355
+ parents: [parentCode],
356
+ });
357
+ if (!visitedCodes.has(reference.code)) {
358
+ queue.push(nextReference);
359
+ }
360
+ });
361
+ };
362
+
363
+ const fetchLinkedPageByCode = async (code, config = {}) => {
364
+ const detailUrl = config.linkedPageDetailUrl || DEFAULT_LINKED_PAGE_DETAIL_URL;
365
+ const separator = detailUrl.includes('?') ? '&' : '?';
366
+ const url = `${detailUrl}${separator}code=${encodeURIComponent(code)}`;
367
+ logLinkedPageDebug('fetch page detail', { code, url });
368
+ const response = await fetch(url, {
369
+ credentials: 'include',
370
+ });
371
+ const text = await response.text();
372
+ let result = null;
373
+ try {
374
+ result = text ? JSON.parse(text) : null;
375
+ } catch (error) {
376
+ throw new Error(`关联 L4 页面详情接口返回非 JSON:${text || response.status}`);
377
+ }
378
+
379
+ if (!response.ok) {
380
+ throw new Error(result?.errorMessage || result?.message || `关联 L4 页面详情接口请求失败:${response.status}`);
381
+ }
382
+ if (result?.success === false) {
383
+ throw new Error(result?.errorMessage || result?.message || result?.msg || '关联 L4 页面详情接口返回失败');
384
+ }
385
+ const pageData = result?.data || result;
386
+ logLinkedPageDebug('fetch page detail success', {
387
+ code,
388
+ name: pageData?.name || pageData?.pageMeta?.name,
389
+ hasSchemaInfo: Boolean(pageData?.schemaInfo),
390
+ hasScriptInfo: Boolean(pageData?.scriptInfo),
391
+ hasCompileFile: Boolean(pageData?.compileFile),
392
+ });
393
+ return pageData;
394
+ };
395
+
396
+ const getLinkedPageResolver = (config) => {
397
+ if (typeof config?.resolveLinkedPage === 'function') return config.resolveLinkedPage;
398
+ if (typeof window !== 'undefined' && typeof window.__MDD_AI_BRIDGE_RESOLVE_L4_PAGE === 'function') {
399
+ return window.__MDD_AI_BRIDGE_RESOLVE_L4_PAGE;
400
+ }
401
+ return (code) => fetchLinkedPageByCode(code, config);
402
+ };
403
+
404
+ const normalizeLinkedPagePayload = (reference, pageData) => {
405
+ if (!pageData) return null;
406
+ const schemaJson = parseSchema(pageData.schemaInfo || pageData.schemaJson || pageData.schema || '{}') || {};
407
+ const rawPageMeta = pageData.pageMeta || pageData.meta || {};
408
+ const pageMeta = {
409
+ ...rawPageMeta,
410
+ code: pageData.code || rawPageMeta.code || reference.code,
411
+ name: pageData.name ?? rawPageMeta.name,
412
+ region: pageData.region ?? rawPageMeta.region,
413
+ module: pageData.module ?? rawPageMeta.module,
414
+ moduleName: pageData.moduleName ?? rawPageMeta.moduleName,
415
+ status: pageData.status ?? rawPageMeta.status,
416
+ grade: pageData.grade ?? rawPageMeta.grade,
417
+ mddTemplateType: pageData.mddTemplateType ?? rawPageMeta.mddTemplateType,
418
+ buildType: pageData.buildType ?? rawPageMeta.buildType,
419
+ operateType: pageData.operateType ?? rawPageMeta.operateType,
420
+ gmtModified: pageData.gmtModified ?? rawPageMeta.gmtModified,
421
+ modifierAccount: pageData.modifierAccount ?? rawPageMeta.modifierAccount,
422
+ modifierName: pageData.modifierName ?? rawPageMeta.modifierName,
423
+ };
424
+ const script = pageData.scriptInfo?.script ?? pageData.scriptInfo ?? pageData.script ?? '';
425
+ const style = pageData.scriptInfo?.style ?? pageData.style ?? '';
426
+ const pageIR =
427
+ pageData.pageIR ||
428
+ buildPageIR({
429
+ schemaJson,
430
+ script,
431
+ style,
432
+ pageMeta,
433
+ });
434
+ return {
435
+ code: pageMeta.code,
436
+ pageMeta,
437
+ schemaInfo: normalizeText(pageData.schemaInfo || schemaJson),
438
+ scriptInfo: script,
439
+ style,
440
+ pageIR,
441
+ extraSources: createLinkedPageExtraSources(
442
+ {
443
+ ...pageData,
444
+ pageMeta,
445
+ pageIR,
446
+ style,
447
+ },
448
+ `linkedPage.${pageMeta.code}`,
449
+ ),
450
+ linkedReference: reference,
451
+ };
452
+ };
453
+
454
+ const buildLinkedPagePayload = async (payload, config) => {
455
+ const rootSchemaJson = parseSchema(payload.schemaInfo) || {};
456
+ const resolver = getLinkedPageResolver(config);
457
+ let rootPageData = null;
458
+ try {
459
+ logLinkedPageDebug('fetch root page detail for raw scan', { code: payload.code });
460
+ rootPageData = await resolver(payload.code, {
461
+ code: payload.code,
462
+ source: 'root',
463
+ depth: 0,
464
+ paths: ['root.get-by-code'],
465
+ });
466
+ } catch (error) {
467
+ logLinkedPageDebug('fetch root page detail failed, fallback to editor payload only', {
468
+ code: payload.code,
469
+ reason: error?.message || String(error),
470
+ });
471
+ }
472
+ const directReferences = collectLinkedPageReferences({
473
+ schemaJson: rootSchemaJson,
474
+ scriptInfo: payload.scriptInfo,
475
+ rootCode: payload.code,
476
+ extraSources: [
477
+ ...(payload.extraSources || []),
478
+ ...createLinkedPageExtraSources(rootPageData || {}, `rootApi.${payload.code}`),
479
+ ],
480
+ });
481
+ logLinkedPageDebug('scan root page', {
482
+ code: payload.code,
483
+ source: rootPageData ? 'editor payload + root get-by-code raw data' : 'editor payload only',
484
+ directReferences: directReferences.map((reference) => ({
485
+ code: reference.code,
486
+ source: reference.source,
487
+ paths: reference.paths,
488
+ })),
489
+ });
490
+ const queue = [];
491
+ const referenceMap = new Map();
492
+ const visitedCodes = new Set([payload.code]);
493
+ const linkedPages = [];
494
+ const failedLinkedPages = [];
495
+
496
+ enqueueLinkedPageReferences({
497
+ queue,
498
+ referenceMap,
499
+ references: directReferences,
500
+ parentCode: payload.code,
501
+ depth: 1,
502
+ rootCode: payload.code,
503
+ visitedCodes,
504
+ });
505
+
506
+ while (queue.length > 0) {
507
+ const currentReference = queue.shift();
508
+ if (!currentReference?.code || visitedCodes.has(currentReference.code)) continue;
509
+ visitedCodes.add(currentReference.code);
510
+ const latestReference = referenceMap.get(currentReference.code) || currentReference;
511
+
512
+ try {
513
+ logLinkedPageDebug('resolve linked page', {
514
+ code: currentReference.code,
515
+ parentCode: latestReference.parentCode,
516
+ depth: latestReference.depth,
517
+ paths: latestReference.paths,
518
+ });
519
+ const pageData = await resolver(currentReference.code, latestReference);
520
+ const linkedPage = normalizeLinkedPagePayload(latestReference, pageData);
521
+ if (!linkedPage) {
522
+ failedLinkedPages.push({
523
+ ...latestReference,
524
+ reason: '关联 L4 页面返回为空',
525
+ });
526
+ continue;
527
+ }
528
+
529
+ linkedPages.push(linkedPage);
530
+ const linkedPageSchemaJson = parseSchema(linkedPage.schemaInfo) || {};
531
+ const childReferences = collectLinkedPageReferences({
532
+ schemaJson: linkedPageSchemaJson,
533
+ scriptInfo: linkedPage.scriptInfo,
534
+ rootCode: payload.code,
535
+ extraSources: linkedPage.extraSources,
536
+ });
537
+ logLinkedPageDebug('scan child page', {
538
+ code: linkedPage.code,
539
+ depth: latestReference.depth,
540
+ childReferences: childReferences.map((reference) => ({
541
+ code: reference.code,
542
+ source: reference.source,
543
+ paths: reference.paths,
544
+ })),
545
+ });
546
+ enqueueLinkedPageReferences({
547
+ queue,
548
+ referenceMap,
549
+ references: childReferences,
550
+ parentCode: linkedPage.code,
551
+ depth: (latestReference.depth || 1) + 1,
552
+ rootCode: payload.code,
553
+ visitedCodes,
554
+ });
555
+ } catch (error) {
556
+ failedLinkedPages.push({
557
+ ...latestReference,
558
+ reason: error?.message || '关联 L4 页面加载失败',
559
+ });
560
+ }
561
+ }
562
+
563
+ const linkedPageReferences = [...referenceMap.values()];
564
+ const maxDepth = linkedPageReferences.reduce((depth, reference) => Math.max(depth, reference.depth || 1), 0);
565
+ const mergedLinkedPages = linkedPages.map((linkedPage) => ({
566
+ ...linkedPage,
567
+ linkedReference: referenceMap.get(linkedPage.code) || linkedPage.linkedReference,
568
+ }));
569
+ logLinkedPageDebug('scan complete', {
570
+ code: payload.code,
571
+ directCount: directReferences.length,
572
+ totalCount: linkedPageReferences.length,
573
+ syncedCount: mergedLinkedPages.length,
574
+ failedCount: failedLinkedPages.length,
575
+ references: linkedPageReferences.map((reference) => ({
576
+ code: reference.code,
577
+ source: reference.source,
578
+ depth: reference.depth,
579
+ paths: reference.paths,
580
+ parents: reference.parents,
581
+ })),
582
+ failed: failedLinkedPages,
583
+ });
584
+
585
+ return {
586
+ ...payload,
587
+ linkedPageReferences,
588
+ linkedPageScan: {
589
+ directCount: directReferences.length,
590
+ totalCount: linkedPageReferences.length,
591
+ maxDepth,
592
+ },
593
+ linkedPages: mergedLinkedPages,
594
+ failedLinkedPages,
595
+ };
596
+ };
597
+
598
+ const isLocalChangeSkippedPage = (page) => String(page?.reason || '').includes(LOCAL_CHANGE_SKIP_REASON);
599
+
600
+ const createLinkedPushSummary = (res, fallbackDir) => ({
601
+ type: 'push',
602
+ dir: res?.dir || fallbackDir,
603
+ linkedPages: res?.linkedPages || {},
604
+ });
605
+
606
+ const createLinkedPullSummary = (res) => {
607
+ const changedPages = (res?.linkedPageDiffs || []).filter((item) => item.changed);
608
+ if (!changedPages.length) return '';
609
+ const codes = changedPages.map((item) => item.code).join('、');
610
+ return `检测到关联 L4 页面也存在本地修改:${codes}。当前只同步了当前页面,请分别打开这些 L4 页面执行“同步本地修改”并保存。`;
611
+ };
612
+
234
613
  export default function LocalAIDrawer(props) {
235
614
  const { schema, scriptInfo, pageMeta = {}, bridgeConfig, onApply } = props;
236
- const config = normalizeBridgeConfig(bridgeConfig);
615
+ const config = React.useMemo(() => normalizeBridgeConfig(bridgeConfig), [bridgeConfig]);
237
616
  const [baseURL, setBaseURL] = React.useState(config.baseURL || DEFAULT_BRIDGE_URL);
238
617
  const [status, setStatus] = React.useState('未连接');
239
618
  const [workspacePath, setWorkspacePath] = React.useState('');
@@ -248,24 +627,33 @@ export default function LocalAIDrawer(props) {
248
627
 
249
628
  const getPayload = React.useCallback(() => {
250
629
  const nextSchemaJson = schema?.getAllJSON?.() || {};
630
+ const nextPageMeta = {
631
+ ...pageMeta,
632
+ code: pageCode,
633
+ };
634
+ const nextScriptInfo = scriptInfo?.script || '';
635
+ const nextStyle = scriptInfo?.style || '';
636
+ const nextPageIR = buildPageIR({
637
+ schemaJson: nextSchemaJson,
638
+ script: nextScriptInfo,
639
+ style: nextStyle,
640
+ pageMeta: nextPageMeta,
641
+ });
251
642
  return {
252
643
  code: pageCode,
253
- pageMeta: {
254
- ...pageMeta,
255
- code: pageCode,
256
- },
644
+ pageMeta: nextPageMeta,
257
645
  schemaInfo: JSON.stringify(nextSchemaJson, null, 2),
258
- scriptInfo: scriptInfo?.script || '',
259
- style: scriptInfo?.style || '',
260
- pageIR: buildPageIR({
261
- schemaJson: nextSchemaJson,
262
- script: scriptInfo?.script || '',
263
- style: scriptInfo?.style || '',
264
- pageMeta: {
265
- ...pageMeta,
266
- code: pageCode,
646
+ scriptInfo: nextScriptInfo,
647
+ style: nextStyle,
648
+ pageIR: nextPageIR,
649
+ extraSources: createLinkedPageExtraSources(
650
+ {
651
+ pageMeta: nextPageMeta,
652
+ pageIR: nextPageIR,
653
+ style: nextStyle,
267
654
  },
268
- }),
655
+ 'editorPayload',
656
+ ),
269
657
  };
270
658
  }, [pageCode, pageMeta, schema, scriptInfo]);
271
659
 
@@ -331,15 +719,22 @@ export default function LocalAIDrawer(props) {
331
719
  setPullError(null);
332
720
  setLastDiffSummary(null);
333
721
  try {
334
- const res = await pushPage(baseURL, pageCode, getPayload());
722
+ const payload = await buildLinkedPagePayload(getPayload(), config);
723
+ const res = await pushPage(baseURL, pageCode, payload);
335
724
  setWorkspacePath(res?.dir || '');
336
725
  setBridgeInfo((prev) => ({
337
726
  ...(prev || {}),
338
727
  workspaceRoot: res?.context?.workspaceRoot || prev?.workspaceRoot,
339
728
  }));
340
729
  setStatus('已同步到本地');
341
- setLastSummary(`已写入本地工作区: ${res?.dir || pageCode}`);
342
- Message.success('已发送到本地 AI 工作区');
730
+ setLastSummary(createLinkedPushSummary(res, pageCode));
731
+ if (res?.linkedPages?.totalReferences > 0) {
732
+ Message.success(
733
+ `已发送到本地 AI 工作区,扫描到直接子 L4 页面 ${res.linkedPages.directCount || 0} 个,递归共 ${res.linkedPages.totalReferences} 个`,
734
+ );
735
+ } else {
736
+ Message.success('已发送到本地 AI 工作区');
737
+ }
343
738
  } catch (e) {
344
739
  Message.error(e.message || '发送到本地 AI 工作区失败');
345
740
  } finally {
@@ -364,9 +759,14 @@ export default function LocalAIDrawer(props) {
364
759
  scriptInfo: nextScriptInfo,
365
760
  raw: res,
366
761
  });
367
- setLastSummary(res?.summary || '已从本地工作区同步修改');
762
+ const linkedPullSummary = createLinkedPullSummary(res);
763
+ setLastSummary([res?.summary || '已从本地工作区同步修改', linkedPullSummary].filter(Boolean).join(';'));
368
764
  setLastDiffSummary(res?.diffSummary || null);
369
- Message.success('已同步本地 AI 修改');
765
+ if (linkedPullSummary) {
766
+ Message.warning('已同步当前页面;关联子 L4 页面也有本地修改,请分别打开对应 L4 页面同步并保存');
767
+ } else {
768
+ Message.success('已同步当前页面的本地修改,请预览确认后点击页面保存');
769
+ }
370
770
  } catch (e) {
371
771
  const detail = getJsonErrorDetail(e);
372
772
  setPullError(detail);
@@ -415,7 +815,15 @@ export default function LocalAIDrawer(props) {
415
815
  const res = await getPageStatus(baseURL, pageCode);
416
816
  setWorkspacePath(res?.dir || '');
417
817
  setLastDiffSummary(null);
418
- setLastSummary(res?.exists ? `本地工作区已存在: ${res.dir}` : '本地还没有这个页面的工作区');
818
+ const linkedChangedCount = res?.linkedPages?.changedCount || 0;
819
+ const linkedStatusText = linkedChangedCount
820
+ ? `;关联 L4 页面中有 ${linkedChangedCount} 个存在本地修改`
821
+ : res?.linkedPages?.totalReferences
822
+ ? `;已记录关联 L4 页面 ${res.linkedPages.totalReferences} 个`
823
+ : '';
824
+ setLastSummary(
825
+ res?.exists ? `本地工作区已存在: ${res.dir}${linkedStatusText}` : '本地还没有这个页面的工作区',
826
+ );
419
827
  Message.success(res?.exists ? '本地工作区已存在' : '本地工作区不存在');
420
828
  } catch (e) {
421
829
  Message.error(e.message || '查询本地工作区失败');
@@ -516,6 +924,9 @@ export default function LocalAIDrawer(props) {
516
924
  回滚上次更改
517
925
  </CnButton>
518
926
  </div>
927
+ <div className="mdd-local-ai-action-notice">
928
+ “同步本地修改”只会同步当前页面的本地变更;如果关联子 L4 页面也在本地改过,请打开对应子页面的 L4 地址,分别执行“同步本地修改”并在页面上点击保存。
929
+ </div>
519
930
  </div>
520
931
 
521
932
  <div className="mdd-local-ai-section">
@@ -533,7 +944,85 @@ export default function LocalAIDrawer(props) {
533
944
  </div>
534
945
  </div>
535
946
 
536
- {lastSummary ? <div className="mdd-local-ai-summary">{lastSummary}</div> : null}
947
+ {lastSummary ? (
948
+ lastSummary.type === 'push' ? (
949
+ <div className="mdd-local-ai-push-result">
950
+ <div className="mdd-local-ai-push-result-head">
951
+ <div>
952
+ <div className="mdd-local-ai-push-result-title">已写入本地工作区</div>
953
+ <div className="mdd-local-ai-push-result-path">{lastSummary.dir}</div>
954
+ </div>
955
+ <span className="mdd-local-ai-push-result-badge">完成</span>
956
+ </div>
957
+
958
+ <div className="mdd-local-ai-push-stats">
959
+ <div className="mdd-local-ai-push-stat">
960
+ <span>直接子 L4</span>
961
+ <strong>{lastSummary.linkedPages?.directCount || 0}</strong>
962
+ </div>
963
+ <div className="mdd-local-ai-push-stat">
964
+ <span>递归关联</span>
965
+ <strong>{lastSummary.linkedPages?.totalReferences || 0}</strong>
966
+ </div>
967
+ <div className="mdd-local-ai-push-stat is-success">
968
+ <span>已同步</span>
969
+ <strong>{lastSummary.linkedPages?.syncedCount || 0}</strong>
970
+ </div>
971
+ <div className="mdd-local-ai-push-stat is-warning">
972
+ <span>需处理</span>
973
+ <strong>{lastSummary.linkedPages?.failedCount || 0}</strong>
974
+ </div>
975
+ </div>
976
+
977
+ {lastSummary.linkedPages?.pages?.length ? (
978
+ <div className="mdd-local-ai-linked-section">
979
+ <div className="mdd-local-ai-linked-section-title">已一起同步的关联页面</div>
980
+ <div className="mdd-local-ai-linked-tags">
981
+ {lastSummary.linkedPages.pages.map((page) => (
982
+ <span key={page.code} className="mdd-local-ai-linked-tag is-success has-name">
983
+ <span className="mdd-local-ai-linked-name">{page.name || '未命名页面'}</span>
984
+ <span className="mdd-local-ai-linked-code">{page.code}</span>
985
+ </span>
986
+ ))}
987
+ </div>
988
+ </div>
989
+ ) : null}
990
+
991
+ {lastSummary.linkedPages?.failed?.length ? (
992
+ <div className="mdd-local-ai-linked-section">
993
+ <div className="mdd-local-ai-linked-section-title">需要用户处理的关联页面</div>
994
+ {lastSummary.linkedPages.failed.map((page) => {
995
+ const isLocalChangeSkipped = isLocalChangeSkippedPage(page);
996
+ return (
997
+ <div key={page.code} className="mdd-local-ai-linked-warning">
998
+ <div className="mdd-local-ai-linked-warning-main">
999
+ <span className="mdd-local-ai-linked-tag is-warning has-name">
1000
+ <span className="mdd-local-ai-linked-name">{page.name || '未命名页面'}</span>
1001
+ <span className="mdd-local-ai-linked-code">{page.code}</span>
1002
+ </span>
1003
+ <span>{page.reason || '未能自动同步'}</span>
1004
+ </div>
1005
+ {isLocalChangeSkipped ? (
1006
+ <div className="mdd-local-ai-linked-actions-tip">
1007
+ <div>这个页面本地已有未同步修改,为避免覆盖,本次没有重新下发。</div>
1008
+ <div>你可以选择:1)打开该 L4 页面先“同步本地修改”并保存;2)确认放弃本地改动后删除本地目录,再从父页面重新下发。</div>
1009
+ {page.dir ? (
1010
+ <CnButton size="small" onClick={() => handleCopy(page.dir, '已复制关联页面本地目录')}>
1011
+ 复制本地目录
1012
+ </CnButton>
1013
+ ) : null}
1014
+ </div>
1015
+ ) : null}
1016
+ </div>
1017
+ );
1018
+ })}
1019
+ </div>
1020
+ ) : null}
1021
+ </div>
1022
+ ) : (
1023
+ <div className="mdd-local-ai-summary">{lastSummary}</div>
1024
+ )
1025
+ ) : null}
537
1026
 
538
1027
  <PullDiffDetail diffSummary={lastDiffSummary} />
539
1028
 
@@ -183,6 +183,7 @@ export function normalizeBridgeConfig(config) {
183
183
  };
184
184
  }
185
185
  return {
186
+ ...(config || {}),
186
187
  enabled: config?.enabled !== false,
187
188
  baseURL: config?.baseURL || DEFAULT_BRIDGE_URL,
188
189
  };