# 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 规定的线性序列，不是"从两套命令里自己挑"**——这才是不乱的根本。
