Files
homeagent-sdk/tools/vscode-hmapdev/src/core.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

175 lines
6.4 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 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<string, string>;
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}`;
}