diff --git a/cmd/gui/renderer/app.js b/cmd/gui/renderer/app.js
index 7696700..6f21fb9 100644
--- a/cmd/gui/renderer/app.js
+++ b/cmd/gui/renderer/app.js
@@ -696,6 +696,10 @@ async function refreshDataOnly() {
try {
state.kernel = await api("/kernel");
} catch (e) {}
+ try {
+ // 运行态快照:只给指标与队列/阶段展示用,不影响其他卡片。
+ state.runtime = await api("/runtime");
+ } catch (e) {}
try {
var s = await api("/settings");
state.settings = s.settings || {};
@@ -775,6 +779,10 @@ async function refreshAll() {
try {
state.kernel = await api("/kernel");
} catch (e) {}
+ try {
+ // 运行态快照(调度器/驻留子/通道),供总览的运行态面板使用。
+ state.runtime = await api("/runtime");
+ } catch (e) {}
try {
var s = await api("/settings");
state.settings = s.settings || {};
@@ -1004,6 +1012,223 @@ function statCard(l, v) {
);
}
+// ===== 运行态面板:阶段管道 + 中断队列 =====
+//
+// 与 WebUI 总览**同一套设计语言:等大表框**。此前桌面版总览只有四个数字卡,
+// 既看不到「这一轮走到哪一步」,也看不到四级中断队列的积压。
+// 数据来自 /api/v1/runtime(KernelStatus 的运行态子集)。
+var RT_LEVELS = [
+ { lv: 4, name: "L4", zh: "内核独占", en: "kernel only", cls: "rt-lv-4" },
+ { lv: 3, name: "L3", zh: "交互", en: "interactive", cls: "rt-lv-3" },
+ { lv: 2, name: "L2", zh: "消息", en: "message", cls: "rt-lv-2" },
+ { lv: 1, name: "L1", zh: "后台", en: "background", cls: "rt-lv-1" },
+];
+// 七阶段归并成五格(与内核 sdk.Stage 的顺序一致):
+// 一轮里工具调用会反复回到「行动后」,线性滑块本身就是错的表述,
+// 所以画成 输入 → 行动 ⇄(工具) → 输出 → 结束,工具那格带循环标记。
+var RT_PIPE_GROUPS = [
+ { zh: "输入", en: "in", ico: "in" },
+ { zh: "行动", en: "act", ico: "act" },
+ { zh: "工具", en: "tool", ico: "tool", loop: true },
+ { zh: "输出", en: "out", ico: "out" },
+ { zh: "结束", en: "done", ico: "done" },
+];
+// 图标一律内联 SVG(24x24 / currentColor),不用 emoji/符号字符充当图标。
+var RT_ICO = {
+ in: ' ',
+ act: ' ',
+ tool: ' ',
+ out: ' ',
+ done: ' ',
+ loop: ' ',
+};
+
+function rtPhaseGroup(phase) {
+ switch (phase) {
+ case "on_input":
+ return 0;
+ case "pre_action":
+ case "post_action":
+ return 1;
+ case "before_toolcall":
+ case "after_toolcall":
+ return 2;
+ case "before_output":
+ return 3;
+ case "after_output":
+ return 4;
+ }
+ return -1;
+}
+function rtShortTool(name) {
+ var n = String(name || "");
+ var i = n.lastIndexOf("__");
+ if (i >= 0) n = n.slice(i + 2);
+ return n.length > 14 ? n.slice(0, 13) + "…" : n;
+}
+// rtSlots 画一组「车位」式格槽:槽位数量固定可见,被占用的点亮。
+// 为什么不用进度条:队列为 0 时进度条宽度就是 0,整行只剩文字,看上去就是「这块空着」。
+function rtSlots(depth, slots, cls) {
+ var n = Math.max(5, Math.min(16, slots || 5));
+ var d = depth || 0;
+ var out = '';
+ for (var i = 0; i < n; i++) out += ' ';
+ // 溢出计数必须留在 .rt-slots 内:格槽是 flex 行,多一个兄弟节点会被挤出去
+ if (d > n) out += '+' + (d - n) + " ";
+ return out + " ";
+}
+// rtTrailPush 把一条「本轮发生过的事」落到它实际发生的阶段列里;
+// 同一阶段重复的同一条(如同一工具连调 3 次)只累加计数,不刷屏。
+function rtTrailPush(g, kind, label, short) {
+ if (!state.stageTrail) state.stageTrail = [];
+ var arr = state.stageTrail;
+ var last = arr.length ? arr[arr.length - 1] : null;
+ if (last && last.g === g && last.kind === kind && last.short === short) {
+ last.n = (last.n || 1) + 1;
+ return;
+ }
+ arr.push({ g: g, kind: kind, label: label, short: short, n: 1 });
+ if (arr.length > 24) arr.shift();
+}
+
+function renderRuntimePanel() {
+ var title = __("运行态", "Runtime");
+ var rt = state.runtime;
+ if (!rt) {
+ return (
+ '
' + title + ' ' +
+ __("运行态数据不可用", "runtime unavailable") + "
"
+ );
+ }
+ var sc = rt.scheduler || {};
+ var q = sc.interrupt_queues || [0, 0, 0, 0, 0];
+ var pending = sc.pending_interrupts || 0;
+ var ready = sc.ready_queue_depth || 0;
+ var stack = sc.suspend_stack || 0;
+ var maxStack = sc.max_suspend_depth || 4;
+ var residents = rt.residents || [];
+ var byLv = sc.interrupts_by_level || [];
+ var preLv = sc.preempts_by_level || [];
+ var maxQ = Math.max(1, ready, q[1] || 0, q[2] || 0, q[3] || 0, q[4] || 0);
+ // 至少 5 格:0 时也有可见形状
+ var qSlots = Math.max(5, Math.min(16, maxQ));
+
+ var html = '' + title + " ";
+ // 四个数字块(沿用本 app 的 statCard 风格)
+ html +=
+ '
' +
+ statCard(__("排队", "Ready"), ready, "") +
+ statCard(__("中断", "Pending"), pending, "") +
+ statCard(__("栈", "Stack"), stack + "/" + maxStack, "") +
+ statCard(__("子代理", "Subagents"), residents.length, "") +
+ "
";
+
+ // ---- 阶段管道:五个等大表框 ----
+ var g = rtPhaseGroup(state.pipelinePhase || "");
+ html +=
+ '
' + __("阶段管道", "Stage pipeline") +
+ (g < 0 ? " " + __("(空闲)", "(idle)") : "") + "
";
+ html += '
';
+ html += RT_PIPE_GROUPS.map(function (s, i) {
+ var items = (state.stageTrail || []).filter(function (t) {
+ return (t.g | 0) === i;
+ });
+ var body = items.length
+ ? items
+ .map(function (t) {
+ var kind = t.kind || "stage";
+ var ico =
+ kind === "output" ? RT_ICO.out : kind === "tool" ? RT_ICO.tool : "";
+ return (
+ '
' + ico +
+ escHtml(t.short || t.label) +
+ (t.n > 1 ? 'x' + t.n + " " : "") +
+ " "
+ );
+ })
+ .join("")
+ : '
' + __("无", "none") + " ";
+ return (
+ '
' +
+ '
' + RT_ICO[s.ico] +
+ "" + __(s.zh, s.en) + " " +
+ (s.loop
+ ? '' + RT_ICO.loop + " "
+ : "") +
+ '
' + body + "
"
+ );
+ }).join("");
+ html += "
";
+
+ // ---- 中断队列:五个等大表框(L4/L3/L2/L1 + 排队)----
+ html += '
' + __("队列", "Queues") + "
";
+ html += '
';
+ RT_LEVELS.forEach(function (L) {
+ var depth = q[L.lv] || 0;
+ var reg = byLv[L.lv] || 0;
+ var pre = preLv[L.lv] || 0;
+ var desc = __(L.zh, L.en);
+ html +=
+ '
' +
+ '
' + L.name + " " + escHtml(desc) + "
" +
+ '
' + depth + "
" +
+ rtSlots(depth, qSlots, L.cls) +
+ '
' + reg + " " + __("登记", "reg") + " · " +
+ pre + " " + __("抢占", "pre") + "
";
+ });
+ // 排队队列无级别:用虚线框与四级中断区分(另一**类别**,不是另一优先级)
+ html +=
+ '
' +
+ '
' + __("排队", "queued") + " FIFO
" +
+ '
' + ready + "
" +
+ rtSlots(ready, qSlots, "rt-lv-q") +
+ '
' + __("无级别", "no priority") + "
";
+ html += "
";
+ return html;
+}
+
+// 开源许可卡:协议标识 + 协议全文 + 源码仓库。
+// AGPL-3.0 §13 的义务是「向网络使用者提供取得 Corresponding Source 的机会」——
+// 只给一个仓库链接、不写协议名,使用者看不出这受什么许可约束。
+function renderLegalCard() {
+ var b = ((state.kernel || {}).build) || {};
+ var src = b.source_url || "";
+ var lic = b.license || "";
+ var licURL = b.license_url || "";
+ if (!lic && !src) return "";
+ function row(key, val) {
+ return (
+ '' + escHtml(key) +
+ ' ' + val + "
"
+ );
+ }
+ function a(href, text) {
+ return (
+ '' + escHtml(text) + " "
+ );
+ }
+ var rows = "";
+ if (lic) rows += row(__("许可协议", "License"), licURL ? a(licURL, lic) : escHtml(lic));
+ if (src) rows += row(__("源码仓库", "Source"), a(src, src));
+ // 网络条款只在 AGPL 系的许可下才成立,所以按标识判断,不硬写协议名。
+ var note =
+ lic && lic.toUpperCase().indexOf("AGPL") >= 0
+ ? '' +
+ __(
+ "网络服务条款(§13):把修改后的版本作为网络服务对外提供时,必须向使用者提供取得对应源码的途径。",
+ "Network clause (section 13): offering a modified version as a network service requires giving users a way to obtain the Corresponding Source.",
+ ) +
+ "
"
+ : "";
+ return '
' + __("开源许可", "License") + " " + rows + note + "";
+}
+
function renderOverview() {
var s = state.status || {};
var k = state.kernel;
@@ -1018,7 +1243,25 @@ function renderOverview() {
"uptime",
) +
statCard(__("插件", "Plugins"), (k?.plugins || []).length || 0, "plugin") +
- statCard(__("版本", "Version"), s.version || "0.1.0", "version") +
+ statCard(
+ __("版本", "Version"),
+ (function () {
+ // 构建身份取自 /kernel 的 build(-ldflags 注入的真实版本/commit)。
+ // 旧实现用的是 /status 的 version 加一个凭空写死的 "0.1.0" 兑底 ——
+ // 拿不到数据时会向用户展示一个不存在的版本号。
+ var b = (k && k.build) || {};
+ var v = b.version || s.version || "";
+ if (!v) return "-";
+ var sha =
+ b.commit && b.commit !== "unknown" ? String(b.commit).slice(0, 7) : "";
+ return (
+ "v" + escHtml(v) +
+ '' + escHtml(b.kernel_name || "HomeAgent") +
+ (sha ? " · " + escHtml(sha) : "") + "
"
+ );
+ })(),
+ "version",
+ ) +
"";
if (k) {
html +=
@@ -1082,6 +1325,7 @@ function renderOverview() {
"" +
"";
}
+ html += renderRuntimePanel();
html +=
'
' +
__("运行时", "Runtime") +
@@ -1094,6 +1338,7 @@ function renderOverview() {
) +
statCard("Go " + __("版本", "Version"), k?.runtime?.go_version || "-", "") +
" ";
+ html += renderLegalCard();
document.getElementById("view-overview").innerHTML = html;
}
@@ -5211,17 +5456,47 @@ async function connectFetchSSE(url) {
var phase = p.phase || "";
var tool = p.tool || "";
if (p.channel !== "_consolidation_") {
- if (phase === "pre_action")
+ // 阶段轨迹:本轮真实发生过什么,按阶段落到运行态面板的对应框里。
+ // 与 WebUI 同一套 g(阶段组)编号,见 rtPhaseGroup。
+ if (phase === "on_input") {
+ state.stageTrail = [];
+ rtTrailPush(0, "stage", __("输入", "input"), __("输入", "input"));
+ } else if (phase === "pre_action") {
+ rtTrailPush(
+ 1,
+ "stage",
+ __("组装上下文并思考", "assemble context and think"),
+ __("思考", "think"),
+ );
state.chatStage = __("AI 思考中...", "AI thinking...");
- else if (phase === "before_toolcall") {
+ } else if (phase === "before_toolcall") {
+ if (tool)
+ rtTrailPush(
+ 2,
+ tool.indexOf("output_") === 0 ? "output" : "tool",
+ tool,
+ rtShortTool(tool),
+ );
state.chatStage = __("工具调用: ", "Tool: ") + (tool || "");
if (tool && (state.pendingTools || []).indexOf(tool) === -1) {
if (!state.pendingTools) state.pendingTools = [];
state.pendingTools.push(tool);
rerenderChatIfActive();
}
- } else if (phase === "before_output")
+ } else if (phase === "before_output") {
+ rtTrailPush(3, "stage", __("生成回复", "generate reply"), __("生成", "gen"));
state.chatStage = __("生成回复中...", "Generating response...");
+ } else if (phase === "after_output") {
+ rtTrailPush(4, "stage", __("本轮完成", "turn complete"), __("完成", "done"));
+ }
+ state.pipelinePhase = phase;
+ // 阶段停留一会儿就回空闲,避免留下一个永远停在 after_output 的假状态。
+ if (state.pipelineTimer) clearTimeout(state.pipelineTimer);
+ state.pipelineTimer = setTimeout(function () {
+ state.pipelinePhase = "";
+ if (state.currentView === "overview") renderOverview();
+ }, 2500);
+ if (state.currentView === "overview") renderOverview();
}
var badge = document.getElementById("chat-stage");
if (badge) {
diff --git a/cmd/gui/renderer/style.css b/cmd/gui/renderer/style.css
index 0d83af4..75cfbd2 100644
--- a/cmd/gui/renderer/style.css
+++ b/cmd/gui/renderer/style.css
@@ -2191,3 +2191,243 @@ td .switch {
.kv-row .val .switch {
margin-right: 4px;
}
+
+/* ===== 运行态面板:阶段管道 + 中断队列(与 WebUI 总览同一套设计语言)=====
+ 两块都统一成**等大表框**,与 KPI 卡同一种骨架。
+ 此前桌面版总览只有四个数字卡:既看不到「这一轮走到哪一步」,
+ 也看不到四级中断队列的积压。 */
+.stat-card .stat-sub {
+ margin-top: 4px;
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+}
+.rt-section-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ letter-spacing: 0.3px;
+ margin: 14px 0 8px;
+ text-transform: uppercase;
+}
+.rt-empty {
+ font-size: 12px;
+ color: var(--text-muted);
+ padding: 4px 0;
+}
+/* 行内小图标:一律 SVG,不使用 emoji/符号字符。 */
+.rt-ico {
+ width: 12px;
+ height: 12px;
+ flex: 0 0 auto;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ vertical-align: -1.5px;
+}
+.rt-pipe-row {
+ display: grid;
+ /* 100px 下限:容器再窄也要保证 5 个框一行,不留「4 个 + 1 个」的孤行 */
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ gap: 10px;
+ margin: 2px 0 8px;
+ transition: opacity 0.3s var(--ease-out);
+}
+.rt-pipe-idle {
+ opacity: 0.55;
+}
+.rt-pipe-cell {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 11px 12px 10px;
+ border-radius: var(--radius-md);
+ background: var(--bg-input);
+ border: 1px solid var(--border-color);
+ min-width: 0;
+ transition:
+ border-color 0.25s var(--ease-out),
+ box-shadow 0.25s var(--ease-out),
+ background 0.25s var(--ease-out);
+}
+.rt-pipe-cell.active {
+ border-color: var(--accent);
+ background: var(--accent-bg);
+ box-shadow: var(--shadow-md);
+}
+.rt-pipe-head {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+.rt-pipe-head > b {
+ font-size: 13.5px;
+ font-weight: 700;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.rt-pipe-head .rt-ico {
+ width: 15px;
+ height: 15px;
+ color: var(--text-secondary);
+}
+.rt-pipe-cell.active .rt-pipe-head > b,
+.rt-pipe-cell.active .rt-pipe-head .rt-ico {
+ color: var(--accent);
+}
+.rt-pipe-events {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ min-height: 23px;
+}
+.rt-loop {
+ display: inline-flex;
+ color: var(--accent);
+ font-weight: 700;
+}
+.rt-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ min-width: 0;
+ max-width: 170px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: var(--radius-pill);
+ background: var(--bg-hover);
+ border: 1px solid var(--border-color);
+ color: var(--text-secondary);
+}
+.rt-chip-tool {
+ border-color: rgba(136, 192, 208, 0.42);
+ color: var(--frost-300);
+}
+.rt-chip-output {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+.rt-chip-stage {
+ opacity: 0.7;
+}
+.rt-chip-none {
+ opacity: 0.35;
+ border-style: dashed;
+}
+.rt-chip-n {
+ font-style: normal;
+ opacity: 0.75;
+ font-variant-numeric: tabular-nums;
+}
+/* 队列:5 个等大框(L4/L3/L2/L1 + 排队),一行排开 */
+.rt-queues {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
+ gap: 10px;
+ margin-bottom: 4px;
+}
+.rt-qcell {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ padding: 11px 12px 10px;
+ border-radius: var(--radius-md);
+ background: var(--bg-input);
+ border: 1px solid var(--border-color);
+ min-width: 0;
+ transition:
+ border-color 0.25s var(--ease-out),
+ box-shadow 0.25s var(--ease-out);
+}
+.rt-qhead {
+ display: flex;
+ align-items: baseline;
+ gap: 7px;
+ min-width: 0;
+}
+.rt-qhead > b {
+ font-size: 16px;
+ font-weight: 800;
+ line-height: 1;
+ letter-spacing: -0.02em;
+}
+.rt-qhead > span {
+ font-size: 10.5px;
+ color: var(--text-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.rt-qnum {
+ font-size: 26px;
+ font-weight: 800;
+ line-height: 1;
+ letter-spacing: -0.02em;
+ font-variant-numeric: tabular-nums;
+}
+.rt-qmeta {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 4px 8px;
+ font-size: 10.5px;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+}
+.rt-qcell.rt-lv-4 .rt-qhead > b { color: #ff5c7a; }
+.rt-qcell.rt-lv-3 .rt-qhead > b { color: #ffa657; }
+.rt-qcell.rt-lv-2 .rt-qhead > b { color: var(--sakura-400); }
+.rt-qcell.rt-lv-1 .rt-qhead > b { color: var(--frost-300); }
+.rt-qcell.rt-lv-q .rt-qhead > b { color: #a3be8c; }
+/* 有积压时整个框描边点亮:一眼看出哪条队列在堵 */
+.rt-qcell.rt-lv-4.rt-active { border-color: rgba(255, 92, 122, 0.55); }
+.rt-qcell.rt-lv-3.rt-active { border-color: rgba(255, 166, 87, 0.55); }
+.rt-qcell.rt-lv-2.rt-active { border-color: var(--sakura-400); }
+.rt-qcell.rt-lv-1.rt-active { border-color: var(--frost-300); }
+.rt-qcell.rt-lv-q.rt-active { border-color: #a3be8c; }
+/* 排队队列无级别:虚线框与四级中断区分(另一**类别**,不是另一优先级) */
+.rt-qcell.rt-qcell-queued {
+ border-style: dashed;
+}
+/* 队列格槽:固定可见的「车位」,占用多少一眼可数。
+ 用进度条时队列为 0 宽度就是 0,整行只剩文字,看上去就是「这块空着」。 */
+.rt-slots {
+ display: flex;
+ align-items: stretch;
+ gap: 3px;
+ height: 16px;
+ min-width: 0;
+}
+.rt-slots > i {
+ flex: 1 1 0;
+ min-width: 3px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ transition:
+ background 0.25s var(--ease-out),
+ box-shadow 0.25s var(--ease-out);
+}
+.rt-slots.rt-lv-4 > i.on { background: #ff5c7a; box-shadow: 0 0 6px rgba(255, 92, 122, 0.45); }
+.rt-slots.rt-lv-3 > i.on { background: #ffa657; box-shadow: 0 0 6px rgba(255, 166, 87, 0.4); }
+.rt-slots.rt-lv-2 > i.on { background: var(--sakura-400); box-shadow: 0 0 6px rgba(255, 127, 172, 0.4); }
+.rt-slots.rt-lv-1 > i.on { background: var(--frost-300); box-shadow: 0 0 6px rgba(136, 192, 208, 0.4); }
+.rt-slots.rt-lv-q > i.on { background: #a3be8c; box-shadow: 0 0 6px rgba(163, 190, 140, 0.4); }
+.rt-slots-more {
+ flex: 0 0 auto;
+ align-self: center;
+ margin-left: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-muted);
+ font-variant-numeric: tabular-nums;
+}
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/common/StatusStore.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/common/StatusStore.ets
index 59e8d9a..0861ce8 100644
--- a/cmd/ohos/HomeAgent/entry/src/main/ets/common/StatusStore.ets
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/common/StatusStore.ets
@@ -37,6 +37,8 @@ export const K_TOOLS: string = 'statTools';
export const K_ERR: string = 'statErr';
export const K_LOADING: string = 'statLoading';
export const K_REV: string = 'statRev';
+/** 内核身份副行:内核名 · commit。版本号光有一个号码分不清是哪个内核、哪次构建。 */
+export const K_BUILD: string = 'statBuildSub';
class StatusStore {
/** 明细分组:只有明细页读它,不进 AppStorage(数组同步语义太脆) */
@@ -52,6 +54,7 @@ class StatusStore {
AppStorage.setOrCreate(K_ERR, '');
AppStorage.setOrCreate(K_LOADING, false);
AppStorage.setOrCreate(K_REV, 0);
+ AppStorage.setOrCreate(K_BUILD, '');
}
getGroups(): StatGroup[] {
@@ -133,8 +136,66 @@ class StatusStore {
if (startTime.length > 0) {
kernelFields.push({ label: '内核启动', value: formatTime(startTime) });
}
+
+ // 构建身份:版本 / commit / SDK 兼容都取自 /kernel 的 build(-ldflags 注入)。
+ // 为何不能只用 /status 的 version:那里只有一个版本号,分不清是哪个内核、
+ // 哪次构建;许可标识更是完全没有。
+ const build: Record | undefined = k['build'] as Record;
+ let lic: string = '';
+ let licURL: string = '';
+ let srcURL: string = '';
+ if (build !== undefined && build !== null) {
+ const bVer: string = build['version'] as string ?? '';
+ const bCommit: string = build['commit'] as string ?? '';
+ const bName: string = build['kernel_name'] as string ?? 'HomeAgent';
+ const bSdk: string = build['sdk_compatible'] as string ?? '';
+ const bTime: string = build['build_time'] as string ?? '';
+ lic = build['license'] as string ?? '';
+ licURL = build['license_url'] as string ?? '';
+ srcURL = build['source_url'] as string ?? '';
+ if (bVer.length > 0) {
+ AppStorage.set(K_VERSION, 'v' + bVer);
+ kernelFields.push({ label: '内核版本', value: 'v' + bVer });
+ }
+ let sub: string = bName;
+ if (bCommit.length > 0 && bCommit !== 'unknown') {
+ sub = sub + ' · ' + bCommit.substring(0, 7);
+ }
+ AppStorage.set(K_BUILD, sub);
+ if (bCommit.length > 0) {
+ kernelFields.push({ label: 'Commit', value: bCommit });
+ }
+ if (bSdk.length > 0) {
+ kernelFields.push({ label: 'SDK 兼容', value: bSdk });
+ }
+ if (bTime.length > 0 && bTime !== 'unknown') {
+ kernelFields.push({ label: '构建时间', value: formatTime(bTime) });
+ }
+ }
groups.push({ title: '内核', fields: kernelFields });
+ // 开源许可:只给一个源码链接、不写协议名,使用者看不出这受什么许可约束,
+ // 也看不出网络服务场景下的 §13 义务。
+ const legalFields: StatField[] = [];
+ if (lic.length > 0) {
+ legalFields.push({ label: '许可协议', value: lic });
+ }
+ if (licURL.length > 0) {
+ legalFields.push({ label: '协议全文', value: licURL });
+ }
+ if (srcURL.length > 0) {
+ legalFields.push({ label: '源码仓库', value: srcURL });
+ }
+ if (legalFields.length > 0) {
+ if (lic.toUpperCase().indexOf('AGPL') >= 0) {
+ legalFields.push({
+ label: '网络条款',
+ value: '把修改后的版本作为网络服务对外提供时,必须向使用者提供取得对应源码的途径(§13)',
+ });
+ }
+ groups.push({ title: '开源许可', fields: legalFields });
+ }
+
// LLM:provider / 可用源 / 是否可用
const llm: Record | undefined = k['llm'] as Record;
if (llm !== undefined && llm !== null) {
diff --git a/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets b/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets
index 29b06f3..1045785 100644
--- a/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets
+++ b/cmd/ohos/HomeAgent/entry/src/main/ets/components/StatusCards.ets
@@ -4,7 +4,7 @@ import { COLOR_ACCENT, COLOR_SUCCESS, COLOR_CYAN, COLOR_ERROR } from '../common/
import { MotionBase } from './MotionBase';
import {
statusStore, StatGroup, StatField, compactDuration,
- K_UP, K_VERSION, K_STARTED, K_AGENTS, K_PLUGINS, K_TOOLS, K_ERR, K_LOADING, K_REV,
+ K_UP, K_VERSION, K_STARTED, K_AGENTS, K_PLUGINS, K_TOOLS, K_ERR, K_LOADING, K_REV, K_BUILD,
} from '../common/StatusStore';
/**
@@ -19,6 +19,8 @@ export struct StatusSummaryCard {
@StorageProp('themeIsDark') private isDark: boolean = true;
@StorageProp(K_UP) private up: boolean = false;
@StorageProp(K_VERSION) private version: string = '-';
+ /** 内核名 · commit。与版本号分开一行:版本号本身没有内核身份。 */
+ @StorageProp(K_BUILD) private buildSub: string = '';
@StorageProp(K_STARTED) private startedAt: string = '';
@StorageProp(K_AGENTS) private agents: number = 0;
@StorageProp(K_PLUGINS) private plugins: number = 0;
@@ -130,14 +132,24 @@ export struct StatusSummaryCard {
Text('版本')
.fontSize(12)
.fontColor(this.palette().textMuted)
- Text(this.version)
- .fontSize(12)
- .fontWeight(FontWeight.Medium)
- .fontColor(this.palette().textSecondary)
- .layoutWeight(1)
- .maxLines(1)
- .textOverflow({ overflow: TextOverflow.Ellipsis })
- .margin({ left: 10 })
+ Column() {
+ Text(this.version)
+ .fontSize(12)
+ .fontWeight(FontWeight.Medium)
+ .fontColor(this.palette().textSecondary)
+ .maxLines(1)
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
+ // 内核名 + commit:光有版本号会分不清是哪个内核、哪次构建
+ Text(this.buildSub)
+ .fontSize(10)
+ .fontColor(this.palette().textMuted)
+ .maxLines(1)
+ .textOverflow({ overflow: TextOverflow.Ellipsis })
+ .visibility(this.buildSub.length > 0 ? Visibility.Visible : Visibility.None)
+ }
+ .alignItems(HorizontalAlign.Start)
+ .layoutWeight(1)
+ .margin({ left: 10 })
Text('明细')
.fontSize(12)
.fontColor(this.palette().accent)