@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.
@@ -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 connect() {
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 WebSocket(url, {
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
- if (msg.sessionKey) {
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
- const now = Date.now();
204
- ordered.forEach((id, index) => {
205
- stmts.setYeaftSessionSortOrder.run(index, now, id, userId, agentId);
206
- });
207
- return true;
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
- const now = Date.now();
231
- ordered.forEach(({ agentId, sessionId }, index) => {
232
- stmts.setYeaftSessionSortOrder.run(index, now, sessionId, userId, agentId);
233
- });
234
- return true;
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 });