@xcanwin/manyoyo 3.9.6 → 4.0.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/bin/manyoyo.js +537 -16
- package/docker/manyoyo.Dockerfile +12 -1
- package/lib/web/frontend/app.css +579 -123
- package/lib/web/frontend/app.html +48 -6
- package/lib/web/frontend/app.js +403 -54
- package/lib/web/frontend/login.css +134 -20
- package/lib/web/frontend/login.html +11 -7
- package/lib/web/frontend/login.js +46 -0
- package/lib/web/server.js +21 -10
- package/package.json +2 -2
package/bin/manyoyo.js
CHANGED
|
@@ -9,6 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
|
+
const net = require('net');
|
|
12
13
|
const readline = require('readline');
|
|
13
14
|
const { Command } = require('commander');
|
|
14
15
|
const JSON5 = require('json5');
|
|
@@ -59,6 +60,7 @@ let SHOW_COMMAND = false;
|
|
|
59
60
|
let YES_MODE = false;
|
|
60
61
|
let RM_ON_EXIT = false;
|
|
61
62
|
let SERVER_MODE = false;
|
|
63
|
+
let SERVER_HOST = '127.0.0.1';
|
|
62
64
|
let SERVER_PORT = 3000;
|
|
63
65
|
let SERVER_AUTH_USER = "";
|
|
64
66
|
let SERVER_AUTH_PASS = "";
|
|
@@ -75,6 +77,7 @@ const NC = '\x1b[0m'; // No Color
|
|
|
75
77
|
|
|
76
78
|
// Docker command (will be set by ensure_docker)
|
|
77
79
|
let DOCKER_CMD = 'docker';
|
|
80
|
+
const SUPPORTED_INIT_AGENTS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
78
81
|
|
|
79
82
|
// ==============================================================================
|
|
80
83
|
// SECTION: Utility Functions
|
|
@@ -90,24 +93,70 @@ function normalizeCommandSuffix(suffix) {
|
|
|
90
93
|
return trimmed ? ` ${trimmed}` : "";
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
function
|
|
94
|
-
if (
|
|
95
|
-
return
|
|
96
|
+
function resolveContainerNameTemplate(name) {
|
|
97
|
+
if (typeof name !== 'string') {
|
|
98
|
+
return name;
|
|
99
|
+
}
|
|
100
|
+
const nowValue = formatDate();
|
|
101
|
+
return name.replace(/\{now\}|\$\{now\}/g, nowValue);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateServerHost(host, rawServer) {
|
|
105
|
+
const value = String(host || '').trim();
|
|
106
|
+
const isIp = net.isIP(value) !== 0;
|
|
107
|
+
const isHostName = /^[A-Za-z0-9.-]+$/.test(value);
|
|
108
|
+
|
|
109
|
+
if (isIp || isHostName) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.error(`${RED}⚠️ 错误: --server 地址格式应为 端口 或 host:port (例如 3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseServerListen(rawServer) {
|
|
118
|
+
if (rawServer === true || rawServer === undefined || rawServer === null || rawServer === '') {
|
|
119
|
+
return { host: '127.0.0.1', port: 3000 };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const value = String(rawServer).trim();
|
|
123
|
+
if (!value) {
|
|
124
|
+
return { host: '127.0.0.1', port: 3000 };
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
let host = '127.0.0.1';
|
|
128
|
+
let portText = value;
|
|
129
|
+
|
|
130
|
+
const ipv6Match = value.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
131
|
+
if (ipv6Match) {
|
|
132
|
+
host = ipv6Match[1].trim();
|
|
133
|
+
portText = ipv6Match[2].trim();
|
|
134
|
+
} else {
|
|
135
|
+
const lastColonIndex = value.lastIndexOf(':');
|
|
136
|
+
if (lastColonIndex > 0) {
|
|
137
|
+
const maybePort = value.slice(lastColonIndex + 1).trim();
|
|
138
|
+
if (/^\d+$/.test(maybePort)) {
|
|
139
|
+
host = value.slice(0, lastColonIndex).trim();
|
|
140
|
+
portText = maybePort;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!/^\d+$/.test(portText)) {
|
|
146
|
+
console.error(`${RED}⚠️ 错误: --server 端口必须是 1-65535 的整数: ${rawServer}${NC}`);
|
|
101
147
|
process.exit(1);
|
|
102
148
|
}
|
|
103
149
|
|
|
104
|
-
const port = Number(
|
|
150
|
+
const port = Number(portText);
|
|
105
151
|
if (port < 1 || port > 65535) {
|
|
106
|
-
console.error(`${RED}⚠️ 错误: --server 端口超出范围 (1-65535): ${
|
|
152
|
+
console.error(`${RED}⚠️ 错误: --server 端口超出范围 (1-65535): ${rawServer}${NC}`);
|
|
107
153
|
process.exit(1);
|
|
108
154
|
}
|
|
109
155
|
|
|
110
|
-
return
|
|
156
|
+
return {
|
|
157
|
+
host: validateServerHost(host, rawServer),
|
|
158
|
+
port
|
|
159
|
+
};
|
|
111
160
|
}
|
|
112
161
|
|
|
113
162
|
function ensureWebServerAuthCredentials() {
|
|
@@ -247,6 +296,457 @@ function loadRunConfig(name) {
|
|
|
247
296
|
return {};
|
|
248
297
|
}
|
|
249
298
|
|
|
299
|
+
function readJsonFileSafely(filePath, label) {
|
|
300
|
+
if (!fs.existsSync(filePath)) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
305
|
+
} catch (e) {
|
|
306
|
+
console.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseSimpleToml(content) {
|
|
312
|
+
const result = {};
|
|
313
|
+
let current = result;
|
|
314
|
+
const lines = String(content || '').split('\n');
|
|
315
|
+
|
|
316
|
+
for (const rawLine of lines) {
|
|
317
|
+
const line = rawLine.trim();
|
|
318
|
+
if (!line || line.startsWith('#')) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
323
|
+
if (sectionMatch) {
|
|
324
|
+
const parts = sectionMatch[1].split('.').map(p => p.trim()).filter(Boolean);
|
|
325
|
+
current = result;
|
|
326
|
+
for (const part of parts) {
|
|
327
|
+
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
|
|
328
|
+
current[part] = {};
|
|
329
|
+
}
|
|
330
|
+
current = current[part];
|
|
331
|
+
}
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const keyValueMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
336
|
+
if (!keyValueMatch) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const key = keyValueMatch[1];
|
|
341
|
+
let valueText = keyValueMatch[2].trim();
|
|
342
|
+
if ((valueText.startsWith('"') && valueText.endsWith('"')) || (valueText.startsWith("'") && valueText.endsWith("'"))) {
|
|
343
|
+
valueText = valueText.slice(1, -1);
|
|
344
|
+
} else if (valueText === 'true') {
|
|
345
|
+
valueText = true;
|
|
346
|
+
} else if (valueText === 'false') {
|
|
347
|
+
valueText = false;
|
|
348
|
+
} else if (/^-?\d+(\.\d+)?$/.test(valueText)) {
|
|
349
|
+
valueText = Number(valueText);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
current[key] = valueText;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function readTomlFileSafely(filePath, label) {
|
|
359
|
+
if (!fs.existsSync(filePath)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
364
|
+
return parseSimpleToml(content);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizeInitConfigAgents(rawAgents) {
|
|
372
|
+
const aliasMap = {
|
|
373
|
+
all: 'all',
|
|
374
|
+
claude: 'claude',
|
|
375
|
+
c: 'claude',
|
|
376
|
+
cc: 'claude',
|
|
377
|
+
codex: 'codex',
|
|
378
|
+
cx: 'codex',
|
|
379
|
+
gemini: 'gemini',
|
|
380
|
+
gm: 'gemini',
|
|
381
|
+
g: 'gemini',
|
|
382
|
+
opencode: 'opencode',
|
|
383
|
+
oc: 'opencode'
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
if (rawAgents === true || rawAgents === undefined || rawAgents === null || rawAgents === '') {
|
|
387
|
+
return [...SUPPORTED_INIT_AGENTS];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const tokens = String(rawAgents).split(/[,\s]+/).map(v => v.trim().toLowerCase()).filter(Boolean);
|
|
391
|
+
if (tokens.length === 0) {
|
|
392
|
+
return [...SUPPORTED_INIT_AGENTS];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const normalized = [];
|
|
396
|
+
for (const token of tokens) {
|
|
397
|
+
const mapped = aliasMap[token];
|
|
398
|
+
if (!mapped) {
|
|
399
|
+
console.error(`${RED}⚠️ 错误: --init-config 不支持的 Agent: ${token}${NC}`);
|
|
400
|
+
console.error(`${YELLOW}支持: ${SUPPORTED_INIT_AGENTS.join(', ')} 或 all${NC}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
if (mapped === 'all') {
|
|
404
|
+
return [...SUPPORTED_INIT_AGENTS];
|
|
405
|
+
}
|
|
406
|
+
if (!normalized.includes(mapped)) {
|
|
407
|
+
normalized.push(mapped);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return normalized;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isSafeInitEnvValue(value) {
|
|
414
|
+
if (value === undefined || value === null) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
const text = String(value).replace(/[\r\n\0]/g, '').trim();
|
|
418
|
+
if (!text) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
if (/[\$\(\)\`\|\&\*\{\};<>]/.test(text)) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
if (/^\(/.test(text)) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function setInitValue(values, key, value) {
|
|
431
|
+
if (value === undefined || value === null) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const text = String(value).replace(/[\r\n\0]/g, '').trim();
|
|
435
|
+
if (!text) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
values[key] = text;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function dedupeList(list) {
|
|
442
|
+
return Array.from(new Set((list || []).filter(Boolean)));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function resolveEnvPlaceholder(value) {
|
|
446
|
+
if (typeof value !== 'string') {
|
|
447
|
+
return "";
|
|
448
|
+
}
|
|
449
|
+
const match = value.match(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/);
|
|
450
|
+
if (!match) {
|
|
451
|
+
return "";
|
|
452
|
+
}
|
|
453
|
+
const envName = match[1];
|
|
454
|
+
return process.env[envName] ? String(process.env[envName]).trim() : "";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function collectClaudeInitData(homeDir) {
|
|
458
|
+
const keys = [
|
|
459
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
460
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
461
|
+
'ANTHROPIC_BASE_URL',
|
|
462
|
+
'ANTHROPIC_MODEL',
|
|
463
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
464
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
465
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
466
|
+
'CLAUDE_CODE_SUBAGENT_MODEL'
|
|
467
|
+
];
|
|
468
|
+
const values = {};
|
|
469
|
+
const notes = [];
|
|
470
|
+
const volumes = [];
|
|
471
|
+
|
|
472
|
+
const claudeDir = path.join(homeDir, '.claude');
|
|
473
|
+
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
|
|
474
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
475
|
+
const settingsJson = readJsonFileSafely(claudeSettingsPath, 'Claude settings');
|
|
476
|
+
const claudeJson = readJsonFileSafely(claudeJsonPath, 'Claude config');
|
|
477
|
+
|
|
478
|
+
keys.forEach(key => setInitValue(values, key, process.env[key]));
|
|
479
|
+
|
|
480
|
+
if (claudeJson && claudeJson.env && typeof claudeJson.env === 'object') {
|
|
481
|
+
keys.forEach(key => setInitValue(values, key, claudeJson.env[key]));
|
|
482
|
+
}
|
|
483
|
+
if (settingsJson && settingsJson.env && typeof settingsJson.env === 'object') {
|
|
484
|
+
keys.forEach(key => setInitValue(values, key, settingsJson.env[key]));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (fs.existsSync(claudeDir)) {
|
|
488
|
+
volumes.push(`${claudeDir}:/root/.claude`);
|
|
489
|
+
}
|
|
490
|
+
if (fs.existsSync(claudeJsonPath)) {
|
|
491
|
+
volumes.push(`${claudeJsonPath}:/root/.claude.json`);
|
|
492
|
+
}
|
|
493
|
+
if (!fs.existsSync(claudeDir) && !fs.existsSync(claudeJsonPath)) {
|
|
494
|
+
notes.push('未检测到 Claude 本地配置(~/.claude 或 ~/.claude.json),已生成占位模板。');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return { keys, values, notes, volumes: dedupeList(volumes) };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function collectGeminiInitData(homeDir) {
|
|
501
|
+
const keys = [
|
|
502
|
+
'GOOGLE_GEMINI_BASE_URL',
|
|
503
|
+
'GEMINI_API_KEY',
|
|
504
|
+
'GEMINI_MODEL'
|
|
505
|
+
];
|
|
506
|
+
const values = {};
|
|
507
|
+
const notes = [];
|
|
508
|
+
const volumes = [];
|
|
509
|
+
const geminiDir = path.join(homeDir, '.gemini');
|
|
510
|
+
|
|
511
|
+
keys.forEach(key => setInitValue(values, key, process.env[key]));
|
|
512
|
+
|
|
513
|
+
if (fs.existsSync(geminiDir)) {
|
|
514
|
+
volumes.push(`${geminiDir}:/root/.gemini`);
|
|
515
|
+
} else {
|
|
516
|
+
notes.push('未检测到 Gemini 本地配置目录(~/.gemini),已生成占位模板。');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return { keys, values, notes, volumes: dedupeList(volumes) };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function collectCodexInitData(homeDir) {
|
|
523
|
+
const keys = [
|
|
524
|
+
'OPENAI_API_KEY',
|
|
525
|
+
'OPENAI_BASE_URL',
|
|
526
|
+
'OPENAI_MODEL'
|
|
527
|
+
];
|
|
528
|
+
const values = {};
|
|
529
|
+
const notes = [];
|
|
530
|
+
const volumes = [];
|
|
531
|
+
|
|
532
|
+
const codexDir = path.join(homeDir, '.codex');
|
|
533
|
+
const authPath = path.join(codexDir, 'auth.json');
|
|
534
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
535
|
+
const authJson = readJsonFileSafely(authPath, 'Codex auth');
|
|
536
|
+
const configToml = readTomlFileSafely(configPath, 'Codex TOML');
|
|
537
|
+
|
|
538
|
+
keys.forEach(key => setInitValue(values, key, process.env[key]));
|
|
539
|
+
|
|
540
|
+
if (authJson && typeof authJson === 'object') {
|
|
541
|
+
setInitValue(values, 'OPENAI_API_KEY', authJson.OPENAI_API_KEY);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (configToml && typeof configToml === 'object') {
|
|
545
|
+
setInitValue(values, 'OPENAI_MODEL', configToml.model);
|
|
546
|
+
|
|
547
|
+
let providerConfig = null;
|
|
548
|
+
const providers = configToml.model_providers;
|
|
549
|
+
if (providers && typeof providers === 'object') {
|
|
550
|
+
if (typeof configToml.model_provider === 'string' && providers[configToml.model_provider]) {
|
|
551
|
+
providerConfig = providers[configToml.model_provider];
|
|
552
|
+
} else {
|
|
553
|
+
const firstProviderName = Object.keys(providers)[0];
|
|
554
|
+
if (firstProviderName) {
|
|
555
|
+
providerConfig = providers[firstProviderName];
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (providerConfig && typeof providerConfig === 'object') {
|
|
560
|
+
setInitValue(values, 'OPENAI_BASE_URL', providerConfig.base_url);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (fs.existsSync(codexDir)) {
|
|
565
|
+
volumes.push(`${codexDir}:/root/.codex`);
|
|
566
|
+
} else {
|
|
567
|
+
notes.push('未检测到 Codex 本地配置目录(~/.codex),已生成占位模板。');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return { keys, values, notes, volumes: dedupeList(volumes) };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function collectOpenCodeInitData(homeDir) {
|
|
574
|
+
const keys = [
|
|
575
|
+
'OPENAI_API_KEY',
|
|
576
|
+
'OPENAI_BASE_URL',
|
|
577
|
+
'OPENAI_MODEL'
|
|
578
|
+
];
|
|
579
|
+
const values = {};
|
|
580
|
+
const notes = [];
|
|
581
|
+
const volumes = [];
|
|
582
|
+
|
|
583
|
+
const opencodeDir = path.join(homeDir, '.config', 'opencode');
|
|
584
|
+
const opencodePath = path.join(opencodeDir, 'opencode.json');
|
|
585
|
+
const opencodeAuthPath = path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
|
|
586
|
+
const opencodeJson = readJsonFileSafely(opencodePath, 'OpenCode config');
|
|
587
|
+
|
|
588
|
+
keys.forEach(key => setInitValue(values, key, process.env[key]));
|
|
589
|
+
|
|
590
|
+
if (opencodeJson && typeof opencodeJson === 'object') {
|
|
591
|
+
const providers = opencodeJson.provider && typeof opencodeJson.provider === 'object'
|
|
592
|
+
? Object.values(opencodeJson.provider).filter(v => v && typeof v === 'object')
|
|
593
|
+
: [];
|
|
594
|
+
const provider = providers[0];
|
|
595
|
+
|
|
596
|
+
if (provider) {
|
|
597
|
+
const options = provider.options && typeof provider.options === 'object' ? provider.options : {};
|
|
598
|
+
const apiKeyValue = resolveEnvPlaceholder(options.apiKey) || options.apiKey;
|
|
599
|
+
const baseUrlValue = resolveEnvPlaceholder(options.baseURL) || options.baseURL;
|
|
600
|
+
setInitValue(values, 'OPENAI_API_KEY', apiKeyValue);
|
|
601
|
+
setInitValue(values, 'OPENAI_BASE_URL', baseUrlValue);
|
|
602
|
+
|
|
603
|
+
if (provider.models && typeof provider.models === 'object') {
|
|
604
|
+
const firstModelName = Object.keys(provider.models)[0];
|
|
605
|
+
if (firstModelName) {
|
|
606
|
+
setInitValue(values, 'OPENAI_MODEL', firstModelName);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (typeof opencodeJson.model === 'string') {
|
|
612
|
+
const modelFromEnv = resolveEnvPlaceholder(opencodeJson.model);
|
|
613
|
+
if (modelFromEnv) {
|
|
614
|
+
setInitValue(values, 'OPENAI_MODEL', modelFromEnv);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (fs.existsSync(opencodePath)) {
|
|
620
|
+
volumes.push(`${opencodePath}:/root/.config/opencode/opencode.json`);
|
|
621
|
+
} else {
|
|
622
|
+
notes.push('未检测到 OpenCode 配置文件(~/.config/opencode/opencode.json),已生成占位模板。');
|
|
623
|
+
}
|
|
624
|
+
if (fs.existsSync(opencodeAuthPath)) {
|
|
625
|
+
volumes.push(`${opencodeAuthPath}:/root/.local/share/opencode/auth.json`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return { keys, values, notes, volumes: dedupeList(volumes) };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function writeInitEnvFile(filePath, keys, values) {
|
|
632
|
+
const lines = [
|
|
633
|
+
'# Auto-generated by manyoyo --init-config',
|
|
634
|
+
'# Remove leading # and fill values if missing.',
|
|
635
|
+
''
|
|
636
|
+
];
|
|
637
|
+
const missingKeys = [];
|
|
638
|
+
const unsafeKeys = [];
|
|
639
|
+
|
|
640
|
+
for (const key of keys) {
|
|
641
|
+
const value = values[key];
|
|
642
|
+
if (isSafeInitEnvValue(value)) {
|
|
643
|
+
lines.push(`export ${key}=${String(value).replace(/[\r\n\0]/g, '')}`);
|
|
644
|
+
} else if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
645
|
+
lines.push(`# export ${key}=""`);
|
|
646
|
+
unsafeKeys.push(key);
|
|
647
|
+
} else {
|
|
648
|
+
lines.push(`# export ${key}=""`);
|
|
649
|
+
missingKeys.push(key);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
lines.push('');
|
|
653
|
+
|
|
654
|
+
fs.writeFileSync(filePath, lines.join('\n'));
|
|
655
|
+
return { missingKeys, unsafeKeys };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function writeInitRunFile(filePath, agent, yolo, volumes) {
|
|
659
|
+
const runConfig = {
|
|
660
|
+
containerName: `my-${agent}-{now}`,
|
|
661
|
+
envFile: [agent],
|
|
662
|
+
yolo
|
|
663
|
+
};
|
|
664
|
+
const volumeList = dedupeList(volumes);
|
|
665
|
+
if (volumeList.length > 0) {
|
|
666
|
+
runConfig.volumes = volumeList;
|
|
667
|
+
}
|
|
668
|
+
fs.writeFileSync(filePath, `${JSON.stringify(runConfig, null, 4)}\n`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function shouldOverwriteInitFile(filePath, fileLabel) {
|
|
672
|
+
if (!fs.existsSync(filePath)) {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (YES_MODE) {
|
|
677
|
+
console.log(`${YELLOW}⚠️ ${filePath} 已存在,--yes 模式自动覆盖 (${fileLabel})${NC}`);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const reply = await askQuestion(`❔ ${filePath} 已存在,是否覆盖? [y/N]: `);
|
|
682
|
+
const firstChar = String(reply || '').trim().toLowerCase()[0];
|
|
683
|
+
if (firstChar === 'y') {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
console.log(`${YELLOW}⏭️ 已保留原文件: ${filePath}${NC}`);
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function initAgentConfigs(rawAgents) {
|
|
691
|
+
const agents = normalizeInitConfigAgents(rawAgents);
|
|
692
|
+
const homeDir = os.homedir();
|
|
693
|
+
const manyoyoHome = path.join(homeDir, '.manyoyo');
|
|
694
|
+
const runDir = path.join(manyoyoHome, 'run');
|
|
695
|
+
const envDir = path.join(manyoyoHome, 'env');
|
|
696
|
+
|
|
697
|
+
fs.mkdirSync(envDir, { recursive: true });
|
|
698
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
699
|
+
|
|
700
|
+
const extractors = {
|
|
701
|
+
claude: collectClaudeInitData,
|
|
702
|
+
codex: collectCodexInitData,
|
|
703
|
+
gemini: collectGeminiInitData,
|
|
704
|
+
opencode: collectOpenCodeInitData
|
|
705
|
+
};
|
|
706
|
+
const yoloMap = {
|
|
707
|
+
claude: 'c',
|
|
708
|
+
codex: 'cx',
|
|
709
|
+
gemini: 'gm',
|
|
710
|
+
opencode: 'oc'
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
console.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
|
|
714
|
+
|
|
715
|
+
for (const agent of agents) {
|
|
716
|
+
const data = extractors[agent](homeDir);
|
|
717
|
+
const runFilePath = path.join(runDir, `${agent}.json`);
|
|
718
|
+
const envFilePath = path.join(envDir, `${agent}.env`);
|
|
719
|
+
const shouldWriteRun = await shouldOverwriteInitFile(runFilePath, `${agent}.json`);
|
|
720
|
+
const shouldWriteEnv = await shouldOverwriteInitFile(envFilePath, `${agent}.env`);
|
|
721
|
+
|
|
722
|
+
let writeResult = { missingKeys: [], unsafeKeys: [] };
|
|
723
|
+
if (shouldWriteEnv) {
|
|
724
|
+
writeResult = writeInitEnvFile(envFilePath, data.keys, data.values);
|
|
725
|
+
}
|
|
726
|
+
if (shouldWriteRun) {
|
|
727
|
+
writeInitRunFile(runFilePath, agent, yoloMap[agent], data.volumes);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (shouldWriteEnv || shouldWriteRun) {
|
|
731
|
+
console.log(`${GREEN}✅ [${agent}] 初始化完成${NC}`);
|
|
732
|
+
} else {
|
|
733
|
+
console.log(`${YELLOW}⚠️ [${agent}] 已跳过(文件均保留)${NC}`);
|
|
734
|
+
}
|
|
735
|
+
console.log(` run: ${shouldWriteRun ? '已写入' : '保留'} ${runFilePath}`);
|
|
736
|
+
console.log(` env: ${shouldWriteEnv ? '已写入' : '保留'} ${envFilePath}`);
|
|
737
|
+
|
|
738
|
+
if (shouldWriteEnv && writeResult.missingKeys.length > 0) {
|
|
739
|
+
console.log(`${YELLOW}⚠️ [${agent}] 以下变量未找到,请手动填写:${NC} ${writeResult.missingKeys.join(', ')}`);
|
|
740
|
+
}
|
|
741
|
+
if (shouldWriteEnv && writeResult.unsafeKeys.length > 0) {
|
|
742
|
+
console.log(`${YELLOW}⚠️ [${agent}] 以下变量包含不安全字符,已留空模板:${NC} ${writeResult.unsafeKeys.join(', ')}`);
|
|
743
|
+
}
|
|
744
|
+
if (data.notes && data.notes.length > 0) {
|
|
745
|
+
data.notes.forEach(note => console.log(`${YELLOW}⚠️ [${agent}] ${note}${NC}`));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
250
750
|
// ==============================================================================
|
|
251
751
|
// SECTION: UI Functions
|
|
252
752
|
// ==============================================================================
|
|
@@ -493,7 +993,7 @@ function showImagePullHint(err) {
|
|
|
493
993
|
}
|
|
494
994
|
const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
|
|
495
995
|
console.log(`${YELLOW}💡 提示: 本地未找到镜像 ${image},并且从 localhost 注册表拉取失败。${NC}`);
|
|
496
|
-
console.log(`${YELLOW} 你可以: 1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion 2) 或先执行 manyoyo --ib --iv <version> 构建镜像。${NC}`);
|
|
996
|
+
console.log(`${YELLOW} 你可以: (1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion。 (2) 或先执行 manyoyo --ib --iv <version> 构建镜像。${NC}`);
|
|
497
997
|
}
|
|
498
998
|
|
|
499
999
|
function runCmd(cmd, args, options = {}) {
|
|
@@ -868,7 +1368,7 @@ async function buildImage(IMAGE_BUILD_ARGS, imageName, imageVersion) {
|
|
|
868
1368
|
// SECTION: Command Line Interface
|
|
869
1369
|
// ==============================================================================
|
|
870
1370
|
|
|
871
|
-
function setupCommander() {
|
|
1371
|
+
async function setupCommander() {
|
|
872
1372
|
// Load config file
|
|
873
1373
|
const config = loadConfig();
|
|
874
1374
|
|
|
@@ -898,10 +1398,12 @@ function setupCommander() {
|
|
|
898
1398
|
${MANYOYO_NAME} -r ./myconfig.json 使用当前目录 ./myconfig.json 配置
|
|
899
1399
|
${MANYOYO_NAME} -n test --ef claude -y c 使用 ~/.manyoyo/env/claude.env 环境变量文件
|
|
900
1400
|
${MANYOYO_NAME} -n test --ef ./myenv.env -y c 使用当前目录 ./myenv.env 环境变量文件
|
|
1401
|
+
${MANYOYO_NAME} --init-config all 从本机 Agent 配置初始化 ~/.manyoyo
|
|
901
1402
|
${MANYOYO_NAME} -n test -- -c 恢复之前会话
|
|
902
1403
|
${MANYOYO_NAME} -x echo 123 指定命令执行
|
|
903
1404
|
${MANYOYO_NAME} --server --server-user admin --server-pass 123456 启动带登录认证的网页服务
|
|
904
1405
|
${MANYOYO_NAME} --server 3000 启动网页交互服务
|
|
1406
|
+
${MANYOYO_NAME} --server 0.0.0.0:3000 监听全部网卡,便于局域网访问
|
|
905
1407
|
${MANYOYO_NAME} -n test -q tip -q cmd 多次使用静默选项
|
|
906
1408
|
`);
|
|
907
1409
|
|
|
@@ -927,10 +1429,11 @@ function setupCommander() {
|
|
|
927
1429
|
.option('--ss, --shell-suffix <command>', '指定命令后缀 (追加到-s之后,等价于 -- <args>)')
|
|
928
1430
|
.option('-x, --shell-full <command...>', '指定完整命令执行 (代替--sp和-s和--命令)')
|
|
929
1431
|
.option('-y, --yolo <cli>', '使AGENT无需确认 (claude/c, gemini/gm, codex/cx, opencode/oc)')
|
|
1432
|
+
.option('--init-config [agents]', '初始化 Agent 配置到 ~/.manyoyo (all 或逗号分隔: claude,codex,gemini,opencode)')
|
|
930
1433
|
.option('--install <name>', '安装manyoyo命令 (docker-cli-plugin)')
|
|
931
1434
|
.option('--show-config', '显示最终生效配置并退出')
|
|
932
1435
|
.option('--show-command', '显示将执行的 docker run 命令并退出')
|
|
933
|
-
.option('--server [port]', '启动网页交互服务 (
|
|
1436
|
+
.option('--server [port]', '启动网页交互服务 (默认 127.0.0.1:3000,支持 host:port)')
|
|
934
1437
|
.option('--server-user <username>', '网页服务登录用户名 (默认 admin)')
|
|
935
1438
|
.option('--server-pass <password>', '网页服务登录密码 (默认自动生成随机密码)')
|
|
936
1439
|
.option('--yes', '所有提示自动确认 (用于CI/脚本)')
|
|
@@ -959,8 +1462,12 @@ function setupCommander() {
|
|
|
959
1462
|
program.help();
|
|
960
1463
|
}
|
|
961
1464
|
|
|
962
|
-
|
|
963
|
-
|
|
1465
|
+
const isInitConfigMode = process.argv.some(arg => arg === '--init-config' || arg.startsWith('--init-config='));
|
|
1466
|
+
// init-config 只处理本地文件,不依赖 docker/podman
|
|
1467
|
+
if (!isInitConfigMode) {
|
|
1468
|
+
// Ensure docker/podman is available
|
|
1469
|
+
ensureDocker();
|
|
1470
|
+
}
|
|
964
1471
|
|
|
965
1472
|
// Pre-handle -x/--shell-full: treat all following args as a single command
|
|
966
1473
|
const shellFullIndex = process.argv.findIndex(arg => arg === '-x' || arg === '--shell-full');
|
|
@@ -975,6 +1482,15 @@ function setupCommander() {
|
|
|
975
1482
|
|
|
976
1483
|
const options = program.opts();
|
|
977
1484
|
|
|
1485
|
+
if (options.yes) {
|
|
1486
|
+
YES_MODE = true;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (options.initConfig !== undefined) {
|
|
1490
|
+
await initAgentConfigs(options.initConfig);
|
|
1491
|
+
process.exit(0);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
978
1494
|
// Load run config if specified
|
|
979
1495
|
const runConfig = options.run ? loadRunConfig(options.run) : {};
|
|
980
1496
|
|
|
@@ -984,6 +1500,7 @@ function setupCommander() {
|
|
|
984
1500
|
if (options.contName || runConfig.containerName || config.containerName) {
|
|
985
1501
|
CONTAINER_NAME = options.contName || runConfig.containerName || config.containerName;
|
|
986
1502
|
}
|
|
1503
|
+
CONTAINER_NAME = resolveContainerNameTemplate(CONTAINER_NAME);
|
|
987
1504
|
if (options.contPath || runConfig.containerPath || config.containerPath) {
|
|
988
1505
|
CONTAINER_PATH = options.contPath || runConfig.containerPath || config.containerPath;
|
|
989
1506
|
}
|
|
@@ -1059,7 +1576,9 @@ function setupCommander() {
|
|
|
1059
1576
|
|
|
1060
1577
|
if (options.server !== undefined) {
|
|
1061
1578
|
SERVER_MODE = true;
|
|
1062
|
-
|
|
1579
|
+
const serverListen = parseServerListen(options.server);
|
|
1580
|
+
SERVER_HOST = serverListen.host;
|
|
1581
|
+
SERVER_PORT = serverListen.port;
|
|
1063
1582
|
}
|
|
1064
1583
|
|
|
1065
1584
|
const serverUserValue = options.serverUser || runConfig.serverUser || config.serverUser || process.env.MANYOYO_SERVER_USER;
|
|
@@ -1095,6 +1614,7 @@ function setupCommander() {
|
|
|
1095
1614
|
yolo: yoloValue || "",
|
|
1096
1615
|
quiet: quietValue || [],
|
|
1097
1616
|
server: SERVER_MODE,
|
|
1617
|
+
serverHost: SERVER_MODE ? SERVER_HOST : null,
|
|
1098
1618
|
serverPort: SERVER_MODE ? SERVER_PORT : null,
|
|
1099
1619
|
serverUser: SERVER_AUTH_USER || "",
|
|
1100
1620
|
serverPass: SERVER_AUTH_PASS || "",
|
|
@@ -1371,6 +1891,7 @@ async function runWebServerMode() {
|
|
|
1371
1891
|
ensureWebServerAuthCredentials();
|
|
1372
1892
|
|
|
1373
1893
|
await startWebServer({
|
|
1894
|
+
serverHost: SERVER_HOST,
|
|
1374
1895
|
serverPort: SERVER_PORT,
|
|
1375
1896
|
authUser: SERVER_AUTH_USER,
|
|
1376
1897
|
authPass: SERVER_AUTH_PASS,
|
|
@@ -1414,7 +1935,7 @@ async function runWebServerMode() {
|
|
|
1414
1935
|
async function main() {
|
|
1415
1936
|
try {
|
|
1416
1937
|
// 1. Setup commander and parse arguments
|
|
1417
|
-
setupCommander();
|
|
1938
|
+
await setupCommander();
|
|
1418
1939
|
|
|
1419
1940
|
// 2. Start web server mode
|
|
1420
1941
|
if (SERVER_MODE) {
|