UNPKG

@lark-project/cli

Version:

飞书项目插件开发工具

248 lines (175 loc) 18.2 kB
# Proposal:插件身份全局化 + 工作态 cache 的 local→global 解析(config-only 退休) > **状态:已搁置(2026-05-23)。** 评估后认为"纯后端场景的插件配置管理"会引入两套并存命令、复杂度过高,**决定暂不做**。本轮试做的 CLI 代码(identity-store / resolve-plugin-context / get-app-certificate / import-secret / workspace 全局函数 / `login --plugin`)已全部 revert;本文与配套记忆 `lpm_global_identity_feature.md` **留作设计存档**,若将来重提可在此基础上继续。`config-only` 维持现状(仍是后端仓改配置的现行机制)。 > > 以下为原提案内容(描述的是当时设想"将来要建"的能力,**未实现**)。 > > **范围**:这是一块 **CLI feature**(外加 skill / README 重写),不是文档收口。它独立于本轮 skill 收口(#1/#3/#6 已落地,#2/#5 因本提案而重新定位)。 --- ## 一、背景与问题 ### 现状 - `plugin.config.json` 同时装了两类东西: - **身份**:`siteDomain` / `pluginId` / `pluginSecret`(加密)/ `app_type` —— 是 per-(domain, pluginId) 的凭据,与"在哪个仓"无关; - **仓库接线**`resources: [{id, entry}]` —— 点位 → 代码文件路径,只在有前端代码的仓里才有意义。 - 改远端数据的命令(`local-config set` / `update` / `publish` / `perm apply`)需要 `pluginSecret`,而 secret 只在本地 `plugin.config.json` 里 → 它们**只能在含 `plugin.config.json` 的目录里跑**- `~/.lpm/plugins.json``src/v2/utils/plugin-registry.ts`)是个**只读**全局注册表:让 `lpm perm list` 能 token-only 在任意目录定位插件,**刻意不存 secret**,所以改类命令进不来。 - `.lpm-cache/``WORKSPACE_DIR`)与 `point.config.local.json``assertPluginRoot` 硬绑在"含 `plugin.config.json` 的目录",否则 throw。 - `config-only``lpm init --config-only`)为"后端代码在独立仓、没有 `plugin.config.json`"的开发者建一个 `meegle-plugin-config/` 最小工作区(只写身份 + 空 `resources` + `backendOnly:true`),让改类命令能在后端仓跑。 ### 问题 1. **README 漏掉了"纯后端迭代要改点位配置"这个 case**。README 对后端仓只想到了"读"(`perm list` token-only)和"写代码"(`meegle-plugin-backend` skill,不依赖工程目录),却没考虑"后端仓要**推/改点位配置**"(典型:联调收口把真 webhook url/token 指回去)。它默认了"点位配置都在前端仓改",因为前端仓天然有 `plugin.config.json`2. **`plugin.config.json` 落后端仓是错位的**。一个业务后端服务可能给**多个插件**收 webhook,凭什么在它仓里塞某一个插件的身份文件。身份是 per-(domain, pluginId) 的,不该是某个仓的私有物。 3. **`config-only` 是个 workaround**:它本质是"为了喂饱 `assertPluginRoot`,在后端仓造一个本地身份锚"。 4. **断指针**`meegle-plugin/SKILL.md` 现状让用户"按 `meegle-plugin-backend` skill 的 setup 建 config-only",但 backend skill 里根本没有这段 setup。 --- ## 二、不变量(本提案不动) 1. **点位开发 + 点位配置(改数据)一律走 workflow**:plan → 用户确认 → 全量提交 → 删除 gate。全局身份只解决"找得到、认得了",**不等于能裸跑 `local-config set`**2. **改远端数据要持 secret + 过 gate**:不引入"纯 token-only、连本地 secret 都不要"的改数据路径。 3. **远端配置是唯一真相**:本地 `.lpm-cache/``point.config.local.json` 都是可丢弃的工作副本,改前 `local-config get --remote` 刷新基线。 --- ## 三、提案核心 ### 3.1 拆分:身份归全局,仓库接线留本地 | 内容 | 归属 | |---|---| | 身份:`siteDomain` / `pluginId` / `pluginSecret` / `app_type` | **全局** `~/.lpm`(按 origin + pluginId 累积,secret 加密 + `0600`,与 `~/.lpm/auth.json` 同信任边界) | | 仓库接线:`resources: [{id, entry}]` | **本地** `plugin.config.json`(只前端代码仓需要,`start` / `build` / `update` 写模板时用) | 改类命令(`local-config set/get` / `perm` / `publish` / `update`)只吃"身份 + 远端配置",**不吃 `resources`**`config-only``resources` 是空的、照样能 `local-config set` —— 反证身份足矣)。 ### 3.2 解析顺序:local → global,同一把 (origin, pluginId) key **身份**``` 1. 当前目录有 plugin.config.json → 用它(最具体;前端 / 老流程不变) 2. 否则 → --plugin <id> --site-domain <url> 从全局 ~/.lpm 取身份 ``` **工作态 cache**`.lpm-cache/` + `point.config.local.json`)跟身份用**同一套**解析、同一把 key: | 场景 | cache 落点 | |---|---| | 当前目录是插件工程(有 `plugin.config.json`) | **仓内** `.lpm-cache/` + 仓根 `point.config.local.json`(今天的行为,前端一字不变) | | 后端仓 / 任意目录(靠全局身份) | **全局** `~/.lpm/cache/<origin>/<pluginId>/``remote.json` / `draft-*` / `schema` / `last-set` / `mcp` / `state.json` / `events.jsonl` / `point.config.local.json`),后端仓零污染 | ### 3.3 三场景落点 - **前端 / 全量迭代**(人在插件工程仓):临时文件全落仓内 `.lpm-cache/` + 仓根 `point.config.local.json`**不变。** - **前后端**(同插件,前端仓 + 后端仓):各按"你在哪个目录跑"决定 —— 前端仓内做配置/代码 → 落仓内;后端仓做(如联调收口)→ 落全局 cache。同一插件可能同时有两份 cache,**不冲突**(远端是真相、各 `get --remote` 刷新;`state.json` 各记各的本就是两个独立 workflow 会话)。 - **纯后端**(后端仓,无 `plugin.config.json`):身份取全局,临时文件落 `~/.lpm/cache/<origin>/<pluginId>/`,后端仓一个文件都不沾。 ### 3.4 config-only 退休 身份全局化后,"在后端仓造本地身份锚"这件事不需要了。后端会话要改点位配置(典型:联调收口推真 webhook url/token)→ **全局身份定位 + 走 feature 的 config-apply A0–A3 gate**(workflow 那套),而不是 `lpm init --config-only``meegle-plugin-config/`### 3.5 防误写:靠 workflow SOP + 现有 CLI gate,不裸放 `plugin-registry.ts` 当前只敢做读,因为"写需要 active 选择 / 防误写护栏"。本提案把那条"目录绑定"的隐式护栏,换成: - 改类命令走全局上下文时**强制** `--plugin <id>` + `--site-domain <url>`(不允许猜 active 插件); - 改类落地前**回显确认**:"即将对 `<pluginName>`(`<id>`) @ `<domain>` 执行 set / publish,确认?";**`publish` 不可逆,硬卡**- 现有 CLI gate **全保留、不动**`local-config diff` 预览、删除点位 `exit 2`、全量 draft 校验(`preflightLocalConfig`); - (可选)`lpm use <id>` 显式选当前插件并随时回显,免得每条都带 flag。 ### 3.6 `~/.lpm` 数据布局总览 ``` ~/.lpm/ ├── auth.json (已有:user token,按 origin) ├── plugins.json (已有:只读注册表 → 扩展为存身份) │ { "<origin>": [ { pluginId, name?, app_type?, │ secret? ← 新增:加密;只有改类用得到 } ] } └── cache/ (新增:无本地工程时的工作态落点) └── <origin>/<pluginId>/ ├── point.config.local.json ├── config/{remote,last-set,draft-*}.json ├── schema/point-schema.json ├── mcp/*.md └── state.json / events.jsonl ``` - secret 与只读注册表共用 `plugins.json` 还是拆独立加密文件,实现时定;不变的是 **secret 加密 + 文件 `0600`**,与 `auth.json` 同姿态。 - `cache/<origin>/<pluginId>/` 的子结构 = 今天 `.lpm-cache/` 的原样照搬,只是 root 换成全局 per-plugin 目录。 ### 3.7 命令对比:纯后端改点位配置(今天 vs 提案) **今天**(要先造本地身份锚,临时文件落 `meegle-plugin-config/`): ``` lpm init --config-only --plugin MII_abc --site-domain meego.feishu.cn cd meegle-plugin-config # 这里才有 plugin.config.json + .lpm-cache lpm local-config get --remote # 改 point.config.local.json lpm local-config set && lpm update ``` **提案**(身份全局、任意目录、临时文件落 `~/.lpm/cache/`、后端仓零污染): ``` # 一次性把 secret 收编进全局(或 create/init 时已自动登记) lpm login --plugin MII_abc --site-domain meego.feishu.cn # 之后在后端仓任意目录直接改(仍走 diff/删除 gate + 改前确认) lpm local-config get --plugin MII_abc --site-domain meego.feishu.cn --remote # 改 ~/.lpm/cache/meego.feishu.cn/MII_abc/point.config.local.json lpm local-config set --plugin MII_abc --site-domain meego.feishu.cn lpm update --plugin MII_abc --site-domain meego.feishu.cn ``` --- ## 四、CLI 改动面 1. **全局身份 store**:扩展 `~/.lpm``plugins.json` 旁或同文件加密分区),按 (origin, pluginId) 存 `pluginSecret`(加密 + `0600`)+ `app_type` + `name`2. **secret 写入 / 导入入口**`lpm create` / `init` 成功时顺手登记;新增"导入已有插件 secret"的入口(如 `lpm login --plugin <id> --site-domain <url>`),让后端开发者不建仓也能拿到改权。 3. **身份 resolver**:local `plugin.config.json` → global,统一出口供所有命令取身份。 4. **cache root resolver**`workspacePaths` 的 root 从"projectDir" 改成"local 工程根 → 否则 `~/.lpm/cache/<origin>/<pluginId>/`"。 5. **放宽 `assertPluginRoot`**:从"必须在插件工程根" → "插件工程根 **或** 全局身份(带 --plugin)时落全局 per-plugin cache"。 6. **改类命令 flag 打通**`local-config set/get` / `perm` / `update` / `publish` / `update-description` 支持 `--plugin` + `--site-domain` 走全局身份 + 全局 cache。 7. **防误写确认**:全局上下文改类的回显确认 + `publish` 硬卡(见 3.5)。 8. **`lpm check context` 演进**:config-only 退休后,不再有 `BACKEND_HANDLE_CWD`("cwd 是 config-only 工作区")这个返回值。check context 收敛成 `PLUGIN_PROJECT`(有本地 `plugin.config.json`)/ `NONE`(靠全局身份 + `--plugin`)两态;`backendOnly` flag 与 `meegle-plugin-config` 固定目录名一并废弃。skill 路由表对应简化。 --- ## 五、skill / README 影响 - **`external-backend` context(#2)**:随 config-only 退休而重定 —— 不再是"建 config-only 工作区",而是"全局身份 + 走 feature config-apply gate"。`BACKEND_HANDLE_CWD` 检测、SKILL.md:56 的断指针一并清理。 - **`meegle-plugin-backend` skill**:补一段"后端会话怎么改点位配置"(全局身份定位 → `get --remote` → 改 → 走 gate set),替掉现在那段不存在的 setup 承诺。 - **交接包**:feature 产的交接包带上 `pluginId` / `siteDomain` + 要改哪个点位的哪个字段(尤其联调收口的 webhook 端点),**不带 secret**(secret 由接手人按拿插件的正常渠道从 Meego 后台取)。 - **`skills/README.md`****等本提案落地后**再把 §1(后端那节)/ §2(路由)按新模型重写;在那之前 README 仍描述当下(config-only 在用),只在 §5 留指针。 --- ## 六、迁移 - **存量 `plugin.config.json` 照常工作**:local 优先,老前端工程零改动。 - **存量 config-only 工作区**:过渡期继续可用;提供把其 secret 收编进全局 store 的路径后,可删目录。 - **secret 上全局**:首次 `create` / `init` 自动登记;存量插件靠导入入口(`lpm login --plugin`)补齐。 --- ## 七、待定 / 风险 1. **并发**:全局 per-plugin `state.json` 是机器级共享,同一插件两个终端同时改会撞 checkpoint。但同插件远端配置本就是单一真相、并发改本有竞态 → 给 session 隔离或简单 lock(或接受 last-writer)。 2. **`point.config.local.json` 版本化**:现按"工作副本"对待 → 落全局 cache。若有团队想把它当点位配置的版本化源 commit 进仓,前端仓内那条路径仍保留(org 选择)。 3. **secret blast radius**:全局 store 让"持有这台机器账户"= 能改所有已登记插件。与 `~/.lpm/auth.json`(user token 已全局)同边界,可接受;加密 + `0600` + 改前确认兜底。 --- ## 八、非目标 - **不**放松"点位开发 + 配置走 workflow"。 - **不**引入"无本地 secret 的纯 token-only 改数据"。 - **不**在本提案里重写 README §1/§2 正文(落地后再做;现在只加指针)。 --- ## 九、后端场景命令集 + 目录依赖分析(2026-05-23 调研结论) ### 9.1 设计决策:后端场景另起一套命令,不复用本地命令 本地命令(前端场景)需要靠"在工程目录里"来识别上下文,**原样不动**。后端场景(无前端工程、靠全局身份)走**独立的命令通道**(全局身份 + 全局 cache + 绕开 dir 墙),不给现有命令叠 `--plugin` 双重身份。 ### 9.2 当前"必须在插件工程目录"的命令(`isPluginProject()` 墙后,共 12 个) **A. 真需要本地工程(动本地代码/文件)—— 不进后端那套,原样保留** | 命令 | 真实依赖 | 离不开 dir 的原因 | |---|---|---| | `start` | plugin.config.json + 前端代码 | 起 dev-server 跑本地 React | | `build` | 身份 + 本地代码 | 构建本地前端产物 | | `config get/set` | `getLocalPluginConfig` | 读写**本地 plugin.config.json 字段本身** | | `update`(整条) | 身份 + `getLocalPointConfig` + 写 `src/features` | 拉配置**并写前端模板代码**(脚手架半边离不开 dir) | | `check npm` | repo npm 源 | 检查本地仓 npm 源 | | `workspace clean` | 本地 `.lpm-cache` | 清本地缓存目录 | **B. 只动远端插件(身份 + 远端数据)—— 后端场景候选** | 命令 | 真实依赖 | 后端场景真需要 dir 吗 | 注意点 | |---|---|---|---| | `local-config get/set/diff` | 身份 + cache | **否** | 卡点=`getLocalPluginConfig`+`assertPluginRoot`,换全局身份 + `ensureWorkspaceAtRoot` 即解 | | `perm list/apply` | 身份(token-only) | **否(已就绪)** | 已能在工程外跑 | | `schema` | **只要可写 cache**`getPointSchema()` 无参,连身份都不要) | **否** | 唯一卡点=`ensureWorkspace→assertPluginRoot`;改 `ensureWorkspaceAtRoot` 即解 | | 拆出的"纯推送" | 身份 + cache 里 point.config.local.json | **否** | 要 dir 的是 `update` 的模板半边;后端只取 `applyLocalPointConfig` 推送半边 | | `publish` | 身份 + 远端已上传 package | **否(不读本地代码)** | ⚠️ 读了本地 `resources?.length` 做 artifact-version 安全判断;全局模式无本地 resources,这处要改(纯后端 resources=0 正好;有前端的靠显式 `--artifact-version` 或查远端) | ### 9.3 结论 后端场景核心命令 = **`local-config get/set/diff`(配置)+ 纯推送 + `perm list/apply`(scope)+ `schema`(辅助)+ 可选 `publish`****数据上没有一个真需要插件目录**——今天的 dir 绑定全是三处实现:①`getLocalPluginConfig` 读 cwd ②`ensureWorkspace`/`assertPluginRoot` 要 plugin.config.json ③dispatcher 的 `isPluginProject()` 墙。换全局身份 + 全局 cache + 后端通道绕开墙即解。 两个真实例外要单独处理:**`update` 整条离不开 dir**(写 src/features)→ 后端只拆"纯推送";**`publish` 的 resources 安全检查**在全局模式要改。 --- ## 十、AI(skill)怎么用——靠单一探测分流,不靠 AI 猜 **担心点**:本地一套命令、后端一套命令并存,AI 会不会用乱?**答案:不会,因为分流是确定性的单点探测,且后端那套由 skill SOP 规定序列,AI 不自由选。** ### 10.1 唯一的分流点:`lpm check context` skill 流程第一步永远先跑 `lpm check context`,按它输出的单 token 分流: | token | 场景 | 用哪套 | |---|---|---| | `PLUGIN_PROJECT` | cwd 是插件工程 | **本地命令**(A 组,不带 flag),走前端/全量 SOP | | `NONE` | 非工程目录 | **后端命令**(B 组),从交接包/用户拿 `pluginId`+`siteDomain`,显式带 `--plugin/--site-domain` | AI 只做这一次判断,之后照对应 SOP 走,不在两套之间反复横跳。 ### 10.2 防乱的三条约束 1. **命名上两套清楚分开**(具体形态待设计:独立 namespace 或后端命令带显式 `--plugin` 必填)——让 AI 一眼看出"这是后端那套"。 2. **后端那套由 `meegle-plugin-backend` skill 的 SOP 规定命令序列**(get→编辑→diff→set→push→联调),AI 跟着走,不自由拼。 3. **确认归 skill SOP**:CLI 只留 `--yes` 非交互安全闩,人面前的确认是 skill 的"不可逆动作前确认"。AI 不卡在交互 prompt。 ### 10.3 AI 在后端场景的典型序列(示意) ``` lpm check context → NONE(后端场景) # skill 从交接包拿到 pluginId + siteDomain lpm login --plugin <id> --site-domain <url> # 首次:换 secret 进全局(若没收编过) lpm <后端> local-config get --plugin <id> --site-domain <url> --remote # 拉基线 # 编辑 ~/.lpm/cache/<host>/<id>/point.config.local.json(填真 webhook url/token) lpm <后端> local-config diff --plugin <id> --site-domain <url> # 预览 + 删除 gate lpm <后端> local-config set --plugin <id> --site-domain <url> --from <draft> lpm <后端> <纯推送> --plugin <id> --site-domain <url> --yes # 推远端(不写前端代码) ``` > `<后端>` / `<纯推送>` 的确切命令名待 §9.1 的形态设计敲定。关键是:**AI 拿到的是一条 skill 规定的线性序列,不是"从两套命令里自己挑"**——这才是不乱的根本。