zyn-ai 1.3.7 → 1.4.0
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/README.md +153 -92
- package/package.json +22 -11
- package/src/agent.js +168 -0
- package/src/cli/commands.js +459 -164
- package/src/cli/print.js +34 -0
- package/src/cli/runtime.js +122 -26
- package/src/cli/selector.js +223 -0
- package/src/config.js +134 -33
- package/src/core/agent.js +119 -223
- package/src/core/prompts.js +181 -123
- package/src/core/skills.js +166 -59
- package/src/i18n.js +0 -14
- package/src/platforms/baileys.js +75 -0
- package/src/platforms/discord.js +66 -0
- package/src/platforms/index.js +28 -0
- package/src/platforms/telegram.js +55 -0
- package/src/providers/catalog.js +216 -22
- package/src/providers/custom/index.js +92 -0
- package/src/providers/deepseek/index.js +317 -0
- package/src/providers/deepseek/sha3_wasm_bg.wasm +0 -0
- package/src/providers/gemini/index.js +117 -337
- package/src/providers/huggingface/index.js +149 -0
- package/src/providers/qwenapi/index.js +132 -0
- package/src/providers/scraperClient.js +27 -10
- package/src/providers/zen/index.js +84 -56
- package/src/public/helpers.js +25 -0
- package/src/tools/index.js +301 -146
- package/src/tui/app.mjs +685 -114
- package/src/utils/backgroundWorker.js +116 -0
- package/src/utils/sessionStorage.js +92 -7
- package/src/web/server.js +54 -1
- package/src/web/store.js +23 -0
- package/src/web/webAgent.js +2 -2
package/src/cli/commands.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { spawn } = require('child_process');
|
|
4
3
|
|
|
5
4
|
const fsp = fs.promises;
|
|
6
|
-
const { listSkills, SKILLS_DIR } = require('../core/skills');
|
|
7
5
|
const { DEFAULT_LANGUAGE, DEFAULT_MODEL_KEY, GEMINI_MODEL_WARNING, MODELS, listProvidersFromModels } = require('../config');
|
|
8
6
|
const { languageLabel, normalizeLanguage, t } = require('../i18n');
|
|
9
|
-
const { createNewSessionState, listSessions, loadSessionState, saveState } = require('../utils/sessionStorage');
|
|
10
|
-
const { compactMemory, normalizeCompactMode } = require('../core/agent');
|
|
7
|
+
const { createNewSessionState, listSessions, loadSessionState, saveState, listBackgroundResults, consumeBackgroundResult, enqueueBackgroundTask } = require('../utils/sessionStorage');
|
|
11
8
|
const { listGitSecrets, removeGitSecret, upsertGitSecret } = require('../utils/secretStorage');
|
|
12
9
|
const { clearGmailAuth, getGmailAuthStatus, startGmailOAuthFlow } = require('../utils/gmailAuth');
|
|
13
10
|
const { exportTranscriptText, formatTranscriptPreview } = require('../utils/transcriptStorage');
|
|
14
11
|
const { resolveInputPath } = require('../utils/pathUtils');
|
|
15
|
-
const {
|
|
12
|
+
const { detachBackgroundTurn } = require('../utils/backgroundWorker');
|
|
13
|
+
const {
|
|
14
|
+
describeProviderConfig,
|
|
15
|
+
fetchProviderModels,
|
|
16
|
+
getActiveModelsForProvider,
|
|
17
|
+
listConfiguredProviders,
|
|
18
|
+
maskSecret,
|
|
19
|
+
removeProviderConfig,
|
|
20
|
+
setProviderField,
|
|
21
|
+
summarizeProviderConfig,
|
|
22
|
+
syncProvider,
|
|
23
|
+
unsetProviderField,
|
|
24
|
+
} = require('../providers/catalog');
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
function getModelWarning(key) {
|
|
@@ -34,41 +43,6 @@ function printLanguageChanged(language) {
|
|
|
34
43
|
: `Updated to language ${label} (${normalized})`);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
function getCompactModeArgs(args) {
|
|
38
|
-
const value = String(args || '').trim().toLowerCase();
|
|
39
|
-
if (!value) return 'medium';
|
|
40
|
-
const [mode] = value.split(/\s+/);
|
|
41
|
-
return ['low', 'medium', 'high'].includes(mode) ? mode : null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function runCompactCommand(state, args, printMemoryFn) {
|
|
45
|
-
const compactMode = getCompactModeArgs(args);
|
|
46
|
-
if (!compactMode) {
|
|
47
|
-
throw new Error(t(state.language, 'compactInvalid'));
|
|
48
|
-
}
|
|
49
|
-
const isHigh = compactMode === 'high';
|
|
50
|
-
if (isHigh) {
|
|
51
|
-
const warn = t(state.language, 'compactWarningHigh');
|
|
52
|
-
console.log(warn);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const ui = {
|
|
56
|
-
logEvent: (_state, kind, title, detail) => {
|
|
57
|
-
const prefix = kind === 'error' ? 'ERROR' : kind === 'warn' ? 'WARN' : 'INFO';
|
|
58
|
-
const line = [prefix, title, detail].filter(Boolean).join(' - ');
|
|
59
|
-
if (line) console.log(line);
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const result = await compactMemory(state, ui, compactMode, { force: !isHigh });
|
|
64
|
-
if (!result?.changed) {
|
|
65
|
-
console.log(t(state.language, 'compactNoChange'));
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
console.log(t(state.language, 'compactCommand', { mode: compactMode }));
|
|
69
|
-
if (typeof printMemoryFn === 'function') printMemoryFn(state);
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
46
|
|
|
73
47
|
const SLASH_COMMANDS = [
|
|
74
48
|
{ name: 'help', desc: 'full help', descEs: 'ayuda completa' },
|
|
@@ -76,32 +50,32 @@ const SLASH_COMMANDS = [
|
|
|
76
50
|
{ name: 'history', desc: 'recent actions', descEs: 'acciones recientes' },
|
|
77
51
|
{ name: 'memory', desc: 'memory summary', descEs: 'resumen de memoria' },
|
|
78
52
|
{ name: 'summary', desc: 'memory summary', descEs: 'resumen de memoria' },
|
|
79
|
-
{ name: 'compact', desc: 'compact memory', descEs: 'compactar memoria' },
|
|
80
53
|
{ name: 'session', desc: 'current session', descEs: 'sesión actual' },
|
|
81
54
|
{ name: 'sessions', desc: 'list sessions', descEs: 'listar sesiones' },
|
|
82
55
|
{ name: 'new', desc: 'new session', descEs: 'nueva sesión' },
|
|
83
56
|
{ name: 'resume', desc: 'resume session', descEs: 'reanudar sesión' },
|
|
84
57
|
{ name: 'title', desc: 'rename session', descEs: 'renombrar sesión' },
|
|
85
58
|
{ name: 'rename', desc: 'rename session', descEs: 'renombrar sesión' },
|
|
86
|
-
{ name: 'model', desc: 'view/change model', descEs: 'ver/cambiar modelo' },
|
|
87
59
|
{ name: 'models', desc: 'list models', descEs: 'listar modelos' },
|
|
88
|
-
{ name: 'providers', desc: 'list providers', descEs: 'listar proveedores' },
|
|
60
|
+
{ name: 'providers', desc: 'list/select providers', descEs: 'listar/seleccionar proveedores' },
|
|
61
|
+
{ name: 'provider', desc: 'manage providers (sync, set, list, remove)', descEs: 'gestionar proveedores (sync, set, list, remove)' },
|
|
89
62
|
{ name: 'git', desc: 'configure git credentials', descEs: 'configurar credenciales git' },
|
|
90
63
|
{ name: 'gmail', desc: 'connect Gmail account', descEs: 'conectar cuenta Gmail' },
|
|
91
64
|
{ name: 'persona', desc: 'set response tone/personality', descEs: 'definir tono/persona' },
|
|
92
65
|
{ name: 'lang', desc: 'change language', descEs: 'cambiar idioma' },
|
|
93
66
|
{ name: 'language', desc: 'change language', descEs: 'cambiar idioma' },
|
|
94
67
|
{ name: 'auto', desc: 'auto-approval', descEs: 'auto-aprobación' },
|
|
95
|
-
{ name: 'concuerdo', desc: 'group model mode', descEs: 'modo de grupo' },
|
|
96
|
-
{ name: 'tools', desc: 'tools', descEs: 'herramientas' },
|
|
97
|
-
{ name: 'skills', desc: 'agent skills', descEs: 'skills del agente' },
|
|
98
68
|
{ name: 'config', desc: 'view/change session settings', descEs: 'ver/cambiar configuración' },
|
|
99
|
-
{ name: '
|
|
69
|
+
{ name: 'bg', desc: 'background: continue current turn in background', descEs: 'segundo plano: continuar el turno en background' },
|
|
70
|
+
{ name: 'undo', desc: 'undo last turn', descEs: 'deshacer último turno' },
|
|
71
|
+
{ name: 'redo', desc: 'redo last turn', descEs: 'rehacer último turno' },
|
|
100
72
|
{ name: 'stop', desc: 'stop agent', descEs: 'detener agente' },
|
|
101
73
|
{ name: 'abort', desc: 'stop agent', descEs: 'detener agente' },
|
|
102
74
|
{ name: 'reset', desc: 'reset context', descEs: 'reiniciar contexto' },
|
|
103
75
|
{ name: 'clear', desc: 'reset context', descEs: 'reiniciar contexto' },
|
|
104
76
|
{ name: 'cwd', desc: 'working directory', descEs: 'directorio de trabajo' },
|
|
77
|
+
{ name: 'compact', desc: 'compact memory', descEs: 'compactar memoria' },
|
|
78
|
+
{ name: 'theme', desc: 'UI theme', descEs: 'tema de la UI' },
|
|
105
79
|
{ name: 'transcript', desc: 'view transcript', descEs: 'ver transcripción' },
|
|
106
80
|
{ name: 'export', desc: 'export to txt', descEs: 'exportar a txt' },
|
|
107
81
|
{ name: 'exit', desc: 'exit', descEs: 'salir' },
|
|
@@ -151,17 +125,14 @@ function printHelp(state = {}) {
|
|
|
151
125
|
|
|
152
126
|
// Configuration
|
|
153
127
|
console.log(` ${paint('── Configuration ──', 'dim')}`);
|
|
154
|
-
console.log(` ${b('/
|
|
155
|
-
console.log(` ${b('/
|
|
156
|
-
console.log(` ${b('/models')} List available models`);
|
|
157
|
-
console.log(` ${b('/providers')} List detected providers`);
|
|
128
|
+
console.log(` ${b('/models')} Open model picker`);
|
|
129
|
+
console.log(` ${b('/providers')} Open provider picker`);
|
|
158
130
|
console.log(` ${b('/lang')} Show current language`);
|
|
159
131
|
console.log(` ${b('/lang <en|es>')} Change language`);
|
|
160
132
|
console.log(` ${b('/language <en|es>')} Alias of /lang`);
|
|
161
133
|
console.log(` ${b('/auto')} Show auto-approval status`);
|
|
162
134
|
console.log(` ${b('/auto on')} Enable auto-approval`);
|
|
163
135
|
console.log(` ${b('/auto off')} Disable auto-approval`);
|
|
164
|
-
console.log(` ${b('/concuerdo')} Toggle group model mode`);
|
|
165
136
|
console.log(` ${b('/persona set <text>')} Set response persona/tone`);
|
|
166
137
|
console.log(` ${b('/persona show')} Show active persona`);
|
|
167
138
|
console.log(` ${b('/persona reset')} Reset to default persona`);
|
|
@@ -169,14 +140,11 @@ function printHelp(state = {}) {
|
|
|
169
140
|
console.log(` ${b('/config lang <en|es>')} Change language from config`);
|
|
170
141
|
console.log(` ${b('/config model <key>')} Change model from config`);
|
|
171
142
|
console.log(` ${b('/config auto on|off')} Toggle auto from config`);
|
|
172
|
-
console.log(` ${b('/config group on|off')} Toggle group mode from config`);
|
|
173
143
|
console.log(` ${b('/config cwd <path>')} Change working dir from config`);
|
|
174
144
|
console.log('');
|
|
175
145
|
|
|
176
146
|
// Tools and Git
|
|
177
147
|
console.log(` ${paint('── Tools and Git ──', 'dim')}`);
|
|
178
|
-
console.log(` ${b('/tools')} List available agent tools`);
|
|
179
|
-
console.log(` ${b('/skills')} List loaded skills`);
|
|
180
148
|
console.log(` ${b('/git set <provider> <token>')} Configure git credentials`);
|
|
181
149
|
console.log(` ${b('/git set <provider> <token> [user] [apiBaseUrl:URL] [cloneBaseUrl:URL] [name:X]')}`);
|
|
182
150
|
console.log(` ${b('/git list')} List configured git profiles`);
|
|
@@ -188,10 +156,9 @@ function printHelp(state = {}) {
|
|
|
188
156
|
console.log(` ${b('/cwd <path>')} Change working directory`);
|
|
189
157
|
console.log('');
|
|
190
158
|
|
|
191
|
-
//
|
|
192
|
-
console.log(` ${paint('──
|
|
193
|
-
console.log(` ${b('/
|
|
194
|
-
console.log(` ${b('/web <host:port>')} Open web version on custom host:port`);
|
|
159
|
+
// Export and background
|
|
160
|
+
console.log(` ${paint('── Export and Background ──', 'dim')}`);
|
|
161
|
+
console.log(` ${b('/bg')} Detach current turn to a background worker`);
|
|
195
162
|
console.log(` ${b('/transcript')} View full session transcript`);
|
|
196
163
|
console.log(` ${b('/export')} Export session to txt`);
|
|
197
164
|
console.log(` ${b('/export <path>')} Export session to specific path`);
|
|
@@ -231,6 +198,155 @@ function printModels() {
|
|
|
231
198
|
console.log('');
|
|
232
199
|
}
|
|
233
200
|
|
|
201
|
+
function buildModelListItems() {
|
|
202
|
+
const providers = listProvidersFromModels(MODELS);
|
|
203
|
+
const items = [];
|
|
204
|
+
for (const provider of providers) {
|
|
205
|
+
for (const model of provider.models) {
|
|
206
|
+
const isActive = model.key === (global.__zynActiveModel || DEFAULT_MODEL_KEY);
|
|
207
|
+
items.push({
|
|
208
|
+
key: model.key,
|
|
209
|
+
label: `${model.key.padEnd(22)} ${model.label}`,
|
|
210
|
+
provider: provider.key,
|
|
211
|
+
active: isActive,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return items;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildProviderListItems() {
|
|
219
|
+
const fromModels = listProvidersFromModels(MODELS);
|
|
220
|
+
const configured = listConfiguredProviders();
|
|
221
|
+
const configuredKeys = new Set(configured.map(p => p.provider));
|
|
222
|
+
const modelKeys = new Set(fromModels.map(p => p.key));
|
|
223
|
+
for (const p of configured) {
|
|
224
|
+
if (!modelKeys.has(p.provider)) {
|
|
225
|
+
fromModels.push({
|
|
226
|
+
key: p.provider,
|
|
227
|
+
label: p.provider,
|
|
228
|
+
models: (p.models || []).map(m => typeof m === 'string' ? { key: m, label: m } : m),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return fromModels.map(p => ({
|
|
233
|
+
key: p.key,
|
|
234
|
+
label: `${p.key} ${p.models.length} model${p.models.length === 1 ? '' : 's'}`,
|
|
235
|
+
models: p.models,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function runModelSelector(state, deps) {
|
|
240
|
+
const items = buildModelListItems();
|
|
241
|
+
if (items.length === 0) return null;
|
|
242
|
+
const active = state.activeModel || DEFAULT_MODEL_KEY;
|
|
243
|
+
const initialIndex = Math.max(0, items.findIndex(it => it.key === active));
|
|
244
|
+
const choice = await deps.askSelect({
|
|
245
|
+
title: state.language === 'es' ? 'Selecciona un modelo' : 'Select a model',
|
|
246
|
+
subtitle: state.language === 'es' ? '↑/↓ navega · Enter elige · Esc cancela' : '↑/↓ move · Enter pick · Esc cancel',
|
|
247
|
+
items,
|
|
248
|
+
initialIndex: initialIndex >= 0 ? initialIndex : 0,
|
|
249
|
+
getLabel: (item) => `${item.active ? '● ' : ' '}${item.label}`,
|
|
250
|
+
getValue: (item) => item.key,
|
|
251
|
+
isActive: (item) => item.active,
|
|
252
|
+
});
|
|
253
|
+
return choice || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function runProviderSelector(state, deps) {
|
|
257
|
+
const items = buildProviderListItems();
|
|
258
|
+
const addCustomKey = '__add_custom__';
|
|
259
|
+
items.push({ key: addCustomKey, label: '', models: [] });
|
|
260
|
+
const es = state?.language === 'es';
|
|
261
|
+
const choice = await deps.askSelect({
|
|
262
|
+
title: es ? 'Selecciona un proveedor' : 'Select a provider',
|
|
263
|
+
subtitle: es ? '↑/↓ navega · Enter elige · Esc cancela' : '↑/↓ move · Enter pick · Esc cancel',
|
|
264
|
+
items,
|
|
265
|
+
getLabel: (item) => {
|
|
266
|
+
if (item.key === addCustomKey) return '+ ' + (es ? 'Agregar proveedor personalizado' : 'Add custom provider');
|
|
267
|
+
return item.label;
|
|
268
|
+
},
|
|
269
|
+
getValue: (item) => item.key,
|
|
270
|
+
});
|
|
271
|
+
if (choice === addCustomKey) {
|
|
272
|
+
const name = await deps.askInput({
|
|
273
|
+
title: es ? 'Nombre del proveedor personalizado' : 'Custom provider name',
|
|
274
|
+
prompt: es ? 'Ej: ollama, groq, anthropic' : 'E.g.: ollama, groq, anthropic',
|
|
275
|
+
defaultValue: 'custom',
|
|
276
|
+
});
|
|
277
|
+
if (!name) return null;
|
|
278
|
+
const baseUrl = await deps.askInput({
|
|
279
|
+
title: es ? 'URL base de la API' : 'API base URL',
|
|
280
|
+
prompt: 'baseUrl',
|
|
281
|
+
defaultValue: '',
|
|
282
|
+
});
|
|
283
|
+
if (baseUrl) setProviderField(name, 'baseUrl', baseUrl);
|
|
284
|
+
const apiKey = await deps.askInput({
|
|
285
|
+
title: es ? `API Key para ${name} (opcional)` : `API Key for ${name} (optional)`,
|
|
286
|
+
prompt: 'apiKey',
|
|
287
|
+
hidden: true,
|
|
288
|
+
defaultValue: '',
|
|
289
|
+
});
|
|
290
|
+
if (apiKey) setProviderField(name, 'apiKey', apiKey);
|
|
291
|
+
const modelId = await deps.askInput({
|
|
292
|
+
title: es ? `Model ID para ${name}` : `Model ID for ${name}`,
|
|
293
|
+
prompt: 'modelId',
|
|
294
|
+
defaultValue: '',
|
|
295
|
+
});
|
|
296
|
+
if (modelId) setProviderField(name, 'modelId', modelId);
|
|
297
|
+
const ctxLen = await deps.askInput({
|
|
298
|
+
title: es ? `Contexto máximo (tokens) para ${name}` : `Max context length (tokens) for ${name}`,
|
|
299
|
+
prompt: es ? 'Ej: 128000 (vacio = 128K)' : 'E.g.: 128000 (empty = 128K)',
|
|
300
|
+
defaultValue: '',
|
|
301
|
+
});
|
|
302
|
+
if (ctxLen && /^\d+$/.test(ctxLen)) setProviderField(name, 'contextLength', ctxLen);
|
|
303
|
+
console.log(es
|
|
304
|
+
? `Proveedor "${name}" agregado. /provider sync ${name} para sincronizar.`
|
|
305
|
+
: `Provider "${name}" added. /provider sync ${name} to sync models.`);
|
|
306
|
+
console.log(es ? ' Campos editables: apiKey, baseUrl, modelId, contextLength' : ' Editable fields: apiKey, baseUrl, modelId, contextLength');
|
|
307
|
+
return name;
|
|
308
|
+
}
|
|
309
|
+
return choice || null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function runProviderModelsSelector(state, deps, providerKey) {
|
|
313
|
+
const providerGroup = listProvidersFromModels(MODELS).find(p => p.key === providerKey);
|
|
314
|
+
if (!providerGroup) return null;
|
|
315
|
+
const items = providerGroup.models.map(m => ({
|
|
316
|
+
key: m.key,
|
|
317
|
+
label: `${m.key.padEnd(22)} ${m.label}`,
|
|
318
|
+
active: m.key === (state.activeModel || DEFAULT_MODEL_KEY),
|
|
319
|
+
}));
|
|
320
|
+
if (items.length === 0) return null;
|
|
321
|
+
const initialIndex = Math.max(0, items.findIndex(it => it.active));
|
|
322
|
+
return deps.askSelect({
|
|
323
|
+
title: state.language === 'es' ? `Modelos de ${providerKey}` : `Models in ${providerKey}`,
|
|
324
|
+
subtitle: state.language === 'es' ? 'Elige un modelo · Esc vuelve' : 'Pick a model · Esc back',
|
|
325
|
+
items,
|
|
326
|
+
initialIndex,
|
|
327
|
+
getLabel: (item) => `${item.active ? '● ' : ' '}${item.label}`,
|
|
328
|
+
getValue: (item) => item.key,
|
|
329
|
+
isActive: (item) => item.active,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function switchActiveModel(state, deps, key) {
|
|
334
|
+
if (!MODELS[key]) {
|
|
335
|
+
const available = Object.keys(MODELS).join(', ');
|
|
336
|
+
throw new Error(`${t(state.language, 'modelInvalid')}: ${available}`);
|
|
337
|
+
}
|
|
338
|
+
state.activeModel = key;
|
|
339
|
+
global.__zynActiveModel = key;
|
|
340
|
+
await saveState(state);
|
|
341
|
+
if (deps.appendTranscriptEntry) {
|
|
342
|
+
await deps.appendTranscriptEntry(state.sessionId, {
|
|
343
|
+
type: 'system',
|
|
344
|
+
content: `Model switched to: ${MODELS[key].label}${getModelWarning(key) ? `\nWarning: ${getModelWarning(key)}` : ''}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return key;
|
|
348
|
+
}
|
|
349
|
+
|
|
234
350
|
function printConfig(state) {
|
|
235
351
|
const key = state.activeModel || DEFAULT_MODEL_KEY;
|
|
236
352
|
const model = MODELS[key];
|
|
@@ -241,7 +357,6 @@ function printConfig(state) {
|
|
|
241
357
|
console.log(` Model : ${key} (${model?.label || '?'})`);
|
|
242
358
|
console.log(` Provider : ${provider}`);
|
|
243
359
|
console.log(` Auto : ${state.autoApprove ? 'on' : 'off'}`);
|
|
244
|
-
console.log(` Group : ${state.concuerdo ? 'on' : 'off'}`);
|
|
245
360
|
console.log(` CWD : ${state.cwd}`);
|
|
246
361
|
console.log('');
|
|
247
362
|
console.log(' Commands:');
|
|
@@ -253,18 +368,6 @@ function printConfig(state) {
|
|
|
253
368
|
console.log('');
|
|
254
369
|
}
|
|
255
370
|
|
|
256
|
-
async function startWebVersion(host = '127.0.0.1', port = 3000) {
|
|
257
|
-
const serverPath = path.join(__dirname, '..', 'web', 'server.js');
|
|
258
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
259
|
-
detached: true,
|
|
260
|
-
stdio: 'ignore',
|
|
261
|
-
windowsHide: true,
|
|
262
|
-
env: { ...process.env, HOST: host, PORT: String(port) },
|
|
263
|
-
});
|
|
264
|
-
child.unref();
|
|
265
|
-
return `http://${host}:${port}`;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
371
|
async function handleLocalCommand(input, state, deps) {
|
|
269
372
|
const parsed = parseSlashCommand(input);
|
|
270
373
|
if (!parsed) return false;
|
|
@@ -279,6 +382,9 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
279
382
|
printSession,
|
|
280
383
|
printSessions: renderSessions,
|
|
281
384
|
printStatus,
|
|
385
|
+
askSelect,
|
|
386
|
+
askInput,
|
|
387
|
+
askConfirm,
|
|
282
388
|
} = deps;
|
|
283
389
|
|
|
284
390
|
if (commandName === 'help') {
|
|
@@ -297,17 +403,10 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
297
403
|
}
|
|
298
404
|
|
|
299
405
|
if (commandName === 'memory' || commandName === 'summary') {
|
|
300
|
-
if (args && args.startsWith('compact')) {
|
|
301
|
-
const compactArgs = args.replace(/^compact\s*/i, '').trim();
|
|
302
|
-
return runCompactCommand(state, compactArgs, printMemory);
|
|
303
|
-
}
|
|
304
406
|
printMemory(state);
|
|
305
407
|
return true;
|
|
306
408
|
}
|
|
307
409
|
|
|
308
|
-
if (commandName === 'compact') {
|
|
309
|
-
return runCompactCommand(state, args, printMemory);
|
|
310
|
-
}
|
|
311
410
|
|
|
312
411
|
if (commandName === 'session') {
|
|
313
412
|
printSession(state);
|
|
@@ -503,16 +602,6 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
503
602
|
return true;
|
|
504
603
|
}
|
|
505
604
|
|
|
506
|
-
if (sub === 'group' || sub === 'concuerdo') {
|
|
507
|
-
if (value !== 'on' && value !== 'off') {
|
|
508
|
-
throw new Error('Use /config group on|off');
|
|
509
|
-
}
|
|
510
|
-
state.concuerdo = value === 'on';
|
|
511
|
-
await saveState(state);
|
|
512
|
-
console.log(state.concuerdo ? 'Group mode enabled.' : 'Group mode disabled.');
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
605
|
if (sub === 'cwd' || sub === 'pwd') {
|
|
517
606
|
if (!value) {
|
|
518
607
|
throw new Error(t(state.language, 'missingPath'));
|
|
@@ -531,48 +620,94 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
531
620
|
throw new Error('Use /config show|lang|model|auto|group|cwd');
|
|
532
621
|
}
|
|
533
622
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
623
|
+
function isProviderConfigured(providerKey) {
|
|
624
|
+
const config = describeProviderConfig(providerKey);
|
|
625
|
+
if (!config) return false;
|
|
626
|
+
if (providerKey === 'qwenapi') return Boolean(config.apiKey || process.env.ZYN_QWEN_API_KEY);
|
|
627
|
+
if (providerKey === 'gemini') return Boolean(config.apiKey || process.env.ZYN_GEMINI_API_KEY);
|
|
628
|
+
if (providerKey === 'huggingface') return Boolean(config.apiKey || process.env.ZYN_HUGGINGFACE_TOKEN || process.env.HF_TOKEN);
|
|
629
|
+
if (providerKey === 'deepseek') return Boolean(config.apiKey || process.env.ZYN_DEEPSEEK_CHAT_KEY || process.env.DEEPSEEK_CHAT_KEY || process.env.ZYN_CHAT_KEY);
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function configureProviderInteractive(state, deps, providerKey) {
|
|
634
|
+
const es = state?.language === 'es';
|
|
635
|
+
const fields = [];
|
|
636
|
+
if (providerKey === 'qwenapi') {
|
|
637
|
+
fields.push({ name: 'apiKey', hidden: true, prompt: es ? 'API Key (dashscope)' : 'API Key (DashScope)' });
|
|
638
|
+
} else if (providerKey === 'gemini') {
|
|
639
|
+
fields.push({ name: 'apiKey', hidden: true, prompt: es ? 'API Key (Google AI Studio)' : 'API Key (Google AI Studio)' });
|
|
640
|
+
} else if (providerKey === 'huggingface') {
|
|
641
|
+
fields.push({ name: 'apiKey', hidden: true, prompt: es ? 'Token (huggingface.co/settings/tokens)' : 'Token (huggingface.co/settings/tokens)' });
|
|
642
|
+
} else if (providerKey === 'deepseek') {
|
|
643
|
+
fields.push({ name: 'apiKey', hidden: true, prompt: es ? 'Bearer Token (chat.deepseek.com)' : 'Bearer Token (chat.deepseek.com)' });
|
|
644
|
+
fields.push({ name: 'hifLeim', hidden: true, prompt: es ? 'x-hif-leim header (opcional)' : 'x-hif-leim header (optional)' });
|
|
645
|
+
}
|
|
646
|
+
for (const field of fields) {
|
|
647
|
+
const value = await deps.askInput({
|
|
648
|
+
title: es ? `Configurar ${field.name} de ${providerKey}` : `Set ${field.name} for ${providerKey}`,
|
|
649
|
+
prompt: field.prompt,
|
|
650
|
+
hidden: field.hidden,
|
|
651
|
+
defaultValue: '',
|
|
652
|
+
});
|
|
653
|
+
if (value) {
|
|
654
|
+
setProviderField(providerKey, field.name, value);
|
|
540
655
|
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
541
658
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
659
|
+
async function runProvidersFlow(state, deps) {
|
|
660
|
+
const es = state?.language === 'es';
|
|
661
|
+
|
|
662
|
+
// Paso 1: Elegir proveedor
|
|
663
|
+
const provider = await runProviderSelector(state, deps);
|
|
664
|
+
if (!provider) return;
|
|
665
|
+
if (provider === '__add_custom__') {
|
|
666
|
+
// Handled inside runProviderSelector; nothing more to do.
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// If it's a custom provider that hasn't been synced yet, sync it now
|
|
670
|
+
const configured = listConfiguredProviders();
|
|
671
|
+
const isCustom = configured.some(p => p.provider === provider) && !listProvidersFromModels(MODELS).some(p => p.key === provider);
|
|
672
|
+
if (isCustom) {
|
|
673
|
+
try {
|
|
674
|
+
const syncedModels = await syncProvider(provider);
|
|
675
|
+
for (const m of syncedModels) MODELS[m.key] = m;
|
|
676
|
+
} catch (_) {
|
|
677
|
+
// sync may fail silently; user can /provider sync later
|
|
546
678
|
}
|
|
679
|
+
}
|
|
547
680
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
await
|
|
552
|
-
|
|
553
|
-
|
|
681
|
+
// Paso 2: Si es opcional y no configurado, preguntar si configurar
|
|
682
|
+
const optionalProviders = ['qwenapi', 'gemini', 'huggingface', 'deepseek'];
|
|
683
|
+
if (optionalProviders.includes(provider) && !isProviderConfigured(provider)) {
|
|
684
|
+
const configure = await deps.askConfirm({
|
|
685
|
+
title: es ? `Configurar ${provider} ahora?` : `Configure ${provider} now?`,
|
|
686
|
+
detail: es ? 'Puedes configurarlo despues con variables de entorno' : 'You can configure later with env vars',
|
|
554
687
|
});
|
|
555
|
-
|
|
556
|
-
|
|
688
|
+
if (configure) {
|
|
689
|
+
await configureProviderInteractive(state, deps, provider);
|
|
690
|
+
}
|
|
557
691
|
}
|
|
558
692
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
693
|
+
// Paso 3: Mostrar modelos del proveedor
|
|
694
|
+
const model = await runProviderModelsSelector(state, deps, provider);
|
|
695
|
+
if (model) {
|
|
696
|
+
await switchActiveModel(state, deps, model);
|
|
697
|
+
printModelChanged(model);
|
|
562
698
|
}
|
|
699
|
+
}
|
|
563
700
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
console.log('');
|
|
574
|
-
return true;
|
|
701
|
+
async function runModelsFlow(state, deps) {
|
|
702
|
+
const currentKey = state.activeModel || DEFAULT_MODEL_KEY;
|
|
703
|
+
const currentModel = MODELS[currentKey];
|
|
704
|
+
const currentProvider = currentModel?.provider || 'zen';
|
|
705
|
+
const model = await runProviderModelsSelector(state, deps, currentProvider);
|
|
706
|
+
if (model) {
|
|
707
|
+
await switchActiveModel(state, deps, model);
|
|
708
|
+
printModelChanged(model);
|
|
575
709
|
}
|
|
710
|
+
}
|
|
576
711
|
|
|
577
712
|
if (commandName === 'auto') {
|
|
578
713
|
if (!args) {
|
|
@@ -594,25 +729,6 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
594
729
|
return true;
|
|
595
730
|
}
|
|
596
731
|
|
|
597
|
-
if (commandName === 'concuerdo') {
|
|
598
|
-
state.concuerdo = !state.concuerdo;
|
|
599
|
-
await saveState(state);
|
|
600
|
-
const activeKey = state.activeModel || DEFAULT_MODEL_KEY;
|
|
601
|
-
const allLabels = Object.keys(MODELS).map(k => MODELS[k].label);
|
|
602
|
-
await appendTranscriptEntry(state.sessionId, {
|
|
603
|
-
type: 'system',
|
|
604
|
-
content: `Group mode: ${state.concuerdo ? 'on' : 'off'}`,
|
|
605
|
-
});
|
|
606
|
-
if (state.concuerdo) {
|
|
607
|
-
console.log(`Group mode enabled — ${allLabels.join(' + ')} work together.`);
|
|
608
|
-
console.log(` Primary: ${MODELS[activeKey]?.label || activeKey}`);
|
|
609
|
-
} else {
|
|
610
|
-
console.log('Group mode disabled.');
|
|
611
|
-
}
|
|
612
|
-
return true;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
732
|
if (commandName === 'gmail') {
|
|
617
733
|
const [subRaw, ...rest] = String(args || 'status').trim().split(/\s+/).filter(Boolean);
|
|
618
734
|
const sub = (subRaw || 'status').toLowerCase();
|
|
@@ -657,21 +773,24 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
657
773
|
throw new Error('Use /gmail connect|status|disconnect');
|
|
658
774
|
}
|
|
659
775
|
|
|
660
|
-
if (commandName === '
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
776
|
+
if (commandName === 'bg') {
|
|
777
|
+
if (!state.__bgDetach) {
|
|
778
|
+
console.log('No hay un turno activo para mandar a segundo plano.');
|
|
779
|
+
console.log(' /bg funciona después de enviar un mensaje; el worker procesa el turno y guarda el resultado.');
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
const { input, signal } = state.__bgDetach;
|
|
783
|
+
const sessionId = state.sessionId;
|
|
784
|
+
const taskId = enqueueBackgroundTask({ sessionId, input, detachedAt: new Date().toISOString() });
|
|
785
|
+
detachBackgroundTurn({ taskId, sessionId, input, cwd: state.cwd, modelKey: state.activeModel, language: state.language, personaPrompt: state.personaPrompt, autoApprove: state.autoApprove });
|
|
786
|
+
if (signal && !signal.aborted) signal.abort();
|
|
787
|
+
if (typeof deps.exitAfterBg === 'function') {
|
|
788
|
+
deps.exitAfterBg();
|
|
789
|
+
} else {
|
|
790
|
+
console.log(`Turno enviado a segundo plano. Task: ${taskId}`);
|
|
791
|
+
console.log(' El worker terminará el turno y guardará la respuesta en la sesión.');
|
|
792
|
+
console.log(' Vuelve a abrir zyn para ver el resultado.');
|
|
672
793
|
}
|
|
673
|
-
const url = await startWebVersion(host, port);
|
|
674
|
-
console.log(`Web version started at ${url}`);
|
|
675
794
|
return true;
|
|
676
795
|
}
|
|
677
796
|
|
|
@@ -684,20 +803,30 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
684
803
|
}
|
|
685
804
|
return true;
|
|
686
805
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
806
|
+
if (commandName === 'undo') {
|
|
807
|
+
const len = state.history.length;
|
|
808
|
+
if (len < 2) {
|
|
809
|
+
console.log('Nothing to undo.');
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
state.redoHistory = state.redoHistory || [];
|
|
813
|
+
const removed = state.history.splice(len - 2, 2);
|
|
814
|
+
state.redoHistory.push(...removed);
|
|
815
|
+
await saveState(state);
|
|
816
|
+
console.log('Last turn undone.');
|
|
690
817
|
return true;
|
|
691
818
|
}
|
|
692
819
|
|
|
693
|
-
if (commandName === '
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
820
|
+
if (commandName === 'redo') {
|
|
821
|
+
const stack = state.redoHistory || [];
|
|
822
|
+
if (stack.length < 2) {
|
|
823
|
+
console.log('Nothing to redo.');
|
|
824
|
+
return true;
|
|
698
825
|
}
|
|
699
|
-
|
|
700
|
-
|
|
826
|
+
const restored = stack.splice(stack.length - 2, 2);
|
|
827
|
+
state.history.push(...restored);
|
|
828
|
+
await saveState(state);
|
|
829
|
+
console.log('Last turn restored.');
|
|
701
830
|
return true;
|
|
702
831
|
}
|
|
703
832
|
|
|
@@ -749,6 +878,172 @@ async function handleLocalCommand(input, state, deps) {
|
|
|
749
878
|
return true;
|
|
750
879
|
}
|
|
751
880
|
|
|
881
|
+
if (commandName === 'models') {
|
|
882
|
+
if (askSelect) {
|
|
883
|
+
await runModelsFlow(state, deps);
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
printModels();
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (commandName === 'providers') {
|
|
891
|
+
if (askSelect) {
|
|
892
|
+
await runProvidersFlow(state, deps);
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
const providers = listProvidersFromModels(MODELS);
|
|
896
|
+
console.log('');
|
|
897
|
+
for (const provider of providers) {
|
|
898
|
+
console.log(` ${provider.key}`);
|
|
899
|
+
for (const model of provider.models) {
|
|
900
|
+
console.log(` ${model.key.padEnd(16)} ${model.label}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
console.log('');
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (commandName === 'provider') {
|
|
908
|
+
const [sub, ...rest] = args.split(' ').filter(Boolean);
|
|
909
|
+
const subArg = rest.join(' ').trim();
|
|
910
|
+
|
|
911
|
+
if (!sub || sub === 'help') {
|
|
912
|
+
console.log('');
|
|
913
|
+
console.log(' /provider list list configured providers');
|
|
914
|
+
console.log(' /provider sync <name> sync models for a provider');
|
|
915
|
+
console.log(' /provider set <name> <k> <v> set a config field');
|
|
916
|
+
console.log(' /provider remove <name> remove a provider config');
|
|
917
|
+
console.log('');
|
|
918
|
+
console.log(' Configurable fields (via set):');
|
|
919
|
+
console.log(' apiKey API key');
|
|
920
|
+
console.log(' baseUrl API base URL');
|
|
921
|
+
console.log(' modelId Model ID override');
|
|
922
|
+
console.log(' contextLength Max context tokens (e.g. 128000)');
|
|
923
|
+
console.log(' email/password Basic auth');
|
|
924
|
+
console.log(' modelEndpoint Custom models endpoint');
|
|
925
|
+
console.log(' chatEndpoint Custom chat endpoint');
|
|
926
|
+
console.log('');
|
|
927
|
+
console.log(' /providers interactive provider picker');
|
|
928
|
+
console.log('');
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (sub === 'list') {
|
|
933
|
+
const configured = listConfiguredProviders();
|
|
934
|
+
console.log('');
|
|
935
|
+
if (configured.length === 0) {
|
|
936
|
+
console.log(' No configured providers. Use /provider set <name> <key> <value> to add one.');
|
|
937
|
+
} else {
|
|
938
|
+
for (const p of configured) {
|
|
939
|
+
const name = p.provider || p.key || '?';
|
|
940
|
+
const summary = summarizeProviderConfig(name);
|
|
941
|
+
console.log(` ${name}`);
|
|
942
|
+
if (summary?.fields?.length) {
|
|
943
|
+
for (const f of summary.fields) {
|
|
944
|
+
console.log(` ${f.name}: ${f.value}`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
console.log('');
|
|
950
|
+
return true;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (sub === 'sync') {
|
|
954
|
+
if (!subArg) {
|
|
955
|
+
console.log(' Usage: /provider sync <provider-name>');
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
const models = await syncProvider(subArg);
|
|
960
|
+
for (const k of Object.keys(MODELS)) {
|
|
961
|
+
if (MODELS[k]?.provider === subArg) delete MODELS[k];
|
|
962
|
+
}
|
|
963
|
+
for (const m of models) MODELS[m.key] = m;
|
|
964
|
+
console.log(` Synced ${models.length} models for "${subArg}":`);
|
|
965
|
+
for (const m of models) {
|
|
966
|
+
console.log(` ${m.key.padEnd(20)} ${m.label}`);
|
|
967
|
+
}
|
|
968
|
+
} catch (err) {
|
|
969
|
+
console.log(` Error: ${err.message}`);
|
|
970
|
+
}
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (sub === 'set') {
|
|
975
|
+
const [providerName, field, ...values] = rest;
|
|
976
|
+
if (!providerName || !field || values.length === 0) {
|
|
977
|
+
console.log(' Usage: /provider set <name> <field> <value>');
|
|
978
|
+
return true;
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
setProviderField(providerName, field, values.join(' '));
|
|
982
|
+
console.log(` ${providerName}.${field} = ${values.join(' ')}`);
|
|
983
|
+
} catch (err) {
|
|
984
|
+
console.log(` Error: ${err.message}`);
|
|
985
|
+
}
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (sub === 'remove') {
|
|
990
|
+
if (!subArg) {
|
|
991
|
+
console.log(' Usage: /provider remove <name>');
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
removeProviderConfig(subArg);
|
|
996
|
+
console.log(` Removed provider "${subArg}"`);
|
|
997
|
+
} catch (err) {
|
|
998
|
+
console.log(` Error: ${err.message}`);
|
|
999
|
+
}
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
console.log(' Unknown subcommand. Use /provider help');
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (commandName === 'compact') {
|
|
1008
|
+
if (!state.history || state.history.length === 0) {
|
|
1009
|
+
console.log('No history to compact.');
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
const before = state.history.length;
|
|
1013
|
+
const beforeChars = state.history.reduce((sum, m) => sum + (m.content?.length || 0), 0);
|
|
1014
|
+
truncateHistory(state);
|
|
1015
|
+
await saveState(state);
|
|
1016
|
+
const after = state.history.length;
|
|
1017
|
+
if (before === after) {
|
|
1018
|
+
console.log(`Already compact (${before} messages, ${beforeChars} chars). Use /reset to clear.`);
|
|
1019
|
+
} else {
|
|
1020
|
+
console.log(`Compacted: ${before} -> ${after} messages. Summary: ~${countTokens(state.memorySummary)} tokens.`);
|
|
1021
|
+
}
|
|
1022
|
+
return true;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (commandName === 'theme') {
|
|
1026
|
+
const themes = ['dark', 'cappuccino', 'light', 'coffee', 'gruvbox', 'dracula', 'nord', 'solarized', 'monokai', 'tokyoNight'];
|
|
1027
|
+
const current = state.theme || 'dark';
|
|
1028
|
+
if (!args) {
|
|
1029
|
+
console.log(`Current theme: ${current}`);
|
|
1030
|
+
console.log('Available: ' + themes.join(', '));
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
const theme = args.toLowerCase().replace(/[-\s]/g, '');
|
|
1034
|
+
const themeMap = { tokyonight: 'tokyoNight' };
|
|
1035
|
+
const resolved = themeMap[theme] || theme;
|
|
1036
|
+
if (!themes.includes(resolved)) {
|
|
1037
|
+
console.log(`Unknown theme. Available: ${themes.join(', ')}`);
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
state.theme = resolved;
|
|
1041
|
+
await saveState(state);
|
|
1042
|
+
console.log(`Theme set to: ${resolved}`);
|
|
1043
|
+
if (global.__zynApplyTheme) global.__zynApplyTheme(resolved);
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
752
1047
|
return false;
|
|
753
1048
|
}
|
|
754
1049
|
|