@yeaft/webchat-agent 1.0.413 → 1.0.415

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.
Files changed (58) hide show
  1. package/browser-runtime/browser-install.js +497 -0
  2. package/browser-runtime/cli.js +88 -0
  3. package/browser-runtime/config.js +116 -0
  4. package/browser-runtime/errors.js +8 -0
  5. package/browser-runtime/extension/manifest.json +18 -0
  6. package/browser-runtime/extension/offscreen.html +5 -0
  7. package/browser-runtime/extension/offscreen.js +101 -0
  8. package/browser-runtime/extension/popup.html +5 -0
  9. package/browser-runtime/extension/popup.js +1 -0
  10. package/browser-runtime/extension/service-worker.js +48 -0
  11. package/browser-runtime/extension.js +45 -0
  12. package/browser-runtime/index.js +5 -0
  13. package/browser-runtime/probe.js +427 -0
  14. package/browser-runtime/protocol.js +71 -0
  15. package/browser-runtime/service.js +132 -0
  16. package/browser-runtime/windows-version-job.ps1 +233 -0
  17. package/browser-runtime/windows-version-worker.js +75 -0
  18. package/browser-runtime/windows-version.js +85 -0
  19. package/cli.js +24 -7
  20. package/connection/index.js +12 -0
  21. package/context.js +1 -0
  22. package/index.js +19 -2
  23. package/llm-config-cli.js +24 -21
  24. package/local-runtime/server/client-protocol.js +14 -0
  25. package/local-runtime/server/context.js +3 -2
  26. package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
  27. package/local-runtime/server/handlers/agent-output.js +3 -0
  28. package/local-runtime/server/handlers/client-misc.js +21 -4
  29. package/local-runtime/server/handlers/client-workbench.js +222 -41
  30. package/local-runtime/server/workbench-correlation.js +184 -0
  31. package/local-runtime/server/workbench-route.js +180 -0
  32. package/local-runtime/server/ws-agent.js +4 -0
  33. package/local-runtime/server/ws-client.js +25 -3
  34. package/local-runtime/version.json +1 -1
  35. package/local-runtime/web/app.bundle.js +191 -135
  36. package/local-runtime/web/app.bundle.js.gz +0 -0
  37. package/local-runtime/web/index.html +2 -2
  38. package/local-runtime/web/style.bundle.css +1 -1
  39. package/local-runtime/web/style.bundle.css.gz +0 -0
  40. package/package.json +5 -1
  41. package/service/config.js +23 -2
  42. package/service/index.js +1 -0
  43. package/service/linux.js +3 -2
  44. package/terminal.js +167 -30
  45. package/workbench/file-ops.js +21 -20
  46. package/workbench/file-search.js +4 -3
  47. package/workbench/git-ops.js +23 -22
  48. package/workbench/request-routing.js +16 -0
  49. package/yeaft/cli.js +57 -1
  50. package/yeaft/config-api.js +138 -192
  51. package/yeaft/config-store.js +192 -0
  52. package/yeaft/config.js +3 -0
  53. package/yeaft/init.js +20 -7
  54. package/yeaft/sessions/feature-flag.js +15 -33
  55. package/yeaft/sessions/session-manifest.js +114 -10
  56. package/yeaft/stdio-protocol.js +57 -0
  57. package/yeaft/storage/atomic.js +43 -17
  58. package/yeaft/tools/process-runner.js +86 -13
@@ -0,0 +1,180 @@
1
+ import { CONFIG } from './config.js';
2
+ import { agents } from './context.js';
3
+ import { sessionDb } from './db/session-db.js';
4
+ import { yeaftSessionDb } from './db/yeaft-session-db.js';
5
+
6
+ export const WORKBENCH_SESSION_ROUTE_CAPABILITY = 'workbench_session_routes';
7
+
8
+ const PROVIDERS = new Set(['yeaft', 'claude-code', 'copilot']);
9
+ const SCOPES = new Set(['main', 'files-folder-picker', 'git-folder-picker']);
10
+
11
+ function clean(value, maxLength = 300) {
12
+ if (typeof value !== 'string') return '';
13
+ const result = value.trim();
14
+ return result && result.length <= maxLength ? result : '';
15
+ }
16
+
17
+ export function workbenchRouteKey(route) {
18
+ const runtimeProvider = clean(route?.runtimeProvider, 32);
19
+ const agentId = clean(route?.agentId);
20
+ const sessionId = clean(route?.sessionId);
21
+ if (!PROVIDERS.has(runtimeProvider) || !agentId || !sessionId) return '';
22
+ return [runtimeProvider, agentId, sessionId]
23
+ .map(part => encodeURIComponent(part))
24
+ .join(':');
25
+ }
26
+
27
+ function stableWorkspaceHash(value) {
28
+ let hash = 0xcbf29ce484222325n;
29
+ for (const char of String(value || '')) {
30
+ hash ^= BigInt(char.codePointAt(0));
31
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
32
+ }
33
+ return hash.toString(16).padStart(16, '0');
34
+ }
35
+
36
+ export function workbenchWorkspaceGeneration(routeKey, workDir) {
37
+ const normalizedRouteKey = clean(routeKey, 1200);
38
+ const normalizedWorkDir = clean(workDir, 4096);
39
+ if (!normalizedRouteKey || !normalizedWorkDir) return '';
40
+ return `${normalizedRouteKey}@${stableWorkspaceHash(normalizedWorkDir)}`;
41
+ }
42
+
43
+ export function workbenchConversationId(route, scope = 'main') {
44
+ const routeKey = workbenchRouteKey(route);
45
+ if (!routeKey) return '';
46
+ const normalizedScope = SCOPES.has(scope) ? scope : 'main';
47
+ return normalizedScope === 'main'
48
+ ? `_workbench:${routeKey}`
49
+ : `_workbench:${routeKey}:${normalizedScope}`;
50
+ }
51
+
52
+ export function workbenchRouteKeyFromConversationId(conversationId, expectedAgentId = '') {
53
+ const raw = clean(conversationId, 4096);
54
+ if (!raw.startsWith('_workbench:')) return '';
55
+ const parts = raw.slice('_workbench:'.length).split(':');
56
+ if (parts.length < 3) return '';
57
+ const routeKey = parts.slice(0, 3).join(':');
58
+ try {
59
+ const decodedRoute = {
60
+ runtimeProvider: decodeURIComponent(parts[0]),
61
+ agentId: decodeURIComponent(parts[1]),
62
+ sessionId: decodeURIComponent(parts[2]),
63
+ };
64
+ if (expectedAgentId && decodedRoute.agentId !== expectedAgentId) return '';
65
+ return workbenchRouteKey(decodedRoute) === routeKey ? routeKey : '';
66
+ } catch {
67
+ return '';
68
+ }
69
+ }
70
+
71
+ function resolveYeaftRow(client, route) {
72
+ if (client?.userId) {
73
+ const owned = yeaftSessionDb.getForAgent(client.userId, route.agentId, route.sessionId);
74
+ if (owned) return owned;
75
+ }
76
+ const agent = agents.get(route.agentId);
77
+ if (!CONFIG.skipAuth) {
78
+ if (client?.role !== 'admin' || agent?.ownerId) return null;
79
+ return agent?.yeaftSessions?.get(route.sessionId) || null;
80
+ }
81
+ return agent?.yeaftSessions?.get(route.sessionId)
82
+ || yeaftSessionDb.getByAgent(route.agentId).find(row => row?.id === route.sessionId)
83
+ || null;
84
+ }
85
+
86
+ function resolveChatRow(client, route) {
87
+ const row = sessionDb.get(route.sessionId);
88
+ if (!row || row.agent_id !== route.agentId) return null;
89
+ const provider = row.provider === 'copilot' ? 'copilot' : 'claude-code';
90
+ if (provider !== route.runtimeProvider) return null;
91
+ if (!CONFIG.skipAuth) {
92
+ if (!client?.userId) return null;
93
+ if (row.user_id) {
94
+ if (row.user_id !== client.userId) return null;
95
+ } else if (client.role !== 'admin' && agents.get(route.agentId)?.ownerId !== client.userId) {
96
+ return null;
97
+ }
98
+ }
99
+ return row;
100
+ }
101
+
102
+ /**
103
+ * Validate a browser-provided Workbench route against Server-owned Session
104
+ * metadata and return canonical execution fields. Browser cwd and synthetic
105
+ * conversation ids are never authoritative.
106
+ *
107
+ * `legacy: true` preserves old clients that predate route-scoped Workbench.
108
+ */
109
+ export function currentWorkbenchWorkspaceGeneration({ route, userId, role }) {
110
+ if (!route || !userId) return '';
111
+ const client = { userId, role };
112
+ const row = route.runtimeProvider === 'yeaft'
113
+ ? resolveYeaftRow(client, route)
114
+ : resolveChatRow(client, route);
115
+ if (!row || row.isArchived) return '';
116
+ const workDir = clean(route.runtimeProvider === 'yeaft' ? row.workDir : row.work_dir, 4096);
117
+ return workbenchWorkspaceGeneration(workbenchRouteKey(route), workDir);
118
+ }
119
+
120
+ export function resolveWorkbenchRequest(client, msg, targetAgentId, { allowMissingSession = false } = {}) {
121
+ const agent = agents.get(targetAgentId);
122
+ const agentSupportsRoutes = Array.isArray(agent?.capabilities)
123
+ && agent.capabilities.includes(WORKBENCH_SESSION_ROUTE_CAPABILITY);
124
+ const clientSupportsRoutes = client?.workbenchRouteProtocol === 1;
125
+
126
+ if (!msg?.workbenchRoute) {
127
+ // Legacy is a negotiated pairing, not a caller-selected downgrade. Once
128
+ // either side speaks the route protocol, route-less Workbench is invalid.
129
+ if (clientSupportsRoutes || agentSupportsRoutes) return null;
130
+ return {
131
+ legacy: true,
132
+ agentId: targetAgentId,
133
+ conversationId: clean(msg?.conversationId) || null,
134
+ workDir: clean(msg?.workDir, 4096),
135
+ routeKey: '',
136
+ };
137
+ }
138
+
139
+ if (!clientSupportsRoutes || !agentSupportsRoutes) return null;
140
+ const route = {
141
+ runtimeProvider: clean(msg.workbenchRoute.runtimeProvider, 32),
142
+ agentId: clean(msg.workbenchRoute.agentId),
143
+ sessionId: clean(msg.workbenchRoute.sessionId),
144
+ };
145
+ const routeKey = workbenchRouteKey(route);
146
+ if (!routeKey || route.agentId !== targetAgentId) return null;
147
+
148
+ const row = route.runtimeProvider === 'yeaft'
149
+ ? resolveYeaftRow(client, route)
150
+ : resolveChatRow(client, route);
151
+ if (row?.isArchived && !allowMissingSession) return null;
152
+ if (!row && !allowMissingSession) return null;
153
+
154
+ let scope = SCOPES.has(msg.workbenchScope) ? msg.workbenchScope : 'main';
155
+ if (scope === 'main') {
156
+ if (msg.conversationId === '_folder_picker') scope = 'files-folder-picker';
157
+ else if (msg.conversationId === '_git_folder_picker') scope = 'git-folder-picker';
158
+ }
159
+ const sessionWorkDir = clean(row
160
+ ? (route.runtimeProvider === 'yeaft' ? row.workDir : row.work_dir)
161
+ : '', 4096);
162
+ const workspaceGeneration = sessionWorkDir
163
+ ? workbenchWorkspaceGeneration(routeKey, sessionWorkDir)
164
+ : clean(msg.workbenchWorkspaceGeneration, 1600);
165
+ if (!workspaceGeneration && !allowMissingSession) return null;
166
+ return {
167
+ legacy: false,
168
+ route,
169
+ routeKey,
170
+ scope,
171
+ agentId: route.agentId,
172
+ conversationId: workbenchConversationId(route, scope),
173
+ // Terminal is pinned to this Server-owned cwd. Git and Files retain their
174
+ // existing Agent-path picker and use requestedWorkDir after route auth.
175
+ workDir: sessionWorkDir,
176
+ requestedWorkDir: clean(msg.workDir, 4096) || sessionWorkDir,
177
+ workspaceGeneration,
178
+ archived: row?.isArchived === true,
179
+ };
180
+ }
@@ -15,6 +15,7 @@ import { handleAgentWorkCenter } from './handlers/agent-work-center.js';
15
15
  import { handleAgentFileTerminal } from './handlers/agent-file-terminal.js';
16
16
  import { handleAgentSync } from './handlers/agent-sync.js';
17
17
  import { recordPerfTraceEvent } from './perf-trace.js';
18
+ import { clearWorkbenchCorrelationsForAgent } from './workbench-correlation.js';
18
19
  import { markAgentHeartbeatSeen } from './heartbeat-policy.js';
19
20
 
20
21
  /**
@@ -227,6 +228,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
227
228
  if (!agent || agent.ws !== ws) return;
228
229
  // Phase 4: 清理目录缓存
229
230
  clearAgentDirCache(agentId);
231
+ clearWorkbenchCorrelationsForAgent(agentId);
230
232
  // Phase 1: 清理同步超时
231
233
  if (agent._syncTimeout) {
232
234
  clearTimeout(agent._syncTimeout);
@@ -245,6 +247,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
245
247
  const proxyPorts = (existingAgent?.proxyPorts || []).map(p => ({ ...p, enabled: false }));
246
248
  const slashCommands = existingAgent?.slashCommands || [];
247
249
  const slashCommandDescriptions = existingAgent?.slashCommandDescriptions || {};
250
+ const yeaftSessions = existingAgent?.yeaftSessions || new Map();
248
251
  if (existingAgent?._syncTimeout) clearTimeout(existingAgent._syncTimeout);
249
252
 
250
253
  // 兼容旧版 agent:未上报 capabilities 时默认全部开启
@@ -271,6 +274,7 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
271
274
  proxyPorts,
272
275
  slashCommands,
273
276
  slashCommandDescriptions,
277
+ yeaftSessions,
274
278
  status: 'syncing',
275
279
  ownerId,
276
280
  ownerUsername,
@@ -6,6 +6,8 @@ import { authenticateRequest } from './auth/request-auth.js';
6
6
  import { encodeKey } from './encryption.js';
7
7
  import { userDb } from './database.js';
8
8
  import { agents, clearYeaftDebugRequestsForClient, webClients, isHeartbeatMessageType, trackRequest } from './context.js';
9
+ import { applyClientHello, WORKBENCH_ROUTE_PROTOCOL } from './client-protocol.js';
10
+ import { clearWorkbenchCorrelationsForClient } from './workbench-correlation.js';
9
11
  import {
10
12
  parseMessage, sendToWebClient, sendToAgent,
11
13
  broadcastAgentList, resolveAgentAccessError
@@ -76,7 +78,9 @@ export function handleWebConnection(ws, url, req = {}) {
76
78
  // (= old client, encrypt outbound for back-compat). Flipped to
77
79
  // `false` when the client sends `client_hello { plaintextOk: true }`
78
80
  // — see early dispatch in handleWebMessage.
79
- encryptOutbound: true
81
+ encryptOutbound: true,
82
+ // Explicit Workbench protocol negotiation. Zero means legacy Web.
83
+ workbenchRouteProtocol: 0,
80
84
  });
81
85
 
82
86
  // 心跳响应处理
@@ -103,6 +107,7 @@ export function handleWebConnection(ws, url, req = {}) {
103
107
  role,
104
108
  acceptPlaintext: true,
105
109
  yeaftSessionInventoryComplete: true,
110
+ workbenchRouteProtocol: WORKBENCH_ROUTE_PROTOCOL,
106
111
  }));
107
112
  setTimeout(() => broadcastAgentList(), 100);
108
113
  } else {
@@ -164,6 +169,18 @@ export function handleWebConnection(ws, url, req = {}) {
164
169
  }
165
170
  clearWorkCenterRequestsForClient(client);
166
171
  clearYeaftDebugRequestsForClient(clientId);
172
+ const ownedTerminals = clearWorkbenchCorrelationsForClient(clientId);
173
+ for (const owner of ownedTerminals) {
174
+ const agent = agents.get(owner.agentId);
175
+ if (!agent) continue;
176
+ void sendToAgent(agent, {
177
+ type: 'terminal_close',
178
+ terminalId: owner.terminalId,
179
+ conversationId: owner.conversationId,
180
+ workbenchRouteKey: owner.routeKey,
181
+ workbenchWorkspaceGeneration: owner.workspaceGeneration,
182
+ }).catch(error => console.warn('[Workbench] PTY disconnect cleanup failed:', error.message));
183
+ }
167
184
  webClients.delete(clientId);
168
185
  console.log(`Web client disconnected: ${clientId}`);
169
186
  });
@@ -195,10 +212,15 @@ async function handleWebMessage(clientId, msg) {
195
212
  // Old clients never send this; their per-client `encryptOutbound` flag
196
213
  // stays `true` and we keep the ciphertext path.
197
214
  if (msg.type === 'client_hello') {
198
- if (msg.plaintextOk === true) {
199
- client.encryptOutbound = false;
215
+ const wasEncrypted = client.encryptOutbound;
216
+ applyClientHello(client, msg);
217
+ if (wasEncrypted && client.encryptOutbound === false) {
200
218
  console.log(`[WS] Client ${clientId} negotiated plaintext mode`);
201
219
  }
220
+ await sendToWebClient(client, {
221
+ type: 'client_hello_ack',
222
+ workbenchRouteProtocol: client.workbenchRouteProtocol,
223
+ });
202
224
  return;
203
225
  }
204
226
 
@@ -1 +1 @@
1
- {"version":"1.0.413"}
1
+ {"version":"1.0.415"}