@silicaclaw/cli 2026.3.19-6 → 2026.3.19-7
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/CHANGELOG.md +1 -1
- package/VERSION +1 -1
- package/apps/local-console/public/app/app.js +473 -0
- package/apps/local-console/public/app/events.js +422 -0
- package/apps/local-console/public/app/i18n.js +43 -0
- package/apps/local-console/public/app/network.js +212 -0
- package/apps/local-console/public/app/overview.js +325 -0
- package/apps/local-console/public/app/profile.js +234 -0
- package/apps/local-console/public/app/shell.js +144 -0
- package/apps/local-console/public/app/social.js +485 -0
- package/apps/local-console/public/app/styles.css +2366 -0
- package/apps/local-console/public/app/template.js +793 -0
- package/apps/local-console/public/app/translations.js +1028 -0
- package/apps/local-console/public/app/utils.js +77 -0
- package/apps/local-console/public/index.html +3 -5831
- package/apps/local-console/src/server.ts +125 -1
- package/apps/public-explorer/public/app/app.js +302 -0
- package/apps/public-explorer/public/app/i18n.js +46 -0
- package/apps/public-explorer/public/app/styles.css +297 -0
- package/apps/public-explorer/public/app/template.js +45 -0
- package/apps/public-explorer/public/app/translations.js +192 -0
- package/apps/public-explorer/public/app/utils.js +37 -0
- package/apps/public-explorer/public/index.html +3 -831
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/silicaclaw-gateway.mjs +47 -4
|
@@ -2,7 +2,7 @@ import express, { NextFunction, Request, Response } from "express";
|
|
|
2
2
|
import cors from "cors";
|
|
3
3
|
import { execFile, spawnSync } from "child_process";
|
|
4
4
|
import { resolve } from "path";
|
|
5
|
-
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
7
|
import { hostname } from "os";
|
|
8
8
|
import { promisify } from "util";
|
|
@@ -172,6 +172,47 @@ function existingPathOrNull(filePath: string): string | null {
|
|
|
172
172
|
return existsSync(filePath) ? filePath : null;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function listDirectories(root: string) {
|
|
176
|
+
if (!root || !existsSync(root)) return [];
|
|
177
|
+
try {
|
|
178
|
+
return readdirSync(root)
|
|
179
|
+
.map((name) => {
|
|
180
|
+
const fullPath = resolve(root, name);
|
|
181
|
+
try {
|
|
182
|
+
return statSync(fullPath).isDirectory() ? { name, path: fullPath } : null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.filter((item): item is { name: string; path: string } => Boolean(item));
|
|
188
|
+
} catch {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function readJsonFileSafe(filePath: string) {
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function summarizeSkillReadme(filePath: string) {
|
|
202
|
+
if (!filePath || !existsSync(filePath)) return "";
|
|
203
|
+
try {
|
|
204
|
+
const raw = readFileSync(filePath, "utf8");
|
|
205
|
+
const lines = raw
|
|
206
|
+
.split(/\r?\n/)
|
|
207
|
+
.map((line) => line.trim())
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.filter((line) => !line.startsWith("#"));
|
|
210
|
+
return String(lines[0] || "").slice(0, 220);
|
|
211
|
+
} catch {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
function detectOpenClawInstallation(workspaceRoot: string) {
|
|
176
217
|
const workspaceDir = resolve(workspaceRoot, ".openclaw");
|
|
177
218
|
const homeDir = resolve(process.env.HOME || "", ".openclaw");
|
|
@@ -1527,6 +1568,85 @@ export class LocalNodeService {
|
|
|
1527
1568
|
};
|
|
1528
1569
|
}
|
|
1529
1570
|
|
|
1571
|
+
getSkillsView() {
|
|
1572
|
+
const bundledRoot = resolve(this.workspaceRoot, "openclaw-skills");
|
|
1573
|
+
const openclawHome = resolve(process.env.HOME || "", ".openclaw");
|
|
1574
|
+
const workspaceInstallRoot = resolve(openclawHome, "workspace", "skills");
|
|
1575
|
+
const legacyInstallRoot = resolve(openclawHome, "skills");
|
|
1576
|
+
const bridge = this.getOpenClawBridgeStatus();
|
|
1577
|
+
const bundledSkills = listDirectories(bundledRoot).map((dir) => {
|
|
1578
|
+
const manifestPath = resolve(dir.path, "manifest.json");
|
|
1579
|
+
const skillPath = resolve(dir.path, "SKILL.md");
|
|
1580
|
+
const versionPath = resolve(dir.path, "VERSION");
|
|
1581
|
+
const manifest = readJsonFileSafe(manifestPath);
|
|
1582
|
+
const name = String(manifest?.name || dir.name);
|
|
1583
|
+
const capabilities = Array.isArray(manifest?.capabilities)
|
|
1584
|
+
? manifest.capabilities.map((item) => String(item))
|
|
1585
|
+
: [];
|
|
1586
|
+
const installedWorkspacePath = resolve(workspaceInstallRoot, name);
|
|
1587
|
+
const installedLegacyPath = resolve(legacyInstallRoot, name);
|
|
1588
|
+
const installedInWorkspace = existsSync(installedWorkspacePath);
|
|
1589
|
+
const installedInLegacy = existsSync(installedLegacyPath);
|
|
1590
|
+
return {
|
|
1591
|
+
key: name,
|
|
1592
|
+
name,
|
|
1593
|
+
display_name: String(manifest?.display_name || name),
|
|
1594
|
+
description: String(manifest?.description || summarizeSkillReadme(skillPath) || ""),
|
|
1595
|
+
version: existsSync(versionPath) ? readFileSync(versionPath, "utf8").trim() : String(manifest?.version || ""),
|
|
1596
|
+
source_path: dir.path,
|
|
1597
|
+
manifest_path: existsSync(manifestPath) ? manifestPath : null,
|
|
1598
|
+
skill_path: existsSync(skillPath) ? skillPath : null,
|
|
1599
|
+
capabilities,
|
|
1600
|
+
transport: manifest?.transport || null,
|
|
1601
|
+
installed_in_openclaw: installedInWorkspace || installedInLegacy,
|
|
1602
|
+
install_mode: installedInWorkspace ? "workspace" : installedInLegacy ? "legacy" : "not_installed",
|
|
1603
|
+
installed_path: installedInWorkspace ? installedWorkspacePath : installedInLegacy ? installedLegacyPath : null,
|
|
1604
|
+
};
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
const installedSkills = [
|
|
1608
|
+
...listDirectories(workspaceInstallRoot).map((dir) => ({ ...dir, install_mode: "workspace" as const })),
|
|
1609
|
+
...listDirectories(legacyInstallRoot).map((dir) => ({ ...dir, install_mode: "legacy" as const })),
|
|
1610
|
+
].map((dir) => {
|
|
1611
|
+
const manifestPath = resolve(dir.path, "manifest.json");
|
|
1612
|
+
const skillPath = resolve(dir.path, "SKILL.md");
|
|
1613
|
+
const versionPath = resolve(dir.path, "VERSION");
|
|
1614
|
+
const manifest = readJsonFileSafe(manifestPath);
|
|
1615
|
+
return {
|
|
1616
|
+
key: `${dir.install_mode}:${dir.name}`,
|
|
1617
|
+
name: String(manifest?.name || dir.name),
|
|
1618
|
+
display_name: String(manifest?.display_name || dir.name),
|
|
1619
|
+
description: String(manifest?.description || summarizeSkillReadme(skillPath) || ""),
|
|
1620
|
+
version: existsSync(versionPath) ? readFileSync(versionPath, "utf8").trim() : String(manifest?.version || ""),
|
|
1621
|
+
install_mode: dir.install_mode,
|
|
1622
|
+
installed_path: dir.path,
|
|
1623
|
+
manifest_path: existsSync(manifestPath) ? manifestPath : null,
|
|
1624
|
+
skill_path: existsSync(skillPath) ? skillPath : null,
|
|
1625
|
+
capabilities: Array.isArray(manifest?.capabilities) ? manifest.capabilities.map((item) => String(item)) : [],
|
|
1626
|
+
bundled_source_path: bundledSkills.find((item) => item.name === String(manifest?.name || dir.name))?.source_path || null,
|
|
1627
|
+
};
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
return {
|
|
1631
|
+
openclaw: {
|
|
1632
|
+
detected: bridge.openclaw_installation.detected,
|
|
1633
|
+
running: bridge.openclaw_runtime.running,
|
|
1634
|
+
detection_mode: bridge.openclaw_runtime.detection_mode,
|
|
1635
|
+
gateway_url: bridge.openclaw_runtime.gateway_url,
|
|
1636
|
+
workspace_install_root: workspaceInstallRoot,
|
|
1637
|
+
legacy_install_root: legacyInstallRoot,
|
|
1638
|
+
},
|
|
1639
|
+
summary: {
|
|
1640
|
+
bundled_count: bundledSkills.length,
|
|
1641
|
+
installed_count: installedSkills.length,
|
|
1642
|
+
installed_bundled_count: bundledSkills.filter((item) => item.installed_in_openclaw).length,
|
|
1643
|
+
},
|
|
1644
|
+
install_action: bridge.skill_learning.install_action,
|
|
1645
|
+
bundled_skills: bundledSkills,
|
|
1646
|
+
installed_skills: installedSkills,
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1530
1650
|
getRuntimeMessageGovernance() {
|
|
1531
1651
|
return this.messageGovernance;
|
|
1532
1652
|
}
|
|
@@ -2830,6 +2950,10 @@ export async function main() {
|
|
|
2830
2950
|
sendOk(res, node.getNetworkStats());
|
|
2831
2951
|
});
|
|
2832
2952
|
|
|
2953
|
+
app.get("/api/skills", (_req, res) => {
|
|
2954
|
+
sendOk(res, node.getSkillsView());
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2833
2957
|
app.post(
|
|
2834
2958
|
"/api/network/quick-connect-global-preview",
|
|
2835
2959
|
asyncRoute(async (req, res) => {
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { appTemplate } from "./template.js";
|
|
2
|
+
import { createI18n } from "./i18n.js";
|
|
3
|
+
import { TRANSLATIONS } from "./translations.js";
|
|
4
|
+
import {
|
|
5
|
+
escapeHtml,
|
|
6
|
+
formatMessageBody,
|
|
7
|
+
freshnessStatusText,
|
|
8
|
+
shortId,
|
|
9
|
+
toPrettyJson,
|
|
10
|
+
verificationStatusText,
|
|
11
|
+
} from "./utils.js";
|
|
12
|
+
|
|
13
|
+
const root = document.getElementById("app-root");
|
|
14
|
+
if (!root) {
|
|
15
|
+
throw new Error("Missing root element: app-root");
|
|
16
|
+
}
|
|
17
|
+
root.innerHTML = appTemplate;
|
|
18
|
+
|
|
19
|
+
const i18n = createI18n(TRANSLATIONS);
|
|
20
|
+
const t = i18n.t;
|
|
21
|
+
function setLocale(locale) {
|
|
22
|
+
return i18n.setLocale(locale);
|
|
23
|
+
}
|
|
24
|
+
function applyTranslations() {
|
|
25
|
+
document.title = t('meta.title');
|
|
26
|
+
document.getElementById('metaDescription').setAttribute('content', t('meta.description'));
|
|
27
|
+
document.getElementById('ogTitle').setAttribute('content', t('meta.title'));
|
|
28
|
+
document.getElementById('ogDescription').setAttribute('content', t('meta.socialDescription'));
|
|
29
|
+
document.getElementById('twitterTitle').setAttribute('content', t('meta.title'));
|
|
30
|
+
document.getElementById('twitterDescription').setAttribute('content', t('meta.socialDescription'));
|
|
31
|
+
document.getElementById('pageTitle').textContent = t('page.title');
|
|
32
|
+
document.getElementById('pageSubtitle').textContent = t('page.subtitle');
|
|
33
|
+
document.getElementById('themeDarkBtn').textContent = t('page.themeDark');
|
|
34
|
+
document.getElementById('themeLightBtn').textContent = t('page.themeLight');
|
|
35
|
+
document.getElementById('q').setAttribute('placeholder', t('page.searchPlaceholder'));
|
|
36
|
+
document.getElementById('searchBtn').textContent = t('page.search');
|
|
37
|
+
document.getElementById('directoryTitle').textContent = t('page.directoryTitle');
|
|
38
|
+
document.getElementById('directorySubtitle').textContent = t('page.directorySubtitle');
|
|
39
|
+
document.getElementById('streamTitle').textContent = t('page.streamTitle');
|
|
40
|
+
document.getElementById('streamSubtitle').textContent = t('page.streamSubtitle');
|
|
41
|
+
document.getElementById('refreshMessagesBtn').textContent = t('page.refreshMessages');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
setLocale(i18n.getCurrentLocale());
|
|
45
|
+
applyTranslations();
|
|
46
|
+
|
|
47
|
+
const API_BASE = localStorage.getItem('silicaclaw_api_base') || 'http://localhost:4310';
|
|
48
|
+
const state = document.getElementById('state');
|
|
49
|
+
const cards = document.getElementById('cards');
|
|
50
|
+
const detail = document.getElementById('detail');
|
|
51
|
+
const messageStreamList = document.getElementById('messageStreamList');
|
|
52
|
+
let publicMessages = [];
|
|
53
|
+
|
|
54
|
+
function toast(msg) {
|
|
55
|
+
const t = document.getElementById('toast');
|
|
56
|
+
t.textContent = msg;
|
|
57
|
+
t.classList.add('show');
|
|
58
|
+
setTimeout(() => t.classList.remove('show'), 1800);
|
|
59
|
+
}
|
|
60
|
+
async function copyText(text, btn, successText = null) {
|
|
61
|
+
try {
|
|
62
|
+
await navigator.clipboard.writeText(text);
|
|
63
|
+
toast(successText || t('common.copied'));
|
|
64
|
+
if (!btn) return;
|
|
65
|
+
const old = btn.textContent || '';
|
|
66
|
+
btn.disabled = true;
|
|
67
|
+
btn.textContent = t('common.copied');
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
btn.textContent = old;
|
|
70
|
+
btn.disabled = false;
|
|
71
|
+
}, 900);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
toast(err instanceof Error ? err.message : t('common.copyFailed'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function applyTheme(mode) {
|
|
77
|
+
const next = mode === 'light' ? 'light' : 'dark';
|
|
78
|
+
document.documentElement.setAttribute('data-theme-mode', next);
|
|
79
|
+
localStorage.setItem('silicaclaw_theme_mode', next);
|
|
80
|
+
document.getElementById('themeDarkBtn').classList.toggle('active', next === 'dark');
|
|
81
|
+
document.getElementById('themeLightBtn').classList.toggle('active', next === 'light');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function api(path) {
|
|
85
|
+
const res = await fetch(`${API_BASE}${path}`);
|
|
86
|
+
const json = await res.json().catch(() => null);
|
|
87
|
+
if (!res.ok || !json || !json.ok) throw new Error(json?.error?.message || t('common.requestFailed', { status: String(res.status) }));
|
|
88
|
+
return json;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderState(text) { state.innerHTML = `<div class="state">${text}</div>`; }
|
|
92
|
+
function clearState() { state.innerHTML = ''; }
|
|
93
|
+
function renderMessageStream(messages) {
|
|
94
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
95
|
+
messageStreamList.innerHTML = `<div class="state">${t('state.noMessages')}</div>`;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
messageStreamList.innerHTML = messages.map((item) => `
|
|
99
|
+
<article class="stream-item" data-agent-id="${item.agent_id}">
|
|
100
|
+
<div class="stream-item__meta">
|
|
101
|
+
<div>
|
|
102
|
+
<strong>${escapeHtml(item.display_name || t('card.unnamedAgent'))}</strong>
|
|
103
|
+
<span class="mono muted" style="margin-left:8px;">${escapeHtml(shortId(item.agent_id || ''))}</span>
|
|
104
|
+
${item.online ? `<span class="badge ok" style="margin-left:8px;">${t('card.online')}</span>` : `<span class="badge warn" style="margin-left:8px;">${t('card.offline')}</span>`}
|
|
105
|
+
</div>
|
|
106
|
+
<div class="mono muted">${item.created_at ? new Date(item.created_at).toLocaleString() : '-'}</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="stream-item__body">${formatMessageBody(item.body || '')}</div>
|
|
109
|
+
</article>
|
|
110
|
+
`).join('');
|
|
111
|
+
messageStreamList.querySelectorAll('.stream-item').forEach((el) => {
|
|
112
|
+
el.addEventListener('click', () => {
|
|
113
|
+
if (el.dataset.agentId) {
|
|
114
|
+
location.hash = `#agent/${el.dataset.agentId}`;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function refreshMessages() {
|
|
121
|
+
try {
|
|
122
|
+
const payload = (await api('/api/messages?limit=24')).data || {};
|
|
123
|
+
publicMessages = Array.isArray(payload.items) ? payload.items : [];
|
|
124
|
+
renderMessageStream(publicMessages);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
messageStreamList.innerHTML = `<div class="state">${t('state.messagesFailed', { message: e instanceof Error ? e.message : t('common.unknownError') })}</div>`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function search() {
|
|
131
|
+
try {
|
|
132
|
+
renderState(t('state.searching'));
|
|
133
|
+
const q = document.getElementById('q').value.trim();
|
|
134
|
+
const profiles = (await api(`/api/search?q=${encodeURIComponent(q)}`)).data || [];
|
|
135
|
+
if (!profiles.length) {
|
|
136
|
+
cards.innerHTML = '';
|
|
137
|
+
renderState(q ? t('state.noResult', { query: q }) : t('state.noAgents'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
clearState();
|
|
141
|
+
cards.innerHTML = profiles.map((p) => `
|
|
142
|
+
<article class="card" data-id="${p.agent_id}">
|
|
143
|
+
<div style="display:flex; justify-content:space-between; gap:8px; align-items:center;">
|
|
144
|
+
<h3 style="margin:0;">${p.display_name || t('card.unnamedAgent')}</h3>
|
|
145
|
+
${p.openclaw_bound ? `<span class="badge">${t('card.openclaw')}</span>` : ''}
|
|
146
|
+
</div>
|
|
147
|
+
<div class="muted" style="margin-top:6px;">${p.bio || t('card.noBioYet')}</div>
|
|
148
|
+
<div class="chips">${(p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('card.noTags')}</span>`}</div>
|
|
149
|
+
<div class="chips">${(p.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('card.noCapabilities')}</span>`}</div>
|
|
150
|
+
<div class="chips">
|
|
151
|
+
<span class="badge ${p.verification_status === 'verified' ? 'ok' : p.verification_status === 'stale' ? 'warn' : 'err'}">${verificationStatusText(t, p.verification_status)}</span>
|
|
152
|
+
<span class="badge ${p.freshness_status === 'live' ? 'ok' : p.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${freshnessStatusText(t, p.freshness_status)}</span>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="meta">
|
|
155
|
+
<span class="mono">${shortId(p.agent_id)} · ${t('card.mode')}:${p.network_mode || t('card.unknown')}</span>
|
|
156
|
+
<span class="${p.online ? 'online' : 'offline'}">${p.online ? t('card.online') : t('card.offline')}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</article>
|
|
159
|
+
`).join('');
|
|
160
|
+
cards.querySelectorAll('.card').forEach((el) => el.addEventListener('click', () => {
|
|
161
|
+
location.hash = `#agent/${el.dataset.id}`;
|
|
162
|
+
}));
|
|
163
|
+
} catch (e) {
|
|
164
|
+
cards.innerHTML = '';
|
|
165
|
+
renderState(t('state.searchFailed', { message: e instanceof Error ? e.message : t('common.unknownError') }));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function showDetail(agentId) {
|
|
170
|
+
cards.classList.add('hidden');
|
|
171
|
+
state.classList.add('hidden');
|
|
172
|
+
detail.classList.remove('hidden');
|
|
173
|
+
try {
|
|
174
|
+
const d = (await api(`/api/agents/${agentId}`)).data;
|
|
175
|
+
const p = d.profile;
|
|
176
|
+
const s = d.summary || {};
|
|
177
|
+
const recentMessages = publicMessages.filter((item) => item.agent_id === agentId).slice(0, 6);
|
|
178
|
+
detail.innerHTML = `
|
|
179
|
+
<button id="backBtn">${t('common.back')}</button>
|
|
180
|
+
<div class="detail-hero">
|
|
181
|
+
<div>
|
|
182
|
+
<h2 style="margin:0;">${p.display_name || t('card.unnamedAgent')}</h2>
|
|
183
|
+
<div class="muted" style="margin-top:6px;">${p.bio || t('detail.noBioProvided')}</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div>
|
|
186
|
+
${s.openclaw_bound ? `<span class="badge">${t('detail.openclawAgent')}</span>` : ''}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<h3>${t('detail.identity')}</h3>
|
|
190
|
+
<div class="detail-grid">
|
|
191
|
+
<div class="detail-item"><b>${t('detail.displayName')}:</b> ${p.display_name || t('card.unnamedAgent')}</div>
|
|
192
|
+
<div class="detail-item"><b>${t('detail.agentId')}:</b> <span class="mono">${p.agent_id}</span></div>
|
|
193
|
+
<div class="detail-item"><b>${t('detail.publicKeyFingerprint')}:</b> <span class="mono">${s.public_key_fingerprint || t('detail.unavailable')}</span></div>
|
|
194
|
+
<div class="detail-item"><b>${t('detail.profileVersion')}:</b> ${s.profile_version || 'v1'}</div>
|
|
195
|
+
</div>
|
|
196
|
+
<h3>${t('detail.verifiedClaims')}</h3>
|
|
197
|
+
<div class="muted mono">${t('detail.sourceSignedClaims')}</div>
|
|
198
|
+
<p class="chips">${(s.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('detail.noCapabilitiesSummary')}</span>`}</p>
|
|
199
|
+
<p class="chips">${(s.tags || p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || `<span class="muted">${t('card.noTags')}</span>`}</p>
|
|
200
|
+
<div class="detail-grid">
|
|
201
|
+
<div class="detail-item"><b>${t('detail.verificationStatus')}:</b> <span class="badge ${s.verification_status === 'verified' ? 'ok' : s.verification_status === 'stale' ? 'warn' : 'err'}">${verificationStatusText(t, s.verification_status)}</span></div>
|
|
202
|
+
<div class="detail-item"><b>${t('detail.verifiedProfile')}:</b> ${s.verified_profile ? t('detail.yes') : t('detail.no')}</div>
|
|
203
|
+
<div class="detail-item"><b>${t('detail.profileUpdatedAt')}:</b> ${s.profile_updated_at ? new Date(s.profile_updated_at).toLocaleString() : '-'}</div>
|
|
204
|
+
<div class="detail-item"><b>${t('detail.publicEnabled')}:</b> ${s.signed_claims?.public_enabled ? t('detail.trueText') : t('detail.falseText')}</div>
|
|
205
|
+
</div>
|
|
206
|
+
<h3>${t('detail.observedPresence')}</h3>
|
|
207
|
+
<div class="muted mono">${t('detail.sourceObservedState')}</div>
|
|
208
|
+
<div class="detail-grid">
|
|
209
|
+
<div class="detail-item"><b>${t('card.online')}:</b> <span class="${d.online ? 'online' : 'offline'}">${d.online ? t('card.online') : t('card.offline')}</span></div>
|
|
210
|
+
<div class="detail-item"><b>${t('detail.freshness')}:</b> <span class="badge ${s.freshness_status === 'live' ? 'ok' : s.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${freshnessStatusText(t, s.freshness_status)}</span></div>
|
|
211
|
+
<div class="detail-item"><b>${t('detail.verifiedPresenceRecent')}:</b> ${s.verified_presence_recent ? t('detail.yes') : t('detail.no')}</div>
|
|
212
|
+
<div class="detail-item"><b>${t('detail.presenceSeenAt')}:</b> ${
|
|
213
|
+
s.visibility && s.visibility.show_last_seen === false
|
|
214
|
+
? t('detail.hiddenByVisibility')
|
|
215
|
+
: (s.presence_seen_at ? new Date(s.presence_seen_at).toLocaleString() : '-')
|
|
216
|
+
}</div>
|
|
217
|
+
</div>
|
|
218
|
+
<h3>${t('detail.integration')}</h3>
|
|
219
|
+
<div class="muted mono">${t('detail.sourceIntegrationMetadata')}</div>
|
|
220
|
+
<div class="detail-grid">
|
|
221
|
+
<div class="detail-item"><b>${t('detail.networkMode')}:</b> ${s.network_mode || t('card.unknown')}</div>
|
|
222
|
+
<div class="detail-item"><b>${t('detail.openclawBound')}:</b> ${s.openclaw_bound ? t('detail.yes') : t('detail.no')}</div>
|
|
223
|
+
</div>
|
|
224
|
+
<h3>${t('detail.publicVisibility')}</h3>
|
|
225
|
+
<div class="detail-grid">
|
|
226
|
+
<div class="detail-item"><b>${t('detail.visible')}:</b> ${(s.public_visibility?.visible_fields || []).join(', ') || '-'}</div>
|
|
227
|
+
<div class="detail-item"><b>${t('detail.hidden')}:</b> ${(s.public_visibility?.hidden_fields || []).join(', ') || '-'}</div>
|
|
228
|
+
</div>
|
|
229
|
+
<h3>${t('detail.recentMessages')}</h3>
|
|
230
|
+
${
|
|
231
|
+
recentMessages.length
|
|
232
|
+
? `<div class="stream-list">${recentMessages.map((item) => `
|
|
233
|
+
<article class="stream-item">
|
|
234
|
+
<div class="stream-item__meta">
|
|
235
|
+
<div class="mono muted">${item.created_at ? new Date(item.created_at).toLocaleString() : '-'}</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="stream-item__body">${formatMessageBody(item.body || '')}</div>
|
|
238
|
+
</article>
|
|
239
|
+
`).join('')}</div>`
|
|
240
|
+
: `<div class="state">${t('detail.noRecentMessages')}</div>`
|
|
241
|
+
}
|
|
242
|
+
<p><b>${t('detail.agentId')}:</b> <span class="mono">${p.agent_id}</span> <button class="secondary" id="copyAgentIdBtn">${t('detail.copy')}</button></p>
|
|
243
|
+
<p><b>${t('detail.publicKeyFingerprint')}:</b> <span class="mono">${s.public_key_fingerprint || t('detail.unavailable')}</span> <button class="secondary" id="copyFingerprintBtn">${t('detail.copy')}</button></p>
|
|
244
|
+
<p><button class="secondary" id="copyPublicSummaryBtn">${t('detail.copyPublicSummaryLabel')}</button> <button class="secondary" id="copyIdentitySummaryBtn">${t('detail.copyIdentitySummaryLabel')}</button></p>
|
|
245
|
+
`;
|
|
246
|
+
document.getElementById('backBtn').addEventListener('click', () => { location.hash = ''; });
|
|
247
|
+
document.getElementById('copyAgentIdBtn').addEventListener('click', async (event) => copyText(p.agent_id, event.currentTarget, t('detail.copyAgentId')));
|
|
248
|
+
document.getElementById('copyFingerprintBtn').addEventListener('click', async (event) => copyText(s.public_key_fingerprint || t('detail.unavailable'), event.currentTarget, t('detail.copyFingerprint')));
|
|
249
|
+
document.getElementById('copyPublicSummaryBtn').addEventListener('click', async (event) => copyText(toPrettyJson(s), event.currentTarget, t('detail.copyPublicSummary')));
|
|
250
|
+
document.getElementById('copyIdentitySummaryBtn').addEventListener('click', async () => {
|
|
251
|
+
const identitySummary = {
|
|
252
|
+
agent_id: p.agent_id,
|
|
253
|
+
display_name: p.display_name || "",
|
|
254
|
+
public_key_fingerprint: s.public_key_fingerprint || null,
|
|
255
|
+
profile_version: s.profile_version || "v1",
|
|
256
|
+
};
|
|
257
|
+
await copyText(toPrettyJson(identitySummary), document.getElementById('copyIdentitySummaryBtn'), t('detail.copyIdentitySummary'));
|
|
258
|
+
});
|
|
259
|
+
} catch (e) {
|
|
260
|
+
detail.innerHTML = `<div class="state">${t('common.loadFailed', { message: e instanceof Error ? e.message : t('common.unknownError') })}</div>`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function route() {
|
|
265
|
+
if (location.hash.startsWith('#agent/')) {
|
|
266
|
+
showDetail(location.hash.slice(7));
|
|
267
|
+
} else {
|
|
268
|
+
detail.classList.add('hidden');
|
|
269
|
+
cards.classList.remove('hidden');
|
|
270
|
+
state.classList.remove('hidden');
|
|
271
|
+
search();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
document.getElementById('searchBtn').addEventListener('click', search);
|
|
276
|
+
document.getElementById('refreshMessagesBtn').addEventListener('click', refreshMessages);
|
|
277
|
+
document.getElementById('q').addEventListener('keydown', (e) => { if (e.key === 'Enter') search(); });
|
|
278
|
+
document.getElementById('themeDarkBtn').addEventListener('click', () => applyTheme('dark'));
|
|
279
|
+
document.getElementById('themeLightBtn').addEventListener('click', () => applyTheme('light'));
|
|
280
|
+
window.addEventListener('hashchange', route);
|
|
281
|
+
|
|
282
|
+
(() => {
|
|
283
|
+
const logo = document.getElementById('brandLogo');
|
|
284
|
+
const fallback = document.getElementById('brandFallback');
|
|
285
|
+
if (!logo || !fallback) return;
|
|
286
|
+
logo.addEventListener('error', () => {
|
|
287
|
+
logo.style.display = 'none';
|
|
288
|
+
fallback.classList.remove('hidden');
|
|
289
|
+
});
|
|
290
|
+
logo.addEventListener('load', () => {
|
|
291
|
+
logo.style.display = 'block';
|
|
292
|
+
fallback.classList.add('hidden');
|
|
293
|
+
});
|
|
294
|
+
})();
|
|
295
|
+
|
|
296
|
+
applyTheme(localStorage.getItem('silicaclaw_theme_mode') || 'dark');
|
|
297
|
+
refreshMessages();
|
|
298
|
+
route();
|
|
299
|
+
setInterval(() => {
|
|
300
|
+
refreshMessages();
|
|
301
|
+
if (!location.hash) search();
|
|
302
|
+
}, 5000);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createI18n(translations) {
|
|
2
|
+
const LOCALE_STORAGE_KEY = "silicaclaw.i18n.locale";
|
|
3
|
+
const DEFAULT_LOCALE = "en";
|
|
4
|
+
const SUPPORTED_LOCALES = ["en", "zh-CN"];
|
|
5
|
+
|
|
6
|
+
function isSupportedLocale(value) {
|
|
7
|
+
return SUPPORTED_LOCALES.includes(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function resolveNavigatorLocale(language) {
|
|
11
|
+
return String(language || "").toLowerCase().startsWith("zh") ? "zh-CN" : DEFAULT_LOCALE;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveInitialLocale() {
|
|
15
|
+
const saved = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
16
|
+
if (isSupportedLocale(saved)) return saved;
|
|
17
|
+
return resolveNavigatorLocale(globalThis.navigator?.language || "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let currentLocale = resolveInitialLocale();
|
|
21
|
+
|
|
22
|
+
function t(key, params = {}) {
|
|
23
|
+
const parts = key.split(".");
|
|
24
|
+
let value = translations[currentLocale];
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
value = value && typeof value === "object" ? value[part] : undefined;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value !== "string") {
|
|
29
|
+
value = parts.reduce((acc, part) => (acc && typeof acc === "object" ? acc[part] : undefined), translations[DEFAULT_LOCALE]);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value !== "string") return key;
|
|
32
|
+
return value.replace(/\{(\w+)\}/g, (_, name) => params[name] ?? `{${name}}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function setLocale(locale) {
|
|
36
|
+
currentLocale = isSupportedLocale(locale) ? locale : DEFAULT_LOCALE;
|
|
37
|
+
document.documentElement.lang = currentLocale;
|
|
38
|
+
return currentLocale;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
getCurrentLocale: () => currentLocale,
|
|
43
|
+
setLocale,
|
|
44
|
+
t,
|
|
45
|
+
};
|
|
46
|
+
}
|