diff --git a/.gitignore b/.gitignore index 9c3175b..06c7b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ homed -waiter +/waiter *.exe *.log *.test @@ -14,3 +14,8 @@ internal/meta/ *.hmap dev/ + +# GUI +cmd/gui/node_modules/ +cmd/gui/dist/ + diff --git a/cmd/waiter/main.go b/cmd/waiter/main.go index 92b80c8..3db213f 100644 --- a/cmd/waiter/main.go +++ b/cmd/waiter/main.go @@ -11,7 +11,6 @@ import ( "strings" "syscall" "time" - "unsafe" ) const ( @@ -238,34 +237,5 @@ func printServerOutput(content string) { } } -func setRawMode(fd int) (func(), error) { - type termios struct { - Iflag uint32 - Oflag uint32 - Cflag uint32 - Lflag uint32 - Cc [20]byte - Ispeed uint32 - Ospeed uint32 - } - const ( - TCGETS = 0x5401 - TCSETS = 0x5402 - ICANON = 0x2 - ECHO = 0x8 - ) - var old termios - if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), TCGETS, uintptr(unsafe.Pointer(&old))); err != 0 { - return func() {}, fmt.Errorf("ioctl TCGETS: %v", err) - } - new := old - new.Lflag &^= ICANON | ECHO - if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), TCSETS, uintptr(unsafe.Pointer(&new))); err != 0 { - return func() {}, fmt.Errorf("ioctl TCSETS: %v", err) - } - return func() { - syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), TCSETS, uintptr(unsafe.Pointer(&old))) - }, nil -} diff --git a/cmd/waiter/rawmode_defs.go b/cmd/waiter/rawmode_defs.go new file mode 100644 index 0000000..4f147aa --- /dev/null +++ b/cmd/waiter/rawmode_defs.go @@ -0,0 +1,18 @@ +package main + +type termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]byte + Ispeed uint32 + Ospeed uint32 +} + +const ( + TCGETS = 0x5401 + TCSETS = 0x5402 + ICANON = 0x2 + ECHO = 0x8 +) diff --git a/cmd/waiter/rawmode_linux.go b/cmd/waiter/rawmode_linux.go new file mode 100644 index 0000000..5cb92c6 --- /dev/null +++ b/cmd/waiter/rawmode_linux.go @@ -0,0 +1,23 @@ +//go:build linux + +package main + +import ( + "syscall" + "unsafe" +) + +func setRawMode(fd int) (func(), error) { + var old termios + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), TCGETS, uintptr(unsafe.Pointer(&old))); err != 0 { + return func() {}, err + } + new := old + new.Lflag &^= ICANON | ECHO + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), TCSETS, uintptr(unsafe.Pointer(&new))); err != 0 { + return func() {}, err + } + return func() { + syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), TCSETS, uintptr(unsafe.Pointer(&old))) + }, nil +} diff --git a/cmd/waiter/rawmode_other.go b/cmd/waiter/rawmode_other.go new file mode 100644 index 0000000..69ae03a --- /dev/null +++ b/cmd/waiter/rawmode_other.go @@ -0,0 +1,9 @@ +//go:build !linux + +package main + +import "fmt" + +func setRawMode(fd int) (func(), error) { + return func() {}, fmt.Errorf("raw terminal mode not supported on this platform") +} diff --git a/package/build.sh b/package/build.sh new file mode 100755 index 0000000..4aac81f --- /dev/null +++ b/package/build.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD_DIR="${PROJECT_ROOT}/build" +VERSION="${VERSION:-$(git -C "$PROJECT_ROOT" describe --tags --dirty 2>/dev/null || echo "0.7.1")}" +COMMIT="${COMMIT:-$(git -C "$PROJECT_ROOT" rev-parse --short HEAD 2>/dev/null || echo "unknown")}" +BUILD_TIME="${BUILD_TIME:-$(date -u '+%Y-%m-%dT%H:%M:%SZ')}" +GO="${GO:-$(command -v go 2>/dev/null || echo "/home/jianf/go1.26.5/go/bin/go")}" +LDFLAGS="-X gitcode.com/JianFeeeee/HomeAgent/internal/meta.Version=${VERSION} -X gitcode.com/JianFeeeee/HomeAgent/internal/meta.Commit=${COMMIT} -X gitcode.com/JianFeeeee/HomeAgent/internal/meta.BuildTime=${BUILD_TIME}" + +TARGET="${1:-native}" +COMPONENT="${2:-all}" + +# ---- platform matrix ---- +# homed: linux/amd64 + linux/arm64 (CGO), macOS native-only (no osxcross) +# windows: blocked — gojieba CXX flags + dlfcn.h not available in MinGW +# waiter: all platforms (CGO-free, raw terminal mode is a no-op on non-Linux) +# gui: electron-builder handles cross-platform natively + +case "$TARGET" in + native) GOOS="" GOARCH="" ;; + linux/amd64) GOOS=linux GOARCH=amd64 CC="${CC:-}" ;; + linux/arm64) GOOS=linux GOARCH=arm64 CC="${CC:-aarch64-linux-gnu-gcc}" ;; + darwin/amd64) GOOS=darwin GOARCH=amd64 CC="${CC:-}" ;; + darwin/arm64) GOOS=darwin GOARCH=arm64 CC="${CC:-}" ;; + windows/amd64) GOOS=windows GOARCH=amd64 CC="${CC:-x86_64-w64-mingw32-gcc}" ;; + all) + "$0" linux/amd64 "$COMPONENT" + "$0" linux/arm64 "$COMPONENT" + "$0" darwin/amd64 "$COMPONENT" + "$0" darwin/arm64 "$COMPONENT" + "$0" windows/amd64 "$COMPONENT" + exit 0 + ;; + *) + echo "Unknown target: $TARGET" + echo "Usage: $0 [native|linux/amd64|linux/arm64|darwin/amd64|darwin/arm64|windows/amd64|all]" + echo " [all|homed|waiter|gui]" + exit 1 +esac + +if [ -n "${GOOS:-}" ]; then + SUFFIX="${GOOS}_${GOARCH}" + export GOOS GOARCH +fi +if [ -n "${CC:-}" ]; then + export CC +fi +export CGO_ENABLED="${CGO_ENABLED:-1}" + +mkdir -p "$BUILD_DIR" + +# ---- homed (CGO, sqlite3) ---- +build_homed() { + local out="$BUILD_DIR/homed${SUFFIX:+_$SUFFIX}" + local plat="${GOOS:-linux}/${GOARCH:-amd64}" + + if [ "$GOOS" = "darwin" ] && [ "${CC:-}" = "" ] && [ "$(uname)" != "Darwin" ]; then + echo "[SKIP] homed ${plat} — requires native macOS build (CGO + sqlite3, no osxcross)" + return + fi + if [ "$GOOS" = "windows" ]; then + echo "[SKIP] homed ${plat} — gojieba CXX flags + dlfcn.h unavailable in MinGW cross-compiler" + return + fi + + if [ "$GOOS" = "windows" ]; then out="${out}.exe"; fi + echo "[BUILD] homed ${plat} → $out" + CGO_ENABLED=1 "$GO" build -trimpath -installsuffix dynlink \ + -ldflags "$LDFLAGS" -o "$out" ./cmd/homed/ + echo " OK ($(file "$out" | sed 's/.*: //') | $(du -h "$out" | cut -f1))" +} + +# ---- waiter (cross-platform, CGO-free) ---- +build_waiter() { + local plat="${GOOS:-linux}/${GOARCH:-amd64}" + local out="$BUILD_DIR/waiter${SUFFIX:+_$SUFFIX}" + if [ "$GOOS" = "windows" ]; then out="${out}.exe"; fi + + echo "[BUILD] waiter ${plat} → $out" + CGO_ENABLED=0 "$GO" build -trimpath -installsuffix dynlink \ + -ldflags "$LDFLAGS" -o "$out" ./cmd/waiter/ + echo " OK ($(du -h "$out" | cut -f1))" +} + +# ---- gui (Electron) ---- +build_gui() { + if [ -n "${GOOS:-}" ] && [ "$GOOS" != "$("$GO" env GOOS)" ]; then + echo "[SKIP] gui ${GOOS}/${GOARCH} — electron-builder handles cross-platform natively; run 'all' on CI host" + return + fi + + local gui_dir="$PROJECT_ROOT/cmd/gui" + echo "[BUILD] gui → $BUILD_DIR/" + + if [ ! -d "$gui_dir/node_modules" ]; then + echo " npm install..." + (cd "$gui_dir" && npm install --production) + fi + + (cd "$gui_dir" && npx electron-builder --config "$gui_dir/package.json" \ + --linux --win --mac \ + --x64 --arm64 \ + -p never \ + -o "$BUILD_DIR") + echo " OK" +} + +# ---- dispatch ---- +case "$COMPONENT" in + all) build_homed; build_waiter; build_gui ;; + homed) build_homed ;; + waiter) build_waiter ;; + gui) build_gui ;; + *) + echo "Unknown component: $COMPONENT" + exit 1 +esac diff --git a/审查.md b/审查.md new file mode 100644 index 0000000..8032472 --- /dev/null +++ b/审查.md @@ -0,0 +1,135 @@ +# HomeAgent 代码审查报告(第三轮逐行全面复审) + +审查日期: 2026-07-18 + +## 审查范围 + +- `cmd/homed/main.go` (453 行) +- `internal/agent/core/agent.go` (3156 行), `stages.go` (155 行), `context.go` (252 行), `plugin_health.go` (128 行) +- `internal/agent/core/stages_test.go` (199 行) +- `internal/agent/api/provider.go` (783 行) +- `internal/agent/io/channel.go` (636 行) +- `internal/sdk/` 全部 9 个文件 (510 行) +- `internal/plugin/` registry.go (426 行), plugin.go (295 行), dynamic.go (142 行) +- `internal/plugin/cabi/loader.go` (651 行) +- `internal/lua/vm.go` (403 行) +- `internal/events/bus.go` (83 行) +- `internal/config/registry.go` (700 行) +- `internal/memory/graph.go` (766 行), indexer.go (312 行), static_embedder.go (369 行), clean_text.go (57 行) +- `internal/memory/document/document.go` (471 行) +- SDK 仓库 `sdk/` 全部 6 个 API 文件 + `plugin_test.go` (~700 行) +- SDK 仓库 `tools/plugindev/templates.go` +- 架构文档 4 份 (`docs/zh/`) + +--- + +## Bug 级别 + +### A-1 `processTextInput` 缺失 `runStage(StageOnInput)` 调用 + +- **位置**: `internal/agent/core/agent.go:494-562` +- **严重程度**: 高 +- **描述**: `processTextInput` 创建了 `stageCtx` (行515) 但从未调用 `a.runStage(sdk.StageOnInput, stageCtx)`。注册了 `on_input` 阶段的插件对文本输入完全不可见。 +- **对照**: `processMediaInput` (行410) 正确调用了 `runStage(StageOnInput)`。 +- **影响**: 所有文本输入的 `on_input` 阶段钩子静默失效。黑名单、限流、短路回复等功能对文本输入无效。 +- **文档对照**: ARCHITECTURE.md 完整链路图中 `processTextInput()` 下方第一支路即为 `on_input stage`。 + +### A-2 `processTextInput` 缺失 `publishEvent(EventRawInput)` + +- **位置**: `internal/agent/core/agent.go:494-562` +- **严重程度**: 中 +- **描述**: `processTextInput` 没有发布 `EventRawInput` 事件。 +- **对照**: `processMediaInput` (行405) 正确调用了 `a.publishEvent(events.EventRawInput, ...)`。 +- **影响**: 订阅 `raw_input` 事件的插件/WebUI 收不到文本输入的原始事件。 + +### A-4 `processMediaInput` 缺失 `emitMemoryCandidate` 调用 + +- **位置**: `internal/agent/core/agent.go:371-436` +- **严重程度**: 中 +- **描述**: 媒体输入处理完成后,没有调用 `emitMemoryCandidate`(文本处理在行560对应调用)。图片/音频对话不会被捕获到文本记忆层。 +- **影响**: `text memory` 中缺失多模态交互记录。 + +### A-5 `StageOnInput` 在 `context.Append` 之后执行 + +- **位置**: `internal/agent/core/agent.go:389-410` +- **严重程度**: 低 +- **描述**: `processMediaInput` 中行389 `a.context.Append()` 先执行,行410 `runStage(StageOnInput)` 后执行。插件在 `on_input` 阶段无法阻止上下文被追加。 +- **文档对照**: ARCHITECTURE.md 描述 `on_input` 为"消息到 Agent,零处理",应在任何处理之前执行。 + +### A-6 `lua/vm.go` 中 `http_get`/`http_post` 返回 mock 数据 + +- **位置**: `internal/lua/vm.go:69-80` +- **严重程度**: 高 +- **描述**: `http_get` 返回 `{"url":...,"body":"mock","status":200}`,`http_post` 返回 `{"url":...,"body":...,"status":200}`。没有实际 HTTP 网络调用。 +- **文档对照**: ADAPTER.md 第93-95行明确说明 `http_get(url)` 和 `http_post(url, body)` 应发起真实 HTTP 请求。 +- **影响**: Lua 适配器脚本中尝试使用 `http_get`/`http_post` 获取外部数据的功能均静默失败。 + +--- + +## 功能限制 + +### A-7 C ABI 注册阶段时未传递 `StageScope` + +- **位置**: `internal/plugin/cabi/loader.go:285-304` +- **描述**: C ABI 的 `CORE_REGISTER_STAGE` (methodID=2) 调用 `s.RegisterStage(sdk.Stage(st), handler)`,没有 scope 参数,总是使用 `StageScopeGlobal`。外部 `.so` 插件无法使用 `StageScopeOwnTools`。 +- **对照**: 公共 SDK `sdk/plugin.go:237` 的 `RegisterStage` 支持 `...scope` 变参。 + +--- + +## 顺序/次要问题 + +### A-8 `processConsolidation` 未注入 source context + +- **位置**: `internal/agent/core/agent.go:2567` +- **描述**: `processConsolidation` 直接传 `&sdk.StageContext{RawMessage: input}`,没有调用 `injectSourceContext`。整理任务在 `process()` 内部的阶段钩子中无法获取来源/通道信息。 + +--- + +## 已确认无问题的功能 + +1. **SDK 接口映射**: `internal/sdk/` 的 5 组 type alias (`MemoryAPI`, `KnowledgeAPI`, `SettingsAPI`, `LLMAPI`, `DocMemoryAPI`) 全部正确对齐 `pubsdk`,impl 包装器逐一桥接。 + +2. **C ABI 45 个 dispatch 方法**: 全部覆盖 SDK API,JSON 序列化/反序列化正确,无遗漏。方法列表: + - 1: CORE_REGISTER_TOOL + - 2: CORE_REGISTER_STAGE + - 3: CORE_REGISTER_OUTPUT_CH + - 4: CORE_REGISTER_PLUGIN_API + - 5-7: CORE_INJECT_TEXT / INJECT_INTERRUPT_TEXT / INJECT_TEXT_NO_MEMORY + - 8: CORE_SET_AUTO_RESTART + - 9-13: CORE_MEMORY_RECALL / COMMIT / INTROSPECT / MERGE / PURGE + - 14: CORE_DOC_QUERY + - 15: CORE_KNOWLEDGE_SEARCH + - 16-18: CORE_SETTINGS_GET / SET / REGISTER_DEF + - 19-20: CORE_LLM_LIST_SOURCES / SET_SOURCE + - 21-22: CORE_SOCIAL_GET_PERSON / GET_NETWORK + - 23-24: CORE_SUBSCRIBE / UNSUBSCRIBE + - 25: CORE_FREE_STRING + - 26-31: CORE_SETTINGS_GET_CORE / SET_CORE / LIST_CORE / GET_PLUGIN / SET_PLUGIN / LIST_PLUGIN + - 32-34: CORE_DOC_INSERT / REMOVE / STATS + - 35-36: CORE_KNOWLEDGE_ADD / LIST + - 37: CORE_LLM_CURRENT_SOURCE + - 38-40: CORE_SOCIAL_GET_TRAIT / GET_RELATIONS / LIST_PERSONS + - 41: CORE_TEXT_MEMORY_APPEND + - 42-45: CORE_SETTINGS_LIST / DEFS / DUMP / PLUGINS + +3. **`pipeline.go` 非死代码**: `internal/memory/pipeline/pipeline.go` 在 `cmd/homed/main.go:122-130` 中被引用和启动。 + +4. **`emitMemoryCandidate` 被消费**: `cmd/homed/main.go:213-247` 的 goroutine 消费 `iom.OutputChan()` 中的 `memory_candidate` 事件,写入 `textMem` 和 `distiller`。 + +5. **向量表示跨会话兼容**: `StaticEmbedder` / `TFIDFVectorizer` 的 `Vectorize()` 是纯函数(基于词袋权重),不依赖会话状态。 + +6. **`processConsolidation` 不发射记忆候选**: 行2579注释说明这是有意为之,防止任务文本被蒸馏进图库造成污染。 + +--- + +## 汇总 + +| 编号 | 级别 | 文件 | 行 | 描述 | +|------|------|------|----|------| +| A-1 | **Bug** | `agent.go` | 494-562 | `processTextInput` 未调用 `StageOnInput` | +| A-2 | **Bug** | `agent.go` | 494-562 | `processTextInput` 未发布 `EventRawInput` | +| A-4 | **Bug** | `agent.go` | 371-436 | `processMediaInput` 未调用 `emitMemoryCandidate` | +| A-5 | **Bug** | `agent.go` | 389-410 | `StageOnInput` 在 `context.Append` 之后执行 | +| A-6 | **Bug** | `lua/vm.go` | 69-80 | `http_get`/`http_post` 返回 mock | +| A-7 | 限制 | `cabi/loader.go` | 285-304 | C ABI 缺 `StageScopeOwnTools` 支持 | +| A-8 | 顺序 | `agent.go` | 2567 | `processConsolidation` 未注入 source context |