mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-21 17:38:03 +00:00
Compare commits
5 Commits
v1.2.0
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d1e899b18 | |||
| f6d9fee2da | |||
| 1df184d47e | |||
| ff4484e193 | |||
| d011d0676a |
51
README.md
51
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;
|
||||
- **跟随内核日志**:读 `<dataDir>/log` 下最新的 `homed_*.log` 并按插件名过滤。
|
||||
|
||||
```bash
|
||||
cd tools/vscode-hmapdev && npm install && npm run compile # 然后在 VSCode 里按 F5
|
||||
```
|
||||
|
||||
它不是源码级调试器(没有断点/单步):插件要么编译成产物在内核里跑、要么用
|
||||
`hmapdev debug` 解释执行,两条路都没有 DAP 会话;扩展做的是构建、运行、看日志与清单校验。
|
||||
|
||||
## 示例插件
|
||||
|
||||
| 插件 | 类型 | 说明 |
|
||||
|
||||
@ -79,6 +79,22 @@ func cmdBuild(args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
// 项目可在 plg.json 里声明 sdk(中版本或完整版本,如 "1.2" / "1.2.1");
|
||||
// 显式 --sdk-path / plg.json 的 sdk_path 优先 —— 那是直指源码目录,
|
||||
// 常用于本机改 SDK 的联调场景。
|
||||
if sdkPath == "" && strings.TrimSpace(plg.SDK) != "" {
|
||||
dir, ver, err := ResolveSDKForProject(plg.SDK)
|
||||
if err != nil {
|
||||
fmt.Printf("error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sdkPath, plg.ResolvedSDK = dir, ver
|
||||
fmt.Printf("[hmapdev] SDK %s(项目声明 sdk=%s)\n", ver, plg.SDK)
|
||||
} else if sdkPath != "" && plg.ResolvedSDK == "" {
|
||||
// 走的是显式路径:尽力记录它是哪版(读不到就不记,不因此失败)
|
||||
plg.ResolvedSDK = normalizeSDKVersion(readMetaVersion(sdkPath))
|
||||
}
|
||||
|
||||
// Ensure go.mod exists with correct SDK path
|
||||
sdkModule := ensureGoMod(plg, sdkPath)
|
||||
|
||||
@ -269,6 +285,10 @@ func writePluginJSON(plg *PlgConfig, platforms []string, entry string) {
|
||||
if len(plg.Tags) > 0 {
|
||||
m["tags"] = plg.Tags
|
||||
}
|
||||
// 记录「用哪版 SDK 编的」:插件产物与内核协议绑定,出问题时这是第一个要看的字段。
|
||||
if plg.ResolvedSDK != "" {
|
||||
m["sdk"] = plg.ResolvedSDK
|
||||
}
|
||||
data, _ := json.MarshalIndent(m, "", " ")
|
||||
os.WriteFile("plugin.json", data, 0644)
|
||||
}
|
||||
@ -370,7 +390,28 @@ func ensureGoMod(plg *PlgConfig, sdkPath string) string {
|
||||
}
|
||||
keep = append(keep, line)
|
||||
}
|
||||
if alreadyExists {
|
||||
// 同步 require 版本:replace 指向 1.2.1 而 require 还写 1.2.0 是自相矛盾的
|
||||
// —— 有人删掉 replace 就会静默退回旧版本去编(`go list -m` 报的也是假版本)。
|
||||
// 以本次真正选中的版本为准改写 require 行。
|
||||
requireChanged := false
|
||||
if v := normalizeSDKVersion(plg.ResolvedSDK); v != "" {
|
||||
want := "require " + sdkModule + " v" + v
|
||||
for i, line := range keep {
|
||||
t := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(t, "require ") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(t)
|
||||
if len(parts) >= 3 && parts[1] == sdkModule {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
if t != want {
|
||||
keep[i] = indent + want
|
||||
requireChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if alreadyExists && !requireChanged {
|
||||
return sdkModule
|
||||
}
|
||||
keep = append(keep, replaceLine, "")
|
||||
|
||||
@ -34,6 +34,18 @@ type PlgConfig struct {
|
||||
GoVersion string `json:"go_version,omitempty"`
|
||||
Replaces map[string]string `json:"replaces,omitempty"`
|
||||
SourceDirs []string `json:"source_dirs,omitempty"`
|
||||
|
||||
// SDK 声明本插件针对的 SDK **接口版本**(中版本或完整版本,如 "1.2" / "1.2.1")。
|
||||
//
|
||||
// 为何需要:工具链存储里可能装有多个 SDK 版本,而插件产物与内核是协议绑定的——
|
||||
// 不给声明就只能猜(旧行为是直接用 current:谁改过 current 就拿谁的版本编,
|
||||
// 出错时表现为莫名其妙的编译错误)。写中版本表示「只要 1.2 这条接口线,
|
||||
// 补丁由工具链挑最新」(patch 只含工具链/打包修复,接口不变,见 README 版本语义)。
|
||||
SDK string `json:"sdk,omitempty"`
|
||||
|
||||
// ResolvedSDK 是本次构建实际选中的 SDK 版本(build 按 SDK 声明解析后回填),
|
||||
// 只写进产物里的 plugin.json,便于事后追溯「这个 .hmap 是哪版 SDK 编的」。
|
||||
ResolvedSDK string `json:"-"`
|
||||
}
|
||||
|
||||
// TargetList parses the Targets string into a slice.
|
||||
@ -159,6 +171,9 @@ func cmdInit(args []string) {
|
||||
data.SDKModule = sdkMod
|
||||
data.SDKVersion = "v" + sdkVer
|
||||
data.SDKLocalPath = strings.ReplaceAll(sdkRoot, "\\", "/")
|
||||
// 声明**完整版本号**:SDK 版本跟随内核中版本、patch 位恒为 .0,
|
||||
// 一条内核线只对应一个 SDK 版本(build 时按此解析,见 ResolveSDKForProject)。
|
||||
data.Plg.SDK = normalizeSDKVersion(sdkVer)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
|
||||
@ -2,7 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/meta"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -21,15 +25,42 @@ func main() {
|
||||
cmdDebug(os.Args[2:])
|
||||
case "sdk":
|
||||
cmdSDK(os.Args[2:])
|
||||
case "version", "-v", "--version":
|
||||
printVersion()
|
||||
default:
|
||||
help()
|
||||
}
|
||||
}
|
||||
|
||||
// printVersion 输出工具链自身的版本身份。
|
||||
//
|
||||
// 为何必须有:此前工具链不报版本,而插件产物与内核是**协议绑定**的——
|
||||
// 手里是哪一版工具链、能不能配当前内核,只能靠翻文件名或猜。
|
||||
// 版本号来自 meta.Version(与 SDK 发布同源,由 -ldflags -X 注入);
|
||||
// lnflags 未注入时它是源码里的默认值,此时提示它可能是开发构建。
|
||||
func printVersion() {
|
||||
printVersionTo(os.Stdout)
|
||||
}
|
||||
|
||||
// printVersionTo 把版本身份写到 w(抽出来是为了能被测试钉住)。
|
||||
func printVersionTo(w io.Writer) {
|
||||
fmt.Fprintf(w, "hmapdev %s\n", meta.Version)
|
||||
fmt.Fprintf(w, " SDK 模块: %s\n", "gitcode.com/JianFeeeee/homeagent-sdk")
|
||||
if meta.Commit != "" && meta.Commit != "unknown" {
|
||||
fmt.Fprintf(w, " 构建提交: %s\n", meta.Commit)
|
||||
}
|
||||
if meta.BuildTime != "" && meta.BuildTime != "unknown" {
|
||||
fmt.Fprintf(w, " 构建时间: %s\n", meta.BuildTime)
|
||||
}
|
||||
fmt.Fprintf(w, " 构建用 Go: %s\n", runtime.Version())
|
||||
fmt.Fprintf(w, " 可执行文件: %s\n", os.Args[0])
|
||||
}
|
||||
|
||||
func help() {
|
||||
fmt.Print(`HomeAgent Plugin Dev Tool
|
||||
|
||||
Usage:
|
||||
hmapdev version Print toolchain version
|
||||
hmapdev init <name> Scaffold a new plugin project
|
||||
hmapdev init <name> --lua Create Lua plugin
|
||||
hmapdev init <name> --type remotedevice
|
||||
|
||||
57
tools/hmapdev/main_version_test.go
Normal file
57
tools/hmapdev/main_version_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/meta"
|
||||
)
|
||||
|
||||
// 工具链必须能报出自己的版本。
|
||||
//
|
||||
// 为什么值得钉住:插件产物与内核是协议绑定的,「手里是哪一版工具链」直接决定
|
||||
// 产物能不能建链;此前既没有 version 子命令,`-ldflags -X meta.Version` 也因为
|
||||
// meta 包没被链接而**静默无效**(表现为报不出任何版本)。
|
||||
func TestPrintVersionReportsInjectedVersion(t *testing.T) {
|
||||
origVersion, origCommit := meta.Version, meta.Commit
|
||||
defer func() { meta.Version, meta.Commit = origVersion, origCommit }()
|
||||
|
||||
// 模拟 -ldflags 注入后的取值
|
||||
meta.Version = "9.9.9"
|
||||
meta.Commit = "deadbee"
|
||||
|
||||
var buf bytes.Buffer
|
||||
printVersionTo(&buf)
|
||||
out := buf.String()
|
||||
|
||||
if !strings.Contains(out, "9.9.9") {
|
||||
t.Fatalf("版本号未出现在输出里(-X 注入会失效):\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "hmapdev") {
|
||||
t.Fatalf("输出里没有工具名:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "deadbee") {
|
||||
t.Fatalf("提交号未出现在输出里:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "HomeAgent") && !strings.Contains(out, "homeagent-sdk") {
|
||||
t.Fatalf("输出里没有 SDK 模块标识:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// 未注入时(源码默认值)也必须能报——否则开发构建和发版构建长得一样。
|
||||
func TestPrintVersionWorksWithoutInjection(t *testing.T) {
|
||||
origCommit, origBuildTime := meta.Commit, meta.BuildTime
|
||||
defer func() { meta.Commit, meta.BuildTime = origCommit, origBuildTime }()
|
||||
meta.Commit, meta.BuildTime = "unknown", "unknown"
|
||||
|
||||
var buf bytes.Buffer
|
||||
printVersionTo(&buf)
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, meta.Version) {
|
||||
t.Fatalf("未注入时应报出源码默认版本 %q:\n%s", meta.Version, out)
|
||||
}
|
||||
if strings.Contains(out, "unknown") {
|
||||
t.Fatalf("unknown 字段不应出现在输出里(噪声):\n%s", out)
|
||||
}
|
||||
}
|
||||
135
tools/hmapdev/sdk_resolve.go
Normal file
135
tools/hmapdev/sdk_resolve.go
Normal file
@ -0,0 +1,135 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sdkVersionEntry 是本地 SDK 存储里的一个版本。
|
||||
//
|
||||
// Dir 是存储目录名(历史上有 "v0.8.0" 与 "1.2.0" 两种写法都出现过,所以
|
||||
// 目录名与规范化版本号要分开存),Version 是去掉 v 前缀的 x.y.z。
|
||||
type sdkVersionEntry struct {
|
||||
Dir string
|
||||
Version string
|
||||
}
|
||||
|
||||
// normalizeSDKVersion 去掉常见的前缀写法,得到 x.y.z。
|
||||
func normalizeSDKVersion(v string) string {
|
||||
return strings.TrimPrefix(strings.TrimSpace(v), "v")
|
||||
}
|
||||
|
||||
// parseSDKVersion 解析 x.y.z / x.y(后者补 0)。
|
||||
func parseSDKVersion(v string) (maj, min, patch int, ok bool) {
|
||||
v = normalizeSDKVersion(v)
|
||||
parts := strings.Split(v, ".")
|
||||
if len(parts) < 2 || len(parts) > 3 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
nums := make([]int, 0, 3)
|
||||
for _, p := range parts {
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil || n < 0 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
nums = append(nums, n)
|
||||
}
|
||||
for len(nums) < 3 {
|
||||
nums = append(nums, 0)
|
||||
}
|
||||
return nums[0], nums[1], nums[2], true
|
||||
}
|
||||
|
||||
// compareSDKVersion 比较两个 x.y.z(a<b 返回 -1,相等 0,a>b 返回 1)。
|
||||
func compareSDKVersion(a, b string) int {
|
||||
amaj, amin, apat, aok := parseSDKVersion(a)
|
||||
bmaj, bmin, bpat, bok := parseSDKVersion(b)
|
||||
if !aok || !bok {
|
||||
return strings.Compare(normalizeSDKVersion(a), normalizeSDKVersion(b))
|
||||
}
|
||||
for _, d := range [][2]int{{amaj, bmaj}, {amin, bmin}, {apat, bpat}} {
|
||||
switch {
|
||||
case d[0] < d[1]:
|
||||
return -1
|
||||
case d[0] > d[1]:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// listInstalledSDKs 列出存储里已安装的 SDK,按版本升序。
|
||||
func listInstalledSDKs() []sdkVersionEntry {
|
||||
store := sdkStore()
|
||||
entries, err := os.ReadDir(store)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []sdkVersionEntry
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if name == "current" || strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
v := normalizeSDKVersion(name)
|
||||
if _, _, _, ok := parseSDKVersion(v); !ok {
|
||||
continue // 非版本目录(用户放别的东西进去时不误判)
|
||||
}
|
||||
out = append(out, sdkVersionEntry{Dir: name, Version: v})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return compareSDKVersion(out[i].Version, out[j].Version) < 0 })
|
||||
return out
|
||||
}
|
||||
|
||||
// ResolveSDKForProject 按项目声明的 SDK 版本(plg.json 的 "sdk" 字段)在本地存储里定位 SDK。
|
||||
//
|
||||
// 声明必须是**完整版本号**(x.y.z,如 "1.2.0"):SDK 的版本纪律是「跟随内核中版本,
|
||||
// patch 位恒为 .0」(内核的 patch 不碰公开接口 → SDK 不跟版),所以一条内核线
|
||||
// 只对应一个 SDK 版本号,写 "1.2" 这种区间写法既不必要、又容易让人以为
|
||||
// 「同一条线里还能挑不同 SDK」。工具链直接拒它,顺便把这条规矩说清楚。
|
||||
//
|
||||
// 找不到时必须报**可执行**的错误:列出已装版本 + 可直接粘贴的安装命令 ——
|
||||
// 只说 "not found" 会让人以为是工具链坏了。
|
||||
func ResolveSDKForProject(declared string) (dir, version string, err error) {
|
||||
declared = normalizeSDKVersion(declared)
|
||||
if strings.Count(declared, ".") != 2 {
|
||||
return "", "", fmt.Errorf(
|
||||
"plg.json 的 sdk 字段 %q 必须是完整版本号(如 \"1.2.0\")——\n"+
|
||||
" SDK 版本跟随内核中版本、patch 位恒为 .0,一条内核线只有一个 SDK 版本", declared)
|
||||
}
|
||||
maj, min, pat, ok := parseSDKVersion(declared)
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("plg.json 的 sdk 字段 %q 不是合法版本号(写法:\"1.2.0\")", declared)
|
||||
}
|
||||
|
||||
installed := listInstalledSDKs()
|
||||
for i := range installed {
|
||||
e := installed[i]
|
||||
emaj, emin, epat, _ := parseSDKVersion(e.Version)
|
||||
if emaj == maj && emin == min && epat == pat {
|
||||
return filepath.Join(sdkStore(), e.Dir), e.Version, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 未命中:给出可执行的下一步
|
||||
var have []string
|
||||
for _, e := range installed {
|
||||
have = append(have, e.Version)
|
||||
}
|
||||
avail := "(存储里还没有任何 SDK)"
|
||||
if len(have) > 0 {
|
||||
avail = "已安装:" + strings.Join(have, ", ")
|
||||
}
|
||||
return "", "", fmt.Errorf(
|
||||
"项目声明 sdk=%s,但本地 SDK 存储里没有这个版本;%s\n"+
|
||||
" 安装:hmapdev sdk install v%s\n"+
|
||||
" 查看:hmapdev sdk list",
|
||||
declared, avail, declared)
|
||||
}
|
||||
103
tools/hmapdev/sdk_resolve_test.go
Normal file
103
tools/hmapdev/sdk_resolve_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// withSDKStore 把 SDK 存储指到临时目录(sdkStore 读 HOME),并造出给定版本目录。
|
||||
func withSDKStore(t *testing.T, versions ...string) {
|
||||
t.Helper()
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
store := filepath.Join(home, ".homeagent", sdkDirName)
|
||||
if err := os.MkdirAll(store, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, v := range versions {
|
||||
if err := os.MkdirAll(filepath.Join(store, v), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 项目声明中版本 → 挑该接口线里最新的补丁;声明完整版本 → 精确命中。
|
||||
//
|
||||
// 为什么允许中版本是关键判据:patch 位只含工具链/打包修复(接口不变),
|
||||
// 让项目声明 "1.2" 而不是死钉 "1.2.0",才能既跟得上工具链修复又不跨接口线。
|
||||
func TestResolveSDKForProject(t *testing.T) {
|
||||
t.Run("区间写法(1.2)被拒并说明版本纪律", func(t *testing.T) {
|
||||
// 判据:SDK 的 patch 位恒为 .0 → 一条内核线只有一个 SDK 版本,
|
||||
// 区间写法会让人误以为「同一条线里还能挑版本」,所以直接拒。
|
||||
withSDKStore(t, "1.2.0")
|
||||
_, _, err := ResolveSDKForProject("1.2")
|
||||
if err == nil {
|
||||
t.Fatal("1.2 这种区间写法应被拒绝")
|
||||
}
|
||||
for _, want := range []string{"完整版本号", "patch 位恒为 .0"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("拒绝理由里应说清规矩(缺少 %q): %v", want, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("完整版本精确命中", func(t *testing.T) {
|
||||
withSDKStore(t, "1.2.0", "1.3.0")
|
||||
_, ver, err := ResolveSDKForProject("1.2.0")
|
||||
if err != nil || ver != "1.2.0" {
|
||||
t.Fatalf("精确命中失败: ver=%s err=%v", ver, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("存储目录带 v 前缀也能命中", func(t *testing.T) {
|
||||
withSDKStore(t, "v1.2.0")
|
||||
dir, ver, err := ResolveSDKForProject("1.2.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ver != "1.2.0" || !strings.HasSuffix(dir, "v1.2.0") {
|
||||
t.Fatalf("带 v 前缀的目录名未被识别: dir=%s ver=%s", dir, ver)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("未命中要给出可执行命令与已装清单", func(t *testing.T) {
|
||||
withSDKStore(t, "1.2.0")
|
||||
_, _, err := ResolveSDKForProject("2.0.0")
|
||||
if err == nil {
|
||||
t.Fatal("应报错")
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, want := range []string{"sdk=2.0.0", "hmapdev sdk install", "hmapdev sdk list", "1.2.0"} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Fatalf("错误信息缺少 %q(要可执行,不能只说 not found):\n%s", want, msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("空存储也能给出安装指引", func(t *testing.T) {
|
||||
withSDKStore(t)
|
||||
_, _, err := ResolveSDKForProject("1.2.0")
|
||||
if err == nil || !strings.Contains(err.Error(), "hmapdev sdk install v1.2.0") {
|
||||
t.Fatalf("空存储时应提示装哪个版本: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("非法声明直接拒绝", func(t *testing.T) {
|
||||
withSDKStore(t, "1.2.0")
|
||||
for _, bad := range []string{"abc", "1", "1.2", "1.2.3.4", "-1.2.0"} {
|
||||
if _, _, err := ResolveSDKForProject(bad); err == nil {
|
||||
t.Fatalf("非法版本 %q 应被拒绝(宁可报错也不许当通配符)", bad)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("非版本目录不参与匹配", func(t *testing.T) {
|
||||
withSDKStore(t, "1.2.0", "backup-old", ".hidden", "current")
|
||||
_, ver, err := ResolveSDKForProject("1.2.0")
|
||||
if err != nil || ver != "1.2.0" {
|
||||
t.Fatalf("杂项目录不应干扰: ver=%s err=%v", ver, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -9,6 +9,7 @@ const tmplPlgJSON = `{
|
||||
"description": "{{.Plg.Description}}",
|
||||
"author": "{{.Plg.Author}}",
|
||||
"entry": "{{.Plg.Entry}}",
|
||||
"sdk": "{{.Plg.SDK}}",
|
||||
"tags": [{{range $i, $t := .Plg.Tags}}{{if $i}}, {{end}}"{{$t}}"{{end}}],
|
||||
"targets": "{{.Plg.Targets}}"
|
||||
}
|
||||
|
||||
3
tools/vscode-hmapdev/.gitignore
vendored
Normal file
3
tools/vscode-hmapdev/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
out/
|
||||
*.vsix
|
||||
5
tools/vscode-hmapdev/.vscodeignore
Normal file
5
tools/vscode-hmapdev/.vscodeignore
Normal file
@ -0,0 +1,5 @@
|
||||
src/
|
||||
tsconfig.json
|
||||
node_modules/
|
||||
out/test/
|
||||
.gitignore
|
||||
69
tools/vscode-hmapdev/README.md
Normal file
69
tools/vscode-hmapdev/README.md
Normal file
@ -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 面板 |
|
||||
| **跟随内核日志** | 读 `<dataDir>/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`。
|
||||
59
tools/vscode-hmapdev/package-lock.json
generated
Normal file
59
tools/vscode-hmapdev/package-lock.json
generated
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
174
tools/vscode-hmapdev/package.json
Normal file
174
tools/vscode-hmapdev/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
41
tools/vscode-hmapdev/schema/plg.schema.json
Normal file
41
tools/vscode-hmapdev/schema/plg.schema.json
Normal file
@ -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
|
||||
}
|
||||
26
tools/vscode-hmapdev/snippets/plg.json.code-snippets
Normal file
26
tools/vscode-hmapdev/snippets/plg.json.code-snippets
Normal file
@ -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)"
|
||||
}
|
||||
}
|
||||
174
tools/vscode-hmapdev/src/core.ts
Normal file
174
tools/vscode-hmapdev/src/core.ts
Normal file
@ -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<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}`;
|
||||
}
|
||||
272
tools/vscode-hmapdev/src/extension.ts
Normal file
272
tools/vscode-hmapdev/src/extension.ts
Normal file
@ -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<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;
|
||||
}
|
||||
106
tools/vscode-hmapdev/src/test/core.test.ts
Normal file
106
tools/vscode-hmapdev/src/test/core.test.ts
Normal file
@ -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 <name>"), "");
|
||||
});
|
||||
|
||||
// 反向核对抓到的真缺陷:`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 未找到");
|
||||
});
|
||||
139
tools/vscode-hmapdev/src/toolchain.ts
Normal file
139
tools/vscode-hmapdev/src/toolchain.ts
Normal file
@ -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<string>("path", "hmapdev") || "hmapdev";
|
||||
}
|
||||
|
||||
/** 记录一条消息到输出通道(加上工具链前缀,便于与构建输出区分)。 */
|
||||
log(line: string): void {
|
||||
this.out.appendLine(line);
|
||||
}
|
||||
|
||||
async version(refresh = false): Promise<string> {
|
||||
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<string[] | undefined> {
|
||||
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<number> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟随内核日志:定位 <dataDir>/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;
|
||||
}
|
||||
}
|
||||
19
tools/vscode-hmapdev/tsconfig.json
Normal file
19
tools/vscode-hmapdev/tsconfig.json
Normal file
@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user