@pingagent/sdk 0.1.0 → 0.1.2
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 +1417 -1124
- package/dist/chunk-MDGELIR5.js +1317 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/web-server.d.ts +2 -0
- package/dist/web-server.js +80 -40
- package/package.json +5 -5
- package/src/client.ts +2 -0
- package/src/web-server.ts +87 -45
- package/src/ws-subscription.ts +3 -0
package/dist/index.d.ts
CHANGED
|
@@ -151,6 +151,8 @@ interface ConversationEntry {
|
|
|
151
151
|
target_did: string;
|
|
152
152
|
trusted: boolean;
|
|
153
153
|
created_at: number;
|
|
154
|
+
/** Server-side last message write time (ms). Omitted or null when unknown; client should fetch when missing. */
|
|
155
|
+
last_activity_at?: number | null;
|
|
154
156
|
}
|
|
155
157
|
interface ConversationListResponse {
|
|
156
158
|
conversations: ConversationEntry[];
|
|
@@ -473,6 +475,8 @@ interface WsSubscriptionOptions {
|
|
|
473
475
|
onMessage: (envelope: any, conversationId: string) => void;
|
|
474
476
|
onControl?: (control: WsControlPayload, conversationId: string) => void;
|
|
475
477
|
onError?: (err: Error) => void;
|
|
478
|
+
/** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
|
|
479
|
+
onOpen?: (conversationId: string) => void;
|
|
476
480
|
}
|
|
477
481
|
declare class WsSubscription {
|
|
478
482
|
private opts;
|
package/dist/index.js
CHANGED
package/dist/web-server.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import * as http from 'node:http';
|
|
|
9
9
|
interface ProfileEntry {
|
|
10
10
|
id: string;
|
|
11
11
|
did?: string;
|
|
12
|
+
/** Server URL for this profile (from identity); used so local vs remote profiles work in one web session. */
|
|
13
|
+
serverUrl?: string;
|
|
12
14
|
identityPath: string;
|
|
13
15
|
storePath: string;
|
|
14
16
|
}
|
package/dist/web-server.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
ensureTokenValid,
|
|
6
6
|
loadIdentity,
|
|
7
7
|
updateStoredToken
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MDGELIR5.js";
|
|
9
9
|
|
|
10
10
|
// src/web-server.ts
|
|
11
11
|
import * as fs from "fs";
|
|
@@ -29,6 +29,7 @@ function listProfiles(rootDir) {
|
|
|
29
29
|
profiles.push({
|
|
30
30
|
id: "default",
|
|
31
31
|
did: id.did,
|
|
32
|
+
serverUrl: id.serverUrl,
|
|
32
33
|
identityPath: defaultIdentity,
|
|
33
34
|
storePath: path.join(root, "store.db")
|
|
34
35
|
});
|
|
@@ -45,7 +46,7 @@ function listProfiles(rootDir) {
|
|
|
45
46
|
if (fs.existsSync(idPath)) {
|
|
46
47
|
try {
|
|
47
48
|
const id = loadIdentity(idPath);
|
|
48
|
-
profiles.push({ id: name, did: id.did, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
49
|
+
profiles.push({ id: name, did: id.did, serverUrl: id.serverUrl, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
49
50
|
} catch {
|
|
50
51
|
profiles.push({ id: name, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
51
52
|
}
|
|
@@ -66,7 +67,7 @@ function listProfiles(rootDir) {
|
|
|
66
67
|
if (fs.existsSync(idPath) && !profiles.some((p) => p.id === name)) {
|
|
67
68
|
try {
|
|
68
69
|
const id = loadIdentity(idPath);
|
|
69
|
-
profiles.push({ id: name, did: id.did, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
70
|
+
profiles.push({ id: name, did: id.did, serverUrl: id.serverUrl, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
70
71
|
} catch {
|
|
71
72
|
profiles.push({ id: name, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
72
73
|
}
|
|
@@ -74,31 +75,34 @@ function listProfiles(rootDir) {
|
|
|
74
75
|
}
|
|
75
76
|
return profiles;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
var DEFAULT_SERVER_URL = "http://localhost:8787";
|
|
79
|
+
async function getContextForProfile(profile, defaultServerUrl) {
|
|
79
80
|
const identity = loadIdentity(profile.identityPath);
|
|
81
|
+
const serverUrl = identity.serverUrl ?? defaultServerUrl ?? DEFAULT_SERVER_URL;
|
|
82
|
+
await ensureTokenValid(profile.identityPath, serverUrl);
|
|
83
|
+
const identityAfter = loadIdentity(profile.identityPath);
|
|
80
84
|
const store = new LocalStore(profile.storePath);
|
|
81
85
|
const contactManager = new ContactManager(store);
|
|
82
86
|
const client = new PingAgentClient({
|
|
83
87
|
serverUrl,
|
|
84
|
-
identity,
|
|
85
|
-
accessToken:
|
|
88
|
+
identity: identityAfter,
|
|
89
|
+
accessToken: identityAfter.accessToken ?? "",
|
|
86
90
|
store,
|
|
87
91
|
onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, profile.identityPath)
|
|
88
92
|
});
|
|
89
|
-
return { client, contactManager, myDid:
|
|
93
|
+
return { client, contactManager, myDid: identityAfter.did, serverUrl };
|
|
90
94
|
}
|
|
91
95
|
var clientCache = /* @__PURE__ */ new Map();
|
|
92
96
|
async function startWebServer(opts) {
|
|
93
97
|
const port = opts.port ?? DEFAULT_PORT;
|
|
94
|
-
const
|
|
98
|
+
const defaultServerUrl = opts.serverUrl ?? DEFAULT_SERVER_URL;
|
|
95
99
|
const rootDir = opts.rootDir ? resolvePath(opts.rootDir) : resolvePath(DEFAULT_ROOT);
|
|
96
100
|
let profiles;
|
|
97
101
|
let fixedContext = null;
|
|
98
102
|
if (opts.fixedIdentityPath && opts.fixedStorePath) {
|
|
99
103
|
const identityPath = resolvePath(opts.fixedIdentityPath);
|
|
100
104
|
const storePath = resolvePath(opts.fixedStorePath);
|
|
101
|
-
fixedContext = await getContextForProfile({ id: "fixed", identityPath, storePath },
|
|
105
|
+
fixedContext = await getContextForProfile({ id: "fixed", identityPath, storePath }, defaultServerUrl);
|
|
102
106
|
profiles = [{ id: "fixed", did: fixedContext.myDid, identityPath, storePath }];
|
|
103
107
|
} else {
|
|
104
108
|
profiles = listProfiles(opts.rootDir ?? DEFAULT_ROOT);
|
|
@@ -106,7 +110,7 @@ async function startWebServer(opts) {
|
|
|
106
110
|
throw new Error(`No identity found in ${rootDir}. Run: pingagent init`);
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
|
-
const html = getHtml(
|
|
113
|
+
const html = getHtml();
|
|
110
114
|
const server = http.createServer(async (req, res) => {
|
|
111
115
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
112
116
|
const pathname = url.pathname;
|
|
@@ -131,7 +135,7 @@ async function startWebServer(opts) {
|
|
|
131
135
|
try {
|
|
132
136
|
if (pathname === "/api/profiles" || pathname === "/api/profiles/") {
|
|
133
137
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
134
|
-
res.end(JSON.stringify({ profiles: profiles.map((p) => ({ id: p.id, did: p.did })) }));
|
|
138
|
+
res.end(JSON.stringify({ profiles: profiles.map((p) => ({ id: p.id, did: p.did, server: p.serverUrl })) }));
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
137
141
|
let ctx = fixedContext;
|
|
@@ -147,11 +151,11 @@ async function startWebServer(opts) {
|
|
|
147
151
|
else {
|
|
148
152
|
const p = profiles.find((x) => x.id === pid);
|
|
149
153
|
if (!p) throw new Error(`Unknown profile: ${pid}`);
|
|
150
|
-
ctx = await getContextForProfile(p,
|
|
154
|
+
ctx = await getContextForProfile(p, defaultServerUrl);
|
|
151
155
|
clientCache.set(pid, ctx);
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
|
-
const result = await handleApi(pathname, req, ctx.client, ctx.contactManager, ctx.myDid, serverUrl);
|
|
158
|
+
const result = await handleApi(pathname, req, ctx.client, ctx.contactManager, ctx.myDid, ctx.serverUrl);
|
|
155
159
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
156
160
|
res.end(JSON.stringify(result));
|
|
157
161
|
} catch (err) {
|
|
@@ -167,10 +171,10 @@ async function startWebServer(opts) {
|
|
|
167
171
|
console.log(`PingAgent Web: http://127.0.0.1:${port}`);
|
|
168
172
|
if (fixedContext) {
|
|
169
173
|
console.log(` DID: ${fixedContext.myDid}`);
|
|
174
|
+
console.log(` Server: ${fixedContext.serverUrl}`);
|
|
170
175
|
} else {
|
|
171
|
-
console.log(` Profiles: ${profiles.map((p) => p.id).join(", ")}`);
|
|
176
|
+
console.log(` Profiles: ${profiles.map((p) => p.id).join(", ")} (each uses its identity server URL)`);
|
|
172
177
|
}
|
|
173
|
-
console.log(` Server: ${serverUrl}`);
|
|
174
178
|
});
|
|
175
179
|
return server;
|
|
176
180
|
}
|
|
@@ -384,11 +388,11 @@ function readBody(req) {
|
|
|
384
388
|
req.on("error", reject);
|
|
385
389
|
});
|
|
386
390
|
}
|
|
387
|
-
function getHtml(
|
|
388
|
-
const profilePicker =
|
|
391
|
+
function getHtml(_fixedOnly) {
|
|
392
|
+
const profilePicker = `
|
|
389
393
|
<div class="profile-picker" id="profilePicker">
|
|
390
394
|
<div class="profile-label">\u9009\u62E9 Profile \u767B\u5F55</div>
|
|
391
|
-
<div class="profile-list" id="profileList"></div>
|
|
395
|
+
<div class="profile-list" id="profileList"><p class="profile-loading">\u52A0\u8F7D\u4E2D...</p></div>
|
|
392
396
|
</div>
|
|
393
397
|
<div class="profile-current" id="profileCurrent" style="display:none">
|
|
394
398
|
<span>\u5F53\u524D: <strong id="currentProfileName"></strong></span>
|
|
@@ -433,8 +437,11 @@ function getHtml(fixedOnly) {
|
|
|
433
437
|
.error { color: #f87171; font-size: 13px; padding: 8px 0; }
|
|
434
438
|
.profile-picker { padding: 12px 16px; border-bottom: 1px solid #27272a; }
|
|
435
439
|
.profile-label { font-size: 12px; color: #71717a; margin-bottom: 8px; }
|
|
440
|
+
.profile-list { min-height: 160px; }
|
|
441
|
+
.profile-loading { font-size: 12px; color: #71717a; margin: 0; }
|
|
436
442
|
.profile-list .profile-btn { display: block; width: 100%; padding: 8px 12px; margin-bottom: 4px; background: #27272a; color: #e4e4e7; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; text-align: left; }
|
|
437
443
|
.profile-list .profile-btn:hover { background: #3f3f46; }
|
|
444
|
+
.profile-list .profile-btn.selected { border: 1px solid #3b82f6; background: #1e3a5f; }
|
|
438
445
|
.profile-list .profile-btn .sub { font-size: 11px; color: #71717a; }
|
|
439
446
|
.profile-current { padding: 12px 16px; border-bottom: 1px solid #27272a; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
440
447
|
.profile-current span { font-size: 13px; color: #a1a1aa; }
|
|
@@ -563,41 +570,72 @@ function getHtml(fixedOnly) {
|
|
|
563
570
|
let profilesCache = null;
|
|
564
571
|
|
|
565
572
|
function showProfileCurrent() {
|
|
566
|
-
if (profileCurrentEl
|
|
573
|
+
if (profileCurrentEl) {
|
|
567
574
|
profileCurrentEl.style.display = 'flex';
|
|
568
|
-
profilePickerEl.style.display = 'none';
|
|
569
575
|
const nameEl = document.getElementById('currentProfileName');
|
|
570
576
|
if (nameEl) nameEl.textContent = selectedProfile || '';
|
|
571
577
|
}
|
|
578
|
+
if (profilePickerEl) profilePickerEl.style.display = '';
|
|
579
|
+
highlightSelectedProfile();
|
|
572
580
|
}
|
|
573
581
|
|
|
574
582
|
function showProfilePicker() {
|
|
575
|
-
if (profileCurrentEl
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
+
if (profileCurrentEl) profileCurrentEl.style.display = 'none';
|
|
584
|
+
if (profilePickerEl) profilePickerEl.style.display = '';
|
|
585
|
+
currentConv = null;
|
|
586
|
+
document.getElementById('inputArea').style.display = 'none';
|
|
587
|
+
document.getElementById('messages').innerHTML = '';
|
|
588
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u9009\u62E9 Profile \u767B\u5F55</strong>';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function highlightSelectedProfile() {
|
|
592
|
+
const listEl = document.getElementById('profileList');
|
|
593
|
+
if (!listEl) return;
|
|
594
|
+
listEl.querySelectorAll('.profile-btn').forEach(btn => {
|
|
595
|
+
btn.classList.toggle('selected', btn.dataset.id === selectedProfile);
|
|
596
|
+
});
|
|
583
597
|
}
|
|
584
598
|
|
|
585
599
|
async function loadProfiles() {
|
|
586
|
-
|
|
600
|
+
const listEl = document.getElementById('profileList');
|
|
601
|
+
if (!profilePickerEl || !listEl) return [];
|
|
587
602
|
if (profilesCache) return profilesCache;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
603
|
+
try {
|
|
604
|
+
const ctrl = new AbortController();
|
|
605
|
+
const t = setTimeout(() => ctrl.abort(), 8000);
|
|
606
|
+
const r = await fetch('/api/profiles', { signal: ctrl.signal });
|
|
607
|
+
clearTimeout(t);
|
|
608
|
+
if (!r.ok) {
|
|
609
|
+
listEl.innerHTML = '<p class="profile-loading">\u8BF7\u6C42\u5931\u8D25 ' + r.status + '</p>';
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
const data = await r.json().catch(() => ({}));
|
|
613
|
+
const list = Array.isArray(data.profiles) ? data.profiles : [];
|
|
614
|
+
profilesCache = list;
|
|
615
|
+
if (list.length === 0) listEl.innerHTML = '<p class="profile-loading">\u672A\u627E\u5230 profile</p>';
|
|
616
|
+
return profilesCache;
|
|
617
|
+
} catch (e) {
|
|
618
|
+
const msg = (e && e.name === 'AbortError') ? '\u8BF7\u6C42\u8D85\u65F6' : '\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0';
|
|
619
|
+
listEl.innerHTML = '<p class="profile-loading">' + msg + '</p>';
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
593
622
|
}
|
|
594
623
|
|
|
595
624
|
function renderProfileList(profiles) {
|
|
596
625
|
const listEl = document.getElementById('profileList');
|
|
597
626
|
if (!listEl) return;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
627
|
+
const list = profiles || [];
|
|
628
|
+
if (list.length === 0) {
|
|
629
|
+
listEl.innerHTML = '<p class="profile-loading">\u672A\u627E\u5230 profile</p>';
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
listEl.innerHTML = list.map(p => {
|
|
633
|
+
var s = (p.server && typeof p.server === 'string') ? p.server : '';
|
|
634
|
+
var serverLabel = s.indexOf('://') >= 0 ? s.split('://')[1].split('/')[0] : (s || 'local');
|
|
635
|
+
var didShort = (p.did && typeof p.did === 'string') ? p.did.slice(0, 24) + '...' : '';
|
|
636
|
+
var sel = p.id === selectedProfile ? ' selected' : '';
|
|
637
|
+
return '<button class="profile-btn' + sel + '" data-id="' + p.id + '">' + p.id + '<div class="sub">' + serverLabel + ' \xB7 ' + didShort + '<' + '/div><' + '/button>';
|
|
638
|
+
}).join('');
|
|
601
639
|
listEl.querySelectorAll('.profile-btn').forEach(btn => {
|
|
602
640
|
btn.addEventListener('click', async () => {
|
|
603
641
|
selectedProfile = btn.dataset.id || null;
|
|
@@ -1011,19 +1049,21 @@ function getHtml(fixedOnly) {
|
|
|
1011
1049
|
}
|
|
1012
1050
|
const profiles = await loadProfiles();
|
|
1013
1051
|
if (profiles.length === 0) {
|
|
1014
|
-
document.getElementById('mainHeader')
|
|
1052
|
+
const header = document.getElementById('mainHeader');
|
|
1053
|
+
if (header) header.innerHTML = '<strong>\u65E0\u53EF\u7528 Profile\uFF0C\u8BF7\u5148 pingagent init</strong>';
|
|
1054
|
+
renderProfileList([]);
|
|
1015
1055
|
return;
|
|
1016
1056
|
}
|
|
1017
1057
|
if (profiles.length === 1 && !selectedProfile) {
|
|
1018
1058
|
selectedProfile = profiles[0].id;
|
|
1019
1059
|
sessionStorage.setItem('pingagent_web_profile', selectedProfile);
|
|
1020
1060
|
}
|
|
1061
|
+
renderProfileList(profiles);
|
|
1021
1062
|
if (selectedProfile && profiles.some(p => p.id === selectedProfile)) {
|
|
1022
1063
|
showProfileCurrent();
|
|
1023
1064
|
await loadDataForProfile();
|
|
1024
1065
|
return;
|
|
1025
1066
|
}
|
|
1026
|
-
renderProfileList(profiles);
|
|
1027
1067
|
showProfilePicker();
|
|
1028
1068
|
}
|
|
1029
1069
|
init();
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pingagent/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/PingAgent/PingAgent.git"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "dist/index.js",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"commander": "^13.0.0",
|
|
27
27
|
"uuid": "^11.0.0",
|
|
28
28
|
"ws": "^8.0.0",
|
|
29
|
-
"@pingagent/protocol": "0.1.
|
|
30
|
-
"@pingagent/a2a": "0.1.
|
|
31
|
-
"@pingagent/schemas": "0.1.
|
|
29
|
+
"@pingagent/protocol": "0.1.1",
|
|
30
|
+
"@pingagent/a2a": "0.1.1",
|
|
31
|
+
"@pingagent/schemas": "0.1.1"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/better-sqlite3": "^7.6.0",
|
package/src/client.ts
CHANGED
|
@@ -62,6 +62,8 @@ export interface ConversationEntry {
|
|
|
62
62
|
target_did: string;
|
|
63
63
|
trusted: boolean;
|
|
64
64
|
created_at: number;
|
|
65
|
+
/** Server-side last message write time (ms). Omitted or null when unknown; client should fetch when missing. */
|
|
66
|
+
last_activity_at?: number | null;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export interface ConversationListResponse {
|
package/src/web-server.ts
CHANGED
|
@@ -28,6 +28,8 @@ function resolvePath(p: string): string {
|
|
|
28
28
|
export interface ProfileEntry {
|
|
29
29
|
id: string;
|
|
30
30
|
did?: string;
|
|
31
|
+
/** Server URL for this profile (from identity); used so local vs remote profiles work in one web session. */
|
|
32
|
+
serverUrl?: string;
|
|
31
33
|
identityPath: string;
|
|
32
34
|
storePath: string;
|
|
33
35
|
}
|
|
@@ -55,6 +57,7 @@ function listProfiles(rootDir: string): ProfileEntry[] {
|
|
|
55
57
|
profiles.push({
|
|
56
58
|
id: 'default',
|
|
57
59
|
did: id.did,
|
|
60
|
+
serverUrl: id.serverUrl,
|
|
58
61
|
identityPath: defaultIdentity,
|
|
59
62
|
storePath: path.join(root, 'store.db'),
|
|
60
63
|
});
|
|
@@ -73,7 +76,7 @@ function listProfiles(rootDir: string): ProfileEntry[] {
|
|
|
73
76
|
if (fs.existsSync(idPath)) {
|
|
74
77
|
try {
|
|
75
78
|
const id = loadIdentity(idPath);
|
|
76
|
-
profiles.push({ id: name, did: id.did, identityPath: idPath, storePath: path.join(sub, 'store.db') });
|
|
79
|
+
profiles.push({ id: name, did: id.did, serverUrl: id.serverUrl, identityPath: idPath, storePath: path.join(sub, 'store.db') });
|
|
77
80
|
} catch {
|
|
78
81
|
profiles.push({ id: name, identityPath: idPath, storePath: path.join(sub, 'store.db') });
|
|
79
82
|
}
|
|
@@ -81,7 +84,7 @@ function listProfiles(rootDir: string): ProfileEntry[] {
|
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
// root/<dir>/identity.json (e.g. receiver, agent1)
|
|
87
|
+
// root/<dir>/identity.json (e.g. receiver, agent1, remote1c)
|
|
85
88
|
let names: string[];
|
|
86
89
|
try {
|
|
87
90
|
names = fs.readdirSync(root);
|
|
@@ -96,7 +99,7 @@ function listProfiles(rootDir: string): ProfileEntry[] {
|
|
|
96
99
|
if (fs.existsSync(idPath) && !profiles.some((p) => p.id === name)) {
|
|
97
100
|
try {
|
|
98
101
|
const id = loadIdentity(idPath);
|
|
99
|
-
profiles.push({ id: name, did: id.did, identityPath: idPath, storePath: path.join(sub, 'store.db') });
|
|
102
|
+
profiles.push({ id: name, did: id.did, serverUrl: id.serverUrl, identityPath: idPath, storePath: path.join(sub, 'store.db') });
|
|
100
103
|
} catch {
|
|
101
104
|
profiles.push({ id: name, identityPath: idPath, storePath: path.join(sub, 'store.db') });
|
|
102
105
|
}
|
|
@@ -106,38 +109,43 @@ function listProfiles(rootDir: string): ProfileEntry[] {
|
|
|
106
109
|
return profiles;
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
/** Default server URL when identity has none (e.g. legacy). */
|
|
113
|
+
const DEFAULT_SERVER_URL = 'http://localhost:8787';
|
|
114
|
+
|
|
109
115
|
async function getContextForProfile(
|
|
110
116
|
profile: ProfileEntry,
|
|
111
|
-
|
|
112
|
-
): Promise<{ client: PingAgentClient; contactManager: ContactManager; myDid: string }> {
|
|
113
|
-
await ensureTokenValid(profile.identityPath, serverUrl);
|
|
117
|
+
defaultServerUrl: string,
|
|
118
|
+
): Promise<{ client: PingAgentClient; contactManager: ContactManager; myDid: string; serverUrl: string }> {
|
|
114
119
|
const identity = loadIdentity(profile.identityPath);
|
|
120
|
+
const serverUrl = identity.serverUrl ?? defaultServerUrl ?? DEFAULT_SERVER_URL;
|
|
121
|
+
await ensureTokenValid(profile.identityPath, serverUrl);
|
|
122
|
+
const identityAfter = loadIdentity(profile.identityPath);
|
|
115
123
|
const store = new LocalStore(profile.storePath);
|
|
116
124
|
const contactManager = new ContactManager(store);
|
|
117
125
|
const client = new PingAgentClient({
|
|
118
126
|
serverUrl,
|
|
119
|
-
identity,
|
|
120
|
-
accessToken:
|
|
127
|
+
identity: identityAfter,
|
|
128
|
+
accessToken: identityAfter.accessToken ?? '',
|
|
121
129
|
store,
|
|
122
130
|
onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, profile.identityPath),
|
|
123
131
|
});
|
|
124
|
-
return { client, contactManager, myDid:
|
|
132
|
+
return { client, contactManager, myDid: identityAfter.did, serverUrl };
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
const clientCache = new Map<string, { client: PingAgentClient; contactManager: ContactManager; myDid: string }>();
|
|
135
|
+
const clientCache = new Map<string, { client: PingAgentClient; contactManager: ContactManager; myDid: string; serverUrl: string }>();
|
|
128
136
|
|
|
129
137
|
export async function startWebServer(opts: WebServerOptions): Promise<http.Server> {
|
|
130
138
|
const port = opts.port ?? DEFAULT_PORT;
|
|
131
|
-
const
|
|
139
|
+
const defaultServerUrl = opts.serverUrl ?? DEFAULT_SERVER_URL;
|
|
132
140
|
const rootDir = opts.rootDir ? resolvePath(opts.rootDir) : resolvePath(DEFAULT_ROOT);
|
|
133
141
|
|
|
134
142
|
let profiles: ProfileEntry[];
|
|
135
|
-
let fixedContext: { client: PingAgentClient; contactManager: ContactManager; myDid: string } | null = null;
|
|
143
|
+
let fixedContext: { client: PingAgentClient; contactManager: ContactManager; myDid: string; serverUrl: string } | null = null;
|
|
136
144
|
|
|
137
145
|
if (opts.fixedIdentityPath && opts.fixedStorePath) {
|
|
138
146
|
const identityPath = resolvePath(opts.fixedIdentityPath);
|
|
139
147
|
const storePath = resolvePath(opts.fixedStorePath);
|
|
140
|
-
fixedContext = await getContextForProfile({ id: 'fixed', identityPath, storePath },
|
|
148
|
+
fixedContext = await getContextForProfile({ id: 'fixed', identityPath, storePath }, defaultServerUrl);
|
|
141
149
|
profiles = [{ id: 'fixed', did: fixedContext.myDid, identityPath, storePath }];
|
|
142
150
|
} else {
|
|
143
151
|
profiles = listProfiles(opts.rootDir ?? DEFAULT_ROOT);
|
|
@@ -146,7 +154,7 @@ export async function startWebServer(opts: WebServerOptions): Promise<http.Serve
|
|
|
146
154
|
}
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
const html = getHtml(
|
|
157
|
+
const html = getHtml();
|
|
150
158
|
|
|
151
159
|
const server = http.createServer(async (req, res) => {
|
|
152
160
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
@@ -176,7 +184,7 @@ export async function startWebServer(opts: WebServerOptions): Promise<http.Serve
|
|
|
176
184
|
try {
|
|
177
185
|
if (pathname === '/api/profiles' || pathname === '/api/profiles/') {
|
|
178
186
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
179
|
-
res.end(JSON.stringify({ profiles: profiles.map((p) => ({ id: p.id, did: p.did })) }));
|
|
187
|
+
res.end(JSON.stringify({ profiles: profiles.map((p) => ({ id: p.id, did: p.did, server: p.serverUrl })) }));
|
|
180
188
|
return;
|
|
181
189
|
}
|
|
182
190
|
let ctx = fixedContext;
|
|
@@ -194,11 +202,11 @@ export async function startWebServer(opts: WebServerOptions): Promise<http.Serve
|
|
|
194
202
|
else {
|
|
195
203
|
const p = profiles.find((x) => x.id === pid);
|
|
196
204
|
if (!p) throw new Error(`Unknown profile: ${pid}`);
|
|
197
|
-
ctx = await getContextForProfile(p,
|
|
205
|
+
ctx = await getContextForProfile(p, defaultServerUrl);
|
|
198
206
|
clientCache.set(pid, ctx);
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
|
-
const result = await handleApi(pathname, req, ctx.client, ctx.contactManager, ctx.myDid, serverUrl);
|
|
209
|
+
const result = await handleApi(pathname, req, ctx.client, ctx.contactManager, ctx.myDid, ctx.serverUrl);
|
|
202
210
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
203
211
|
res.end(JSON.stringify(result));
|
|
204
212
|
} catch (err: any) {
|
|
@@ -216,10 +224,10 @@ export async function startWebServer(opts: WebServerOptions): Promise<http.Serve
|
|
|
216
224
|
console.log(`PingAgent Web: http://127.0.0.1:${port}`);
|
|
217
225
|
if (fixedContext) {
|
|
218
226
|
console.log(` DID: ${fixedContext.myDid}`);
|
|
227
|
+
console.log(` Server: ${fixedContext.serverUrl}`);
|
|
219
228
|
} else {
|
|
220
|
-
console.log(` Profiles: ${profiles.map((p) => p.id).join(', ')}`);
|
|
229
|
+
console.log(` Profiles: ${profiles.map((p) => p.id).join(', ')} (each uses its identity server URL)`);
|
|
221
230
|
}
|
|
222
|
-
console.log(` Server: ${serverUrl}`);
|
|
223
231
|
});
|
|
224
232
|
|
|
225
233
|
return server;
|
|
@@ -455,13 +463,11 @@ function readBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
|
|
|
455
463
|
});
|
|
456
464
|
}
|
|
457
465
|
|
|
458
|
-
function getHtml(
|
|
459
|
-
const profilePicker =
|
|
460
|
-
? ''
|
|
461
|
-
: `
|
|
466
|
+
function getHtml(_fixedOnly?: boolean): string {
|
|
467
|
+
const profilePicker = `
|
|
462
468
|
<div class="profile-picker" id="profilePicker">
|
|
463
469
|
<div class="profile-label">选择 Profile 登录</div>
|
|
464
|
-
<div class="profile-list" id="profileList"></div>
|
|
470
|
+
<div class="profile-list" id="profileList"><p class="profile-loading">加载中...</p></div>
|
|
465
471
|
</div>
|
|
466
472
|
<div class="profile-current" id="profileCurrent" style="display:none">
|
|
467
473
|
<span>当前: <strong id="currentProfileName"></strong></span>
|
|
@@ -506,8 +512,11 @@ function getHtml(fixedOnly: boolean): string {
|
|
|
506
512
|
.error { color: #f87171; font-size: 13px; padding: 8px 0; }
|
|
507
513
|
.profile-picker { padding: 12px 16px; border-bottom: 1px solid #27272a; }
|
|
508
514
|
.profile-label { font-size: 12px; color: #71717a; margin-bottom: 8px; }
|
|
515
|
+
.profile-list { min-height: 160px; }
|
|
516
|
+
.profile-loading { font-size: 12px; color: #71717a; margin: 0; }
|
|
509
517
|
.profile-list .profile-btn { display: block; width: 100%; padding: 8px 12px; margin-bottom: 4px; background: #27272a; color: #e4e4e7; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; text-align: left; }
|
|
510
518
|
.profile-list .profile-btn:hover { background: #3f3f46; }
|
|
519
|
+
.profile-list .profile-btn.selected { border: 1px solid #3b82f6; background: #1e3a5f; }
|
|
511
520
|
.profile-list .profile-btn .sub { font-size: 11px; color: #71717a; }
|
|
512
521
|
.profile-current { padding: 12px 16px; border-bottom: 1px solid #27272a; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
513
522
|
.profile-current span { font-size: 13px; color: #a1a1aa; }
|
|
@@ -636,41 +645,72 @@ function getHtml(fixedOnly: boolean): string {
|
|
|
636
645
|
let profilesCache = null;
|
|
637
646
|
|
|
638
647
|
function showProfileCurrent() {
|
|
639
|
-
if (profileCurrentEl
|
|
648
|
+
if (profileCurrentEl) {
|
|
640
649
|
profileCurrentEl.style.display = 'flex';
|
|
641
|
-
profilePickerEl.style.display = 'none';
|
|
642
650
|
const nameEl = document.getElementById('currentProfileName');
|
|
643
651
|
if (nameEl) nameEl.textContent = selectedProfile || '';
|
|
644
652
|
}
|
|
653
|
+
if (profilePickerEl) profilePickerEl.style.display = '';
|
|
654
|
+
highlightSelectedProfile();
|
|
645
655
|
}
|
|
646
656
|
|
|
647
657
|
function showProfilePicker() {
|
|
648
|
-
if (profileCurrentEl
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
658
|
+
if (profileCurrentEl) profileCurrentEl.style.display = 'none';
|
|
659
|
+
if (profilePickerEl) profilePickerEl.style.display = '';
|
|
660
|
+
currentConv = null;
|
|
661
|
+
document.getElementById('inputArea').style.display = 'none';
|
|
662
|
+
document.getElementById('messages').innerHTML = '';
|
|
663
|
+
document.getElementById('mainHeader').innerHTML = '<strong>选择 Profile 登录</strong>';
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function highlightSelectedProfile() {
|
|
667
|
+
const listEl = document.getElementById('profileList');
|
|
668
|
+
if (!listEl) return;
|
|
669
|
+
listEl.querySelectorAll('.profile-btn').forEach(btn => {
|
|
670
|
+
btn.classList.toggle('selected', btn.dataset.id === selectedProfile);
|
|
671
|
+
});
|
|
656
672
|
}
|
|
657
673
|
|
|
658
674
|
async function loadProfiles() {
|
|
659
|
-
|
|
675
|
+
const listEl = document.getElementById('profileList');
|
|
676
|
+
if (!profilePickerEl || !listEl) return [];
|
|
660
677
|
if (profilesCache) return profilesCache;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
678
|
+
try {
|
|
679
|
+
const ctrl = new AbortController();
|
|
680
|
+
const t = setTimeout(() => ctrl.abort(), 8000);
|
|
681
|
+
const r = await fetch('/api/profiles', { signal: ctrl.signal });
|
|
682
|
+
clearTimeout(t);
|
|
683
|
+
if (!r.ok) {
|
|
684
|
+
listEl.innerHTML = '<p class="profile-loading">请求失败 ' + r.status + '</p>';
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
const data = await r.json().catch(() => ({}));
|
|
688
|
+
const list = Array.isArray(data.profiles) ? data.profiles : [];
|
|
689
|
+
profilesCache = list;
|
|
690
|
+
if (list.length === 0) listEl.innerHTML = '<p class="profile-loading">未找到 profile</p>';
|
|
691
|
+
return profilesCache;
|
|
692
|
+
} catch (e) {
|
|
693
|
+
const msg = (e && e.name === 'AbortError') ? '请求超时' : '加载失败,请刷新';
|
|
694
|
+
listEl.innerHTML = '<p class="profile-loading">' + msg + '</p>';
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
666
697
|
}
|
|
667
698
|
|
|
668
699
|
function renderProfileList(profiles) {
|
|
669
700
|
const listEl = document.getElementById('profileList');
|
|
670
701
|
if (!listEl) return;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
702
|
+
const list = profiles || [];
|
|
703
|
+
if (list.length === 0) {
|
|
704
|
+
listEl.innerHTML = '<p class="profile-loading">未找到 profile</p>';
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
listEl.innerHTML = list.map(p => {
|
|
708
|
+
var s = (p.server && typeof p.server === 'string') ? p.server : '';
|
|
709
|
+
var serverLabel = s.indexOf('://') >= 0 ? s.split('://')[1].split('/')[0] : (s || 'local');
|
|
710
|
+
var didShort = (p.did && typeof p.did === 'string') ? p.did.slice(0, 24) + '...' : '';
|
|
711
|
+
var sel = p.id === selectedProfile ? ' selected' : '';
|
|
712
|
+
return '<button class="profile-btn' + sel + '" data-id="' + p.id + '">' + p.id + '<div class="sub">' + serverLabel + ' \u00B7 ' + didShort + '<' + '/div><' + '/button>';
|
|
713
|
+
}).join('');
|
|
674
714
|
listEl.querySelectorAll('.profile-btn').forEach(btn => {
|
|
675
715
|
btn.addEventListener('click', async () => {
|
|
676
716
|
selectedProfile = btn.dataset.id || null;
|
|
@@ -1084,19 +1124,21 @@ function getHtml(fixedOnly: boolean): string {
|
|
|
1084
1124
|
}
|
|
1085
1125
|
const profiles = await loadProfiles();
|
|
1086
1126
|
if (profiles.length === 0) {
|
|
1087
|
-
document.getElementById('mainHeader')
|
|
1127
|
+
const header = document.getElementById('mainHeader');
|
|
1128
|
+
if (header) header.innerHTML = '<strong>无可用 Profile,请先 pingagent init</strong>';
|
|
1129
|
+
renderProfileList([]);
|
|
1088
1130
|
return;
|
|
1089
1131
|
}
|
|
1090
1132
|
if (profiles.length === 1 && !selectedProfile) {
|
|
1091
1133
|
selectedProfile = profiles[0].id;
|
|
1092
1134
|
sessionStorage.setItem('pingagent_web_profile', selectedProfile);
|
|
1093
1135
|
}
|
|
1136
|
+
renderProfileList(profiles);
|
|
1094
1137
|
if (selectedProfile && profiles.some(p => p.id === selectedProfile)) {
|
|
1095
1138
|
showProfileCurrent();
|
|
1096
1139
|
await loadDataForProfile();
|
|
1097
1140
|
return;
|
|
1098
1141
|
}
|
|
1099
|
-
renderProfileList(profiles);
|
|
1100
1142
|
showProfilePicker();
|
|
1101
1143
|
}
|
|
1102
1144
|
init();
|
package/src/ws-subscription.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface WsSubscriptionOptions {
|
|
|
19
19
|
onMessage: (envelope: any, conversationId: string) => void;
|
|
20
20
|
onControl?: (control: WsControlPayload, conversationId: string) => void;
|
|
21
21
|
onError?: (err: Error) => void;
|
|
22
|
+
/** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
|
|
23
|
+
onOpen?: (conversationId: string) => void;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const RECONNECT_BASE_MS = 1000;
|
|
@@ -106,6 +108,7 @@ export class WsSubscription {
|
|
|
106
108
|
|
|
107
109
|
ws.on('open', () => {
|
|
108
110
|
this.reconnectAttempts.set(conversationId, 0);
|
|
111
|
+
this.opts.onOpen?.(conversationId);
|
|
109
112
|
// ws_connected will arrive as first message
|
|
110
113
|
});
|
|
111
114
|
|