@lark-project/cli
Version:
飞书项目插件开发工具
343 lines (342 loc) • 21 kB
JavaScript
;
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;