dsh-xray 0.4.1 → 0.6.0
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/README.md +4 -1
- package/README.zh.md +5 -1
- package/bin/xray.js +40 -2
- package/lib/collect/static.js +35 -0
- package/lib/compare.js +74 -0
- package/lib/index.js +37 -0
- package/lib/panel.js +213 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
**dsh-xray does.**
|
|
29
29
|
|
|
30
|
-
> **Status: 0.
|
|
30
|
+
> **Status: 0.6.x — static + runtime imaging, context-cost attribution, and a web panel.** Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
@@ -79,6 +79,9 @@ Per-plugin fiber lifecycle state, startup failures, transition history.
|
|
|
79
79
|
### 🤖 Agent Self-Introspection
|
|
80
80
|
The `xray_composition` tool lets agents inspect their own capability set.
|
|
81
81
|
|
|
82
|
+
### 🖥️ Web Panel
|
|
83
|
+
Mounted in `dsh web`, the plugin serves a zero-dependency panel at **`/xray`** — summary, health, deps (with the disable-cascade table), cost, and shadow views, live from the running composition. JSON endpoints under `/xray/api/*` serve the same data.
|
|
84
|
+
|
|
82
85
|
What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
|
|
83
86
|
|
|
84
87
|
```console
|
package/README.zh.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[English](./README.md)
|
|
12
12
|
|
|
13
|
-
> **状态:0.
|
|
13
|
+
> **状态:0.6.x — 静态 + 运行时成像、上下文成本归因、Web 面板。** 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
|
|
14
14
|
|
|
15
15
|
`dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件*为什么*在这、停用它会*连带瘫掉什么*、它在*悄悄消耗什么*。dsh-xray 回答这些。
|
|
16
16
|
|
|
@@ -91,6 +91,10 @@ orphan overrides (silently skipped) (1)
|
|
|
91
91
|
|
|
92
92
|
挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
|
|
93
93
|
|
|
94
|
+
## Web 面板
|
|
95
|
+
|
|
96
|
+
在 `dsh web` 中挂载后,插件在 **`/xray`** 提供零依赖面板——summary、health、deps(含停用级联表)、cost、shadow 五个视图,数据实时来自运行中的组合树;`/xray/api/*` 提供同源 JSON。
|
|
97
|
+
|
|
94
98
|
## 安全立场
|
|
95
99
|
|
|
96
100
|
dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为不透明标记、绝不求值;CLI 从不执行插件代码(`audit` 是对源码文本的模式扫描);挂载的插件只写 `$DSH_HOME/xray/` 目录。详见 [SECURITY.md](./SECURITY.md)。
|
package/bin/xray.js
CHANGED
|
@@ -12,6 +12,7 @@ function parseArgs(argv) {
|
|
|
12
12
|
for (let i = 0; i < argv.length; i++) {
|
|
13
13
|
const a = argv[i];
|
|
14
14
|
if (a === '--profile' || a === '-p') args.profile = argv[++i];
|
|
15
|
+
else if (a === '--against') args.against = argv[++i];
|
|
15
16
|
else if (a === '--json') args.json = true;
|
|
16
17
|
else args._.push(a);
|
|
17
18
|
}
|
|
@@ -118,7 +119,44 @@ function cmdDiff(args) {
|
|
|
118
119
|
function cmdSnapshot(args) {
|
|
119
120
|
const data = collectStatic(args.profile);
|
|
120
121
|
const { dump } = tryDump(args.profile);
|
|
121
|
-
|
|
122
|
+
const current = model.snapshot(data, dump);
|
|
123
|
+
const againstFile = args.against;
|
|
124
|
+
if (!againstFile) return console.log(JSON.stringify(current, null, 2));
|
|
125
|
+
|
|
126
|
+
const fs = require('node:fs');
|
|
127
|
+
const { compareSnapshots } = require('../lib/compare.js');
|
|
128
|
+
const saved = JSON.parse(fs.readFileSync(againstFile, 'utf8'));
|
|
129
|
+
const result = compareSnapshots(saved, current);
|
|
130
|
+
if (args.json) return console.log(JSON.stringify(result, null, 2));
|
|
131
|
+
|
|
132
|
+
if (result.identical) {
|
|
133
|
+
return console.log(`composition identical to snapshot from ${result.savedAt}`);
|
|
134
|
+
}
|
|
135
|
+
console.log(`composition drifted from snapshot (${result.savedAt}):`);
|
|
136
|
+
for (const b of result.changes.bundles) {
|
|
137
|
+
if (b.change === 'added') console.log(` bundle + ${b.name}@${b.version}`);
|
|
138
|
+
else if (b.change === 'removed') console.log(` bundle - ${b.name}`);
|
|
139
|
+
else
|
|
140
|
+
console.log(
|
|
141
|
+
` bundle ~ ${b.name}: ${b.change} ${b.from.version ?? b.from.patchHash} → ${b.to.version ?? b.to.patchHash}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
for (const p of result.changes.patches) {
|
|
145
|
+
console.log(
|
|
146
|
+
` patch ${p.change === 'added' ? '+' : p.change === 'removed' ? '-' : '~'} ${p.kind}${p.change === 'content' ? `: ${p.from} → ${p.to}` : ''}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
for (const p of result.changes.packages) {
|
|
150
|
+
if (p.change === 'added') console.log(` package + ${p.name}@${p.version}`);
|
|
151
|
+
else if (p.change === 'removed') console.log(` package - ${p.name}`);
|
|
152
|
+
else console.log(` package ~ ${p.name}: ${p.from} → ${p.to}`);
|
|
153
|
+
}
|
|
154
|
+
if (result.changes.composed) {
|
|
155
|
+
console.log(
|
|
156
|
+
` composed tree hash: ${result.changes.composed.from} → ${result.changes.composed.to}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
process.exitCode = 1;
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
function readRuntimeSnapshot() {
|
|
@@ -266,7 +304,7 @@ Commands:
|
|
|
266
304
|
attribute which layer introduced each row, and who patched it since
|
|
267
305
|
conflicts rows whose fields have multiple writers, and who wins
|
|
268
306
|
diff declared (static layers) vs actual (dump-config) tree
|
|
269
|
-
snapshot content-addressed lockfile
|
|
307
|
+
snapshot content-addressed lockfile; --against <file> diffs a saved one
|
|
270
308
|
deps service dependency graph from the live runtime snapshot
|
|
271
309
|
health plugin lifecycle health from the live runtime snapshot
|
|
272
310
|
cost estimated context-token cost of each model-facing tool schema
|
package/lib/collect/static.js
CHANGED
|
@@ -109,6 +109,41 @@ function collectStatic(profileName) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Repository plugins: the third-party plugin-console mechanism mounts
|
|
113
|
+
// `.dsh-plugin` directories under the harness home; each carries its own
|
|
114
|
+
// patch file. Not dsh core — absence is normal.
|
|
115
|
+
const repoRoot = path.join(home, '.dsh-plugin');
|
|
116
|
+
if (fs.existsSync(repoRoot)) {
|
|
117
|
+
let entries = [];
|
|
118
|
+
try {
|
|
119
|
+
entries = fs.readdirSync(repoRoot, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
120
|
+
} catch {
|
|
121
|
+
/* unreadable repository root: skip */
|
|
122
|
+
}
|
|
123
|
+
for (const e of entries) {
|
|
124
|
+
const dir = path.join(repoRoot, e.name);
|
|
125
|
+
const pkg = readJson(path.join(dir, 'package.json'));
|
|
126
|
+
const rel = pkg?.dsh?.bundle?.patch;
|
|
127
|
+
if (!rel) continue;
|
|
128
|
+
const file = path.join(dir, rel);
|
|
129
|
+
if (!fs.existsSync(file)) {
|
|
130
|
+
warnings.push(`repository plugin ${e.name}: patch missing (${rel})`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
134
|
+
const { value, error } = parseYaml(text, file);
|
|
135
|
+
if (error) warnings.push(error);
|
|
136
|
+
layers.push({
|
|
137
|
+
kind: 'repository',
|
|
138
|
+
name: pkg.name ?? e.name,
|
|
139
|
+
version: pkg.version ?? null,
|
|
140
|
+
file,
|
|
141
|
+
entries: Array.isArray(value) ? value : [],
|
|
142
|
+
text,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
112
147
|
// Out-of-tree plugins: profile dependencies carrying a `dsh` field.
|
|
113
148
|
const packages = [];
|
|
114
149
|
for (const dep of Object.keys(manifest.dependencies ?? {})) {
|
package/lib/compare.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Snapshot comparison: current composition vs a saved lockfile.
|
|
2
|
+
|
|
3
|
+
function indexBy(list, key) {
|
|
4
|
+
const m = new Map();
|
|
5
|
+
for (const item of list ?? []) m.set(item[key], item);
|
|
6
|
+
return m;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* F9b: compare a live snapshot against a saved one (`xray snapshot > lock.json`).
|
|
11
|
+
* Returns per-category changes; `identical` is true only when everything matches.
|
|
12
|
+
*/
|
|
13
|
+
function compareSnapshots(saved, current) {
|
|
14
|
+
if (saved?.schema !== 'dsh-xray/snapshot@1') {
|
|
15
|
+
throw new Error(`not a dsh-xray snapshot: schema=${saved?.schema ?? 'missing'}`);
|
|
16
|
+
}
|
|
17
|
+
const changes = { bundles: [], patches: [], packages: [], composed: null };
|
|
18
|
+
|
|
19
|
+
const savedBundles = indexBy(saved.bundles, 'name');
|
|
20
|
+
const currentBundles = indexBy(current.bundles, 'name');
|
|
21
|
+
for (const [name, b] of currentBundles) {
|
|
22
|
+
const old = savedBundles.get(name);
|
|
23
|
+
if (!old) changes.bundles.push({ name, change: 'added', version: b.version });
|
|
24
|
+
else if (old.version !== b.version || old.patchHash !== b.patchHash) {
|
|
25
|
+
changes.bundles.push({
|
|
26
|
+
name,
|
|
27
|
+
change: old.version !== b.version ? 'version' : 'patch-content',
|
|
28
|
+
from: { version: old.version, patchHash: old.patchHash },
|
|
29
|
+
to: { version: b.version, patchHash: b.patchHash },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const name of savedBundles.keys()) {
|
|
34
|
+
if (!currentBundles.has(name)) changes.bundles.push({ name, change: 'removed' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const savedPatches = indexBy(saved.patches, 'kind');
|
|
38
|
+
const currentPatches = indexBy(current.patches, 'kind');
|
|
39
|
+
for (const [kind, p] of currentPatches) {
|
|
40
|
+
const old = savedPatches.get(kind);
|
|
41
|
+
if (!old) changes.patches.push({ kind, change: 'added' });
|
|
42
|
+
else if (old.hash !== p.hash)
|
|
43
|
+
changes.patches.push({ kind, change: 'content', from: old.hash, to: p.hash });
|
|
44
|
+
}
|
|
45
|
+
for (const kind of savedPatches.keys()) {
|
|
46
|
+
if (!currentPatches.has(kind)) changes.patches.push({ kind, change: 'removed' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const savedPkgs = indexBy(saved.packages, 'name');
|
|
50
|
+
const currentPkgs = indexBy(current.packages, 'name');
|
|
51
|
+
for (const [name, p] of currentPkgs) {
|
|
52
|
+
const old = savedPkgs.get(name);
|
|
53
|
+
if (!old) changes.packages.push({ name, change: 'added', version: p.version });
|
|
54
|
+
else if (old.version !== p.version) {
|
|
55
|
+
changes.packages.push({ name, change: 'version', from: old.version, to: p.version });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const name of savedPkgs.keys()) {
|
|
59
|
+
if (!currentPkgs.has(name)) changes.packages.push({ name, change: 'removed' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (saved.composedHash && current.composedHash && saved.composedHash !== current.composedHash) {
|
|
63
|
+
changes.composed = { from: saved.composedHash, to: current.composedHash };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const identical =
|
|
67
|
+
!changes.bundles.length &&
|
|
68
|
+
!changes.patches.length &&
|
|
69
|
+
!changes.packages.length &&
|
|
70
|
+
!changes.composed;
|
|
71
|
+
return { identical, savedAt: saved.createdAt, profile: saved.profile, changes };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { compareSnapshots };
|
package/lib/index.js
CHANGED
|
@@ -91,6 +91,43 @@ function apply(ctx) {
|
|
|
91
91
|
];
|
|
92
92
|
}, 'xray-runtime-snapshot');
|
|
93
93
|
|
|
94
|
+
// Web panel: mounts when the profile composes a webServer (dsh web).
|
|
95
|
+
// Headless profiles simply never activate this subplugin.
|
|
96
|
+
ctx.plugin({
|
|
97
|
+
name: 'dsh-xray-panel',
|
|
98
|
+
inject: ['webServer'],
|
|
99
|
+
apply: (wctx) => {
|
|
100
|
+
const { mountPanel } = require('./panel.js');
|
|
101
|
+
const freshSnap = () => {
|
|
102
|
+
const snap = snapshotRegistry(ctx);
|
|
103
|
+
snap.transitions = Object.fromEntries(transitions);
|
|
104
|
+
snap.promptAssembly = lastAssembly;
|
|
105
|
+
return snap;
|
|
106
|
+
};
|
|
107
|
+
wctx.effect(
|
|
108
|
+
() =>
|
|
109
|
+
mountPanel(wctx.webServer, {
|
|
110
|
+
summary: () => {
|
|
111
|
+
const snap = freshSnap();
|
|
112
|
+
return {
|
|
113
|
+
plugins: snap.plugins.length,
|
|
114
|
+
unhealthy: health(snap).unhealthy.length,
|
|
115
|
+
services: Object.keys(serviceGraph(snap).services).length,
|
|
116
|
+
toolSchemaTokens: contextCost(snap).totalTokens,
|
|
117
|
+
capturedAt: snap.capturedAt,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
deps: () => serviceGraph(freshSnap()),
|
|
121
|
+
health: () => health(freshSnap()),
|
|
122
|
+
cost: () => contextCost(freshSnap()),
|
|
123
|
+
shadow: () => shadowing(freshSnap()),
|
|
124
|
+
}),
|
|
125
|
+
'xray-panel-routes',
|
|
126
|
+
);
|
|
127
|
+
logger.info('xray panel mounted at /xray');
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
94
131
|
// Agent self-introspection tool: only when a tool registry exists
|
|
95
132
|
// (headless/web both have one; keep it optional so xray mounts anywhere).
|
|
96
133
|
ctx.plugin({
|
package/lib/panel.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Self-contained web panel: one HTML page + JSON endpoints, mounted on the
|
|
2
|
+
// host webServer. No client-module bundle, no React, no build step — the
|
|
3
|
+
// model layer already computes everything; this only renders it.
|
|
4
|
+
|
|
5
|
+
const PAGE = `<!doctype html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<title>dsh-xray</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root { color-scheme: dark; }
|
|
13
|
+
* { box-sizing: border-box; margin: 0; }
|
|
14
|
+
body { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
15
|
+
background: #0d1117; color: #e6edf3; padding: 24px; }
|
|
16
|
+
h1 { font-size: 16px; margin-bottom: 4px; }
|
|
17
|
+
.sub { color: #8b949e; margin-bottom: 20px; }
|
|
18
|
+
nav { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
19
|
+
nav button { background: #21262d; color: #e6edf3; border: 1px solid #30363d;
|
|
20
|
+
border-radius: 6px; padding: 5px 12px; cursor: pointer; font: inherit; }
|
|
21
|
+
nav button.active { background: #1f6feb; border-color: #1f6feb; }
|
|
22
|
+
table { border-collapse: collapse; width: 100%; margin-top: 8px; }
|
|
23
|
+
th, td { text-align: left; padding: 4px 10px; border-bottom: 1px solid #21262d; }
|
|
24
|
+
th { color: #8b949e; font-weight: normal; }
|
|
25
|
+
.num { text-align: right; }
|
|
26
|
+
.bar { background: #1f6feb; height: 10px; border-radius: 2px; display: inline-block; }
|
|
27
|
+
.warn { color: #f85149; }
|
|
28
|
+
.ok { color: #7ee787; }
|
|
29
|
+
.muted { color: #8b949e; }
|
|
30
|
+
#status { margin: 12px 0; color: #8b949e; }
|
|
31
|
+
.tag { background: #21262d; border-radius: 4px; padding: 1px 6px; margin-left: 6px; font-size: 11px; }
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<h1>dsh-xray</h1>
|
|
36
|
+
<div class="sub">composition X-ray — live from this harness</div>
|
|
37
|
+
<nav id="nav"></nav>
|
|
38
|
+
<div id="status"></div>
|
|
39
|
+
<div id="content"></div>
|
|
40
|
+
<script>
|
|
41
|
+
const views = ['summary', 'health', 'deps', 'cost', 'shadow'];
|
|
42
|
+
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c]));
|
|
43
|
+
const nav = document.getElementById('nav');
|
|
44
|
+
const content = document.getElementById('content');
|
|
45
|
+
const status = document.getElementById('status');
|
|
46
|
+
let active = 'summary';
|
|
47
|
+
|
|
48
|
+
for (const v of views) {
|
|
49
|
+
const b = document.createElement('button');
|
|
50
|
+
b.textContent = v;
|
|
51
|
+
b.onclick = () => { active = v; render(); };
|
|
52
|
+
b.id = 'nav-' + v;
|
|
53
|
+
nav.appendChild(b);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function table(headers, rows) {
|
|
57
|
+
return '<table><tr>' + headers.map((h) => '<th>' + esc(h) + '</th>').join('') + '</tr>'
|
|
58
|
+
+ rows.join('') + '</table>';
|
|
59
|
+
}
|
|
60
|
+
function bar(share) {
|
|
61
|
+
return '<span class="bar" style="width:' + Math.max(2, share * 2) + 'px"></span>';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const renderers = {
|
|
65
|
+
summary(d) {
|
|
66
|
+
return table(['metric', 'value'], [
|
|
67
|
+
'<tr><td>plugins mounted</td><td class="num">' + d.plugins + '</td></tr>',
|
|
68
|
+
'<tr><td>unhealthy</td><td class="num ' + (d.unhealthy ? 'warn' : 'ok') + '">' + d.unhealthy + '</td></tr>',
|
|
69
|
+
'<tr><td>services</td><td class="num">' + d.services + '</td></tr>',
|
|
70
|
+
'<tr><td>context tokens (tools + sections)</td><td class="num">~' + d.toolSchemaTokens + '</td></tr>',
|
|
71
|
+
]) + '<p class="muted" style="margin-top:12px">captured ' + esc(d.capturedAt) + '</p>';
|
|
72
|
+
},
|
|
73
|
+
health(d) {
|
|
74
|
+
let html = '<p><span class="ok">' + d.healthy.length + ' healthy</span>'
|
|
75
|
+
+ (d.waiting.length ? ' · ' + d.waiting.length + ' waiting' : '')
|
|
76
|
+
+ (d.unhealthy.length ? ' · <span class="warn">' + d.unhealthy.length + ' unhealthy</span>' : '') + '</p>';
|
|
77
|
+
if (d.unhealthy.length) {
|
|
78
|
+
html += table(['plugin', 'fiber', 'state', 'error'], d.unhealthy.flatMap((p) =>
|
|
79
|
+
p.fibers.map((f) => '<tr><td>' + esc(p.name) + '</td><td class="num">' + f.uid
|
|
80
|
+
+ '</td><td class="warn">' + esc(f.state) + '</td><td>' + esc(f.error ?? '') + '</td></tr>')));
|
|
81
|
+
}
|
|
82
|
+
if (d.waiting.length) {
|
|
83
|
+
html += table(['waiting plugin', 'wants'], d.waiting.map((p) =>
|
|
84
|
+
'<tr><td>' + esc(p.name) + '</td><td>' + esc(p.inject.join(', ')) + '</td></tr>'));
|
|
85
|
+
}
|
|
86
|
+
return html;
|
|
87
|
+
},
|
|
88
|
+
deps(d) {
|
|
89
|
+
const services = Object.entries(d.services).map(([name, node]) =>
|
|
90
|
+
'<tr><td>' + esc(name) + '</td><td>' + esc(node.providers.join(', ') || '—')
|
|
91
|
+
+ '</td><td>' + esc(node.consumers.join(', ') || '—') + '</td></tr>');
|
|
92
|
+
let html = table(['service', 'provided by', 'consumed by'], services);
|
|
93
|
+
const cascade = Object.entries(d.cascade);
|
|
94
|
+
if (cascade.length) {
|
|
95
|
+
html = '<h3 style="margin:8px 0">disable-cascade</h3>'
|
|
96
|
+
+ table(['provider', 'affects'], cascade.map(([p, a]) =>
|
|
97
|
+
'<tr><td>' + esc(p) + '</td><td>' + esc(a.join(', ')) + '</td></tr>'))
|
|
98
|
+
+ '<h3 style="margin:16px 0 8px">services</h3>' + html;
|
|
99
|
+
}
|
|
100
|
+
if (d.unsatisfied.length) {
|
|
101
|
+
html = '<p class="warn">' + d.unsatisfied.length + ' unsatisfied inject(s)</p>' + html;
|
|
102
|
+
}
|
|
103
|
+
return html;
|
|
104
|
+
},
|
|
105
|
+
cost(d) {
|
|
106
|
+
let html = '<p>~' + d.totalTokens + ' tokens: ' + d.toolCount + ' tool schema(s) ~' + d.toolTokens
|
|
107
|
+
+ ' + ' + d.sectionCount + ' prompt section(s) ~' + d.sectionTokens + '</p>';
|
|
108
|
+
if (d.sections.length) {
|
|
109
|
+
html += '<h3 style="margin:12px 0 4px">prompt sections</h3>'
|
|
110
|
+
+ table(['section', 'tokens', 'share', ''], d.sections.map((s) =>
|
|
111
|
+
'<tr><td>' + esc(s.name) + '</td><td class="num">~' + s.tokens + '</td><td class="num">'
|
|
112
|
+
+ s.share + '%</td><td>' + bar(s.share) + '</td></tr>'));
|
|
113
|
+
} else {
|
|
114
|
+
html += '<p class="muted">no prompt assembly observed yet — send one agent message first</p>';
|
|
115
|
+
}
|
|
116
|
+
html += '<h3 style="margin:12px 0 4px">tool schemas</h3>'
|
|
117
|
+
+ table(['tool', 'tokens', 'share', ''], d.tools.map((t) =>
|
|
118
|
+
'<tr><td>' + esc(t.name) + '</td><td class="num">~' + t.tokens + '</td><td class="num">'
|
|
119
|
+
+ t.share + '%</td><td>' + bar(t.share) + '</td></tr>'));
|
|
120
|
+
return html;
|
|
121
|
+
},
|
|
122
|
+
shadow(d) {
|
|
123
|
+
let html = d.services.length
|
|
124
|
+
? table(['service', 'providers'], d.services.map((s) =>
|
|
125
|
+
'<tr><td>' + esc(s.service) + '</td><td class="warn">' + esc(s.providers.join(' AND ')) + '</td></tr>'))
|
|
126
|
+
: '<p class="ok">no service is provided by more than one plugin</p>';
|
|
127
|
+
if (d.registrars.length) {
|
|
128
|
+
html += '<h3 style="margin:12px 0 4px">registrars</h3>'
|
|
129
|
+
+ table(['plugin', 'registrations'], d.registrars.map((r) =>
|
|
130
|
+
'<tr><td>' + esc(r.plugin) + '</td><td class="num">' + r.registrations + '</td></tr>'));
|
|
131
|
+
}
|
|
132
|
+
return html;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
async function render() {
|
|
137
|
+
for (const v of views) document.getElementById('nav-' + v).className = v === active ? 'active' : '';
|
|
138
|
+
status.textContent = 'loading ' + active + '…';
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch('/xray/api/' + active);
|
|
141
|
+
if (!res.ok) throw new Error(await res.text());
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
content.innerHTML = renderers[active](data);
|
|
144
|
+
status.textContent = '';
|
|
145
|
+
} catch (err) {
|
|
146
|
+
status.innerHTML = '<span class="warn">' + esc(err.message) + '</span>';
|
|
147
|
+
content.innerHTML = '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
render();
|
|
151
|
+
setInterval(() => { if (active === 'health' || active === 'summary') render(); }, 5000);
|
|
152
|
+
</script>
|
|
153
|
+
</body>
|
|
154
|
+
</html>`;
|
|
155
|
+
|
|
156
|
+
function sendJson(response, code, value) {
|
|
157
|
+
const body = JSON.stringify(value);
|
|
158
|
+
response.writeHead(code, {
|
|
159
|
+
'content-type': 'application/json; charset=utf-8',
|
|
160
|
+
'cache-control': 'no-store',
|
|
161
|
+
});
|
|
162
|
+
response.end(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Mount the panel routes. `views` supplies fresh data per request:
|
|
167
|
+
* { summary, deps, health, cost, shadow } — each a () => object.
|
|
168
|
+
* Returns the disposers webServer.register produced.
|
|
169
|
+
*/
|
|
170
|
+
function mountPanel(webServer, views) {
|
|
171
|
+
const disposers = [];
|
|
172
|
+
disposers.push(
|
|
173
|
+
webServer.register({
|
|
174
|
+
kind: 'exact',
|
|
175
|
+
path: '/xray',
|
|
176
|
+
handler: (request, response) => {
|
|
177
|
+
if (request.method !== 'GET') {
|
|
178
|
+
response.writeHead(405, { allow: 'GET' });
|
|
179
|
+
response.end();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
response.writeHead(200, {
|
|
183
|
+
'content-type': 'text/html; charset=utf-8',
|
|
184
|
+
'cache-control': 'no-store',
|
|
185
|
+
});
|
|
186
|
+
response.end(PAGE);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
for (const [name, compute] of Object.entries(views)) {
|
|
191
|
+
disposers.push(
|
|
192
|
+
webServer.register({
|
|
193
|
+
kind: 'exact',
|
|
194
|
+
path: `/xray/api/${name}`,
|
|
195
|
+
handler: (request, response) => {
|
|
196
|
+
if (request.method !== 'GET') {
|
|
197
|
+
response.writeHead(405, { allow: 'GET' });
|
|
198
|
+
response.end();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
sendJson(response, 200, compute());
|
|
203
|
+
} catch (err) {
|
|
204
|
+
sendJson(response, 500, { error: err.message });
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return disposers;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { mountPanel, PAGE };
|