@pingagent/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/__tests__/cli.test.ts +225 -0
- package/__tests__/identity.test.ts +47 -0
- package/__tests__/store.test.ts +332 -0
- package/bin/pingagent.js +1125 -0
- package/dist/chunk-4SRPVWK4.js +1316 -0
- package/dist/chunk-AI3NCJTH.js +1289 -0
- package/dist/chunk-BGMBF5ZM.js +1222 -0
- package/dist/chunk-DN7SKV2D.js +1237 -0
- package/dist/chunk-JQVK24KX.js +1231 -0
- package/dist/chunk-O4EHWIKD.js +1291 -0
- package/dist/chunk-PXMADBHD.js +1275 -0
- package/dist/chunk-TMAANDH6.js +1240 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +38 -0
- package/dist/web-server.d.ts +26 -0
- package/dist/web-server.js +1036 -0
- package/package.json +46 -0
- package/src/a2a-adapter.ts +159 -0
- package/src/auth.ts +50 -0
- package/src/client.ts +580 -0
- package/src/contacts.ts +210 -0
- package/src/history.ts +227 -0
- package/src/identity.ts +86 -0
- package/src/index.ts +25 -0
- package/src/paths.ts +52 -0
- package/src/store.ts +62 -0
- package/src/transport.ts +141 -0
- package/src/web-server.ts +1106 -0
- package/src/ws-subscription.ts +198 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ContactManager,
|
|
3
|
+
LocalStore,
|
|
4
|
+
PingAgentClient,
|
|
5
|
+
ensureTokenValid,
|
|
6
|
+
loadIdentity,
|
|
7
|
+
updateStoredToken
|
|
8
|
+
} from "./chunk-4SRPVWK4.js";
|
|
9
|
+
|
|
10
|
+
// src/web-server.ts
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as http from "http";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { SCHEMA_TEXT } from "@pingagent/schemas";
|
|
15
|
+
var DEFAULT_PORT = 3846;
|
|
16
|
+
var DEFAULT_ROOT = "~/.pingagent";
|
|
17
|
+
function resolvePath(p) {
|
|
18
|
+
if (!p || !p.startsWith("~")) return p;
|
|
19
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(1));
|
|
20
|
+
}
|
|
21
|
+
function listProfiles(rootDir) {
|
|
22
|
+
const root = resolvePath(rootDir);
|
|
23
|
+
const profiles = [];
|
|
24
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return profiles;
|
|
25
|
+
const defaultIdentity = path.join(root, "identity.json");
|
|
26
|
+
if (fs.existsSync(defaultIdentity)) {
|
|
27
|
+
try {
|
|
28
|
+
const id = loadIdentity(defaultIdentity);
|
|
29
|
+
profiles.push({
|
|
30
|
+
id: "default",
|
|
31
|
+
did: id.did,
|
|
32
|
+
identityPath: defaultIdentity,
|
|
33
|
+
storePath: path.join(root, "store.db")
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
profiles.push({ id: "default", identityPath: defaultIdentity, storePath: path.join(root, "store.db") });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const profilesDir = path.join(root, "profiles");
|
|
40
|
+
if (fs.existsSync(profilesDir) && fs.statSync(profilesDir).isDirectory()) {
|
|
41
|
+
for (const name of fs.readdirSync(profilesDir)) {
|
|
42
|
+
const sub = path.join(profilesDir, name);
|
|
43
|
+
if (!fs.statSync(sub).isDirectory()) continue;
|
|
44
|
+
const idPath = path.join(sub, "identity.json");
|
|
45
|
+
if (fs.existsSync(idPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const id = loadIdentity(idPath);
|
|
48
|
+
profiles.push({ id: name, did: id.did, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
49
|
+
} catch {
|
|
50
|
+
profiles.push({ id: name, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let names;
|
|
56
|
+
try {
|
|
57
|
+
names = fs.readdirSync(root);
|
|
58
|
+
} catch {
|
|
59
|
+
return profiles;
|
|
60
|
+
}
|
|
61
|
+
for (const name of names) {
|
|
62
|
+
if (name === "profiles" || name === "identity.json" || name === "store.db") continue;
|
|
63
|
+
const sub = path.join(root, name);
|
|
64
|
+
if (!fs.statSync(sub).isDirectory()) continue;
|
|
65
|
+
const idPath = path.join(sub, "identity.json");
|
|
66
|
+
if (fs.existsSync(idPath) && !profiles.some((p) => p.id === name)) {
|
|
67
|
+
try {
|
|
68
|
+
const id = loadIdentity(idPath);
|
|
69
|
+
profiles.push({ id: name, did: id.did, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
70
|
+
} catch {
|
|
71
|
+
profiles.push({ id: name, identityPath: idPath, storePath: path.join(sub, "store.db") });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return profiles;
|
|
76
|
+
}
|
|
77
|
+
async function getContextForProfile(profile, serverUrl) {
|
|
78
|
+
await ensureTokenValid(profile.identityPath, serverUrl);
|
|
79
|
+
const identity = loadIdentity(profile.identityPath);
|
|
80
|
+
const store = new LocalStore(profile.storePath);
|
|
81
|
+
const contactManager = new ContactManager(store);
|
|
82
|
+
const client = new PingAgentClient({
|
|
83
|
+
serverUrl,
|
|
84
|
+
identity,
|
|
85
|
+
accessToken: identity.accessToken ?? "",
|
|
86
|
+
store,
|
|
87
|
+
onTokenRefreshed: (token, expiresAt) => updateStoredToken(token, expiresAt, profile.identityPath)
|
|
88
|
+
});
|
|
89
|
+
return { client, contactManager, myDid: identity.did };
|
|
90
|
+
}
|
|
91
|
+
var clientCache = /* @__PURE__ */ new Map();
|
|
92
|
+
async function startWebServer(opts) {
|
|
93
|
+
const port = opts.port ?? DEFAULT_PORT;
|
|
94
|
+
const serverUrl = opts.serverUrl;
|
|
95
|
+
const rootDir = opts.rootDir ? resolvePath(opts.rootDir) : resolvePath(DEFAULT_ROOT);
|
|
96
|
+
let profiles;
|
|
97
|
+
let fixedContext = null;
|
|
98
|
+
if (opts.fixedIdentityPath && opts.fixedStorePath) {
|
|
99
|
+
const identityPath = resolvePath(opts.fixedIdentityPath);
|
|
100
|
+
const storePath = resolvePath(opts.fixedStorePath);
|
|
101
|
+
fixedContext = await getContextForProfile({ id: "fixed", identityPath, storePath }, serverUrl);
|
|
102
|
+
profiles = [{ id: "fixed", did: fixedContext.myDid, identityPath, storePath }];
|
|
103
|
+
} else {
|
|
104
|
+
profiles = listProfiles(opts.rootDir ?? DEFAULT_ROOT);
|
|
105
|
+
if (profiles.length === 0) {
|
|
106
|
+
throw new Error(`No identity found in ${rootDir}. Run: pingagent init`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const html = getHtml(!!opts.fixedIdentityPath);
|
|
110
|
+
const server = http.createServer(async (req, res) => {
|
|
111
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
112
|
+
const pathname = url.pathname;
|
|
113
|
+
const raw = url.searchParams.get("profile") || req.headers["x-profile"];
|
|
114
|
+
const profileId = Array.isArray(raw) ? raw[0] : raw;
|
|
115
|
+
const origin = req.headers.origin || "";
|
|
116
|
+
const allowOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin) ? origin : "null";
|
|
117
|
+
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
118
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
119
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Profile");
|
|
120
|
+
if (req.method === "OPTIONS") {
|
|
121
|
+
res.writeHead(204);
|
|
122
|
+
res.end();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
126
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
127
|
+
res.end(html);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (pathname.startsWith("/api/")) {
|
|
131
|
+
try {
|
|
132
|
+
if (pathname === "/api/profiles" || pathname === "/api/profiles/") {
|
|
133
|
+
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 })) }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let ctx = fixedContext;
|
|
138
|
+
if (!ctx) {
|
|
139
|
+
const pid = (typeof profileId === "string" ? profileId : null) || (profiles.length === 1 ? profiles[0].id : null);
|
|
140
|
+
if (!pid) {
|
|
141
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
142
|
+
res.end(JSON.stringify({ error: "Select a profile first (?profile=xxx)" }));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const cached = clientCache.get(pid);
|
|
146
|
+
if (cached) ctx = cached;
|
|
147
|
+
else {
|
|
148
|
+
const p = profiles.find((x) => x.id === pid);
|
|
149
|
+
if (!p) throw new Error(`Unknown profile: ${pid}`);
|
|
150
|
+
ctx = await getContextForProfile(p, serverUrl);
|
|
151
|
+
clientCache.set(pid, ctx);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const result = await handleApi(pathname, req, ctx.client, ctx.contactManager, ctx.myDid, serverUrl);
|
|
155
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
156
|
+
res.end(JSON.stringify(result));
|
|
157
|
+
} catch (err) {
|
|
158
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
159
|
+
res.end(JSON.stringify({ error: err?.message ?? "Internal error" }));
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
res.writeHead(404);
|
|
164
|
+
res.end("Not found");
|
|
165
|
+
});
|
|
166
|
+
server.listen(port, "127.0.0.1", () => {
|
|
167
|
+
console.log(`PingAgent Web: http://127.0.0.1:${port}`);
|
|
168
|
+
if (fixedContext) {
|
|
169
|
+
console.log(` DID: ${fixedContext.myDid}`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` Profiles: ${profiles.map((p) => p.id).join(", ")}`);
|
|
172
|
+
}
|
|
173
|
+
console.log(` Server: ${serverUrl}`);
|
|
174
|
+
});
|
|
175
|
+
return server;
|
|
176
|
+
}
|
|
177
|
+
async function handleApi(pathname, req, client, contactManager, myDid, serverUrl) {
|
|
178
|
+
const parts = pathname.slice(5).split("/").filter(Boolean);
|
|
179
|
+
if (parts[0] === "me") {
|
|
180
|
+
return { did: myDid, serverUrl };
|
|
181
|
+
}
|
|
182
|
+
if (parts[0] === "profile") {
|
|
183
|
+
if (req.method === "GET") {
|
|
184
|
+
const res = await client.getProfile();
|
|
185
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to get profile");
|
|
186
|
+
return res.data;
|
|
187
|
+
}
|
|
188
|
+
if (req.method === "POST") {
|
|
189
|
+
const body = await readBody(req);
|
|
190
|
+
const profile = {};
|
|
191
|
+
if (body?.display_name != null) profile.display_name = body.display_name;
|
|
192
|
+
if (body?.bio != null) profile.bio = body.bio;
|
|
193
|
+
if (Array.isArray(body?.capabilities)) profile.capabilities = body.capabilities;
|
|
194
|
+
else if (typeof body?.capabilities === "string") profile.capabilities = body.capabilities.split(",").map((s) => s.trim()).filter(Boolean);
|
|
195
|
+
if (Array.isArray(body?.tags)) profile.tags = body.tags;
|
|
196
|
+
else if (typeof body?.tags === "string") profile.tags = body.tags.split(",").map((s) => s.trim()).filter(Boolean);
|
|
197
|
+
if (typeof body?.discoverable === "boolean") profile.discoverable = body.discoverable;
|
|
198
|
+
const res = await client.updateProfile(profile);
|
|
199
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to update profile");
|
|
200
|
+
return res.data;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (parts[0] === "feed") {
|
|
204
|
+
if (parts[1] === "publish" && req.method === "POST") {
|
|
205
|
+
const body = await readBody(req);
|
|
206
|
+
const text = String(body?.text ?? "").trim();
|
|
207
|
+
if (!text) throw new Error("Missing text");
|
|
208
|
+
const res = await client.publishPost({ text, artifact_ref: body?.artifact_ref });
|
|
209
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to publish");
|
|
210
|
+
return res.data;
|
|
211
|
+
}
|
|
212
|
+
if (parts[1] === "public") {
|
|
213
|
+
const url = new URL(req.url || "", "http://x");
|
|
214
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
|
|
215
|
+
const since = url.searchParams.get("since");
|
|
216
|
+
const res = await client.listFeedPublic({ limit, since: since ? parseInt(since, 10) : void 0 });
|
|
217
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to list feed");
|
|
218
|
+
return res.data;
|
|
219
|
+
}
|
|
220
|
+
if (parts[1] === "by_did") {
|
|
221
|
+
const url = new URL(req.url || "", "http://x");
|
|
222
|
+
const did = url.searchParams.get("did") || myDid;
|
|
223
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
|
|
224
|
+
const res = await client.listFeedByDid(did, { limit });
|
|
225
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to list feed");
|
|
226
|
+
return res.data;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (parts[0] === "channels") {
|
|
230
|
+
if (!parts[1] || parts[1] === "mine") {
|
|
231
|
+
const listRes = await client.listConversations({ type: "channel" });
|
|
232
|
+
if (!listRes.ok) throw new Error(listRes.error?.message ?? "Failed to list channels");
|
|
233
|
+
return { channels: listRes.data?.conversations ?? [] };
|
|
234
|
+
}
|
|
235
|
+
if (parts[1] === "discover") {
|
|
236
|
+
const url = new URL(req.url || "", "http://x");
|
|
237
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "20", 10);
|
|
238
|
+
const q = url.searchParams.get("q") || void 0;
|
|
239
|
+
const res = await client.discoverChannels({ limit, query: q });
|
|
240
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to discover channels");
|
|
241
|
+
return res.data;
|
|
242
|
+
}
|
|
243
|
+
if (parts[1] === "create" && req.method === "POST") {
|
|
244
|
+
const body = await readBody(req);
|
|
245
|
+
const name = String(body?.name ?? "").trim();
|
|
246
|
+
if (!name) throw new Error("Missing name");
|
|
247
|
+
const res = await client.createChannel({
|
|
248
|
+
name,
|
|
249
|
+
alias: body?.alias,
|
|
250
|
+
description: body?.description,
|
|
251
|
+
discoverable: body?.discoverable
|
|
252
|
+
});
|
|
253
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to create channel");
|
|
254
|
+
return res.data;
|
|
255
|
+
}
|
|
256
|
+
if (parts[1] === "update" && req.method === "PATCH") {
|
|
257
|
+
const body = await readBody(req);
|
|
258
|
+
const alias = String(body?.alias ?? "").trim();
|
|
259
|
+
if (!alias) throw new Error("Missing alias");
|
|
260
|
+
const res = await client.updateChannel({
|
|
261
|
+
alias,
|
|
262
|
+
name: body?.name,
|
|
263
|
+
description: body?.description,
|
|
264
|
+
discoverable: body?.discoverable
|
|
265
|
+
});
|
|
266
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to update channel");
|
|
267
|
+
return res.data;
|
|
268
|
+
}
|
|
269
|
+
if (parts[1] === "delete" && req.method === "DELETE") {
|
|
270
|
+
const body = await readBody(req);
|
|
271
|
+
const alias = String(body?.alias ?? "").trim();
|
|
272
|
+
if (!alias) throw new Error("Missing alias");
|
|
273
|
+
const res = await client.deleteChannel(alias);
|
|
274
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to delete channel");
|
|
275
|
+
return res.data;
|
|
276
|
+
}
|
|
277
|
+
if (parts[1] === "join" && req.method === "POST") {
|
|
278
|
+
const body = await readBody(req);
|
|
279
|
+
const alias = String(body?.alias ?? "").trim().replace(/^@ch\//, "");
|
|
280
|
+
if (!alias) throw new Error("Missing alias");
|
|
281
|
+
const res = await client.joinChannel(alias);
|
|
282
|
+
if (!res.ok) throw new Error(res.error?.message ?? "Failed to join channel");
|
|
283
|
+
return res.data;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (parts[0] === "contacts") {
|
|
287
|
+
const list = contactManager.list();
|
|
288
|
+
return { contacts: list };
|
|
289
|
+
}
|
|
290
|
+
if (parts[0] === "conversations" && !parts[1]) {
|
|
291
|
+
const listRes = await client.listConversations({ type: "dm" });
|
|
292
|
+
if (!listRes.ok) throw new Error(listRes.error?.message ?? "Failed to list conversations");
|
|
293
|
+
const convos = listRes.data?.conversations ?? [];
|
|
294
|
+
const contacts = contactManager.list();
|
|
295
|
+
const byDid = new Map(contacts.map((c) => [c.did, c]));
|
|
296
|
+
const enriched = convos.map((c) => ({
|
|
297
|
+
...c,
|
|
298
|
+
display_name: byDid.get(c.target_did)?.display_name ?? byDid.get(c.target_did)?.alias
|
|
299
|
+
}));
|
|
300
|
+
return { conversations: enriched };
|
|
301
|
+
}
|
|
302
|
+
if (parts[0] === "conversations" && parts[1] && parts[2] === "messages") {
|
|
303
|
+
const conversationId = decodeURIComponent(parts[1]);
|
|
304
|
+
const sinceSeq = parseInt(new URL(req.url || "", "http://x").searchParams.get("since_seq") ?? "0", 10) || 0;
|
|
305
|
+
const toMsg = (m) => ({
|
|
306
|
+
message_id: m.message_id,
|
|
307
|
+
seq: m.seq,
|
|
308
|
+
sender_did: m.sender_did,
|
|
309
|
+
schema: m.schema,
|
|
310
|
+
payload: m.payload,
|
|
311
|
+
ts_ms: m.ts_ms,
|
|
312
|
+
isMe: m.sender_did === myDid
|
|
313
|
+
});
|
|
314
|
+
const seen = /* @__PURE__ */ new Set();
|
|
315
|
+
const merged = [];
|
|
316
|
+
for (const box of ["ready", "strangers"]) {
|
|
317
|
+
const fetchRes = await client.fetchInbox(conversationId, { sinceSeq, limit: 100, box });
|
|
318
|
+
if (!fetchRes.ok) continue;
|
|
319
|
+
for (const m of fetchRes.data?.messages ?? []) {
|
|
320
|
+
if (m.message_id && !seen.has(m.message_id)) {
|
|
321
|
+
seen.add(m.message_id);
|
|
322
|
+
merged.push(toMsg(m));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
merged.sort((a, b) => (a.ts_ms ?? 0) - (b.ts_ms ?? 0));
|
|
327
|
+
const hm = client.getHistoryManager();
|
|
328
|
+
if (hm) {
|
|
329
|
+
const stored = hm.list(conversationId, { limit: 100 });
|
|
330
|
+
for (const m of stored) {
|
|
331
|
+
if (!seen.has(m.message_id)) {
|
|
332
|
+
seen.add(m.message_id);
|
|
333
|
+
merged.push({
|
|
334
|
+
message_id: m.message_id,
|
|
335
|
+
seq: m.seq,
|
|
336
|
+
sender_did: m.sender_did,
|
|
337
|
+
schema: m.schema,
|
|
338
|
+
payload: m.payload,
|
|
339
|
+
ts_ms: m.ts_ms,
|
|
340
|
+
isMe: m.sender_did === myDid
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
merged.sort((a, b) => (a.ts_ms ?? 0) - (b.ts_ms ?? 0));
|
|
345
|
+
}
|
|
346
|
+
return { messages: merged };
|
|
347
|
+
}
|
|
348
|
+
if (parts[0] === "conversations" && parts[1] && parts[2] === "send" && req.method === "POST") {
|
|
349
|
+
const conversationId = decodeURIComponent(parts[1]);
|
|
350
|
+
const body = await readBody(req);
|
|
351
|
+
const text = body?.text ?? body?.message ?? "";
|
|
352
|
+
if (!text) throw new Error("Missing text or message");
|
|
353
|
+
const sendRes = await client.sendMessage(conversationId, SCHEMA_TEXT, { text });
|
|
354
|
+
if (!sendRes.ok) throw new Error(sendRes.error?.message ?? "Failed to send");
|
|
355
|
+
return { ok: true, message_id: sendRes.data?.message_id };
|
|
356
|
+
}
|
|
357
|
+
if (parts[0] === "open" && req.method === "POST") {
|
|
358
|
+
const body = await readBody(req);
|
|
359
|
+
const targetDid = String(body?.target_did ?? body?.did ?? "").trim();
|
|
360
|
+
if (!targetDid) throw new Error("Missing target_did");
|
|
361
|
+
const openRes = await client.openConversation(targetDid);
|
|
362
|
+
if (!openRes.ok) throw new Error(openRes.error?.message ?? "Failed to open conversation");
|
|
363
|
+
const conv = openRes.data;
|
|
364
|
+
if (!conv.trusted && body?.send_contact_request) {
|
|
365
|
+
const msg = typeof body?.message === "string" ? body.message : void 0;
|
|
366
|
+
await client.sendContactRequest(conv.conversation_id, msg);
|
|
367
|
+
}
|
|
368
|
+
return { conversation_id: conv.conversation_id, type: conv.type, trusted: conv.trusted };
|
|
369
|
+
}
|
|
370
|
+
throw new Error("Unknown API");
|
|
371
|
+
}
|
|
372
|
+
function readBody(req) {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
const chunks = [];
|
|
375
|
+
req.on("data", (c) => chunks.push(c));
|
|
376
|
+
req.on("end", () => {
|
|
377
|
+
try {
|
|
378
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
379
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
380
|
+
} catch (e) {
|
|
381
|
+
reject(new Error("Invalid JSON"));
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
req.on("error", reject);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function getHtml(fixedOnly) {
|
|
388
|
+
const profilePicker = fixedOnly ? "" : `
|
|
389
|
+
<div class="profile-picker" id="profilePicker">
|
|
390
|
+
<div class="profile-label">\u9009\u62E9 Profile \u767B\u5F55</div>
|
|
391
|
+
<div class="profile-list" id="profileList"></div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="profile-current" id="profileCurrent" style="display:none">
|
|
394
|
+
<span>\u5F53\u524D: <strong id="currentProfileName"></strong></span>
|
|
395
|
+
<button class="profile-switch-btn" id="switchProfileBtn">\u5207\u6362</button>
|
|
396
|
+
</div>`;
|
|
397
|
+
return `<!DOCTYPE html>
|
|
398
|
+
<html lang="zh-CN">
|
|
399
|
+
<head>
|
|
400
|
+
<meta charset="UTF-8">
|
|
401
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
402
|
+
<title>PingAgent Web - \u672C\u5730\u8C03\u8BD5\u4E0E\u5BA1\u8BA1</title>
|
|
403
|
+
<style>
|
|
404
|
+
* { box-sizing: border-box; }
|
|
405
|
+
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; background: #0f0f12; color: #e4e4e7; }
|
|
406
|
+
.layout { display: flex; height: 100vh; }
|
|
407
|
+
.sidebar { width: 280px; border-right: 1px solid #27272a; overflow-y: auto; }
|
|
408
|
+
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
409
|
+
.header { padding: 12px 16px; border-bottom: 1px solid #27272a; font-size: 13px; color: #a1a1aa; }
|
|
410
|
+
.header strong { color: #fafafa; }
|
|
411
|
+
.list { padding: 8px 0; }
|
|
412
|
+
.list-item { padding: 10px 16px; cursor: pointer; font-size: 14px; border-left: 3px solid transparent; }
|
|
413
|
+
.list-item:hover { background: #18181b; }
|
|
414
|
+
.list-item.active { background: #18181b; border-left-color: #3b82f6; }
|
|
415
|
+
.list-item .sub { font-size: 12px; color: #71717a; margin-top: 2px; }
|
|
416
|
+
.messages { flex: 1; overflow-y: auto; padding: 16px; }
|
|
417
|
+
.msg { max-width: 75%; margin-bottom: 12px; padding: 10px 14px; border-radius: 12px; font-size: 14px; }
|
|
418
|
+
.msg.me { margin-left: auto; background: #3b82f6; color: #fff; }
|
|
419
|
+
.msg.other { background: #27272a; }
|
|
420
|
+
.msg .meta { font-size: 11px; color: #71717a; margin-bottom: 4px; }
|
|
421
|
+
.msg.me .meta { color: rgba(255,255,255,0.7); }
|
|
422
|
+
.input-area { padding: 12px 16px; border-top: 1px solid #27272a; display: flex; gap: 8px; }
|
|
423
|
+
.input-area input { flex: 1; padding: 10px 14px; border: 1px solid #3f3f46; border-radius: 8px; background: #18181b; color: #fafafa; font-size: 14px; }
|
|
424
|
+
.input-area input:focus { outline: none; border-color: #3b82f6; }
|
|
425
|
+
.input-area button { padding: 10px 20px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; }
|
|
426
|
+
.input-area button:hover { background: #2563eb; }
|
|
427
|
+
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
428
|
+
.empty { padding: 24px; text-align: center; color: #71717a; font-size: 14px; }
|
|
429
|
+
.add-conv { padding: 12px 16px; border-bottom: 1px solid #27272a; }
|
|
430
|
+
.add-conv input { width: 100%; padding: 8px 12px; border: 1px solid #3f3f46; border-radius: 6px; background: #18181b; color: #fafafa; font-size: 13px; }
|
|
431
|
+
.add-conv button { margin-top: 8px; padding: 8px 12px; background: #27272a; color: #e4e4e7; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
432
|
+
.add-conv button:hover { background: #3f3f46; }
|
|
433
|
+
.error { color: #f87171; font-size: 13px; padding: 8px 0; }
|
|
434
|
+
.profile-picker { padding: 12px 16px; border-bottom: 1px solid #27272a; }
|
|
435
|
+
.profile-label { font-size: 12px; color: #71717a; margin-bottom: 8px; }
|
|
436
|
+
.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
|
+
.profile-list .profile-btn:hover { background: #3f3f46; }
|
|
438
|
+
.profile-list .profile-btn .sub { font-size: 11px; color: #71717a; }
|
|
439
|
+
.profile-current { padding: 12px 16px; border-bottom: 1px solid #27272a; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
440
|
+
.profile-current span { font-size: 13px; color: #a1a1aa; }
|
|
441
|
+
.profile-switch-btn { padding: 4px 10px; font-size: 12px; background: #27272a; color: #e4e4e7; border: none; border-radius: 6px; cursor: pointer; }
|
|
442
|
+
.profile-switch-btn:hover { background: #3f3f46; }
|
|
443
|
+
.section-label { font-size: 11px; color: #71717a; padding: 8px 16px 4px; text-transform: uppercase; }
|
|
444
|
+
.panel { display: none; flex-direction: column; flex: 1; min-height: 0; }
|
|
445
|
+
.panel.active { display: flex; }
|
|
446
|
+
.panel-content { flex: 1; overflow-y: auto; padding: 16px; }
|
|
447
|
+
.form-group { margin-bottom: 12px; }
|
|
448
|
+
.form-group label { display: block; font-size: 12px; color: #71717a; margin-bottom: 4px; }
|
|
449
|
+
.form-group input, .form-group textarea { width: 100%; padding: 8px 12px; border: 1px solid #3f3f46; border-radius: 6px; background: #18181b; color: #fafafa; font-size: 13px; }
|
|
450
|
+
.form-group textarea { min-height: 60px; resize: vertical; }
|
|
451
|
+
.btn-primary { padding: 8px 16px; background: #3b82f6; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
452
|
+
.btn-primary:hover { background: #2563eb; }
|
|
453
|
+
.btn-secondary { padding: 8px 16px; background: #27272a; color: #e4e4e7; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
|
|
454
|
+
.btn-secondary:hover { background: #3f3f46; }
|
|
455
|
+
.nav-item { padding: 10px 16px; cursor: pointer; font-size: 14px; border-left: 3px solid transparent; display: flex; align-items: center; gap: 8px; }
|
|
456
|
+
.nav-item:hover { background: #18181b; }
|
|
457
|
+
.nav-item.active { background: #18181b; border-left-color: #3b82f6; }
|
|
458
|
+
</style>
|
|
459
|
+
</head>
|
|
460
|
+
<body>
|
|
461
|
+
<div class="layout">
|
|
462
|
+
<div class="sidebar">
|
|
463
|
+
<div class="header"><strong>PingAgent Web</strong><br>\u672C\u5730\u8C03\u8BD5\u4E0E\u5BA1\u8BA1</div>
|
|
464
|
+
${profilePicker}
|
|
465
|
+
<div class="add-conv">
|
|
466
|
+
<input type="text" id="newDid" placeholder="\u8F93\u5165 DID \u6216\u522B\u540D\u65B0\u5EFA\u4F1A\u8BDD">
|
|
467
|
+
<button id="openConv">\u6253\u5F00\u4F1A\u8BDD</button>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="section-label">\u4F1A\u8BDD</div>
|
|
470
|
+
<div class="list" id="convList"></div>
|
|
471
|
+
<div class="section-label">\u8054\u7CFB\u4EBA</div>
|
|
472
|
+
<div class="list" id="contactList"></div>
|
|
473
|
+
<div class="section-label">\u9891\u9053</div>
|
|
474
|
+
<div class="list" id="channelList"></div>
|
|
475
|
+
<div class="add-conv">
|
|
476
|
+
<button id="discoverChannelsBtn" class="btn-secondary" style="width:100%">\u53D1\u73B0\u9891\u9053</button>
|
|
477
|
+
<button id="createChannelBtn" class="btn-secondary" style="width:100%;margin-top:4px">\u521B\u5EFA\u9891\u9053</button>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="section-label">Feed</div>
|
|
480
|
+
<div class="nav-item" id="navFeed">\u53D1\u5E03\u52A8\u6001</div>
|
|
481
|
+
<div class="section-label">Profile</div>
|
|
482
|
+
<div class="nav-item" id="navProfile">\u7F16\u8F91\u8D44\u6599</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="main">
|
|
485
|
+
<div class="header" id="mainHeader">\u9009\u62E9\u4F1A\u8BDD\u6216\u65B0\u5EFA</div>
|
|
486
|
+
<div id="dmPanel" class="panel active">
|
|
487
|
+
<div class="messages" id="messages"></div>
|
|
488
|
+
<div class="input-area" id="inputArea" style="display:none">
|
|
489
|
+
<input type="text" id="msgInput" placeholder="\u8F93\u5165\u6D88\u606F..." autocomplete="off">
|
|
490
|
+
<button id="sendBtn">\u53D1\u9001</button>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
<div id="feedPanel" class="panel">
|
|
494
|
+
<div class="panel-content">
|
|
495
|
+
<div class="form-group">
|
|
496
|
+
<label>\u53D1\u5E03 Feed</label>
|
|
497
|
+
<textarea id="feedText" placeholder="\u5206\u4EAB\u52A8\u6001..." rows="3"></textarea>
|
|
498
|
+
<button id="publishFeedBtn" class="btn-primary" style="margin-top:8px">\u53D1\u5E03</button>
|
|
499
|
+
</div>
|
|
500
|
+
<div style="margin-top:16px">
|
|
501
|
+
<div class="section-label">\u52A8\u6001\u6D41</div>
|
|
502
|
+
<div id="feedTimeline"></div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
<div id="profilePanel" class="panel">
|
|
507
|
+
<div class="panel-content">
|
|
508
|
+
<div class="form-group">
|
|
509
|
+
<label>\u6635\u79F0 (display_name)</label>
|
|
510
|
+
<input type="text" id="profileDisplayName" placeholder="\u6635\u79F0">
|
|
511
|
+
</div>
|
|
512
|
+
<div class="form-group">
|
|
513
|
+
<label>\u7B80\u4ECB (bio)</label>
|
|
514
|
+
<textarea id="profileBio" placeholder="\u7B80\u77ED\u4ECB\u7ECD" rows="3"></textarea>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="form-group">
|
|
517
|
+
<label>\u6807\u7B7E (tags, \u9017\u53F7\u5206\u9694)</label>
|
|
518
|
+
<input type="text" id="profileTags" placeholder="coding, devops">
|
|
519
|
+
</div>
|
|
520
|
+
<div class="form-group">
|
|
521
|
+
<label>\u80FD\u529B (capabilities, \u9017\u53F7\u5206\u9694)</label>
|
|
522
|
+
<input type="text" id="profileCapabilities" placeholder="coding, testing">
|
|
523
|
+
</div>
|
|
524
|
+
<button id="saveProfileBtn" class="btn-primary">\u4FDD\u5B58</button>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div id="discoverPanel" class="panel">
|
|
528
|
+
<div class="panel-content">
|
|
529
|
+
<div class="form-group">
|
|
530
|
+
<input type="text" id="discoverQuery" placeholder="\u641C\u7D22\u9891\u9053...">
|
|
531
|
+
<button id="discoverSearchBtn" class="btn-primary" style="margin-top:8px">\u641C\u7D22</button>
|
|
532
|
+
</div>
|
|
533
|
+
<div id="discoverChannelList"></div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
<div id="createChannelPanel" class="panel">
|
|
537
|
+
<div class="panel-content">
|
|
538
|
+
<div class="form-group">
|
|
539
|
+
<label>\u9891\u9053\u540D\u79F0</label>
|
|
540
|
+
<input type="text" id="createChannelName" placeholder="\u540D\u79F0" required>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="form-group">
|
|
543
|
+
<label>\u522B\u540D (\u53EF\u9009, \u5982 my-channel)</label>
|
|
544
|
+
<input type="text" id="createChannelAlias" placeholder="alias">
|
|
545
|
+
</div>
|
|
546
|
+
<div class="form-group">
|
|
547
|
+
<label>\u63CF\u8FF0 (\u53EF\u9009)</label>
|
|
548
|
+
<input type="text" id="createChannelDesc" placeholder="\u63CF\u8FF0">
|
|
549
|
+
</div>
|
|
550
|
+
<button id="createChannelSubmitBtn" class="btn-primary">\u521B\u5EFA</button>
|
|
551
|
+
<button id="createChannelBackBtn" class="btn-secondary" style="margin-left:8px">\u8FD4\u56DE</button>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
<script>
|
|
557
|
+
const API = '';
|
|
558
|
+
let me = {}, conversations = [], contacts = [], channels = [], currentConv = null, currentView = 'dm';
|
|
559
|
+
let selectedProfile = sessionStorage.getItem('pingagent_web_profile') || null;
|
|
560
|
+
const profilePickerEl = document.getElementById('profilePicker');
|
|
561
|
+
const profileCurrentEl = document.getElementById('profileCurrent');
|
|
562
|
+
const switchProfileBtn = document.getElementById('switchProfileBtn');
|
|
563
|
+
let profilesCache = null;
|
|
564
|
+
|
|
565
|
+
function showProfileCurrent() {
|
|
566
|
+
if (profileCurrentEl && profilePickerEl) {
|
|
567
|
+
profileCurrentEl.style.display = 'flex';
|
|
568
|
+
profilePickerEl.style.display = 'none';
|
|
569
|
+
const nameEl = document.getElementById('currentProfileName');
|
|
570
|
+
if (nameEl) nameEl.textContent = selectedProfile || '';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function showProfilePicker() {
|
|
575
|
+
if (profileCurrentEl && profilePickerEl) {
|
|
576
|
+
profileCurrentEl.style.display = 'none';
|
|
577
|
+
profilePickerEl.style.display = '';
|
|
578
|
+
currentConv = null;
|
|
579
|
+
document.getElementById('inputArea').style.display = 'none';
|
|
580
|
+
document.getElementById('messages').innerHTML = '';
|
|
581
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u9009\u62E9 Profile \u767B\u5F55</strong>';
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function loadProfiles() {
|
|
586
|
+
if (!profilePickerEl) return [];
|
|
587
|
+
if (profilesCache) return profilesCache;
|
|
588
|
+
const { profiles } = await fetch(API + '/api/profiles')
|
|
589
|
+
.then(r => r.json())
|
|
590
|
+
.catch(() => ({ profiles: [] }));
|
|
591
|
+
profilesCache = Array.isArray(profiles) ? profiles : [];
|
|
592
|
+
return profilesCache;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function renderProfileList(profiles) {
|
|
596
|
+
const listEl = document.getElementById('profileList');
|
|
597
|
+
if (!listEl) return;
|
|
598
|
+
listEl.innerHTML = (profiles || []).map(p =>
|
|
599
|
+
'<button class="profile-btn" data-id="' + p.id + '">' + p.id + '<div class="sub">' + (p.did || '').slice(0, 32) + '...</div></button>'
|
|
600
|
+
).join('');
|
|
601
|
+
listEl.querySelectorAll('.profile-btn').forEach(btn => {
|
|
602
|
+
btn.addEventListener('click', async () => {
|
|
603
|
+
selectedProfile = btn.dataset.id || null;
|
|
604
|
+
if (selectedProfile) sessionStorage.setItem('pingagent_web_profile', selectedProfile);
|
|
605
|
+
showProfileCurrent();
|
|
606
|
+
await loadDataForProfile();
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function showPanel(panelId) {
|
|
612
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
613
|
+
const el = document.getElementById(panelId);
|
|
614
|
+
if (el) el.classList.add('active');
|
|
615
|
+
currentView = panelId.replace('Panel', '');
|
|
616
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
617
|
+
const navMap = { feed: 'navFeed', profile: 'navProfile' };
|
|
618
|
+
const nav = document.getElementById(navMap[currentView]);
|
|
619
|
+
if (nav) nav.classList.add('active');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function loadDataForProfile() {
|
|
623
|
+
const headerEl = document.getElementById('mainHeader');
|
|
624
|
+
try {
|
|
625
|
+
await loadMe();
|
|
626
|
+
await loadConversations();
|
|
627
|
+
await loadContacts();
|
|
628
|
+
await loadChannels();
|
|
629
|
+
} catch (e) {
|
|
630
|
+
const msg = (e && e.message) ? e.message : '\u52A0\u8F7D\u5931\u8D25';
|
|
631
|
+
if (headerEl) headerEl.innerHTML = '<span class="error">' + msg.replace(/</g, '<') + '</span>';
|
|
632
|
+
conversations = [];
|
|
633
|
+
contacts = [];
|
|
634
|
+
channels = [];
|
|
635
|
+
renderConvList();
|
|
636
|
+
renderContactList();
|
|
637
|
+
renderChannelList();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function switchProfile() {
|
|
642
|
+
selectedProfile = null;
|
|
643
|
+
sessionStorage.removeItem('pingagent_web_profile');
|
|
644
|
+
const profiles = await loadProfiles();
|
|
645
|
+
renderProfileList(profiles);
|
|
646
|
+
showProfilePicker();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function api(path, opts = {}) {
|
|
650
|
+
let url = API + path;
|
|
651
|
+
if (profilePickerEl && selectedProfile) {
|
|
652
|
+
url += (path.includes('?') ? '&' : '?') + 'profile=' + encodeURIComponent(selectedProfile);
|
|
653
|
+
}
|
|
654
|
+
const res = await fetch(url, opts);
|
|
655
|
+
const data = await res.json().catch(() => ({}));
|
|
656
|
+
if (data.error) throw new Error(data.error);
|
|
657
|
+
return data;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function loadMe() {
|
|
661
|
+
me = await api('/api/me');
|
|
662
|
+
document.getElementById('mainHeader').innerHTML = '<strong>' + (me.did || 'Loading...').slice(0, 40) + '...</strong>';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function loadConversations() {
|
|
666
|
+
const data = await api('/api/conversations');
|
|
667
|
+
conversations = data.conversations || [];
|
|
668
|
+
renderConvList();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function loadContacts() {
|
|
672
|
+
const data = await api('/api/contacts');
|
|
673
|
+
contacts = data.contacts || [];
|
|
674
|
+
renderContactList();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function loadChannels() {
|
|
678
|
+
try {
|
|
679
|
+
const data = await api('/api/channels');
|
|
680
|
+
channels = data.channels || [];
|
|
681
|
+
renderChannelList();
|
|
682
|
+
} catch (e) {
|
|
683
|
+
channels = [];
|
|
684
|
+
renderChannelList();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function renderChannelList() {
|
|
689
|
+
const el = document.getElementById('channelList');
|
|
690
|
+
if (!el) return;
|
|
691
|
+
if (!channels.length) {
|
|
692
|
+
el.innerHTML = '<div class="empty">\u6682\u65E0\u9891\u9053</div>';
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
el.innerHTML = channels.map(c => {
|
|
696
|
+
const name = (c.target_did || c.conversation_id || '').slice(0, 24) + (c.target_did ? '...' : '');
|
|
697
|
+
const convId = (c.conversation_id || '').replace(/"/g, '"');
|
|
698
|
+
const targetDid = (c.target_did || '').replace(/"/g, '"');
|
|
699
|
+
return '<div class="list-item channel-item' + (currentConv?.conversation_id === c.conversation_id && currentConv?.isChannel ? ' active' : '') + '" data-id="' + convId + '" data-did="' + targetDid + '">\u9891\u9053 ' + name + '</div>';
|
|
700
|
+
}).join('');
|
|
701
|
+
el.querySelectorAll('.channel-item').forEach(n => {
|
|
702
|
+
n.addEventListener('click', () => selectChannel(n.dataset.id, n.dataset.did));
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function selectChannel(convId, targetDid) {
|
|
707
|
+
currentConv = { conversation_id: convId, target_did: targetDid, isChannel: true };
|
|
708
|
+
renderConvList();
|
|
709
|
+
renderChannelList();
|
|
710
|
+
showPanel('dmPanel');
|
|
711
|
+
document.getElementById('inputArea').style.display = 'flex';
|
|
712
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u9891\u9053 ' + (targetDid || convId).slice(0, 40) + '</strong>';
|
|
713
|
+
await loadMessages(convId);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function loadProfile() {
|
|
717
|
+
try {
|
|
718
|
+
const p = await api('/api/profile');
|
|
719
|
+
document.getElementById('profileDisplayName').value = p.display_name || '';
|
|
720
|
+
document.getElementById('profileBio').value = p.bio || '';
|
|
721
|
+
document.getElementById('profileTags').value = (p.tags || []).join(', ');
|
|
722
|
+
document.getElementById('profileCapabilities').value = (p.capabilities || []).join(', ');
|
|
723
|
+
} catch (e) {
|
|
724
|
+
document.getElementById('profilePanel').querySelector('.panel-content').innerHTML = '<div class="error">\u52A0\u8F7D\u5931\u8D25: ' + (e.message || '') + '</div>';
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function saveProfile() {
|
|
729
|
+
try {
|
|
730
|
+
const display_name = document.getElementById('profileDisplayName').value.trim();
|
|
731
|
+
const bio = document.getElementById('profileBio').value.trim();
|
|
732
|
+
const tags = document.getElementById('profileTags').value.split(',').map(s => s.trim()).filter(Boolean);
|
|
733
|
+
const capabilities = document.getElementById('profileCapabilities').value.split(',').map(s => s.trim()).filter(Boolean);
|
|
734
|
+
await api('/api/profile', {
|
|
735
|
+
method: 'POST',
|
|
736
|
+
headers: { 'Content-Type': 'application/json' },
|
|
737
|
+
body: JSON.stringify({ display_name, bio, tags, capabilities })
|
|
738
|
+
});
|
|
739
|
+
alert('\u4FDD\u5B58\u6210\u529F');
|
|
740
|
+
} catch (e) {
|
|
741
|
+
alert('\u4FDD\u5B58\u5931\u8D25: ' + e.message);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async function publishFeed() {
|
|
746
|
+
const text = document.getElementById('feedText').value.trim();
|
|
747
|
+
if (!text) return;
|
|
748
|
+
try {
|
|
749
|
+
await api('/api/feed/publish', {
|
|
750
|
+
method: 'POST',
|
|
751
|
+
headers: { 'Content-Type': 'application/json' },
|
|
752
|
+
body: JSON.stringify({ text })
|
|
753
|
+
});
|
|
754
|
+
document.getElementById('feedText').value = '';
|
|
755
|
+
await loadFeedTimeline();
|
|
756
|
+
} catch (e) {
|
|
757
|
+
alert('\u53D1\u5E03\u5931\u8D25: ' + e.message);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function loadFeedTimeline() {
|
|
762
|
+
const el = document.getElementById('feedTimeline');
|
|
763
|
+
try {
|
|
764
|
+
const data = await api('/api/feed/by_did?did=' + encodeURIComponent(me.did || '') + '&limit=30');
|
|
765
|
+
const posts = (data.posts || []).slice(0, 30);
|
|
766
|
+
if (!posts.length) {
|
|
767
|
+
el.innerHTML = '<div class="empty">\u6682\u65E0\u52A8\u6001\uFF0C\u53BB\u53D1\u5E03\u4E00\u6761\u5427</div>';
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
el.innerHTML = posts.map(p => {
|
|
771
|
+
const time = new Date(p.ts_ms).toLocaleString();
|
|
772
|
+
const text = (p.text || '').replace(/</g, '<').replace(/>/g, '>');
|
|
773
|
+
return '<div class="msg" style="margin-bottom:12px"><div class="meta">' + time + '</div>' + text + '</div>';
|
|
774
|
+
}).join('');
|
|
775
|
+
} catch (e) {
|
|
776
|
+
el.innerHTML = '<div class="error">\u52A0\u8F7D\u5931\u8D25: ' + (e.message || '') + '</div>';
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function discoverChannels() {
|
|
781
|
+
showPanel('discoverPanel');
|
|
782
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u53D1\u73B0\u9891\u9053</strong>';
|
|
783
|
+
await doDiscoverChannels();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function doDiscoverChannels() {
|
|
787
|
+
const el = document.getElementById('discoverChannelList');
|
|
788
|
+
const q = document.getElementById('discoverQuery')?.value?.trim() || '';
|
|
789
|
+
try {
|
|
790
|
+
const url = '/api/channels/discover?limit=20' + (q ? '&q=' + encodeURIComponent(q) : '');
|
|
791
|
+
const data = await api(url);
|
|
792
|
+
const list = data.channels || [];
|
|
793
|
+
if (!list.length) {
|
|
794
|
+
el.innerHTML = '<div class="empty">\u6682\u65E0\u9891\u9053</div>';
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
el.innerHTML = list.map(ch => {
|
|
798
|
+
const alias = (ch.alias || '').replace(/"/g, '"');
|
|
799
|
+
const name = (ch.name || ch.alias || '').replace(/</g, '<').replace(/>/g, '>');
|
|
800
|
+
const desc = (ch.description || '').slice(0, 60).replace(/</g, '<');
|
|
801
|
+
return '<div class="msg" style="margin-bottom:12px"><strong>' + name + '</strong> <code>' + alias + '</code><div class="sub">' + desc + '</div><button class="btn-secondary join-channel-btn" data-alias="' + alias.replace(/@ch//g, '') + '" style="margin-top:6px">\u52A0\u5165</button></div>';
|
|
802
|
+
}).join('');
|
|
803
|
+
el.querySelectorAll('.join-channel-btn').forEach(btn => {
|
|
804
|
+
btn.addEventListener('click', async () => {
|
|
805
|
+
const alias = btn.dataset.alias;
|
|
806
|
+
try {
|
|
807
|
+
await api('/api/channels/join', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ alias }) });
|
|
808
|
+
alert('\u52A0\u5165\u6210\u529F');
|
|
809
|
+
await loadChannels();
|
|
810
|
+
showPanel('dmPanel');
|
|
811
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u9891\u9053</strong>';
|
|
812
|
+
} catch (e) {
|
|
813
|
+
alert('\u52A0\u5165\u5931\u8D25: ' + e.message);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
} catch (e) {
|
|
818
|
+
el.innerHTML = '<div class="error">\u52A0\u8F7D\u5931\u8D25: ' + (e.message || '') + '</div>';
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function showCreateChannel() {
|
|
823
|
+
showPanel('createChannelPanel');
|
|
824
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u521B\u5EFA\u9891\u9053</strong>';
|
|
825
|
+
document.getElementById('createChannelName').value = '';
|
|
826
|
+
document.getElementById('createChannelAlias').value = '';
|
|
827
|
+
document.getElementById('createChannelDesc').value = '';
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function createChannelSubmit() {
|
|
831
|
+
const name = document.getElementById('createChannelName').value.trim();
|
|
832
|
+
if (!name) { alert('\u8BF7\u8F93\u5165\u9891\u9053\u540D\u79F0'); return; }
|
|
833
|
+
const alias = document.getElementById('createChannelAlias').value.trim() || undefined;
|
|
834
|
+
const description = document.getElementById('createChannelDesc').value.trim() || undefined;
|
|
835
|
+
try {
|
|
836
|
+
const data = await api('/api/channels/create', {
|
|
837
|
+
method: 'POST',
|
|
838
|
+
headers: { 'Content-Type': 'application/json' },
|
|
839
|
+
body: JSON.stringify({ name, alias, description })
|
|
840
|
+
});
|
|
841
|
+
alert('\u521B\u5EFA\u6210\u529F');
|
|
842
|
+
await loadChannels();
|
|
843
|
+
if (data.conversation_id) selectChannel(data.conversation_id, data.alias || '');
|
|
844
|
+
showPanel('dmPanel');
|
|
845
|
+
} catch (e) {
|
|
846
|
+
alert('\u521B\u5EFA\u5931\u8D25: ' + e.message);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function renderContactList() {
|
|
851
|
+
const el = document.getElementById('contactList');
|
|
852
|
+
if (!el) return;
|
|
853
|
+
if (!contacts.length) {
|
|
854
|
+
el.innerHTML = '<div class="empty">\u6682\u65E0\u8054\u7CFB\u4EBA</div>';
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
el.innerHTML = contacts.map(c => {
|
|
858
|
+
const name = (c.display_name || c.alias || c.did || '').slice(0, 28);
|
|
859
|
+
const did = (c.did || '').replace(/"/g, '"');
|
|
860
|
+
const convId = (c.conversation_id || '').replace(/"/g, '"');
|
|
861
|
+
return '<div class="list-item contact-item" data-did="' + did + '" data-conv-id="' + convId + '">' + name + '</div>';
|
|
862
|
+
}).join('');
|
|
863
|
+
el.querySelectorAll('.contact-item').forEach(n => {
|
|
864
|
+
n.addEventListener('click', () => {
|
|
865
|
+
const did = n.dataset.did;
|
|
866
|
+
const convId = n.dataset.convId;
|
|
867
|
+
if (did) openConversationWithDid(did, convId);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function openConversationWithDid(targetDid, knownConvId) {
|
|
873
|
+
if (!targetDid) return;
|
|
874
|
+
if (knownConvId) {
|
|
875
|
+
await loadConversations();
|
|
876
|
+
selectConv(knownConvId, targetDid);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const data = await api('/api/open', {
|
|
881
|
+
method: 'POST',
|
|
882
|
+
headers: { 'Content-Type': 'application/json' },
|
|
883
|
+
body: JSON.stringify({ target_did: targetDid, send_contact_request: false })
|
|
884
|
+
});
|
|
885
|
+
if (data.conversation_id) {
|
|
886
|
+
await loadConversations();
|
|
887
|
+
selectConv(data.conversation_id, targetDid);
|
|
888
|
+
}
|
|
889
|
+
} catch (e) {
|
|
890
|
+
alert('\u6253\u5F00\u4F1A\u8BDD\u5931\u8D25: ' + e.message);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function renderConvList() {
|
|
895
|
+
const el = document.getElementById('convList');
|
|
896
|
+
if (!conversations.length) {
|
|
897
|
+
el.innerHTML = '<div class="empty">\u6682\u65E0\u4F1A\u8BDD<br>\u8F93\u5165 DID \u65B0\u5EFA</div>';
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
el.innerHTML = conversations.map(c => {
|
|
901
|
+
const name = c.display_name || c.target_did?.slice(0, 24) + '...';
|
|
902
|
+
const sub = c.trusted ? 'DM' : '\u5F85\u6279\u51C6';
|
|
903
|
+
return '<div class="list-item' + (currentConv?.conversation_id === c.conversation_id ? ' active' : '') + '" data-id="' + c.conversation_id + '" data-did="' + (c.target_did||'') + '">' + name + '<div class="sub">' + sub + '</div></div>';
|
|
904
|
+
}).join('');
|
|
905
|
+
el.querySelectorAll('.list-item').forEach(n => n.addEventListener('click', () => selectConv(n.dataset.id, n.dataset.did)));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function selectConv(convId, targetDid) {
|
|
909
|
+
currentConv = { conversation_id: convId, target_did: targetDid, isChannel: false };
|
|
910
|
+
renderConvList();
|
|
911
|
+
renderChannelList();
|
|
912
|
+
showPanel('dmPanel');
|
|
913
|
+
document.getElementById('inputArea').style.display = 'flex';
|
|
914
|
+
document.getElementById('mainHeader').innerHTML = '<strong>' + (targetDid || convId).slice(0, 50) + '</strong>';
|
|
915
|
+
await loadMessages(convId);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function loadMessages(convId, since) {
|
|
919
|
+
const el = document.getElementById('messages');
|
|
920
|
+
if (!since) el.innerHTML = '';
|
|
921
|
+
try {
|
|
922
|
+
const data = await api('/api/conversations/' + encodeURIComponent(convId) + '/messages' + (since ? '?since_seq=' + since : ''));
|
|
923
|
+
const msgs = data.messages || [];
|
|
924
|
+
msgs.forEach(m => {
|
|
925
|
+
const div = document.createElement('div');
|
|
926
|
+
div.className = 'msg ' + (m.isMe ? 'me' : 'other');
|
|
927
|
+
const meta = new Date(m.ts_ms).toLocaleString() + ' ' + (m.isMe ? '(\u6211)' : (m.sender_did || '').slice(0, 16) + '...');
|
|
928
|
+
let body = '';
|
|
929
|
+
if (m.schema === 'pingagent.text@1' && m.payload?.text) body = m.payload.text;
|
|
930
|
+
else body = JSON.stringify(m.payload || m).slice(0, 200);
|
|
931
|
+
div.innerHTML = '<div class="meta">' + meta + '</div>' + body.replace(/</g, '<').replace(/>/g, '>');
|
|
932
|
+
el.appendChild(div);
|
|
933
|
+
});
|
|
934
|
+
if (!since && msgs.length === 0) el.innerHTML = '<div class="empty">\u6682\u65E0\u6D88\u606F</div>';
|
|
935
|
+
el.scrollTop = el.scrollHeight;
|
|
936
|
+
} catch (e) {
|
|
937
|
+
el.innerHTML = '<div class="error">\u52A0\u8F7D\u5931\u8D25: ' + (e.message || '\u672A\u77E5\u9519\u8BEF') + '</div>';
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function sendMessage() {
|
|
942
|
+
const input = document.getElementById('msgInput');
|
|
943
|
+
const text = input.value.trim();
|
|
944
|
+
if (!text || !currentConv) return;
|
|
945
|
+
try {
|
|
946
|
+
await api('/api/conversations/' + encodeURIComponent(currentConv.conversation_id) + '/send', {
|
|
947
|
+
method: 'POST',
|
|
948
|
+
headers: { 'Content-Type': 'application/json' },
|
|
949
|
+
body: JSON.stringify({ text })
|
|
950
|
+
});
|
|
951
|
+
input.value = '';
|
|
952
|
+
await loadMessages(currentConv.conversation_id);
|
|
953
|
+
} catch (e) {
|
|
954
|
+
document.getElementById('messages').innerHTML += '<div class="error">\u53D1\u9001\u5931\u8D25: ' + e.message + '</div>';
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function openConversation() {
|
|
959
|
+
const input = document.getElementById('newDid');
|
|
960
|
+
const targetDid = input.value.trim();
|
|
961
|
+
if (!targetDid) return;
|
|
962
|
+
try {
|
|
963
|
+
const data = await api('/api/open', {
|
|
964
|
+
method: 'POST',
|
|
965
|
+
headers: { 'Content-Type': 'application/json' },
|
|
966
|
+
body: JSON.stringify({ target_did: targetDid, send_contact_request: true })
|
|
967
|
+
});
|
|
968
|
+
input.value = '';
|
|
969
|
+
await loadConversations();
|
|
970
|
+
if (data.conversation_id) selectConv(data.conversation_id, targetDid);
|
|
971
|
+
} catch (e) {
|
|
972
|
+
alert('\u6253\u5F00\u5931\u8D25: ' + e.message);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
document.getElementById('openConv').addEventListener('click', openConversation);
|
|
977
|
+
document.getElementById('newDid').addEventListener('keydown', e => { if (e.key === 'Enter') openConversation(); });
|
|
978
|
+
document.getElementById('sendBtn').addEventListener('click', sendMessage);
|
|
979
|
+
document.getElementById('msgInput').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } });
|
|
980
|
+
document.getElementById('navFeed')?.addEventListener('click', () => { showPanel('feedPanel'); document.getElementById('mainHeader').innerHTML = '<strong>Feed</strong>'; loadFeedTimeline(); });
|
|
981
|
+
document.getElementById('navProfile')?.addEventListener('click', () => { showPanel('profilePanel'); document.getElementById('mainHeader').innerHTML = '<strong>\u7F16\u8F91 Profile</strong>'; loadProfile(); });
|
|
982
|
+
document.getElementById('discoverChannelsBtn')?.addEventListener('click', discoverChannels);
|
|
983
|
+
document.getElementById('createChannelBtn')?.addEventListener('click', showCreateChannel);
|
|
984
|
+
document.getElementById('publishFeedBtn')?.addEventListener('click', publishFeed);
|
|
985
|
+
document.getElementById('saveProfileBtn')?.addEventListener('click', saveProfile);
|
|
986
|
+
document.getElementById('discoverSearchBtn')?.addEventListener('click', doDiscoverChannels);
|
|
987
|
+
document.getElementById('createChannelSubmitBtn')?.addEventListener('click', createChannelSubmit);
|
|
988
|
+
document.getElementById('createChannelBackBtn')?.addEventListener('click', () => { showPanel('discoverPanel'); document.getElementById('mainHeader').innerHTML = '<strong>\u53D1\u73B0\u9891\u9053</strong>'; });
|
|
989
|
+
|
|
990
|
+
function showProfileCurrent() {
|
|
991
|
+
if (!profilePickerEl) return;
|
|
992
|
+
profilePickerEl.style.display = 'none';
|
|
993
|
+
if (profileCurrentEl) {
|
|
994
|
+
profileCurrentEl.style.display = 'flex';
|
|
995
|
+
const nameEl = document.getElementById('currentProfileName');
|
|
996
|
+
if (nameEl) nameEl.textContent = selectedProfile || '';
|
|
997
|
+
}
|
|
998
|
+
if (switchProfileBtn) {
|
|
999
|
+
switchProfileBtn.onclick = async () => {
|
|
1000
|
+
await switchProfile();
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async function init() {
|
|
1006
|
+
if (!profilePickerEl) {
|
|
1007
|
+
await loadMe();
|
|
1008
|
+
await loadConversations();
|
|
1009
|
+
await loadContacts();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const profiles = await loadProfiles();
|
|
1013
|
+
if (profiles.length === 0) {
|
|
1014
|
+
document.getElementById('mainHeader').innerHTML = '<strong>\u65E0\u53EF\u7528 Profile\uFF0C\u8BF7\u5148 pingagent init</strong>';
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (profiles.length === 1 && !selectedProfile) {
|
|
1018
|
+
selectedProfile = profiles[0].id;
|
|
1019
|
+
sessionStorage.setItem('pingagent_web_profile', selectedProfile);
|
|
1020
|
+
}
|
|
1021
|
+
if (selectedProfile && profiles.some(p => p.id === selectedProfile)) {
|
|
1022
|
+
showProfileCurrent();
|
|
1023
|
+
await loadDataForProfile();
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
renderProfileList(profiles);
|
|
1027
|
+
showProfilePicker();
|
|
1028
|
+
}
|
|
1029
|
+
init();
|
|
1030
|
+
</script>
|
|
1031
|
+
</body>
|
|
1032
|
+
</html>`;
|
|
1033
|
+
}
|
|
1034
|
+
export {
|
|
1035
|
+
startWebServer
|
|
1036
|
+
};
|