@yeaft/webchat-agent 1.0.190 → 1.0.191
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 +13 -6
- package/connection/message-router.js +23 -4
- package/local-runtime/server/handlers/agent-output.js +1 -0
- package/local-runtime/server/handlers/agent-sync.js +2 -0
- package/local-runtime/server/handlers/client-misc.js +2 -0
- package/local-runtime/server/ws-agent.js +20 -14
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +89 -89
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/config-api.js +57 -2
- package/yeaft/engine.js +16 -8
- package/yeaft/llm/router.js +16 -3
- package/yeaft/sessions/session-config.js +159 -76
- package/yeaft/status-cache.js +74 -17
- package/yeaft/web-bridge.js +99 -47
package/connection/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export function connect() {
|
|
|
24
24
|
console.log(`Disallowed tools: ${ctx.CONFIG.disallowedTools.join(', ')}`);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const socket = new WebSocket(url, {
|
|
28
28
|
// Match server's permessage-deflate config (bounded memory,
|
|
29
29
|
// skip compression for small frames). The `ws` library handles
|
|
30
30
|
// streaming compression so we no longer need the synchronous
|
|
@@ -35,15 +35,18 @@ export function connect() {
|
|
|
35
35
|
threshold: 1024
|
|
36
36
|
}
|
|
37
37
|
});
|
|
38
|
+
ctx.ws = socket;
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
socket.on('open', () => {
|
|
41
|
+
if (socket !== ctx.ws) return;
|
|
40
42
|
console.log('Connected to server, waiting for auth challenge...');
|
|
41
43
|
clearTimeout(ctx.reconnectTimer);
|
|
42
44
|
// 启动 agent 端心跳: 每 25 秒发一次 ping 帧
|
|
43
45
|
startAgentHeartbeat();
|
|
44
46
|
});
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
socket.on('message', async (data) => {
|
|
49
|
+
if (socket !== ctx.ws) return;
|
|
47
50
|
// 收到任何消息都说明连接活着
|
|
48
51
|
ctx.lastPongAt = Date.now();
|
|
49
52
|
|
|
@@ -54,7 +57,7 @@ export function connect() {
|
|
|
54
57
|
console.log('Received auth challenge, sending credentials...');
|
|
55
58
|
ctx.pendingAuthTempId = msg.tempId;
|
|
56
59
|
// Send authentication via WebSocket (not URL)
|
|
57
|
-
|
|
60
|
+
socket.send(JSON.stringify({
|
|
58
61
|
type: 'auth',
|
|
59
62
|
tempId: msg.tempId,
|
|
60
63
|
secret: ctx.CONFIG.agentSecret,
|
|
@@ -68,6 +71,9 @@ export function connect() {
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
const msg = await parseMessage(data);
|
|
74
|
+
// Decryption can yield after a reconnect replaces this socket. Fence again
|
|
75
|
+
// before a stale command can mutate Agent-local config or runtime state.
|
|
76
|
+
if (socket !== ctx.ws) return;
|
|
71
77
|
if (msg) {
|
|
72
78
|
handleMessage(msg).catch(err => {
|
|
73
79
|
console.error('[WS] handleMessage error:', err.message || err);
|
|
@@ -75,7 +81,8 @@ export function connect() {
|
|
|
75
81
|
}
|
|
76
82
|
});
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
socket.on('close', (code, reason) => {
|
|
85
|
+
if (socket !== ctx.ws) return;
|
|
79
86
|
console.log(`Disconnected from server: ${code} ${reason}`);
|
|
80
87
|
ctx.sessionKey = null;
|
|
81
88
|
ctx.pendingAuthTempId = null;
|
|
@@ -89,7 +96,7 @@ export function connect() {
|
|
|
89
96
|
scheduleReconnect(connect);
|
|
90
97
|
});
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
socket.on('error', (err) => {
|
|
93
100
|
console.error('WebSocket error:', err.message);
|
|
94
101
|
});
|
|
95
102
|
}
|
|
@@ -27,9 +27,10 @@ import { sendAgentMetricsSnapshot } from '../metrics.js';
|
|
|
27
27
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
28
28
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
29
29
|
import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
|
|
30
|
+
import { loadConfig } from '../yeaft/config.js';
|
|
30
31
|
import { discoverLlmModels } from '../llm-model-discovery.js';
|
|
31
32
|
import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
|
|
32
|
-
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
33
|
+
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
33
34
|
import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
|
|
34
35
|
import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
|
|
35
36
|
|
|
@@ -37,25 +38,43 @@ export async function applyLlmConfigUpdate(msg, dependencies = {}) {
|
|
|
37
38
|
const updateConfig = dependencies.updateLlmConfig || updateLlmConfig;
|
|
38
39
|
const broadcastLanguage = dependencies.broadcastLanguageChange || broadcastLanguageChange;
|
|
39
40
|
const forceStatusRefresh = dependencies.forceRefreshYeaftStatus || forceRefreshYeaftStatus;
|
|
41
|
+
const refreshRuntimeConfig = dependencies.refreshLiveSessionConfig || refreshLiveSessionConfig;
|
|
42
|
+
const readConfig = dependencies.loadConfig || loadConfig;
|
|
40
43
|
const send = dependencies.sendToServer || sendToServer;
|
|
41
44
|
const yeaftDir = dependencies.yeaftDir ?? ctx.CONFIG?.yeaftDir;
|
|
42
45
|
const incomingLanguage = typeof msg.config?.language === 'string' && msg.config.language
|
|
43
46
|
? msg.config.language
|
|
44
47
|
: null;
|
|
48
|
+
let previousDefaultModel = null;
|
|
49
|
+
try {
|
|
50
|
+
const previousConfig = readConfig({ dir: yeaftDir });
|
|
51
|
+
previousDefaultModel = previousConfig?.primaryModel || previousConfig?.model || null;
|
|
52
|
+
} catch { /* updateLlmConfig will report the actual config error */ }
|
|
45
53
|
const result = updateConfig(msg.config || {}, yeaftDir);
|
|
46
54
|
if (!result.error && incomingLanguage) broadcastLanguage(result.language);
|
|
47
55
|
|
|
48
56
|
let statusRefreshError = null;
|
|
49
57
|
if (!result.error) {
|
|
50
58
|
try {
|
|
51
|
-
|
|
52
|
-
statusRefreshError = statusEvent?.refreshError || null;
|
|
59
|
+
await refreshRuntimeConfig({ previousDefaultModel });
|
|
53
60
|
} catch (err) {
|
|
54
61
|
statusRefreshError = err?.message || String(err);
|
|
55
62
|
}
|
|
63
|
+
try {
|
|
64
|
+
const statusEvent = await forceStatusRefresh({ reason: 'llm_config_updated' });
|
|
65
|
+
statusRefreshError = statusRefreshError || statusEvent?.refreshError || null;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
statusRefreshError = statusRefreshError || err?.message || String(err);
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
const response = {
|
|
71
|
+
const response = {
|
|
72
|
+
type: 'llm_config_updated',
|
|
73
|
+
...result,
|
|
74
|
+
agentId: msg.agentId ?? null,
|
|
75
|
+
requestId: msg.requestId ?? null,
|
|
76
|
+
statusRefreshError,
|
|
77
|
+
};
|
|
59
78
|
send(response);
|
|
60
79
|
return response;
|
|
61
80
|
}
|
|
@@ -614,6 +614,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
614
614
|
if (c.authenticated && (CONFIG.skipAuth || c.userId === agent.ownerId)) {
|
|
615
615
|
await sendToWebClient(c, {
|
|
616
616
|
type: 'yeaft_history_chunk',
|
|
617
|
+
agentId,
|
|
617
618
|
conversationId: msg.conversationId,
|
|
618
619
|
...(msg.perfTraceId != null ? { perfTraceId: msg.perfTraceId } : {}),
|
|
619
620
|
...(msg.sessionId != null ? { sessionId: msg.sessionId } : {}),
|
|
@@ -301,12 +301,14 @@ export async function handleAgentSync(agentId, agent, msg) {
|
|
|
301
301
|
await sendToWebClient(client, {
|
|
302
302
|
type: 'llm_config_updated',
|
|
303
303
|
agentId,
|
|
304
|
+
requestId: msg.requestId,
|
|
304
305
|
providers: msg.providers,
|
|
305
306
|
primaryModel: msg.primaryModel,
|
|
306
307
|
fastModel: msg.fastModel,
|
|
307
308
|
language: msg.language,
|
|
308
309
|
agentConfig: msg.agentConfig,
|
|
309
310
|
effectiveConfig: msg.effectiveConfig,
|
|
311
|
+
statusRefreshError: msg.statusRefreshError,
|
|
310
312
|
error: msg.error
|
|
311
313
|
});
|
|
312
314
|
}
|
|
@@ -140,6 +140,8 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
|
|
|
140
140
|
if (!await checkAgentAccess(llmUpdateAgentId)) break;
|
|
141
141
|
await forwardToAgent(llmUpdateAgentId, {
|
|
142
142
|
type: 'update_llm_config',
|
|
143
|
+
agentId: llmUpdateAgentId,
|
|
144
|
+
requestId: msg.requestId,
|
|
143
145
|
config: msg.config || {}
|
|
144
146
|
});
|
|
145
147
|
break;
|
|
@@ -39,8 +39,8 @@ export function handleAgentConnection(ws, url) {
|
|
|
39
39
|
|
|
40
40
|
ws.on('message', async (data) => {
|
|
41
41
|
const agent = agents.get(clientAgentId);
|
|
42
|
-
if (!agent) {
|
|
43
|
-
console.error(`[Agent] No agent found for id: ${clientAgentId}`);
|
|
42
|
+
if (!agent || agent.ws !== ws) {
|
|
43
|
+
if (!agent) console.error(`[Agent] No agent found for id: ${clientAgentId}`);
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
markAgentHeartbeatSeen(agent);
|
|
@@ -62,14 +62,14 @@ export function handleAgentConnection(ws, url) {
|
|
|
62
62
|
bytes: data.length || 0,
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
handleAgentMessage(clientAgentId, msg);
|
|
65
|
+
handleAgentMessage(clientAgentId, msg, ws);
|
|
66
66
|
} else {
|
|
67
67
|
console.error(`[Agent] Failed to parse message from ${clientAgentId}`);
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
ws.on('close', () => {
|
|
72
|
-
handleAgentDisconnect(clientAgentId, agentName);
|
|
72
|
+
handleAgentDisconnect(clientAgentId, agentName, ws);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
ws.on('error', (err) => {
|
|
@@ -136,8 +136,8 @@ export function handleAgentConnection(ws, url) {
|
|
|
136
136
|
// Already authenticated, handle normally
|
|
137
137
|
if (!resolvedAgentId) return;
|
|
138
138
|
const agent = agents.get(resolvedAgentId);
|
|
139
|
-
if (!agent) {
|
|
140
|
-
console.error(`[Agent] No agent found for id: ${resolvedAgentId}`);
|
|
139
|
+
if (!agent || agent.ws !== ws) {
|
|
140
|
+
if (!agent) console.error(`[Agent] No agent found for id: ${resolvedAgentId}`);
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
markAgentHeartbeatSeen(agent);
|
|
@@ -159,7 +159,7 @@ export function handleAgentConnection(ws, url) {
|
|
|
159
159
|
bytes: data.length || 0,
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
|
-
handleAgentMessage(resolvedAgentId, msg);
|
|
162
|
+
handleAgentMessage(resolvedAgentId, msg, ws);
|
|
163
163
|
} else {
|
|
164
164
|
console.error(`[Agent] Failed to parse message from ${resolvedAgentId}`);
|
|
165
165
|
}
|
|
@@ -174,7 +174,7 @@ export function handleAgentConnection(ws, url) {
|
|
|
174
174
|
}
|
|
175
175
|
// Use resolvedAgentId if auth completed, otherwise nothing to clean
|
|
176
176
|
if (resolvedAgentId) {
|
|
177
|
-
handleAgentDisconnect(resolvedAgentId, agentName);
|
|
177
|
+
handleAgentDisconnect(resolvedAgentId, agentName, ws);
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
@@ -188,12 +188,13 @@ export function handleAgentConnection(ws, url) {
|
|
|
188
188
|
* Conversations are persisted in DB and will be restored on reconnect via
|
|
189
189
|
* get_agents (client-side recovery) and conversation_list (agent-side sync).
|
|
190
190
|
*/
|
|
191
|
-
function handleAgentDisconnect(agentId, agentName) {
|
|
191
|
+
function handleAgentDisconnect(agentId, agentName, ws) {
|
|
192
192
|
const agent = agents.get(agentId);
|
|
193
|
+
if (!agent || agent.ws !== ws) return;
|
|
193
194
|
// Phase 4: 清理目录缓存
|
|
194
195
|
clearAgentDirCache(agentId);
|
|
195
196
|
// Phase 1: 清理同步超时
|
|
196
|
-
if (agent
|
|
197
|
+
if (agent._syncTimeout) {
|
|
197
198
|
clearTimeout(agent._syncTimeout);
|
|
198
199
|
}
|
|
199
200
|
// Remove agent entirely — eliminates zombie agents from broadcastAgentList
|
|
@@ -209,6 +210,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
209
210
|
const proxyPorts = (existingAgent?.proxyPorts || []).map(p => ({ ...p, enabled: false }));
|
|
210
211
|
const slashCommands = existingAgent?.slashCommands || [];
|
|
211
212
|
const slashCommandDescriptions = existingAgent?.slashCommandDescriptions || {};
|
|
213
|
+
if (existingAgent?._syncTimeout) clearTimeout(existingAgent._syncTimeout);
|
|
212
214
|
|
|
213
215
|
// 兼容旧版 agent:未上报 capabilities 时默认全部开启
|
|
214
216
|
const effectiveCapabilities = capabilities.length > 0
|
|
@@ -244,7 +246,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
244
246
|
// 同步超时保护:30 秒后强制 ready
|
|
245
247
|
const syncTimeout = setTimeout(() => {
|
|
246
248
|
const ag = agents.get(agentId);
|
|
247
|
-
if (ag && ag.status === 'syncing') {
|
|
249
|
+
if (ag?.ws === ws && ag.status === 'syncing') {
|
|
248
250
|
console.warn(`[Sync] Agent ${agentName} sync timeout, forcing ready`);
|
|
249
251
|
ag.status = 'ready';
|
|
250
252
|
broadcastAgentList();
|
|
@@ -252,10 +254,14 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
252
254
|
}, 30000);
|
|
253
255
|
agents.get(agentId)._syncTimeout = syncTimeout;
|
|
254
256
|
|
|
257
|
+
if (existingAgent?.ws && existingAgent.ws !== ws) {
|
|
258
|
+
existingAgent.ws.close(1008, 'Superseded by a newer Agent connection');
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
// 心跳响应处理 + latency 测量
|
|
256
262
|
ws.on('pong', () => {
|
|
257
263
|
const agent = agents.get(agentId);
|
|
258
|
-
if (agent) {
|
|
264
|
+
if (agent?.ws === ws) {
|
|
259
265
|
markAgentHeartbeatSeen(agent);
|
|
260
266
|
if (agent.pingSentAt) {
|
|
261
267
|
agent.latency = Date.now() - agent.pingSentAt;
|
|
@@ -284,9 +290,9 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
|
|
|
284
290
|
broadcastAgentList();
|
|
285
291
|
}
|
|
286
292
|
|
|
287
|
-
async function handleAgentMessage(agentId, msg) {
|
|
293
|
+
async function handleAgentMessage(agentId, msg, ws) {
|
|
288
294
|
const agent = agents.get(agentId);
|
|
289
|
-
if (!agent) return;
|
|
295
|
+
if (!agent || agent.ws !== ws) return;
|
|
290
296
|
|
|
291
297
|
// Security: 需要 conversationId 的消息类型,验证该 conversation 属于此 agent.
|
|
292
298
|
// The conversation-id check only authorizes Chat flows where the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.191"}
|