mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
fix(webui): 运行态面板不再闪、通道分配带归属(含驻留子)、改图形化
三个用户可见问题,逐个说明根因与改法。 1) 首页「一闪一闪」——运行态每 3s 轮询一次,renderRuntime 无条件重建 #rt-panel 的 innerHTML:数据没变也把整块 DOM(含各级条的 transition) 推倒重来。改法:缓存数据签名(**不含 uptime**——它每秒都变,带上等于没缓存), 签名相同直接 return,一个字节都不动。另:renderOverview 会整块重建 #rt-panel(面板本身是空的),所以那里必须让签名失效,否则空面板填不上。 2) 通道分配只显示内核/根 agent,看不见驻留子——根因是状态面只暴露了设备能力 (KernelStatus.Channels,来自 iom.ListChannels),而「这条输入归谁」是 ChannelRegistry 的属性(InputChannel.Owner/Capacity/Output),从未出过内核。 而登记表本来就是根 agent 与驻留子**共用同一份**,所以数据一直都在,只是没画。 改法:KernelStatus 新增 InputChannels(+ sdk.InputChannelInfo), /api/v1/runtime 带出 input_channels;前端把它按 owner 分进「归属容器」, 驻留子即使一条 inputch 都没划到也照样出现在图里(否则"子存在但看不见" 与"子不存在"无法区分),并显示其 allowed_outputs / 轮次 / 上下文满标记。 3) 「这些信息明明可以图形化」——四级中断的登记/抢占由纯文本改成并排迷你条; 通道分配用归属容器 + 容量滑块(轨道/填充/把手/读数),并把回程通道、 注册插件做成胶囊标签。设备能力拓扑(原有)保留。 验证:go build/vet 干净;go test ./internal/agent/... ./internal/plugin/... ./internal/sdk/... ./cmd/... 全绿;node --check dashboard.js 语法通过。 TestRuntimeEndpoint 扩展为同时钉住 input_channels 的归属与「驻留子划走的那条」。
This commit is contained in:
@ -26,6 +26,9 @@ type KernelStatus = sdk.KernelStatus
|
||||
type PluginInfo = sdk.PluginInfo
|
||||
type ChannelInfo = sdk.ChannelInfo
|
||||
|
||||
// InputChannelInfo 是 inputch 的登记与归属视图(见 internal/sdk/status.go)。
|
||||
type InputChannelInfo = sdk.InputChannelInfo
|
||||
|
||||
// ResidentStatus 是驻留式子 agent 的运行时视图(见 internal/sdk/status.go)。
|
||||
type ResidentStatus = sdk.ResidentStatus
|
||||
type MemoryStatus = sdk.MemoryStatus
|
||||
@ -130,6 +133,18 @@ func collectKernelStatus(
|
||||
for _, ch := range iom.ListChannels() {
|
||||
status.Channels = append(status.Channels, channelInfoFromIO(ch))
|
||||
}
|
||||
// inputch 登记与归属:与 Channels(设备能力)互补——那条回答「能做什么」,
|
||||
// 这条回答「这条输入归谁」。登记表是根 agent 与驻留子共用的,所以驻留子
|
||||
// 划走的 inputch(owner=子 id)也会出现在这里。
|
||||
for _, ic := range iom.InputChannels() {
|
||||
status.InputChannels = append(status.InputChannels, InputChannelInfo{
|
||||
Name: ic.Name,
|
||||
Plugin: ic.Plugin,
|
||||
Owner: ic.Owner,
|
||||
Capacity: ic.Capacity,
|
||||
Output: ic.Output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Graph memory
|
||||
|
||||
@ -2262,6 +2262,135 @@
|
||||
background: rgba(136, 192, 208, 0.16);
|
||||
color: var(--frost-300);
|
||||
}
|
||||
/* —— 通道分配(按归属):容器 + 容量滑块 ——
|
||||
为什么用容器而不是一排文字:归属是“分组”语义,只有把同一 owner 的
|
||||
inputch 画进同一个盒子,才能一眼看出“这条输入被划给了哪个驻留子”。 */
|
||||
.rt-owner {
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid var(--frost-300);
|
||||
border-radius: 8px;
|
||||
padding: 7px 9px;
|
||||
margin-bottom: 7px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.rt-owner.rt-owner-child {
|
||||
border-left-color: #b48ead;
|
||||
background: rgba(180, 142, 173, 0.07);
|
||||
}
|
||||
.rt-owner-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.rt-owner-name {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.rt-owner-meta {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.rt-assign {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.06);
|
||||
font-size: 11px;
|
||||
}
|
||||
.rt-assign .rt-chan-name {
|
||||
font-weight: 600;
|
||||
min-width: 84px;
|
||||
}
|
||||
.rt-assign .rt-chan-sub {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
}
|
||||
.rt-chip {
|
||||
font-size: 9px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(163, 190, 140, 0.16);
|
||||
color: var(--text-secondary, var(--text-muted));
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rt-badge-warn {
|
||||
font-size: 9px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(191, 97, 106, 0.22);
|
||||
color: #d08770;
|
||||
}
|
||||
/* 容量滑块:轨道 + 填充 + 把手 + 读数。 */
|
||||
.rt-slider {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.rt-slider-label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.rt-slider-track {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
overflow: visible;
|
||||
}
|
||||
.rt-slider-track > i {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--frost-300);
|
||||
}
|
||||
.rt-slider-track > b {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-left: -4.5px;
|
||||
margin-top: -4.5px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.rt-slider-val {
|
||||
font-size: 10px;
|
||||
min-width: 34px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
/* 行内迷你条(登记 / 抢占并排) */
|
||||
.rt-mini {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-left: 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.rt-mini-track {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.rt-mini-track > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: var(--frost-300);
|
||||
}
|
||||
.rt-core {
|
||||
width: 74px;
|
||||
height: 74px;
|
||||
|
||||
@ -583,6 +583,38 @@
|
||||
];
|
||||
var RT_CAP_NAMES = { 1: "text", 2: "file", 4: "image", 8: "audio", 16: "structured" };
|
||||
|
||||
// _rtSig 缓存上一次渲染的数据签名。
|
||||
//
|
||||
// 为什么必须缓存:运行态每 3s 轮询一次,数据绝大多数时候是**没变**的;
|
||||
// 无条件 `innerHTML =` 会把整块 DOM(含各级条的 transition)每 3s 重建一遍,
|
||||
// 视觉效果就是“首页一闪一闪”。签名相同就一个字节也不动。
|
||||
var _rtSig = null;
|
||||
|
||||
// rtSlider 把“值 / 上限”画成轨道 + 滑块 + 读数(比纯文本数字直观)。
|
||||
function rtSlider(value, max, label, display) {
|
||||
var pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
|
||||
return (
|
||||
'<span class="rt-slider"><span class="rt-slider-label">' +
|
||||
escHtml(label) +
|
||||
'</span><span class="rt-slider-track"><i style="width:' + pct + '%"></i><b style="left:' + pct + '%"></b></span>' +
|
||||
'<span class="rt-slider-val">' +
|
||||
escHtml(String(display === undefined ? value : display)) +
|
||||
"</span></span>"
|
||||
);
|
||||
}
|
||||
|
||||
// rtMini 是行内迷你条(登记/抢占这类要并排两个的量)。
|
||||
function rtMini(value, max, label) {
|
||||
var pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
|
||||
return (
|
||||
'<span class="rt-mini" title="' +
|
||||
escHtml(label) + " " + value +
|
||||
'"><span class="rt-mini-track"><i style="width:' + pct + '%"></i></span>' +
|
||||
value +
|
||||
"</span>"
|
||||
);
|
||||
}
|
||||
|
||||
function rtTile(num, label, sub, pct, active, warn) {
|
||||
return (
|
||||
'<div class="rt-tile' +
|
||||
@ -616,11 +648,18 @@
|
||||
var rt = state.runtime;
|
||||
if (!rt) {
|
||||
el.innerHTML = '<div class="rt-empty">' + __("运行态数据不可用", "runtime unavailable") + "</div>";
|
||||
_rtSig = null;
|
||||
return;
|
||||
}
|
||||
var sc = rt.scheduler || {};
|
||||
var residents = rt.residents || [];
|
||||
var channels = rt.channels || [];
|
||||
var inputs = rt.input_channels || [];
|
||||
// 数据签名(**不含 uptime**——它每秒都变,带上就等于没缓存):
|
||||
// 与上次相同则直接返回,一个字节都不动(“首页一闪一闪”的根因)。
|
||||
var sig = JSON.stringify([sc, residents, channels, inputs]);
|
||||
if (sig === _rtSig) return;
|
||||
_rtSig = sig;
|
||||
var q = sc.interrupt_queues || [0, 0, 0, 0, 0];
|
||||
var pending = sc.pending_interrupts || 0;
|
||||
var ready = sc.ready_queue_depth || 0;
|
||||
@ -643,6 +682,12 @@
|
||||
// 四级中断队列
|
||||
html += '<div class="rt-section-title">' + __("中断队列(按级别)", "Interrupt queues by level") + "</div>";
|
||||
var maxQ = Math.max(1, q[1] || 0, q[2] || 0, q[3] || 0, q[4] || 0);
|
||||
var maxReg = 1;
|
||||
var maxPre = 1;
|
||||
RT_LEVELS.forEach(function (L) {
|
||||
maxReg = Math.max(maxReg, byLv[L.lv] || 0);
|
||||
maxPre = Math.max(maxPre, preLv[L.lv] || 0);
|
||||
});
|
||||
html += '<div class="rt-levels">';
|
||||
RT_LEVELS.forEach(function (L) {
|
||||
var depth = q[L.lv] || 0;
|
||||
@ -654,7 +699,8 @@
|
||||
(depth ? Math.max(4, (depth / maxQ) * 100) : 0) +
|
||||
'%"></i></span>' +
|
||||
'<span class="rt-lv-meta">' + depth + " · " +
|
||||
__("登记", "reg") + " " + reg + " / " + __("抢占", "pre") + " " + pre +
|
||||
rtMini(reg, maxReg, __("登记", "registered")) +
|
||||
rtMini(pre, maxPre, __("抢占", "preempted")) +
|
||||
"</span></div>";
|
||||
});
|
||||
html += "</div>";
|
||||
@ -683,7 +729,82 @@
|
||||
escHtml(sc.running.kind || "") + " #" + sc.running.id + " (L" + (sc.running.level || 0) + ")</div>";
|
||||
}
|
||||
|
||||
// 通道拓扑
|
||||
// 通道**分配(按归属)**:inputch 是输入路由单位,而登记表由根 agent 与
|
||||
// 驻留子**共用同一份**——所以“这条输入归谁”必须画出来。只画设备能力
|
||||
// (下面的拓扑)等于把“路由发生在进内核之前”这条设计事实藏起来,
|
||||
// 于是驻留子的通道分配在界面上完全不可见。
|
||||
html += '<div class="rt-section-title">' + __("通道分配(按归属)", "Input channels by owner") + "</div>";
|
||||
var maxCap = 1;
|
||||
inputs.forEach(function (c) {
|
||||
if ((c.capacity || 0) > maxCap) maxCap = c.capacity;
|
||||
});
|
||||
var owners = [];
|
||||
var byOwner = {};
|
||||
inputs.forEach(function (c) {
|
||||
var o = c.owner || "";
|
||||
if (!byOwner[o]) {
|
||||
byOwner[o] = [];
|
||||
owners.push(o);
|
||||
}
|
||||
byOwner[o].push(c);
|
||||
});
|
||||
// 驻留子即使一条 inputch 也没划到,也要出现在分配图里——
|
||||
// 否则“子存在但界面上看不见”与“子不存在”无法区分。
|
||||
residents.forEach(function (r) {
|
||||
var o = r.id || "";
|
||||
if (o && !byOwner[o]) {
|
||||
byOwner[o] = [];
|
||||
owners.push(o);
|
||||
}
|
||||
});
|
||||
owners.sort(function (a, b) {
|
||||
if (a === "") return -1;
|
||||
if (b === "") return 1;
|
||||
return a < b ? -1 : 1;
|
||||
});
|
||||
if (!owners.length) {
|
||||
html += '<div class="rt-empty">' + __("暂无通道登记", "no channel registered") + "</div>";
|
||||
}
|
||||
owners.forEach(function (o) {
|
||||
var list = byOwner[o] || [];
|
||||
var res = null;
|
||||
if (o) {
|
||||
residents.forEach(function (r) {
|
||||
if (r.id === o) res = r;
|
||||
});
|
||||
}
|
||||
html += '<div class="rt-owner' + (o ? " rt-owner-child" : "") + '">';
|
||||
html += '<div class="rt-owner-head"><span class="rt-owner-name">' +
|
||||
(o ? "▸ " : "◆ ") +
|
||||
(o ? __("驻留子 ", "resident ") + escHtml(o) : __("根 agent / 内核默认", "root agent / kernel default")) +
|
||||
'</span><span class="rt-owner-meta">' + list.length + " " + __("条通道", "channels") +
|
||||
(res ? " · " + __("轮次", "rounds") + " " + (res.rounds || 0) : "") +
|
||||
(res && res.context_full ? ' <span class="rt-badge-warn">' + __("上下文已满", "ctx full") + "</span>" : "") +
|
||||
"</span></div>";
|
||||
if (list.length) {
|
||||
list.forEach(function (c) {
|
||||
var cap = c.capacity || 0;
|
||||
html += '<div class="rt-assign">' +
|
||||
'<span class="rt-chan-name">' + escHtml(c.name) + "</span>" +
|
||||
(c.plugin ? '<span class="rt-chip">' + escHtml(c.plugin) + "</span>" : "") +
|
||||
rtSlider(cap, maxCap, __("容量", "cap"), cap ? String(cap) : __("默认", "default")) +
|
||||
'<span class="rt-chan-sub">' +
|
||||
(c.output ? __("回程 ", "out ") + escHtml(c.output) : __("回程由来源决定", "out by source")) +
|
||||
"</span></div>";
|
||||
});
|
||||
} else {
|
||||
var allowed = (res && res.allowed_outputs) || [];
|
||||
html += '<div class="rt-chan-sub">' +
|
||||
(allowed.length
|
||||
? __("可发往输出通道:", "allowed outputs: ") +
|
||||
allowed.map(function (x) { return '<span class="rt-chip">' + escHtml(x) + "</span>"; }).join("")
|
||||
: __("未划入任何 inputch", "no input channel assigned")) +
|
||||
"</div>";
|
||||
}
|
||||
html += "</div>";
|
||||
});
|
||||
|
||||
// 通道拓扑(设备能力面:能收什么、能发什么、有哪些工具)
|
||||
html += '<div class="rt-section-title">' + __("通道拓扑", "Channel topology") + "</div>";
|
||||
var ins = channels.filter(function (c) { return c.direction === "in" || c.direction === "io"; });
|
||||
var outs = channels.filter(function (c) { return c.direction === "out" || c.direction === "io"; });
|
||||
@ -730,6 +851,9 @@
|
||||
}
|
||||
|
||||
function renderOverview() {
|
||||
// 总览页会整块重建 #rt-panel(面板本身是空的),所以必须让运行态的
|
||||
// 数据签名失效,否则 renderRuntime 会以为“没变化”而不去填这块空面板。
|
||||
_rtSig = null;
|
||||
var s = state.status || {};
|
||||
var k = state.kernel;
|
||||
var html =
|
||||
|
||||
@ -265,5 +265,8 @@ func (h *Handler) handleRuntime(w http.ResponseWriter, r *http.Request) {
|
||||
"scheduler": ks.Scheduler,
|
||||
"residents": ks.Residents,
|
||||
"channels": ks.Channels,
|
||||
// inputch 的登记与归属(谁注册、划给了哪个 agent、容量、默认回程)。
|
||||
// 只给设备能力(channels)无法回答「这条输入归谁」——驻留子存在时这就是缺口。
|
||||
"input_channels": ks.InputChannels,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1409,6 +1409,13 @@ func TestRuntimeEndpoint(t *testing.T) {
|
||||
},
|
||||
Residents: []sdk.ResidentStatus{{ID: "r1", State: "running", Rounds: 3}},
|
||||
Channels: []sdk.ChannelInfo{{Name: "webui", Type: "1", Direction: "out", Ready: true, OutputCaps: 7, CapsText: "[text file image]"}},
|
||||
// inputch 的登记与归属:必须带出**驻留子划走的那条**(owner=子 id)——
|
||||
// 只回设备能力(channels)就无法回答「这条输入归谁」,驻留子的通道分配
|
||||
// 在界面上完全不可见(本次修的缺口)。
|
||||
InputChannels: []sdk.InputChannelInfo{
|
||||
{Name: "qq", Plugin: "qq", Capacity: 32, Output: "qq"},
|
||||
{Name: "timer", Plugin: "timer", Owner: "r1", Capacity: 8},
|
||||
},
|
||||
}
|
||||
s := testSDK(sdk.SDKConfig{
|
||||
Settings: sdk.NewSettings("webui", internalConfig.NewConfigRegistry("")),
|
||||
@ -1424,9 +1431,10 @@ func TestRuntimeEndpoint(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var out struct {
|
||||
Scheduler sdk.SchedulerStatus `json:"scheduler"`
|
||||
Residents []sdk.ResidentStatus `json:"residents"`
|
||||
Channels []sdk.ChannelInfo `json:"channels"`
|
||||
Scheduler sdk.SchedulerStatus `json:"scheduler"`
|
||||
Residents []sdk.ResidentStatus `json:"residents"`
|
||||
Channels []sdk.ChannelInfo `json:"channels"`
|
||||
InputChannels []sdk.InputChannelInfo `json:"input_channels"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
@ -1446,6 +1454,25 @@ func TestRuntimeEndpoint(t *testing.T) {
|
||||
if len(out.Channels) != 1 || out.Channels[0].Direction != "out" {
|
||||
t.Fatalf("通道方向丢失:%+v", out.Channels)
|
||||
}
|
||||
// 通道分配:既要带归属,也要带**驻留子划走的那条**
|
||||
if len(out.InputChannels) != 2 {
|
||||
t.Fatalf("input_channels 应回 2 条,实际 %+v", out.InputChannels)
|
||||
}
|
||||
var childCh, rootCh *sdk.InputChannelInfo
|
||||
for i := range out.InputChannels {
|
||||
switch out.InputChannels[i].Owner {
|
||||
case "":
|
||||
rootCh = &out.InputChannels[i]
|
||||
case "r1":
|
||||
childCh = &out.InputChannels[i]
|
||||
}
|
||||
}
|
||||
if rootCh == nil || rootCh.Name != "qq" || rootCh.Capacity != 32 || rootCh.Output != "qq" {
|
||||
t.Fatalf("根 agent 的 inputch 归属/容量/回程丢失:%+v", rootCh)
|
||||
}
|
||||
if childCh == nil || childCh.Name != "timer" || childCh.Capacity != 8 {
|
||||
t.Fatalf("驻留子划走的 inputch 未带出(这正是本次修的缺口):%+v", childCh)
|
||||
}
|
||||
// 只回运行态:不该把 tools/plugins 这类大块带上
|
||||
if strings.Contains(w.Body.String(), "\"tools\"") || strings.Contains(w.Body.String(), "\"plugins\"") {
|
||||
t.Fatal("/runtime 不应携带 tools/plugins(那是 /kernel 的内容)")
|
||||
|
||||
@ -25,6 +25,13 @@ type KernelStatus struct {
|
||||
Plugins []PluginInfo `json:"plugins"`
|
||||
Tools []ToolDef `json:"tools"`
|
||||
Channels []ChannelInfo `json:"channels"`
|
||||
// InputChannels 是 inputch 的**登记与归属**视图(谁注册、划给了哪个 agent、
|
||||
// 容量、默认回程输出)。
|
||||
//
|
||||
// 为何单列:Channels 只描述设备侧能力,回答不了「这条输入归谁」。驻留子 agent
|
||||
// 出现之后,归属才是运行态里最该看见的东西——只画设备能力,等于把
|
||||
// 「输入路由发生在进内核之前」这条设计事实藏了起来。
|
||||
InputChannels []InputChannelInfo `json:"input_channels"`
|
||||
|
||||
Memory MemoryStatus `json:"memory"`
|
||||
Knowledge KnowledgeStatus `json:"knowledge"`
|
||||
@ -156,6 +163,22 @@ type ChannelInfo struct {
|
||||
CapsText string `json:"caps_text,omitempty"`
|
||||
}
|
||||
|
||||
// InputChannelInfo 是一条 inputch 的登记与归属视图。
|
||||
//
|
||||
// 字段取自 agent/io 的 ChannelRegistry(根 agent 与驻留子**共用同一份**登记表),
|
||||
// 因此它天然覆盖驻留子的通道分配。
|
||||
type InputChannelInfo struct {
|
||||
Name string `json:"name"`
|
||||
// Plugin 是注册它的插件名(归属可追溯)。
|
||||
Plugin string `json:"plugin,omitempty"`
|
||||
// Owner 是被划给的 agent id;"" = 未分配,归根 agent / 内核默认。
|
||||
Owner string `json:"owner,omitempty"`
|
||||
// Capacity 是该 inputch 的队列容量;0 = 用内核默认值。
|
||||
Capacity int `json:"capacity,omitempty"`
|
||||
// Output 是该 inputch 的默认回程输出通道;"" = 由来源决定。
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
type MemoryStatus struct {
|
||||
Available bool `json:"available"`
|
||||
EntityCount int `json:"entity_count"`
|
||||
|
||||
Reference in New Issue
Block a user