@lark-project/cli
Version:
飞书项目插件开发工具
199 lines (198 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.permService = void 0;
const local_plugin_config_1 = require("../../../local-plugin-config");
const logger_1 = require("../../../utils/logger");
const app_perm_1 = require("../../../api/app-perm");
const plugin_registry_1 = require("../../utils/plugin-registry");
const auth_config_1 = require("../../utils/auth-config");
function toScopeView(scopeKey, meta) {
var _a;
return { scope: scopeKey, name: meta === null || meta === void 0 ? void 0 : meta.name, openapis: (_a = meta === null || meta === void 0 ? void 0 : meta.resource) !== null && _a !== void 0 ? _a : [] };
}
function deriveListView(result, appType, isAIPlugin) {
var _a, _b, _c;
const metaByKey = new Map(((_a = result.perm_meta) !== null && _a !== void 0 ? _a : []).filter(m => m.key).map(m => [m.key, m]));
const grantedKeys = new Set(((_b = result.perm_info) !== null && _b !== void 0 ? _b : []).map(p => p.key).filter((k) => !!k));
const granted = [...grantedKeys].map(key => toScopeView(key, metaByKey.get(key)));
const view = { appType, granted };
// AI 应用不能 apply:天花板 = granted 固定集,省略 applicable。
if (!isAIPlugin) {
view.applicable = ((_c = result.perm_meta) !== null && _c !== void 0 ? _c : [])
.filter(m => m.key && !grantedKeys.has(m.key))
.map(m => toScopeView(m.key, m));
}
return view;
}
/**
* 反查一个 OpenAPI token 归属哪些 scope。token 可为完整 resource key
* (bytedance.bits.workitem:OAPIGetWorkItemsByIds)、短名(OAPIGetWorkItemsByIds)或中文名(获取工作项详情)。
*/
function matchApiToScopes(token, perm_meta) {
var _a, _b;
const scopes = new Set();
for (const meta of perm_meta !== null && perm_meta !== void 0 ? perm_meta : []) {
if (!meta.key)
continue;
for (const res of (_a = meta.resource) !== null && _a !== void 0 ? _a : []) {
const shortKey = (_b = res.key) === null || _b === void 0 ? void 0 : _b.split(':').pop();
if (res.key === token || shortKey === token || res.name === token) {
scopes.add(meta.key);
}
}
}
return { scopes: [...scopes] };
}
function deriveCheckView(apis, result, appType, isAIPlugin) {
var _a, _b, _c;
const metaByKey = new Map(((_a = result.perm_meta) !== null && _a !== void 0 ? _a : []).filter(m => m.key).map(m => [m.key, m]));
const grantedKeys = new Set(((_b = result.perm_info) !== null && _b !== void 0 ? _b : []).map(p => p.key).filter((k) => !!k));
const view = { appType, satisfied: [], needApply: [], infeasible: [], unknown: [], ambiguous: [] };
const needApplyByScope = new Map();
for (const api of apis) {
const { scopes } = matchApiToScopes(api, result.perm_meta);
if (scopes.length === 0) {
view.unknown.push(api);
}
else if (scopes.length > 1) {
view.ambiguous.push({ api, candidates: scopes });
}
else if (grantedKeys.has(scopes[0])) {
view.satisfied.push(api);
}
else if (isAIPlugin) {
view.infeasible.push({ api, reason: 'ai-plugin-fixed-scope' });
}
else {
const arr = (_c = needApplyByScope.get(scopes[0])) !== null && _c !== void 0 ? _c : [];
arr.push(api);
needApplyByScope.set(scopes[0], arr);
}
}
view.needApply = [...needApplyByScope.entries()].map(([scope, list]) => {
var _a;
return ({
scope,
name: (_a = metaByKey.get(scope)) === null || _a === void 0 ? void 0 : _a.name,
apis: list,
});
});
return view;
}
/**
* 定位目标插件,优先级:
* 1. 显式 `--plugin/--site-domain`(工程目录外);
* 2. 当前工程目录的 plugin.config.json(看得见的锚);
* 3. 全局只读注册表(单登录态 + 该站点恰好记录过一个插件时自动定位,否则报错列出让用户加 flag)。
* 只读定位无 secret;写仍受 plugin.config.json(含 secret)约束,与本函数无关。
*/
function resolveTarget(payload) {
var _a;
if (payload.pluginId) {
if (!payload.siteDomain) {
return { error: '`--plugin <id>` requires `--site-domain <url>` (the site that owns the plugin).' };
}
const appType = (_a = (0, plugin_registry_1.listPlugins)(payload.siteDomain).find(p => p.pluginId === payload.pluginId)) === null || _a === void 0 ? void 0 : _a.app_type;
(0, plugin_registry_1.recordPlugin)(payload.siteDomain, { pluginId: payload.pluginId });
return { pluginId: payload.pluginId, siteDomain: payload.siteDomain, appType, fromLocal: false };
}
const local = (0, local_plugin_config_1.getLocalPluginConfig)();
if (local) {
return { pluginId: local.pluginId, siteDomain: local.siteDomain, appType: local.app_type, fromLocal: true };
}
const domains = Object.keys((0, auth_config_1.loadStore)());
const siteDomain = payload.siteDomain || (domains.length === 1 ? domains[0] : undefined);
if (!siteDomain) {
return {
error: domains.length === 0
? '`lpm perm` outside a plugin project needs `--plugin <id> --site-domain <url>` (no logged-in site found — run `lpm login` or pass both flags).'
: `Multiple logged-in sites (${domains.join(', ')}). Re-run with \`--plugin <id> --site-domain <url>\`.`,
};
}
const plugins = (0, plugin_registry_1.listPlugins)(siteDomain);
if (plugins.length === 1) {
return { pluginId: plugins[0].pluginId, siteDomain, appType: plugins[0].app_type, fromLocal: false };
}
if (plugins.length === 0) {
return {
error: `No plugins recorded for ${siteDomain}. Run \`lpm perm list --plugin <id> --site-domain ${siteDomain}\` once (it gets remembered), or run inside the plugin project.`,
};
}
return {
error: `Multiple plugins recorded for ${siteDomain}:\n` +
plugins.map(p => ` - ${p.pluginId}${p.app_type ? ` (${p.app_type})` : ''}`).join('\n') +
`\nRe-run with \`--plugin <id> --site-domain ${siteDomain}\`.`,
};
}
async function permService(payload) {
const resolved = resolveTarget(payload);
if ('error' in resolved) {
logger_1.logger.error(resolved.error);
process.exit(1);
}
const { pluginId, siteDomain, appType, fromLocal } = resolved;
// 工程目录外定位时回显目标,避免对着错插件操作而不自知(cwd 锚本就看得见,无需回显)。
if (!fromLocal) {
process.stderr.write(`perm ${payload.action}: plugin ${pluginId} @ ${siteDomain}\n`);
}
const isAIPlugin = appType === 'ai_node' || appType === 'ai_field';
const effectiveAppType = appType !== null && appType !== void 0 ? appType : 'normal';
const { action, scopes, apis, raw } = payload;
// AI plugins: OpenAPI scopes are auto-granted at create time; surface that up
// front (on read actions) so AI agents stop hunting for an apply step that
// doesn't exist. Derived views already encode this (no applicable / infeasible),
// but the note keeps the human-facing reason explicit.
const emitAINote = () => {
if (isAIPlugin) {
process.stderr.write(`info: this is an ${appType} plugin — OpenAPI scopes are auto-granted at create time. ` +
'Use `lpm perm list` (this command) to verify; `lpm perm apply` is rejected by the backend on AI plugins.\n');
}
};
if (action === 'list') {
emitAINote();
const result = await (0, app_perm_1.getAppPermList)(pluginId, siteDomain);
// --raw: backend's original { perm_info, perm_meta } (debug / escape hatch).
// Default: CLI-derived { granted, applicable } so callers don't re-implement
// the perm_info ∩ perm_meta join themselves.
console.log(JSON.stringify(raw ? result : deriveListView(result, effectiveAppType, isAIPlugin), null, 2));
return;
}
if (action === 'check') {
if (!apis || apis.length === 0) {
logger_1.logger.error('`lpm perm check` requires --apis <a,b,c> (OpenAPI keys / short names / Chinese names).');
process.exit(1);
}
emitAINote();
const result = await (0, app_perm_1.getAppPermList)(pluginId, siteDomain);
console.log(JSON.stringify(deriveCheckView(apis, result, effectiveAppType, isAIPlugin), null, 2));
return;
}
if (action === 'apply') {
// Hard-stop apply on AI plugins: backend rejects it anyway, and AI agents
// routinely waste turns retrying. Exit 1 with the actionable next-step.
if (isAIPlugin) {
logger_1.logger.error(`\`lpm perm apply\` is not available on ${appType} plugins — OpenAPI scopes are auto-granted at create time. ` +
'Run `lpm perm list` to verify the already-granted scopes.');
process.exit(1);
}
if (!scopes || scopes.length === 0) {
logger_1.logger.error('`lpm perm apply` requires --scopes <a,b,c>.');
process.exit(1);
}
// Validate against the catalog (perm_meta) before hitting the backend, so a
// typo in a scope key fails fast with an actionable hint instead of a backend error.
const { perm_meta } = await (0, app_perm_1.getAppPermList)(pluginId, siteDomain);
const known = new Set((perm_meta !== null && perm_meta !== void 0 ? perm_meta : []).map(m => m.key));
const unknown = scopes.filter(s => !known.has(s));
if (unknown.length > 0) {
logger_1.logger.error(`Unknown scope(s): ${unknown.join(', ')}. Run \`lpm perm list\` to see the scopes available for this plugin.`);
process.exit(1);
}
const result = await (0, app_perm_1.applyAppPerm)(pluginId, siteDomain, scopes);
console.log(JSON.stringify(result, null, 2));
return;
}
logger_1.logger.error(`Unknown perm action "${action}". Use "list", "check", or "apply".`);
process.exit(1);
}
exports.permService = permService;