mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 17:08:01 +00:00
feat(example): plugindev 插件 —— 把 hmapdev 工具链封装成 Agent 可调用的工具
用户要求:把 SDK 的 hmapdev 额外封装为 HomeAgent 插件并装上。
## 为什么
hmapdev 原本是"给人/CI 用"的命令行。做成插件后,Agent 能自己完成
「新建插件 → 构建 → 安装 → 重载」全流程(配合已有的 plugin_install / plgreload):
plugindev_init → plugindev_build → plugin_install(path) → plgreload
## 工具面(5 个)
`plugindev_status`(可用性/版本/当前 SDK/工作区,排障首选)、
`plugindev_init`(脚手架,插件名约束 `[a-zA-Z0-9_-]{1,64}`)、
`plugindev_build`(在工程目录构建打包,返回产物路径与下一步提示)、
`plugindev_sdk`(SDK 版本 list/current/path/latest/install/use,`from` 支持本地源码)、
`plugindev_projects`(列出工作区已有工程与产物)。
## 安全边界(实现里落实)
- 只 exec **hmapdev 一个可执行文件**,不做 shell 拼接;
- `plugindev_build` 只接受含 `plg.json` 的目录 —— 这个工具不会变成"对任意目录跑构建";
- 子进程全部带超时;输出截断(6000 字符,保留首尾)后才返回,避免几百 KB 构建日志灌爆模型上下文;
- 工作区默认落在 `<data_dir>/plugindev`,配置可改。
## 现场验证
已在本机生产装上(`plugin_install` 带 path):5 个工具注册成功、
启动日志 `[plugindev] 就绪:hmapdev=/usr/local/bin/hmapdev 工作区=/home/newqqagent/plugindev`,
`hmapdev` 已装到 `/usr/local/bin/hmapdev`(版本 1.3.0)。
This commit is contained in:
47
example/plugindev/README.md
Normal file
47
example/plugindev/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# plugindev — 插件开发工具链(Agent 可调用)
|
||||
|
||||
把 SDK 的 `hmapdev` 封装成插件,让 **Agent 自己**走完「新建插件 → 构建 → 安装」全流程,
|
||||
不需要人来敲命令行:
|
||||
|
||||
```
|
||||
plugindev_init 生成工程骨架(等价 hmapdev init <name> [--lua])
|
||||
↓ 改 plugin.go
|
||||
plugindev_build 构建打包(等价在该目录 hmapdev build)→ dist/*.hmap
|
||||
↓
|
||||
plugin_install 安装(用 path 指向刚构建出的 .hmap,overwrite=true 表示原地更新)
|
||||
↓
|
||||
plgreload 重载生效
|
||||
```
|
||||
|
||||
## 工具
|
||||
|
||||
| 工具 | 参数 | 说明 |
|
||||
|---|---|---|
|
||||
| `plugindev_status` | — | hmapdev 是否可用/版本/当前 SDK 版本与路径/工作区;**排查"为什么不能构建"先用它** |
|
||||
| `plugindev_init` | `name`、`lang`(go/lua)、`dir` | 生成工程骨架;插件名必须 `[a-zA-Z0-9_-]{1,64}` |
|
||||
| `plugindev_build` | `dir`、`target` | 在工程目录构建打包;产物路径会在返回里给出 |
|
||||
| `plugindev_sdk` | `action`、`version`、`from` | SDK 版本管理(list/current/path/latest/install/use);`from` 可指向本地 SDK 源码 |
|
||||
| `plugindev_projects` | — | 列出工作区里已有工程与产物 |
|
||||
|
||||
## 配置
|
||||
|
||||
| 键 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `hmapdev_path` | 自动查找 | 依次尝试:本配置项 → PATH → `/usr/local/bin/hmapdev` → `/root/go/bin/hmapdev` |
|
||||
| `workspace_dir` | `<data_dir>/plugindev` | `plugindev_init` 生成工程的默认目录 |
|
||||
| `build_timeout_sec` | 600 | 单次 hmapdev 调用超时 |
|
||||
|
||||
## 前置:装 hmapdev
|
||||
|
||||
```bash
|
||||
cd <sdk-repo>/tools/hmapdev && go build -buildvcs=false -o /usr/local/bin/hmapdev .
|
||||
hmapdev version
|
||||
```
|
||||
|
||||
## 安全边界(都在实现里,不只写在文档里)
|
||||
|
||||
- 只 exec **hmapdev 一个可执行文件**,不接受任意命令、不做 shell 拼接;
|
||||
- `plugindev_build` 只接受含 `plg.json` 的目录("看起来是插件工程"才构建),
|
||||
避免把这个工具变成对任意目录跑构建;
|
||||
- 子进程全部带超时,输出**截断**后才返回(构建日志动辄几百 KB,直接回灌会撑爆模型上下文);
|
||||
- 工程名约束与内核/上游对"进工具名的标识符"的规则一致(`[a-zA-Z0-9_-]{1,64}`)。
|
||||
7
example/plugindev/go.mod
Normal file
7
example/plugindev/go.mod
Normal file
@ -0,0 +1,7 @@
|
||||
module plugindev
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitcode.com/JianFeeeee/homeagent-sdk v0.0.0
|
||||
|
||||
replace gitcode.com/JianFeeeee/homeagent-sdk => ../../
|
||||
11
example/plugindev/main.go
Normal file
11
example/plugindev/main.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build !windows || !cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return NewPluginFactory(name, config)
|
||||
}
|
||||
19
example/plugindev/plg.json
Normal file
19
example/plugindev/plg.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "plugindev",
|
||||
"name_zh": "插件开发工具链",
|
||||
"name_en": "Plugin Dev Toolchain",
|
||||
"version": "1.0.0",
|
||||
"description": "把 hmapdev 工具链封装成 Agent 可调用的工具:脚手架生成插件工程、构建打包 .hmap、管理 SDK 版本。配合 plugin_install 即可让 Agent 自己做完「新建插件 → 构建 → 安装」全流程。",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": [
|
||||
"plugindev",
|
||||
"toolchain",
|
||||
"developer"
|
||||
],
|
||||
"targets": "linux/amd64",
|
||||
"outdir": "dist",
|
||||
"bundle": true,
|
||||
"replaces": {},
|
||||
"source_dirs": []
|
||||
}
|
||||
455
example/plugindev/plugin.go
Normal file
455
example/plugindev/plugin.go
Normal file
@ -0,0 +1,455 @@
|
||||
package main
|
||||
|
||||
// plugindev:把 SDK 的 hmapdev 工具链封装成 Agent 可调用的插件。
|
||||
//
|
||||
// 为什么需要它:hmapdev 是"给人和 CI 用"的命令行工具。做成插件后,Agent 能自己:
|
||||
// plugindev_init(脚手架)→ plugindev_build(构建出 .hmap)→ plugin_install(安装)→ plgreload
|
||||
// 也就是"让 Agent 自己写/改/装插件"这条链不需要人来敲命令。
|
||||
//
|
||||
// 安全边界(都在实现里落实,不只写在描述里):
|
||||
// - 只有 **hmapdev 一个可执行文件**会被 exec(不接受任意命令/参数拼接);
|
||||
// - `plugindev_build` 只接受"看起来是插件工程"的目录(含 plg.json),
|
||||
// 避免把一个 `hmapdev build` 变成对任意目录的操作;
|
||||
// - `plugindev_init` 生成的工程名必须满足 `[a-zA-Z0-9_-]{1,64}`(与 LLM 函数名
|
||||
// 同一套约束 —— 插件名会进 `output_send__<通道>` 之类的工具名);
|
||||
// - 所有子进程都有超时,输出截断后再返回(防止把几十 MB 构建日志灌进模型上下文)。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBuildTimeout = 10 * time.Minute
|
||||
maxOutputChars = 6000
|
||||
)
|
||||
|
||||
// namePattern 与内核/上游对"会进工具名的标识符"的约束一致。
|
||||
var namePattern = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`)
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
|
||||
hmapdev string // 解析到的 hmapdev 可执行文件路径
|
||||
workspace string // 默认工作区(生成的工程落在这里)
|
||||
timeout time.Duration // 单次 hmapdev 调用的超时
|
||||
}
|
||||
|
||||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
s.SetAutoRestart(true)
|
||||
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "hmapdev_path", Type: "string", DisplayName: "hmapdev 路径",
|
||||
Description: "插件开发工具链可执行文件路径。留空则按 PATH → /usr/local/bin/hmapdev → /root/go/bin/hmapdev 查找",
|
||||
Category: p.name,
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "workspace_dir", Type: "string", DisplayName: "工程工作区",
|
||||
Description: "plugindev_init 生成工程的默认目录。留空则用 <data_dir>/plugindev",
|
||||
Category: p.name,
|
||||
})
|
||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||
Key: "build_timeout_sec", Default: 600, Type: "int", DisplayName: "构建超时(秒)",
|
||||
Description: "单次 hmapdev 调用的超时上限",
|
||||
Category: p.name,
|
||||
})
|
||||
|
||||
p.hmapdev = p.resolveHmapdev()
|
||||
p.workspace = p.resolveWorkspace()
|
||||
p.timeout = defaultBuildTimeout
|
||||
if v, _ := s.Settings().Get("build_timeout_sec"); v != nil {
|
||||
if n, ok := toInt(v); ok && n > 0 {
|
||||
p.timeout = time.Duration(n) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
s.RegisterTool("plugindev_status", sdk.ToolDef{
|
||||
Name: "plugindev_status",
|
||||
Description: "查看插件开发工具链状态:hmapdev 是否可用、版本、当前 SDK 版本与路径、工程工作区目录。排查\"为什么不能构建插件\"时先用它。",
|
||||
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||
}, p.handleStatus)
|
||||
|
||||
s.RegisterTool("plugindev_init", sdk.ToolDef{
|
||||
Name: "plugindev_init",
|
||||
Description: "生成一个新的插件工程骨架(等价于 `hmapdev init <name> [--lua]`)。生成后在返回的目录里改 plugin.go,再用 plugindev_build 构建。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "插件名(也是工程目录名):只允许字母数字下划线短横,长度 1-64。例:my_plugin",
|
||||
},
|
||||
"lang": map[string]interface{}{
|
||||
"type": "string", "description": "go(默认)或 lua",
|
||||
},
|
||||
"dir": map[string]interface{}{
|
||||
"type": "string", "description": "在哪个目录下生成(默认工作区)。必须是已存在的目录",
|
||||
},
|
||||
},
|
||||
"required": []string{"name"},
|
||||
},
|
||||
}, p.handleInit)
|
||||
|
||||
s.RegisterTool("plugindev_build", sdk.ToolDef{
|
||||
Name: "plugindev_build",
|
||||
Description: "构建并打包一个插件工程(等价于在该工程目录里执行 `hmapdev build [target]`),产物是 dist/*.hmap。构建成功后用 plugin_install 安装(本地路径)。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"dir": map[string]interface{}{
|
||||
"type": "string", "description": "插件工程目录(必须含 plg.json)",
|
||||
},
|
||||
"target": map[string]interface{}{
|
||||
"type": "string", "description": "构建目标,留空 = native(当前平台)。例:linux/amd64",
|
||||
},
|
||||
},
|
||||
"required": []string{"dir"},
|
||||
},
|
||||
}, p.handleBuild)
|
||||
|
||||
s.RegisterTool("plugindev_sdk", sdk.ToolDef{
|
||||
Name: "plugindev_sdk",
|
||||
Description: "管理插件 SDK 版本(hmapdev sdk 子命令):list 列出已安装、current 当前版本、path 当前路径、latest 远端最新、install 安装某版本(可用 from 指定本地源码目录)、use 切换版本。构建插件报\"SDK 缺少某能力\"时用它升级 SDK。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"type": "string", "description": "list | current | path | latest | install | use",
|
||||
},
|
||||
"version": map[string]interface{}{
|
||||
"type": "string", "description": "install/use 的版本号,如 v1.3.0",
|
||||
},
|
||||
"from": map[string]interface{}{
|
||||
"type": "string", "description": "install 时用本地 SDK 源码目录(开发中的 SDK 用这个)",
|
||||
},
|
||||
},
|
||||
"required": []string{"action"},
|
||||
},
|
||||
}, p.handleSDK)
|
||||
|
||||
s.RegisterTool("plugindev_projects", sdk.ToolDef{
|
||||
Name: "plugindev_projects",
|
||||
Description: "列出工作区里已有的插件工程(名字、版本、是否已构建出 dist 产物),用于接续之前的开发。",
|
||||
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||
}, p.handleProjects)
|
||||
|
||||
log.Printf("[plugindev] 就绪:hmapdev=%s 工作区=%s", fallback(p.hmapdev, "(未找到)"), p.workspace)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error { return nil }
|
||||
|
||||
// ---------------- 工具实现 ----------------
|
||||
|
||||
func (p *Plugin) handleStatus(args map[string]interface{}) (interface{}, error) {
|
||||
out := map[string]interface{}{
|
||||
"hmapdev": fallback(p.hmapdev, ""),
|
||||
"workspace": p.workspace,
|
||||
}
|
||||
if p.hmapdev == "" {
|
||||
out["available"] = false
|
||||
out["hint"] = "未找到 hmapdev。请安装:go build -o /usr/local/bin/hmapdev <sdk>/tools/hmapdev"
|
||||
return out, nil
|
||||
}
|
||||
out["available"] = true
|
||||
if txt, err := p.run(nil, ""); err == nil {
|
||||
out["version"] = strings.TrimSpace(txt)
|
||||
} else {
|
||||
out["error"] = err.Error()
|
||||
}
|
||||
if txt, err := p.run([]string{"sdk", "current"}, ""); err == nil {
|
||||
out["sdk_current"] = strings.TrimSpace(txt)
|
||||
}
|
||||
if txt, err := p.run([]string{"sdk", "path"}, ""); err == nil {
|
||||
out["sdk_path"] = strings.TrimSpace(txt)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleInit(args map[string]interface{}) (interface{}, error) {
|
||||
name, _ := args["name"].(string)
|
||||
name = strings.TrimSpace(name)
|
||||
if !namePattern.MatchString(name) {
|
||||
return map[string]interface{}{
|
||||
"error": "插件名只允许 [a-zA-Z0-9_-],长度 1-64(它会进 LLM 工具名,违规会让整条请求被上游拒绝)",
|
||||
}, nil
|
||||
}
|
||||
dir, _ := args["dir"].(string)
|
||||
if dir == "" {
|
||||
dir = p.workspace
|
||||
}
|
||||
if st, err := os.Stat(dir); err != nil || !st.IsDir() {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("目录不存在: %s", dir)}, nil
|
||||
}
|
||||
cmd := []string{"init", name}
|
||||
if lang, _ := args["lang"].(string); strings.EqualFold(lang, "lua") {
|
||||
cmd = append(cmd, "--lua")
|
||||
}
|
||||
txt, err := p.run(cmd, dir)
|
||||
res := map[string]interface{}{"output": txt, "project_dir": filepath.Join(dir, name)}
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleBuild(args map[string]interface{}) (interface{}, error) {
|
||||
dir, _ := args["dir"].(string)
|
||||
if dir == "" {
|
||||
return map[string]interface{}{"error": "dir 不能为空"}, nil
|
||||
}
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error()}, nil
|
||||
}
|
||||
// 只在"插件工程"里构建:必须存在 plg.json。这样这个工具不会变成对任意目录跑构建。
|
||||
manifest := filepath.Join(abs, "plg.json")
|
||||
if _, err := os.Stat(manifest); err != nil {
|
||||
return map[string]interface{}{
|
||||
"error": fmt.Sprintf("%s 不是插件工程(缺 plg.json);用 plugindev_init 先建一个", abs),
|
||||
}, nil
|
||||
}
|
||||
cmd := []string{"build"}
|
||||
if target, _ := args["target"].(string); strings.TrimSpace(target) != "" {
|
||||
cmd = append(cmd, strings.TrimSpace(target))
|
||||
}
|
||||
txt, runErr := p.run(cmd, abs)
|
||||
res := map[string]interface{}{"output": txt, "project_dir": abs}
|
||||
if pkgs := listHmap(filepath.Join(abs, "dist")); len(pkgs) > 0 {
|
||||
res["artifacts"] = pkgs
|
||||
res["next"] = "用 plugin_install 安装本地产物(path 指向上面 artifacts 里的 .hmap),然后 plgreload"
|
||||
}
|
||||
if runErr != nil {
|
||||
res["error"] = runErr.Error()
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleSDK(args map[string]interface{}) (interface{}, error) {
|
||||
action, _ := args["action"].(string)
|
||||
action = strings.TrimSpace(action)
|
||||
switch action {
|
||||
case "list", "current", "path", "latest":
|
||||
txt, err := p.run([]string{"sdk", action}, "")
|
||||
res := map[string]interface{}{"output": txt}
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
}
|
||||
return res, nil
|
||||
case "install":
|
||||
version, _ := args["version"].(string)
|
||||
from, _ := args["from"].(string)
|
||||
cmd := []string{"sdk", "install"}
|
||||
if strings.TrimSpace(from) != "" {
|
||||
cmd = append(cmd, "--from", strings.TrimSpace(from))
|
||||
}
|
||||
if strings.TrimSpace(version) != "" {
|
||||
cmd = append(cmd, strings.TrimSpace(version))
|
||||
} else if strings.TrimSpace(from) == "" {
|
||||
cmd = append(cmd, "latest")
|
||||
}
|
||||
txt, err := p.run(cmd, "")
|
||||
res := map[string]interface{}{"output": txt}
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
}
|
||||
return res, nil
|
||||
case "use":
|
||||
version, _ := args["version"].(string)
|
||||
if strings.TrimSpace(version) == "" {
|
||||
return map[string]interface{}{"error": "use 需要 version"}, nil
|
||||
}
|
||||
txt, err := p.run([]string{"sdk", "use", strings.TrimSpace(version)}, "")
|
||||
res := map[string]interface{}{"output": txt}
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
return map[string]interface{}{"error": "action 只能是 list/current/path/latest/install/use"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) handleProjects(args map[string]interface{}) (interface{}, error) {
|
||||
entries, err := os.ReadDir(p.workspace)
|
||||
if err != nil {
|
||||
return map[string]interface{}{"error": err.Error(), "workspace": p.workspace}, nil
|
||||
}
|
||||
var out []map[string]interface{}
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(p.workspace, e.Name())
|
||||
projects := []string{dir}
|
||||
// 有些工程会被生成到子目录里(hmapdev init 支持指定目录),这里只看一层
|
||||
for _, sub := range projects {
|
||||
if _, err := os.Stat(filepath.Join(sub, "plg.json")); err != nil {
|
||||
continue
|
||||
}
|
||||
item := map[string]interface{}{"name": e.Name(), "dir": sub}
|
||||
if v := readPlgVersion(filepath.Join(sub, "plg.json")); v != "" {
|
||||
item["version"] = v
|
||||
}
|
||||
if pkgs := listHmap(filepath.Join(sub, "dist")); len(pkgs) > 0 {
|
||||
item["artifacts"] = pkgs
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i]["name"].(string) < out[j]["name"].(string) })
|
||||
return map[string]interface{}{"workspace": p.workspace, "projects": out}, nil
|
||||
}
|
||||
|
||||
// ---------------- 基础设施 ----------------
|
||||
|
||||
// run 执行一次 hmapdev。args 为空时执行 `hmapdev version`(用于探活)。
|
||||
func (p *Plugin) run(args []string, dir string) (string, error) {
|
||||
if p.hmapdev == "" {
|
||||
return "", fmt.Errorf("未找到 hmapdev 可执行文件")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, p.hmapdev, args...)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
// 继承环境(Go 工具链需要 GOCACHE/GOPATH/PATH 等)。
|
||||
out, err := cmd.CombinedOutput()
|
||||
txt := truncateOutput(string(out))
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return txt, fmt.Errorf("hmapdev %s 超时(%s)", strings.Join(args, " "), p.timeout)
|
||||
}
|
||||
if err != nil {
|
||||
return txt, fmt.Errorf("hmapdev %s 失败: %v", strings.Join(args, " "), err)
|
||||
}
|
||||
return txt, nil
|
||||
}
|
||||
|
||||
// resolveHmapdev 依次尝试:配置项 → PATH → 常见安装位置。
|
||||
func (p *Plugin) resolveHmapdev() string {
|
||||
if v, _ := p.sdk.Settings().Get("hmapdev_path"); v != nil {
|
||||
if s, _ := v.(string); strings.TrimSpace(s) != "" {
|
||||
if _, err := os.Stat(strings.TrimSpace(s)); err == nil {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if path, err := exec.LookPath("hmapdev"); err == nil {
|
||||
return path
|
||||
}
|
||||
for _, cand := range []string{"/usr/local/bin/hmapdev", "/root/go/bin/hmapdev"} {
|
||||
if _, err := os.Stat(cand); err == nil {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// resolveWorkspace:配置项 → <data_dir>/plugindev → ./plugindev。
|
||||
func (p *Plugin) resolveWorkspace() string {
|
||||
if v, _ := p.sdk.Settings().Get("workspace_dir"); v != nil {
|
||||
if s, _ := v.(string); strings.TrimSpace(s) != "" {
|
||||
ws := strings.TrimSpace(s)
|
||||
_ = os.MkdirAll(ws, 0o755)
|
||||
return ws
|
||||
}
|
||||
}
|
||||
if v, err := p.sdk.Settings().GetCore("core.daemon.data_dir"); err == nil {
|
||||
if dd, _ := v.(string); dd != "" {
|
||||
ws := filepath.Join(dd, "plugindev")
|
||||
_ = os.MkdirAll(ws, 0o755)
|
||||
return ws
|
||||
}
|
||||
}
|
||||
ws := "plugindev"
|
||||
_ = os.MkdirAll(ws, 0o755)
|
||||
return ws
|
||||
}
|
||||
|
||||
// truncateOutput 截断长输出:构建日志动辄几百 KB,直接返回会灌爆模型上下文。
|
||||
func truncateOutput(s string) string {
|
||||
if len(s) <= maxOutputChars {
|
||||
return s
|
||||
}
|
||||
head := s[:maxOutputChars/2]
|
||||
tail := s[len(s)-maxOutputChars/2:]
|
||||
return fmt.Sprintf("%s\n…(输出被截断,共 %d 字节)…\n%s", head, len(s), tail)
|
||||
}
|
||||
|
||||
// listHmap 列出目录下的 .hmap 产物(按名字排序,稳定输出)。
|
||||
func listHmap(dir string) []string {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".hmap") {
|
||||
continue
|
||||
}
|
||||
out = append(out, filepath.Join(dir, e.Name()))
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func readPlgVersion(path string) string {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
s := string(b)
|
||||
i := strings.Index(s, `"version"`)
|
||||
if i < 0 {
|
||||
return ""
|
||||
}
|
||||
rest := s[i:]
|
||||
j := strings.Index(rest, ":")
|
||||
if j < 0 {
|
||||
return ""
|
||||
}
|
||||
rest = strings.TrimSpace(rest[j+1:])
|
||||
rest = strings.TrimPrefix(rest, `"`)
|
||||
if k := strings.Index(rest, `"`); k > 0 {
|
||||
return rest[:k]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func toInt(v interface{}) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
case float64:
|
||||
return int(n), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func fallback(s, def string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return def
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user