Files
homeagent-sdk/tools/vscode-hmapdev/src/extension.ts
JianFeeeee fcb7490f63 feat(vscode): 插件工程调试扩展(plg.json 诊断 / SDK 版本 / 构建运行 / 内核日志跟随)
插件的真实形态是「独立子进程 + 内核侧握手」,所以插件问题几乎都在 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);
  - **内核日志跟随**:读 `<dataDir>/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` 字段语义(含「为什么必须有」与「为什么拒区间写法」)、
本扩展的用法与边界。
2026-09-12 16:07:34 +08:00

273 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<vscode.Uri | undefined> {
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<PlgConfig | undefined> {
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<void> {
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<boolean>("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<string | undefined> {
const uri = await findPlg();
return uri ? path.dirname(uri.fsPath) : undefined;
}
async function withDir(fn: (dir: string) => unknown | Promise<unknown>): Promise<void> {
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<string>("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<string>("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;
}