UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

341 lines (327 loc) 18 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.addAiCommand = void 0; // ⚠️ 改命令名 / flag / alias 时,同步 grep `lpm` skills/ .claude/skills/ 修文档引用 const path_1 = __importDefault(require("path")); const types_1 = require("../../types"); const run_script_1 = __importDefault(require("../../utils/run-script")); const logger_1 = require("../../utils/logger"); /** * `lpm ai` — AI agent utilities used by meegle-plugin-* skills. * * 设计原则: * - 所有"只给 AI 用"的命令收在此命名空间,与开发者命令(create/start/build/…)隔离 * - 开发者在 `lpm --help` 顶层只看到 `ai` 一个入口,不会被 peek/checkpoint 等噪音干扰 * - AI 通过 skill 文档学习 `lpm ai <sub>` 形式;弱 AI 只需记 `lpm ai peek <file> <name>` 一条公式 * - 跨 OS / 跨 agent 工作(node 实现,不依赖 jq/rg/sed) */ function addAiCommand(program) { const ai = program .command('ai') .description('AI agent tools (used by meegle-plugin-* skills; developers rarely need these directly)'); // ================================================================= // lpm ai peek — 大文件切片 // ================================================================= const peek = ai .command('peek') .description('Slice a large file by symbol / path / section; falls back to line ranges'); peek .argument('<file>', 'File to peek (JSON / YAML / .d.ts / .md / plain text)') .argument('[name]', 'Symbol / path / section title to locate; CLI auto-detects type by file extension') .option('--line <n>', 'Positional mode: start line (1-based)', parseInt) .option('--limit <n>', 'Positional mode: number of lines (works only with --line or --match)', parseInt) .option('--match <pattern>', 'Match mode: regex to search') .option('--after <n>', 'Match mode: lines to show after match', parseInt) .option('--before <n>', 'Match mode: lines to show before match', parseInt) .option('--head <n>', 'Take first N lines', parseInt) .option('--tail <n>', 'Take last N lines', parseInt) .option('--index', 'Structural mode: list all top-level symbols / point types') .option('--list', 'Structural mode: list all symbols (class/type/interface for .d.ts; headings for .md)') .option('--summary', 'Structural mode: TOC for config-like JSON (buckets of keyed objects) — ~1k token global view') .option('--no-follow', 'Do not follow $ref (JSON/YAML) or type references (.d.ts); default is follow') .option('--max-depth <n>', 'Max recursion depth for --follow (default 5)', parseInt, 5) .option('--descriptions-only', 'Structural mode: return only description fields, skip validation noise') .option('--format <fmt>', 'Output format: text | json', 'text') .action((file, name, opts) => { // 互斥参数校验:结构模式不允许 --limit if ((opts.limit || opts.line) && (name || opts.index || opts.list || opts.descriptionsOnly)) { // 允许 --limit 仅与 --match 搭配;其他结构模式禁止 --limit if (!opts.match) { logger_1.logger.error('--limit/--line 不能与结构模式(<name> / --index / --list / --descriptions-only)同时使用'); logger_1.logger.error('结构模式自动取完整单元,不需要截断;位置切片请单独使用 --line + --limit'); process.exit(1); } } (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'peek', file, name, options: opts, }), ]); }); // ================================================================= // lpm ai checkpoint — 工作流事件日志 // // 采用 append-only 的 events.jsonl(每步一行 JSON),避免 state.json // 并发写冲突;status/get 从 jsonl 重放得到当前状态。 // ================================================================= const checkpoint = ai .command('checkpoint') .description('Workflow event log (append-only .lpm-cache/events.jsonl)'); checkpoint .command('append') .description('Append a workflow event to .lpm-cache/events.jsonl') .argument('<event>', 'JSON event payload, e.g. \'{"phase":1,"step":"1.1","status":"success"}\'') .action((event) => { // 验证是合法 JSON try { JSON.parse(event); } catch (e) { logger_1.logger.error(`event payload must be valid JSON: ${e.message}`); process.exit(1); } (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'checkpoint.append', event }), ]); }); checkpoint .command('status') .description('Show current workflow phase/step summary (replayed from events.jsonl)') .option('--format <fmt>', 'Output format: text | json', 'text') .action((opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'checkpoint.status', options: opts }), ]); }); checkpoint .command('get') .description('Get full checkpoint state as JSON (replayed from events.jsonl)') .option('--phase <n>', 'Filter by phase', parseInt) .option('--step <s>', 'Filter by step key') .action((opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'checkpoint.get', options: opts }), ]); }); // ================================================================= // lpm ai state — 工作流断点状态(.lpm-cache/state.json) // // 与 checkpoint(events.jsonl) 互补:state 保存单一的"上下文 + 进度"快照 // (projectRoot / pluginId / nextStep 等),路径经 workspacePaths() 锚定到 // 插件工程根。配合 `lpm --cwd <dir>` 可在任意目录写对位置,取代裸 `cat >`。 // ================================================================= const state = ai .command('state') .description('Read/write the workflow checkpoint state (.lpm-cache/state.json), resolved to the plugin project root'); state .command('set') .description('Write the full checkpoint state JSON to .lpm-cache/state.json (anchor the project root with `lpm --cwd <dir>`)') .argument('<json>', 'State JSON object — inline, @file, or @- to read stdin') .action((json) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'state.set', value: json }), ]); }); state .command('get') .description('Print .lpm-cache/state.json to stdout (empty output if absent)') .action(() => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'state.get' }), ]); }); // ================================================================= // lpm ai init-draft — 从 remote 快照一步建 draft(cp + 路径校验合并) // // 笨 AI 最易忘记 `cp remote → draft`,这一步 CLI 兜底: // - 自动拼 `.lpm-cache/config/draft-<ts>.json` 文件名 // - 拒绝把 draft 命名成非 draft-* 前缀(防止误覆盖 remote / point.config.local) // - stdout 回显 draft 路径,方便后续 peek / patch-json 链式使用 // ================================================================= ai .command('init-draft') .description('Initialize a draft from .lpm-cache/config/remote.json (cp + path validation in one step)') .option('--from <path>', 'Source file (default: .lpm-cache/config/remote.json)') .option('--to <path>', 'Explicit draft path (must start with "draft-"); default auto-generates by timestamp') .option('--format <fmt>', 'Output format: text | json', 'text') .action((opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'init-draft', options: opts }), ]); }); // ================================================================= // lpm ai patch-json — 磁盘层结构化 patch,避免把整份 JSON 塞进 context // // 核心价值:配 `cp remote.json draft.json` 使用时,draft 保证全量、AI 只 // 通过 path 精准动几处——解决"整读 lint"和"全量提交约束"之间的张力。 // ================================================================= ai .command('patch-json') .description('Surgically patch a JSON file by path (compatible with peek syntax)') .argument('<file>', 'JSON file to patch (will be modified in place)') .option('--set-path <path>', 'Path to replace (pair with --set-value)') .option('--set-value <json>', 'New JSON value (@file or @- also OK)') .option('--add-path <path>', 'Array path to append into (pair with --add-value)') .option('--add-value <json>', 'JSON value to append') .option('--delete <path>', 'Path to delete') .option('--merge-path <path>', 'Object path to merge fields into (pair with --merge-value)') .option('--merge-value <json>', 'Partial object (JSON) to shallow-merge') .option('--patches <file>', 'Batch mode: apply JSON Patch-like list [{op,path,value?}, ...]') .option('--format <fmt>', 'Output format: text | json', 'text') .addHelpText('after', ` 重要提示(给 AI 用): patch-json 只修改本地 draft-*.json 文件,\x1b[1m不会\x1b[0m把改动推到后台。 真正提交变更必须用: lpm local-config set <draft-path> set 阶段会对比远端基线,发现减少点位时会要求二次确认。 不要把 patch-json 当作"已提交"——它只是让 draft 在磁盘上保持全量、 context 里只装 delta,减少整读压力。 提交流程:init-draft → patch-json (一次或多次) → local-config set `) .action((file, opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'patch-json', file, options: opts, }), ]); }); // ================================================================= // lpm ai audit-jssdk — 校验 window.JSSDK.<ns>.<method>(…) 调用在 types 里存在 // // 原先 skill 里的 A1.5 审计步骤用 bash + grep + awk 实现,非 Windows 环境/ // 非标准 shell 跑不起来;这里下沉成原生 Node 子命令,任意 agent 任意 OS 均可调。 // ================================================================= ai .command('audit-jssdk') .description('Audit window.JSSDK.<ns>.<method>( calls against a .d.ts to catch fabricated method names') .option('--types <path>', 'Path to js-sdk .d.ts', 'node_modules/@lark-project/js-sdk/dist/types/index.d.ts') .option('--files <glob>', 'Explicit file list (comma-separated). Default: `git diff` changed → fallback src/features/**/*.{ts,tsx}') .option('--format <fmt>', 'Output format: text | json', 'text') .action((opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'audit-jssdk', options: opts }), ]); }); // ================================================================= // lpm ai validate — 校验 point.config.local.json 是否符合 schema // // weak AI 反复在 prop_type 命名上凭空(multiSelect camelCase / // workItemTypeWithField 不在 enum)+ 漏 outputs / 漏 multi_select 的 // options。让用户当 QA 是错位——schema 是权威,机械跑一次就能定位。 // ================================================================= ai .command('validate') .description('Validate point.config.local.json against the plugin schema') .argument('[file]', 'Path to point.config.local.json (default: ./point.config.local.json)') .option('--format <fmt>', 'Output format: text | json', 'text') .action((file, opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'validate', file, options: opts }), ]); }); // ================================================================= // lpm ai docs — 检索官方插件开发文档(替代 feishu-project-knowledge MCP) // // 原先 skill 直接调宿主的 MCP tool search_meegle_plugin_docs,硬依赖宿主 // 配了那个 MCP server。这里下沉成 CLI 命令,用插件工程已 login 的 token 调 // ${siteDomain}/mcp_server/knowledge,跨 agent / 跨机器都能用,不依赖宿主 MCP。 // 结果写 .lpm-cache/mcp/<slug>.md,沿用既有缓存 + lpm ai peek 读法。 // // hidden:功能尚未发版、skill 也未改(仍引导直调 MCP)。命令可跑但暂不在 // `lpm ai --help` 列出,避免把人引到本机 CLI 还没有的命令。待发版 + 改 skill // 时去掉 hidden。 // ================================================================= ai .command('docs', { hidden: true }) .description('Search official Feishu Project / Meego plugin dev docs (replaces the feishu-project-knowledge MCP; uses your lpm-login token)') .argument('<query>', 'Search keywords / question, e.g. "如何订阅工作项字段变更"') .option('--format <fmt>', 'Output format: text | json', 'text') .action((query, opts) => { (0, run_script_1.default)(path_1.default.join(__dirname, '../dispatcher'), [ '--command', types_1.ECommandName.ai, '--payload', JSON.stringify({ action: 'docs', query, options: opts }), ]); }); // ================================================================= // 顶层 help:手写 usage example 让 AI(尤其弱模型)能 pattern match // ================================================================= ai.on('--help', () => { console.log(` Examples (mostly for AI skill authors / debugging): # 查 SDK 类型定义(.d.ts 自动按 class/type/interface 名定位 + 追引用链) lpm ai peek node_modules/@lark-project/js-sdk/dist/types/index.d.ts Navigation # 查点位 schema 完整规则(JSON 自动按 key 定位 + 追 $ref) lpm ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint # 列所有点位类型(--index 取轻量目录,用于选型) lpm ai peek .lpm-cache/schema/point-schema.json --index # 只看 description(过滤校验噪音) lpm ai peek .lpm-cache/schema/point-schema.json LiteAppComponentPoint --descriptions-only # 查 MCP 缓存文档某一章节 lpm ai peek .lpm-cache/mcp/lite-app-api.md "订阅属性" # 位置切片(兜底,无结构文件) lpm ai peek build.log --tail 50 lpm ai peek build.log --match ERROR --before 2 --after 10 # Checkpoint 事件日志(workflow 每步一行) lpm ai checkpoint append '{"phase":1,"step":"1.1","status":"success"}' lpm ai checkpoint status lpm ai checkpoint get --phase 2 # 审计 JSSDK 调用(method 是不是都在 types 里;替代 bash+grep+awk) lpm ai audit-jssdk lpm ai audit-jssdk --format json lpm ai audit-jssdk --files src/features/foo/index.tsx,src/features/bar/index.tsx # 校验 point.config.local.json 是否符合 schema(prop_type enum / required / x-cross-unique) lpm ai validate lpm ai validate --format json lpm ai validate path/to/point.config.local.json # 配置文件 TOC(config-like JSON 的 summary 视图,给 AI 做全局决策用) lpm ai peek .lpm-cache/config/remote.json --summary # 从 remote 一步建 draft(cp + 自动命名 + 目标校验) DRAFT=$(lpm ai init-draft) # 磁盘层 JSON patch(patch 只认 draft-*.json;改动停在点位级别) lpm ai patch-json $DRAFT --add-path 'data[point_type=button].point_config' --add-value @/tmp/new-point.json lpm ai patch-json $DRAFT --set-path 'data[point_type=button].point_config[key=foo]' --set-value @/tmp/updated-point.json lpm ai patch-json $DRAFT --merge-path 'data[point_type=board].point_config[key=foo]' --merge-value '{"name":"新名字"}' lpm ai patch-json $DRAFT --delete 'data[point_type=button].point_config[key=old]' `); }); } exports.addAiCommand = addAiCommand;