返回正文目录

Summary / relationship to #106

疑似回归,关联 #106。 Launcher 0.1.29 对同 Namespace、已登记的同一模型目录再次执行 models validate --models-dir,报 duplicate query model resource in namespace。重启及显式 clear-existing 仍失败;相同六文件在新 Namespace 首次校验全部通过。

#106 的已关闭合同明确同 namespace / 同 path 应复用已加载 Bundle 并给出 BUNDLE_ALREADY_REGISTERED warning。本 issue 记录新版本的复现条件,不改写旧修复记录,也尚未断言与旧问题是相同代码根因。

归属:Java Runtime / TM-QM detached validation、Bundle/catalog 候选合并。与 #146 的查询名/输出别名不同;后者在本轮已修复并通过。失败的模型校验没有被绕过后发布。

Environment / 发行身份

基础准备

将发行启动脚本及 JAR 放在同一目录。下载公开 SQLite 为当前目录的 Chinook.sqlite,将下文源码按文件名保存到 ./models/model/ 与 ./models/query/。以下目录均为调用者自己的练习目录。

.\start-foggy-runtime.ps1 -Port 18068 -WorkDir "$PWD/work"
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook wait-ready --timeout-seconds 45
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook capabilities
$dbPath = (Resolve-Path ./Chinook.sqlite).Path -replace '\\', '/'
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook datasources add --name chinook-sqlite --type sqlite --jdbc-url "jdbc:sqlite:$dbPath"
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook datasources bind --namespace chinook --data-source chinook-sqlite
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook datasources test chinook-sqlite

后续每条 foggy-runtime 均在命令名前附加全局参数 --base-url http://127.0.0.1:18068 --namespace chinook。

Reproduction (实际顺序)

  1. 准备下方六个模型。首次加载时,明细 QM 的“专辑与艺术家”两项先使用旧展开形式:
{ caption: '专辑与艺术家', items: [
    { ref: line.track.album },
    { ref: line.track.album.artist }
] }
  1. 在尚未登记该目录的 chinook 校验,实际六文件通过,然后登记一个 Bundle 并刷新:
foggy-runtime models validate --models-dir ./models
foggy-runtime bundles add --name chinook-0129-control --path ./models
foggy-runtime models refresh
foggy-runtime models describe ChinookInvoiceLineQueryModel
  1. 将明细 QM 的上述两个整维度 ref 改成下文完整文件里的四个具体列 name/alias 项,其余五文件保持。再次对相同 Namespace、同一物理路径校验,不新增第二个 Bundle:
foggy-runtime models validate --models-dir ./models --include-stack-trace

退出 2。实际 3 个 TM 通过、3 个 QM 失败,均指向相同 duplicate QM 资源。提供的是已编辑明细 QM 的实际场景,不把“完全未编辑目录也失败”冒称本轮单独验证项。

  1. 关闭隔离实例,仍用同 WorkDir、相同注册文件和源码重启,再执行步骤 3;仍失败。重启轮显式启用内嵌 Console,但同样的失败已在默认 lite 观察到。再补测:
foggy-runtime models validate --models-dir ./models --clear-existing --include-stack-trace

仍退出 2,同样的重复资源失败。

  1. 不改这六文件,为新的隔离 Namespace 绑定同一 named datasource,再只做首次校验:
foggy-runtime datasources bind --namespace chinook-alias-probe --data-source chinook-sqlite
foggy-runtime --base-url http://127.0.0.1:18068 --namespace chinook-alias-probe models validate --models-dir ./models

实际 6/6 valid、0 invalid。随后在这个新 Namespace 首次登记/刷新,实时元数据与第十二章 11 个别名查询通过。这仅排除了明显模型配置错误,不是原 Namespace 正常更新流程已通过。

Expected

Actual — exact CLI diagnostics

下面是重启后执行 --include-stack-trace 的实际 JSON(本机路径已脱敏);服务并未给出额外可用的 Java cause 栈,stackTrace 实际值就是 Lifecycle operation failed.,没有补造堆栈:

{
  "diagnostics": {
    "attributes": {
      "invalidFiles": 3,
      "validation": {
        "cascadingErrors": 0,
        "durationMs": 37,
        "errors": [
          {
            "category": "MODEL",
            "code": "IllegalStateException",
            "file": "ChinookInvoiceLineQueryModel.qm",
            "message": "duplicate query model resource in namespace: ChinookInvoiceLineQueryModel",
            "severity": "ERROR",
            "stackTrace": "Lifecycle operation failed.",
            "type": "QM"
          },
          {
            "category": "MODEL",
            "code": "IllegalStateException",
            "file": "ChinookInvoiceQueryModel.qm",
            "message": "duplicate query model resource in namespace: ChinookInvoiceLineQueryModel",
            "severity": "ERROR",
            "stackTrace": "Lifecycle operation failed.",
            "type": "QM"
          },
          {
            "category": "MODEL",
            "code": "IllegalStateException",
            "file": "ChinookInvoiceTimeQueryModel.qm",
            "message": "duplicate query model resource in namespace: ChinookInvoiceLineQueryModel",
            "severity": "ERROR",
            "stackTrace": "Lifecycle operation failed.",
            "type": "QM"
          }
        ],
        "invalidFiles": 3,
        "namespace": "chinook",
        "path": "<redacted-path>",
        "totalFiles": 6,
        "valid": false,
        "validFiles": 3,
        "warnings": []
      }
    },
    "warnings": [
      "Detached model validation rejected the candidate."
    ]
  },
  "engine": "java",
  "error": {
    "code": "MODEL_VALIDATE_FAILED",
    "lifecycle": {
      "affectedBindingGenerations": [
        {
          "backendId": "runtime-registry:chinook-sqlite",
          "bindingKey": "runtime:namespace-default:chinook",
          "generation": "f04995a6-94b0-4c1b-85aa-5f15791e8628:2"
        }
      ],
      "beforeCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:2",
      "catalogState": "ACTIVE",
      "diagnostics": [
        {
          "message": "Detached model validation failed (IllegalStateException).",
          "phase": "validate.qm",
          "suggestedNextAction": "Fix the model definition and retry validation.",
          "target": "ChinookInvoiceLineQueryModel"
        },
        {
          "message": "Detached model validation failed (IllegalStateException).",
          "phase": "validate.qm",
          "suggestedNextAction": "Fix the model definition and retry validation.",
          "target": "ChinookInvoiceQueryModel"
        },
        {
          "message": "Detached model validation failed (IllegalStateException).",
          "phase": "validate.qm",
          "suggestedNextAction": "Fix the model definition and retry validation.",
          "target": "ChinookInvoiceTimeQueryModel"
        }
      ],
      "failedTargets": [
        "ChinookInvoiceLineQueryModel",
        "ChinookInvoiceQueryModel",
        "ChinookInvoiceTimeQueryModel"
      ],
      "namespace": "chinook",
      "sourceRevision": "source:103e6b0b-d9de-4d38-a8c4-d1bacc5ab9cc:g0:n2"
    },
    "lifecycleCode": "CATALOG_VALIDATION_FAILED",
    "message": "Detached model validation rejected the candidate.",
    "model": "ChinookInvoiceLineQueryModel",
    "phase": "models.validate",
    "safeToAutoRepair": false,
    "suggestedNextAction": "Fix the model definition and retry validation."
  },
  "runtimeApiVersion": "foggy-runtime-api/v1",
  "success": false
}

显式 --clear-existing 的错误摘要:

{
  "success": false,
  "error": {
    "code": "MODEL_VALIDATE_FAILED",
    "lifecycle": {
      "affectedBindingGenerations": [
        {
          "backendId": "runtime-registry:chinook-sqlite",
          "bindingKey": "runtime:namespace-default:chinook",
          "generation": "f04995a6-94b0-4c1b-85aa-5f15791e8628:2"
        }
      ],
      "beforeCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:2",
      "catalogState": "ACTIVE",
      "diagnostics": [
        {
          "message": "Detached model validation failed (IllegalStateException).",
          "phase": "validate.qm",
          "suggestedNextAction": "Fix the model definition and retry validation.",
          "target": "ChinookInvoiceLineQueryModel"
        },
        {
          "message": "Detached model validation failed (IllegalStateException).",
          "phase": "validate.qm",
          "suggestedNextAction": "Fix the model definition and retry validation.",
          "target": "ChinookInvoiceQueryModel"
        },
        {
          "message": "Detached model validation failed (IllegalStateException).",
          "phase": "validate.qm",
          "suggestedNextAction": "Fix the model definition and retry validation.",
          "target": "ChinookInvoiceTimeQueryModel"
        }
      ],
      "failedTargets": [
        "ChinookInvoiceLineQueryModel",
        "ChinookInvoiceQueryModel",
        "ChinookInvoiceTimeQueryModel"
      ],
      "namespace": "chinook",
      "sourceRevision": "source:103e6b0b-d9de-4d38-a8c4-d1bacc5ab9cc:g0:n2"
    },
    "lifecycleCode": "CATALOG_VALIDATION_FAILED",
    "message": "Detached model validation rejected the candidate.",
    "model": "ChinookInvoiceLineQueryModel",
    "phase": "models.validate",
    "safeToAutoRepair": false,
    "suggestedNextAction": "Fix the model definition and retry validation."
  }
}

新 Namespace 首次校验摘要:

{
  "success": true,
  "data": {
    "affectedBindingGenerations": [],
    "afterCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:3",
    "beforeCatalogGeneration": "catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:3",
    "cascadingErrors": 0,
    "catalogState": "ACTIVE",
    "durationMs": 41,
    "errors": [],
    "invalidFiles": 0,
    "namespace": "chinook-alias-probe",
    "path": "[lab-root]\\releases\\launcher-0.1.29\\control-models",
    "sourceRevision": "source:103e6b0b-d9de-4d38-a8c4-d1bacc5ab9cc:g0:n0",
    "totalFiles": 6,
    "valid": true,
    "validFiles": 6,
    "warnings": []
  }
}

Runtime registration log / registry

重启后 Runtime stdout 的加载记录(保留原行号,脱敏路径):

80: 2026-10-21 10:03:29.686 [main] INFO  c.f.bundle.SystemBundlesContextImpl - 动态添加外部Bundle成功: chinook-0129-control -> [lab-root]\releases\launcher-0.1.29\control-models (namespace: chinook)
83: 2026-10-21 10:03:29.874 [main] INFO  c.f.d.m.e.BundleLifecycleListener - Bundle catalog refresh published: bundleName=chinook-0129-control, namespace=chinook, generation=catalog:4b33b0e0-56a8-4ca7-bc11-9e69bd687ecc:1, pivotOuterCacheRemoved=0

收尾时 chinook 的实际注册条目仅一个;watch=false,避免把文件监视重复登记当作已知根因:

[
  {
    "name": "chinook-0129-control",
    "namespace": "chinook",
    "path": "[lab-root]\\releases\\launcher-0.1.29\\control-models",
    "watch": false,
    "enabled": true,
    "createdAt": "2026-10-21T02:00:29.550752300Z",
    "updatedAt": "2026-10-21T02:00:29.550752300Z",
    "immutablePublication": false
  }
]

独立 stdout/stderr、各次 CLI JSON 与命令日志已留档。Runtime 对重复校验失败没有另打印完整 cause 栈;上面的结构化诊断是可取得的关键错误日志。

Six-file reproduction fixture (实际编辑后版本)

下列六文件使用公开 Chinook;首次加载时只把明细 QM 两项换成步骤 1 的旧形式,再按步骤 3 恢复为这里的四个具体列。此处为实际六文件复现,尚未额外缩减成单 TM/QM 对。

model/ChinookInvoiceLineModel.tm
export const model = {
    name: 'ChinookInvoiceLineModel',
    caption: 'Chinook 账单明细',
    description: '一行是一条账单明细。分类分析累计交易明细金额,不累计账单总金额,不改用目录单价。',
    tableName: 'InvoiceLine',
    idColumn: 'InvoiceLineId',
    properties: [
        { column: 'InvoiceLineId', name: 'invoiceLineId', caption: '明细 ID', type: 'INTEGER' },
        { column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' },
        { column: 'TrackId', name: 'trackId', caption: '曲目或媒体 ID', type: 'INTEGER' },
        { column: 'UnitPrice', name: 'unitPrice', caption: '交易单价', type: 'MONEY',
          description: '账单明细上的交易单价,不是 Track 目录单价。样例原始单位,币种未指定。' }
    ],
    dimensions: [
        {
            name: 'invoice', caption: '账单', tableName: 'Invoice',
            foreignKey: 'InvoiceId', primaryKey: 'InvoiceId', captionColumn: 'InvoiceId',
            properties: [
                { column: 'InvoiceDate', name: 'invoiceDate', caption: '账单日期', type: 'DATETIME',
                  description: '按账单记录日期统计,不自行做时区转换。' },
                { column: 'BillingCountry', name: 'billingCountry', caption: '账单国家', type: 'STRING' },
                { column: 'CustomerId', name: 'customerId', caption: '客户 ID', type: 'INTEGER' }
            ],
            dimensions: [
                {
                    name: 'customer', caption: '客户', tableName: 'Customer',
                    foreignKey: 'CustomerId', primaryKey: 'CustomerId', captionColumn: 'LastName',
                    properties: [
                        { column: 'FirstName', name: 'fullName', caption: '客户姓名', type: 'STRING',
                          description: 'FirstName 与 LastName 拼接;与客户 ID 一起使用。',
                          formulaDef: { builder: (alias) => `${alias}.FirstName || ' ' || ${alias}.LastName` } }
                    ]
                }
            ]
        },
        {
            name: 'track', caption: '曲目或媒体', tableName: 'Track',
            foreignKey: 'TrackId', primaryKey: 'TrackId', captionColumn: 'Name',
            properties: [
                { column: 'Name', name: 'title', caption: '曲目或媒体名称', type: 'STRING' }
            ],
            dimensions: [
                {
                    name: 'genre', caption: '内容分类', tableName: 'Genre',
                    foreignKey: 'GenreId', primaryKey: 'GenreId', captionColumn: 'Name',
                    description: '包含音乐和影视内容;分类缺失不应丢弃交易金额。',
                    properties: [
                        { column: 'GenreId', name: 'genreId', caption: '分类 ID', type: 'INTEGER' },
                        { column: 'Name', name: 'genreName', caption: '内容分类名称', type: 'STRING' }
                    ]
                },
                {
                    name: 'album', caption: '专辑', tableName: 'Album',
                    foreignKey: 'AlbumId', primaryKey: 'AlbumId', captionColumn: 'Title',
                    description: '曲目所属专辑,包含音乐及影视专辑;用 ID 区分同名专辑。',
                    dimensions: [
                        {
                            name: 'artist', caption: '艺术家', tableName: 'Artist',
                            foreignKey: 'ArtistId', primaryKey: 'ArtistId', captionColumn: 'Name',
                            description: '以专辑所属艺术家归属交易金额,不等同于 Track.Composer 作曲者;用 ID 区分同名。'
                        }
                    ]
                }
            ]
        }
    ],
    measures: [
        { column: 'UnitPrice', name: 'lineAmount', caption: '明细金额', type: 'MONEY', aggregation: 'sum',
          description: '交易单价乘数量,沿用样例原始单位,不除以 100,不指定币种。',
          formulaDef: { builder: (alias) => `${alias}.UnitPrice * ${alias}.Quantity` } },
        { column: 'Quantity', name: 'salesQuantity', caption: '销售数量', type: 'INTEGER', aggregation: 'sum',
          description: '累计 Quantity;本样例全部为 1,也不能将数量定义成明细条数。' }
    ]
};
model/ChinookInvoiceModel.tm
export const model = {
    name: 'ChinookInvoiceModel',
    caption: 'Chinook 账单',
    description: '一行是一张账单,不关联明细后累计账单金额。金额沿用样例原始单位,币种未指定。',
    tableName: 'Invoice',
    idColumn: 'InvoiceId',
    properties: [
        { column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' },
        { column: 'CustomerId', name: 'customerId', caption: '客户 ID', type: 'INTEGER',
          description: '客户识别键,排名时与姓名一起分组,避免同名客户合并。' },
        { column: 'InvoiceDate', name: 'invoiceDate', caption: '账单日期', type: 'DATETIME',
          description: '按账单记录日期统计,不自行做时区转换。' },
        { column: 'BillingCountry', name: 'billingCountry', caption: '账单国家', type: 'STRING',
          description: '账单地址中的国家,不是客户地址国家。' }
    ],
    dimensions: [
        {
            name: 'customer', caption: '客户', tableName: 'Customer',
            foreignKey: 'CustomerId', primaryKey: 'CustomerId',
            captionColumn: 'LastName',
            keyCaption: '客户 ID',
            description: '用客户 ID 区分客户,姓名仅用于展示。',
            properties: [
                { column: 'FirstName', name: 'fullName', caption: '客户姓名', type: 'STRING',
                  description: 'FirstName 与 LastName 拼接;展示时与客户 ID 一起使用。',
                  formulaDef: { builder: (alias) => `${alias}.FirstName || ' ' || ${alias}.LastName` } },
                { column: 'Country', name: 'country', caption: '客户国家', type: 'STRING' }
            ]
        }
    ],
    measures: [
        { column: 'Total', name: 'invoiceAmount', caption: '账单金额', type: 'MONEY', aggregation: 'sum',
          description: '每张账单只累计一次。样例原始单位,不除以 100,不指定币种。' },
        { column: 'InvoiceId', name: 'invoiceCount', caption: '账单数', type: 'INTEGER', aggregation: 'count',
          description: '按账单 ID 计数,只在账单分析中统计。' }
    ]
};
model/ChinookInvoiceTimeModel.tm
export const model = {
    name: 'ChinookInvoiceTimeModel',
    caption: 'Chinook 账单时间分析',
    description: '一行是一张账单,按账单记录日期的自然年、自然月统计。金额沿用样例原始单位,币种未指定,不自行做时区转换。',
    tableName: 'Invoice',
    idColumn: 'InvoiceId',
    properties: [
        { column: 'InvoiceId', name: 'invoiceId', caption: '账单 ID', type: 'INTEGER' }
    ],
    dimensions: [
        {
            name: 'invoiceDate', caption: '账单日期',
            foreignKey: 'InvoiceDate', primaryKey: 'InvoiceDate', captionColumn: 'InvoiceDate',
            type: 'DATETIME', timeRole: 'business_date',
            description: '直接使用账单表的 InvoiceDate,不关联额外日期表;按记录日期统计,不自行做时区转换。',
            properties: [
                { column: 'InvoiceDate', name: 'year', caption: '账单年份', type: 'INTEGER',
                  dialectFormulaDef: { sqlite: { builder: (alias) => `CAST(strftime('%Y', ${alias}.InvoiceDate) AS INTEGER)` } } },
                { column: 'InvoiceDate', name: 'month', caption: '账单月份', type: 'INTEGER',
                  description: '月份序号 1—12,跨年分析时应与年份一起使用。',
                  dialectFormulaDef: { sqlite: { builder: (alias) => `CAST(strftime('%m', ${alias}.InvoiceDate) AS INTEGER)` } } },
                { column: 'InvoiceDate', name: 'yearMonth', caption: '账单年月', type: 'STRING',
                  description: 'YYYY-MM,自然月分组键,保留年份。',
                  dialectFormulaDef: { sqlite: { builder: (alias) => `strftime('%Y-%m', ${alias}.InvoiceDate)` } } }
            ]
        }
    ],
    measures: [
        { column: 'Total', name: 'invoiceAmount', caption: '账单金额', type: 'MONEY', aggregation: 'sum',
          description: '每张账单只累计一次,样例原始单位,不指定币种,不代表利润或回款。' },
        { column: 'InvoiceId', name: 'invoiceCount', caption: '账单数', type: 'INTEGER', aggregation: 'count' }
    ]
};
query/ChinookInvoiceLineQueryModel.qm
const line = loadTableModel('ChinookInvoiceLineModel');

export const queryModel = {
    name: 'ChinookInvoiceLineQueryModel',
    caption: 'Chinook 明细分析',
    description: '按账单日期、客户、曲目或媒体、内容分类、专辑及其所属艺术家分析明细金额和销售数量。每行是一条明细,不提供可累计的账单总金额。艺术家按 Album.ArtistId 归属,不使用 Track.Composer。金额沿用样例原始单位,不指定币种、不做分元换算,不代表利润、回款或退款。',
    loader: 'v2',
    model: line,
    columnGroups: [
        { caption: '交易明细', items: [
            { ref: line.invoiceLineId },
            { ref: line.invoiceId },
            { ref: line.trackId },
            { ref: line.unitPrice },
            { ref: line.invoice$invoiceDate },
            { ref: line.invoice$billingCountry },
            { ref: line.invoice$customerId },
            { ref: line.invoice.customer$fullName },
            { ref: line.track$title },
            { ref: line.track.genre$genreId },
            { ref: line.track.genre$genreName }
        ] },
        { caption: '专辑与艺术家', items: [
            { ref: line.track.album$id, name: 'albumId', alias: 'albumId' },
            { ref: line.track.album$caption, name: 'albumName', alias: 'albumName' },
            { ref: line.track.album.artist$id, name: 'albumArtistId', alias: 'albumArtistId' },
            { ref: line.track.album.artist$caption, name: 'albumArtistName', alias: 'albumArtistName' }
        ] },
        { caption: '明细指标', items: [
            { ref: line.lineAmount },
            { ref: line.salesQuantity }
        ] }
    ],
    accesses: []
};
query/ChinookInvoiceQueryModel.qm
const invoice = loadTableModel('ChinookInvoiceModel');

export const queryModel = {
    name: 'ChinookInvoiceQueryModel',
    caption: 'Chinook 账单分析',
    description: '按账单日期、客户和账单国家分析账单金额与张数。金额沿用样例原始单位,不指定币种、不做分元换算,不代表利润、回款或退款。分类分析请用明细分析,不按分类分摊账单总额。',
    loader: 'v2',
    model: invoice,
    columnGroups: [
        { caption: '账单与客户', items: [
            { ref: invoice.invoiceId },
            { ref: invoice.invoiceDate },
            { ref: invoice.billingCountry },
            { ref: invoice.customerId },
            { ref: invoice.customer$fullName },
            { ref: invoice.customer$country }
        ] },
        { caption: '账单指标', items: [
            { ref: invoice.invoiceAmount },
            { ref: invoice.invoiceCount },
            {
                name: 'averageAmount',
                caption: '平均账单金额',
                formula: 'sum(invoiceAmount) / nullif(count(invoiceId), 0)',
                type: 'MONEY',
                description: '当前筛选与分组范围的账单总金额除以账单数;无账单时为空,不累计或简单平均分组均额。样例原始单位,币种未指定。'
            }
        ] }
    ],
    accesses: []
};
query/ChinookInvoiceTimeQueryModel.qm
const invoice = loadTableModel('ChinookInvoiceTimeModel');

export const queryModel = {
    name: 'ChinookInvoiceTimeQueryModel',
    caption: 'Chinook 账单时间分析',
    description: '按账单日期、自然年和自然月分析账单金额与张数。月份序号需与年份结合,年月字段为 YYYY-MM;金额沿用样例原始单位,币种未指定。',
    loader: 'v2',
    model: invoice,
    columnGroups: [
        { caption: '账单与时间', items: [
            { ref: invoice.invoiceId },
            { ref: invoice.invoiceDate },
            { ref: invoice.invoiceDate$year },
            { ref: invoice.invoiceDate$month },
            { ref: invoice.invoiceDate$yearMonth }
        ] },
        { caption: '账单指标', items: [
            { ref: invoice.invoiceAmount },
            { ref: invoice.invoiceCount }
        ] }
    ],
    accesses: []
};

Scope / regression requests

建议覆盖同 path 重复校验、真实 QM 修改后重复校验、clear-existing、重启恢复、Windows 路径规范化、旧 validation Bundle 与已有 Bundle 合并,以及失败前后 catalog 可用性;同时检查新 Namespace 首次校验这个对照。

目前只证明上述正式发行版、SQLite/Chinook 与明确命令顺序;具体代码根因、所有数据库/宿主的影响未验证。未删除已有 Bundle 作为修复,未改产品代码,无生产环境操作。