@yeaft/webchat-agent 1.0.268 → 1.0.270
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/connection/index.js +12 -2
- package/connection/message-router.js +15 -13
- package/local-runtime/server/database.js +1 -0
- package/local-runtime/server/db/connection.js +48 -0
- package/local-runtime/server/db/session-db.js +5 -0
- package/local-runtime/server/db/session-ui-metadata-db.js +78 -0
- package/local-runtime/server/db/yeaft-session-db.js +22 -11
- package/local-runtime/server/handlers/agent-output.js +7 -1
- package/local-runtime/server/handlers/client-conversation.js +232 -58
- package/local-runtime/server/handlers/client-workbench.js +2 -2
- package/local-runtime/server/handlers/session-pin-router.js +7 -6
- package/local-runtime/server/session-catalog.js +91 -0
- package/local-runtime/server/ws-utils.js +78 -5
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +185 -85
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
package/connection/index.js
CHANGED
|
@@ -4,7 +4,17 @@ import { sendToServer, parseMessage } from './buffer.js';
|
|
|
4
4
|
import { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './heartbeat.js';
|
|
5
5
|
import { handleMessage } from './message-router.js';
|
|
6
6
|
|
|
7
|
-
export function
|
|
7
|
+
export function resetConnectionTransport() {
|
|
8
|
+
ctx.sessionKey = null;
|
|
9
|
+
ctx.serverEncryptionRequired = true;
|
|
10
|
+
ctx.pendingAuthTempId = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function connect(WebSocketImpl = WebSocket) {
|
|
14
|
+
// Transport negotiation is connection-scoped. Always start conservatively;
|
|
15
|
+
// only the registered frame from this socket may enable plaintext outbound.
|
|
16
|
+
resetConnectionTransport();
|
|
17
|
+
|
|
8
18
|
// Don't include secret in URL - it will be sent via WebSocket message after connection.
|
|
9
19
|
// instanceId is the stable local service identity; agentName is display-only.
|
|
10
20
|
// Old configs without instanceId still use agentName for backward-compatible identity.
|
|
@@ -24,7 +34,7 @@ export function connect() {
|
|
|
24
34
|
console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
const socket = new
|
|
37
|
+
const socket = new WebSocketImpl(url, {
|
|
28
38
|
// Match server's permessage-deflate config (bounded memory,
|
|
29
39
|
// skip compression for small frames). The `ws` library handles
|
|
30
40
|
// streaming compression so we no longer need the synchronous
|
|
@@ -79,22 +79,24 @@ export async function applyLlmConfigUpdate(msg, dependencies = {}) {
|
|
|
79
79
|
return response;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function applyRegisteredTransport(msg) {
|
|
83
|
+
if (msg.sessionKey) {
|
|
84
|
+
ctx.sessionKey = decodeKey(msg.sessionKey);
|
|
85
|
+
console.log('Encryption enabled');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// New servers advertise plaintext acceptance. This mutation is scoped to
|
|
89
|
+
// the active connection because connect() restores conservative defaults.
|
|
90
|
+
if (msg.acceptPlaintext === true) {
|
|
91
|
+
ctx.serverEncryptionRequired = false;
|
|
92
|
+
console.log('[WS] Server accepts plaintext, disabling outbound encryption');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
export async function handleMessage(msg) {
|
|
83
97
|
switch (msg.type) {
|
|
84
98
|
case 'registered':
|
|
85
|
-
|
|
86
|
-
ctx.sessionKey = decodeKey(msg.sessionKey);
|
|
87
|
-
console.log('Encryption enabled');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// feat-ws-plaintext-negotiation: new server advertises that it
|
|
91
|
-
// will accept plaintext frames from us. Stop encrypting outbound.
|
|
92
|
-
// The receive path (parseMessage) stays unconditional so the old
|
|
93
|
-
// ciphertext that may already be in flight still decrypts.
|
|
94
|
-
if (msg.acceptPlaintext === true) {
|
|
95
|
-
ctx.serverEncryptionRequired = false;
|
|
96
|
-
console.log('[WS] Server accepts plaintext, disabling outbound encryption');
|
|
97
|
-
}
|
|
99
|
+
applyRegisteredTransport(msg);
|
|
98
100
|
|
|
99
101
|
// 只保存基本配置。instanceId 是本地服务实例身份;agentName 只用于展示。
|
|
100
102
|
ctx.saveConfig({
|
|
@@ -4,6 +4,7 @@ export { userDb } from './db/user-db.js';
|
|
|
4
4
|
export { invitationDb } from './db/invitation-db.js';
|
|
5
5
|
export { sessionDb } from './db/session-db.js';
|
|
6
6
|
export { yeaftSessionDb } from './db/yeaft-session-db.js';
|
|
7
|
+
export { sessionUiMetadataDb } from './db/session-ui-metadata-db.js';
|
|
7
8
|
export { messageDb } from './db/message-db.js';
|
|
8
9
|
export { userStatsDb } from './db/user-stats-db.js';
|
|
9
10
|
export { expertDb } from './db/expert-db.js';
|
|
@@ -227,6 +227,19 @@ const yeaftSessionsTable = `
|
|
|
227
227
|
`;
|
|
228
228
|
db.exec(yeaftSessionsTable);
|
|
229
229
|
|
|
230
|
+
db.exec(`
|
|
231
|
+
CREATE TABLE IF NOT EXISTS session_ui_metadata (
|
|
232
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
233
|
+
catalog_key TEXT NOT NULL,
|
|
234
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
235
|
+
sort_rank INTEGER,
|
|
236
|
+
updated_at INTEGER NOT NULL,
|
|
237
|
+
PRIMARY KEY (user_id, catalog_key)
|
|
238
|
+
);
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_session_ui_metadata_user_sort
|
|
240
|
+
ON session_ui_metadata(user_id, pinned DESC, sort_rank ASC);
|
|
241
|
+
`);
|
|
242
|
+
|
|
230
243
|
try {
|
|
231
244
|
const tableInfo = db.prepare(`PRAGMA table_info(yeaft_sessions)`).all();
|
|
232
245
|
const idColumn = tableInfo.find(col => col && col.name === 'id');
|
|
@@ -538,6 +551,11 @@ export const stmts = {
|
|
|
538
551
|
UPDATE sessions SET is_pinned = ?, updated_at = ? WHERE id = ?
|
|
539
552
|
`),
|
|
540
553
|
|
|
554
|
+
updateSessionPinnedForRoute: db.prepare(`
|
|
555
|
+
UPDATE sessions SET is_pinned = ?, updated_at = ?
|
|
556
|
+
WHERE id = ? AND agent_id = ? AND (user_id = ? OR user_id IS NULL)
|
|
557
|
+
`),
|
|
558
|
+
|
|
541
559
|
// fix-copilot-provider-persist: persist the conversation's code-agent
|
|
542
560
|
// provider so it survives an agent process restart. Mirrors the pinned/
|
|
543
561
|
// agent update shape. Only written when a non-default provider is known
|
|
@@ -577,6 +595,10 @@ export const stmts = {
|
|
|
577
595
|
SELECT * FROM sessions WHERE user_id = ? AND agent_id = ? ORDER BY updated_at DESC LIMIT ?
|
|
578
596
|
`),
|
|
579
597
|
|
|
598
|
+
hasSessionOwnedByUserAndAgent: db.prepare(`
|
|
599
|
+
SELECT 1 FROM sessions WHERE user_id = ? AND agent_id = ? LIMIT 1
|
|
600
|
+
`),
|
|
601
|
+
|
|
580
602
|
getAllSessions: db.prepare(`
|
|
581
603
|
SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?
|
|
582
604
|
`),
|
|
@@ -593,6 +615,28 @@ export const stmts = {
|
|
|
593
615
|
DELETE FROM sessions WHERE id = ?
|
|
594
616
|
`),
|
|
595
617
|
|
|
618
|
+
upsertSessionUiMetadata: db.prepare(`
|
|
619
|
+
INSERT INTO session_ui_metadata (user_id, catalog_key, pinned, sort_rank, updated_at)
|
|
620
|
+
VALUES (?, ?, ?, ?, ?)
|
|
621
|
+
ON CONFLICT(user_id, catalog_key) DO UPDATE SET
|
|
622
|
+
pinned = excluded.pinned,
|
|
623
|
+
sort_rank = excluded.sort_rank,
|
|
624
|
+
updated_at = excluded.updated_at
|
|
625
|
+
`),
|
|
626
|
+
|
|
627
|
+
getSessionUiMetadata: db.prepare(`
|
|
628
|
+
SELECT * FROM session_ui_metadata WHERE user_id = ? AND catalog_key = ?
|
|
629
|
+
`),
|
|
630
|
+
|
|
631
|
+
getSessionUiMetadataByUser: db.prepare(`
|
|
632
|
+
SELECT * FROM session_ui_metadata WHERE user_id = ?
|
|
633
|
+
ORDER BY pinned DESC, sort_rank ASC, updated_at DESC
|
|
634
|
+
`),
|
|
635
|
+
|
|
636
|
+
deleteSessionUiMetadata: db.prepare(`
|
|
637
|
+
DELETE FROM session_ui_metadata WHERE user_id = ? AND catalog_key = ?
|
|
638
|
+
`),
|
|
639
|
+
|
|
596
640
|
// Message 操作
|
|
597
641
|
insertMessage: db.prepare(`
|
|
598
642
|
INSERT INTO messages (session_id, role, content, message_type, tool_name, tool_input, created_at, metadata)
|
|
@@ -853,6 +897,10 @@ export const stmts = {
|
|
|
853
897
|
SELECT * FROM yeaft_sessions WHERE id = ? ORDER BY updated_at DESC LIMIT 1
|
|
854
898
|
`),
|
|
855
899
|
|
|
900
|
+
getYeaftSessionsById: db.prepare(`
|
|
901
|
+
SELECT * FROM yeaft_sessions WHERE id = ? ORDER BY updated_at DESC
|
|
902
|
+
`),
|
|
903
|
+
|
|
856
904
|
getYeaftSessionForAgent: db.prepare(`
|
|
857
905
|
SELECT * FROM yeaft_sessions WHERE id = ? AND user_id = ? AND agent_id = ?
|
|
858
906
|
`),
|
|
@@ -96,6 +96,11 @@ export const sessionDb = {
|
|
|
96
96
|
return stmts.getSessionsByUserAndAgent.all(userId, agentId, limit).map(mapRow);
|
|
97
97
|
},
|
|
98
98
|
|
|
99
|
+
hasOwnedRoute(userId, agentId) {
|
|
100
|
+
if (!userId || !agentId) return false;
|
|
101
|
+
return !!stmts.hasSessionOwnedByUserAndAgent.get(userId, agentId);
|
|
102
|
+
},
|
|
103
|
+
|
|
99
104
|
getAll(limit = 100) {
|
|
100
105
|
return stmts.getAllSessions.all(limit).map(mapRow);
|
|
101
106
|
},
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { stmts, transaction } from './connection.js';
|
|
2
|
+
|
|
3
|
+
function mapRow(row) {
|
|
4
|
+
if (!row) return null;
|
|
5
|
+
return {
|
|
6
|
+
userId: row.user_id,
|
|
7
|
+
catalogKey: row.catalog_key,
|
|
8
|
+
pinned: row.pinned === 1,
|
|
9
|
+
sortRank: Number.isFinite(row.sort_rank) ? row.sort_rank : null,
|
|
10
|
+
updatedAt: row.updated_at,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const sessionUiMetadataDb = {
|
|
15
|
+
upsert(userId, catalogKey, { pinned = false, sortRank = null } = {}) {
|
|
16
|
+
if (!userId || !catalogKey) return false;
|
|
17
|
+
stmts.upsertSessionUiMetadata.run(
|
|
18
|
+
userId,
|
|
19
|
+
catalogKey,
|
|
20
|
+
pinned ? 1 : 0,
|
|
21
|
+
Number.isFinite(sortRank) ? sortRank : null,
|
|
22
|
+
Date.now(),
|
|
23
|
+
);
|
|
24
|
+
return true;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
get(userId, catalogKey) {
|
|
28
|
+
if (!userId || !catalogKey) return null;
|
|
29
|
+
return mapRow(stmts.getSessionUiMetadata.get(userId, catalogKey));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getByUser(userId) {
|
|
33
|
+
if (!userId) return [];
|
|
34
|
+
return stmts.getSessionUiMetadataByUser.all(userId).map(mapRow);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
applyBatch(userId, updates) {
|
|
38
|
+
if (!userId || !Array.isArray(updates) || updates.length === 0) return false;
|
|
39
|
+
return transaction(() => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const update of updates) {
|
|
42
|
+
if (!update?.catalogKey) throw new Error('Catalog metadata update requires catalogKey');
|
|
43
|
+
stmts.upsertSessionUiMetadata.run(
|
|
44
|
+
userId,
|
|
45
|
+
update.catalogKey,
|
|
46
|
+
update.pinned === true ? 1 : 0,
|
|
47
|
+
Number.isFinite(update.sortRank) ? update.sortRank : null,
|
|
48
|
+
now,
|
|
49
|
+
);
|
|
50
|
+
if (update.runtimeProvider === 'yeaft') {
|
|
51
|
+
const result = stmts.setYeaftSessionPinnedForAgent.run(
|
|
52
|
+
update.pinned === true ? 1 : 0,
|
|
53
|
+
now,
|
|
54
|
+
update.sessionId,
|
|
55
|
+
userId,
|
|
56
|
+
update.agentId,
|
|
57
|
+
);
|
|
58
|
+
if (result.changes !== 1) throw new Error('Yeaft Session identity changed during metadata update');
|
|
59
|
+
} else if (update.runtimeProvider === 'claude-code' || update.runtimeProvider === 'copilot') {
|
|
60
|
+
const result = stmts.updateSessionPinnedForRoute.run(
|
|
61
|
+
update.pinned === true ? 1 : 0,
|
|
62
|
+
now,
|
|
63
|
+
update.sessionId,
|
|
64
|
+
update.agentId,
|
|
65
|
+
userId,
|
|
66
|
+
);
|
|
67
|
+
if (result.changes !== 1) throw new Error('Chat Session identity changed during metadata update');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
})();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
delete(userId, catalogKey) {
|
|
75
|
+
if (!userId || !catalogKey) return false;
|
|
76
|
+
return stmts.deleteSessionUiMetadata.run(userId, catalogKey).changes > 0;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* or op=archive.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { stmts } from './connection.js';
|
|
22
|
+
import { stmts, transaction } from './connection.js';
|
|
23
23
|
|
|
24
24
|
function safeJsonParse(s, fallback) {
|
|
25
25
|
if (s == null || s === '') return fallback;
|
|
@@ -140,6 +140,11 @@ export const yeaftSessionDb = {
|
|
|
140
140
|
return mapRow(stmts.getYeaftSession.get(id));
|
|
141
141
|
},
|
|
142
142
|
|
|
143
|
+
getAllById(id) {
|
|
144
|
+
if (!id) return [];
|
|
145
|
+
return stmts.getYeaftSessionsById.all(id).map(mapRow);
|
|
146
|
+
},
|
|
147
|
+
|
|
143
148
|
getForAgent(userId, agentId, id) {
|
|
144
149
|
if (!userId || !agentId || !id) return null;
|
|
145
150
|
return mapRow(stmts.getYeaftSessionForAgent.get(id, userId, agentId));
|
|
@@ -200,11 +205,14 @@ export const yeaftSessionDb = {
|
|
|
200
205
|
ordered.push(id);
|
|
201
206
|
}
|
|
202
207
|
if (ordered.length === 0) return false;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
return transaction(() => {
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
ordered.forEach((id, index) => {
|
|
211
|
+
const result = stmts.setYeaftSessionSortOrder.run(index, now, id, userId, agentId);
|
|
212
|
+
if (result.changes !== 1) throw new Error('Yeaft Session identity changed during reorder');
|
|
213
|
+
});
|
|
214
|
+
return true;
|
|
215
|
+
})();
|
|
208
216
|
},
|
|
209
217
|
|
|
210
218
|
setOrderForUser(userId, sessions) {
|
|
@@ -227,11 +235,14 @@ export const yeaftSessionDb = {
|
|
|
227
235
|
ordered.push({ agentId, sessionId });
|
|
228
236
|
}
|
|
229
237
|
if (ordered.length === 0) return false;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
return transaction(() => {
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
ordered.forEach(({ agentId, sessionId }, index) => {
|
|
241
|
+
const result = stmts.setYeaftSessionSortOrder.run(index, now, sessionId, userId, agentId);
|
|
242
|
+
if (result.changes !== 1) throw new Error('Yeaft Session identity changed during reorder');
|
|
243
|
+
});
|
|
244
|
+
return true;
|
|
245
|
+
})();
|
|
235
246
|
},
|
|
236
247
|
|
|
237
248
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { messageDb, yeaftSessionDb } from '../database.js';
|
|
3
|
-
import { broadcastAgentList, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
3
|
+
import { broadcastAgentList, broadcastSessionCatalog, forwardToClients, sendToAgent, sendToWebClient } from '../ws-utils.js';
|
|
4
4
|
import { webClients, previewFiles } from '../context.js';
|
|
5
5
|
import { CONFIG } from '../config.js';
|
|
6
6
|
import { yeaftAssetStore } from '../yeaft-asset-store.js';
|
|
@@ -802,6 +802,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
802
802
|
// snapshot per connected client.
|
|
803
803
|
const syncedEvent = syncYeaftSessionMetadata(agentId, agent, msg);
|
|
804
804
|
const decoratedSessions = syncedEvent.sessions;
|
|
805
|
+
await broadcastSessionCatalog(agent.ownerId);
|
|
805
806
|
// Relay verbatim to web (agentId stamped so the web sessions store
|
|
806
807
|
// can merge per-agent rosters).
|
|
807
808
|
for (const [, c] of webClients) {
|
|
@@ -866,6 +867,11 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
866
867
|
// sessions, and that response must carry persisted server-side pin
|
|
867
868
|
// state before it hits the web store.
|
|
868
869
|
const outboundMsg = syncYeaftSessionMetadata(agentId, agent, msg);
|
|
870
|
+
// CRUD acknowledgements are not authoritative catalog snapshots. The
|
|
871
|
+
// agent emits session_list_updated after mutations; broadcast only after
|
|
872
|
+
// that reconciliation so create/rename cannot briefly project stale data.
|
|
873
|
+
const catalogChanged = outboundMsg?.op === 'list';
|
|
874
|
+
if (catalogChanged) await broadcastSessionCatalog(agent.ownerId);
|
|
869
875
|
for (const [, c] of webClients) {
|
|
870
876
|
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
871
877
|
await sendToWebClient(c, { ...outboundMsg, agentId: agentId });
|