@pingagent/sdk 0.1.10 → 0.1.12
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/bin/pingagent.js +1046 -27
- package/dist/chunk-6OA4F66H.js +3193 -0
- package/dist/chunk-BLHMTUID.js +3610 -0
- package/dist/chunk-HCQ7CEDE.js +3556 -0
- package/dist/chunk-R3D7LOGB.js +3553 -0
- package/dist/chunk-SMDQYV7Z.js +3173 -0
- package/dist/chunk-YBNFPOKO.js +3553 -0
- package/dist/index.d.ts +229 -8
- package/dist/index.js +21 -1
- package/dist/web-server.js +1041 -43
- package/package.json +2 -2
package/dist/web-server.js
CHANGED
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
getActiveSessionFilePath,
|
|
12
12
|
getSessionBindingAlertsFilePath,
|
|
13
13
|
getSessionMapFilePath,
|
|
14
|
+
getTrustRecommendationActionLabel,
|
|
14
15
|
loadIdentity,
|
|
15
16
|
normalizeTrustPolicyDoc,
|
|
16
17
|
readCurrentActiveSessionKey,
|
|
18
|
+
readIngressRuntimeStatus,
|
|
17
19
|
readSessionBindingAlerts,
|
|
18
20
|
readSessionBindings,
|
|
19
21
|
removeSessionBinding,
|
|
@@ -21,12 +23,13 @@ import {
|
|
|
21
23
|
summarizeTrustPolicyAudit,
|
|
22
24
|
updateStoredToken,
|
|
23
25
|
upsertTrustPolicyRecommendation
|
|
24
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-BLHMTUID.js";
|
|
25
27
|
|
|
26
28
|
// src/web-server.ts
|
|
27
29
|
import * as fs from "fs";
|
|
28
30
|
import * as http from "http";
|
|
29
31
|
import * as path from "path";
|
|
32
|
+
import { spawnSync } from "child_process";
|
|
30
33
|
import { SCHEMA_TEXT } from "@pingagent/schemas";
|
|
31
34
|
|
|
32
35
|
// src/host-panel-html.ts
|
|
@@ -75,6 +78,30 @@ function getHostPanelHtml() {
|
|
|
75
78
|
padding: 16px;
|
|
76
79
|
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.22);
|
|
77
80
|
}
|
|
81
|
+
.status-strip {
|
|
82
|
+
display: flex;
|
|
83
|
+
justify-content: space-between;
|
|
84
|
+
gap: 16px;
|
|
85
|
+
align-items: flex-start;
|
|
86
|
+
flex-wrap: wrap;
|
|
87
|
+
}
|
|
88
|
+
.status-main { display: grid; gap: 8px; }
|
|
89
|
+
.status-main h2 { margin: 0; font-size: 18px; }
|
|
90
|
+
.status-state {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 8px;
|
|
94
|
+
width: fit-content;
|
|
95
|
+
padding: 6px 12px;
|
|
96
|
+
border-radius: 999px;
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
letter-spacing: 0.08em;
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
border: 1px solid #334155;
|
|
101
|
+
}
|
|
102
|
+
.status-state.ready { background: rgba(34, 197, 94, 0.12); color: #86efac; border-color: rgba(34, 197, 94, 0.35); }
|
|
103
|
+
.status-state.degraded { background: rgba(248, 113, 113, 0.14); color: #fecaca; border-color: rgba(248, 113, 113, 0.45); }
|
|
104
|
+
.quickstart-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
|
78
105
|
.card h2, .card h3 { margin: 0 0 12px; font-size: 16px; }
|
|
79
106
|
.stats .value { font-size: 28px; font-weight: 700; margin-top: 6px; }
|
|
80
107
|
.runtime-layout { grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); align-items: start; }
|
|
@@ -126,6 +153,20 @@ function getHostPanelHtml() {
|
|
|
126
153
|
}
|
|
127
154
|
.empty { color: #94a3b8; font-size: 13px; }
|
|
128
155
|
.link-row { display: flex; gap: 10px; margin-top: 20px; }
|
|
156
|
+
.toolbar-row { display: flex; justify-content: space-between; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
|
|
157
|
+
.toolbar-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
|
158
|
+
.mode-toggle { display: inline-flex; gap: 6px; padding: 4px; border-radius: 999px; border: 1px solid #334155; background: #020617; }
|
|
159
|
+
.mode-toggle button {
|
|
160
|
+
border: 0;
|
|
161
|
+
background: transparent;
|
|
162
|
+
color: #94a3b8;
|
|
163
|
+
border-radius: 999px;
|
|
164
|
+
padding: 6px 10px;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
}
|
|
167
|
+
.mode-toggle button.active { background: #0f766e; color: #ecfeff; }
|
|
168
|
+
.summary-pills { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
|
169
|
+
.summary-pills .pill { font-size: 11px; }
|
|
129
170
|
@media (max-width: 1000px) {
|
|
130
171
|
.layout { grid-template-columns: 1fr; }
|
|
131
172
|
.sidebar { border-right: none; border-bottom: 1px solid #1e293b; }
|
|
@@ -155,15 +196,31 @@ function getHostPanelHtml() {
|
|
|
155
196
|
</div>
|
|
156
197
|
<div class="header-actions">
|
|
157
198
|
<span class="pill" id="runtimeModePill">runtime_mode=bridge</span>
|
|
199
|
+
<span class="pill" id="receiveModePill">receive_mode=webhook</span>
|
|
158
200
|
<span class="pill" id="policyPathPill">policy=\u2026</span>
|
|
201
|
+
<button class="secondary-btn" id="fixHooksBtn" style="width:auto">Fix OpenClaw Hooks</button>
|
|
202
|
+
<button class="secondary-btn" id="publicLinkBtn" style="width:auto">Public Link</button>
|
|
203
|
+
<button class="secondary-btn" id="contactCardBtn" style="width:auto">Contact Card</button>
|
|
204
|
+
<a class="secondary-btn" href="/a/demo" target="_blank" rel="noreferrer" style="width:auto">Try Demo Agent</a>
|
|
159
205
|
</div>
|
|
160
206
|
</div>
|
|
161
207
|
|
|
162
208
|
<section id="runtimePanel" class="panel active">
|
|
209
|
+
<div class="card" id="activationCard" style="margin-bottom:16px"></div>
|
|
163
210
|
<div class="grid stats" id="statsGrid"></div>
|
|
164
211
|
<div class="grid runtime-layout" style="margin-top:16px">
|
|
165
212
|
<div class="card">
|
|
166
|
-
<
|
|
213
|
+
<div class="toolbar-row">
|
|
214
|
+
<h2 style="margin:0">Recent Sessions</h2>
|
|
215
|
+
<div class="toolbar-actions">
|
|
216
|
+
<button class="secondary-btn" id="toggleUnreadBtn" style="width:auto">Unread only: off</button>
|
|
217
|
+
<button class="secondary-btn" id="nextUnreadBtn" style="width:auto">Next unread</button>
|
|
218
|
+
<div class="mode-toggle" aria-label="Runtime detail mode">
|
|
219
|
+
<button id="detailModeBasicBtn" class="active" type="button">Basic</button>
|
|
220
|
+
<button id="detailModeAdvancedBtn" type="button">Advanced</button>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
167
224
|
<div class="sessions" id="sessionList"></div>
|
|
168
225
|
</div>
|
|
169
226
|
<div class="grid">
|
|
@@ -253,19 +310,67 @@ function getHostPanelHtml() {
|
|
|
253
310
|
<div class="audit-list" id="policyAuditList"></div>
|
|
254
311
|
</div>
|
|
255
312
|
</div>
|
|
313
|
+
|
|
314
|
+
<div class="card" style="margin-top:16px">
|
|
315
|
+
<h2>Profile + Capability Card</h2>
|
|
316
|
+
<div class="form-grid">
|
|
317
|
+
<label class="label">Display name</label>
|
|
318
|
+
<input id="profileDisplayName" placeholder="Agent name">
|
|
319
|
+
<label class="label">Bio</label>
|
|
320
|
+
<textarea id="profileBio" placeholder="What this agent does"></textarea>
|
|
321
|
+
<label class="label">Tags (comma separated)</label>
|
|
322
|
+
<input id="profileTags" placeholder="coding, devops">
|
|
323
|
+
<label class="label">Legacy capabilities (comma separated)</label>
|
|
324
|
+
<input id="profileCapabilities" placeholder="coding, testing">
|
|
325
|
+
<label class="label">Capability summary</label>
|
|
326
|
+
<textarea id="capabilityCardSummary" placeholder="Short machine-readable summary"></textarea>
|
|
327
|
+
<label class="label">Accepts new work</label>
|
|
328
|
+
<select id="capabilityCardAcceptsNewWork">
|
|
329
|
+
<option value="">(unspecified)</option>
|
|
330
|
+
<option value="true">true</option>
|
|
331
|
+
<option value="false">false</option>
|
|
332
|
+
</select>
|
|
333
|
+
<label class="label">Preferred contact mode</label>
|
|
334
|
+
<select id="capabilityCardContactMode">
|
|
335
|
+
<option value="">(unspecified)</option>
|
|
336
|
+
<option value="dm">dm</option>
|
|
337
|
+
<option value="task">task</option>
|
|
338
|
+
<option value="either">either</option>
|
|
339
|
+
</select>
|
|
340
|
+
<label class="label">Capability entries JSON</label>
|
|
341
|
+
<textarea id="capabilityCardItems" placeholder='[{"id":"coding","label":"Coding","accepts_tasks":true}]'></textarea>
|
|
342
|
+
<div class="row-actions">
|
|
343
|
+
<button class="action-btn" id="saveProfileBtn">Save Profile</button>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
256
347
|
</section>
|
|
257
348
|
</main>
|
|
258
349
|
</div>
|
|
259
350
|
|
|
260
351
|
<script>
|
|
352
|
+
const initialQuery = (function () {
|
|
353
|
+
const params = new URLSearchParams(window.location.search);
|
|
354
|
+
const profile = params.get('profile');
|
|
355
|
+
const sessionKey = params.get('session_key');
|
|
356
|
+
const view = params.get('view');
|
|
357
|
+
return {
|
|
358
|
+
profile: profile && profile.trim() ? profile.trim() : null,
|
|
359
|
+
sessionKey: sessionKey && sessionKey.trim() ? sessionKey.trim() : null,
|
|
360
|
+
view: view === 'policy' ? 'policy' : 'runtime',
|
|
361
|
+
};
|
|
362
|
+
})();
|
|
363
|
+
|
|
261
364
|
const state = {
|
|
262
|
-
selectedProfile: sessionStorage.getItem('pingagent_host_panel_profile') || null,
|
|
263
|
-
currentTab:
|
|
365
|
+
selectedProfile: initialQuery.profile || sessionStorage.getItem('pingagent_host_panel_profile') || null,
|
|
366
|
+
currentTab: initialQuery.view,
|
|
264
367
|
profiles: [],
|
|
265
368
|
overview: null,
|
|
266
369
|
session: null,
|
|
267
370
|
policy: null,
|
|
268
|
-
selectedSessionKey: null,
|
|
371
|
+
selectedSessionKey: initialQuery.sessionKey || null,
|
|
372
|
+
detailMode: sessionStorage.getItem('pingagent_host_panel_detail_mode') || 'basic',
|
|
373
|
+
showUnreadOnly: false,
|
|
269
374
|
};
|
|
270
375
|
|
|
271
376
|
function esc(value) {
|
|
@@ -277,11 +382,57 @@ function getHostPanelHtml() {
|
|
|
277
382
|
.replace(/'/g, ''');
|
|
278
383
|
}
|
|
279
384
|
|
|
385
|
+
function parseCsvList(value) {
|
|
386
|
+
return String(value == null ? '' : value)
|
|
387
|
+
.split(',')
|
|
388
|
+
.map(function (item) { return item.trim(); })
|
|
389
|
+
.filter(Boolean);
|
|
390
|
+
}
|
|
391
|
+
|
|
280
392
|
function fmtTs(value) {
|
|
281
393
|
if (!value) return '-';
|
|
282
394
|
try { return new Date(value).toLocaleString(); } catch { return String(value); }
|
|
283
395
|
}
|
|
284
396
|
|
|
397
|
+
function syncUrlState() {
|
|
398
|
+
const url = new URL(window.location.href);
|
|
399
|
+
if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
|
|
400
|
+
else url.searchParams.delete('profile');
|
|
401
|
+
if (state.selectedSessionKey) url.searchParams.set('session_key', state.selectedSessionKey);
|
|
402
|
+
else url.searchParams.delete('session_key');
|
|
403
|
+
url.searchParams.set('view', state.currentTab === 'policy' ? 'policy' : 'runtime');
|
|
404
|
+
history.replaceState(null, '', url.pathname + (url.search ? url.search : ''));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function setDetailMode(mode) {
|
|
408
|
+
state.detailMode = mode === 'advanced' ? 'advanced' : 'basic';
|
|
409
|
+
sessionStorage.setItem('pingagent_host_panel_detail_mode', state.detailMode);
|
|
410
|
+
document.getElementById('detailModeBasicBtn').classList.toggle('active', state.detailMode === 'basic');
|
|
411
|
+
document.getElementById('detailModeAdvancedBtn').classList.toggle('active', state.detailMode === 'advanced');
|
|
412
|
+
if (state.overview) renderOverview();
|
|
413
|
+
if (state.session) renderSession();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function buildSessionLink(sessionKey) {
|
|
417
|
+
const url = new URL(window.location.href);
|
|
418
|
+
if (state.selectedProfile) url.searchParams.set('profile', state.selectedProfile);
|
|
419
|
+
else url.searchParams.delete('profile');
|
|
420
|
+
if (sessionKey) url.searchParams.set('session_key', sessionKey);
|
|
421
|
+
else url.searchParams.delete('session_key');
|
|
422
|
+
url.searchParams.set('view', 'runtime');
|
|
423
|
+
return url.toString();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function copyText(text, fallbackLabel) {
|
|
427
|
+
try {
|
|
428
|
+
await navigator.clipboard.writeText(text);
|
|
429
|
+
window.alert((fallbackLabel || 'Copied') + ':
|
|
430
|
+
' + text);
|
|
431
|
+
} catch {
|
|
432
|
+
window.prompt(fallbackLabel || 'Copy', text);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
285
436
|
async function api(path, opts) {
|
|
286
437
|
let url = path;
|
|
287
438
|
if (state.selectedProfile) {
|
|
@@ -331,6 +482,7 @@ function getHostPanelHtml() {
|
|
|
331
482
|
document.getElementById('navPolicy').classList.toggle('active', tab === 'policy');
|
|
332
483
|
document.getElementById('runtimePanel').classList.toggle('active', tab === 'runtime');
|
|
333
484
|
document.getElementById('policyPanel').classList.toggle('active', tab === 'policy');
|
|
485
|
+
syncUrlState();
|
|
334
486
|
}
|
|
335
487
|
|
|
336
488
|
function renderHeader() {
|
|
@@ -338,20 +490,148 @@ function getHostPanelHtml() {
|
|
|
338
490
|
const profileLabel = state.selectedProfile ? 'profile=' + state.selectedProfile : 'Select profile';
|
|
339
491
|
const title = overview ? ('Host Panel \xB7 ' + overview.did) : 'PingAgent Host Panel';
|
|
340
492
|
const tier = overview && overview.subscription ? overview.subscription.tier : null;
|
|
493
|
+
const receiveMode = overview && overview.ingressRuntime ? overview.ingressRuntime.receive_mode : 'webhook';
|
|
341
494
|
document.getElementById('headerTitle').textContent = title;
|
|
342
495
|
document.getElementById('headerSubtitle').textContent = overview
|
|
343
496
|
? (profileLabel + ' \xB7 ' + overview.serverUrl + (tier ? (' \xB7 tier=' + tier) : '') + ' \xB7 sessions=' + overview.sessionsTotal + ' \xB7 unread=' + overview.unreadTotal)
|
|
344
497
|
: profileLabel;
|
|
345
498
|
document.getElementById('runtimeModePill').textContent = overview ? ('runtime_mode=' + overview.runtimeMode) : 'runtime_mode=\u2026';
|
|
499
|
+
document.getElementById('receiveModePill').textContent = 'receive_mode=' + receiveMode;
|
|
346
500
|
document.getElementById('policyPathPill').textContent = overview ? ('policy=' + overview.trustPolicyPath) : 'policy=\u2026';
|
|
501
|
+
document.getElementById('fixHooksBtn').textContent = receiveMode === 'polling_degraded' || (overview && overview.ingressRuntime && overview.ingressRuntime.hooks_last_error) ? 'Fix now' : 'Fix OpenClaw Hooks';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function ingressStatusModel(overview) {
|
|
505
|
+
const ingress = overview && overview.ingressRuntime ? overview.ingressRuntime : null;
|
|
506
|
+
const degraded = !ingress || ingress.receive_mode === 'polling_degraded' || !!ingress.hooks_last_error;
|
|
507
|
+
return {
|
|
508
|
+
degraded: degraded,
|
|
509
|
+
label: degraded ? 'Degraded' : 'Ready',
|
|
510
|
+
className: degraded ? 'degraded' : 'ready',
|
|
511
|
+
detail: degraded
|
|
512
|
+
? (ingress && ingress.hooks_last_error ? ingress.hooks_last_error : 'Webhook ingress is degraded. Fix hooks or keep running on polling fallback.')
|
|
513
|
+
: 'Webhook ingress is healthy. New inbound messages land on the main runtime path.',
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function recommendationActionLabel(item) {
|
|
518
|
+
if (!item) return 'Apply Recommendation';
|
|
519
|
+
if (item.status === 'dismissed' || item.status === 'superseded') return 'Reopen';
|
|
520
|
+
if (item.status === 'applied') return 'Applied';
|
|
521
|
+
if (item.primary_action_label) return item.primary_action_label;
|
|
522
|
+
if (item.policy === 'contact' && item.action === 'approve') return 'Approve + remember sender';
|
|
523
|
+
if (item.policy === 'contact' && item.action === 'manual') return 'Keep contact manual';
|
|
524
|
+
if (item.policy === 'contact' && item.action === 'reject') return 'Block this sender';
|
|
525
|
+
if (item.policy === 'task' && item.action === 'bridge') return 'Keep tasks manual';
|
|
526
|
+
if (item.policy === 'task' && item.action === 'execute') return 'Allow tasks from this sender';
|
|
527
|
+
if (item.policy === 'task' && item.action === 'deny') return 'Block tasks from this sender';
|
|
528
|
+
return 'Apply Recommendation';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function formatCapabilityCardEditor(card) {
|
|
532
|
+
if (!card) return '';
|
|
533
|
+
try {
|
|
534
|
+
return JSON.stringify(Array.isArray(card.capabilities) ? card.capabilities : [], null, 2);
|
|
535
|
+
} catch {
|
|
536
|
+
return '';
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function renderSummaryBlock(summary) {
|
|
541
|
+
if (!summary) {
|
|
542
|
+
return '<div class="empty">No carry-forward summary yet. Save one below to make delegation and session continuation cheaper.</div>';
|
|
543
|
+
}
|
|
544
|
+
const parts = [
|
|
545
|
+
summary.objective ? '<div><span class="label">Objective</span><div style="margin-top:6px">' + esc(summary.objective) + '</div></div>' : '',
|
|
546
|
+
summary.context ? '<div><span class="label">Context</span><div style="margin-top:6px">' + esc(summary.context) + '</div></div>' : '',
|
|
547
|
+
summary.constraints ? '<div><span class="label">Constraints</span><div style="margin-top:6px">' + esc(summary.constraints) + '</div></div>' : '',
|
|
548
|
+
summary.decisions ? '<div><span class="label">Decisions</span><div style="margin-top:6px">' + esc(summary.decisions) + '</div></div>' : '',
|
|
549
|
+
summary.open_questions ? '<div><span class="label">Open Questions</span><div style="margin-top:6px">' + esc(summary.open_questions) + '</div></div>' : '',
|
|
550
|
+
summary.next_action ? '<div><span class="label">Next Action</span><div style="margin-top:6px">' + esc(summary.next_action) + '</div></div>' : '',
|
|
551
|
+
summary.handoff_ready_text ? '<div><span class="label">Handoff Ready</span><pre style="margin-top:6px">' + esc(summary.handoff_ready_text) + '</pre></div>' : '',
|
|
552
|
+
].filter(Boolean);
|
|
553
|
+
parts.push('<div class="muted small">updated=' + esc(fmtTs(summary.updated_at)) + '</div>');
|
|
554
|
+
return parts.join('');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function renderHandoffBlock(handoff) {
|
|
558
|
+
if (!handoff) return '';
|
|
559
|
+
return '' +
|
|
560
|
+
'<div style="margin-top:8px;padding:10px 12px;border:1px solid #1d4ed8;border-radius:10px;background:rgba(30,64,175,0.18)">' +
|
|
561
|
+
'<div class="label">Handoff</div>' +
|
|
562
|
+
(handoff.objective ? '<div class="muted small" style="margin-top:6px">objective=' + esc(handoff.objective) + '</div>' : '') +
|
|
563
|
+
(handoff.priority ? '<div class="muted small">priority=' + esc(handoff.priority) + '</div>' : '') +
|
|
564
|
+
(handoff.success_criteria ? '<div class="muted small">success=' + esc(handoff.success_criteria) + '</div>' : '') +
|
|
565
|
+
(handoff.callback_session_key ? '<div class="muted small">callback=' + esc(handoff.callback_session_key) + '</div>' : '') +
|
|
566
|
+
(handoff.delegated_by || handoff.delegated_to
|
|
567
|
+
? '<div class="muted small">delegated_by=' + esc(handoff.delegated_by || '(unknown)') + ' \xB7 delegated_to=' + esc(handoff.delegated_to || '(unknown)') + '</div>'
|
|
568
|
+
: '') +
|
|
569
|
+
(handoff.carry_forward_summary
|
|
570
|
+
? '<pre style="margin-top:8px">' + esc(handoff.carry_forward_summary) + '</pre>'
|
|
571
|
+
: '') +
|
|
572
|
+
'</div>';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function getOverviewSessions() {
|
|
576
|
+
return state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function getVisibleSessions() {
|
|
580
|
+
const sessions = getOverviewSessions();
|
|
581
|
+
return state.showUnreadOnly
|
|
582
|
+
? sessions.filter(function (session) { return Number(session.unread_count || 0) > 0; })
|
|
583
|
+
: sessions;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function syncSelectedSessionFromOverview() {
|
|
587
|
+
const allSessions = getOverviewSessions();
|
|
588
|
+
const visibleSessions = getVisibleSessions();
|
|
589
|
+
if (!allSessions.length) {
|
|
590
|
+
state.selectedSessionKey = null;
|
|
591
|
+
state.session = null;
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const hasSelected = allSessions.some(function (session) { return session.session_key === state.selectedSessionKey; });
|
|
595
|
+
if (!hasSelected) {
|
|
596
|
+
state.selectedSessionKey = visibleSessions.length ? visibleSessions[0].session_key : allSessions[0].session_key;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (state.showUnreadOnly) {
|
|
600
|
+
const stillVisible = visibleSessions.some(function (session) { return session.session_key === state.selectedSessionKey; });
|
|
601
|
+
if (!stillVisible) state.selectedSessionKey = visibleSessions.length ? visibleSessions[0].session_key : null;
|
|
602
|
+
}
|
|
347
603
|
}
|
|
348
604
|
|
|
349
605
|
function renderOverview() {
|
|
350
606
|
const overview = state.overview;
|
|
351
607
|
if (!overview) return;
|
|
608
|
+
syncSelectedSessionFromOverview();
|
|
609
|
+
const ingressState = ingressStatusModel(overview);
|
|
610
|
+
document.getElementById('activationCard').innerHTML =
|
|
611
|
+
'<div class="status-strip">' +
|
|
612
|
+
'<div class="status-main">' +
|
|
613
|
+
'<h2>Activation</h2>' +
|
|
614
|
+
'<div class="status-state ' + ingressState.className + '">' + esc(ingressState.label) + '</div>' +
|
|
615
|
+
'<div class="muted small">' + esc(ingressState.detail) + '</div>' +
|
|
616
|
+
'<div class="muted small">Public link: ' + esc(overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '(not ready yet)') + '</div>' +
|
|
617
|
+
'</div>' +
|
|
618
|
+
'<div style="min-width:320px">' +
|
|
619
|
+
'<div class="label">Quick Start</div>' +
|
|
620
|
+
'<div class="quickstart-row">' +
|
|
621
|
+
'<button class="action-btn demo-preset-btn" data-preset="hello">Demo: hello</button>' +
|
|
622
|
+
'<button class="secondary-btn demo-preset-btn" data-preset="delegate">Demo: delegate</button>' +
|
|
623
|
+
'<button class="secondary-btn demo-preset-btn" data-preset="trust">Demo: trust</button>' +
|
|
624
|
+
'</div>' +
|
|
625
|
+
'<div class="quickstart-row">' +
|
|
626
|
+
'<button class="secondary-btn" id="copyPublicLinkBtn">Copy Public Link</button>' +
|
|
627
|
+
'<button class="secondary-btn" id="makeContactCardBtn">Share Contact Card</button>' +
|
|
628
|
+
'</div>' +
|
|
629
|
+
'</div>' +
|
|
630
|
+
'</div>';
|
|
352
631
|
const subscription = overview.subscription || null;
|
|
353
632
|
const stats = [
|
|
354
633
|
{ label: 'Plan', value: subscription ? subscription.tier : 'ghost', sub: subscription ? subscription.summary : 'subscription unavailable' },
|
|
634
|
+
{ label: 'Ingress', value: overview.ingressRuntime ? overview.ingressRuntime.receive_mode : 'webhook', sub: overview.ingressRuntime && overview.ingressRuntime.reason ? overview.ingressRuntime.reason : 'OpenClaw ingress receive mode' },
|
|
355
635
|
{ label: 'Relay', value: subscription ? (subscription.usage.relay_today + '/' + subscription.usage.relay_limit) : '-', sub: subscription ? ('retention=' + subscription.retention_label) : 'daily relay usage' },
|
|
356
636
|
{ label: 'Alias', value: subscription ? (subscription.usage.alias_count + '/' + subscription.usage.alias_limit) : '-', sub: subscription ? (subscription.audit_export_allowed ? 'audit export enabled' : 'audit export off on this plan') : 'identity limits' },
|
|
357
637
|
{ label: 'Sessions', value: overview.sessionsTotal, sub: JSON.stringify(overview.trustCounts || {}) },
|
|
@@ -359,29 +639,33 @@ function getHostPanelHtml() {
|
|
|
359
639
|
{ label: 'Tasks', value: overview.tasksTotal, sub: 'recent local task threads' },
|
|
360
640
|
{ label: 'Audit', value: overview.auditSummary.total_events, sub: 'policy / runtime audit events' },
|
|
361
641
|
{ label: 'Recommendations', value: overview.recommendationSummary ? overview.recommendationSummary.total : overview.recommendations.length, sub: overview.recommendationSummary ? JSON.stringify(overview.recommendationSummary.by_status || {}) : 'learned policy suggestions' },
|
|
642
|
+
{ label: 'Public Link', value: overview.publicSelf && overview.publicSelf.public_url ? 'ready' : 'disabled', sub: overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : 'create a hosted shareable profile link' },
|
|
362
643
|
];
|
|
363
644
|
document.getElementById('statsGrid').innerHTML = stats.map(function (item) {
|
|
364
645
|
return '<div class="card"><div class="label">' + esc(item.label) + '</div><div class="value">' + esc(item.value) + '</div><div class="muted small">' + esc(item.sub) + '</div></div>';
|
|
365
646
|
}).join('');
|
|
366
647
|
|
|
367
|
-
const
|
|
648
|
+
const toggleUnreadBtn = document.getElementById('toggleUnreadBtn');
|
|
649
|
+
if (toggleUnreadBtn) toggleUnreadBtn.textContent = 'Unread only: ' + (state.showUnreadOnly ? 'on' : 'off');
|
|
650
|
+
const sessions = getVisibleSessions();
|
|
368
651
|
if (!sessions.length) {
|
|
369
|
-
document.getElementById('sessionList').innerHTML = '<div class="empty">No sessions yet
|
|
652
|
+
document.getElementById('sessionList').innerHTML = '<div class="empty">' + (state.showUnreadOnly ? 'No unread sessions.' : 'No sessions yet.') + '</div>';
|
|
370
653
|
} else {
|
|
371
|
-
if (!state.selectedSessionKey) state.selectedSessionKey = sessions[0].session_key;
|
|
372
654
|
document.getElementById('sessionList').innerHTML = sessions.map(function (session) {
|
|
373
655
|
const active = session.session_key === state.selectedSessionKey ? ' active' : '';
|
|
374
656
|
const badges = [
|
|
375
657
|
'<span class="badge ' + esc(session.trust_state) + '">' + esc(session.trust_state) + '</span>',
|
|
376
658
|
session.binding_alert
|
|
377
|
-
? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="
|
|
659
|
+
? '<button type="button" class="badge alert rebind-badge-btn" data-session="' + esc(session.session_key) + '" data-conversation="' + esc(session.conversation_id || '') + '" data-bound-session="' + esc(session.mapped_work_session || '') + '" data-remote-did="' + esc(session.remote_did || '') + '" title="OpenClaw chat link needs attention">Needs reconnect</button>'
|
|
378
660
|
: '',
|
|
379
661
|
].filter(Boolean).join('');
|
|
380
662
|
return '<div class="session-row' + active + '" data-session="' + esc(session.session_key) + '">' +
|
|
381
|
-
'<div class="top"><strong>' + esc(session.remote_did || session.
|
|
382
|
-
'<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
|
|
663
|
+
'<div class="top"><strong>' + esc(session.remote_did || session.session_key || 'unknown') + '</strong><div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">' + badges + '</div></div>' +
|
|
383
664
|
'<div class="muted small">unread=' + esc(session.unread_count) + ' \xB7 last=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
|
|
384
|
-
|
|
665
|
+
(state.detailMode === 'advanced'
|
|
666
|
+
? '<div class="muted small" style="margin-top:6px">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
|
|
667
|
+
'<div class="muted small">work_session=' + esc(session.mapped_work_session || '(unbound)') + (session.is_active_work_session ? ' \xB7 active_chat=true' : '') + '</div>'
|
|
668
|
+
: '') +
|
|
385
669
|
'<div class="muted small" style="margin-top:6px">' + esc(session.last_message_preview || '(no preview)') + '</div>' +
|
|
386
670
|
'</div>';
|
|
387
671
|
}).join('');
|
|
@@ -416,6 +700,7 @@ function getHostPanelHtml() {
|
|
|
416
700
|
'<div class="muted small">updated=' + esc(fmtTs(task.updated_at)) + '</div>' +
|
|
417
701
|
(task.result_summary ? '<div style="margin-top:8px">' + esc(task.result_summary) + '</div>' : '') +
|
|
418
702
|
(task.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(task.error_message) + '</div>' : '') +
|
|
703
|
+
renderHandoffBlock(task.handoff) +
|
|
419
704
|
'</div>';
|
|
420
705
|
}).join('')
|
|
421
706
|
: '<div class="empty">No recent task threads.</div>';
|
|
@@ -433,6 +718,56 @@ function getHostPanelHtml() {
|
|
|
433
718
|
: '') +
|
|
434
719
|
'</div>' + document.getElementById('taskList').innerHTML;
|
|
435
720
|
}
|
|
721
|
+
|
|
722
|
+
document.querySelectorAll('.demo-preset-btn').forEach(function (btn) {
|
|
723
|
+
btn.addEventListener('click', async function () {
|
|
724
|
+
const preset = btn.getAttribute('data-preset');
|
|
725
|
+
const result = await api('/api/runtime/demo', {
|
|
726
|
+
method: 'POST',
|
|
727
|
+
headers: { 'Content-Type': 'application/json' },
|
|
728
|
+
body: JSON.stringify({ preset: preset }),
|
|
729
|
+
});
|
|
730
|
+
window.alert(result.sent
|
|
731
|
+
? ('Demo agent messaged.
|
|
732
|
+
|
|
733
|
+
conversation=' + result.conversation_id)
|
|
734
|
+
: ('Demo agent ready.
|
|
735
|
+
|
|
736
|
+
conversation=' + result.conversation_id));
|
|
737
|
+
await refreshAll();
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
const copyPublicLinkBtn = document.getElementById('copyPublicLinkBtn');
|
|
741
|
+
if (copyPublicLinkBtn) {
|
|
742
|
+
copyPublicLinkBtn.addEventListener('click', async function () {
|
|
743
|
+
const url = overview.publicSelf && overview.publicSelf.public_url ? overview.publicSelf.public_url : '';
|
|
744
|
+
if (!url) {
|
|
745
|
+
window.alert('No public link yet. Use Public Link or refresh after hosted auto-create.');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
await navigator.clipboard.writeText(url);
|
|
750
|
+
window.alert('Copied public link:
|
|
751
|
+
' + url);
|
|
752
|
+
} catch {
|
|
753
|
+
window.prompt('Copy public link', url);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
const makeContactCardBtn = document.getElementById('makeContactCardBtn');
|
|
758
|
+
if (makeContactCardBtn) {
|
|
759
|
+
makeContactCardBtn.addEventListener('click', async function () {
|
|
760
|
+
const result = await api('/api/public/contact-card', {
|
|
761
|
+
method: 'POST',
|
|
762
|
+
headers: { 'Content-Type': 'application/json' },
|
|
763
|
+
body: JSON.stringify({
|
|
764
|
+
target_did: overview && overview.did ? overview.did : undefined,
|
|
765
|
+
}),
|
|
766
|
+
});
|
|
767
|
+
window.alert('Contact card ready:
|
|
768
|
+
' + (result.share_url || '(missing share_url)'));
|
|
769
|
+
});
|
|
770
|
+
}
|
|
436
771
|
}
|
|
437
772
|
|
|
438
773
|
function renderSession() {
|
|
@@ -449,32 +784,99 @@ function getHostPanelHtml() {
|
|
|
449
784
|
const messages = Array.isArray(detail.messages) ? detail.messages : [];
|
|
450
785
|
const auditEvents = Array.isArray(detail.auditEvents) ? detail.auditEvents : [];
|
|
451
786
|
const recommendations = Array.isArray(detail.recommendations) ? detail.recommendations : [];
|
|
787
|
+
const openRecommendation = recommendations.find(function (item) { return item.status === 'open'; }) || null;
|
|
788
|
+
const reopenRecommendation = recommendations.find(function (item) { return item.status === 'dismissed' || item.status === 'superseded'; }) || null;
|
|
452
789
|
const binding = detail.binding || null;
|
|
453
790
|
const bindingAlert = detail.bindingAlert || null;
|
|
454
791
|
const activeWorkSession = detail.activeWorkSession || null;
|
|
792
|
+
const summary = detail.sessionSummary || null;
|
|
793
|
+
const isAdvanced = state.detailMode === 'advanced';
|
|
794
|
+
const sessionLink = buildSessionLink(session.session_key);
|
|
795
|
+
const summaryPills = [];
|
|
796
|
+
if (contact && contact.action) summaryPills.push('<span class="pill">contact=' + esc(contact.action) + '</span>');
|
|
797
|
+
if (task && task.action) summaryPills.push('<span class="pill">task=' + esc(task.action) + '</span>');
|
|
798
|
+
if (session.trust_state) summaryPills.push('<span class="pill">trust=' + esc(session.trust_state) + '</span>');
|
|
799
|
+
if (Number(session.unread_count || 0) > 0) summaryPills.push('<span class="pill">unread=' + esc(session.unread_count) + '</span>');
|
|
800
|
+
if (binding && binding.session_key) summaryPills.push('<span class="pill">chat link attached</span>');
|
|
801
|
+
else if (activeWorkSession) summaryPills.push('<span class="pill">current OpenClaw chat available</span>');
|
|
802
|
+
const policyBlock = isAdvanced
|
|
803
|
+
? '<pre style="margin-top:8px">[Contact]\\naction=' + esc(contact.action) + '\\nsource=' + esc(contact.source) + (contact.matched_rule ? '\\nmatched_rule=' + esc(contact.matched_rule) : '') + '\\n' + esc(contact.explanation) + '\\n\\n[Task]\\naction=' + esc(task.action) + '\\nsource=' + esc(task.source) + (task.matched_rule ? '\\nmatched_rule=' + esc(task.matched_rule) : '') + '\\n' + esc(task.explanation) + '</pre>'
|
|
804
|
+
: '<div class="summary-pills" style="margin-top:8px">' + summaryPills.join('') + '</div>' +
|
|
805
|
+
'<div class="muted small" style="margin-top:10px">' + esc(contact.explanation || '(no contact explanation)') + '</div>' +
|
|
806
|
+
'<div class="muted small" style="margin-top:6px">' + esc(task.explanation || '(no task explanation)') + '</div>';
|
|
455
807
|
|
|
456
808
|
el.innerHTML = '' +
|
|
457
809
|
'<div class="two-col">' +
|
|
458
810
|
'<div>' +
|
|
459
811
|
'<div class="label">Session</div>' +
|
|
460
812
|
'<div style="margin-top:8px"><strong>' + esc(session.remote_did || '(unknown)') + '</strong></div>' +
|
|
461
|
-
'<div class="muted small">session=' + esc(session.session_key) + '</div>' +
|
|
462
|
-
'<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
|
|
463
813
|
'<div class="muted small">trust=' + esc(session.trust_state) + ' \xB7 unread=' + esc(session.unread_count) + '</div>' +
|
|
464
814
|
'<div class="muted small">last activity=' + esc(fmtTs(session.last_remote_activity_at || session.updated_at)) + '</div>' +
|
|
465
|
-
|
|
466
|
-
|
|
815
|
+
(isAdvanced
|
|
816
|
+
? '<div style="margin-top:8px">' +
|
|
817
|
+
'<div class="muted small">session=' + esc(session.session_key) + '</div>' +
|
|
818
|
+
'<div class="muted small">conversation=' + esc(session.conversation_id || '(none)') + '</div>' +
|
|
819
|
+
'<div class="muted small">active_chat_session=' + esc(activeWorkSession || '(none)') + '</div>' +
|
|
820
|
+
'<div class="muted small">binding=' + esc(binding ? binding.session_key : '(unbound)') + '</div>' +
|
|
821
|
+
'</div>'
|
|
822
|
+
: '') +
|
|
823
|
+
(openRecommendation
|
|
824
|
+
? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(openRecommendation)) + '</div>'
|
|
825
|
+
: (reopenRecommendation ? '<div class="muted small" style="margin-top:8px">trust_action=' + esc(recommendationActionLabel(reopenRecommendation)) + '</div>' : '')) +
|
|
826
|
+
(summaryPills.length ? '<div class="summary-pills">' + summaryPills.join('') + '</div>' : '') +
|
|
467
827
|
(bindingAlert
|
|
468
|
-
? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs
|
|
828
|
+
? '<div style="margin-top:10px;padding:10px 12px;border:1px solid #ef4444;border-radius:10px;background:rgba(127,29,29,0.25);color:#fecaca"><strong>Needs reconnect</strong><div class="small" style="margin-top:6px">' + esc(isAdvanced ? (bindingAlert.message || 'Bound work session is missing. Rebind this PingAgent conversation to the current chat session.') : 'OpenClaw chat link is stale. Attach this PingAgent session to the current OpenClaw chat.') + '</div></div>'
|
|
469
829
|
: '') +
|
|
470
830
|
'<div class="row-actions">' +
|
|
471
|
-
|
|
472
|
-
|
|
831
|
+
(session.trust_state === 'pending'
|
|
832
|
+
? '<button class="action-btn approve-session-btn" data-session="' + esc(session.session_key) + '">Approve Contact</button>'
|
|
833
|
+
: '') +
|
|
834
|
+
(openRecommendation
|
|
835
|
+
? '<button class="action-btn apply-session-recommendation-btn" data-session="' + esc(session.session_key) + '">' + esc(recommendationActionLabel(openRecommendation)) + '</button>'
|
|
836
|
+
: '') +
|
|
837
|
+
(openRecommendation
|
|
838
|
+
? '<button class="danger-btn dismiss-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Dismiss</button>'
|
|
839
|
+
: '') +
|
|
840
|
+
(!openRecommendation && reopenRecommendation
|
|
841
|
+
? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
|
|
842
|
+
: '') +
|
|
843
|
+
'<button class="action-btn bind-current-btn" data-conversation="' + esc(session.conversation_id || '') + '">Attach to Current Chat</button>' +
|
|
844
|
+
'<button class="secondary-btn mark-read-btn" data-session="' + esc(session.session_key) + '">Mark read</button>' +
|
|
845
|
+
'<button class="secondary-btn copy-session-link-btn" data-session="' + esc(session.session_key) + '">Copy Session Link</button>' +
|
|
846
|
+
'<button class="danger-btn clear-binding-btn" data-conversation="' + esc(session.conversation_id || '') + '">Detach Chat Link</button>' +
|
|
847
|
+
'</div>' +
|
|
848
|
+
'<div class="form-grid" style="margin-top:16px">' +
|
|
849
|
+
'<label class="label">Reply in this session</label>' +
|
|
850
|
+
'<textarea id="sessionReplyInput" placeholder="Send a text reply in this session"></textarea>' +
|
|
851
|
+
'<div class="row-actions"><button class="action-btn" id="sendSessionReplyBtn">Send Reply</button></div>' +
|
|
473
852
|
'</div>' +
|
|
474
853
|
'</div>' +
|
|
475
854
|
'<div>' +
|
|
476
855
|
'<div class="label">Policy Decisions</div>' +
|
|
477
|
-
|
|
856
|
+
policyBlock +
|
|
857
|
+
(isAdvanced && recommendations.length
|
|
858
|
+
? '<pre style="margin-top:12px">recommendations_debug=' + esc(JSON.stringify(recommendations.map(function (item) { return { id: item.id, status: item.status, policy: item.policy, action: item.action, current_action: item.current_action, match: item.match, confidence: item.confidence }; }), null, 2)) + '</pre>'
|
|
859
|
+
: '') +
|
|
860
|
+
(isAdvanced ? '<div class="muted small" style="margin-top:10px">permalink=' + esc(sessionLink) + '</div>' : '') +
|
|
861
|
+
'</div>' +
|
|
862
|
+
'</div>' +
|
|
863
|
+
'<div class="grid two-col" style="margin-top:16px">' +
|
|
864
|
+
'<div>' +
|
|
865
|
+
'<div class="label">Carry-Forward Summary</div>' +
|
|
866
|
+
'<div style="margin-top:8px">' + renderSummaryBlock(summary) + '</div>' +
|
|
867
|
+
'</div>' +
|
|
868
|
+
'<div>' +
|
|
869
|
+
'<div class="label">Update Summary</div>' +
|
|
870
|
+
'<div class="form-grid" style="margin-top:8px">' +
|
|
871
|
+
'<input id="sessionSummaryObjective" placeholder="Objective" value="' + esc(summary && summary.objective ? summary.objective : '') + '">' +
|
|
872
|
+
'<textarea id="sessionSummaryContext" placeholder="Context">' + esc(summary && summary.context ? summary.context : '') + '</textarea>' +
|
|
873
|
+
'<textarea id="sessionSummaryConstraints" placeholder="Constraints">' + esc(summary && summary.constraints ? summary.constraints : '') + '</textarea>' +
|
|
874
|
+
'<textarea id="sessionSummaryDecisions" placeholder="Decisions">' + esc(summary && summary.decisions ? summary.decisions : '') + '</textarea>' +
|
|
875
|
+
'<textarea id="sessionSummaryOpenQuestions" placeholder="Open questions">' + esc(summary && summary.open_questions ? summary.open_questions : '') + '</textarea>' +
|
|
876
|
+
'<textarea id="sessionSummaryNextAction" placeholder="Next action">' + esc(summary && summary.next_action ? summary.next_action : '') + '</textarea>' +
|
|
877
|
+
'<textarea id="sessionSummaryHandoff" placeholder="Handoff-ready summary">' + esc(summary && summary.handoff_ready_text ? summary.handoff_ready_text : '') + '</textarea>' +
|
|
878
|
+
'<div class="row-actions"><button class="action-btn" id="saveSessionSummaryBtn">Save Summary</button></div>' +
|
|
879
|
+
'</div>' +
|
|
478
880
|
'</div>' +
|
|
479
881
|
'</div>' +
|
|
480
882
|
'<div class="grid two-col" style="margin-top:16px">' +
|
|
@@ -484,15 +886,26 @@ function getHostPanelHtml() {
|
|
|
484
886
|
'<div class="muted small">updated=' + esc(fmtTs(taskItem.updated_at)) + '</div>' +
|
|
485
887
|
(taskItem.result_summary ? '<div style="margin-top:8px">' + esc(taskItem.result_summary) + '</div>' : '') +
|
|
486
888
|
(taskItem.error_message ? '<div style="margin-top:8px;color:#fca5a5">' + esc(taskItem.error_message) + '</div>' : '') +
|
|
889
|
+
renderHandoffBlock(taskItem.handoff) +
|
|
487
890
|
'</div>';
|
|
488
891
|
}).join('') : '<div class="empty">No tasks in this session.</div>') +
|
|
489
892
|
'</div></div>' +
|
|
490
893
|
'<div><div class="label">Learned Recommendations</div><div class="recommendation-list" style="margin-top:8px">' +
|
|
491
894
|
(recommendations.length ? recommendations.map(function (item) {
|
|
895
|
+
const actionButton = item.status === 'open'
|
|
896
|
+
? '<button class="action-btn apply-session-recommendation-btn" data-session="' + esc(session.session_key) + '">' + esc(recommendationActionLabel(item)) + '</button>'
|
|
897
|
+
: '';
|
|
898
|
+
const dismissButton = item.status === 'open'
|
|
899
|
+
? '<button class="danger-btn dismiss-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Dismiss</button>'
|
|
900
|
+
: '';
|
|
901
|
+
const reopenButton = (item.status === 'dismissed' || item.status === 'superseded')
|
|
902
|
+
? '<button class="secondary-btn reopen-session-recommendation-btn" data-session="' + esc(session.session_key) + '">Reopen</button>'
|
|
903
|
+
: '';
|
|
492
904
|
return '<div class="recommendation-row"><div class="top"><strong>' + esc(item.policy) + '</strong><span class="badge">' + esc(item.status + ' \xB7 ' + item.action) + '</span></div>' +
|
|
493
|
-
'<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' +
|
|
494
|
-
'<div class="muted small">match=' + esc(item.match) + '</div>' +
|
|
905
|
+
(isAdvanced ? '<div class="muted small">current=' + esc(item.current_action) + ' \xB7 confidence=' + esc(item.confidence) + '</div>' : '') +
|
|
906
|
+
(isAdvanced ? '<div class="muted small">match=' + esc(item.match) + '</div>' : '') +
|
|
495
907
|
'<div style="margin-top:8px">' + esc(item.reason) + '</div>' +
|
|
908
|
+
'<div class="row-actions">' + actionButton + dismissButton + reopenButton + '</div>' +
|
|
496
909
|
'</div>';
|
|
497
910
|
}).join('') : '<div class="empty">No learned recommendation for this session.</div>') +
|
|
498
911
|
'</div></div>' +
|
|
@@ -500,10 +913,10 @@ function getHostPanelHtml() {
|
|
|
500
913
|
'<div class="grid two-col" style="margin-top:16px">' +
|
|
501
914
|
'<div><div class="label">Recent Messages</div><div class="message-list" style="margin-top:8px">' +
|
|
502
915
|
(messages.length ? messages.map(function (msg) {
|
|
503
|
-
const
|
|
916
|
+
const messageSummary = msg.schema === 'pingagent.text@1' && msg.payload && msg.payload.text
|
|
504
917
|
? msg.payload.text
|
|
505
918
|
: JSON.stringify(msg.payload || {});
|
|
506
|
-
return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(
|
|
919
|
+
return '<div class="message-row"><div class="muted small">' + esc(fmtTs(msg.ts_ms)) + ' \xB7 ' + esc(msg.direction) + ' \xB7 ' + esc(msg.schema) + '</div><div style="margin-top:8px">' + esc(messageSummary) + '</div></div>';
|
|
507
920
|
}).join('') : '<div class="empty">No local message history yet.</div>') +
|
|
508
921
|
'</div></div>' +
|
|
509
922
|
'<div><div class="label">Policy Audit</div><div class="audit-list" style="margin-top:8px">' +
|
|
@@ -516,6 +929,24 @@ function getHostPanelHtml() {
|
|
|
516
929
|
'</div></div>' +
|
|
517
930
|
'</div>';
|
|
518
931
|
|
|
932
|
+
el.querySelectorAll('.approve-session-btn').forEach(function (btn) {
|
|
933
|
+
btn.addEventListener('click', async function () {
|
|
934
|
+
const sessionKey = btn.getAttribute('data-session');
|
|
935
|
+
const result = await api('/api/runtime/session/approve', {
|
|
936
|
+
method: 'POST',
|
|
937
|
+
headers: { 'Content-Type': 'application/json' },
|
|
938
|
+
body: JSON.stringify({ session_key: sessionKey }),
|
|
939
|
+
});
|
|
940
|
+
await refreshAll();
|
|
941
|
+
const promoted = result && result.dm_conversation_id
|
|
942
|
+
? getOverviewSessions().find(function (item) { return item.conversation_id === result.dm_conversation_id; })
|
|
943
|
+
: null;
|
|
944
|
+
state.selectedSessionKey = promoted ? promoted.session_key : sessionKey;
|
|
945
|
+
renderOverview();
|
|
946
|
+
if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
|
|
947
|
+
setTab('runtime');
|
|
948
|
+
});
|
|
949
|
+
});
|
|
519
950
|
el.querySelectorAll('.bind-current-btn').forEach(function (btn) {
|
|
520
951
|
btn.addEventListener('click', async function () {
|
|
521
952
|
const conversationId = btn.getAttribute('data-conversation');
|
|
@@ -523,6 +954,22 @@ function getHostPanelHtml() {
|
|
|
523
954
|
await promptBindCurrentChat(conversationId);
|
|
524
955
|
});
|
|
525
956
|
});
|
|
957
|
+
el.querySelectorAll('.mark-read-btn').forEach(function (btn) {
|
|
958
|
+
btn.addEventListener('click', async function () {
|
|
959
|
+
await api('/api/runtime/session/mark-read', {
|
|
960
|
+
method: 'POST',
|
|
961
|
+
headers: { 'Content-Type': 'application/json' },
|
|
962
|
+
body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
|
|
963
|
+
});
|
|
964
|
+
await refreshAll();
|
|
965
|
+
setTab('runtime');
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
el.querySelectorAll('.copy-session-link-btn').forEach(function (btn) {
|
|
969
|
+
btn.addEventListener('click', async function () {
|
|
970
|
+
await copyText(buildSessionLink(btn.getAttribute('data-session')), 'Session link');
|
|
971
|
+
});
|
|
972
|
+
});
|
|
526
973
|
el.querySelectorAll('.clear-binding-btn').forEach(function (btn) {
|
|
527
974
|
btn.addEventListener('click', async function () {
|
|
528
975
|
await api('/api/runtime/session-bindings/clear', {
|
|
@@ -534,6 +981,79 @@ function getHostPanelHtml() {
|
|
|
534
981
|
setTab('runtime');
|
|
535
982
|
});
|
|
536
983
|
});
|
|
984
|
+
el.querySelectorAll('.apply-session-recommendation-btn').forEach(function (btn) {
|
|
985
|
+
btn.addEventListener('click', async function () {
|
|
986
|
+
await api('/api/runtime/policy/recommendations/apply-session', {
|
|
987
|
+
method: 'POST',
|
|
988
|
+
headers: { 'Content-Type': 'application/json' },
|
|
989
|
+
body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
|
|
990
|
+
});
|
|
991
|
+
await refreshAll();
|
|
992
|
+
setTab('runtime');
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
el.querySelectorAll('.dismiss-session-recommendation-btn').forEach(function (btn) {
|
|
996
|
+
btn.addEventListener('click', async function () {
|
|
997
|
+
await api('/api/runtime/policy/recommendations/dismiss-session', {
|
|
998
|
+
method: 'POST',
|
|
999
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1000
|
+
body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
|
|
1001
|
+
});
|
|
1002
|
+
await refreshAll();
|
|
1003
|
+
setTab('runtime');
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
el.querySelectorAll('.reopen-session-recommendation-btn').forEach(function (btn) {
|
|
1007
|
+
btn.addEventListener('click', async function () {
|
|
1008
|
+
await api('/api/runtime/policy/recommendations/reopen-session', {
|
|
1009
|
+
method: 'POST',
|
|
1010
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1011
|
+
body: JSON.stringify({ session_key: btn.getAttribute('data-session') }),
|
|
1012
|
+
});
|
|
1013
|
+
await refreshAll();
|
|
1014
|
+
setTab('runtime');
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
const sendSessionReplyBtn = document.getElementById('sendSessionReplyBtn');
|
|
1018
|
+
if (sendSessionReplyBtn) {
|
|
1019
|
+
sendSessionReplyBtn.addEventListener('click', async function () {
|
|
1020
|
+
const input = document.getElementById('sessionReplyInput');
|
|
1021
|
+
const message = input.value.trim();
|
|
1022
|
+
if (!message) {
|
|
1023
|
+
window.alert('Reply text is required.');
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
await api('/api/runtime/session/reply', {
|
|
1027
|
+
method: 'POST',
|
|
1028
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1029
|
+
body: JSON.stringify({ session_key: session.session_key, message: message }),
|
|
1030
|
+
});
|
|
1031
|
+
input.value = '';
|
|
1032
|
+
await refreshAll();
|
|
1033
|
+
setTab('runtime');
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
const saveSessionSummaryBtn = document.getElementById('saveSessionSummaryBtn');
|
|
1037
|
+
if (saveSessionSummaryBtn) {
|
|
1038
|
+
saveSessionSummaryBtn.addEventListener('click', async function () {
|
|
1039
|
+
await api('/api/runtime/session-summary', {
|
|
1040
|
+
method: 'POST',
|
|
1041
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1042
|
+
body: JSON.stringify({
|
|
1043
|
+
session_key: session.session_key,
|
|
1044
|
+
objective: document.getElementById('sessionSummaryObjective').value.trim(),
|
|
1045
|
+
context: document.getElementById('sessionSummaryContext').value.trim(),
|
|
1046
|
+
constraints: document.getElementById('sessionSummaryConstraints').value.trim(),
|
|
1047
|
+
decisions: document.getElementById('sessionSummaryDecisions').value.trim(),
|
|
1048
|
+
open_questions: document.getElementById('sessionSummaryOpenQuestions').value.trim(),
|
|
1049
|
+
next_action: document.getElementById('sessionSummaryNextAction').value.trim(),
|
|
1050
|
+
handoff_ready_text: document.getElementById('sessionSummaryHandoff').value.trim(),
|
|
1051
|
+
}),
|
|
1052
|
+
});
|
|
1053
|
+
await refreshAll();
|
|
1054
|
+
setTab('runtime');
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
537
1057
|
}
|
|
538
1058
|
|
|
539
1059
|
async function promptBindCurrentChat(conversationId, previousBinding, remoteDid) {
|
|
@@ -543,7 +1063,7 @@ function getHostPanelHtml() {
|
|
|
543
1063
|
const previous = previousBinding || (state.session && state.session.binding ? state.session.binding.session_key : null) || '(unbound)';
|
|
544
1064
|
const targetRemoteDid = remoteDid || (state.session && state.session.session ? state.session.session.remote_did : null) || '(unknown)';
|
|
545
1065
|
const confirmed = window.confirm(
|
|
546
|
-
'
|
|
1066
|
+
'Attach this PingAgent session to the current OpenClaw chat?' +
|
|
547
1067
|
'
|
|
548
1068
|
|
|
549
1069
|
Conversation: ' + conversationId +
|
|
@@ -551,9 +1071,9 @@ Conversation: ' + conversationId +
|
|
|
551
1071
|
Remote DID: ' + targetRemoteDid +
|
|
552
1072
|
'
|
|
553
1073
|
|
|
554
|
-
Current chat: ' + (current || '(none)') +
|
|
1074
|
+
Current OpenClaw chat: ' + (current || '(none)') +
|
|
555
1075
|
'
|
|
556
|
-
Previous
|
|
1076
|
+
Previous chat link: ' + previous
|
|
557
1077
|
);
|
|
558
1078
|
if (!confirmed) return;
|
|
559
1079
|
await api('/api/runtime/session-bindings/bind-current', {
|
|
@@ -568,8 +1088,23 @@ Previous binding: ' + previous
|
|
|
568
1088
|
function renderPolicy() {
|
|
569
1089
|
const policy = state.policy;
|
|
570
1090
|
if (!policy) return;
|
|
1091
|
+
const profile = state.overview && state.overview.profile ? state.overview.profile : null;
|
|
571
1092
|
document.getElementById('contactDefault').value = policy.doc.contact_policy.default_action;
|
|
572
1093
|
document.getElementById('taskDefault').value = policy.doc.task_policy.default_action;
|
|
1094
|
+
document.getElementById('profileDisplayName').value = profile && profile.display_name ? profile.display_name : '';
|
|
1095
|
+
document.getElementById('profileBio').value = profile && profile.bio ? profile.bio : '';
|
|
1096
|
+
document.getElementById('profileTags').value = profile && Array.isArray(profile.tags) ? profile.tags.join(', ') : '';
|
|
1097
|
+
document.getElementById('profileCapabilities').value = profile && Array.isArray(profile.capabilities) ? profile.capabilities.join(', ') : '';
|
|
1098
|
+
document.getElementById('capabilityCardSummary').value = profile && profile.capability_card && profile.capability_card.summary ? profile.capability_card.summary : '';
|
|
1099
|
+
document.getElementById('capabilityCardAcceptsNewWork').value =
|
|
1100
|
+
profile && profile.capability_card && typeof profile.capability_card.accepts_new_work === 'boolean'
|
|
1101
|
+
? String(profile.capability_card.accepts_new_work)
|
|
1102
|
+
: '';
|
|
1103
|
+
document.getElementById('capabilityCardContactMode').value =
|
|
1104
|
+
profile && profile.capability_card && profile.capability_card.preferred_contact_mode
|
|
1105
|
+
? profile.capability_card.preferred_contact_mode
|
|
1106
|
+
: '';
|
|
1107
|
+
document.getElementById('capabilityCardItems').value = formatCapabilityCardEditor(profile && profile.capability_card ? profile.capability_card : null);
|
|
573
1108
|
|
|
574
1109
|
const rules = [];
|
|
575
1110
|
policy.doc.contact_policy.rules.forEach(function (rule) {
|
|
@@ -610,7 +1145,7 @@ Previous binding: ' + previous
|
|
|
610
1145
|
if (!list.length) return '';
|
|
611
1146
|
return '<div><div class="label" style="margin-bottom:8px">' + esc(status) + '</div>' + list.map(function (item) {
|
|
612
1147
|
const applyButton = status !== 'applied'
|
|
613
|
-
? '<button class="action-btn apply-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">
|
|
1148
|
+
? '<button class="action-btn apply-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">' + esc(recommendationActionLabel(item)) + '</button>'
|
|
614
1149
|
: '';
|
|
615
1150
|
const dismissButton = status === 'open'
|
|
616
1151
|
? '<button class="danger-btn dismiss-recommendation-btn" data-recommendation-id="' + esc(item.id) + '">Dismiss</button>'
|
|
@@ -672,6 +1207,45 @@ Previous binding: ' + previous
|
|
|
672
1207
|
: '<div class="empty">No audit events yet.</div>';
|
|
673
1208
|
|
|
674
1209
|
updateRuleActionOptions();
|
|
1210
|
+
|
|
1211
|
+
const saveProfileBtn = document.getElementById('saveProfileBtn');
|
|
1212
|
+
if (saveProfileBtn) {
|
|
1213
|
+
saveProfileBtn.onclick = async function () {
|
|
1214
|
+
let capabilityItems = [];
|
|
1215
|
+
const rawItems = document.getElementById('capabilityCardItems').value.trim();
|
|
1216
|
+
if (rawItems) {
|
|
1217
|
+
try {
|
|
1218
|
+
const parsed = JSON.parse(rawItems);
|
|
1219
|
+
if (!Array.isArray(parsed)) throw new Error('Capability entries JSON must be an array.');
|
|
1220
|
+
capabilityItems = parsed;
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
window.alert(err && err.message ? err.message : 'Invalid capability entries JSON');
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const acceptsNewWorkValue = document.getElementById('capabilityCardAcceptsNewWork').value;
|
|
1227
|
+
const capabilityCard = {
|
|
1228
|
+
version: '1',
|
|
1229
|
+
summary: document.getElementById('capabilityCardSummary').value.trim() || undefined,
|
|
1230
|
+
accepts_new_work: acceptsNewWorkValue === '' ? undefined : acceptsNewWorkValue === 'true',
|
|
1231
|
+
preferred_contact_mode: document.getElementById('capabilityCardContactMode').value || undefined,
|
|
1232
|
+
capabilities: capabilityItems,
|
|
1233
|
+
};
|
|
1234
|
+
await api('/api/profile', {
|
|
1235
|
+
method: 'POST',
|
|
1236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1237
|
+
body: JSON.stringify({
|
|
1238
|
+
display_name: document.getElementById('profileDisplayName').value.trim() || undefined,
|
|
1239
|
+
bio: document.getElementById('profileBio').value.trim() || undefined,
|
|
1240
|
+
tags: parseCsvList(document.getElementById('profileTags').value),
|
|
1241
|
+
capabilities: parseCsvList(document.getElementById('profileCapabilities').value),
|
|
1242
|
+
capability_card: capabilityCard,
|
|
1243
|
+
}),
|
|
1244
|
+
});
|
|
1245
|
+
await refreshAll();
|
|
1246
|
+
setTab('policy');
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
675
1249
|
}
|
|
676
1250
|
|
|
677
1251
|
function updateRuleActionOptions() {
|
|
@@ -683,14 +1257,15 @@ Previous binding: ' + previous
|
|
|
683
1257
|
|
|
684
1258
|
async function loadOverview() {
|
|
685
1259
|
state.overview = await api('/api/runtime/overview');
|
|
1260
|
+
syncSelectedSessionFromOverview();
|
|
686
1261
|
renderHeader();
|
|
687
1262
|
renderOverview();
|
|
688
|
-
const sessions = state.overview && Array.isArray(state.overview.sessions) ? state.overview.sessions : [];
|
|
689
|
-
if (!state.selectedSessionKey && sessions.length) {
|
|
690
|
-
state.selectedSessionKey = sessions[0].session_key;
|
|
691
|
-
}
|
|
692
1263
|
if (state.selectedSessionKey) {
|
|
693
1264
|
await loadSession(state.selectedSessionKey);
|
|
1265
|
+
} else {
|
|
1266
|
+
state.session = null;
|
|
1267
|
+
renderSession();
|
|
1268
|
+
syncUrlState();
|
|
694
1269
|
}
|
|
695
1270
|
}
|
|
696
1271
|
|
|
@@ -698,6 +1273,7 @@ Previous binding: ' + previous
|
|
|
698
1273
|
if (!sessionKey) return;
|
|
699
1274
|
state.selectedSessionKey = sessionKey;
|
|
700
1275
|
state.session = await api('/api/runtime/session?session_key=' + encodeURIComponent(sessionKey));
|
|
1276
|
+
syncUrlState();
|
|
701
1277
|
renderSession();
|
|
702
1278
|
}
|
|
703
1279
|
|
|
@@ -718,6 +1294,30 @@ Previous binding: ' + previous
|
|
|
718
1294
|
|
|
719
1295
|
document.getElementById('navRuntime').addEventListener('click', function () { setTab('runtime'); });
|
|
720
1296
|
document.getElementById('navPolicy').addEventListener('click', function () { setTab('policy'); });
|
|
1297
|
+
document.getElementById('toggleUnreadBtn').addEventListener('click', async function () {
|
|
1298
|
+
state.showUnreadOnly = !state.showUnreadOnly;
|
|
1299
|
+
syncSelectedSessionFromOverview();
|
|
1300
|
+
renderOverview();
|
|
1301
|
+
if (state.selectedSessionKey) await loadSession(state.selectedSessionKey);
|
|
1302
|
+
else {
|
|
1303
|
+
syncUrlState();
|
|
1304
|
+
renderSession();
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
document.getElementById('nextUnreadBtn').addEventListener('click', async function () {
|
|
1308
|
+
const unreadSessions = getOverviewSessions().filter(function (session) { return Number(session.unread_count || 0) > 0; });
|
|
1309
|
+
if (!unreadSessions.length) {
|
|
1310
|
+
window.alert('No unread sessions.');
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const currentIndex = unreadSessions.findIndex(function (session) { return session.session_key === state.selectedSessionKey; });
|
|
1314
|
+
const next = unreadSessions[(currentIndex + 1 + unreadSessions.length) % unreadSessions.length];
|
|
1315
|
+
state.selectedSessionKey = next.session_key;
|
|
1316
|
+
renderOverview();
|
|
1317
|
+
await loadSession(state.selectedSessionKey);
|
|
1318
|
+
});
|
|
1319
|
+
document.getElementById('detailModeBasicBtn').addEventListener('click', function () { setDetailMode('basic'); });
|
|
1320
|
+
document.getElementById('detailModeAdvancedBtn').addEventListener('click', function () { setDetailMode('advanced'); });
|
|
721
1321
|
document.getElementById('rulePolicy').addEventListener('change', updateRuleActionOptions);
|
|
722
1322
|
document.getElementById('saveDefaultsBtn').addEventListener('click', async function () {
|
|
723
1323
|
await api('/api/runtime/policy/defaults', {
|
|
@@ -758,11 +1358,54 @@ Previous binding: ' + previous
|
|
|
758
1358
|
document.getElementById('simulateOutput').textContent = JSON.stringify(result, null, 2);
|
|
759
1359
|
setTab('policy');
|
|
760
1360
|
});
|
|
1361
|
+
document.getElementById('fixHooksBtn').addEventListener('click', async function () {
|
|
1362
|
+
const confirmed = window.confirm('Repair OpenClaw hooks config now? A timestamped backup of openclaw.json will be written first.');
|
|
1363
|
+
if (!confirmed) return;
|
|
1364
|
+
const result = await api('/api/runtime/openclaw/fix-hooks', { method: 'POST' });
|
|
1365
|
+
window.alert((result.ok ? 'Hooks repaired.
|
|
1366
|
+
|
|
1367
|
+
' : 'Hooks repair reported an error.
|
|
1368
|
+
|
|
1369
|
+
') + (result.stdout || result.stderr || 'No output'));
|
|
1370
|
+
await refreshAll();
|
|
1371
|
+
});
|
|
1372
|
+
document.getElementById('publicLinkBtn').addEventListener('click', async function () {
|
|
1373
|
+
const suggested = state.overview && state.overview.publicSelf && state.overview.publicSelf.public_slug
|
|
1374
|
+
? state.overview.publicSelf.public_slug
|
|
1375
|
+
: '';
|
|
1376
|
+
const slug = window.prompt('Public share slug (leave empty to use the recommended value):', suggested);
|
|
1377
|
+
if (slug == null) return;
|
|
1378
|
+
const result = await api('/api/public/link', {
|
|
1379
|
+
method: 'POST',
|
|
1380
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1381
|
+
body: JSON.stringify({ slug: slug.trim() || undefined, enabled: true }),
|
|
1382
|
+
});
|
|
1383
|
+
window.alert('Public link ready:
|
|
1384
|
+
' + (result.public_url || '(missing public_url)'));
|
|
1385
|
+
await refreshAll();
|
|
1386
|
+
});
|
|
1387
|
+
document.getElementById('contactCardBtn').addEventListener('click', async function () {
|
|
1388
|
+
const intro = window.prompt('Optional intro note for this contact card:', '');
|
|
1389
|
+
if (intro == null) return;
|
|
1390
|
+
const result = await api('/api/public/contact-card', {
|
|
1391
|
+
method: 'POST',
|
|
1392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1393
|
+
body: JSON.stringify({
|
|
1394
|
+
target_did: state.overview && state.overview.did ? state.overview.did : undefined,
|
|
1395
|
+
intro_note: intro.trim() || undefined,
|
|
1396
|
+
}),
|
|
1397
|
+
});
|
|
1398
|
+
window.alert('Contact card ready:
|
|
1399
|
+
' + (result.share_url || '(missing share_url)'));
|
|
1400
|
+
});
|
|
761
1401
|
|
|
762
1402
|
async function init() {
|
|
1403
|
+
setTab(state.currentTab);
|
|
1404
|
+
setDetailMode(state.detailMode);
|
|
763
1405
|
await loadProfiles();
|
|
764
1406
|
updateRuleActionOptions();
|
|
765
1407
|
await refreshAll();
|
|
1408
|
+
syncUrlState();
|
|
766
1409
|
}
|
|
767
1410
|
|
|
768
1411
|
init().catch(function (error) {
|
|
@@ -781,6 +1424,29 @@ function resolvePath(p) {
|
|
|
781
1424
|
if (!p || !p.startsWith("~")) return p;
|
|
782
1425
|
return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(1));
|
|
783
1426
|
}
|
|
1427
|
+
function findOpenClawInstallScript() {
|
|
1428
|
+
const explicit = process.env.PINGAGENT_OPENCLAW_INSTALL_BIN?.trim();
|
|
1429
|
+
if (explicit) return { kind: "script", cmd: process.execPath, args: [path.resolve(explicit)] };
|
|
1430
|
+
const repoScript = path.resolve(process.cwd(), "packages", "openclaw-install", "install.mjs");
|
|
1431
|
+
if (fs.existsSync(repoScript)) return { kind: "script", cmd: process.execPath, args: [repoScript] };
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
function runOpenClawInstall(args) {
|
|
1435
|
+
const resolved = findOpenClawInstallScript();
|
|
1436
|
+
if (!resolved) {
|
|
1437
|
+
return { ok: false, stdout: "", stderr: "OpenClaw installer script not found locally. Set PINGAGENT_OPENCLAW_INSTALL_BIN." };
|
|
1438
|
+
}
|
|
1439
|
+
const result = spawnSync(resolved.cmd, [...resolved.args, ...args], {
|
|
1440
|
+
encoding: "utf-8",
|
|
1441
|
+
env: process.env
|
|
1442
|
+
});
|
|
1443
|
+
return {
|
|
1444
|
+
ok: result.status === 0,
|
|
1445
|
+
stdout: String(result.stdout ?? ""),
|
|
1446
|
+
stderr: String(result.stderr ?? ""),
|
|
1447
|
+
status: result.status ?? 1
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
784
1450
|
function listProfiles(rootDir) {
|
|
785
1451
|
const root = resolvePath(rootDir);
|
|
786
1452
|
const profiles = [];
|
|
@@ -839,6 +1505,30 @@ function listProfiles(rootDir) {
|
|
|
839
1505
|
return profiles;
|
|
840
1506
|
}
|
|
841
1507
|
var DEFAULT_SERVER_URL = "https://pingagent.chat";
|
|
1508
|
+
var OFFICIAL_HOSTED_ORIGIN = new URL(DEFAULT_SERVER_URL).origin;
|
|
1509
|
+
var autoPublicLinkAttempts = /* @__PURE__ */ new Set();
|
|
1510
|
+
function normalizeOrigin(input) {
|
|
1511
|
+
try {
|
|
1512
|
+
return new URL(String(input ?? "")).origin;
|
|
1513
|
+
} catch {
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function isOfficialHostedServer(serverUrl) {
|
|
1518
|
+
return normalizeOrigin(serverUrl) === OFFICIAL_HOSTED_ORIGIN;
|
|
1519
|
+
}
|
|
1520
|
+
async function maybeEnsureHostedPublicLink(ctx) {
|
|
1521
|
+
if (!isOfficialHostedServer(ctx.serverUrl)) return;
|
|
1522
|
+
const key = `${ctx.identityPath}:${normalizeOrigin(ctx.serverUrl)}`;
|
|
1523
|
+
if (autoPublicLinkAttempts.has(key)) return;
|
|
1524
|
+
autoPublicLinkAttempts.add(key);
|
|
1525
|
+
try {
|
|
1526
|
+
const current = await ctx.client.getPublicSelf().catch(() => ({ ok: false }));
|
|
1527
|
+
if (current.ok && current.data?.public_slug) return;
|
|
1528
|
+
await ctx.client.createPublicLink({ enabled: true }).catch(() => ({ ok: false }));
|
|
1529
|
+
} catch {
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
842
1532
|
async function getContextForProfile(profile, defaultServerUrl) {
|
|
843
1533
|
const identity = loadIdentity(profile.identityPath);
|
|
844
1534
|
const serverUrl = identity.serverUrl ?? defaultServerUrl ?? DEFAULT_SERVER_URL;
|
|
@@ -1044,11 +1734,14 @@ function describeHostedTier(tier) {
|
|
|
1044
1734
|
}
|
|
1045
1735
|
async function buildRuntimeOverviewPayload(ctx) {
|
|
1046
1736
|
const client = ctx.client;
|
|
1737
|
+
await maybeEnsureHostedPublicLink(ctx);
|
|
1047
1738
|
await client.listConversations({ type: "dm" });
|
|
1048
1739
|
const sessionManager = client.getSessionManager();
|
|
1740
|
+
const sessionSummaryManager = client.getSessionSummaryManager();
|
|
1049
1741
|
const taskManager = client.getTaskThreadManager();
|
|
1742
|
+
const taskHandoffManager = client.getTaskHandoffManager();
|
|
1050
1743
|
const historyManager = client.getHistoryManager();
|
|
1051
|
-
if (!sessionManager || !taskManager || !historyManager) {
|
|
1744
|
+
if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager) {
|
|
1052
1745
|
throw new Error("Runtime overview requires a writable local store");
|
|
1053
1746
|
}
|
|
1054
1747
|
const sessions = sessionManager.listRecentSessions(24);
|
|
@@ -1071,6 +1764,10 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1071
1764
|
const runtimeMode = getRuntimeMode();
|
|
1072
1765
|
const subRes = await client.getSubscription().catch(() => ({ ok: false }));
|
|
1073
1766
|
const subscription = subRes.ok && subRes.data ? subRes.data : null;
|
|
1767
|
+
const publicSelfRes = await client.getPublicSelf().catch(() => ({ ok: false }));
|
|
1768
|
+
const publicSelf = publicSelfRes.ok && publicSelfRes.data ? publicSelfRes.data : null;
|
|
1769
|
+
const profileRes = await client.getProfile().catch(() => ({ ok: false }));
|
|
1770
|
+
const profile = profileRes.ok && profileRes.data ? profileRes.data : null;
|
|
1074
1771
|
const recommendationState = syncTrustRecommendations(ctx.storePath, {
|
|
1075
1772
|
policyDoc: policy,
|
|
1076
1773
|
sessions,
|
|
@@ -1082,6 +1779,7 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1082
1779
|
const unreadTotal = sessions.reduce((sum, session) => sum + session.unread_count, 0);
|
|
1083
1780
|
const sessionBindings = readSessionBindings();
|
|
1084
1781
|
const sessionBindingAlerts = readSessionBindingAlerts();
|
|
1782
|
+
const ingressRuntime = readIngressRuntimeStatus();
|
|
1085
1783
|
const activeWorkSession = readCurrentActiveSessionKey();
|
|
1086
1784
|
const bindingByConversation = new Map(sessionBindings.map((row) => [row.conversation_id, row.session_key]));
|
|
1087
1785
|
const bindingAlertByConversation = new Map(sessionBindingAlerts.map((row) => [row.conversation_id, row]));
|
|
@@ -1096,6 +1794,7 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1096
1794
|
trustPolicyPath: getTrustPolicyPath(ctx.identityPath),
|
|
1097
1795
|
activeWorkSessionFile: getActiveSessionFilePath(),
|
|
1098
1796
|
activeWorkSession,
|
|
1797
|
+
ingressRuntime,
|
|
1099
1798
|
sessionMapPath: getSessionMapFilePath(),
|
|
1100
1799
|
sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
|
|
1101
1800
|
sessionBindings,
|
|
@@ -1111,6 +1810,8 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1111
1810
|
retention_label: formatRetentionLabel(subscription.limits.store_forward_ttl_ms),
|
|
1112
1811
|
audit_export_allowed: !!subscription.limits.audit_export_allowed
|
|
1113
1812
|
} : null,
|
|
1813
|
+
publicSelf,
|
|
1814
|
+
profile,
|
|
1114
1815
|
policyDefaults: {
|
|
1115
1816
|
contact: policy.contact_policy.enabled ? policy.contact_policy.default_action : "disabled",
|
|
1116
1817
|
task: policy.task_policy.enabled ? policy.task_policy.default_action : "disabled"
|
|
@@ -1122,22 +1823,31 @@ async function buildRuntimeOverviewPayload(ctx) {
|
|
|
1122
1823
|
recommendationSummary: recommendationState.summary,
|
|
1123
1824
|
sessions: sessions.map((session) => ({
|
|
1124
1825
|
...session,
|
|
1826
|
+
session_summary: sessionSummaryManager.get(session.session_key),
|
|
1125
1827
|
mapped_work_session: session.conversation_id ? bindingByConversation.get(session.conversation_id) ?? null : null,
|
|
1126
1828
|
binding_alert: session.conversation_id ? bindingAlertByConversation.get(session.conversation_id) ?? null : null,
|
|
1127
1829
|
is_active_work_session: session.session_key === activeWorkSession,
|
|
1128
1830
|
latest_messages: session.conversation_id ? historyManager.listRecent(session.conversation_id, 3) : []
|
|
1129
1831
|
})),
|
|
1130
|
-
tasks: refreshedTasks
|
|
1832
|
+
tasks: refreshedTasks.map((task) => ({
|
|
1833
|
+
...task,
|
|
1834
|
+
handoff: taskHandoffManager.get(task.task_id)
|
|
1835
|
+
})),
|
|
1131
1836
|
auditSummary,
|
|
1132
|
-
recommendations: recommendationState.recommendations
|
|
1837
|
+
recommendations: recommendationState.recommendations.map((item) => ({
|
|
1838
|
+
...item,
|
|
1839
|
+
primary_action_label: getTrustRecommendationActionLabel(item)
|
|
1840
|
+
}))
|
|
1133
1841
|
};
|
|
1134
1842
|
}
|
|
1135
1843
|
async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
1136
1844
|
const client = ctx.client;
|
|
1137
1845
|
const sessionManager = client.getSessionManager();
|
|
1846
|
+
const sessionSummaryManager = client.getSessionSummaryManager();
|
|
1138
1847
|
const taskManager = client.getTaskThreadManager();
|
|
1848
|
+
const taskHandoffManager = client.getTaskHandoffManager();
|
|
1139
1849
|
const historyManager = client.getHistoryManager();
|
|
1140
|
-
if (!sessionManager || !taskManager || !historyManager) {
|
|
1850
|
+
if (!sessionManager || !sessionSummaryManager || !taskManager || !taskHandoffManager || !historyManager) {
|
|
1141
1851
|
throw new Error("Session overview requires a writable local store");
|
|
1142
1852
|
}
|
|
1143
1853
|
const session = sessionKey ? sessionManager.get(sessionKey) : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
|
|
@@ -1173,6 +1883,8 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
1173
1883
|
});
|
|
1174
1884
|
return {
|
|
1175
1885
|
session,
|
|
1886
|
+
sessionSummary: sessionSummaryManager.get(session.session_key),
|
|
1887
|
+
ingressRuntime: readIngressRuntimeStatus(),
|
|
1176
1888
|
binding,
|
|
1177
1889
|
bindingAlert,
|
|
1178
1890
|
activeWorkSession,
|
|
@@ -1180,12 +1892,30 @@ async function buildSessionOverviewPayload(ctx, sessionKey) {
|
|
|
1180
1892
|
sessionMapPath: getSessionMapFilePath(),
|
|
1181
1893
|
sessionBindingAlertsPath: getSessionBindingAlertsFilePath(),
|
|
1182
1894
|
policyExplain: buildPolicyDecisionShape(ctx.identityPath, session.remote_did, { runtimeMode: getRuntimeMode() }),
|
|
1183
|
-
tasks
|
|
1895
|
+
tasks: tasks.map((task) => ({
|
|
1896
|
+
...task,
|
|
1897
|
+
handoff: taskHandoffManager.get(task.task_id)
|
|
1898
|
+
})),
|
|
1184
1899
|
messages,
|
|
1185
1900
|
auditEvents,
|
|
1186
|
-
recommendations: recommendationState.recommendations.filter((item) => item.remote_did === session.remote_did)
|
|
1901
|
+
recommendations: recommendationState.recommendations.filter((item) => item.remote_did === session.remote_did).map((item) => ({
|
|
1902
|
+
...item,
|
|
1903
|
+
primary_action_label: getTrustRecommendationActionLabel(item)
|
|
1904
|
+
}))
|
|
1187
1905
|
};
|
|
1188
1906
|
}
|
|
1907
|
+
function resolveSessionForInput(sessionManager, input) {
|
|
1908
|
+
if (!sessionManager) return null;
|
|
1909
|
+
const sessionKey = String(input.session_key ?? "").trim();
|
|
1910
|
+
const conversationId = String(input.conversation_id ?? "").trim();
|
|
1911
|
+
const remoteDid = String(input.remote_did ?? "").trim();
|
|
1912
|
+
let session = sessionKey ? sessionManager.get(sessionKey) : null;
|
|
1913
|
+
if (!session && conversationId) session = sessionManager.getByConversationId(conversationId);
|
|
1914
|
+
if (!session && remoteDid) {
|
|
1915
|
+
session = sessionManager.listRecentSessions(100).find((item) => item.remote_did === remoteDid) ?? null;
|
|
1916
|
+
}
|
|
1917
|
+
return session ?? sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
|
|
1918
|
+
}
|
|
1189
1919
|
async function handleApi(pathname, req, ctx) {
|
|
1190
1920
|
const client = ctx.client;
|
|
1191
1921
|
const contactManager = ctx.contactManager;
|
|
@@ -1196,10 +1926,139 @@ async function handleApi(pathname, req, ctx) {
|
|
|
1196
1926
|
return { did: myDid, serverUrl };
|
|
1197
1927
|
}
|
|
1198
1928
|
if (parts[0] === "runtime") {
|
|
1929
|
+
if (parts[1] === "session-summary") {
|
|
1930
|
+
const sessionManager = client.getSessionManager();
|
|
1931
|
+
const sessionSummaryManager = client.getSessionSummaryManager();
|
|
1932
|
+
if (!sessionManager || !sessionSummaryManager) throw new Error("Session summary requires a writable local store");
|
|
1933
|
+
const url = new URL(req.url || "", "http://x");
|
|
1934
|
+
const body = req.method === "POST" ? await readBody(req) : null;
|
|
1935
|
+
const sessionKey = String(
|
|
1936
|
+
body?.session_key ?? url.searchParams.get("session_key") ?? ""
|
|
1937
|
+
).trim();
|
|
1938
|
+
const conversationId = String(
|
|
1939
|
+
body?.conversation_id ?? url.searchParams.get("conversation_id") ?? ""
|
|
1940
|
+
).trim();
|
|
1941
|
+
const remoteDid = String(
|
|
1942
|
+
body?.remote_did ?? url.searchParams.get("remote_did") ?? ""
|
|
1943
|
+
).trim();
|
|
1944
|
+
const session = sessionKey ? sessionManager.get(sessionKey) : conversationId ? sessionManager.getByConversationId(conversationId) : remoteDid ? sessionManager.listRecentSessions(100).find((item) => item.remote_did === remoteDid) ?? null : sessionManager.getActiveSession() ?? sessionManager.listRecentSessions(1)[0] ?? null;
|
|
1945
|
+
if (!session) throw new Error("No session available");
|
|
1946
|
+
if (req.method === "GET") {
|
|
1947
|
+
return { session, summary: sessionSummaryManager.get(session.session_key) };
|
|
1948
|
+
}
|
|
1949
|
+
const summary = sessionSummaryManager.upsert({
|
|
1950
|
+
session_key: session.session_key,
|
|
1951
|
+
objective: typeof body?.objective === "string" ? body.objective : void 0,
|
|
1952
|
+
context: typeof body?.context === "string" ? body.context : void 0,
|
|
1953
|
+
constraints: typeof body?.constraints === "string" ? body.constraints : void 0,
|
|
1954
|
+
decisions: typeof body?.decisions === "string" ? body.decisions : void 0,
|
|
1955
|
+
open_questions: typeof body?.open_questions === "string" ? body.open_questions : void 0,
|
|
1956
|
+
next_action: typeof body?.next_action === "string" ? body.next_action : void 0,
|
|
1957
|
+
handoff_ready_text: typeof body?.handoff_ready_text === "string" ? body.handoff_ready_text : void 0
|
|
1958
|
+
});
|
|
1959
|
+
return { ok: true, session, summary };
|
|
1960
|
+
}
|
|
1199
1961
|
if (!parts[1] || parts[1] === "overview") {
|
|
1200
1962
|
return buildRuntimeOverviewPayload(ctx);
|
|
1201
1963
|
}
|
|
1964
|
+
if (parts[1] === "receive-mode" && req.method === "GET") {
|
|
1965
|
+
return {
|
|
1966
|
+
runtimeStatusPath: path.resolve(process.env.IM_INGRESS_RUNTIME_STATUS_FILE || ""),
|
|
1967
|
+
status: readIngressRuntimeStatus()
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
if (parts[1] === "demo" && req.method === "POST") {
|
|
1971
|
+
const body = await readBody(req);
|
|
1972
|
+
const preset = typeof body?.preset === "string" ? body.preset.trim().toLowerCase() : "";
|
|
1973
|
+
const presetMessages = {
|
|
1974
|
+
hello: "Hello",
|
|
1975
|
+
delegate: "Please show me how task delegation works in PingAgent.",
|
|
1976
|
+
trust: "Show me how trust decisions and recommendations work."
|
|
1977
|
+
};
|
|
1978
|
+
const resolved = await client.resolveAlias("pingagent/demo");
|
|
1979
|
+
if (!resolved.ok || !resolved.data?.did) {
|
|
1980
|
+
throw new Error(resolved.error?.message ?? "Failed to resolve demo agent");
|
|
1981
|
+
}
|
|
1982
|
+
const convo = await client.openConversation(resolved.data.did);
|
|
1983
|
+
if (!convo.ok || !convo.data?.conversation_id) {
|
|
1984
|
+
throw new Error(convo.error?.message ?? "Failed to open demo conversation");
|
|
1985
|
+
}
|
|
1986
|
+
const message = typeof body?.message === "string" && body.message.trim() ? body.message.trim() : presetMessages[preset] ?? "";
|
|
1987
|
+
if (!message) {
|
|
1988
|
+
return {
|
|
1989
|
+
ok: true,
|
|
1990
|
+
did: resolved.data.did,
|
|
1991
|
+
conversation_id: convo.data.conversation_id,
|
|
1992
|
+
preset: preset || null,
|
|
1993
|
+
sent: false
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const sendRes = await client.sendMessage(convo.data.conversation_id, SCHEMA_TEXT, { text: message });
|
|
1997
|
+
if (!sendRes.ok) {
|
|
1998
|
+
throw new Error(sendRes.error?.message ?? "Failed to send demo message");
|
|
1999
|
+
}
|
|
2000
|
+
return {
|
|
2001
|
+
ok: true,
|
|
2002
|
+
did: resolved.data.did,
|
|
2003
|
+
conversation_id: convo.data.conversation_id,
|
|
2004
|
+
preset: preset || null,
|
|
2005
|
+
sent: true,
|
|
2006
|
+
message_id: sendRes.data?.message_id ?? null
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
if (parts[1] === "openclaw" && parts[2] === "fix-hooks" && req.method === "POST") {
|
|
2010
|
+
const result = runOpenClawInstall(["fix-hooks"]);
|
|
2011
|
+
return {
|
|
2012
|
+
ok: result.ok,
|
|
2013
|
+
stdout: result.stdout,
|
|
2014
|
+
stderr: result.stderr,
|
|
2015
|
+
status: result.status,
|
|
2016
|
+
receiveMode: readIngressRuntimeStatus()
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
1202
2019
|
if (parts[1] === "session") {
|
|
2020
|
+
const sessionManager = ctx.client.getSessionManager();
|
|
2021
|
+
if (!sessionManager) throw new Error("Session actions require a writable local store");
|
|
2022
|
+
if (parts[2] === "reply" && req.method === "POST") {
|
|
2023
|
+
const body = await readBody(req);
|
|
2024
|
+
const session = resolveSessionForInput(sessionManager, body);
|
|
2025
|
+
if (!session?.conversation_id) throw new Error("No session selected");
|
|
2026
|
+
const text = String(body?.message ?? "").trim();
|
|
2027
|
+
if (!text) throw new Error("Missing message");
|
|
2028
|
+
const sendRes = await client.sendMessage(session.conversation_id, SCHEMA_TEXT, { text });
|
|
2029
|
+
if (!sendRes.ok) throw new Error(sendRes.error?.message ?? "Failed to send");
|
|
2030
|
+
sessionManager.focusSession(session.session_key);
|
|
2031
|
+
return {
|
|
2032
|
+
ok: true,
|
|
2033
|
+
session: sessionManager.get(session.session_key),
|
|
2034
|
+
message_id: sendRes.data?.message_id ?? null
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
if (parts[2] === "approve" && req.method === "POST") {
|
|
2038
|
+
const body = await readBody(req);
|
|
2039
|
+
const session = resolveSessionForInput(sessionManager, body);
|
|
2040
|
+
if (!session?.conversation_id) throw new Error("No session selected");
|
|
2041
|
+
const approveRes = await client.approveContact(session.conversation_id);
|
|
2042
|
+
if (!approveRes.ok) throw new Error(approveRes.error?.message ?? "Failed to approve contact");
|
|
2043
|
+
sessionManager.focusSession(session.session_key);
|
|
2044
|
+
return {
|
|
2045
|
+
ok: true,
|
|
2046
|
+
session: sessionManager.get(session.session_key),
|
|
2047
|
+
trusted: approveRes.data?.trusted ?? true,
|
|
2048
|
+
dm_conversation_id: approveRes.data?.dm_conversation_id ?? session.conversation_id
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
if (parts[2] === "mark-read" && req.method === "POST") {
|
|
2052
|
+
const body = await readBody(req);
|
|
2053
|
+
const session = resolveSessionForInput(sessionManager, body);
|
|
2054
|
+
if (!session?.session_key) throw new Error("No session selected");
|
|
2055
|
+
const updated = sessionManager.markRead(session.session_key);
|
|
2056
|
+
if (!updated) throw new Error("Failed to mark session as read");
|
|
2057
|
+
return {
|
|
2058
|
+
ok: true,
|
|
2059
|
+
session: updated
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
1203
2062
|
const url = new URL(req.url || "", "http://x");
|
|
1204
2063
|
const sessionKey = url.searchParams.get("session_key");
|
|
1205
2064
|
return buildSessionOverviewPayload(ctx, sessionKey);
|
|
@@ -1290,7 +2149,10 @@ async function handleApi(pathname, req, ctx) {
|
|
|
1290
2149
|
auditSummary: summarizeTrustPolicyAudit(auditEvents),
|
|
1291
2150
|
auditEvents,
|
|
1292
2151
|
recommendationSummary: recommendationState.summary,
|
|
1293
|
-
recommendations: recommendationState.recommendations
|
|
2152
|
+
recommendations: recommendationState.recommendations.map((item) => ({
|
|
2153
|
+
...item,
|
|
2154
|
+
primary_action_label: getTrustRecommendationActionLabel(item)
|
|
2155
|
+
}))
|
|
1294
2156
|
};
|
|
1295
2157
|
} finally {
|
|
1296
2158
|
auditStore.close();
|
|
@@ -1488,6 +2350,141 @@ async function handleApi(pathname, req, ctx) {
|
|
|
1488
2350
|
}
|
|
1489
2351
|
};
|
|
1490
2352
|
}
|
|
2353
|
+
if (parts[2] === "recommendations" && parts[3] === "apply-session" && req.method === "POST") {
|
|
2354
|
+
const body = await readBody(req);
|
|
2355
|
+
const sessionKey = typeof body?.session_key === "string" ? body.session_key : ctx.client.getSessionManager()?.getActiveSession()?.session_key ?? "";
|
|
2356
|
+
if (!sessionKey) throw new Error("session_key is required");
|
|
2357
|
+
const session = ctx.client.getSessionManager()?.get(sessionKey);
|
|
2358
|
+
if (!session?.remote_did) throw new Error("No active session recommendation target");
|
|
2359
|
+
const doc = readTrustPolicyDoc(ctx.identityPath);
|
|
2360
|
+
const sharedStore = new LocalStore(ctx.storePath);
|
|
2361
|
+
try {
|
|
2362
|
+
const auditManager = new TrustPolicyAuditManager(sharedStore);
|
|
2363
|
+
const recommendationManager = new TrustRecommendationManager(sharedStore);
|
|
2364
|
+
const auditEvents = auditManager.listRecent(200);
|
|
2365
|
+
recommendationManager.sync({
|
|
2366
|
+
policyDoc: doc,
|
|
2367
|
+
sessions: sessionManager.listRecentSessions(100),
|
|
2368
|
+
tasks: taskManager.listRecent(100),
|
|
2369
|
+
auditEvents,
|
|
2370
|
+
runtimeMode,
|
|
2371
|
+
limit: 50
|
|
2372
|
+
});
|
|
2373
|
+
const recommendation = recommendationManager.list({ remoteDid: session.remote_did, status: "open", limit: 1 })[0];
|
|
2374
|
+
if (!recommendation) throw new Error("No open recommendation for this session");
|
|
2375
|
+
const nextDoc = upsertTrustPolicyRecommendation(doc, recommendation);
|
|
2376
|
+
const savedPath = writeTrustPolicyDoc(ctx.identityPath, nextDoc);
|
|
2377
|
+
const stored = recommendationManager.apply(recommendation.id) ?? recommendation;
|
|
2378
|
+
auditManager.record({
|
|
2379
|
+
event_type: "recommendation_applied",
|
|
2380
|
+
policy_scope: recommendation.policy,
|
|
2381
|
+
remote_did: recommendation.remote_did,
|
|
2382
|
+
action: String(recommendation.action),
|
|
2383
|
+
outcome: "recommendation_applied",
|
|
2384
|
+
explanation: recommendation.reason,
|
|
2385
|
+
matched_rule: recommendation.match,
|
|
2386
|
+
detail: { recommendation_id: recommendation.id, session_key: sessionKey }
|
|
2387
|
+
});
|
|
2388
|
+
return { ok: true, path: savedPath, recommendation: stored, doc: nextDoc };
|
|
2389
|
+
} finally {
|
|
2390
|
+
sharedStore.close();
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (parts[2] === "recommendations" && parts[3] === "dismiss-session" && req.method === "POST") {
|
|
2394
|
+
const body = await readBody(req);
|
|
2395
|
+
const sessionKey = typeof body?.session_key === "string" ? body.session_key : ctx.client.getSessionManager()?.getActiveSession()?.session_key ?? "";
|
|
2396
|
+
if (!sessionKey) throw new Error("session_key is required");
|
|
2397
|
+
const session = ctx.client.getSessionManager()?.get(sessionKey);
|
|
2398
|
+
if (!session?.remote_did) throw new Error("No active session recommendation target");
|
|
2399
|
+
const sharedStore = new LocalStore(ctx.storePath);
|
|
2400
|
+
try {
|
|
2401
|
+
const recommendationManager = new TrustRecommendationManager(sharedStore);
|
|
2402
|
+
const recommendation = recommendationManager.list({ remoteDid: session.remote_did, status: "open", limit: 1 })[0];
|
|
2403
|
+
if (!recommendation) throw new Error("No open recommendation for this session");
|
|
2404
|
+
const stored = recommendationManager.dismiss(recommendation.id) ?? recommendation;
|
|
2405
|
+
new TrustPolicyAuditManager(sharedStore).record({
|
|
2406
|
+
event_type: "recommendation_dismissed",
|
|
2407
|
+
policy_scope: recommendation.policy,
|
|
2408
|
+
remote_did: recommendation.remote_did,
|
|
2409
|
+
action: String(recommendation.action),
|
|
2410
|
+
outcome: "recommendation_dismissed",
|
|
2411
|
+
explanation: recommendation.reason,
|
|
2412
|
+
matched_rule: recommendation.match,
|
|
2413
|
+
detail: { recommendation_id: recommendation.id, session_key: sessionKey }
|
|
2414
|
+
});
|
|
2415
|
+
return { ok: true, recommendation: stored };
|
|
2416
|
+
} finally {
|
|
2417
|
+
sharedStore.close();
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (parts[2] === "recommendations" && parts[3] === "reopen-session" && req.method === "POST") {
|
|
2421
|
+
const body = await readBody(req);
|
|
2422
|
+
const sessionKey = typeof body?.session_key === "string" ? body.session_key : ctx.client.getSessionManager()?.getActiveSession()?.session_key ?? "";
|
|
2423
|
+
if (!sessionKey) throw new Error("session_key is required");
|
|
2424
|
+
const session = ctx.client.getSessionManager()?.get(sessionKey);
|
|
2425
|
+
if (!session?.remote_did) throw new Error("No active session recommendation target");
|
|
2426
|
+
const sharedStore = new LocalStore(ctx.storePath);
|
|
2427
|
+
try {
|
|
2428
|
+
const recommendationManager = new TrustRecommendationManager(sharedStore);
|
|
2429
|
+
const recommendation = recommendationManager.list({ remoteDid: session.remote_did, status: ["dismissed", "superseded"], limit: 1 })[0];
|
|
2430
|
+
if (!recommendation) throw new Error("No dismissed or superseded recommendation for this session");
|
|
2431
|
+
const stored = recommendationManager.reopen(recommendation.id) ?? recommendation;
|
|
2432
|
+
new TrustPolicyAuditManager(sharedStore).record({
|
|
2433
|
+
event_type: "recommendation_reopened",
|
|
2434
|
+
policy_scope: recommendation.policy,
|
|
2435
|
+
remote_did: recommendation.remote_did,
|
|
2436
|
+
action: String(recommendation.action),
|
|
2437
|
+
outcome: "recommendation_reopened",
|
|
2438
|
+
explanation: recommendation.reason,
|
|
2439
|
+
matched_rule: recommendation.match,
|
|
2440
|
+
detail: { recommendation_id: recommendation.id, session_key: sessionKey }
|
|
2441
|
+
});
|
|
2442
|
+
return { ok: true, recommendation: stored };
|
|
2443
|
+
} finally {
|
|
2444
|
+
sharedStore.close();
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (parts[0] === "public") {
|
|
2450
|
+
if ((!parts[1] || parts[1] === "self") && req.method === "GET") {
|
|
2451
|
+
await maybeEnsureHostedPublicLink(ctx);
|
|
2452
|
+
const res = await client.getPublicSelf();
|
|
2453
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to get public link state");
|
|
2454
|
+
return res.data;
|
|
2455
|
+
}
|
|
2456
|
+
if (parts[1] === "link" && req.method === "POST") {
|
|
2457
|
+
const body = await readBody(req);
|
|
2458
|
+
const res = await client.createPublicLink({
|
|
2459
|
+
slug: typeof body?.slug === "string" ? body.slug : void 0,
|
|
2460
|
+
enabled: typeof body?.enabled === "boolean" ? body.enabled : void 0
|
|
2461
|
+
});
|
|
2462
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to create public link");
|
|
2463
|
+
return res.data;
|
|
2464
|
+
}
|
|
2465
|
+
if (parts[1] === "contact-card" && req.method === "POST") {
|
|
2466
|
+
const body = await readBody(req);
|
|
2467
|
+
const res = await client.createContactCard({
|
|
2468
|
+
target_did: typeof body?.target_did === "string" ? body.target_did : void 0,
|
|
2469
|
+
referrer_did: typeof body?.referrer_did === "string" ? body.referrer_did : void 0,
|
|
2470
|
+
intro_note: typeof body?.intro_note === "string" ? body.intro_note : void 0,
|
|
2471
|
+
message_template: typeof body?.message_template === "string" ? body.message_template : void 0
|
|
2472
|
+
});
|
|
2473
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to create contact card");
|
|
2474
|
+
return res.data;
|
|
2475
|
+
}
|
|
2476
|
+
if (parts[1] === "task-share" && req.method === "POST") {
|
|
2477
|
+
const body = await readBody(req);
|
|
2478
|
+
const res = await client.createTaskShare({
|
|
2479
|
+
task_id: typeof body?.task_id === "string" ? body.task_id : void 0,
|
|
2480
|
+
title: typeof body?.title === "string" ? body.title : void 0,
|
|
2481
|
+
status: typeof body?.status === "string" ? body.status : void 0,
|
|
2482
|
+
summary: String(body?.summary ?? "").trim(),
|
|
2483
|
+
conversation_id: typeof body?.conversation_id === "string" ? body.conversation_id : void 0,
|
|
2484
|
+
redacted_metadata: body?.redacted_metadata && typeof body.redacted_metadata === "object" ? body.redacted_metadata : void 0
|
|
2485
|
+
});
|
|
2486
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to create task share");
|
|
2487
|
+
return res.data;
|
|
1491
2488
|
}
|
|
1492
2489
|
}
|
|
1493
2490
|
if (parts[0] === "profile") {
|
|
@@ -1499,11 +2496,12 @@ async function handleApi(pathname, req, ctx) {
|
|
|
1499
2496
|
if (req.method === "POST") {
|
|
1500
2497
|
const body = await readBody(req);
|
|
1501
2498
|
const profile = {};
|
|
1502
|
-
if (body?.display_name
|
|
1503
|
-
if (body?.bio
|
|
1504
|
-
if (Array.isArray(body?.capabilities)) profile.capabilities = body.capabilities;
|
|
2499
|
+
if (typeof body?.display_name === "string") profile.display_name = body.display_name;
|
|
2500
|
+
if (typeof body?.bio === "string") profile.bio = body.bio;
|
|
2501
|
+
if (Array.isArray(body?.capabilities)) profile.capabilities = body.capabilities.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
1505
2502
|
else if (typeof body?.capabilities === "string") profile.capabilities = body.capabilities.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1506
|
-
if (
|
|
2503
|
+
if (body?.capability_card && typeof body.capability_card === "object") profile.capability_card = body.capability_card;
|
|
2504
|
+
if (Array.isArray(body?.tags)) profile.tags = body.tags.map((value) => String(value ?? "").trim()).filter(Boolean);
|
|
1507
2505
|
else if (typeof body?.tags === "string") profile.tags = body.tags.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1508
2506
|
if (typeof body?.discoverable === "boolean") profile.discoverable = body.discoverable;
|
|
1509
2507
|
const res = await client.updateProfile(profile);
|