niuma-codex 0.0.1-beta.28 → 0.0.1-beta.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/control/app.js +8 -0
- package/dist/assets/control/index.html +4 -0
- package/dist/assets/control/thread-loads.js +175 -0
- package/dist/cli.js +1943 -1421
- package/dist/managed-tauri.js +593 -129
- package/dist/managed.js +564 -123
- package/package.json +1 -1
- package/resources/lite-release.json +7 -7
- package/resources/niuma-daemon-arm64.tgz +0 -0
- package/resources/niuma-lite-arm64.dmg +0 -0
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"/connections": { key: "connections", title: "外部连接", intro: "查看当前设备连接与额度占用,页面可见时每 3 秒刷新。", sectionId: "control-page-connections", navId: "control-nav-connections" },
|
|
52
52
|
"/": { key: "overview", title: "概览", intro: "查看当前设备上的 daemon 运行概览和生命周期。", sectionId: "control-page-overview", navId: "control-nav-overview" },
|
|
53
53
|
"/sessions": { key: "sessions", title: "会话跟踪", intro: "查看 follower 生命周期、共享订阅和租约快照。", sectionId: "control-page-sessions", navId: "control-nav-sessions" },
|
|
54
|
+
"/thread-loads": { key: "thread-loads", title: "会话加载", intro: "查看 Web / iOS 从打开会话到首屏正文可见的完整过程。", sectionId: "control-page-thread-loads", navId: "control-nav-thread-loads" },
|
|
54
55
|
"/subscriptions": { key: "subscriptions", title: "会话订阅", intro: "查看最近 24 小时的会话订阅 Trace。", sectionId: "control-page-subscriptions", navId: "control-nav-subscriptions" },
|
|
55
56
|
"/turn-audit": { key: "turn-audit", title: "Turn 推送审计", intro: "查看最近 24 小时的安全投递证据。", sectionId: "control-page-turn-audit", navId: "control-nav-turn-audit" },
|
|
56
57
|
"/logs": { key: "logs", title: "诊断日志", intro: "查看安全投影后的错误与调试日志。", sectionId: "control-page-logs", navId: "control-nav-logs" },
|
|
@@ -87,6 +88,9 @@
|
|
|
87
88
|
controlPageTitle.textContent = currentPage.title;
|
|
88
89
|
controlPageIntro.textContent = currentPage.intro;
|
|
89
90
|
|
|
91
|
+
const threadLoadsPage = currentPage.key === "thread-loads"
|
|
92
|
+
? window.NiumaThreadLoads?.mount({ root: document.getElementById("control-page-thread-loads"), request: getControlJson }) : null;
|
|
93
|
+
|
|
90
94
|
// 旧的最小 DOM 测试夹具不含 Trace 区域;真实静态页面始终完整启用此功能。
|
|
91
95
|
const followerTracesEnabled = Boolean(
|
|
92
96
|
followerTracesList && followerTracesEmpty && followerTracesCoverage,
|
|
@@ -796,6 +800,9 @@
|
|
|
796
800
|
if (slowest) summary.append(textElement("div", `当前已采集浏览器阶段中耗时最长:${slowest.label}(${slowest.ms} ms)`, "small mt-2"));
|
|
797
801
|
summary.append(textElement("div", "浏览器数据为客户端测量;两端时钟独立,不作跨端差值。首屏不包含图片全部解码完成,缺失采样不计为 0。", "small mt-2"));
|
|
798
802
|
container.append(summary);
|
|
803
|
+
const loadLink = textElement("a", "在会话加载中查看", "small d-block mb-2");
|
|
804
|
+
loadLink.href = `/thread-loads?load_id=${encodeURIComponent(steps[0].loading.load_id)}`;
|
|
805
|
+
container.append(loadLink);
|
|
799
806
|
container.append(textElement("p", `加载编号:${steps[0].loading.load_id}`, "small font-monospace text-break text-body-secondary"));
|
|
800
807
|
if (data.coverage?.status !== "complete") container.append(textElement("p", followerTraceCoverageLabel(data.coverage), "text-body-secondary small"));
|
|
801
808
|
for (const [label, entries] of [["浏览器时间线", browser], ["Desktop 时间线", daemon]]) {
|
|
@@ -1565,6 +1572,7 @@
|
|
|
1565
1572
|
}
|
|
1566
1573
|
|
|
1567
1574
|
async function refreshCurrentPage() {
|
|
1575
|
+
if (currentPage.key === "thread-loads") return threadLoadsPage?.refresh();
|
|
1568
1576
|
if (currentPage.key === "connections") return refreshConnections();
|
|
1569
1577
|
controlRefreshGeneration += 1;
|
|
1570
1578
|
const refreshGeneration = controlRefreshGeneration;
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
</style>
|
|
79
79
|
<script src="/bootstrap.bundle.min.js" defer></script>
|
|
80
80
|
<script src="/chart.umd.js" defer></script>
|
|
81
|
+
<script src="/thread-loads.js" defer></script>
|
|
81
82
|
<script src="/app.js" defer></script>
|
|
82
83
|
</head>
|
|
83
84
|
<body class="bg-body-tertiary">
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
<a id="control-nav-connections" class="nav-link" href="/connections">外部连接</a>
|
|
106
107
|
<a id="control-nav-sessions" class="nav-link" href="/sessions">会话跟踪</a>
|
|
107
108
|
<a id="control-nav-subscriptions" class="nav-link" href="/subscriptions">会话订阅</a>
|
|
109
|
+
<a id="control-nav-thread-loads" class="nav-link" href="/thread-loads">会话加载</a>
|
|
108
110
|
<a id="control-nav-turn-audit" class="nav-link" href="/turn-audit">Turn 推送审计</a>
|
|
109
111
|
<a id="control-nav-logs" class="nav-link" href="/logs">诊断日志</a>
|
|
110
112
|
</div>
|
|
@@ -219,6 +221,8 @@
|
|
|
219
221
|
</section>
|
|
220
222
|
</section>
|
|
221
223
|
|
|
224
|
+
<section id="control-page-thread-loads" class="control-page" hidden></section>
|
|
225
|
+
|
|
222
226
|
<section id="control-page-subscriptions" class="control-page" hidden>
|
|
223
227
|
<section id="follower-traces-section" class="card shadow-sm mb-4" aria-labelledby="follower-traces-heading">
|
|
224
228
|
<div class="card-body">
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
const phases = { selected: '选择会话', restored: '恢复会话', request_started: '发起请求', snapshot_received: '收到快照', history_ready: '首屏数据就绪', content_presented: '首屏正文可见', open_received: '收到打开请求', identity_ready: '身份核验完成', lease_acquired: '取得订阅租约', shell_sent: '发布加载壳', history_sent: '发布首屏正文', stream_closed: '订阅关闭', closed: '采样关闭', failed: '业务失败', cancelled: '已取消', superseded: '已被替代' };
|
|
4
|
+
const statuses = { loading: '加载中', presented: '首屏正文可见', failed: '失败', cancelled: '已取消', superseded: '已被替代', incomplete: '采样不完整' };
|
|
5
|
+
const clients = { web: 'Web', ios: 'iOS', other: '其他', unknown: '未知' };
|
|
6
|
+
const transports = { local: 'Local', direct: 'Direct', relay: 'Relay', unknown: '未采集' };
|
|
7
|
+
const ms = value => Number.isFinite(value) ? `${value} ms` : '未采集';
|
|
8
|
+
const time = value => Number.isFinite(value) ? new Date(value).toLocaleString() : '未采集';
|
|
9
|
+
function el(tag, text, className = '') {
|
|
10
|
+
const node = document.createElement(tag); if (text !== undefined) node.textContent = text;
|
|
11
|
+
node.className = className; return node;
|
|
12
|
+
}
|
|
13
|
+
function button(text, action, id) {
|
|
14
|
+
const node = el('button', text, 'btn btn-outline-secondary btn-sm'); node.type = 'button';
|
|
15
|
+
if (id) node.id = id; node.addEventListener('click', action); return node;
|
|
16
|
+
}
|
|
17
|
+
function coverageText(value) {
|
|
18
|
+
return ({ complete: '覆盖完整', partial: '采样不完整', historical_unverified: '历史记录,当前 daemon 未运行' })[value?.status] ?? '未采集覆盖信息';
|
|
19
|
+
}
|
|
20
|
+
function mount({ root, request }) {
|
|
21
|
+
let page = 1, detailPage = 1, total = 0, generation = 0, inFlight = null, queued = false;
|
|
22
|
+
let disposed = false, pageClosed = false, paused = false, timer = null, selected = null;
|
|
23
|
+
const linked = new URLSearchParams(window.location.search).get('load_id');
|
|
24
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(linked ?? '')) selected = linked;
|
|
25
|
+
const rows = new Map();
|
|
26
|
+
const note = el('p', '完成点为首屏正文可见(含真实空会话);图片、附件下载和更早历史不阻塞。', 'text-body-secondary');
|
|
27
|
+
const limits = el('p', '最近 24 小时可读取的诊断记录。未抵达 Desktop、认证前拒绝及客户端退出可能没有完整采样。', 'small text-body-secondary');
|
|
28
|
+
const form = el('div', undefined, 'd-flex flex-wrap gap-3 align-items-end mb-3');
|
|
29
|
+
function field(label, id, options) {
|
|
30
|
+
const group = el('label', undefined, 'd-flex flex-column gap-1 small'); group.append(el('span', label));
|
|
31
|
+
const input = el(options ? 'select' : 'input', undefined, options ? 'form-select form-select-sm' : 'form-control form-control-sm');
|
|
32
|
+
input.id = id;
|
|
33
|
+
if (options) for (const [value, text] of Object.entries(options)) { const option = el('option', text); option.value = value; input.append(option); }
|
|
34
|
+
else { input.type = 'text'; input.placeholder = '输入完整会话 ID'; input.maxLength = 256; }
|
|
35
|
+
input.addEventListener('change', () => { page = 1; void refresh(); });
|
|
36
|
+
group.append(input); form.append(group); return input;
|
|
37
|
+
}
|
|
38
|
+
const thread = field('会话 ID', 'thread-load-filter-thread');
|
|
39
|
+
const client = field('客户端', 'thread-load-filter-client', { '': '全部客户端', ...clients });
|
|
40
|
+
const status = field('结果', 'thread-load-filter-status', { '': '全部结果', ...statuses });
|
|
41
|
+
const autoLabel = el('label', undefined, 'form-check d-flex align-items-center gap-2 mb-1');
|
|
42
|
+
const auto = el('input', undefined, 'form-check-input mt-0'); auto.type = 'checkbox'; auto.id = 'thread-load-auto';
|
|
43
|
+
autoLabel.append(auto, el('span', '前台每 3 秒自动刷新', 'small')); form.append(autoLabel);
|
|
44
|
+
const notice = el('p', '', 'small text-body-secondary'); notice.id = 'thread-load-notice'; notice.setAttribute('role', 'status');
|
|
45
|
+
const coverage = el('p', '尚未读取', 'small text-body-secondary'); coverage.id = 'thread-load-coverage';
|
|
46
|
+
const list = el('div', undefined, 'list-group mb-3'); list.id = 'thread-load-items';
|
|
47
|
+
const deepDetail = el('div');
|
|
48
|
+
const pager = el('div', undefined, 'd-flex flex-wrap gap-2 align-items-center');
|
|
49
|
+
const pageText = el('span', '', 'small text-body-secondary');
|
|
50
|
+
const prev = button('上一页', () => { if (page > 1) { page--; void refresh(); } }, 'thread-load-prev');
|
|
51
|
+
const next = button('下一页', () => { if (page * 50 < total) { page++; void refresh(); } }, 'thread-load-next');
|
|
52
|
+
pager.append(prev, pageText, next); root.replaceChildren(note, limits, form, notice, coverage, list, deepDetail, pager);
|
|
53
|
+
const stopTimer = () => { if (timer !== null) clearTimeout(timer); timer = null; };
|
|
54
|
+
function schedule() {
|
|
55
|
+
stopTimer();
|
|
56
|
+
if (!disposed && !pageClosed && !paused && auto.checked && !document.hidden && !inFlight) timer = setTimeout(() => { timer = null; void refresh(false); }, 3000);
|
|
57
|
+
}
|
|
58
|
+
auto.addEventListener('change', schedule);
|
|
59
|
+
function renderList(data) {
|
|
60
|
+
total = data.total;
|
|
61
|
+
coverage.textContent = `${coverageText(data.coverage)} · ${time(data.window?.start_at_ms)} — ${time(data.window?.end_at_ms)}`;
|
|
62
|
+
const keep = new Set(data.list.map(row => row.load_id));
|
|
63
|
+
for (const [id, item] of rows) if (!keep.has(id)) { item.node.remove(); rows.delete(id); }
|
|
64
|
+
// 保留行按钮和展开节点的身份;刷新不会替换获得键盘焦点的控件。
|
|
65
|
+
data.list.forEach((row, index) => {
|
|
66
|
+
let item = rows.get(row.load_id);
|
|
67
|
+
if (!item) {
|
|
68
|
+
const node = el('article', undefined, 'list-group-item p-3'); node.setAttribute('data-load-id', row.load_id);
|
|
69
|
+
const heading = el('h2', undefined, 'h6 mb-2');
|
|
70
|
+
const toggle = button('', () => { selected = selected === row.load_id ? null : row.load_id; detailPage = 1; void refresh(); });
|
|
71
|
+
toggle.className = 'btn btn-link text-start p-0 fw-semibold text-break'; heading.append(toggle);
|
|
72
|
+
const info = el('p', '', 'small mb-1'); const result = el('p', '', 'small mb-1');
|
|
73
|
+
const identity = el('p', '', 'small font-monospace text-body-secondary text-break mb-0');
|
|
74
|
+
const detail = el('div', undefined, 'mt-3'); detail.setAttribute('data-detail', row.load_id);
|
|
75
|
+
node.append(heading, info, result, identity, detail); item = { node, toggle, info, result, identity, detail }; rows.set(row.load_id, item);
|
|
76
|
+
}
|
|
77
|
+
item.toggle.textContent = row.title || '未命名会话'; item.toggle.setAttribute('aria-expanded', String(selected === row.load_id));
|
|
78
|
+
item.info.textContent = `${clients[row.client_class] ?? '未知'} · ${transports[row.source_transport] ?? '未采集'} · ${time(row.opened_at_ms)}`;
|
|
79
|
+
const resultText = statuses[row.status] ?? '采样不完整', phaseText = phases[row.latest_phase] ?? '未采集阶段';
|
|
80
|
+
item.result.textContent = `${resultText}${phaseText === resultText ? '' : ` · ${phaseText}`} · 客户端总耗时:${ms(row.client_elapsed_ms)} · ${coverageText(row.coverage)}`;
|
|
81
|
+
item.identity.textContent = `会话 ${row.thread_id} · 加载 ${row.load_id}`;
|
|
82
|
+
item.detail.hidden = selected !== row.load_id;
|
|
83
|
+
if (list.children[index] !== item.node) list.insertBefore(item.node, list.children[index] ?? null);
|
|
84
|
+
});
|
|
85
|
+
// 成功的空结果必须清除旧行,不能从缓存恢复已清空的账本。
|
|
86
|
+
if (!data.list.length) list.replaceChildren(el('p', '暂无匹配的会话加载记录。', 'list-group-item text-body-secondary'));
|
|
87
|
+
else for (const node of [...list.children]) if (!node.hasAttribute('data-load-id')) node.remove();
|
|
88
|
+
pageText.textContent = `第 ${page} 页 · 共 ${total} 次打开`; prev.disabled = page <= 1; next.disabled = page * 50 >= total;
|
|
89
|
+
}
|
|
90
|
+
function renderDetail(data, id) {
|
|
91
|
+
const target = rows.get(id)?.detail ?? deepDetail;
|
|
92
|
+
if (target !== deepDetail) deepDetail.replaceChildren();
|
|
93
|
+
const contentKey = JSON.stringify([id, detailPage, data.coverage, data.list]);
|
|
94
|
+
if (target.dataset.contentKey === contentKey) { target.hidden = false; return; }
|
|
95
|
+
target.dataset.contentKey = contentKey;
|
|
96
|
+
const activePageAction = target.contains(document.activeElement) ? document.activeElement?.getAttribute('data-step-page') : null;
|
|
97
|
+
const summary = el('p', `加载编号:${id} · ${coverageText(data.coverage)}`, 'small text-body-secondary text-break');
|
|
98
|
+
const timelines = el('div', undefined, 'row g-3');
|
|
99
|
+
for (const clock of ['client', 'daemon']) {
|
|
100
|
+
const column = el('section', undefined, 'col-12 col-xl-6');
|
|
101
|
+
column.append(el('h3', clock === 'client' ? '客户端时间线' : 'Desktop 时间线', 'h6'));
|
|
102
|
+
const entries = data.list.filter(step => step.clock === clock).sort((a, b) => a.elapsed_ms - b.elapsed_ms || a.sequence - b.sequence);
|
|
103
|
+
const timeline = el('ol', undefined, 'list-unstyled small mb-2');
|
|
104
|
+
for (const step of entries) {
|
|
105
|
+
const line = el('li', undefined, 'border-bottom py-2');
|
|
106
|
+
line.append(el('strong', `${ms(step.elapsed_ms)} · ${phases[step.phase] ?? '未采集阶段'}`));
|
|
107
|
+
line.append(el('div', `发生:${time(step.occurred_at_ms)} · 记录:${time(step.recorded_at_ms)}`, 'text-body-secondary'));
|
|
108
|
+
if (step.terminal_phase) line.append(el('div', `终结阶段:${(phases[step.terminal_phase] ?? step.terminal_phase)}`));
|
|
109
|
+
if (step.terminal) line.append(el('div', `终结原因:${({ left_view: '离开会话', user_cancelled: '主动取消', stream_replaced: '订阅替代', open_rejected: '打开被拒绝', history_failed: '首屏历史失败' })[step.terminal] ?? '未采集'}`));
|
|
110
|
+
if (step.lease_id || step.trace_id) line.append(el('div', `租约 ${step.lease_id ?? '未采集'} · Trace ${step.trace_id ?? '未采集'}`, 'text-break text-body-secondary'));
|
|
111
|
+
timeline.append(line);
|
|
112
|
+
}
|
|
113
|
+
if (!entries.length) timeline.append(el('li', '未采集该端阶段', 'text-body-secondary'));
|
|
114
|
+
column.append(timeline);
|
|
115
|
+
const pairs = clock === 'client' ? [['request_started', 'snapshot_received'], ['snapshot_received', 'history_ready'], ['history_ready', 'content_presented']] : [['open_received', 'identity_ready'], ['identity_ready', 'lease_acquired'], ['lease_acquired', 'history_sent']];
|
|
116
|
+
const intervals = pairs.map(([from, to]) => {
|
|
117
|
+
const start = entries.find(step => step.phase === from), end = entries.find(step => step.phase === to);
|
|
118
|
+
return { text: `${phases[from]} → ${phases[to]}`, duration: start && end && end.elapsed_ms >= start.elapsed_ms ? end.elapsed_ms - start.elapsed_ms : null };
|
|
119
|
+
});
|
|
120
|
+
const known = intervals.filter(part => part.duration !== null).sort((a, b) => b.duration - a.duration);
|
|
121
|
+
column.append(el('p', known.length ? `最慢已采集区间:${known[0].text} · ${ms(known[0].duration)}` : '最慢区间:未采集', 'small'));
|
|
122
|
+
for (const part of intervals) column.append(el('div', `${part.text}:${ms(part.duration)}`, 'small text-body-secondary'));
|
|
123
|
+
timelines.append(column);
|
|
124
|
+
}
|
|
125
|
+
const paging = el('div', undefined, 'd-flex gap-2 align-items-center mt-3');
|
|
126
|
+
const earlier = button('上页步骤', () => { detailPage--; void refresh(); }); earlier.disabled = detailPage <= 1; earlier.setAttribute('data-step-page', 'earlier');
|
|
127
|
+
const later = button('下页步骤', () => { detailPage++; void refresh(); }); later.disabled = detailPage * 100 >= data.total; later.setAttribute('data-step-page', 'later');
|
|
128
|
+
paging.append(earlier, el('span', `步骤第 ${detailPage} 页`, 'small'), later);
|
|
129
|
+
target.replaceChildren(summary, el('p', '两端分别从各自起点计时;墙钟仅供查看,不跨端计算区间。', 'small text-body-secondary'), timelines, paging);
|
|
130
|
+
target.hidden = false;
|
|
131
|
+
if (activePageAction) target.querySelector(`[data-step-page="${activePageAction}"]`)?.focus({ preventScroll: true });
|
|
132
|
+
}
|
|
133
|
+
function refresh(manual = true) {
|
|
134
|
+
if (disposed || pageClosed || document.hidden) return Promise.resolve();
|
|
135
|
+
if (manual) { paused = false; generation++; }
|
|
136
|
+
stopTimer();
|
|
137
|
+
if (inFlight) { if (manual) queued = true; return inFlight; }
|
|
138
|
+
const token = generation, selectedID = selected;
|
|
139
|
+
const query = new URLSearchParams({ page: String(page), page_size: '50' });
|
|
140
|
+
if (thread.value.trim()) query.set('thread_id', thread.value.trim());
|
|
141
|
+
if (client.value) query.set('client_class', client.value);
|
|
142
|
+
if (status.value) query.set('status', status.value);
|
|
143
|
+
notice.textContent = '正在读取…';
|
|
144
|
+
inFlight = (async () => {
|
|
145
|
+
try {
|
|
146
|
+
const data = await request(`/api/v1/control/thread-loads?${query}`);
|
|
147
|
+
if (disposed || token !== generation) return;
|
|
148
|
+
renderList(data);
|
|
149
|
+
if (selectedID) {
|
|
150
|
+
const detail = await request(`/api/v1/control/thread-load-detail?load_id=${encodeURIComponent(selectedID)}&page=${detailPage}&page_size=100`);
|
|
151
|
+
if (disposed || token !== generation || selectedID !== selected) return;
|
|
152
|
+
renderDetail(detail, selectedID);
|
|
153
|
+
} else deepDetail.replaceChildren();
|
|
154
|
+
notice.textContent = '已刷新。';
|
|
155
|
+
} catch {
|
|
156
|
+
if (!disposed && token === generation) { paused = true; notice.textContent = '刷新失败,自动刷新已暂停。请点击“刷新当前页”重试;保留上次结果。'; }
|
|
157
|
+
} finally {
|
|
158
|
+
inFlight = null;
|
|
159
|
+
if (queued && !disposed && !document.hidden) { queued = false; void refresh(false); } else schedule();
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
return inFlight;
|
|
163
|
+
}
|
|
164
|
+
const visibility = () => { stopTimer(); if (document.hidden) { generation++; queued = false; } else schedule(); };
|
|
165
|
+
const leave = () => { pageClosed = true; generation++; queued = false; stopTimer(); };
|
|
166
|
+
const returnToPage = () => { pageClosed = false; schedule(); };
|
|
167
|
+
function dispose() {
|
|
168
|
+
disposed = true; leave(); document.removeEventListener('visibilitychange', visibility);
|
|
169
|
+
window.removeEventListener('pagehide', leave); window.removeEventListener('pageshow', returnToPage);
|
|
170
|
+
}
|
|
171
|
+
document.addEventListener('visibilitychange', visibility); window.addEventListener('pagehide', leave); window.addEventListener('pageshow', returnToPage);
|
|
172
|
+
return { refresh, dispose };
|
|
173
|
+
}
|
|
174
|
+
window.NiumaThreadLoads = Object.freeze({ mount });
|
|
175
|
+
})();
|