Files
HomeAgent/internal/plugins/webui/starmap_vendor_test.go
JianFeeeee f256098c9f refactor: 全面改名为 HologramAgent + 提示词技术内容下沉到 skill/工具描述 + 修 21 处乱码
## ① 全面改名 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:客户端与内核版本一致
2026-10-22 20:49:44 +08:00

162 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package webui
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// ===== 星图依赖 CDN 不可达 =====
//
// 真实现象:用户点「星图」看到「3D 星图不可用(CDN 加载失败)」。
// 服务端 curl 同一批 URL 是 200 —— 说明**不是**服务端的问题,是浏览器
// 访问不到那两个公共 CDN(内网 / 出口受限 / 断网 / 被墙)。
//
// 为什么必须本地化而不是「换个 CDN」:
// 换 CDN 只是把同一个赌注重下一遍。HologramAgent 明确支持离线/内网部署,
// 前端却有 4 个硬依赖在公网上 ⇒ 断网时星图必坏、且用户无从修复。
// marked / DOMPurify 同理(Markdown 渲染与 XSS 净化)。
//
// 判据钉住:页面**不得**再引用任何外部 CDN。
// html 不得引用公网 CDN 资源(这是根因的直接判据)。
func TestDashboardHasNoExternalCDN(t *testing.T) {
html := readDashboardHTML(t)
// 只查 src/href 的绝对 URL;页面里出现的普通文本不算依赖。
re := regexp.MustCompile(`(?:src|href)\s*=\s*["'](https?://[^"']+)["']`)
matches := re.FindAllStringSubmatch(html, -1)
for _, m := range matches {
url := m[1]
t.Errorf("dashboard.html 仍引用外部 CDN:%s\n"+
" 断网/内网/出口受限环境下该功能必坏(星图即因此失效)。\n"+
" 修法:把库 vendor 进二进制,由本服务同源提供。", url)
}
}
// 具体到星图:three.js 与 OrbitControls 必须来自本地路由。
func TestStarmapDependenciesAreLocal(t *testing.T) {
html := readDashboardHTML(t)
if strings.Contains(html, "cdnjs.cloudflare.com/ajax/libs/three.js") {
t.Error("three.js 仍走 cdnjs CDN —— 星图在无公网环境下必坏")
}
if strings.Contains(html, "three@0.128.0/examples/js/controls/OrbitControls.js") {
t.Error("OrbitControls 仍走 jsdelivr CDN —— 星图无法旋转视角")
}
// 必须有本地引用
if !strings.Contains(html, "/static/three.min.js") {
t.Error("未引用本地 /static/three.min.js")
}
if !strings.Contains(html, "/static/OrbitControls.js") {
t.Error("未引用本地 /static/OrbitControls.js")
}
}
// 本地路由必须真的能返回这些库(且是 JS 内容类型,不是 HTML 错误页)。
//
// ★ 这一条是「容易假绿」的地方:路由写了但没 embed 成功时,
//
// 返回 404 或空体,页面依然报「CDN 加载失败」,症状与修复前完全一样。
// 所以判据必须实际取内容并验证它**像 three.js**(含特征串)。
func TestStaticVendorRoutesServeRealLibraries(t *testing.T) {
h, _ := newTestHandler(t)
mux := http.NewServeMux()
h.RegisterRoutes(mux)
srv := httptest.NewServer(mux)
defer srv.Close()
cases := []struct {
path string
mustHave string
}{
// three.js UMD 头里有 REVISION;OrbitControls 是非压缩源码
{"/static/three.min.js", "REVISION"},
{"/static/OrbitControls.js", "OrbitControls"},
{"/static/marked.min.js", "marked"},
{"/static/purify.min.js", "DOMPurify"},
}
for _, c := range cases {
resp, err := http.Get(srv.URL + c.path)
if err != nil {
t.Errorf("%s: %v", c.path, err)
continue
}
body := readAllLimited(t, resp)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("%s 应 200(否则浏览器拿到的是错误页,症状与修复前一模一样),实际 %d",
c.path, resp.StatusCode)
continue
}
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "javascript") {
t.Errorf("%s 的 Content-Type = %q,应为 javascript(浏览器会拒绝执行)", c.path, ct)
}
// ★ 必须同时判「状态码」与「内容」。变异验证发现:把 w.Write(data)
// 去掉(只写 200 头 + 空体)时,**只判状态码的判据会通过** ——
// 而浏览器拿到空体同样报「加载失败」,症状与修复前一模一样。
// 这类「假绿」是本轮踩到的真陷阱,所以体积与特征串都要判。
if len(body) < 1000 {
t.Errorf("%s 体积仅 %d 字节,疑似空体/占位(这几个库都是几万~几十万字节)。"+
"空体在浏览器里的症状与 404 完全一样,但更难看出是文件没写出来",
c.path, len(body))
continue
}
if !strings.Contains(body, c.mustHave) {
t.Errorf("%s 内容不含特征串 %q —— 不是真正的库(可能被 embed 成了错误文本)",
c.path, c.mustHave)
}
}
}
// vendor 文件必须真的存在于源码树(embed 的前提)。
//
// 单独判这一条的原因://go:embed 找不到文件会**编译失败**,
// 但如果有人改成运行时读文件,缺文件就变成「运行时才发现」——
// 从「编译期报错」退化成「用户看到星图坏了」。用判据钉住前提。
func TestVendorFilesExistInSourceTree(t *testing.T) {
dir := filepath.Join(".", "static")
for _, name := range []string{
"three.min.js", "OrbitControls.js", "marked.min.js", "purify.min.js",
} {
p := filepath.Join(dir, name)
st, err := os.Stat(p)
if err != nil {
t.Errorf("缺少 vendor 文件 %s —— 星图/Markdown 依赖它,缺了就静默失效", p)
continue
}
if st.Size() < 1000 {
t.Errorf("%s 仅 %d 字节,疑似下载失败的占位文件", p, st.Size())
}
}
}
func readDashboardHTML(t *testing.T) string {
t.Helper()
raw, err := dashboardFS.ReadFile("dashboard.html")
if err != nil {
t.Fatalf("读 dashboard.html: %v", err)
}
// 模板占位符不影响 CDN 检查,保留原样即可
return string(raw)
}
func readAllLimited(t *testing.T, resp *http.Response) string {
t.Helper()
buf := make([]byte, 0, 64*1024)
tmp := make([]byte, 32*1024)
for {
n, err := resp.Body.Read(tmp)
if n > 0 {
buf = append(buf, tmp[:n]...)
}
if err != nil || len(buf) > 2*1024*1024 {
break
}
}
return string(buf)
}