@yeaft/webchat-agent 1.0.417 → 1.0.419

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.
@@ -5,6 +5,7 @@ import {
5
5
  browserRoutes,
6
6
  completeBrowserRequest,
7
7
  deleteBrowserPeer,
8
+ findBrowserRequest,
8
9
  deleteBrowserRoute,
9
10
  getBrowserPeer,
10
11
  getBrowserRoute,
@@ -12,6 +13,9 @@ import {
12
13
  } from '../browser-runtime-routes.js';
13
14
 
14
15
  const AGENT_BROWSER_TYPES = new Set([
16
+ 'browser_runtime_status_result',
17
+ 'browser_runtime_install_progress',
18
+ 'browser_runtime_error',
15
19
  'browser_session_created',
16
20
  'browser_session_error',
17
21
  'browser_session_snapshot',
@@ -56,6 +60,50 @@ function sessionSnapshot(msg) {
56
60
  };
57
61
  }
58
62
 
63
+ function safeByteCount(value) {
64
+ const number = Number(value);
65
+ if (!Number.isSafeInteger(number) || number < 0) return 0;
66
+ return Math.min(number, 4 * 1024 * 1024 * 1024);
67
+ }
68
+
69
+ function publicRuntimeMessage(agentId, requestId, msg) {
70
+ if (msg?.type === 'browser_runtime_error') {
71
+ return {
72
+ type: msg.type,
73
+ agentId,
74
+ requestId,
75
+ code: clean(msg.code, 128) || 'browser_runtime_error',
76
+ safeError: clean(msg.safeError, 500) || null,
77
+ };
78
+ }
79
+ if (msg?.type === 'browser_runtime_install_progress') {
80
+ return {
81
+ type: msg.type,
82
+ agentId,
83
+ requestId,
84
+ downloadedBytes: safeByteCount(msg.downloadedBytes),
85
+ totalBytes: safeByteCount(msg.totalBytes),
86
+ };
87
+ }
88
+ return {
89
+ type: 'browser_runtime_status_result',
90
+ agentId,
91
+ requestId,
92
+ supported: msg?.supported === true,
93
+ state: clean(msg?.state, 32) || 'unknown',
94
+ installed: msg?.installed === true,
95
+ enabled: msg?.enabled === true,
96
+ ready: msg?.ready === true,
97
+ buildId: clean(msg?.buildId, 128) || null,
98
+ platform: clean(msg?.platform, 32) || null,
99
+ downloadBytes: safeByteCount(msg?.downloadBytes),
100
+ downloadedBytes: safeByteCount(msg?.downloadedBytes),
101
+ totalBytes: safeByteCount(msg?.totalBytes),
102
+ probeCode: clean(msg?.probeCode, 128) || null,
103
+ safeError: clean(msg?.safeError, 500) || null,
104
+ };
105
+ }
106
+
59
107
  function publicSessionMessage(agentId, msg) {
60
108
  if (msg?.type === 'browser_session_error') {
61
109
  return {
@@ -84,7 +132,7 @@ function publicSessionMessage(agentId, msg) {
84
132
  }
85
133
 
86
134
  async function sendCreateResponse(agentId, msg) {
87
- const request = completeBrowserRequest(agentId, msg);
135
+ const request = completeBrowserRequest(agentId, msg, { kinds: 'browser_session_create' });
88
136
  if (!request) return true;
89
137
  const client = browserClientForPeer({
90
138
  clientId: request.clientId,
@@ -130,13 +178,47 @@ async function sendCreateResponse(agentId, msg) {
130
178
  /** Agent-authenticated Browser events routed only through Server-owned ledgers. */
131
179
  export async function handleAgentBrowser(agentId, agent, msg) {
132
180
  if (!AGENT_BROWSER_TYPES.has(msg?.type)) return false;
181
+ if (msg.type === 'browser_runtime_install_progress') {
182
+ const request = findBrowserRequest(agentId, msg.requestId);
183
+ if (!request || request.state !== 'pending' || request.kind !== 'browser_runtime_install') return true;
184
+ const client = browserClientForPeer({
185
+ clientId: request.clientId,
186
+ webConnectionId: request.webConnectionId,
187
+ webConnectionGeneration: request.webConnectionGeneration,
188
+ });
189
+ if (client?.userId === request.ownerUserId) {
190
+ await sendToWebClient(client, publicRuntimeMessage(agentId, request.requestId, msg));
191
+ }
192
+ return true;
193
+ }
194
+ if (msg.type === 'browser_runtime_status_result' || msg.type === 'browser_runtime_error') {
195
+ const pendingRequest = findBrowserRequest(agentId, msg.requestId);
196
+ const request = completeBrowserRequest(agentId, msg, {
197
+ consume: pendingRequest?.kind === 'browser_runtime_status',
198
+ kinds: ['browser_runtime_status', 'browser_runtime_install', 'browser_runtime_enable'],
199
+ });
200
+ if (!request) return true;
201
+ const client = browserClientForPeer({
202
+ clientId: request.clientId,
203
+ webConnectionId: request.webConnectionId,
204
+ webConnectionGeneration: request.webConnectionGeneration,
205
+ });
206
+ if (client?.userId !== request.ownerUserId) return true;
207
+ const response = publicRuntimeMessage(agentId, request.requestId, msg);
208
+ request.response = response;
209
+ await sendToWebClient(client, response);
210
+ return true;
211
+ }
133
212
  if (msg.type === 'browser_session_created'
134
213
  || (msg.type === 'browser_session_error' && msg.requestId)) {
135
214
  return sendCreateResponse(agentId, msg);
136
215
  }
137
216
 
138
217
  if (msg.type === 'browser_session_list_result') {
139
- const request = completeBrowserRequest(agentId, msg, { consume: true });
218
+ const request = completeBrowserRequest(agentId, msg, {
219
+ consume: true,
220
+ kinds: 'browser_session_list',
221
+ });
140
222
  if (!request) return true;
141
223
  for (const snapshot of Array.isArray(msg.sessions) ? msg.sessions : []) {
142
224
  if (!snapshot?.browserSessionId) continue;
@@ -154,7 +236,10 @@ export async function handleAgentBrowser(agentId, agent, msg) {
154
236
  }
155
237
 
156
238
  if (msg.type === 'browser_session_snapshot' && msg.requestId) {
157
- const request = completeBrowserRequest(agentId, msg, { consume: true });
239
+ const request = completeBrowserRequest(agentId, msg, {
240
+ consume: true,
241
+ kinds: ['browser_session_get', 'browser_session_close'],
242
+ });
158
243
  if (!request) return true;
159
244
  const client = browserClientForPeer({
160
245
  clientId: request.clientId,
@@ -74,6 +74,20 @@ export async function handleAgentSync(agentId, agent, msg) {
74
74
  break;
75
75
  }
76
76
 
77
+ case 'agent_capabilities_updated': {
78
+ const capabilities = Array.isArray(msg.capabilities)
79
+ ? [...new Set(msg.capabilities.filter(value => (
80
+ typeof value === 'string' && value.length > 0 && value.length <= 128
81
+ )).slice(0, 128))]
82
+ : null;
83
+ if (!capabilities || capabilities.length === 0) break;
84
+ agent.capabilities = capabilities;
85
+ // Transport encryption negotiation is connection-scoped and immutable.
86
+ // A runtime capability refresh must never flip framing mid-connection.
87
+ await broadcastAgentList();
88
+ break;
89
+ }
90
+
77
91
  case 'agent_metrics': {
78
92
  agent.metrics = normalizeAgentMetrics(msg.metrics || {});
79
93
  agent.metricsUpdatedAt = Date.now();
@@ -15,6 +15,9 @@ import {
15
15
  } from '../browser-runtime-routes.js';
16
16
 
17
17
  const CLIENT_BROWSER_TYPES = new Set([
18
+ 'browser_runtime_status',
19
+ 'browser_runtime_install',
20
+ 'browser_runtime_enable',
18
21
  'browser_session_create',
19
22
  'browser_session_get',
20
23
  'browser_session_list',
@@ -34,8 +37,11 @@ function digest(value) {
34
37
  }
35
38
 
36
39
  async function fail(client, msg, code, safeError = code) {
40
+ const type = String(msg.type || '');
37
41
  await sendToWebClient(client, {
38
- type: String(msg.type || '').startsWith('browser_peer_') ? 'browser_peer_error' : 'browser_session_error',
42
+ type: type.startsWith('browser_peer_') ? 'browser_peer_error'
43
+ : type.startsWith('browser_runtime_') ? 'browser_runtime_error'
44
+ : 'browser_session_error',
39
45
  requestId: clean(msg.requestId) || null,
40
46
  browserSessionId: clean(msg.browserSessionId) || null,
41
47
  peerId: clean(msg.peerId) || null,
@@ -46,6 +52,10 @@ async function fail(client, msg, code, safeError = code) {
46
52
  return true;
47
53
  }
48
54
 
55
+ function agentSupportsBrowserSetup(agent) {
56
+ return new Set(agent?.capabilities || []).has('browser_runtime_setup');
57
+ }
58
+
49
59
  function agentSupportsBrowser(agent) {
50
60
  const capabilities = new Set(agent?.capabilities || []);
51
61
  return capabilities.has('browser_runtime')
@@ -98,17 +108,54 @@ function ownerRoute(client, msg) {
98
108
  export async function handleClientBrowser(client, msg, checkAgentAccess) {
99
109
  if (!CLIENT_BROWSER_TYPES.has(msg?.type)) return false;
100
110
  if (!CONFIG.browserRuntime.enabled) return fail(client, msg, 'browser_runtime_disabled');
101
- if (client.browserRuntimeProtocol !== 1) return fail(client, msg, 'browser_protocol_required');
111
+ const setupRequest = String(msg.type || '').startsWith('browser_runtime_');
112
+ if (setupRequest) {
113
+ if (client.browserRuntimeSetupProtocol !== 1) {
114
+ return fail(client, msg, 'browser_setup_protocol_required');
115
+ }
116
+ } else if (client.browserRuntimeProtocol !== 1) {
117
+ return fail(client, msg, 'browser_protocol_required');
118
+ }
102
119
  const agentId = clean(msg.agentId);
103
120
  if (!await checkAgentAccess(agentId)) return true;
104
121
  const agent = agents.get(agentId);
105
- if (!agentSupportsBrowser(agent)) return fail(client, msg, 'browser_runtime_unavailable');
122
+ if (setupRequest ? !agentSupportsBrowserSetup(agent) : !agentSupportsBrowser(agent)) {
123
+ return fail(client, msg, 'browser_runtime_unavailable');
124
+ }
106
125
  const requestId = clean(msg.requestId);
107
126
  const requestRequired = msg.type !== 'browser_peer_answer'
108
127
  && msg.type !== 'browser_peer_ice_candidate';
109
128
  if (requestRequired && !requestId) return fail(client, msg, 'browser_request_id_required');
110
129
  const identity = browserServerIdentity(client);
111
130
 
131
+ if (setupRequest) {
132
+ const canonical = msg.type === 'browser_runtime_install' ? {
133
+ confirmedBuildId: clean(msg.confirmedBuildId, 128),
134
+ confirmedDownloadBytes: Number(msg.confirmedDownloadBytes) || 0,
135
+ } : {};
136
+ const registration = registerBrowserCreateRequest({
137
+ agentId,
138
+ client,
139
+ requestId,
140
+ digest: digest({ type: msg.type, ...canonical }),
141
+ kind: msg.type,
142
+ });
143
+ if (registration.conflict) return fail(client, msg, 'browser_request_conflict');
144
+ if (registration.capacity) return fail(client, msg, 'browser_request_capacity');
145
+ if (registration.duplicate) {
146
+ if (registration.request.response) await sendToWebClient(client, registration.request.response);
147
+ return true;
148
+ }
149
+ await sendToAgent(agent, {
150
+ type: msg.type,
151
+ agentId,
152
+ requestId: registration.request.serverRequestId,
153
+ ...canonical,
154
+ serverIdentity: identity,
155
+ });
156
+ return true;
157
+ }
158
+
112
159
  if (msg.type === 'browser_session_create') {
113
160
  const canonical = canonicalCreate(msg);
114
161
  if (!canonical) return fail(client, msg, 'browser_url_invalid');
@@ -27,6 +27,10 @@ import {
27
27
  chatCatalogKey,
28
28
  yeaftCatalogKey,
29
29
  } from '../session-catalog.js';
30
+ import {
31
+ agentSupportsYeaftPlugins,
32
+ YEAFT_PLUGINS_UNSUPPORTED_ERROR,
33
+ } from '../yeaft-plugin-capability.js';
30
34
 
31
35
 
32
36
  function isRetiredCollabSessionId(id) {
@@ -44,6 +48,14 @@ function emptyYeaftToolStats(reason = '') {
44
48
  return payload;
45
49
  }
46
50
 
51
+ function emptyYeaftPluginCatalog(error = null) {
52
+ return {
53
+ type: 'yeaft_plugin_catalog_result',
54
+ catalog: { tools: [], skills: [], mcpServers: [] },
55
+ ...(error ? { error } : {}),
56
+ };
57
+ }
58
+
47
59
  async function sendVpSnapshotError(client, msg, error) {
48
60
  await sendToWebClient(client, {
49
61
  type: 'yeaft_output',
@@ -420,6 +432,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
420
432
  agentName: agent.name,
421
433
  workDir: agent.workDir,
422
434
  capabilities: agent.capabilities || ['terminal', 'file_editor', 'background_tasks'],
435
+ ...(agent.capabilityMetadataProvided === true ? { capabilityMetadataProvided: true } : {}),
423
436
  version: agent.version || null,
424
437
  conversations: filteredConvs,
425
438
  slashCommands: agent.slashCommands || [],
@@ -1628,6 +1641,15 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1628
1641
  }
1629
1642
  return true;
1630
1643
  }
1644
+ if (relayType === 'yeaft_plugin_catalog'
1645
+ && !agentSupportsYeaftPlugins(agents.get(relayAgentId))) {
1646
+ await sendToWebClient(client, {
1647
+ ...emptyYeaftPluginCatalog(YEAFT_PLUGINS_UNSUPPORTED_ERROR),
1648
+ agentId: relayAgentId,
1649
+ requestId: msg.requestId || null,
1650
+ });
1651
+ return true;
1652
+ }
1631
1653
  const relayAgent = agents.get(relayAgentId);
1632
1654
  if (!relayAgent || relayAgent.ws?.readyState !== 1) {
1633
1655
  if (relayType === 'yeaft_fetch_tool_stats') {
@@ -3,11 +3,27 @@ import {
3
3
  sendToWebClient, forwardToAgent, broadcastAgentList
4
4
  } from '../ws-utils.js';
5
5
  import { resolveWorkbenchRequest } from '../workbench-route.js';
6
+ import {
7
+ agentSupportsYeaftPlugins,
8
+ YEAFT_PLUGINS_CAPABILITY,
9
+ YEAFT_PLUGINS_UNSUPPORTED_ERROR,
10
+ } from '../yeaft-plugin-capability.js';
6
11
 
7
12
  // Only Agents that explicitly advertise the package-replacement-safe updater
8
13
  // may receive remote upgrade commands. Version thresholds are insufficient:
9
14
  // builds without this capability may still inherit the installed package cwd.
10
15
  export const SAFE_REMOTE_UPGRADE_CAPABILITY = 'remote_upgrade_safe';
16
+ export { YEAFT_PLUGINS_CAPABILITY, YEAFT_PLUGINS_UNSUPPORTED_ERROR };
17
+
18
+ async function rejectUnsupportedYeaftPlugins(client, msg, agentId) {
19
+ await sendToWebClient(client, {
20
+ type: msg.type === 'update_yeaft_plugins' ? 'yeaft_plugins_updated' : 'yeaft_plugins',
21
+ agentId,
22
+ requestId: msg.requestId || null,
23
+ plugins: {},
24
+ error: YEAFT_PLUGINS_UNSUPPORTED_ERROR,
25
+ });
26
+ }
11
27
 
12
28
  export function requiresManualUpgradeBridge(capabilities, platform = null) {
13
29
  if (Array.isArray(capabilities) && capabilities.includes(SAFE_REMOTE_UPGRADE_CAPABILITY)) return false;
@@ -204,6 +220,10 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
204
220
  const targetAgentId = msg.agentId || client.currentAgent;
205
221
  if (!targetAgentId) break;
206
222
  if (!await checkAgentAccess(targetAgentId)) break;
223
+ if (!agentSupportsYeaftPlugins(agents.get(targetAgentId))) {
224
+ await rejectUnsupportedYeaftPlugins(client, msg, targetAgentId);
225
+ break;
226
+ }
207
227
  await forwardToAgent(targetAgentId, {
208
228
  type: 'get_yeaft_plugins',
209
229
  requestId: msg.requestId || null,
@@ -215,6 +235,10 @@ export async function handleClientMisc(clientId, client, msg, checkAgentAccess)
215
235
  const targetAgentId = msg.agentId || client.currentAgent;
216
236
  if (!targetAgentId) break;
217
237
  if (!await checkAgentAccess(targetAgentId)) break;
238
+ if (!agentSupportsYeaftPlugins(agents.get(targetAgentId))) {
239
+ await rejectUnsupportedYeaftPlugins(client, msg, targetAgentId);
240
+ break;
241
+ }
218
242
  const hasPlugins = Object.prototype.hasOwnProperty.call(msg, 'plugins');
219
243
  const hasConfig = Object.prototype.hasOwnProperty.call(msg, 'config');
220
244
  await forwardToAgent(targetAgentId, {
@@ -120,7 +120,13 @@ export function handleAgentConnection(ws, url) {
120
120
  return;
121
121
  }
122
122
 
123
- const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
123
+ const authCapabilitiesProvided = Array.isArray(msg.capabilities);
124
+ // Modern Agents always include this query key, including for an
125
+ // explicit empty list. The key's presence is the metadata signal;
126
+ // its value only determines the effective capability list.
127
+ const urlCapabilitiesProvided = url.searchParams.has('capabilities');
128
+ const capabilityMetadataProvided = authCapabilitiesProvided || urlCapabilitiesProvided;
129
+ const capabilities = authCapabilitiesProvided ? msg.capabilities : urlCapabilities;
124
130
  const agentVersion = msg.version || null;
125
131
  const agentPlatform = typeof msg.platform === 'string' && msg.platform
126
132
  ? msg.platform
@@ -152,6 +158,7 @@ export function handleAgentConnection(ws, url) {
152
158
  pending.workDir,
153
159
  authResult.sessionKey,
154
160
  capabilities,
161
+ capabilityMetadataProvided,
155
162
  ownerId,
156
163
  ownerUsername,
157
164
  agentVersion,
@@ -243,7 +250,7 @@ function handleAgentDisconnect(agentId, agentName, ws) {
243
250
  broadcastAgentList();
244
251
  }
245
252
 
246
- function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey, capabilities = [], ownerId = null, ownerUsername = null, agentVersion = null, instanceId = null, agentPlatform = null) {
253
+ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey, capabilities = [], capabilityMetadataProvided = false, ownerId = null, ownerUsername = null, agentVersion = null, instanceId = null, agentPlatform = null) {
247
254
  // 如果是重连,保留 conversations;否则(server 重启)创建空 Map
248
255
  const existingAgent = agents.get(agentId);
249
256
  const conversations = existingAgent?.conversations || new Map();
@@ -274,6 +281,9 @@ function completeAgentRegistration(ws, agentId, agentName, workDir, sessionKey,
274
281
  isAlive: true,
275
282
  lastSeenAt: Date.now(),
276
283
  capabilities: effectiveCapabilities,
284
+ // The fallback list supports old UI features but cannot prove that an
285
+ // Agent made an explicit capability claim.
286
+ capabilityMetadataProvided,
277
287
  proxyPorts,
278
288
  slashCommands,
279
289
  slashCommandDescriptions,
@@ -352,6 +362,7 @@ async function handleAgentMessage(agentId, msg, ws) {
352
362
  'file_content', 'file_saved', 'file_op_result', 'file_search_result',
353
363
  'git_status_result', 'git_diff_result', 'git_op_result',
354
364
  'terminal_created', 'terminal_output', 'terminal_closed', 'terminal_error',
365
+ 'agent_capabilities_updated', 'browser_runtime_status_result', 'browser_runtime_install_progress', 'browser_runtime_error',
355
366
  'browser_session_created', 'browser_session_error', 'browser_session_snapshot', 'browser_session_list_result',
356
367
  'browser_peer_prepared', 'browser_peer_offer', 'browser_peer_ice_candidate', 'browser_peer_state', 'browser_peer_error'
357
368
  ]);
@@ -6,7 +6,12 @@ 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, BROWSER_RUNTIME_PROTOCOL, WORKBENCH_ROUTE_PROTOCOL } from './client-protocol.js';
9
+ import {
10
+ applyClientHello,
11
+ BROWSER_RUNTIME_PROTOCOL,
12
+ BROWSER_RUNTIME_SETUP_PROTOCOL,
13
+ WORKBENCH_ROUTE_PROTOCOL,
14
+ } from './client-protocol.js';
10
15
  import { clearWorkbenchCorrelationsForClient } from './workbench-correlation.js';
11
16
  import { clearBrowserRuntimeForClient } from './browser-runtime-routes.js';
12
17
  import {
@@ -88,6 +93,7 @@ export function handleWebConnection(ws, url, req = {}) {
88
93
  // Explicit protocols have no omission-based downgrade for security fields.
89
94
  workbenchRouteProtocol: 0,
90
95
  browserRuntimeProtocol: 0,
96
+ browserRuntimeSetupProtocol: 0,
91
97
  });
92
98
 
93
99
  // 心跳响应处理
@@ -116,6 +122,7 @@ export function handleWebConnection(ws, url, req = {}) {
116
122
  yeaftSessionInventoryComplete: true,
117
123
  workbenchRouteProtocol: WORKBENCH_ROUTE_PROTOCOL,
118
124
  browserRuntimeProtocol: BROWSER_RUNTIME_PROTOCOL,
125
+ browserRuntimeSetupProtocol: BROWSER_RUNTIME_SETUP_PROTOCOL,
119
126
  browserRuntimeEnabled: CONFIG.browserRuntime.enabled,
120
127
  }));
121
128
  setTimeout(() => broadcastAgentList(), 100);
@@ -249,6 +256,7 @@ async function handleWebMessage(clientId, msg) {
249
256
  type: 'client_hello_ack',
250
257
  workbenchRouteProtocol: client.workbenchRouteProtocol,
251
258
  browserRuntimeProtocol: client.browserRuntimeProtocol,
259
+ browserRuntimeSetupProtocol: client.browserRuntimeSetupProtocol,
252
260
  browserRuntimeEnabled: CONFIG.browserRuntime.enabled,
253
261
  });
254
262
  return;
@@ -169,6 +169,7 @@ export async function broadcastAgentList() {
169
169
  status: agent.status || 'ready',
170
170
  latency: agent.latency || null,
171
171
  capabilities: agent.capabilities || ['terminal', 'file_editor', 'background_tasks'],
172
+ ...(agent.capabilityMetadataProvided === true ? { capabilityMetadataProvided: true } : {}),
172
173
  version: agent.version || null,
173
174
  yeaftStatus: agent.yeaftStatus || null,
174
175
  proxyPorts: agent.proxyPorts || [],
@@ -0,0 +1,11 @@
1
+ // Plugin protocol support is omission-compatible: old Agents that never
2
+ // advertised capability metadata keep the legacy relay path. Once an Agent
3
+ // explicitly provides metadata, omitting this token is an explicit refusal.
4
+ export const YEAFT_PLUGINS_CAPABILITY = 'yeaft_plugins';
5
+ export const YEAFT_PLUGINS_UNSUPPORTED_ERROR = 'The selected Agent does not support Plugins; upgrade and restart the Agent';
6
+
7
+ export function agentSupportsYeaftPlugins(agent) {
8
+ if (agent?.capabilityMetadataProvided !== true) return true;
9
+ return Array.isArray(agent.capabilities)
10
+ && agent.capabilities.includes(YEAFT_PLUGINS_CAPABILITY);
11
+ }
@@ -1 +1 @@
1
- {"version":"1.0.417"}
1
+ {"version":"1.0.419"}