feat(webui): 总览底部源码区改为独立的「开源许可」框(协议 + 全文 + 源码)

此前总览底部只在 KPI 卡里挂了一行小链接(.ov-foot),既看不出受什么许可
约束,也看不出 AGPL 网络服务场景下的义务。现在单独成一张卡:

  开销许可
    许可协议     AGPL-3.0-only   → GNU 官方全文
    源码仓库     <source_url>    → 仓库
    网络服务条款(§13):把修改后的版本作为网络服务对外提供时,
                        必须向使用者提供取得对应源码的途径。

内核侧(License 是新事实,不能只靠前端写死):
- internal/meta:新增 License(SPDX 标识)与 LicenseURL,都可 -ldflags 覆盖。
  LicenseURL 默认指向 GNU 官方 AGPL-3.0 全文页 —— 与仓库托管方、分支名、
  文件路径都无关,换仓库/换分支不会失效。
- internal/sdk/status.go 的 BuildStatus:新增 License / LicenseURL 两个
  json 字段(additive,旧消费方忽略未知字段即可)。
  注意 internal/sdk 不受公开接口冻结约束(docs/git-branching.md §六),
  本次未触碰 third_party/homeagent-sdk/sdk/。
- internal/agent/core/status.go:从 meta 填充。

前端:
- 骨架里 .ov-foot 换成独立的 <div class="card" id="ov-legal">(放在 KPI 卡之后)。
- 网络条款那一段按许可标识是否含 AGPL 决定是否渲染,不硬写协议名。
- 内容对一次构建是常量,沿用 __html 比对,填一次后不再重建(不引入闪烁)。

验证(现网 1400x920,CDP 实测):
- /api/v1/kernel 的 build 现在带 license="AGPL-3.0-only"、
  license_url="https://www.gnu.org/licenses/agpl-3.0.html"
- 卡片为真框:class=card、border 1px、radius 14px;总览结构 = rt-panel | card | ov-legal
- 两个链接均为真 <a>,target=_blank + rel=noopener noreferrer
- updateOverview() 再跑一次,卡片子节点身份不变(不重建、不闪)
- 回归:KPI 版本副行、阶段管道 5 节点/5 列/6 SVG、队列 5 行×5 格 均正常
- 无横向溢出;总览/侧栏/顶栏渲染文本无 emoji
- go vet 干净;agent/core、plugins/webui、plugins 全量测试通过
This commit is contained in:
JianFeeeee
2026-09-14 17:48:54 +08:00
parent bead5746c3
commit 37924b295b
5 changed files with 84 additions and 14 deletions

View File

@ -103,6 +103,9 @@ func collectKernelStatus(
KernelName: meta.KernelName,
// AGPL-3.0 §13状态页向网络使用者展示取得源码的入口。
SourceURL: meta.SourceURL,
// 许可标识与全文:与源码链接一起构成「这是什么许可 + 怎么拿到源码」。
License: meta.License,
LicenseURL: meta.LicenseURL,
},
Runtime: RuntimeStatus{
Goroutines: runtime.NumGoroutine(),

View File

@ -65,6 +65,22 @@ var (
// 构建时可用 -ldflags 覆盖,无需改源码:
// -X gitcode.com/JianFeeeee/HomeAgent/internal/meta.SourceURL=<你的仓库>
SourceURL = "https://gitcode.com/JianFeeeee/HomeAgent"
// License 是本项目的开源许可标识SPDX 表达式)。
//
// AGPL-3.0-only 带**网络条款**:把修改过的版本作为网络服务对外提供时,
// 必须给使用者提供取得 Corresponding Source 的机会§13。所以 WebUI
// 总览底部把「许可标识 + 许可全文 + 源码仓库」三者一起渲染成可见的许可卡——
// 只给一个源码链接、不标明协议名,使用者在界面上根本看不出这是什么许可。
License = "AGPL-3.0-only"
// LicenseURL 是上述许可的全文地址。
//
// 默认指向 GNU 官方的 AGPL-3.0 全文页:与仓库托管方、分支名、文件路径都
// 无关,换仓库/换分支都不会失效。若你的发行版把 LICENSE 放在别处,
// 用 -ldflags 覆盖即可:
// -X gitcode.com/JianFeeeee/HomeAgent/internal/meta.LicenseURL=<你的链接>
LicenseURL = "https://www.gnu.org/licenses/agpl-3.0.html"
)
// FullVersion 返回完整的版本字符串。

View File

@ -2855,9 +2855,15 @@
.ov-dot.bad {
background: var(--error);
}
.ov-foot {
margin-top: 10px;
/* 开源许可卡:协议标识 + 协议全文 + 源码仓库。
此前只是 KPI 卡底部一行小链接(.ov-foot在长页面里基本看不见。 */
.ov-legal h2 {
margin-bottom: 6px;
}
.ov-legal-note {
margin: 10px 0 0;
font-size: 11px;
line-height: 1.55;
color: var(--text-muted);
}

View File

@ -1378,7 +1378,10 @@
ovKpi("memory", OV_ICONS.db, __("记忆", "Memory")) +
ovKpi("docs", OV_ICONS.file, __("文档", "Docs")) +
ovKpi("runtime", OV_ICONS.cpu, __("运行时", "Runtime")) +
'</div><div class="ov-foot" id="ov-foot"></div></div>';
'</div></div>' +
// 开源许可单独成框:之前在 KPI 卡底部只是一行小链接(.ov-foot
// 几乎看不见,也看不出是许可证还是别的什么。
'<div class="card" id="ov-legal"></div>';
}
updateOverview();
}
@ -1427,20 +1430,55 @@
"ov-runtime",
rt.goroutines ? rt.goroutines + (rt.memory_mb ? " · " + rt.memory_mb + "M" : "") : "—",
);
// 源码链接AGPL §13静态文本第一次填好后不参与轮询刷新
var foot = document.getElementById("ov-foot");
if (foot) {
var src = k && k.build && k.build.source_url;
var want = src
? '<a href="' +
// 开源许可卡:协议标识 + 协议全文 + 源码仓库
//
// 为什么单独成框、且三者都给AGPL-3.0 §13 的义务是「向网络使用者提供
// 取得 Corresponding Source 的机会」——只给一个仓库链接、不写协议名,
// 使用者看不出这受什么许可约束,也看不出网络服务场景下还有 §13 的义务。
// 内容对一次构建是常量所以填一次就够__html 比对避免重复重建)。
var legal = document.getElementById("ov-legal");
if (legal) {
var lb = (k && k.build) || {};
var src = lb.source_url || "";
var lic = lb.license || "";
var licURL = lb.license_url || "";
var rows = "";
if (lic) {
rows +=
'<div class="kv-row"><span class="key">' +
__("许可协议", "License") +
'</span><span class="val">' +
(licURL
? '<a href="' + escHtml(licURL) + '" target="_blank" rel="noopener noreferrer">' + escHtml(lic) + "</a>"
: escHtml(lic)) +
"</span></div>";
}
if (src) {
rows +=
'<div class="kv-row"><span class="key">' +
__("源码仓库", "Source") +
'</span><span class="val"><a href="' +
escHtml(src) +
'" target="_blank" rel="noopener noreferrer">' +
__("源码", "Source") +
"</a>"
escHtml(src) +
"</a></span></div>";
}
// 网络条款只在 AGPL 系的许可下才成立,所以按标识判断,不硬写协议名。
var note =
lic && lic.toUpperCase().indexOf("AGPL") >= 0
? '<p class="ov-legal-note">' +
__(
"网络服务条款§13把修改后的版本作为网络服务对外提供时必须向使用者提供取得对应源码的途径。",
"Network clause (section 13): offering a modified version as a network service requires giving users a way to obtain the Corresponding Source.",
) +
"</p>"
: "";
var want = rows
? '<h2>' + __("开源许可", "License") + "</h2>" + rows + note
: "";
if (foot.__html !== want) {
foot.__html = want;
foot.innerHTML = want;
if (legal.__html !== want) {
legal.__html = want;
legal.innerHTML = want;
}
}
}

View File

@ -251,6 +251,13 @@ type BuildStatus struct {
// 服务时,要给他们拿到 Corresponding Source 的机会——WebUI 状态页会把它渲染成
// 可见链接,所以**修改后对外部署的分支必须把这个值改指向自己的源码仓库**。
SourceURL string `json:"source_url,omitempty"`
// License 是开源许可标识SPDX如 AGPL-3.0-only
//
// 为何与 SourceURL 分开:只看一个源码链接,使用者没法从界面上看出这是什么
// 许可、网络服务场景下有哪些义务。许可名与许可全文地址是两个独立事实。
License string `json:"license,omitempty"`
// LicenseURL 是 License 对应的全文地址。
LicenseURL string `json:"license_url,omitempty"`
}
type TrackerStatus struct {