mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-28 05:13:27 +00:00
feat(webui): 星图跟随 agent 活动 + 搬到主页 + 修分类配色从未生效
星图此前只是静态展示:starmapAnimate() 只转星空,节点完全静止,
与 agent 的动作零关联。本轮三件事。
★ 修一个从未被发现的 bug:分类配色一直是坏的
服务端 type 是首字母大写("Concept",见 internal/memory/graph.go),
而 smTypeColors 的键全是小写 ⇒ 永远匹配不上 ⇒ 1151 个节点全渲染成
同一个灰色 0xcccccc。浏览器实测确认:改前 colors=["cccccc"],
改后 ["44ff88"](概念绿)。
一、跟随 agent 动(三路信号,全部在渲染循环里推进,不另起定时器)
1. tool_call / stage / agent_output 的 SSE 事件 → 命中节点发光冲高
+ 尺寸微扩。工具名按**词**匹配实体(knowledge_list → knowledge_*)。
2. /runtime 调度器(3s)→ 排队/中断/挂起时全图绷紧;中断或抢占计数
上升时来一记强脉冲。
3. /memory/graph/pulse(10s,新端点)→ 新记忆「生长」:从 0 弹到
正常大小并留余晖。
另:距上次活动越近,全图越亮(抽样呼吸)—— agent 一忙图就活。
二、搬到主页 + 独立页签
总览页内嵌 360px 星图;顶部导航加「星图」独立页签(全高 + 图例)。
同一套 renderer 用 appendChild 在容器间搬运 canvas(three.js 的
canvas 只能有一个父节点,同时渲染会一边黑屏)。
三、性能:保留全部 1151 节点,但全部降规格
改前每节点 = 独立 SphereGeometry(16,12) + 独立光晕球 + 一张 256x64
CanvasTexture ⇒ 2302 个独立 geometry、约 88 万三角形、1151 个
<canvas>,仅文字贴图就吃约 72MB 显存。全景远看根本读不清那些标签。
改后:共享 SphereGeometry(8,6)(约 84 三角形/节点);标签改为 hover
时在容器角上显示 HTML 文本(零显存,且比 3D 贴图更清晰);
866 条边按关系类型合并成 4 个 LineSegments(draw call 866 → 4)。
hover 复位随之改为 baseScale —— 旧的 set(1,1,1) 会把按 mention_count
缩放过的大节点缩成最小尺寸。
四、/memory/graph 瘦身:不再下发稠密向量
星图是本接口唯一消费者,却从不读 vector。生产实测该字段占
79,314 / 402,811 字节 = 19%,而 8 块记忆就这么多,200 块就是 ~2MB
白查白发白堆。真实数据集实测响应 402,811 → 326,997 字节(-18%)。
新增 /memory/graph/pulse:只回 since 窗口内变动过的实体(id/name/type/
mention_count/updated_at)。星图每 10s 拉它来判断「哪个节点新长出来」,
而不必重拉 400KB 全量。
验证(不是「应该能跑」):
- go vet 干净;go test 全绿;新增 5 个测试(向量裁剪 / pulse 窗口 /
since 放大 / pulse 不带向量 / 类型断言失败时透传不丢数据)
- 覆盖率 65.5% → 66.5%
- 真实 1151 节点数据集上跑 headless chromium + SwiftShader 实测:
nodeMeshes=1151、edgeSegs=4、geoShared=true、控制台零报错、
图例与统计(1151 节点 / 866 关系)正常、canvas 在两个容器间正确搬运
- 脉冲匹配在浏览器里逐个 hint 验证:
knowledge_list → 2 个(只命中 knowledge_base / knowledge_list)
qq_get_message 等无匹配 → 8 个(走「整体活动」兜底)
★ 途中修掉自己的两个错:① 最早的子串匹配让 hint="knowledge" 命中
全部单字实体(一次 pulse 选中 250 个、队列顶到 260 上限);
② 改成词匹配后,旧的「补齐到 20 个」逻辑又把 1 个真实命中补成 20 个
无关节点 —— 现象与①一样,只是成因不同。补齐现在只在**完全无匹配**
时启用。
未做(本轮范围外):服务端 gzip(首屏 793KB 无压缩,实测可压到 ~240KB)、
renderAll 按页签懒加载(首屏仍在拉隐藏页签的 kernel+settings 共 230KB)、
setInterval 15s 全量重拉(空闲 53MB/h)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -9,9 +9,11 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
@ -109,6 +111,77 @@ func (h *Handler) handleMemoryTools(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// graphBlockView 是 memory_blocks 的**瘦身**下发视图。
|
||||
//
|
||||
// 为什么要瘦身(实测生产实例 1151 节点 / 866 边):
|
||||
//
|
||||
// 原始 /memory/graph 响应 408,146 B,其中 memory_blocks[].vector 占 79,314 B
|
||||
// (19%)。那是稠密向量 —— 检索侧(SearchIn / 稠密召回)才需要它,
|
||||
// 而星图是本接口**唯一**消费者,它只画节点/连线,压根不读 vector。
|
||||
//
|
||||
// 更大的问题是量级:每多一块记忆就多一份向量。8 块已经 79KB,
|
||||
// 200 块就是约 2MB 白白从库里查出来、序列化、走 socket、丢进浏览器堆,
|
||||
// 全程没有一行代码看过它。文本向量的维度还随模型走(数百到数千),
|
||||
// 换一次 embedder 就能让这个开销翻几倍。
|
||||
//
|
||||
// 所以这里显式裁掉 vector,而不是让 GraphData 返回值带个开关:
|
||||
// 本接口的语义就是「图谱的可视化数据」,让唯一调用方拿到它要的东西。
|
||||
type graphBlockView struct {
|
||||
ID string `json:"id"`
|
||||
Modality string `json:"modality"`
|
||||
Text string `json:"text,omitempty"`
|
||||
PayloadDigest string `json:"payload_digest"`
|
||||
MIME string `json:"mime,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
Width int `json:"width,omitempty"`
|
||||
Height int `json:"height,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Tool string `json:"tool,omitempty"`
|
||||
Scene string `json:"scene,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// graphDataForVisual 把 GraphData 的原始 map 裁成可视化视图(去掉稠密向量)。
|
||||
//
|
||||
// 用 map 断言而不是泛型/反射:GraphData 返回 map[string]interface{},
|
||||
// 里面的具体类型是包内私有的 graphEntity/[]*memory.MemoryBlock,
|
||||
// 断言不中就原样透传(宁可多发也不让接口挂掉)。
|
||||
func graphDataForVisual(data map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(data))
|
||||
for k, v := range data {
|
||||
out[k] = v
|
||||
}
|
||||
blocks, _ := out["memory_blocks"].([]memory.MemoryBlock)
|
||||
if blocks == nil {
|
||||
// 可能是 []*memory.MemoryBlock 或空;两种都不是就直接跳过裁剪。
|
||||
return out
|
||||
}
|
||||
views := make([]graphBlockView, 0, len(blocks))
|
||||
for i := range blocks {
|
||||
b := blocks[i]
|
||||
views = append(views, graphBlockView{
|
||||
ID: b.ID,
|
||||
Modality: string(b.Modality),
|
||||
Text: b.Text,
|
||||
PayloadDigest: b.PayloadDigest,
|
||||
MIME: b.MIME,
|
||||
Size: b.Size,
|
||||
Width: b.Width,
|
||||
Height: b.Height,
|
||||
Fingerprint: b.Fingerprint,
|
||||
Source: b.Source,
|
||||
Tool: b.Tool,
|
||||
Scene: b.Scene,
|
||||
CreatedAt: b.CreatedAt,
|
||||
UpdatedAt: b.UpdatedAt,
|
||||
})
|
||||
}
|
||||
out["memory_blocks"] = views
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) handleMemoryGraph(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
@ -123,9 +196,79 @@ func (h *Handler) handleMemoryGraph(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "data": data})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"success": true, "data": graphDataForVisual(data)})
|
||||
}
|
||||
|
||||
// handleMemoryGraphPulse 是给星图「跟随 agent 动」用的**轻量**活动端点。
|
||||
//
|
||||
// 为什么不让星图反复拉完整 /memory/graph 做对比:
|
||||
//
|
||||
// 完整图谱生产实例 408KB(瘦身前 408KB→瘦身后约 329KB,仍含 1151 个节点
|
||||
// 和 866 条边的全量 JSON)。为了「知道哪些节点是新的」而每 N 秒拉一次全量,
|
||||
// 是把带宽和 JSON.parse 全花在重复数据上。
|
||||
//
|
||||
// 这里只回「最近 since 秒内变动过的实体」,字段压到最小(id + name +
|
||||
// mention_count + updated_at),实测是几百字节到几 KB 的量级 ——
|
||||
// 与完整图谱差两个数量级。新节点「生长」出来、老节点被再次提及而计数变化,
|
||||
// 都能从这份清单里看出来。
|
||||
//
|
||||
// since 缺省给 900s(15 分钟):略大于星图轮询周期(10s),
|
||||
// 即使客户端漏掉几个周期也能自愈,不必担心漏掉节点。
|
||||
func (h *Handler) handleMemoryGraphPulse(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.memory == nil {
|
||||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "memory system not available"})
|
||||
return
|
||||
}
|
||||
since := time.Now().Add(-defaultGraphPulseWindow)
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("since")); raw != "" {
|
||||
if sec, err := strconv.Atoi(raw); err == nil && sec > 0 {
|
||||
since = time.Now().Add(-time.Duration(sec) * time.Second)
|
||||
}
|
||||
}
|
||||
data, err := h.memory.GraphData()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
out := graphDataForVisual(data)
|
||||
// 过滤出窗口内变动过的节点。GraphData 的 nodes 是 []graphEntity(私有类型),
|
||||
// 这里用重新序列化的方式裁剪:字段少、无向量、且不依赖私有类型断言。
|
||||
// 成本是「再序列化一次节点」,但相比把 400KB 发出去仍然划算得多。
|
||||
rawNodes, _ := json.Marshal(out["nodes"])
|
||||
var nodes []struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
MentionCount int `json:"mention_count"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
_ = json.Unmarshal(rawNodes, &nodes)
|
||||
pulse := make([]map[string]interface{}, 0, 8)
|
||||
for _, n := range nodes {
|
||||
if n.UpdatedAt.Before(since) {
|
||||
continue
|
||||
}
|
||||
pulse = append(pulse, map[string]interface{}{
|
||||
"id": n.ID,
|
||||
"name": n.Name,
|
||||
"type": n.Type,
|
||||
"mention_count": n.MentionCount,
|
||||
"updated_at": n.UpdatedAt,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": map[string]interface{}{"nodes": pulse},
|
||||
})
|
||||
}
|
||||
|
||||
// defaultGraphPulseWindow 是 /memory/graph/pulse 不带 since 时的回看窗口。
|
||||
const defaultGraphPulseWindow = 900 * time.Second
|
||||
|
||||
// knowledgeWriteReq 是知识写入请求体(JSON 分支)。
|
||||
type knowledgeWriteReq struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Reference in New Issue
Block a user