UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

343 lines (342 loc) 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.publishProject = void 0; const version_1 = require("../../../api/tools/version"); const release_app_1 = require("../../../api/tools/release-app"); const package_1 = require("../../../api/tools/package"); const local_plugin_config_1 = require("../../../local-plugin-config"); const get_plugin_app_type_1 = require("../../../utils/get-plugin-app-type"); const logger_1 = require("../../../utils/logger"); const workspace_1 = require("../../utils/workspace"); const ensure_plugin_metadata_ready_1 = require("../utils/ensure-plugin-metadata-ready"); const collect_missing_base_info_1 = require("../utils/collect-missing-base-info"); function getErrorMessage(error) { return error instanceof Error ? error.message : String(error); } // 规避 DB 主从同步延迟:关键写操作之间留出缓冲,避免下一步读到旧数据 const DB_REPLICATION_DELAY_MS = 500; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function incrementVersion(version) { const parts = version.split('.'); if (parts.length !== 3) { return '1.0.0'; } const patch = parseInt(parts[2], 10); return `${parts[0]}.${parts[1]}.${patch + 1}`; } function buildVisibility(payload, latestVersion) { // 用户显式指定了 store 时使用用户的值 if (payload.store) { return { status: 0, show_store: payload.store === 'yes', }; } // 继承最新版本的 visibility if (latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.visibility) { return latestVersion.visibility; } // 首次发布默认值 return { status: 0, show_store: true, }; } function buildUpgradeStrategy(payload, latestVersion) { // 用户显式指定了 upgrade 时使用用户的值 if (payload.upgrade) { const upgrade = payload.upgrade; if (upgrade === 'manual') { return { strategy_type: 0, limit_type: 1 }; } if (upgrade === 'all') { return { strategy_type: 2 }; } if (!payload.upgradeLimitVersion) { return undefined; } return { strategy_type: 1, limit_version: payload.upgradeLimitVersion, limit_type: payload.upgradeLimitType === 'lower' ? 1 : 0, }; } // 继承最新版本的 upgrade_strategy if (latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.upgrade_strategy) { return latestVersion.upgrade_strategy; } // 首次发布默认值 return { strategy_type: 0, limit_type: 1 }; } async function ensureArtifactVersionInPackageList({ siteDomain, appKey, artifactVersion, }) { logger_1.logger.info('Fetching package list...'); const { list: packageList } = await (0, package_1.getPackageList)({ siteDomain, appKey }); const packageVersions = (packageList !== null && packageList !== void 0 ? packageList : []) .map(item => { var _a, _b; return ({ version: (_a = item.package_version) !== null && _a !== void 0 ? _a : '', updatedTime: (_b = item.updated_time) !== null && _b !== void 0 ? _b : 0, }); }) .filter(item => item.version); if (packageVersions.length === 0) { throw new Error(`Artifact version "${artifactVersion}" not found: no packages have been uploaded yet. ` + 'Please run "lpm release" to upload a package before publishing.'); } if (!packageVersions.some(item => item.version === artifactVersion)) { const sorted = [...packageVersions].sort((a, b) => b.updatedTime - a.updatedTime); const latestPackage = sorted[0].version; const availableVersions = sorted.map(item => item.version).join(', '); throw new Error(`Artifact version "${artifactVersion}" not found in uploaded packages. ` + `Available versions: [${availableVersions}]. Latest: ${latestPackage}.`); } logger_1.logger.success(`Artifact version ${artifactVersion} found in package list.`); } function emitWorkspaceCleanup() { try { (0, workspace_1.cleanWorkspace)({ scope: 'all', keepState: true }); } catch (cleanupErr) { logger_1.logger.warn(`Workspace cleanup failed (non-fatal): ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`); } } /** * AI 插件发布通道:与普通插件版本提交三步走完全不同,只有一次 release_app 调用, * 不创建版本号、不需要 release_notes / upgrade_strategy。前端 publishAINodeConfigByPayload * 等价于此函数。 * * release_type 永远用 Normal(全量):每次发布都从草稿态全量发布——base info(name/icon/ * short/category)+ 点位一起上线,让首发后的基本信息改动也能随 publish 生效(ReleasePointOnly * 只发点位、冻结 base info,会导致改了名字/图标 publish 推不上去)。后端对已上架应用重复 * Normal 不拦(AllowRepeatedRelease 对 VersionStatusReleased 放行),AI 应用用固定 version code、 * 重复发只更新同一版本不产生版本冲突。代价:每次都用草稿 base info 覆盖线上,故发布前非阻断 * 打印这份 base info 让用户看清要发什么。 * * 仍拉 versionList——不为判 release_type,而是从上一版继承 visibility(project_key/tenant_key/ * display_id),首发则用默认值。 * * 注意:不调 releaseAppBaseInfo —— 那个接口要求插件**已有线上版本**(后端 * QueryAppLastOnlineVersionByAppKey 必须非空),首发必然 nil 报 ErrAppCenterPluginVersionNotFound * (1000050276);且 Normal 全量本就覆盖 base info,无需单独发。releaseAppBaseInfo 仅用于 * "已上架后单独更新基础信息(不发新版本)"——本 CLI 当前没有该用例。 */ async function publishAIPlugin({ siteDomain, appKey, appType, payload, }) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; // 无前端产物(ai_field 永远 / ai_node 未开节点卡片)的 AI 应用不需要产物版本——可省略 --artifact-version, // 兜底为 '0'(与 release 对无前端 AI 输出的 'Artifact version: 0' / 前端 jsOptions 的 '0' 哨兵一致)。 // 开了节点卡片的 ai_node(resources 非空)有真实产物版本,仍需显式传,否则按 '0' 发布会丢前端。 let artifactVersion = (_a = payload.artifactVersion) === null || _a === void 0 ? void 0 : _a.trim(); if (!artifactVersion) { const hasFrontend = ((_d = (_c = (_b = (0, local_plugin_config_1.getLocalPluginConfig)()) === null || _b === void 0 ? void 0 : _b.resources) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0; if (hasFrontend) { throw new Error('--artifact-version is required: this AI plugin has a node card (resources is non-empty). ' + 'Run "lpm release" and pass its "Artifact version" output. Only AI plugins without a frontend artifact may omit it.'); } artifactVersion = '0'; logger_1.logger.info('AI plugin has no frontend artifact (resources=0); --artifact-version omitted, using 0.'); } // 提示忽略 normal 专属字段——传了不报错,仅 stderr 提醒(避免 skill 编排时打断流水线) const aiIgnoredFlags = []; if (payload.releaseNotes) aiIgnoredFlags.push('--release-notes'); if (payload.version) aiIgnoredFlags.push('--version'); if (payload.upgrade && payload.upgrade !== 'manual') aiIgnoredFlags.push('--upgrade'); if (payload.upgradeLimitVersion) aiIgnoredFlags.push('--upgrade-limit-version'); if (payload.upgradeLimitType) aiIgnoredFlags.push('--upgrade-limit-type'); if (aiIgnoredFlags.length > 0) { logger_1.logger.warn(`AI plugin (app_type=${appType}) ignores: ${aiIgnoredFlags.join(', ')} — release_app does not consume these.`); } const descInfo = await (0, ensure_plugin_metadata_ready_1.ensurePluginMetadataReady)({ siteDomain, appKey, localAppType: appType }); // AI 发布永远全量,base info 每次都会用草稿值覆盖线上——非阻断打印让用户看清要发什么。 // 只打印不暂停(publish 会被 skill 编排 / CI 调用,不能卡在交互上)。 const zhInfo = (_e = descInfo.i18n_info) === null || _e === void 0 ? void 0 : _e['zh-cn']; logger_1.logger.info('AI full release — the following draft base info will be published live:'); logger_1.logger.info(` • name: ${(_f = zhInfo === null || zhInfo === void 0 ? void 0 : zhInfo.name) !== null && _f !== void 0 ? _f : '(none)'}`); logger_1.logger.info(` • short: ${(_g = zhInfo === null || zhInfo === void 0 ? void 0 : zhInfo.short) !== null && _g !== void 0 ? _g : '(none)'}`); logger_1.logger.info(` • icon: ${(_h = descInfo.icon) !== null && _h !== void 0 ? _h : '(none)'}`); logger_1.logger.info(` • category: ${((_j = descInfo.category_ids) !== null && _j !== void 0 ? _j : []).join(', ') || '(none)'}`); // artifactVersion='0' 表示无前端产物(与前端 jsOptions 的 '0' 哨兵对齐),跳过 packageList 校验 if (artifactVersion !== '0') { await ensureArtifactVersionInPackageList({ siteDomain, appKey, artifactVersion }); } else { logger_1.logger.info('artifactVersion=0 (no frontend artifact), skipping package list check.'); } // 仍拉 versionList,但只为继承上一版 visibility(不再据此判 release_type) logger_1.logger.info('Fetching version list to inherit visibility...'); const { list: versionList } = await (0, version_1.getVersionList)({ siteDomain, appKey }); const latestVersion = versionList === null || versionList === void 0 ? void 0 : versionList[0]; // AI 发布每次全量:release_type 恒为 Normal,从草稿全量发布(base info + 点位) const releaseType = release_app_1.AIReleaseType.Normal; // visibility:继承上一版兜底,缺省走默认值;AI 插件 show_store 强制为 true(与前端 PublishForm 行为一致) // 用户传 --store no 也无效(仅 stderr 提示),符合 AI 弹窗"不暴露上架开关"的产品决定。 if (payload.store === 'no') { logger_1.logger.warn('AI plugin always lists in store; --store=no is ignored.'); } const inherited = latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.visibility; const visibility = { status: (_k = inherited === null || inherited === void 0 ? void 0 : inherited.status) !== null && _k !== void 0 ? _k : 0, project_key: (_l = inherited === null || inherited === void 0 ? void 0 : inherited.project_key) !== null && _l !== void 0 ? _l : [], tenant_key: (_m = inherited === null || inherited === void 0 ? void 0 : inherited.tenant_key) !== null && _m !== void 0 ? _m : [], display_id: (_o = inherited === null || inherited === void 0 ? void 0 : inherited.display_id) !== null && _o !== void 0 ? _o : [], show_store: true, }; const sceneType = (_p = payload.sceneType) !== null && _p !== void 0 ? _p : 1; logger_1.logger.info(`Releasing AI plugin (release_type=${releaseType === release_app_1.AIReleaseType.Normal ? 'Normal' : 'ReleasePointOnly'})...`); await (0, release_app_1.releaseApp)({ siteDomain, appKey, sceneType, frontVersion: artifactVersion, visibility, releaseType, }).catch(error => { throw new Error(`Release app failed: ${getErrorMessage(error)}`); }); logger_1.logger.success('🍻🍻🍻 Publish successfully!'); const PluginShareUrl = `${siteDomain}/openapp/plugin_share?appKey=${appKey}`; logger_1.logger.info(`Plugin share url: ${PluginShareUrl}`); emitWorkspaceCleanup(); } async function publishProject(payload) { var _a, _b; // 普通插件:一旦目标版本已存在于服务端(新建成功后 / 复用一进入),记下版本号; // 之后任何失败都在顶层 catch 追加「如何复用该版本重发」的恢复指引。 let pendingVersion; try { const localPluginConfig = (0, local_plugin_config_1.getLocalPluginConfig)(); if (!localPluginConfig) { throw new Error('Local plugin config not found, please check and retry.'); } const appKey = localPluginConfig.pluginId; const siteDomain = localPluginConfig.siteDomain; const appType = (0, get_plugin_app_type_1.getPluginAppType)(); if (appType === 'ai_node' || appType === 'ai_field') { await publishAIPlugin({ siteDomain, appKey, appType, payload }); return; } // ─── 普通插件流程 ─── const releaseNotes = (_a = payload.releaseNotes) === null || _a === void 0 ? void 0 : _a.trim(); const artifactVersion = (_b = payload.artifactVersion) === null || _b === void 0 ? void 0 : _b.trim(); if (!releaseNotes) { throw new Error('--release-notes is required, please check and retry.'); } if (!artifactVersion) { throw new Error('--artifact-version is required (from release output), please check and retry.'); } // Layer 1:发布前 fail-fast 闸门(name/short/detail/icon/category 完整性 + app_type 对账)。 // 返回的 descInfo 复用给 Layer 2,避免二次拉取。 const descInfo = await (0, ensure_plugin_metadata_ready_1.ensurePluginMetadataReady)({ siteDomain, appKey, localAppType: appType }); await ensureArtifactVersionInPackageList({ siteDomain, appKey, artifactVersion }); // 获取版本列表:用于分流(新建 vs 复用未完成版本)+ 继承历史配置 logger_1.logger.info('Fetching version list...'); const { list: versionList } = await (0, version_1.getVersionList)({ siteDomain, appKey }); const latestVersion = versionList === null || versionList === void 0 ? void 0 : versionList[0]; // 分流:决定目标版本号 + 模式(create=新建版本 / reuse=复用已存在版本重新提交)。 // ① 显式 --version:命中已存在 → reuse;否则 → create // ② 未传:无版本 → 1.0.0 create;最新版本已 OnShelf(线上生效)→ patch+1 create; // 最新版本非 OnShelf(= 未完成发布)→ 复用其版本号 reuse // 仅当 list[0] 自身是 OnShelf 才新建——不是「存在任意 OnShelf」;老版本已上架而最新是 // 未完成草稿时,要复用那条草稿(正是本次要修的卡死场景)。 let version; let mode; if (payload.version) { version = payload.version; mode = (versionList === null || versionList === void 0 ? void 0 : versionList.some(v => v.app_version === version)) ? 'reuse' : 'create'; } else if (!(latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.app_version)) { version = '1.0.0'; mode = 'create'; logger_1.logger.info('No existing versions found, using 1.0.0'); } else if (latestVersion.status === version_1.AppVersionStatus.OnShelf) { version = incrementVersion(latestVersion.app_version); mode = 'create'; logger_1.logger.info(`Latest version ${latestVersion.app_version} is on shelf, creating new version ${version}`); } else { version = latestVersion.app_version; mode = 'reuse'; logger_1.logger.warn(`检测到未完成发布的版本 ${version},将复用该版本号并重新提交(不新建版本)。`); } // 从最新版本继承 visibility、upgrade_strategy const visibility = buildVisibility(payload, latestVersion); const upgradeStrategy = buildUpgradeStrategy(payload, latestVersion); const versionInfo = Object.assign(Object.assign(Object.assign({ app_version: version, front_version: artifactVersion, runtime_version: '2.0.0', scene_type: 1, // 暂不支持生成isv插件 visibility }, (upgradeStrategy ? { upgrade_strategy: upgradeStrategy } : {})), { description: { 'zh-cn': releaseNotes, } }), ((latestVersion === null || latestVersion === void 0 ? void 0 : latestVersion.store_version) ? { store_version: latestVersion.store_version } : {})); if (mode === 'create') { logger_1.logger.info(`Validating version ${version}...`); const validateResult = await (0, version_1.validateVersion)({ siteDomain, appKey, version }).catch(error => { throw new Error(`Validate version failed: ${getErrorMessage(error)}`); }); if (validateResult.reason !== 0) { throw new Error(`Validate version failed: version ${version} is invalid (reason: ${validateResult.reason}).`); } logger_1.logger.success(`Version ${version} validated successfully.`); await sleep(DB_REPLICATION_DELAY_MS); logger_1.logger.info(`Creating version info for ${version}...`); const createResult = await (0, version_1.createVersionInfo)({ siteDomain, appKey, versionInfo }).catch(error => { throw new Error(`Create version info failed: ${getErrorMessage(error)}`); }); logger_1.logger.debug('createVersionInfo response:', JSON.stringify(createResult)); logger_1.logger.success(`Version info ${version} created successfully.`); // 版本此刻已存在于服务端:后续 commit 若失败,catch 要能给出复用重发指引。 pendingVersion = version; await sleep(DB_REPLICATION_DELAY_MS); } else { // reuse:复用已存在版本(跳过 validate——版本已存在,validate 必报 reason!=0)。 pendingVersion = version; logger_1.logger.info(`Updating version info for existing version ${version}...`); const updateResult = await (0, version_1.updateVersionInfo)({ siteDomain, appKey, versionInfo }).catch(error => { throw new Error(`Update version info failed: ${getErrorMessage(error)}`); }); logger_1.logger.debug('updateVersionInfo response:', JSON.stringify(updateResult)); logger_1.logger.success(`Version info ${version} updated successfully.`); await sleep(DB_REPLICATION_DELAY_MS); } // Layer 2:commit 前硬屏障——新建/复用两条路径都过。复用 Layer 1 拉到的 descInfo(其间无 // 元信息写操作,无需二次拉取),确保任何不可逆 commit 都不带不完整 base info 上线。 const missingBeforeCommit = (0, collect_missing_base_info_1.collectMissingBaseInfo)(descInfo); if (missingBeforeCommit.length > 0) { throw new Error((0, collect_missing_base_info_1.formatMissingBaseInfoError)(missingBeforeCommit)); } logger_1.logger.info(`Committing version ${version}...`); const commitResult = await (0, version_1.commitVersion)({ siteDomain, appKey, appVersion: version }).catch(error => { throw new Error(`Commit version failed: ${getErrorMessage(error)}`); }); logger_1.logger.debug('commitVersion response:', JSON.stringify(commitResult)); await sleep(DB_REPLICATION_DELAY_MS); logger_1.logger.success(`🍻🍻🍻 Publish successfully! Current version is ${version}.`); const PluginShareUrl = `${siteDomain}/openapp/plugin_share?appKey=${appKey}`; logger_1.logger.info(`Plugin share url: ${PluginShareUrl}`); // 发布成功收尾:清理全部中间缓存,仅保留 state.json 供 workflow 断点恢复使用。 // 失败路径不清 —— 保留 draft/schema/mcp 缓存便于排障和续跑。 emitWorkspaceCleanup(); } catch (error) { logger_1.logger.error(getErrorMessage(error)); // 版本已建出但没走到上架:给出复用重发指引,避免被迫新建 +1 版本而卡死。 if (pendingVersion) { logger_1.logger.error(`\n版本 ${pendingVersion} 已创建但未完成发布。修复后无需新建版本:\n` + ' 1. 若因 base info 不全被打回 → 先补对应字段(见上方指引 / lpm update-description)\n' + ' 2. 重新运行 lpm publish(可不传 --version)\n' + ` CLI 会识别 ${pendingVersion} 处于"未完成发布"状态,复用该版本号重新提交,不会新建版本。`); } process.exit(1); } } exports.publishProject = publishProject;