@lark-project/cli
Version:
飞书项目插件开发工具
491 lines (490 loc) • 18.8 kB
JavaScript
;
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.followSegments = exports.parseSegments = exports.matchFilterValue = exports.peekJsonValue = exports.peekJson = void 0;
const fs_extra_1 = require("fs-extra");
const text_1 = require("./text");
const types_1 = require("../../../../types");
/**
* Peek a JSON file by symbol/path with optional $ref follow.
*
* Why this shape:
* - Schemas we ship (point-schema.json) are often 1000+ lines with heavy $ref
* indirection. `jq` + manual $ref chasing is what AI has to do today.
* - --index gives AI a lightweight TOC for "which point types exist".
* - follow + cycle detection lets AI get a *self-contained* slice in one call.
*/
function peekJson(file, name, opts) {
if (opts.line || opts.match || opts.head || opts.tail) {
return (0, text_1.peekText)(file, opts);
}
const raw = (0, fs_extra_1.readFileSync)(file, 'utf8');
let root;
try {
root = JSON.parse(raw);
}
catch (e) {
process.stderr.write(`Failed to parse JSON: ${e.message}\n`);
process.exit(1);
}
peekJsonValue(root, name, opts, file);
}
exports.peekJson = peekJson;
/**
* Shared entrypoint used by both JSON and YAML peekers so YAML can hand off
* after parsing to js-yaml.
*/
function peekJsonValue(root, name, opts, file) {
var _a;
if (opts.index) {
const idx = buildIndex(root);
if (opts.format === 'json') {
process.stdout.write(JSON.stringify(idx) + '\n');
}
else {
for (const entry of idx) {
process.stdout.write(`${entry.path}\t${(_a = entry.summary) !== null && _a !== void 0 ? _a : ''}\n`);
}
}
return;
}
if (opts.list) {
const names = listDefinitions(root);
if (opts.format === 'json') {
process.stdout.write(JSON.stringify(names) + '\n');
}
else {
for (const n of names)
process.stdout.write(n + '\n');
}
return;
}
if (opts.summary) {
const buckets = buildSummary(root);
if (opts.format === 'json') {
process.stdout.write(JSON.stringify(buckets, null, 2) + '\n');
}
else {
for (const bucket of buckets) {
process.stdout.write(`${bucket.path} (${bucket.items.length}):\n`);
for (const item of bucket.items) {
const label = item.name || item.key;
const desc = item.description ? ` · ${item.description}` : '';
process.stdout.write(` - ${item.key}: ${label}${desc}\n`);
}
}
}
return;
}
if (!name) {
process.stderr.write('JSON peek requires a symbol/path (e.g. `lpm ai peek schema.json LiteAppComponentPoint`), or --index / --list.\n');
process.exit(1);
}
const hit = locate(root, name);
if (!hit) {
process.stderr.write(`Not found: ${name}\n`);
process.stderr.write('Use --index to see top-level entries or --list for definitions.\n');
process.exit(1);
}
const follow = opts.follow !== false;
const maxDepth = typeof opts.maxDepth === 'number' ? opts.maxDepth : 5;
const resolved = follow ? resolveRefs(hit.value, root, maxDepth) : hit.value;
if (opts.descriptionsOnly) {
const descs = collectDescriptions(resolved);
if (opts.format === 'json') {
process.stdout.write(JSON.stringify({ path: hit.path, descriptions: descs }, null, 2) + '\n');
}
else {
process.stdout.write(`# ${hit.path}\n`);
for (const d of descs) {
process.stdout.write(`- ${d.path}: ${d.text}\n`);
}
}
return;
}
const output = { file, path: hit.path, value: resolved };
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
}
exports.peekJsonValue = peekJsonValue;
function locate(root, name) {
// 1) JSON pointer: starts with `#/` or `/`
if (name.startsWith('#/') || name.startsWith('/')) {
const v = resolvePointer(root, name);
if (v !== undefined)
return { path: name, value: v };
}
// 2) Segmented path: dot / slash separators, 支持 `segment[field=value]` 过滤数组
if (name.includes('.') || name.includes('/') || name.includes('[')) {
try {
const segs = parseSegments(name);
const v = followSegments(root, segs);
if (v !== undefined)
return { path: name, value: v };
}
catch (_a) {
// 语法不合法,继续尝试 DFS
}
}
// 3) DFS search — return first node where `properties[name]`, `definitions[name]`
// exists, or where the current key itself equals `name`.
return dfsLocate(root, name, '');
}
/**
* Evaluate a `[field=value]` bracket predicate against an element. Applies the
* `point_type` local-name → backend-wire alias so AI skills can use one
* vocabulary (`page` / `liteAppComponent`…) across schema and draft JSON.
*/
function matchFilterValue(actual, filter) {
const target = filter.field === 'point_type' ? (0, types_1.resolvePointTypeAlias)(filter.value) : filter.value;
return String(actual) === target;
}
exports.matchFilterValue = matchFilterValue;
function parseSegments(path) {
const segs = [];
let i = 0;
while (i < path.length) {
while (i < path.length && (path[i] === '.' || path[i] === '/'))
i++;
if (i >= path.length)
break;
let j = i;
while (j < path.length && path[j] !== '.' && path[j] !== '/' && path[j] !== '[')
j++;
const name = path.slice(i, j);
if (!name)
throw new Error(`empty segment at ${i}`);
let filter;
if (path[j] === '[') {
const close = path.indexOf(']', j);
if (close < 0)
throw new Error('unclosed [');
const expr = path.slice(j + 1, close);
const eq = expr.indexOf('=');
if (eq < 0)
throw new Error(`bracket filter must be "field=value": ${expr}`);
filter = { field: expr.slice(0, eq), value: expr.slice(eq + 1) };
j = close + 1;
}
segs.push({ name, filter });
i = j;
}
return segs;
}
exports.parseSegments = parseSegments;
function followSegments(root, segs) {
let cur = root;
for (const seg of segs) {
if (!cur || typeof cur !== 'object' || Array.isArray(cur))
return undefined;
const obj = cur;
if (!(seg.name in obj))
return undefined;
cur = obj[seg.name];
if (seg.filter) {
if (!Array.isArray(cur))
return undefined;
const matches = cur.filter(x => {
if (!x || typeof x !== 'object' || Array.isArray(x))
return false;
const val = x[seg.filter.field];
return matchFilterValue(val, seg.filter);
});
if (matches.length === 0)
return undefined;
cur = matches.length === 1 ? matches[0] : matches;
}
}
return cur;
}
exports.followSegments = followSegments;
function dfsLocate(node, name, path) {
if (!node || typeof node !== 'object' || Array.isArray(node))
return undefined;
const obj = node;
// direct key match on this object
if (name in obj) {
return { path: path ? `${path}.${name}` : name, value: obj[name] };
}
// common schema holders
for (const holder of ['definitions', 'properties', 'components', 'defs', '$defs']) {
if (holder in obj && obj[holder] && typeof obj[holder] === 'object') {
const h = obj[holder];
if (name in h) {
return { path: path ? `${path}.${holder}.${name}` : `${holder}.${name}`, value: h[name] };
}
}
}
for (const [k, v] of Object.entries(obj)) {
const hit = dfsLocate(v, name, path ? `${path}.${k}` : k);
if (hit)
return hit;
}
return undefined;
}
function resolvePointer(root, pointer) {
const clean = pointer.startsWith('#') ? pointer.slice(1) : pointer;
if (clean === '' || clean === '/')
return root;
const parts = clean.split('/').slice(1).map(decodePointerSegment);
let cur = root;
for (const p of parts) {
if (cur && typeof cur === 'object') {
const obj = cur;
if (Array.isArray(obj)) {
const idx = Number(p);
if (Number.isNaN(idx) || !(idx in obj))
return undefined;
cur = obj[idx];
}
else {
if (!(p in obj))
return undefined;
cur = obj[p];
}
}
else {
return undefined;
}
}
return cur;
}
function decodePointerSegment(s) {
return s.replace(/~1/g, '/').replace(/~0/g, '~');
}
function resolveRefs(node, root, maxDepth) {
const visited = new Set();
return walk(node, 0);
function walk(n, depth) {
if (depth > maxDepth)
return n;
if (!n || typeof n !== 'object')
return n;
if (Array.isArray(n)) {
return n.map(x => walk(x, depth));
}
const obj = n;
const ref = obj.$ref;
if (typeof ref === 'string' && (ref.startsWith('#/') || ref.startsWith('/'))) {
if (visited.has(ref)) {
// 循环:用标记代替继续展开,避免无限嵌套
return { $ref: ref, $circular: true };
}
const target = resolvePointer(root, ref);
if (target === undefined) {
return { $ref: ref, $unresolved: true };
}
visited.add(ref);
const resolved = walk(target, depth + 1);
visited.delete(ref);
// 合并 ref 之外的同级字段(比如 $comment / description),让 AI 同时看到
const { $ref: _unused } = obj, rest = __rest(obj, ["$ref"]);
const restWalked = walk(rest, depth);
if (Object.keys(rest).length > 0 && resolved && typeof resolved === 'object' && !Array.isArray(resolved)) {
return Object.assign(Object.assign({ $resolved_from: ref }, resolved), restWalked);
}
return resolved;
}
const out = {};
for (const [k, v] of Object.entries(obj)) {
out[k] = walk(v, depth);
}
return out;
}
}
function buildIndex(root) {
// 优先:root.properties 下的每个 key(JSON Schema 常见根)
if (root && typeof root === 'object' && !Array.isArray(root)) {
const r = root;
if (r.properties && typeof r.properties === 'object') {
const props = r.properties;
return Object.entries(props).map(([k, v]) => ({
path: `properties.${k}`,
summary: extractSummary(v),
}));
}
// 顶层就一个 wrapper(例如 integrate_point_schema: { properties: {...} })
const entries = Object.entries(r);
if (entries.length === 1) {
const [wrapKey, wrapVal] = entries[0];
if (wrapVal && typeof wrapVal === 'object' && !Array.isArray(wrapVal)) {
const inner = wrapVal;
if (inner.properties && typeof inner.properties === 'object') {
const props = inner.properties;
return Object.entries(props).map(([k, v]) => ({
path: `${wrapKey}.properties.${k}`,
summary: extractSummary(v),
}));
}
}
}
// fallback: 顶层 keys
return entries.map(([k, v]) => ({ path: k, summary: extractSummary(v) }));
}
return [];
}
/**
* Walk the JSON and collect every "array of objects with `.key`" as a summary
* bucket — this is the TOC weak AI uses to get a global view without reading
* each element's deeply nested extension / DSL data.
*
* Path is expressed with `[field=value]` bracket notation so it's directly
* consumable by later peek / patch-json calls.
*/
function buildSummary(root) {
const buckets = [];
walk(root, '');
return buckets;
function walk(node, currentPath) {
if (!node || typeof node !== 'object')
return;
if (Array.isArray(node)) {
if (node.every(el => el && typeof el === 'object' && !Array.isArray(el) && 'key' in el)) {
// array of keyed objects → a bucket
const items = node.map(el => extractSummaryItem(el));
if (items.length > 0) {
buckets.push({ path: currentPath, items });
}
// still descend: elements may contain nested arrays of keyed objects
for (const el of node) {
const keyVal = String(el.key);
for (const [k, v] of Object.entries(el)) {
if (k === 'key')
continue;
walk(v, `${currentPath}[key=${keyVal}].${k}`);
}
}
}
else {
// array but not of keyed objects — descend into each element using index-free approximation
node.forEach((el, i) => walk(el, `${currentPath}[${i}]`));
}
return;
}
const obj = node;
// Special case: array of objects where each object has a "type-like" field + a nested keyed array
// e.g. {data: [{point_type: "board", point_config: [...]}]}
for (const [k, v] of Object.entries(obj)) {
if (Array.isArray(v) &&
v.every(el => el && typeof el === 'object' && !Array.isArray(el)) &&
v.length > 0 &&
hasTypeTag(v)) {
// treat each element as a sub-structure keyed by its type tag
for (const el of v) {
const tagField = findTypeTagField(el);
const tagValue = tagField ? String(el[tagField]) : undefined;
const subPath = tagField && tagValue !== undefined ? `${currentPath ? currentPath + '.' : ''}${k}[${tagField}=${tagValue}]` : `${currentPath ? currentPath + '.' : ''}${k}`;
for (const [sk, sv] of Object.entries(el)) {
if (sk === tagField)
continue;
walk(sv, `${subPath}.${sk}`);
}
}
continue;
}
walk(v, currentPath ? `${currentPath}.${k}` : k);
}
}
}
function hasTypeTag(arr) {
return arr.every(el => findTypeTagField(el) !== undefined);
}
function findTypeTagField(obj) {
// heuristic: look for a *_type scalar field (point_type, type, kind)
for (const candidate of ['point_type', 'type', 'kind']) {
if (candidate in obj && typeof obj[candidate] === 'string') {
// also must have a sibling that looks like data (non-scalar, non-empty)
const hasDataSibling = Object.entries(obj).some(([k, v]) => k !== candidate && typeof v === 'object' && v !== null);
if (hasDataSibling)
return candidate;
}
}
return undefined;
}
function extractSummaryItem(el) {
const key = String(el.key);
let name;
let description;
if (typeof el.name === 'string')
name = el.name;
if (typeof el.description === 'string')
description = el.description;
// fall back to i18n_info.*.name / description
if (!name && el.i18n_info && typeof el.i18n_info === 'object' && !Array.isArray(el.i18n_info)) {
for (const locale of Object.values(el.i18n_info)) {
if (locale && typeof locale === 'object' && !Array.isArray(locale)) {
const l = locale;
if (!name && typeof l.name === 'string')
name = l.name;
if (!description && typeof l.description === 'string')
description = l.description;
}
}
}
return { key, name, description };
}
function listDefinitions(root) {
const names = new Set();
walk(root);
return Array.from(names).sort();
function walk(n) {
if (!n || typeof n !== 'object' || Array.isArray(n))
return;
const obj = n;
for (const holder of ['definitions', 'defs', '$defs']) {
const h = obj[holder];
if (h && typeof h === 'object' && !Array.isArray(h)) {
for (const k of Object.keys(h))
names.add(k);
}
}
for (const v of Object.values(obj))
walk(v);
}
}
function extractSummary(v) {
if (!v || typeof v !== 'object' || Array.isArray(v))
return undefined;
const o = v;
const cand = (typeof o.$comment === 'string' && o.$comment) ||
(typeof o.description === 'string' && o.description) ||
(typeof o.title === 'string' && o.title);
if (!cand)
return undefined;
const oneLine = String(cand).replace(/\s+/g, ' ').trim();
return oneLine.length > 120 ? oneLine.slice(0, 117) + '...' : oneLine;
}
function collectDescriptions(node) {
const out = [];
walk(node, '');
return out;
function walk(n, p) {
if (!n || typeof n !== 'object')
return;
if (Array.isArray(n)) {
n.forEach((x, i) => walk(x, `${p}[${i}]`));
return;
}
const obj = n;
for (const key of ['description', '$comment', 'title']) {
if (typeof obj[key] === 'string') {
out.push({ path: p || '.', text: String(obj[key]).replace(/\s+/g, ' ').trim() });
break; // 一个节点只取最有信息量的一个
}
}
for (const [k, v] of Object.entries(obj)) {
if (k === 'description' || k === '$comment' || k === 'title')
continue;
walk(v, p ? `${p}.${k}` : k);
}
}
}