Skip to content

feat(plugins): 插件注入的工具链框架与 macOS 只读环境设置页 - #1581

Open
FxRayHughes wants to merge 2 commits into
1lck:previewfrom
FxRayHughes:feat/plugin-toolchain-sources
Open

FxRayHughes wants to merge 2 commits into
1lck:previewfrom
FxRayHughes:feat/plugin-toolchain-sources

Conversation

@FxRayHughes

@FxRayHughes FxRayHughes commented Oct 29, 2026 •

Copy link
Copy Markdown

关联

原则

宿主不内置任何工具:Java、Maven、Node 等工具由插件定义,mise、nvm、SDKMAN 这类版本管理器由插件作为提供方接入。Core 只负责校验和按四层解析,不解释任何版本管理器的私有格式。

改动

插件契约(shared/contracts/plugins/v1/)

  • 新增能力 toolDefinitions: 1(工具定义)和 toolchainProviders: 1(提供方声明,需要权限 toolchains.provide)。
  • executableResolution 增加 v2,只用于工具定义的候选,不改变现有 Build 的候选集合。
  • 同步更新 schema、README 和 rust-core-api.md。新增 toolchains.md,以及两个 fixture(tool-definition.plugin.json、toolchain-provider.plugin.json)。
  • proposals/toolchains/ 存放完整方案草案,包括查询协议、local.json、清单贡献和解析结果的 schema 与用例。这些只用于评审,不是可以安装的清单。
  • 工具 ID 规则:
    • 不带前缀的短 ID(java、node 等)只能由宿主验证过的官方插件定义;
    • 社区插件使用 <pluginID>:<tool>;
    • 重复的 ID 拒绝后注册的一方。

Rust Core(rust/lithe-core/src/toolchain/)

工具链是一个独立的领域。插件清单模块只在准入时调用它做校验。

文件 内容
definitions.rs 校验工具定义和提供方声明,构建注册表:核对来源身份、保护短 ID、拒绝重复
query.rs 校验提供方的答复是否匹配其身份和代次、是否只涉及声明过的工具,并核对宿主提供的文件证据
pins.rs 读取 .lithe/toolchains/local.json 的 pins,找出最近祖先目录上的固定值
resolution.rs 按固定 → 项目文件 → 全局 → 自动四层解析:固定值无效时直接失败,不往下回落;项目要求的版本没装时保留「未满足」状态;冲突也保留下来
paths.rs 规范化工作区相对路径,处理 Windows 路径大小写
  • 新增六个命令:toolchains.registry、toolchains.decodeReply、toolchains.resolvePins、toolchains.resolve、toolchains.checkVersion、toolchains.discoveryPlan。它们只处理宿主传入的快照,不启动进程、不扫描目录,也不写文件。
  • backend.rs:backend 2 的插件可以由已准入的工具链查询触发启动。
  • declarative.rs:清单中的 toolDefinitions(最多 32 个)和 toolchainProviders(最多 8 个)必须非空,能力声明要精确对应。

macOS

  • 在「设置 › 构建」下新增「运行环境」和「环境来源」两页,排在「项目」「运行配置」之前,设置搜索词同步更新。旧的项目 JDK/Maven 页面保留,等实际的使用方迁移过来后再删除。
  • 运行环境:
    • 左侧是项目的目录树,右侧是当前目录下的工具。
    • 每个工具占一行,收起时显示当前生效的来源;展开后显示固定、项目文件、全局、自动四张来源卡片。第一个命中的卡片带边框,没有值的卡片变暗显示,不会被隐藏。
    • 卡片按窗口宽度在 4、2、1 列之间切换,同一行的卡片高度一致。路径只占一行、从前面截断,鼠标悬停显示完整路径。
  • 环境来源:
    • 登录 Shell 部分显示 Shell 路径、读取状态、变量个数(只显示数量,不显示值)、读取耗时,以及 PATH 里每个目录来自登录 Shell 还是 Lithe 的启动环境,并提供「重新读取」。
    • 版本管理器部分显示插件声明的版本管理器在当前项目里找到了哪些版本文件。
  • 页面上的所有内容都来自已启用插件的声明和磁盘上的真实文件。添加、切换、保存、查询这些还没实现的入口直接不显示,也不放禁用状态的占位按钮。
  • MacLoginShellEnvironment 新增只读的 inspect,供来源页使用。重新读取只更新缓存和页面显示,不会改变当前进程的 PATH。
  • 插件启用、停用后,两页会重新加载数据。
  • 新增 Debug 预览入口 --toolchain-settings-demo 和 --toolchain-sources-demo。预览里只有插件清单是样例,工作区文件、固定值和登录 Shell 都是真实读取的。可以用以下参数指定初始状态:
    • --toolchain-demo-directory <目录>
    • --toolchain-demo-expand <工具,…>
    • --toolchain-demo-size <宽>x<高>
    • --toolchain-demo-workspace <路径>

文档与其他

  • 新增 Agent Note(提议中)2026-10-31-plugin-injected-toolchains.md;在设置层级 Note 里补充这两页属于哪个分组。
  • 功能矩阵新增 plugin-injected-toolchains,macOS 和 Windows 都标为 partial / pending。
  • 补充中英文词条。

尚未完成

以下内容这次都没做,页面上也不显示:

  • 提供方查询进程(查询 mise、nvm、uv 实际装了哪些版本),以及文件监听
  • 全局默认值、写入固定值、手动添加安装
  • 把现有的 Java、Maven、Node 选择链迁移到这里
  • Windows 原生接入
  • 合并后需要在 lithe-plugin-docs 同步契约目录

验证

  • Rust Core:verify-rust-core-comments.sh、verify-rust-core.sh、cargo fmt --check 都通过。
  • macOS 单测:以下测试全部通过,test-stability-macos 共 71 个测试,0 个失败。
    • ToolchainSettingsLoaderTests、ToolchainSettingsFeatureTests、ToolchainSettingsLayoutTests
    • MacLoginShellEnvironmentTests(13 个)
    • SettingsSearchVocabulary、SettingsSidebar
  • 其中布局测试会:
    • 用 900/1180/1400/900/1180 几种宽度反复缩放窗口,并拖动真实的分隔条;
    • 把来源页按 500/666/946/500 缩放,检查控件边界、卡片不重叠、选择状态和快照不变。
  • 已有测试 coldStartReadsTheShellAndWarmStartUsesTheCachedPath 用时约 1.7 秒,超过 1 秒的提醒线,但低于 15 秒上限。这个测试本 PR 没有改动。
  • 静态检查:
    • verify-runtime-bundle-immutability.sh、verify-service-boundaries.sh、verify-shared-contracts.sh
    • verify-platform-feature-matrix.sh、verify-agent-notes.sh、verify-test-stability.sh
    • i18n 三项检查
  • 实际运行:在 Debug 预览里分别用 900、1180、1500 宽查看了两页,目录切换、展开和窄宽度换行都正常。
  • 未执行:Windows 原生构建,以及 macOS 全量测试。

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator

技术方案评审:toolchainSources 的边界

先说明:PR 要解决的问题(Dock 启动看不到 mise 装的运行时、项目版本文件不生效)是真实的。安全边界(path: 落在工作区内不选版本)、按 inputs 缓存、只读、免重启、功能矩阵这些周边做得扎实,下面的意见不针对它们,针对的是方案本身的架构边界。

验收标准

新增一个版本管理器(例如 SDKMAN、nvm、asdf)只改插件,不改 Rust Core;宿主适配层只保留通用机制。这才是“声明式”的意义。

现方案没达到这个标准的地方

1. Core 在重新实现版本管理器的语义。
lts 别名、latest、prefix:、path:、全局文件的覆盖顺序都是 mise 自己的规则,PR 把它们写进了 rust/lithe-core/src/plugins/toolchain_sources.rs。“已知限制”那一节(conf.d、mise.<env>.toml、模板、system、ref:)其实就是与 mise 的分歧清单。develop-lithe 第二节写明“上游拥有的事实仍由上游负责,Core 里不再维护第二份真源”。

2. “新增版本管理器只需要一份清单”实际不成立。
versionFiles[].format 是封闭枚举 miseToml | toolVersions,Core 里已经有带 mise 名字的解析器。nvm(.nvmrc 是单工具纯文本,目录名带 v 前缀)、pyenv(.python-version)、SDKMAN(.sdkmanrc 是 properties 格式)都需要新增格式,也就是改 Core。

3. 宿主侧的工具知识是写死的,且注入面过宽。

  • PluginToolchainSources.swift 里 tool.tool == "java" 是魔法字符串,其余工具按文件名 executables(named:) 匹配。maven 能进 Maven 选择链,只是因为可执行文件恰好叫 mvn,宿主里没有 maven → mvn 的映射。
  • 任何来源都可以为任意同名命令注入候选(比如声明 {tool: "git", executables: ["git"]}),而版本文件点名的那个版本排在 PATH 前面。权限 toolchains.read 的描述是“查找运行时”,实际效果是“影响宿主启动哪个程序”。

4. 优先级和作用域。

  • JDK、Maven、其他命令是三条顺序各不相同的链,硬编码在 ProjectRuntimeService。
  • 没有“项目级”和“全局”的统一概念,只有各条链里各自的位置。
  • 版本文件要求的版本没装时静默回落,而这正是 PR 描述里“选错版本不易察觉”的那个问题。

建议的方案

三种贡献,宿主不认识任何工具

贡献 回答什么 形式 谁提供
工具定义 工具是什么:怎么识别、探测版本、导出环境、启动 清单里的静态声明 任何插件
提供方 装了什么、项目要什么、全局要什么 插件通过 API 返回事实 版本管理器插件
使用方 谁需要这个工具 已有(通过 tool ID 引用) 语言服务器、构建系统、运行配置

1. 工具定义(示意)

"toolDefinitions": [
  { "id": "java", "displayName": "JDK",
    "executables": ["bin/java"], "commands": ["java"],
    "version": { "arguments": ["-version"], "pattern": "(?:1\\.)?(\\d+(?:\\.\\d+)*)" },
    "exports": [{"name": "JAVA_HOME", "value": "${home}"},
                {"name": "LITHE_JAVA_PATH", "value": "${executable}"}],
    "pathPrepend": ["${home}/bin"],
    "launch": ["${executable}", "${runtimeArguments}", "-jar", "${entry}", "${arguments}"] },
  { "id": "maven", "displayName": "Maven",
    "executables": ["bin/mvn"], "commands": ["mvn"],
    "projectExecutables": ["mvnw"],
    "version": { "arguments": ["-v"] },
    "pathPrepend": ["${home}/bin"] }
]

这张“工具 ID 到宿主用途”的映射表就是适配层里本来要写死的东西,改成插件贡献。它取代的是:java 当 JDK home 的特判、maven → mvn 的文件名巧合、契约里写死的版本探测参数和 LITHE_<TOOL>_PATH 导出规则、launch.kind: runtime 里 tool 只能是 java 或 node 的限制,以及选择链里专门给 mvnw、gradlew 写的分支。

  • ID 归属。 插件自己的工具用 插件id:工具;不带前缀的短名(java、node、go、maven)是保留名,只有宿主内置或官方签名插件能定义。同一 ID 被重复定义时拒绝后来者,不允许覆盖。
  • 宿主内置的那几个用同一种格式的数据文件表达,放在 shared/ 下,各平台随应用打包,作为第一个贡献者注册。适配层里不出现任何工具名,以后各插件可以逐个接管自己的定义。
  • 安全校验由 Core 统一做,与具体工具无关。 环境变量值只能是 ${home}、${executable} 模板;禁止 PATH、DYLD_*、LD_*;pathPrepend 只能在 ${home} 之内;探测参数必须是 -x/--xx 形状。安装审查里列出每个定义会运行的程序和参数。

2. 提供方 API

清单只声明“我能回答哪些工具”,不描述目录结构和文件格式:

"toolchainProviders": [{ "id": "mise", "displayName": "mise",
                         "tools": ["java", "node", "maven", "pnpm", "go"] }],
"permissions": ["toolchains.provide"]

宿主向插件发 toolchains/query({operationID, workspace, tools},workspace 为空表示只问全局),插件返回:

{ "installations": [
    {"tool": "java", "version": "21.0.2", "home": "/…/installs/java/21.0.2"} ],
  "statements": [
    {"tool": "java", "scope": "project", "source": "/proj/mise.toml",
     "request": "21", "installation": "/…/java/21.0.2"},
    {"tool": "java", "scope": "global", "source": "~/.config/mise/config.toml",
     "request": "17", "installation": "/…/java/17.0.9"},
    {"tool": "node", "scope": "project", "source": "/proj/.tool-versions",
     "request": "lts", "installation": null} ],
  "watch": ["/proj/mise.toml", "/proj/.tool-versions", "…/installs/java"] }
  • 插件只能回答自己在 tools 里声明过的、且有已启用插件定义的 ID,其余丢弃。这样“任意同名文件被注入”的口子就堵上了。
  • 版本语义(哪个文件优先、lts 对应哪个目录)归插件,宿主不解释 request。
  • installation: null 表示项目要求了但没装。
  • watch 沿用 PR 里“缓存到输入变化”的思路:宿主只做 stat 指纹比较,没变就不再调用插件。
  • 沿用现有的 operationID、取消、超时和过期结果规则。
  • 返回的路径由 Core 统一校验:绝对路径、存在、可执行、不落在工作区内(保留 PR 现有的这条安全规则)、数量有上限。
  • .tool-versions、mise.toml 这类通用解析函数放进 lithe-plugin-docs 的 Node/Java SDK,不放进宿主。

3. 统一的选择规则

所有工具共用一条规则,按层从上到下取第一个有值的:

层 内容 范围
1 固定 用户在 Lithe 里为这个项目选的 项目
2 项目要求 提供方上报的 scope: project,以及 mvnw、gradlew 等项目自带的包装脚本 项目
3 全局默认 Lithe 里的全局默认 → 提供方上报的 scope: global → JAVA_HOME/PATH 全局
4 自动检测 满足最低版本的已安装最新版(现有的 Core 规则) —
  • 没装任何插件时,第 2 层为空,行为与现在完全一致。
  • Lithe 里固定的值失效:报错,不回落(与调试适配器那篇 Note 的做法一致)。
  • 项目要求了但没装:回落到下一层继续工作,但状态标成“未满足”,界面明确提示。
  • 界面上每个工具一行,显示“JDK 21 · 来自项目要求(mise.toml)”,展开后列出每层的值,被盖住的划掉。

对这个 PR 的建议

可以保留: 工作区内路径拒绝、按输入缓存、只读原则、i18n 词条、功能矩阵条目、RuntimeChoiceSource 的“来自 %@”展示。

需要重做: Core 里的 toolchain_sources.rs(管理器专属解析与选择语义)、清单 schema 里的目录与格式描述、ProjectRuntimeService 里三条各自的链。

建议拆分:

  1. 契约(工具定义、提供方、校验)加 Agent Note,先定契约再写代码;
  2. 适配层的查询客户端(缓存、取消、过期);
  3. 统一选择规则加运行时设置界面;
  4. mise 插件加 SDK 帮手,再写一个 SDKMAN 插件当验收,并把 MacRuntimeDiscovery 里写死的 SDKMAN 位置迁出去。

契约在 shared/contracts/plugins/v1/,合并后要在 lithe-plugin-docs 运行 node scripts/contract.mjs sync 同步。

待讨论

  1. API 方向。 上面是“宿主请求、插件返回事实”,不是“插件主动推送”。推送需要插件进程为每个打开的项目常驻;请求式有明确的超时和取消。需要时可以在同一协议上加 toolchains/changed 通知。
  2. 规则细节。 “固定 > 项目要求 > 全局默认”,以及“要求没装就回落并警告”,是建议值,欢迎提出不同意见。
  3. 保留名。 只允许官方签名的插件或宿主内置定义不带前缀的短名;若允许社区插件定义,冲突处理会复杂很多。
  4. 自举和免重启。
    • 提供运行时的插件,它自己的后端不能依赖它要发现的运行时,否则循环依赖。后端必须自带运行时或是单文件可执行程序(契约已有 packagedRuntime)。
    • PR 的 canActivateWithoutRestart 只放行“无后端”的插件,这个条件需要放宽。
    • 这两点我们还没有原型验证过。
  5. Core 的一次性改动。 加定义的校验,以及把 checkRuntimeVersion 里 Java 8 的 1.x 特例改成定义里的正则。之后新增工具或管理器都不再改 Core。

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator

这个具体的先后顺序啥的我记得构建工具里面我之前就提过一个方案,按照优先级去维护一个顺序,你可以去看看,具体细节我记不清楚了哎

@FxRayHughes

Copy link
Copy Markdown
Author

感谢这么细的评审。结论先说:同意按「工具定义 / 提供方 / 使用方」重做,本 PR 不再往下推。 我对照代码核对了几处问题,都成立:

  • toolchain_sources.rs 里的 lts、latest、prefix: 和覆盖顺序,等于在 Core 里维护了第二份 mise 真源。「已知限制」那一节列的正是两边的分歧。
  • versionFiles[].format 是封闭枚举,nvm、pyenv、SDKMAN 都要改 Core。
  • 宿主里有 tool == "java" 特判,maven 能进选择链只是因为文件名恰好叫 mvn。任何来源都能给同名命令注入候选,还排在 PATH 前面。toolchains.read 的描述低估了实际影响:它决定的是 Lithe 启动哪个程序。
  • 要求的版本没装时静默回落,这正是 [Bug] macOS 下自动检测不到 mise/SDKMAN 等版本管理器安装的 Maven 和 JDK,必须手动指定路径 #1514 抱怨的「选错版本不易察觉」。

你说的「按优先级维护顺序」,我找到的是你在 #1420 的评审(#1420 (comment)):

  • 单一 executables 入口,数组顺序就是优先级,可选 priority / insertBefore / insertAfter;
  • ExecutableCandidateResolver 区分 skip / resolved / terminalFailure;
  • 明确要复用到 Language Server、Runtime Dependencies 等。

如果是这条,我打算把新方案的选择规则直接建在它上面,不再写第二套分层逻辑。下面是调整方案,请看方向对不对。

1. 选择规则:写成每个工具定义的默认候选数组

四层规则不再写成 ProjectRuntimeService 里的三条链,而是写成工具定义里 executables 的默认顺序,由同一个 resolver 解析。plugins::executables 只需要两处扩展:

  • 新增一种来源 kind: "toolchain",向提供方查询;
  • setting 可以只取项目层或全局层。

示意(Maven;Java 结构相同,把 MAVEN_HOME 换成 JAVA_HOME,并且没有 Wrapper):

{ "id": "maven", "displayName": "Maven", "commands": ["mvn"],
  "version": { "arguments": ["-v"] },
  "executables": [
    { "id": "pinned",         "kind": "setting",     "settingID": "executable", "layer": "project" },
    { "id": "wrapper",        "kind": "projectFile", "path": "mvnw" },
    { "id": "projectRequest", "kind": "toolchain",   "scope": "project" },
    { "id": "default",        "kind": "setting",     "settingID": "executable", "layer": "global" },
    { "id": "globalRequest",  "kind": "toolchain",   "scope": "global" },
    { "id": "home",           "kind": "environment", "variable": "MAVEN_HOME", "path": "bin/mvn" },
    { "id": "path",           "kind": "command",     "command": "mvn" },
    { "id": "detected",       "kind": "toolchain",   "scope": "installed" }
  ] }

各层在 resolver 里的语义:

  • 固定值失效:非空 setting 指向的程序不存在或不可用时返回 terminalFailure,报错,不回落。这是 feat(plugins): 统一可执行文件候选与宿主注入解析 #1420 已有的行为。
  • 项目要求没装:toolchain 候选返回 skip,继续往下找,但带上原因,例如「mise.toml 要求 node 99,未安装」。目前的 skip 不带原因,需要扩展;界面据此标出「未满足」。
  • 自动检测(scope: "installed"):在所有提供方报告的安装里取满足最低版本的最新版。宿主现有的系统发现(java_home、Homebrew)改成第一个内置提供方,SDKMAN 迁到插件。
  • 使用方调整顺序:构建系统等使用方只能在自己的集合里 insertBefore / insertAfter,与 feat(plugins): 统一可执行文件候选与宿主注入解析 #1420 的限制一致。不新增 projectExecutables 这类旁路字段。

这样三条链变成一条。Provider、模型加载和实际执行用同一个结果,#1420 已经保证了这一点。没装版本管理器插件时,所有 toolchain 候选都是 skip,除「自动检测」层外,行为与现在一致。

2. 提供方:mise 的语义交还给 mise

同意「宿主请求,插件返回事实」。mise 插件改成带后端,后端直接调用 mise 自己的机器可读输出,任何一方都不再重写 mise 的规则:

  • mise ls --current --json:每个工具的 requested_version、install_path、source.path、installed,直接映射为 statements;
  • mise ls --installed --json:映射为 installations。

本机验证(mise 2026.9.8):用 env -i HOME=… MISE_OFFLINE=1 模拟从 Dock 启动的环境,一次调用约 20 ms。

  • 父目录的 mise.toml、子目录的 .tool-versions、全局配置都带着来源文件路径返回。
  • 要求了但没装的版本返回 installed: false。
  • conf.d、MISE_ENV、别名由 mise 自己处理,feat(plugins): 插件注入的工具链框架与 macOS 只读环境设置页 #1581「已知限制」那一节整个消失。
  • mise 会拒绝解析未信任、且含 [env] 等内容的配置。插件照实上报「配置未信任」,不替用户执行 mise trust,也不执行 mise install;设置 MISE_OFFLINE=1,不联网。

自举:后端做成单文件原生程序(Rust,按 darwin-arm64 / x64 分包,Store 已支持按架构下载),不依赖它要发现的 node 或 java。mise 本身的位置用 #1420 的候选声明:设置 → ~/.local/bin/mise → Homebrew → PATH。安装审查里列出它会运行的 mise ls --json。

SDKMAN 适合做第二个验收插件:sdk 是 shell 函数,没有可调用的 CLI,插件只能自己读 ~/.sdkman/candidates 和 .sdkmanrc。这正好检验「上游没有现成答案时,不改 Core 也能接入」。.tool-versions / .sdkmanrc 的解析帮手按你的建议放进 lithe-plugin-docs 的 SDK。用 Node / Java SDK 写的版本管理器插件必须用 packagedRuntime 自带运行时,这一点会写进 Note。

3. 对待讨论各点的意见

  1. API 方向:同意请求式。补充一点:项目里新建的版本文件不在上一次的 watch 里,所以提供方可以在声明里列出它关心的文件名(如 mise.toml、.tool-versions)。工作区里出现或删除这些文件时,宿主重新查询。
  2. 规则:同意「固定 > 项目要求 > 全局默认 > 自动检测」,以及「没装就回落并警告」。有两个细节想确认:
    • 同一层里 mvnw 和 mise 要求的 Maven 谁先?建议 Wrapper 先:它是项目自带的,更具体,也和现有 resolve_maven_executable「覆盖 → Wrapper → 机器安装」一致。Wrapper 属于执行仓库代码,仍按现有规则只在用户操作后启动。
    • 两个提供方对同一工具都给出项目要求时,建议不猜:按插件 ID 顺序取第一个,并在界面标出冲突。
  3. 保留名:同意不带前缀的短名只能由宿主内置或官方签名插件定义。需要确认的是回答:社区提供方(如 mise)应当可以回答 java、node 这类保留名工具,否则 mise 插件对 JDK 没有用。安全靠三层保证:Core 校验答案,安装审查列出影响,界面标明「来自 mise」。
  4. 自举和免重启:自举用单文件后端解决。免重启先不放宽:带后端的提供方在第一阶段装完需要重启,feat(plugins): 插件注入的工具链框架与 macOS 只读环境设置页 #1581 里「仅工具链来源免重启」的改动撤回。等查询客户端的撤回、取消做完,再按热激活 Note 的规则逐类放行。
  5. Core 的一次性改动:同意把 checkRuntimeVersion 里 Java 8 的 1.x 特例改成定义里的正则。toolchain 候选来源和答案校验(绝对路径、存在、可执行、不在工作区内、数量上限)也放在这次。

4. 拆分

按你的建议,先契约后代码,拆成 4 个 PR:

  1. 提案:proposed/ 下的 Agent Note,加上 shared/contracts/plugins/v1/ 的契约草案(工具定义、提供方、toolchain 候选、答案校验)和 fixture,不改代码。参照 docs(vcs): 统一 VCS 模型与 Provider 接入提案(#827) #1428 VCS 提案的方式先评审。
  2. Core 与查询客户端:
  3. 统一选择与运行时设置界面:ProjectRuntimeService 的三条链换成按工具定义解析。每个工具一行,展开后列出各层的值,被覆盖的划掉,未满足的给出提示。
  4. 插件:mise 0.2.0(后端调用 mise),SDKMAN 插件验收,并把 MacRuntimeDiscovery 里写死的 SDKMAN 位置迁出去。

你列的可以保留的部分(工作区内路径拒绝、按输入缓存、只读原则、i18n 词条、功能矩阵、「来自 %@」展示)会分别带进第 2、3 个 PR。第 1 个提案 PR 提交后关闭本 PR,分支保留作参考;配套的 Store 收录 PR(Lithe-IDEA/Lithe-store#32)同时关闭,等新契约落地后重新提交。

如果方向没问题,我先提第 1 个提案 PR。

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator
  1. 这个删除逻辑是可以的,让 core 去进行监控,但是监控文件类型还是需要声明式的。确实有道理比如修改 swift 里面的版本链文件项目级别控制的时候确实可能导致项目级别的工具链版本进行变更
  2. 查询模块麻烦进行内置,监听的文件列表只要有一个变化了整个都需要进行重载的
  3. 运行时这些东西要不就移动到插件的那个管理界面即可了吧,在那里进行修改吧,可以看看下图这种模式
image 就是说你可以弄一个界面去进行统一展示环境相关的内容,但是一定是需要插件去注入式的,这个很关键。我上面是一个例子呀,因为注入的东西不是通用的模板,这个插件采取的方式是在插件的设置页面注入的(如果你打算做插件的话,插件自己的配置都是需要在这个界面的——也就是你说的 PR4)

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator

剩下的没啥问题了,你大胆去做吧

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator

@FxRayHughes 这个可能需要催一下进度,因为有一个 PR 里面我要求使用你这个 PR 的版本管理方案来处理了,如果你没有空的话可以跟我说我来接手

@xiaoyumuxi

Copy link
Copy Markdown
Collaborator
image image image 相关设计的 HTML 等内容都已经附进你的 PR 了的了,这些 Java 等内容都不能写死而是让插件去进行注入的

@xiaoyumuxi

xiaoyumuxi commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

改写后的计划:工具全部由插件注入

上面几张截图换成了一个可交互的 HTML 原型(链接和用法在最后),以原型和这条评论为准。你 4 个 PR 的拆分和 #1420 候选数组的思路都保留;主要改动是下面这条原则。另外,#1558 的 Maven 迁移要用这里的工具定义来声明 Maven,所以第 1 个提案 PR 请优先提。

0. 原则:宿主不认识任何工具

  • 宿主里没有任何工具的定义。 原计划第 2 个 PR 里放在 shared/ 下的“内置定义数据文件”去掉,也不做兼容别名。
  • 保留名:同意你的第 3 点。不带前缀的短名(java、node……)只能由官方签名插件定义;社区提供方(mise)可以回答这些工具。
  • 宿主只做框架:
    • 四层解析(resolver);
    • 查询模块;
    • 目录树;
    • 固定文件 .lithe/toolchains/local.json;
    • 环境页和来源页的骨架。
  • 插件自己的配置放在插件自己的设置页,例如 mise 可执行文件的位置、是否离线。
  • 插件停用或卸载时:
    • 对应的工具行、使用方、导出的环境变量、插件设置页一起消失;
    • 已经写下的固定值留在 local.json 里,重新启用后恢复;
    • 项目文件要求了某个工具、但定义它的插件没启用时,提示「…要求 Maven,但定义它的插件「Maven」未启用」,并给出「在插件页启用」按钮。

1. 三类贡献:界面上每一块由谁注入

界面上的东西 谁注入 建议的契约字段
工具行的名字、图标 工具定义(插件) toolDefinitions[].id / displayName / icon
怎么确认一个目录是这个工具(版本、手动添加时的校验) 工具定义 executables、version.arguments / version.pattern、homeLayouts(例如 *.jdk → Contents/Home)
自动层扫描哪些位置 工具定义 discovery(宿主的系统扫描只执行各定义声明的位置,自己不写路径)
登录 Shell 读哪个变量、PATH 里找哪些命令、Wrapper 工具定义 homeVariable、commands、wrappers
导出的环境变量和 PATH 工具定义 exports(如 JAVA_HOME: ${home})、pathPrepend
已安装版本、项目或全局的要求(带来源文件) 提供方(mise、SDKMAN……) toolchainProviders[],后端实现 toolchains/query,返回 installations / statements;声明 watchFiles;权限 toolchains.provide
“谁在用这个工具”、切换后怎么生效 使用方(语言服务器、构建、运行配置) launch.tool、requiresTools、onToolchainChange:restart / resync / nextRun

2. 选择规则:四层,建在 #1420 的候选数组上

你在第 1 节给的 executables 示意保留。四层规则在 resolver 里的语义:

  1. 固定
    • 按目录生效,取最近的祖先目录的值。
    • 来源是 local.json 的 pins,也就是你示意里 layer: "project" 的 setting。
    • 固定的安装不存在或不可用时返回 terminalFailure,报错,不回落。
  2. 项目文件
    • 离当前目录最近的文件生效。
    • 同一目录里:Wrapper > 环境(如 venv)> 版本声明。这样 mvnw 排在 mise 要求的 Maven 前面,同意你的建议。
    • 同一个文件被多个提供方读到时:原生读取 > 兼容读取。例如 .nvmrc 归 nvm,mise 读它算兼容。
    • 要求的版本没装时返回 skip,必须带原因,例如「mise.toml 要求 node 99,未安装」;界面标「未满足」。
  3. 全局
    • Lithe 全局默认优先,其次是各提供方的全局设置。
    • 多个提供方的全局设置冲突时,不按插件 ID 猜:让用户选;选之前跟登录 Shell 的 PATH 走。
    • 最后是登录 Shell。
  4. 自动:在所有提供方报告的安装里,取满足最低版本的最新版。

3. 按目录(模块)拆分和固定

  • 环境页左侧是 IDEA 式目录树。每个目录(模块)单独解析,子目录继承最近祖先的固定值。
  • 在某个目录上固定一个版本,会写进 .lithe/toolchains/local.json,重新打开项目时自动应用。建议格式:
    {"version": 1, "toolchains": {}, "pins": {".": {"java": {"home": "/Users/me/.local/share/mise/installs/java/21.0.4", "version": "21.0.4", "provider": "mise"}}}}
    需要在契约里给 local.json 加上 pins 字段。
  • 第一次写入时,往 .lithe/.gitignore 加一行 /toolchains/local.json。打开项目时只读取,不写任何文件。
  • 使用方也按目录拆开显示:每个目录下列出用了哪些工具(Maven 构建任务、语言服务器、运行配置),以及它们实际拿到的版本和环境变量。

4. 手动添加

  • 版本下拉框里有「添加…」。选中目录后,按工具定义里的 homeLayouts 和 version 校验,例如 JDK 跑 bin/java -version。
  • 手动添加的安装只存在应用存储里,不写 app bundle,也不写项目。它们只能用来固定,不参与自动层。

5. 紧凑展示

  • 每个工具一行,展开后默认只显示正在生效的那一层。没有值的层写「没有」。
  • 有一个小开关「显示全部」,打开后列出全部层:被覆盖的划掉,未满足的写原因。

6. 查询模块和文件监听

  • 查询模块内置在宿主里:每次查询带 operationID,支持取消和超时,过期结果直接丢弃。
  • 监听哪些文件是声明式的:提供方用 watchFiles 列出文件名或 glob(mise.toml、.tool-versions、.sdkmanrc……),新建或删除这些文件也会触发。
  • 任意一个被监听的文件变化,就整体重新查询所有提供方,不做增量。可以复用 feat(plugins): 插件注入的工具链框架与 macOS 只读环境设置页 #1581 的 stat 指纹缓存。

7. 拆分(改写后)

  1. 提案:proposed/ 下的 Agent Note,加上契约草案和 fixture,不改代码。契约包括:
    • toolDefinitions[]:第 1 节表格里的字段;
    • toolchainProviders[]、toolchains/query 的请求和应答、watchFiles、权限 toolchains.provide;
    • toolchain 候选来源,以及 skip 的原因字段;
    • launch.tool 和 runtimeDependencies[].tool 改成可以填任意已定义的工具 ID(现在是写死的枚举);新增 requiresTools、onToolchainChange;
    • local.json 的 pins;
    • fixture 至少包括:一个「Java 支持」式的工具定义、一个 mise 提供方、一个引用未定义工具的反例。
    • 合并后记得在 lithe-plugin-docs 运行 node scripts/contract.mjs sync。
  2. Core 与查询客户端:
    • 定义校验和答案校验(绝对路径、存在、可执行、不在工作区内、数量上限);
    • resolver 的扩展:toolchain 候选、带原因的 skip、项目层从 pins 取最近的祖先;
    • macOS 查询客户端;
    • 系统扫描作为第一个内置提供方,扫哪些位置来自各工具定义的 discovery;
    • 不再有内置定义数据文件。
  3. 统一选择与设置界面:
    • 按原型做环境页(目录树、工具行、紧凑展示、使用方、导出的环境变量)和来源页(提供方、手动添加),文案全部走 shared/i18n;
    • ProjectRuntimeService 的三条链换成按工具定义解析;
    • 把 Java 的定义迁进「Java 支持」,Maven 的定义迁进 Maven 插件(和 feat(plugins): #1495 Maven 模型、配置、测试与服务运行迁移(阶段 D 进行中) #1558 协调),并删掉 project-jdk / project-maven 的写死;
    • Node、Go、Python 等它们的官方插件声明定义后再迁。在那之前保持现状,但不要再新增特判。
  4. 插件:
    • mise 0.2.0,后端调用 mise ls --json;
    • SDKMAN 验收插件,同时把 SDKMAN 的位置迁出 MacRuntimeDiscovery;
    • 两个插件各自的设置放在自己的插件设置页。

你第 3 节里的其余几点照原样进行:免重启第一阶段不放宽,checkRuntimeVersion 的 Java 8 特例改成定义里的正则。

8. 现有代码里要清掉的写死

  • 契约:
    • plugin-manifest.schema.json:launch.kind: "runtime" 的 tool 只允许 java / node;runtimeDependencies[].tool 只允许 go / java / node。
    • rust/lithe-core/src/debug/adapters.rs:SELECTED_RUNTIME_TOOLS = ["java", "node", "python"]。
  • Core:project-jdk、project-maven、project-node 出现在:
    • execution/configuration.rs
    • execution/environment.rs
    • execution/java_selection.rs
    • execution/detectors/{maven,gradle,npm}.rs
    • project/maven.rs
  • macOS:
    • 本 PR 里的 tool == "java";
    • ProjectRuntimeService 的三条选择链;
    • MacRuntimeDiscovery 里写死的 SDKMAN 路径。

9. 原型怎么用

原型 HTML:https://gist.github.com/xiaoyumuxi/2ae889a6592599b13998bc811c647942

下载后打开:curl -L -o environment-settings.html https://gist.githubusercontent.com/xiaoyumuxi/2ae889a6592599b13998bc811c647942/raw/environment-settings.html && open environment-settings.html;也可以在 gist 页面点 Raw,另存为 .html。

  • 下载后用浏览器直接打开,不需要联网。
  • 顶部的演示栏有「工具插件」开关。关掉「Maven」或「Java 支持」,可以看到工具行、使用方、导出的环境变量、来源页里提供方的工具胶囊和设置导航一起变化;固定值保留,重新启用后恢复。全部关掉时显示空状态。
  • 演示栏的「实现说明」会打开一个侧栏,内容包括:
    • 界面每一块对应哪个插件注入、建议的契约字段;
    • 宿主负责哪些部分;
    • 原型里定下来的行为;
    • 要清掉的写死;
    • 「Java 支持」工具定义和 mise 提供方的清单示意。
  • 每一层旁边的 ⓘ 和每个工具展开后的「定义」行,会说明这个工具的数据来自哪个插件的哪个字段。

xiaoyumuxi added a commit that referenced this pull request Oct 31, 2026
* feat(plugins): 声明式语法高亮与运行标记(API-25/27/29)

- languages 2:插件语言可带 Monarch 语法和编辑器配置,Core 校验,纯语法插件免重启
- runMarkers 1:backend 2 插件函数回答可运行行,编辑器画运行按钮,点击经 commands/didRun 带 runMarker
- 契约、fixture、Agent Note、计划文档与功能矩阵同步更新;Windows 暂不支持

Refs #570

* feat(plugins): 独立编辑器高亮与动作扩展

- languages 2 提供静态语法和独立编辑配置,插件正则在可终止 worker 中运行\n- editor/registerActions 运行期注册文本动作,点击复用普通函数调用,不扩张 Run/LSP/Build/Debug 领域契约\n- 补齐文档版本、动作租约、取消与清理、独立工具后端启用及安全模式边界\n- 更新 Skill、审查规则与决策笔记,总结 #1581/#1558 的架构边界\n- 同步契约、fixtures、中英词条、功能矩阵与资源复用说明\n\nRefs #570\nReworks #1617

* docs(plugins): 明确 API-25 JSON 贡献的支持范围与局限

区分包内静态 JSON 贡献与尚未开放的运行期 Provider 注册、替换和释放;说明独立语义 Provider、新引擎适配及平台、SDK 和验收限制。同步计划、架构笔记、正式契约与功能矩阵。

* fix(plugins): 修复编辑器扩展审查中的生命周期与高亮问题

动作刷新立即确认调度并隔离非致命失败,整批收集共享八秒期限并在撤销时清理计时任务。

Core 保留校验后的 grammar 原始文本,worker 按源顺序解析 cases 并显式默认 root;配置-only 别名借用既有 tokenizer,重映射 token 语言身份以保留独立注释配置。

模块状态观察弱引用 runtime,补齐有界、确定性的 Swift/Rust/WebKit 回归与测试文档模型清理,同步契约、Agent Note 和功能矩阵。

验证:57 项 Swift、1459 项 Rust、39 项编辑器和 119 项真实 WebKit 测试通过;静态边界、i18n、只读 bundle、资源复用与矩阵门禁通过。未执行 Windows 原生构建或打包安装验收。

Refs #1617, #570
按 1lck#1581 评审后的改写计划重做:宿主不认识任何工具,工具定义、版本管理器
(提供方)全部由插件声明;Core 只做校验和四层解析,不解析 mise 等管理器
的私有格式。

- 契约:新增 toolDefinitions 1、toolchainProviders 1,executableResolution
  增加 v2(只用于工具定义候选);同步 schema、README、toolchains.md、
  fixture 和 rust-core-api.md;proposals/toolchains/ 放完整方案草案
- Rust Core:新领域模块 rust/lithe-core/src/toolchain/(定义注册表、
  提供方答复校验、目录固定、固定/项目文件/全局/自动四层解析、版本匹配、
  扫描计划)和六个 toolchains.* 命令,全部只处理宿主快照,不启动进程、
  不扫描、不写文件
- macOS:设置 › 构建新增「运行环境」「环境来源」两页,内容来自已启用
  插件的声明和磁盘上的真实文件;未实现的添加、切换、保存、查询入口不显示
- 登录 Shell:新增只读 inspect,来源页显示读取状态、变量数、耗时和
  PATH 各目录来源;重新读取不改变本进程 PATH
- Debug 预览入口 --toolchain-settings-demo / --toolchain-sources-demo
- Agent Note、设置层级 Note、功能矩阵和中英词条同步

Refs 1lck#1514
@xiaoyumuxi
xiaoyumuxi force-pushed the feat/plugin-toolchain-sources branch from 27852b0 to 71f9f65 Compare October 31, 2026 14:07
@xiaoyumuxi xiaoyumuxi changed the title feat(plugins): 声明式版本管理器 toolchainSources,自动发现 mise 等安装的运行时并按项目版本选择 feat(plugins): 插件注入的工具链框架与 macOS 只读环境设置页 Oct 31, 2026
@xiaoyumuxi
xiaoyumuxi marked this pull request as ready for review October 31, 2026 14:11
@lithe-code-review

lithe-code-review Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Lithe Review

结论: ⚠️ 有建议
范围: preview ← feat/plugin-toolchain-sources · head: 71f9f65

未发现需要阻塞合并的问题。

Findings

  1. [P2] 保留规范化安装路径对应的文件证据
    rust/lithe-core/src/toolchain/resolution.rs:362

    • 实际影响:合法的符号链接安装通过提供方答复校验后,在解析阶段被误判为 unsafePath,导致项目要求回落、自动层遗漏该安装。
    • 触发条件与复现输入:提供方返回 /tools/java/latest,宿主事实以该路径及其 bin/java 为 path,以 /tools/java/21 下对应路径为 canonicalPath,并提供成功的版本探测记录;所有路径均在工作区之外。
    • 调用路径:query::decode 接受这些事实,并将安装及 statement 中的路径规范化为 /tools/java/21;这里再调用 snapshot.selection,经 validate_installation 按规范化路径重新查找事实。但事实表仅以原始 path 建索引,契约没有要求再提供一份规范化路径记录,因此同一快照会在第二次校验时失败。全局和自动分支也存在相同问题。
    • 来源:本次引入,属于新增 Core 解析命令的内部一致性问题。
  2. [P2] 环境候选应使用其声明的可执行文件路径
    rust/lithe-core/src/toolchain/resolution.rs:1071

    • 实际影响:解析结果可能选择另一个可执行文件及版本,与插件候选计划不符,也与 macOS 设置加载器按候选 path 查找的行为不一致。
    • 触发条件与复现输入:以工具定义 fixture 为基础,增加同平台布局 alt/bin/java,保留 bin/java 为第一个布局,将环境候选的 path 指向 alt/bin/java。设置 JAVA_HOME,为两个程序提供有效文件事实与不同版本的成功探测记录。
    • 调用路径:定义校验允许环境候选引用任一匹配布局;此分支却只读取 variable,未使用 candidate.path。随后 checked_installation 从布局列表首项开始选择,最终返回 bin/java。既有环境候选的路径语义因此丢失。
    • 来源:本次引入,属于新增候选解析实现。

Scope Check

  • 关联 [Bug] macOS 下自动检测不到 mise/SDKMAN 等版本管理器安装的 Maven 和 JDK,必须手动指定路径 #1514 仅交付基础部分:实际版本管理器发现和运行时选择尚未接入,Issue 仍未解决;这与 PR 明确声明的范围一致。
  • 已完成范围:Core 注册、答复校验、固定值和分层解析,以及 macOS 只读设置骨架。
  • 未完成范围:提供方查询进程、监听、持久化、使用方迁移及 Windows 原生接入均已明确延期,不作为本次缺陷。
  • 候选排序复用了现有图校验;未发现重新实现 mise/SDKMAN 私有版本语义。两项 Finding 均位于本次新增逻辑,属于本次职责范围。

Suggestions

  1. 保留已校验安装的原始查找路径或证据关联,使规范化结果能够继续使用同一份快照;补充“外部符号链接安装从 decode 到 resolve”的正向回归用例。
  2. 将环境候选指定的布局传入校验与选择流程,确保探测、导出和最终 executable 都对应其 path;补充同平台多个布局同时存在的用例。
  3. 补充可确认对应本 head 的两页实现截图,覆盖宽窄布局;已有界面受影响时提供前后对比。当前上下文中的图片是设计参考,无法据此确认最终视觉效果。

Summary

整体分层与延期边界清晰。建议在后续原生接入前修复上述两处解析不一致,避免设置展示与实际选择产生分歧。

Verification

  • 静态检查了新增 toolchain 模块、插件准入和命令分发、直接相关契约与测试,以及 macOS 加载器、登录 Shell、设置模型和视图调用链;未读取生成资源正文。
  • 作者报告 Rust 检查、71 个定向 macOS 测试及相关静态检查通过,本次未独立运行或核验这些结果。
  • 所提供 head checks 中 Notes、矩阵及 Ubuntu i18n 有成功结果;Swift/Rust 测试任务为 skipped,部分检查仍 queued,平台 gate 成功不能视为对应测试已执行。
  • 本次未运行任何代码、测试或应用,未访问网络。Windows 原生构建、macOS 全量测试及本 head 的视觉效果仍未验证。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review: no-score ghfind author score missing (not zero)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants