feat: add package/build.sh cross-platform build script

- build.sh supports linux/amd64, linux/arm64, darwin/amd64, darwin/arm64,
  windows/amd64, and 'all' for matrix build
- Components: homed (CGO, Linux/macOS-only), waiter (all platforms), gui
- Platform matrix documented in script header
- waiter: extract setRawMode into rawmode_linux.go + rawmode_other.go for
  cross-platform support
- Add cmd/gui/node_modules/ and dist/ to .gitignore
- Fix .gitignore waiter pattern to not ignore cmd/waiter/ directory
This commit is contained in:
2026-07-19 11:40:14 +08:00
parent 24698e7c82
commit c7209b9861
7 changed files with 310 additions and 31 deletions

7
.gitignore vendored
View File

@ -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/

View File

@ -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
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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")
}

119
package/build.sh Executable file
View File

@ -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

135
审查.md Normal file
View File

@ -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 APIJSON 序列化/反序列化正确,无遗漏。方法列表:
- 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 |