mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## ① 全面改名 HomeAgent → HologramAgent
仓库已改名,但代码与文档里仍有大量旧名。本次按"用户能看到的优先"全量替换:
- 用户可见:登录页标题/页脚、默认人格第一句、CLI 状态输出、启动日志、
plugin_install 的工具描述、AI 图片插件的 User-Agent
- 文档与脚本:README、assets/docs、docs/zh、deploy/*.sh、.github/workflows
- 鸿蒙工程:cmd/ohos/HomeAgent → cmd/ohos/HologramAgent(git mv)
目录内的 module/app 描述同步;**证书路径原样保留**
(default_HomeAgent_xxx.cer/p7b/p12 是签名材料的实际文件名,改了会找不到证书)
- 插件元数据:plugin.json 的 author
★ 必须保持原样的两处(改了会坏):
· legacySeededPromptSignature = "你是 HomeAgent 的看板娘「小宅」…"
—— 那是识别历史播种人格卡的**原文锚点**,改了就识别不出旧数据
· 公开 SDK 的 ProxyAuthHomeAgent = "homeagent"
—— 那是 SDK 契约里的值,属独立仓库,本仓不改
## ② 修 21 处乱码(U+FFFD)
这些是历史编辑留下的编码破坏,全在注释里,例如:
「赌注重下���遍」→「赌注重下一遍」
「「不��滤」」 →「「不过滤」」
「**待��退场**」→「**待退场**」
★ 保留 providers/chineseclip/tokenizer.go 里的 2 处:那是**故意**用
U+FFFD 演示"非法 UTF-8 字节被归一成 U+FFFD",不是乱码。
## ③ 提示词技术内容下沉(用户:技术性内容应该收敛到 skill 与 -help 中)
实测系统提示词 2309 字,其中两段几乎全是**模型无法据以行动的实现细节**:
【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、batchRunnable 判据…)
【三层记忆】 1069 字(L1/L2/L3 编号、消费式删除、向量空间、指纹…)
收敛后 **2309 → 1723 字(-25%)**,保留的全是"模型据此做什么不同的事":
工具顺序:默认并行 / 不要依赖先后 / 同通道保序 / 写类工具可能串行
三层记忆:时序里能看最近 / doc_query 把历史事件按原始时间戳插回时序 /
关系用 memory_recall / 成文知识用 knowledge_search
判断口径写进判据:新加一段提示词前先问"模型会据此做什么不同的事",
答不上来就该进 skill 或工具描述。
## 判据
prompt_memory_test.go 新增 2 条:
- TestSystemPromptHasNoImplementationJargon
黑名单:ParallelSafe / batchRunnable / 消费式 / DenseCosine / fingerprint /
向量空间 / 批内降级 / 自激
- TestSystemPromptStaysCompact:上限 2000 字(当前 1723)
变异:把 "ParallelSafe 声明、批内降级" 加回工具顺序段 ⇒ 第一条立刻红。
## 验证
- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- make check-client-versions:客户端与内核版本一致
135 lines
4.8 KiB
Go
135 lines
4.8 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// discoveryPath 是 HologramAgent 的「设备网关在哪」端点(相对门户根)。
|
||
const discoveryPath = "/api/v1/device/gateway"
|
||
|
||
// discoveryResponse 是发现端点的响应。
|
||
type discoveryResponse struct {
|
||
Available bool `json:"available"`
|
||
// URL 是**子域形态**(devices.<基域名>)。浏览器能解析(RFC 6761 内置
|
||
// 特例),但系统解析器(getent/Go/Node)通常解析不到 *.localhost —— 实测如此。
|
||
URL string `json:"url"`
|
||
// URLPortal 是**门户同源形态**(同一 host、同一端口,走路径挂载),
|
||
// 无任何 DNS 依赖。非浏览器客户端应当用这个。
|
||
URLPortal string `json:"url_portal"`
|
||
Preferred string `json:"preferred"`
|
||
Host string `json:"host"` // devices.<基域名>
|
||
Auth string `json:"auth"` // homeagent | none
|
||
Reason string `json:"reason"` // available=false 时的原因
|
||
Hint string `json:"hint"`
|
||
}
|
||
|
||
// normalizeGateway 把用户给的地址整理成可直接连接的 WebSocket URL。
|
||
//
|
||
// 保留两种输入形态的旧行为:
|
||
// - 已含路径(含 /api/v1/device/ws)→ 原样使用;
|
||
// - 只有 host[:port] → 补 ws:// 与默认 WS 路径。
|
||
//
|
||
// 新增:**已带子域标签的地址不再被改写**(例如 devices.example.com)——
|
||
// 旧实现只判断"是否含路径",对子域地址是对的;这里把这条显式化,
|
||
// 避免以后有人加"自动补门户路径"的逻辑时把它改坏。
|
||
func normalizeGateway(addr string) string {
|
||
g := strings.TrimSpace(addr)
|
||
if g == "" {
|
||
return ""
|
||
}
|
||
if strings.HasPrefix(g, "ws://") || strings.HasPrefix(g, "wss://") {
|
||
if strings.Contains(g, "/api/v1/device/ws") {
|
||
return g
|
||
}
|
||
return strings.TrimRight(g, "/") + "/api/v1/device/ws"
|
||
}
|
||
// http(s):// 形态:转成 ws(s)://,其余同下
|
||
if strings.HasPrefix(g, "https://") {
|
||
g = "wss://" + strings.TrimPrefix(g, "https://")
|
||
} else if strings.HasPrefix(g, "http://") {
|
||
g = "ws://" + strings.TrimPrefix(g, "http://")
|
||
} else {
|
||
g = "ws://" + g
|
||
}
|
||
if strings.Contains(g, "/api/v1/device/ws") {
|
||
return g
|
||
}
|
||
return strings.TrimRight(g, "/") + "/api/v1/device/ws"
|
||
}
|
||
|
||
// discoverGateway 向门户询问设备网关的**权威地址**。
|
||
//
|
||
// 为什么需要:设备网关现在位于 devices.<基域名> 的子域反代上,而基域名与
|
||
// 子域标签都是**服务端配置**(webui.base_domain / 插件声明),客户端无从得知。
|
||
// 让服务端回答「网关在哪」是唯一不会漂移的做法。
|
||
//
|
||
// portalURL 是用户配置的门户地址(可能带路径/尾斜杠);token 是门户 api_key。
|
||
// 任何失败都返回错误,由调用方决定是否回退到自配地址 —— 发现是**增强**而非
|
||
// 必需,老版本 HologramAgent 没有这个端点。
|
||
func discoverGateway(portalURL, token string, timeout time.Duration) (string, error) {
|
||
base := strings.TrimSpace(portalURL)
|
||
if base == "" {
|
||
return "", fmt.Errorf("门户地址为空")
|
||
}
|
||
// http(s) → 对应的门户根;ws(s) 输入也要能问(GUI 里同一字段混用两种形态)
|
||
switch {
|
||
case strings.HasPrefix(base, "wss://"):
|
||
base = "https://" + strings.TrimPrefix(base, "wss://")
|
||
case strings.HasPrefix(base, "ws://"):
|
||
base = "http://" + strings.TrimPrefix(base, "ws://")
|
||
case !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://"):
|
||
base = "http://" + base
|
||
}
|
||
// 用户可能填的是完整网关地址(含 /api/v1/device/ws):截到根再拼发现路径
|
||
if i := strings.Index(base, "/api/v1/"); i >= 0 {
|
||
base = base[:i]
|
||
}
|
||
base = strings.TrimRight(base, "/")
|
||
|
||
if timeout <= 0 {
|
||
timeout = 5 * time.Second
|
||
}
|
||
client := &http.Client{Timeout: timeout}
|
||
req, err := http.NewRequest(http.MethodGet, base+discoveryPath, nil)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if token != "" {
|
||
req.Header.Set("X-API-Key", token)
|
||
}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer resp.Body.Close()
|
||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
||
if resp.StatusCode != http.StatusOK {
|
||
return "", fmt.Errorf("发现端点返回 %d:%s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||
}
|
||
var d discoveryResponse
|
||
if err := json.Unmarshal(body, &d); err != nil {
|
||
return "", fmt.Errorf("发现响应无法解析: %w", err)
|
||
}
|
||
if !d.Available {
|
||
msg := d.Reason
|
||
if msg == "" {
|
||
msg = "服务端报告设备网关不可用"
|
||
}
|
||
return "", fmt.Errorf("%s", msg)
|
||
}
|
||
// 优先门户同源形态:waiter 是普通进程,走系统解析器,
|
||
// 而 *.localhost 在系统解析器下通常解析不到(只有浏览器内置该特例)。
|
||
if p := strings.TrimSpace(d.URLPortal); p != "" {
|
||
return p, nil
|
||
}
|
||
if p := strings.TrimSpace(d.URL); p != "" {
|
||
return p, nil
|
||
}
|
||
return "", fmt.Errorf("服务端未给出可用的网关地址")
|
||
}
|