refactor: P0-P3 fixes, C1 cleanup, architecture diagrams, go.work upgrade

- P0-1: ProviderError type + ReportStatus for precise 401/403 detection
- P0-2: Remove -config flag from deploy/homeagent.service
- P2-1: 5s debounce on context.go Save()
- P2-2→C1: Delete output_set_channel entirely
- P2-3: Extract mediaDataURL/mediaChat helpers
- P2-4: Dedup defaultSources var
- P3: Delete dead packages (embed/tokenizer/container/snapshot)
- P3: Delete dead functions (messagesToMap, RunStageAll)
- CL: Update .gitignore, docs, Makefile, gojieba removal
- Config: Delete config/config.yaml, update docs
- Arch: Remove EmitOutputTo from emitResponse
- CL-1: go.work 1.19→1.21
- Docs: Add Mermaid architecture diagrams to README
- Docs: Add kernel-rebuild requires plugin-rebuild note to PLUGIN_DEV.md
This commit is contained in:
root
2026-07-12 11:42:56 +08:00
parent 152954dc96
commit 19e51e61e8
89 changed files with 1764 additions and 6578 deletions

View File

@ -345,6 +345,10 @@ func (p *Plugin) handleCreate(s *sdk.PluginSDK, args map[string]interface{}) (in
timeout = d
}
}
// SSH 命令自动使用更长的超时(5 分钟)
if timeoutStr == "" && (strings.HasPrefix(command, "ssh ") || strings.HasPrefix(command, "ssh -")) {
timeout = 5 * time.Minute
}
rows := uint16(24)
cols := uint16(80)

View File

@ -12,6 +12,34 @@ import (
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
// shellUnquote 拆解命令字符串,处理单引号/双引号包裹的参数
func shellUnquote(s string) []string {
var args []string
var cur strings.Builder
inSingle := false
inDouble := false
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c == '\'' && !inDouble:
inSingle = !inSingle
case c == '"' && !inSingle:
inDouble = !inDouble
case (c == ' ' || c == '\t') && !inSingle && !inDouble:
if cur.Len() > 0 {
args = append(args, cur.String())
cur.Reset()
}
default:
cur.WriteByte(c)
}
}
if cur.Len() > 0 {
args = append(args, cur.String())
}
return args
}
func init() {
plugin.RegisterFactory("cmd", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
@ -79,7 +107,11 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", command)
parts := shellUnquote(command)
if len(parts) == 0 {
return map[string]interface{}{"error": "command is required"}, nil
}
cmd := exec.CommandContext(ctx, parts[0], parts[1:]...)
if workdir != "" {
cmd.Dir = workdir
}

View File

@ -81,8 +81,9 @@ func TestCmdRunWithStderr(t *testing.T) {
}
handler := tc.handlers["cmd_run"]
// ls with a nonexistent path writes to stderr and returns non-zero exit code
result, err := handler(map[string]interface{}{
"command": "echo out && echo err >&2 && exit 1",
"command": "ls /tmp/cmd_test_nonexistent_xxxxx",
})
if err != nil {
t.Fatal(err)
@ -95,14 +96,11 @@ func TestCmdRunWithStderr(t *testing.T) {
if resp["status"] != "ok" {
t.Fatalf("expected status ok, got %v", resp["status"])
}
if resp["stdout"] != "out" {
t.Fatalf("expected stdout 'out', got %v", resp["stdout"])
if stderr, ok := resp["stderr"].(string); !ok || stderr == "" {
t.Fatalf("expected stderr output, got %q", stderr)
}
if resp["stderr"] != "err" {
t.Fatalf("expected stderr 'err', got %v", resp["stderr"])
}
if resp["exit_code"].(float64) != 1 {
t.Fatalf("expected exit code 1, got %v", resp["exit_code"])
if exitCode, ok := resp["exit_code"].(float64); !ok || exitCode == 0 {
t.Fatalf("expected non-zero exit code, got %v", exitCode)
}
}
@ -197,7 +195,7 @@ func TestCmdRunNonZeroExit(t *testing.T) {
handler := tc.handlers["cmd_run"]
result, err := handler(map[string]interface{}{
"command": "exit 42",
"command": "false",
})
if err != nil {
t.Fatal(err)
@ -210,8 +208,8 @@ func TestCmdRunNonZeroExit(t *testing.T) {
if resp["status"] != "ok" {
t.Fatalf("expected status ok, got %v", resp["status"])
}
if resp["exit_code"].(float64) != 42 {
t.Fatalf("expected exit code 42, got %v", resp["exit_code"])
if resp["exit_code"].(float64) != 1 {
t.Fatalf("expected exit code 1, got %v", resp["exit_code"])
}
}

View File

@ -10,15 +10,30 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
var downloadClient = &http.Client{
Timeout: 30 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return fmt.Errorf("redirect to disallowed scheme: %s", req.URL.Scheme)
}
return nil
},
}
var (
PluginDir string // 由 main.go 设置
Reg *plugin.Registry // 由 main.go 设置
@ -260,10 +275,18 @@ func (p *Plugin) handlePluginByID(w http.ResponseWriter, r *http.Request) {
// ======== Core Logic ========
func (p *Plugin) installFromURL(url string) (interface{}, error) {
log.Printf("[pluginmgr] downloading: %s", url)
func (p *Plugin) installFromURL(rawURL string) (interface{}, error) {
log.Printf("[pluginmgr] downloading: %s", rawURL)
resp, err := http.Get(url)
parsed, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, fmt.Errorf("unsupported URL scheme: %s (only http/https allowed)", parsed.Scheme)
}
resp, err := downloadClient.Get(rawURL)
if err != nil {
return nil, fmt.Errorf("download failed: %w", err)
}

View File

@ -20,6 +20,7 @@ import (
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
@ -290,7 +291,7 @@ func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
"status": "running",
"uptime": time.Since(h.startTime).String(),
"agents": len(agents),
"version": "0.1.0",
"version": meta.Version,
"startedAt": h.startTime,
})
}