From fcb7490f630fbcb61948601757179b88ed8bff61 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 12 Sep 2026 16:07:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(vscode):=20=E6=8F=92=E4=BB=B6=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E8=B0=83=E8=AF=95=E6=89=A9=E5=B1=95=EF=BC=88plg.json?= =?UTF-8?q?=20=E8=AF=8A=E6=96=AD=20/=20SDK=20=E7=89=88=E6=9C=AC=20/=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E8=BF=90=E8=A1=8C=20/=20=E5=86=85=E6=A0=B8?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=B7=9F=E9=9A=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 插件的真实形态是「独立子进程 + 内核侧握手」,所以插件问题几乎都在 IDE 之外发生: 编不出来(多半是没声明用哪版 SDK,工具链拿了存储里的 current)、编出来起不来 (产物与内核协议绑定)、起来了行为不对(真因只在内核日志里)。这个扩展把这三件事 拉进 IDE。 - `tools/vscode-hmapdev`:TypeScript 扩展(零运行时依赖,仅 devDeps: typescript + @types/vscode) - **plg.json 诊断**:必需字段;`sdk` 必须是完整版本号(区间写法 `1.2` 报错并说明 「patch 位恒为 .0」,与工具链 ResolveSDKForProject 同一套规矩);声明的 SDK 未安装时 直接给 `hmapdev sdk install vX.Y.Z`; - **状态栏**:`插件 · SDK <声明> · hmapdev <版本>`,工具链缺失/工程有错时变色,tooltip 列已装 SDK; - **命令**:build / build --target all / clean / debug(解释执行)/ 工具链版本 / SDK 列表-安装-切换 / 跟随内核日志 / 停止跟随 / 刷新; - **任务**:同一批动作注册为 `hmapdev` 任务 + Go 问题匹配器(编译错误进 Problems); - **内核日志跟随**:读 `/log` 最新 `homed_*.log`,按插件名过滤持续输出; - schema 校验 + plg.json 骨架片段。 - 诚实边界(写进 README):**不是源码级调试器**——没有断点/单步,没有 DAP 会话; 做的是构建、运行、看内核日志、清单校验。 验证:`tsc` 零错误;`node --test` 13/13(版本规则、诊断分级、SDK 列表解析、状态栏文本、 以及**反向核对抓到的真缺陷**:`hmapdev 未找到` 这类输出曾被解析成版本号 → 已要求版本 token 以数字开头,否则「工具链不在」会被显示成「工具链 <垃圾词>」并让 SDK 诊断失真); 与真实工具链输出的集成核对 PASS(`hmapdev version` / `sdk list` 的真输出解析正确)。 README 同时补两节:plg.json 的 `sdk` 字段语义(含「为什么必须有」与「为什么拒区间写法」)、 本扩展的用法与边界。 --- README.md | 51 ++++ tools/vscode-hmapdev/.gitignore | 3 + tools/vscode-hmapdev/.vscodeignore | 5 + tools/vscode-hmapdev/README.md | 69 +++++ tools/vscode-hmapdev/package-lock.json | 59 ++++ tools/vscode-hmapdev/package.json | 174 +++++++++++ tools/vscode-hmapdev/schema/plg.schema.json | 41 +++ .../snippets/plg.json.code-snippets | 26 ++ tools/vscode-hmapdev/src/core.ts | 174 +++++++++++ tools/vscode-hmapdev/src/extension.ts | 272 ++++++++++++++++++ tools/vscode-hmapdev/src/test/core.test.ts | 106 +++++++ tools/vscode-hmapdev/src/toolchain.ts | 139 +++++++++ tools/vscode-hmapdev/tsconfig.json | 19 ++ 13 files changed, 1138 insertions(+) create mode 100644 tools/vscode-hmapdev/.gitignore create mode 100644 tools/vscode-hmapdev/.vscodeignore create mode 100644 tools/vscode-hmapdev/README.md create mode 100644 tools/vscode-hmapdev/package-lock.json create mode 100644 tools/vscode-hmapdev/package.json create mode 100644 tools/vscode-hmapdev/schema/plg.schema.json create mode 100644 tools/vscode-hmapdev/snippets/plg.json.code-snippets create mode 100644 tools/vscode-hmapdev/src/core.ts create mode 100644 tools/vscode-hmapdev/src/extension.ts create mode 100644 tools/vscode-hmapdev/src/test/core.test.ts create mode 100644 tools/vscode-hmapdev/src/toolchain.ts create mode 100644 tools/vscode-hmapdev/tsconfig.json diff --git a/README.md b/README.md index bdba2fa..f171a66 100644 --- a/README.md +++ b/README.md @@ -473,6 +473,57 @@ ctx.Unlock() 内部插件(平台内置)拥有完整 SDK 访问权限,包括 SocialAPI 写操作和 EventPublisher。 +## 项目声明 SDK 版本(plg.json 的 `sdk` 字段) + +`hmapdev init` 生成的工程里,`plg.json` 会带一个 `sdk` 字段: + +```json +{ + "name": "MyPlugin", + "version": "0.1.0", + "entry": "plugin.bin", + "sdk": "1.2.0" +} +``` + +它的语义是**本插件针对的 SDK 版本**,工具链据此在本地 SDK 存储里选择版本: +命中就用它,并把 `go.mod` 的 `require`/`replace` 同步到该版本;未命中则**明确报错** +(列出已装版本 + `hmapdev sdk install vX.Y.Z`),**绝不静默退化成 `current`**。 + +```bash +$ hmapdev build +[hmapdev] SDK 1.2.0(项目声明 sdk=1.2.0) +``` + +为什么要这个字段:以前项目里没有任何「我要哪版 SDK」的声明,工具链只能用存储里的 +`current`——谁改过 `current` 就拿谁的版本编,出错时表现为一堆看不懂的编译错误 +(例如存储里只有陈旧的 `v0.8.0` 时,模板项目首次构建会报 `undefined: sdk.InjectOptions`)。 + +**写法必须是完整版本号(`1.2.0`),不接受区间写法(`1.2`)。** 原因见上文的版本纪律: +SDK 版本跟随内核中版本、patch 位恒为 `.0`,一条内核线只对应一个 SDK 版本; +写区间会让人误以为同一条线里还能挑不同 SDK(工具链会直接拒绝并说明这条规矩)。 + +- 显式 `--sdk-path` 或 `plg.json` 的 `sdk_path` 优先(本机改 SDK 联调时用); +- 存量工程(`plg.json` 没有 `sdk` 字段)行为不变,仍按 `current` 构建; +- 产物 `.hmap` 里的 `plugin.json` 会记录**实际选中的 SDK 版本**,便于事后追溯。 + +## IDE 支持:VSCode 扩展(`tools/vscode-hmapdev`) + +调试插件的实操回路是「构建 → 运行 → 看内核日志」,这三步都在 IDE 之外很别扭, +所以仓库里带了一个 VSCode 扩展([tools/vscode-hmapdev](tools/vscode-hmapdev)): + +- **plg.json 诊断**:必需字段、`sdk` 是否是完整版本号、声明的 SDK 是否已安装(直接给安装命令); +- **状态栏**:`插件 · SDK <声明> · hmapdev <版本>`,工具链缺失或工程有错时变色; +- **命令 / 任务**:build / build(全部目标)/ clean / debug(解释执行),编译错误进 Problems; +- **跟随内核日志**:读 `/log` 下最新的 `homed_*.log` 并按插件名过滤。 + +```bash +cd tools/vscode-hmapdev && npm install && npm run compile # 然后在 VSCode 里按 F5 +``` + +它不是源码级调试器(没有断点/单步):插件要么编译成产物在内核里跑、要么用 +`hmapdev debug` 解释执行,两条路都没有 DAP 会话;扩展做的是构建、运行、看日志与清单校验。 + ## 示例插件 | 插件 | 类型 | 说明 | diff --git a/tools/vscode-hmapdev/.gitignore b/tools/vscode-hmapdev/.gitignore new file mode 100644 index 0000000..d3e15b1 --- /dev/null +++ b/tools/vscode-hmapdev/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +out/ +*.vsix diff --git a/tools/vscode-hmapdev/.vscodeignore b/tools/vscode-hmapdev/.vscodeignore new file mode 100644 index 0000000..4315409 --- /dev/null +++ b/tools/vscode-hmapdev/.vscodeignore @@ -0,0 +1,5 @@ +src/ +tsconfig.json +node_modules/ +out/test/ +.gitignore diff --git a/tools/vscode-hmapdev/README.md b/tools/vscode-hmapdev/README.md new file mode 100644 index 0000000..696dc7a --- /dev/null +++ b/tools/vscode-hmapdev/README.md @@ -0,0 +1,69 @@ +# hmapdev — HomeAgent 插件开发 VSCode 扩展 + +调试与构建 HomeAgent 插件工程的 IDE 支持:**plg.json 校验、SDK 版本解析、构建/运行、内核日志跟随**。 + +## 为什么需要它 + +插件的真实形态是「**独立子进程 + 内核侧握手**」,所以插件的三类问题几乎都在 IDE 之外发生: + +1. **编不出来** —— 最常见的原因不是代码,而是项目没声明要用哪版 SDK,工具链拿了存储里的 + `current`(可能是陈旧的 `v0.8.0`),于是报一堆看不懂的 `undefined: sdk.XXX`; +2. **编出来但起不来** —— 产物与内核**协议绑定**(协议版本 + 共享内存魔数),用错工具链编出来的 + 插件会在握手时被拒; +3. **起来了但行为不对** —— 真因往往只在内核日志里(建链失败、崩溃重启、工具报错)。 + +本扩展把这三件事拉进 IDE:**先把「用哪版 SDK」摆到明面上**,再让构建/运行/看日志变成一条动作链。 + +## 功能 + +| 功能 | 说明 | +|---|---| +| **plg.json 诊断** | 必需字段;`sdk` 必须是**完整版本号**(区间写法 `1.2` 会报错并说明「patch 位恒为 .0」);声明的 SDK 若未安装在本地存储,直接给出 `hmapdev sdk install vX.Y.Z` | +| **状态栏** | `插件 · SDK <声明> · hmapdev <版本>`;工具链缺失或工程有错时变红/黄,tooltip 列出已装 SDK | +| **构建 / 清理 / 运行** | `hmapdev build`、`build --target all`、`clean`、`debug`(解释执行,快速迭代)——在集成终端里跑,可 Ctrl-C | +| **任务(Tasks)** | 同一批动作注册为 `hmapdev` 任务,可绑快捷键、串依赖;带 **Go 问题匹配器**,编译错误进 Problems 面板 | +| **跟随内核日志** | 读 `/log` 下最新的 `homed_*.log`,按插件名过滤后持续输出(真正的联调回路) | +| **SDK 管理** | 查看工具链版本、列出/安装/切换 SDK 版本(走 QuickPick,不用记命令) | +| **JSON 支持** | `plg.json` 的 schema 校验 + 骨架片段 | + +## 安装 + +```bash +cd tools/vscode-hmapdev +npm install +npm run compile +``` + +然后二选一: + +- **开发模式**:在 VSCode 里打开本目录,按 `F5`(Extension Development Host),把插件工程目录作为工作区打开; +- **安装到本机**:`npx @vscode/vsce package` 生成 `.vsix`,再 `code --install-extension hmapdev-vscode-0.1.0.vsix`。 + +前提:`hmapdev` 在 `PATH` 上(或设置 `hmapdev.path`)。 + +## 配置 + +| 设置 | 默认 | 说明 | +|---|---|---| +| `hmapdev.path` | `hmapdev` | 工具链可执行文件路径 | +| `hmapdev.kernelDataDir` | 空 | 内核数据目录(`homed -data` 的那个);填了才能跟随内核日志 | +| `hmapdev.diagnoseSdk` | `true` | 是否校验声明的 SDK 是否已安装(需要能执行 hmapdev) | + +## 用法(典型开发回路) + +1. 打开插件工程(含 `plg.json`)→ 状态栏出现 `插件 · SDK <版本> · hmapdev <版本>`; +2. 若 `sdk` 报错(未声明 / 区间写法 / 未安装)→ 按提示执行 `hmapdev: 安装 SDK 版本…`,再 `hmapdev: 刷新状态`; +3. `hmapdev: 构建插件`(或 `构建(全部目标平台)`)→ 编译错误直接进 Problems; +4. 快速验证行为:`hmapdev: 运行插件(解释执行)`; +5. 与内核联调:设置 `hmapdev.kernelDataDir` → `hmapdev: 跟随内核日志`,只看本插件的行; +6. 改代码 → 重复 3/5。装进内核时记得**与内核同批替换**(协议绑定的产物不支持滚动升级)。 + +## 诚实的边界 + +- **这不是源码级调试器**:没有断点/单步。插件的 Go 代码要么编译成产物在内核里跑、要么用 + `hmapdev debug`(yaegi 解释执行)跑,两条路都不提供 DAP 调试会话。本扩展做的是 + 「构建 + 运行 + 看内核日志 + 清单校验」,这也是插件问题实际能被定位的方式。 +- **Windows 目标**:不支持(协议 2 的统一共享内存区未移植到 Windows,内核侧改走 WSL2), + 扩展只给提示,不假装能构建。 +- **`sdk` 字段的语义**:它声明的是**本插件针对的 SDK 版本**(= 接口线),不是内核版本。 + SDK 版本跟随内核中版本、patch 位恒为 `.0`。 diff --git a/tools/vscode-hmapdev/package-lock.json b/tools/vscode-hmapdev/package-lock.json new file mode 100644 index 0000000..4eab583 --- /dev/null +++ b/tools/vscode-hmapdev/package-lock.json @@ -0,0 +1,59 @@ +{ + "name": "hmapdev-vscode", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hmapdev-vscode", + "version": "0.1.0", + "license": "AGPL-3.0-only", + "devDependencies": { + "@types/node": "^22.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.6.0" + }, + "engines": { + "vscode": "^1.85.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.20.2.tgz", + "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.137.0", + "resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.137.0.tgz", + "integrity": "sha512-0dc/BBWxkyUsJzXIZ7PkKSalThmS4xiBT+8YEDiWdCefRKHGVV5ZNkM5NB5ULYamallYJujIfncNoXWFlyzL8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tools/vscode-hmapdev/package.json b/tools/vscode-hmapdev/package.json new file mode 100644 index 0000000..76c1f7e --- /dev/null +++ b/tools/vscode-hmapdev/package.json @@ -0,0 +1,174 @@ +{ + "name": "hmapdev-vscode", + "displayName": "HomeAgent Plugin Dev (hmapdev)", + "description": "调试与构建 HomeAgent 插件工程:plg.json 校验、SDK 版本解析、hmapdev 构建/运行、内核日志跟随。", + "version": "0.1.0", + "publisher": "JianFeeeee", + "license": "AGPL-3.0-only", + "private": true, + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Programming Languages", + "Debuggers", + "Other" + ], + "main": "./out/extension.js", + "activationEvents": [ + "workspaceContains:plg.json", + "workspaceContains:**/plg.json" + ], + "contributes": { + "commands": [ + { + "command": "hmapdev.build", + "title": "hmapdev: 构建插件" + }, + { + "command": "hmapdev.buildAll", + "title": "hmapdev: 构建插件(全部目标平台)" + }, + { + "command": "hmapdev.clean", + "title": "hmapdev: 清理产物" + }, + { + "command": "hmapdev.run", + "title": "hmapdev: 运行插件(解释执行,快速迭代)" + }, + { + "command": "hmapdev.showVersion", + "title": "hmapdev: 查看工具链版本" + }, + { + "command": "hmapdev.listSdk", + "title": "hmapdev: 列出 SDK 版本" + }, + { + "command": "hmapdev.installSdk", + "title": "hmapdev: 安装 SDK 版本…" + }, + { + "command": "hmapdev.useSdk", + "title": "hmapdev: 切换当前 SDK 版本…" + }, + { + "command": "hmapdev.tailKernelLog", + "title": "hmapdev: 跟随内核日志(按插件过滤)" + }, + { + "command": "hmapdev.stopTailKernelLog", + "title": "hmapdev: 停止跟随内核日志" + }, + { + "command": "hmapdev.openPlgJson", + "title": "hmapdev: 打开 plg.json" + }, + { + "command": "hmapdev.refresh", + "title": "hmapdev: 刷新状态(重新探测工具链与 SDK)" + } + ], + "configuration": { + "title": "HomeAgent Plugin Dev", + "properties": { + "hmapdev.path": { + "type": "string", + "default": "hmapdev", + "description": "hmapdev 可执行文件路径(默认从 PATH 找)。" + }, + "hmapdev.kernelDataDir": { + "type": "string", + "default": "", + "description": "内核数据目录(homed -data 的那个目录)。填了才能跟随内核日志调试;留空则「跟随内核日志」会先询问。" + }, + "hmapdev.diagnoseSdk": { + "type": "boolean", + "default": true, + "description": "校验 plg.json 里声明的 SDK 版本是否已安装在本地 SDK 存储(需要能执行 hmapdev)。" + } + } + }, + "taskDefinitions": [ + { + "type": "hmapdev", + "required": [ + "action" + ], + "properties": { + "action": { + "type": "string", + "enum": [ + "build", + "buildAll", + "clean", + "run" + ], + "description": "要执行的 hmapdev 动作。" + }, + "cwd": { + "type": "string", + "description": "插件工程目录(默认取 plg.json 所在目录)。" + } + } + } + ], + "problemMatchers": [ + { + "name": "hmapdev-go", + "owner": "go", + "source": "hmapdev", + "fileLocation": [ + "relative", + "${workspaceFolder}" + ], + "pattern": [ + { + "regexp": "^(.+\\.go):(\\d+):(\\d+):\\s+(.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + }, + { + "regexp": "^(.+\\.go):(\\d+):\\s+(.+)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ], + "languages": [ + { + "id": "json", + "filenames": [ + "plg.json" + ] + } + ], + "jsonValidation": [ + { + "fileMatch": "plg.json", + "url": "./schema/plg.schema.json" + } + ], + "snippets": [ + { + "language": "json", + "path": "./snippets/plg.json.code-snippets" + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "test": "tsc -p ./ && node --test out/test/*.test.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/vscode": "^1.85.0", + "typescript": "^5.6.0" + } +} diff --git a/tools/vscode-hmapdev/schema/plg.schema.json b/tools/vscode-hmapdev/schema/plg.schema.json new file mode 100644 index 0000000..645a7e4 --- /dev/null +++ b/tools/vscode-hmapdev/schema/plg.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HomeAgent 插件清单(plg.json)", + "type": "object", + "required": ["name", "version", "entry"], + "properties": { + "name": { + "type": "string", + "description": "插件名(与目录名一致最省事)" + }, + "name_zh": { "type": "string", "description": "中文显示名" }, + "name_en": { "type": "string", "description": "英文显示名" }, + "version": { "type": "string", "description": "插件自身版本号(如 0.1.0),与内核/SDK 版本无关" }, + "description": { "type": "string" }, + "author": { "type": "string" }, + "entry": { + "type": "string", + "description": "入口产物文件名(子进程模式通常是 plugin.bin;Lua 是 main.lua)" + }, + "sdk": { + "type": "string", + "pattern": "^v?\\d+\\.\\d+\\.\\d+$", + "description": "本插件针对的 SDK 版本,必须是完整版本号(如 1.2.0)。SDK 版本跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本;工具链按此在本地 SDK 存储里选择版本。" + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "targets": { + "type": "string", + "description": "目标平台,逗号分隔,如 linux/amd64,darwin/arm64(windows 目标暂不支持插件产物)" + }, + "outdir": { "type": "string", "description": "产物目录(默认 dist)" }, + "bundle": { "type": "boolean", "description": "是否打包成 .hmap(默认 true)" }, + "sdk_path": { + "type": "string", + "description": "直接指定 SDK 源码目录(本机改 SDK 联调时用);设置后优先于 sdk 字段" + }, + "go_version": { "type": "string" }, + "replaces": { "type": "object", "additionalProperties": { "type": "string" } }, + "source_dirs": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": true +} diff --git a/tools/vscode-hmapdev/snippets/plg.json.code-snippets b/tools/vscode-hmapdev/snippets/plg.json.code-snippets new file mode 100644 index 0000000..fbc89f7 --- /dev/null +++ b/tools/vscode-hmapdev/snippets/plg.json.code-snippets @@ -0,0 +1,26 @@ +{ + "plg.json 骨架": { + "prefix": "plg", + "body": [ + "{", + " \"name\": \"${1:MyPlugin}\",", + " \"name_zh\": \"${2:中文名}\",", + " \"name_en\": \"${1:MyPlugin}\",", + " \"version\": \"0.1.0\",", + " \"description\": \"${3:插件说明}\",", + " \"author\": \"${4:HomeAgent}\",", + " \"entry\": \"plugin.bin\",", + " \"sdk\": \"${5:1.2.0}\",", + " \"tags\": [\"${1:MyPlugin}\"],", + " \"targets\": \"linux/amd64,darwin/arm64\"", + "}", + "$0" + ], + "description": "plg.json 必需字段骨架(sdk 必须是完整版本号)" + }, + "sdk 字段": { + "prefix": "sdk", + "body": ["\"sdk\": \"${1:1.2.0}\","], + "description": "声明本插件针对的 SDK 版本(完整版本号;patch 位恒为 .0)" + } +} diff --git a/tools/vscode-hmapdev/src/core.ts b/tools/vscode-hmapdev/src/core.ts new file mode 100644 index 0000000..31d9b89 --- /dev/null +++ b/tools/vscode-hmapdev/src/core.ts @@ -0,0 +1,174 @@ +/** + * 纯逻辑层:不 import vscode,便于用 node --test 直接单测。 + * + * 这里的规矩必须与工具链一致(tools/hmapdev/sdk_resolve.go): + * - SDK 版本跟随内核中版本、**patch 位恒为 .0** → 一条内核线只有一个 SDK 版本; + * - 因此 plg.json 的 `sdk` 必须是**完整版本号**(x.y.z),区间写法("1.2")要报错, + * 否则项目会以为「同一条线里还能挑不同 SDK」。 + */ + +/** plg.json 的字段(未知字段保留,不做拒绝)。 */ +export interface PlgConfig { + name?: string; + name_zh?: string; + name_en?: string; + version?: string; + description?: string; + author?: string; + entry?: string; + sdk?: string; + tags?: string[]; + targets?: string; + sdk_path?: string; + outdir?: string; + bundle?: boolean; + replaces?: Record; + source_dirs?: string[]; +} + +/** 诊断级别(与 vscode.DiagnosticSeverity 数值对齐,避免耦合)。 */ +export enum Severity { + Error = 0, + Warning = 1, + Information = 2, + Hint = 3, +} + +export interface PlgDiagnostic { + severity: Severity; + message: string; + /** plg.json 里的字段名(用于在 JSON 文档里定位)。 */ + field?: string; +} + +/** 完整版本号(x.y.z,允许 v 前缀)。 */ +export function isFullVersion(v: string): boolean { + return /^v?\d+\.\d+\.\d+$/.test((v ?? "").trim()); +} + +/** 中版本(x.y)。 */ +export function isMinorVersion(v: string): boolean { + return /^v?\d+\.\d+$/.test((v ?? "").trim()); +} + +export function normalizeVersion(v: string): string { + return (v ?? "").trim().replace(/^v/, ""); +} + +/** + * 校验 plg.json。 + * + * `installedSdks` 为本地 SDK 存储里已安装的版本(不带 v 前缀);传 undefined 表示 + * 没探测(例如工具链不可用),此时只校验格式、不报「未安装」。 + */ +export function validatePlg(cfg: PlgConfig, installedSdks?: string[]): PlgDiagnostic[] { + const out: PlgDiagnostic[] = []; + const req = (field: keyof PlgConfig, hint: string) => { + const v = cfg[field]; + if (v === undefined || v === null || String(v).trim() === "") { + out.push({ severity: Severity.Error, message: `${field} 不能为空(${hint})`, field: field as string }); + } + }; + req("name", "插件名,与目录名一致最省事"); + req("entry", "入口产物,子进程模式通常是 plugin.bin"); + req("version", "插件自身版本号,如 0.1.0"); + + // SDK 声明:这是「工具链自动选 SDK 版本」的依据,缺了就只能退回 current + if (cfg.sdk === undefined || cfg.sdk === null || String(cfg.sdk).trim() === "") { + out.push({ + severity: Severity.Warning, + message: "缺少 sdk 字段:工具链无法据此选择 SDK 版本,会退回存储里的 current(换机器/换人后容易编出与预期不符的产物)", + field: "sdk", + }); + } else if (isMinorVersion(cfg.sdk)) { + out.push({ + severity: Severity.Error, + message: + `sdk 必须是完整版本号(如 "1.2.0"):${cfg.sdk} 这种区间写法会让人以为同一条内核线里还能挑不同 SDK。` + + `SDK 版本跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本。`, + field: "sdk", + }); + } else if (!isFullVersion(cfg.sdk)) { + out.push({ severity: Severity.Error, message: `sdk 不是合法版本号(写法:"1.2.0")`, field: "sdk" }); + } else if (installedSdks && !installedSdks.includes(normalizeVersion(cfg.sdk))) { + const have = installedSdks.length ? installedSdks.join(", ") : "(存储里还没有任何 SDK)"; + out.push({ + severity: Severity.Error, + message: `声明的 SDK ${normalizeVersion(cfg.sdk)} 未安装。已安装:${have}。安装:hmapdev sdk install v${normalizeVersion(cfg.sdk)}`, + field: "sdk", + }); + } + + // 目标平台:windows 目前不支持(协议 2 的统一共享内存区未移植) + const targets = (cfg.targets ?? "").toLowerCase(); + if (targets.includes("windows")) { + out.push({ + severity: Severity.Information, + message: "windows 目标暂不支持插件产物:协议 2 的统一共享内存区未移植 Windows(内核改走 WSL2)。构建会在该目标上明确报错。", + field: "targets", + }); + } + if (!cfg.targets) { + out.push({ severity: Severity.Information, message: "未声明 targets,构建时按默认目标处理", field: "targets" }); + } + return out; +} + +/** + * 解析 `hmapdev sdk list` 的输出,返回已安装版本(去 v 前缀、升序)。 + * + * 输出形如: + * Installed SDK versions: + * * v1.2.0 + * v0.8.0 + * 每行可能带 `*` 标记(当前版本)或前导空格。 + */ +export function parseSdkList(text: string): string[] { + const out: string[] = []; + for (const raw of (text ?? "").split(/\r?\n/)) { + const line = raw.trim().replace(/^\*\s*/, "").trim(); + const m = /^v?(\d+\.\d+\.\d+)$/.exec(line); + if (m) { + out.push(m[1]); + } + } + return out.sort(compareVersions); +} + +/** + * 解析 `hmapdev version` 的自述(首行形如 `hmapdev 1.2.0`)。 + * + * 只认**以数字开头**的版本 token:否则 `hmapdev 未找到` / `hmapdev error` + * 这类输出会被当成版本号,把「工具链不在」误报成「工具链 1.x」 + * (状态栏与「是否已装 SDK」的判断都基于它,假版本会让诊断全面失真)。 + */ +export function parseToolchainVersion(text: string): string { + for (const raw of (text ?? "").split(/\r?\n/)) { + const m = /^hmapdev\s+v?(\d+(?:\.\d+)*(?:[-+.][0-9A-Za-z.-]+)?)\s*$/.exec(raw.trim()); + if (m) { + return m[1]; + } + } + return ""; +} + +/** 数值比较 x.y.z(字典序会把 1.2.9 排在 1.2.10 之后)。 */ +export function compareVersions(a: string, b: string): number { + const pa = normalizeVersion(a).split(".").map((n) => parseInt(n, 10) || 0); + const pb = normalizeVersion(b).split(".").map((n) => parseInt(n, 10) || 0); + for (let i = 0; i < 3; i++) { + const d = (pa[i] ?? 0) - (pb[i] ?? 0); + if (d !== 0) { + return d; + } + } + return 0; +} + +/** 状态栏文本:插件 + 声明 SDK + 工具链版本(缺项用 "?")。 */ +export function statusBarText(cfg: PlgConfig | undefined, toolchainVersion: string): string { + const name = cfg?.name?.trim() || "(未识别插件)"; + const sdk = cfg?.sdk ? normalizeVersion(cfg.sdk) : "未声明"; + const tc = toolchainVersion ? `hmapdev ${toolchainVersion}` : "hmapdev 未找到"; + return `${name} · SDK ${sdk} · ${tc}`; +} diff --git a/tools/vscode-hmapdev/src/extension.ts b/tools/vscode-hmapdev/src/extension.ts new file mode 100644 index 0000000..40c7d53 --- /dev/null +++ b/tools/vscode-hmapdev/src/extension.ts @@ -0,0 +1,272 @@ +import * as cp from "child_process"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { PlgConfig, Severity, statusBarText, validatePlg } from "./core"; +import { Toolchain } from "./toolchain"; + +let out: vscode.OutputChannel; +let tc: Toolchain; +let status: vscode.StatusBarItem; +let diagnostics: vscode.DiagnosticCollection; +let tailChild: cp.ChildProcess | undefined; + +/** 找到工作区里的 plg.json(多个时取第一个并提示)。 */ +async function findPlg(): Promise { + const found = await vscode.workspace.findFiles("**/plg.json", "**/{node_modules,out,dist,build}/**", 5); + if (found.length === 0) { + return undefined; + } + return found[0]; +} + +async function readPlg(uri: vscode.Uri): Promise { + try { + const txt = Buffer.from(await vscode.workspace.fs.readFile(uri)).toString("utf8"); + return JSON.parse(txt) as PlgConfig; + } catch (e) { + out.appendLine(`error: 解析 ${uri.fsPath} 失败:${e instanceof Error ? e.message : String(e)}`); + return undefined; + } +} + +function severityToVscode(s: Severity): vscode.DiagnosticSeverity { + switch (s) { + case Severity.Error: + return vscode.DiagnosticSeverity.Error; + case Severity.Warning: + return vscode.DiagnosticSeverity.Warning; + case Severity.Information: + return vscode.DiagnosticSeverity.Information; + default: + return vscode.DiagnosticSeverity.Hint; + } +} + +/** 在 JSON 文档里定位字段(找不到就标整个文件,至少让人看见)。 */ +function rangeForField(doc: vscode.TextDocument, field?: string): vscode.Range { + if (field) { + const idx = doc.getText().indexOf(`"${field}"`); + if (idx >= 0) { + const start = doc.positionAt(idx); + const end = doc.positionAt(idx + field.length + 2); + return new vscode.Range(start, end); + } + } + return new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)); +} + +async function refresh(): Promise { + const uri = await findPlg(); + diagnostics.clear(); + if (!uri) { + status.text = statusBarText(undefined, await tc.version()); + status.tooltip = "工作区里没有找到 plg.json(本扩展只在插件工程里工作)"; + return; + } + const cfg = await readPlg(uri); + if (!cfg) { + return; + } + + const tcVersion = await tc.version(); + const diagSetting = vscode.workspace.getConfiguration("hmapdev").get("diagnoseSdk", true); + const installed = diagSetting ? await tc.sdkList() : undefined; + + const doc = await vscode.workspace.openTextDocument(uri); + const items = validatePlg(cfg, installed).map((d) => { + const vd = new vscode.Diagnostic(rangeForField(doc, d.field), d.message, severityToVscode(d.severity)); + vd.source = "hmapdev"; + return vd; + }); + diagnostics.set(uri, items); + + const errors = items.filter((d) => d.severity === vscode.DiagnosticSeverity.Error).length; + status.text = `$(tools) ${statusBarText(cfg, tcVersion)}`; + status.backgroundColor = tc.isMissing() + ? new vscode.ThemeColor("statusBarItem.errorBackground") + : errors > 0 + ? new vscode.ThemeColor("statusBarItem.warningBackground") + : undefined; + const installedText = installed ? installed.join(", ") || "(无)" : "(未探测)"; + status.tooltip = [ + `插件:${cfg.name ?? "?"}`, + `声明 SDK:${cfg.sdk ?? "未声明"}`, + `已安装 SDK:${installedText}`, + `工具链:${tcVersion ? `hmapdev ${tcVersion}` : "未找到(检查 hmapdev.path / PATH)"}`, + `plg.json:${uri.fsPath}`, + ].join("\n"); + status.command = "hmapdev.openPlgJson"; + status.show(); +} + +async function pluginDir(): Promise { + const uri = await findPlg(); + return uri ? path.dirname(uri.fsPath) : undefined; +} + +async function withDir(fn: (dir: string) => unknown | Promise): Promise { + const dir = await pluginDir(); + if (!dir) { + void vscode.window.showWarningMessage("当前工作区没有 plg.json,无法定位插件工程。"); + return; + } + await fn(dir); +} + +export function activate(context: vscode.ExtensionContext): void { + out = vscode.window.createOutputChannel("hmapdev"); + tc = new Toolchain(out); + diagnostics = vscode.languages.createDiagnosticCollection("hmapdev"); + status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + + context.subscriptions.push(out, diagnostics, status); + + const reg = (id: string, fn: () => unknown) => + context.subscriptions.push(vscode.commands.registerCommand(id, async () => { + try { + await fn(); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + out.appendLine(`error: ${msg}`); + void vscode.window.showErrorMessage(`hmapdev: ${msg}`); + } + })); + + reg("hmapdev.build", () => withDir((d) => tc.runInTerminal(["build"], d, "hmapdev build"))); + reg("hmapdev.buildAll", () => withDir((d) => tc.runInTerminal(["build", "--target", "all"], d, "hmapdev build all"))); + reg("hmapdev.clean", () => withDir((d) => tc.runInTerminal(["clean"], d, "hmapdev clean"))); + reg("hmapdev.run", () => withDir((d) => tc.runInTerminal(["debug", d], d, "hmapdev debug"))); + + reg("hmapdev.showVersion", async () => { + out.show(true); + const v = await tc.version(true); + if (!v) { + void vscode.window.showErrorMessage("找不到 hmapdev:请把它放到 PATH,或设置 hmapdev.path。"); + return; + } + await tc.runAndLog(["version"], process.cwd()); + }); + + reg("hmapdev.listSdk", async () => { + out.show(true); + await tc.runAndLog(["sdk", "list"], process.cwd()); + await refresh(); + }); + + reg("hmapdev.installSdk", async () => { + const v = await vscode.window.showInputBox({ + title: "安装 SDK 版本", + prompt: '输入完整版本号(如 1.2.0)或 latest。注意:SDK 版本跟随内核中版本,patch 位恒为 .0。', + placeHolder: "1.2.0", + }); + if (!v) { + return; + } + tc.runInTerminal(["sdk", "install", `v${v.replace(/^v/, "")}`], process.cwd(), "hmapdev sdk install"); + void vscode.window.showInformationMessage(`安装完成后执行「hmapdev: 刷新状态」以重新校验。`); + }); + + reg("hmapdev.useSdk", async () => { + const list = await tc.sdkList(true); + if (!list || list.length === 0) { + void vscode.window.showWarningMessage("没有探测到已安装的 SDK 版本(先跑「hmapdev: 列出 SDK 版本」看看)。"); + return; + } + const pick = await vscode.window.showQuickPick(list, { title: "切换当前 SDK 版本(存储里的 current)" }); + if (!pick) { + return; + } + tc.runInTerminal(["sdk", "use", `v${pick}`], process.cwd(), "hmapdev sdk use"); + setTimeout(() => void refresh(), 1500); + }); + + reg("hmapdev.tailKernelLog", async () => { + const cfgDir = vscode.workspace.getConfiguration("hmapdev"); + let dataDir = cfgDir.get("kernelDataDir", ""); + if (!dataDir) { + const answer = await vscode.window.showInputBox({ + title: "内核数据目录", + prompt: "homed -data 指向的目录(用于跟随内核日志)。填一次会记住到设置里。", + placeHolder: "/home/newqqagent", + }); + if (!answer) { + return; + } + dataDir = answer; + await cfgDir.update("kernelDataDir", dataDir, vscode.ConfigurationTarget.Workspace); + } + const uri = await findPlg(); + const filter = uri ? (await readPlg(uri))?.name ?? "" : ""; + tailChild?.kill(); + tailChild = tc.tailKernelLog(dataDir, filter); + out.show(true); + }); + + reg("hmapdev.stopTailKernelLog", () => { + if (tailChild) { + tailChild.kill(); + tailChild = undefined; + out.appendLine("已停止跟随内核日志"); + } + }); + + reg("hmapdev.openPlgJson", async () => { + const uri = await findPlg(); + if (uri) { + await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(uri)); + } else { + void vscode.window.showWarningMessage("工作区里没有 plg.json。"); + } + }); + + reg("hmapdev.refresh", async () => { + await tc.sdkList(true); + await tc.version(true); + await refresh(); + }); + + // 任务提供者:把 hmapdev 动作接进 VSCode 的任务体系(可绑定快捷键 / 串联依赖 / 复用问题匹配器) + context.subscriptions.push( + vscode.tasks.registerTaskProvider("hmapdev", { + provideTasks: async () => { + const dir = await pluginDir(); + if (!dir) { + return []; + } + const mk = (action: string, label: string, args: string[]) => { + const def: vscode.TaskDefinition = { type: "hmapdev", action }; + const exec = new vscode.ProcessExecution( + vscode.workspace.getConfiguration("hmapdev").get("path", "hmapdev") || "hmapdev", + args, + { cwd: dir } + ); + return new vscode.Task(def, vscode.TaskScope.Workspace, label, "hmapdev", exec, ["$hmapdev-go"]); + }; + return [ + mk("build", "hmapdev: build", ["build"]), + mk("buildAll", "hmapdev: build (all targets)", ["build", "--target", "all"]), + mk("clean", "hmapdev: clean", ["clean"]), + mk("run", "hmapdev: run (interpreted)", ["debug", dir]), + ]; + }, + resolveTask: (task) => task, + }) + ); + + // plg.json 变化 → 重算诊断(含保存与外部修改) + const watcher = vscode.workspace.createFileSystemWatcher("**/plg.json"); + context.subscriptions.push( + watcher, + watcher.onDidChange(() => void refresh()), + watcher.onDidCreate(() => void refresh()), + watcher.onDidDelete(() => void refresh()) + ); + + void refresh(); +} + +export function deactivate(): void { + tailChild?.kill(); + tailChild = undefined; +} diff --git a/tools/vscode-hmapdev/src/test/core.test.ts b/tools/vscode-hmapdev/src/test/core.test.ts new file mode 100644 index 0000000..47927dc --- /dev/null +++ b/tools/vscode-hmapdev/src/test/core.test.ts @@ -0,0 +1,106 @@ +import * as assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + PlgConfig, + Severity, + compareVersions, + isFullVersion, + isMinorVersion, + normalizeVersion, + parseSdkList, + parseToolchainVersion, + statusBarText, + validatePlg, +} from "../core"; + +const good: PlgConfig = { name: "memo", version: "0.1.0", entry: "plugin.bin", sdk: "1.2.0" }; + +test("isFullVersion / isMinorVersion 区分完整版本与区间写法", () => { + assert.equal(isFullVersion("1.2.0"), true); + assert.equal(isFullVersion("v1.2.0"), true); + assert.equal(isFullVersion("1.2"), false); + assert.equal(isFullVersion("1.2.3.4"), false); + assert.equal(isMinorVersion("1.2"), true); + assert.equal(isMinorVersion("1.2.0"), false); + assert.equal(normalizeVersion("v1.2.0"), "1.2.0"); +}); + +test("合法的 plg.json 不产生错误", () => { + const d = validatePlg(good, ["1.2.0"]); + assert.equal(d.filter((x) => x.severity === Severity.Error).length, 0, JSON.stringify(d)); +}); + +test("缺必需字段要报错并指出字段", () => { + const d = validatePlg({ sdk: "1.2.0" }, ["1.2.0"]); + const fields = d.filter((x) => x.severity === Severity.Error).map((x) => x.field).sort(); + assert.deepEqual(fields, ["entry", "name", "version"]); +}); + +test("区间写法 1.2 必须被拒,并说明 patch 位恒为 .0", () => { + const d = validatePlg({ ...good, sdk: "1.2" }, ["1.2.0"]); + const err = d.find((x) => x.field === "sdk" && x.severity === Severity.Error); + assert.ok(err, "区间写法应报错"); + assert.match(err!.message, /完整版本号/); + assert.match(err!.message, /patch 位恒为 \.0/); +}); + +test("缺 sdk 字段只警告(向后兼容存量项目)", () => { + const d = validatePlg({ name: "memo", version: "0.1.0", entry: "plugin.bin" }, ["1.2.0"]); + const sdk = d.find((x) => x.field === "sdk"); + assert.ok(sdk); + assert.equal(sdk!.severity, Severity.Warning); +}); + +test("声明的 SDK 未安装要报错并给出安装命令", () => { + const d = validatePlg(good, ["0.8.0"]); + const err = d.find((x) => x.field === "sdk" && x.severity === Severity.Error); + assert.ok(err, "未安装应报错"); + assert.match(err!.message, /hmapdev sdk install v1\.2\.0/); + assert.match(err!.message, /0\.8\.0/); +}); + +test("探测不到已装列表时不误报未安装", () => { + const d = validatePlg(good, undefined); + assert.equal(d.filter((x) => x.severity === Severity.Error).length, 0, JSON.stringify(d)); +}); + +test("windows 目标给提示(协议 2 未移植)", () => { + const d = validatePlg({ ...good, targets: "linux/amd64,windows/amd64" }, ["1.2.0"]); + const info = d.find((x) => x.field === "targets"); + assert.ok(info); + assert.match(info!.message, /WSL2/); +}); + +test("parseSdkList 吃掉 * 标记与空格,并按数值排序", () => { + const text = ["Installed SDK versions:", " * v1.2.0", " v0.8.0", " v1.2.10"].join("\n"); + assert.deepEqual(parseSdkList(text), ["0.8.0", "1.2.0", "1.2.10"]); + assert.deepEqual(parseSdkList("No SDK versions installed."), []); +}); + +test("parseToolchainVersion 从 self-report 里取版本", () => { + const text = ["hmapdev 1.2.0", " SDK 模块: gitcode.com/JianFeeeee/homeagent-sdk", " 构建用 Go: go1.25.12"].join("\n"); + assert.equal(parseToolchainVersion(text), "1.2.0"); + assert.equal(parseToolchainVersion("Usage:\n hmapdev init "), ""); +}); + +// 反向核对抓到的真缺陷:`hmapdev <非版本>` 形状的输出曾被当成版本号, +// 于是「工具链不在」会被显示成「工具链 <垃圾词>」,并让 SDK 诊断跟着失真。 +test("parseToolchainVersion 不会把非版本 token 当成版本", () => { + for (const bad of ["hmapdev 未找到", "hmapdev error", "hmapdev not found", "hmapdev -v", "hmapdev"]) { + assert.equal(parseToolchainVersion(bad), "", `不应从 ${JSON.stringify(bad)} 解析出版本`); + } + assert.equal(parseToolchainVersion("hmapdev 1.3.0-dev"), "1.3.0-dev"); // 开发构建的后缀要带出来 + assert.equal(parseToolchainVersion("hmapdev v1.2.0"), "1.2.0"); +}); + +test("compareVersions 是数值比较(1.2.10 > 1.2.9)", () => { + assert.ok(compareVersions("1.2.10", "1.2.9") > 0); + assert.ok(compareVersions("1.2.0", "1.2.0") === 0); + assert.ok(compareVersions("0.8.0", "1.2.0") < 0); +}); + +test("状态栏文本包含插件、声明 SDK 与工具链版本", () => { + assert.equal(statusBarText(good, "1.2.0"), "memo · SDK 1.2.0 · hmapdev 1.2.0"); + assert.equal(statusBarText({ ...good, sdk: undefined }, ""), "memo · SDK 未声明 · hmapdev 未找到"); +}); diff --git a/tools/vscode-hmapdev/src/toolchain.ts b/tools/vscode-hmapdev/src/toolchain.ts new file mode 100644 index 0000000..44e3280 --- /dev/null +++ b/tools/vscode-hmapdev/src/toolchain.ts @@ -0,0 +1,139 @@ +import * as cp from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { parseSdkList, parseToolchainVersion } from "./core"; + +/** execFile 的 Promise 版(不引第三方依赖)。 */ +function execFile( + file: string, + args: string[], + cwd: string, + timeoutMs = 120_000 +): Promise<{ code: number; stdout: string; stderr: string }> { + return new Promise((resolve) => { + cp.execFile(file, args, { cwd, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => { + const code = err && typeof (err as { code?: number }).code === "number" ? (err as { code: number }).code : err ? 1 : 0; + resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" }); + }); + }); +} + +/** + * 工具链封装:定位 hmapdev、执行命令、缓存 version / sdk list。 + * + * 为什么要缓存并显式 refresh:SDK 存储会在外部变化(`hmapdev sdk install` 之后), + * 而诊断信息依赖它——不刷新就会一直报「未安装」。 + */ +export class Toolchain { + private versionCache?: string; + private sdkCache?: string[]; + private missing = false; + + constructor(private readonly out: vscode.OutputChannel) {} + + private get exe(): string { + return vscode.workspace.getConfiguration("hmapdev").get("path", "hmapdev") || "hmapdev"; + } + + /** 记录一条消息到输出通道(加上工具链前缀,便于与构建输出区分)。 */ + log(line: string): void { + this.out.appendLine(line); + } + + async version(refresh = false): Promise { + if (this.versionCache !== undefined && !refresh) { + return this.versionCache; + } + const r = await execFile(this.exe, ["version"], process.cwd(), 20_000); + if (r.code !== 0 && !r.stdout) { + this.missing = true; + this.versionCache = ""; + return ""; + } + this.missing = false; + this.versionCache = parseToolchainVersion(r.stdout + r.stderr); + return this.versionCache; + } + + async sdkList(refresh = false): Promise { + if (this.sdkCache !== undefined && !refresh) { + return this.sdkCache; + } + const r = await execFile(this.exe, ["sdk", "list"], process.cwd(), 20_000); + if (r.code !== 0 && !r.stdout) { + this.sdkCache = undefined; // 探测不到就不做「未安装」判断,避免误报 + return undefined; + } + this.sdkCache = parseSdkList(r.stdout + r.stderr); + return this.sdkCache; + } + + isMissing(): boolean { + return this.missing; + } + + /** 在集成终端里执行(构建/运行这类长命令:要能看进度、能 Ctrl-C)。 */ + runInTerminal(args: string[], cwd: string, name: string): vscode.Terminal { + const term = vscode.window.createTerminal({ name, cwd }); + term.show(true); + const cmd = [this.exe, ...args].map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(" "); + this.log(`$ ${cmd}`); + term.sendText(cmd, true); + return term; + } + + /** 一次性执行并把输出写进输出通道(查询类命令)。 */ + async runAndLog(args: string[], cwd: string): Promise { + this.log(`$ ${this.exe} ${args.join(" ")}`); + const r = await execFile(this.exe, args, cwd, 60_000); + if (r.stdout) { + this.out.append(r.stdout); + } + if (r.stderr) { + this.out.append(r.stderr); + } + return r.code; + } + + /** + * 跟随内核日志:定位 /log 下最新的 homed 日志并按插件名过滤。 + * + * 为什么这是「调试插件」的正路:插件是子进程、跑在内核里,真正的问题几乎都 + * 表现为内核日志里的几行(握手失败/崩溃重启/工具报错),在 IDE 里跟住它比 + * 反复手动 tail 高效得多。 + */ + tailKernelLog(dataDir: string, filter: string): cp.ChildProcess | undefined { + const logDir = path.join(dataDir, "log"); + if (!fs.existsSync(logDir)) { + this.log(`error: 日志目录不存在:${logDir}(hmapdev.kernelDataDir 是否指对?)`); + return undefined; + } + const newest = fs + .readdirSync(logDir) + .filter((f) => f.startsWith("homed_") && f.endsWith(".log")) + .map((f) => ({ f, m: fs.statSync(path.join(logDir, f)).mtimeMs })) + .sort((a, b) => b.m - a.m)[0]; + if (!newest) { + this.log(`error: ${logDir} 下没有 homed_*.log`); + return undefined; + } + const file = path.join(logDir, newest.f); + this.log(`跟随 ${file}${filter ? `(过滤 ${filter})` : ""}`); + const child = cp.spawn("tail", ["-F", file], { stdio: ["ignore", "pipe", "pipe"] }); + const emit = (buf: Buffer) => { + for (const line of buf.toString("utf8").split(/\r?\n/)) { + if (!line) { + continue; + } + if (!filter || line.includes(filter)) { + this.out.appendLine(line); + } + } + }; + child.stdout?.on("data", emit); + child.stderr?.on("data", emit); + return child; + } +} diff --git a/tools/vscode-hmapdev/tsconfig.json b/tools/vscode-hmapdev/tsconfig.json new file mode 100644 index 0000000..e12880c --- /dev/null +++ b/tools/vscode-hmapdev/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES2020"], + "outDir": "out", + "rootDir": "src", + "strict": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node", "vscode"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "out"] +}