UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

491 lines (490 loc) 18.8 kB
"use strict"; 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); } } }