@xcanwin/manyoyo 6.0.5 → 6.1.3
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 +58 -48
- package/lib/agent-adapters/index.js +47 -0
- package/lib/container-modes.js +37 -0
- package/lib/core/app-error.js +81 -0
- package/lib/core/event-store.js +106 -0
- package/lib/core/events.js +149 -0
- package/lib/doctor.js +84 -0
- package/lib/web/frontend/app.css +168 -12
- package/lib/web/frontend/app.html +19 -5
- package/lib/web/frontend/app.js +150 -10
- package/lib/web/frontend/chat-behavior.js +30 -0
- package/lib/web/frontend/codemirror.bundle.js +31 -31800
- package/lib/web/server.js +111 -29
- package/package.json +3 -1
package/bin/manyoyo.js
CHANGED
|
@@ -14,6 +14,9 @@ const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = requ
|
|
|
14
14
|
const { initAgentConfigs } = require('../lib/init-config');
|
|
15
15
|
const { buildImage } = require('../lib/image-build');
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
|
+
const { resolveYoloCommand } = require('../lib/agent-adapters');
|
|
18
|
+
const { runDoctorChecks } = require('../lib/doctor');
|
|
19
|
+
const { resolveContainerMode } = require('../lib/container-modes');
|
|
17
20
|
const { runPluginCommand, createPlugin } = require('../lib/plugin');
|
|
18
21
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
19
22
|
const { resolveRuntimeConfig } = require('../lib/runtime-resolver');
|
|
@@ -635,27 +638,13 @@ function addImageBuildArg(value) {
|
|
|
635
638
|
IMAGE_BUILD_ARGS.push("--build-arg", value);
|
|
636
639
|
}
|
|
637
640
|
|
|
638
|
-
const YOLO_COMMAND_MAP = {
|
|
639
|
-
claude: "IS_SANDBOX=1 claude --dangerously-skip-permissions",
|
|
640
|
-
cc: "IS_SANDBOX=1 claude --dangerously-skip-permissions",
|
|
641
|
-
c: "IS_SANDBOX=1 claude --dangerously-skip-permissions",
|
|
642
|
-
gemini: "gemini --yolo",
|
|
643
|
-
gm: "gemini --yolo",
|
|
644
|
-
g: "gemini --yolo",
|
|
645
|
-
codex: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
646
|
-
cx: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
647
|
-
opencode: "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode",
|
|
648
|
-
oc: "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode"
|
|
649
|
-
};
|
|
650
|
-
|
|
651
641
|
function setYolo(cli) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
642
|
+
try {
|
|
643
|
+
EXEC_COMMAND = resolveYoloCommand(cli);
|
|
644
|
+
} catch (error) {
|
|
655
645
|
console.log(`${RED}⚠️ 未知LLM CLI: ${cli}${NC}`);
|
|
656
646
|
process.exit(0);
|
|
657
647
|
}
|
|
658
|
-
EXEC_COMMAND = mappedCommand;
|
|
659
648
|
}
|
|
660
649
|
|
|
661
650
|
/**
|
|
@@ -663,41 +652,21 @@ function setYolo(cli) {
|
|
|
663
652
|
* @param {string} mode - 模式名称 (common, dind, sock)
|
|
664
653
|
*/
|
|
665
654
|
function setContMode(mode) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
sock: 'sock',
|
|
673
|
-
s: 'sock'
|
|
674
|
-
};
|
|
675
|
-
const normalizedMode = modeAliasMap[String(mode || '').trim().toLowerCase()];
|
|
676
|
-
|
|
677
|
-
if (normalizedMode === 'common') {
|
|
678
|
-
CONT_MODE_ARGS = [];
|
|
679
|
-
return;
|
|
655
|
+
let resolved;
|
|
656
|
+
try {
|
|
657
|
+
resolved = resolveContainerMode(mode);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.log(`${RED}⚠️ 未知模式: ${mode}${NC}`);
|
|
660
|
+
process.exit(0);
|
|
680
661
|
}
|
|
681
662
|
|
|
682
|
-
|
|
683
|
-
CONT_MODE_ARGS = ['--privileged'];
|
|
684
|
-
console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
663
|
+
CONT_MODE_ARGS = resolved.args;
|
|
687
664
|
|
|
688
|
-
if (
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
'--volume', '/var/run/docker.sock:/var/run/docker.sock',
|
|
692
|
-
'--env', 'DOCKER_HOST=unix:///var/run/docker.sock',
|
|
693
|
-
'--env', 'CONTAINER_HOST=unix:///var/run/docker.sock'
|
|
694
|
-
];
|
|
665
|
+
if (resolved.mode === 'dind') {
|
|
666
|
+
console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
|
|
667
|
+
} else if (resolved.mode === 'sock') {
|
|
695
668
|
console.log(`${RED}⚠️ 开启危险的容器嵌套容器模式, 危害: 容器可访问宿主机文件${NC}`);
|
|
696
|
-
return;
|
|
697
669
|
}
|
|
698
|
-
|
|
699
|
-
console.log(`${RED}⚠️ 未知模式: ${mode}${NC}`);
|
|
700
|
-
process.exit(0);
|
|
701
670
|
}
|
|
702
671
|
|
|
703
672
|
function showImagePullHint(err) {
|
|
@@ -757,6 +726,14 @@ function runCmd(cmd, args, options = {}) {
|
|
|
757
726
|
return result.stdout || '';
|
|
758
727
|
}
|
|
759
728
|
|
|
729
|
+
function checkPortAvailability(port) {
|
|
730
|
+
return new Promise(resolve => {
|
|
731
|
+
const server = net.createServer();
|
|
732
|
+
server.once('error', () => resolve('occupied'));
|
|
733
|
+
server.listen(port, '127.0.0.1', () => server.close(() => resolve('available')));
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
760
737
|
function dockerExecArgs(args, options = {}) {
|
|
761
738
|
try {
|
|
762
739
|
return runCmd(DOCKER_CMD, args, options);
|
|
@@ -1288,6 +1265,13 @@ Notes:
|
|
|
1288
1265
|
.option('--yes', '所有提示自动确认 (用于CI/脚本)')
|
|
1289
1266
|
.action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
|
|
1290
1267
|
|
|
1268
|
+
program.command('doctor')
|
|
1269
|
+
.description('诊断容器运行时、镜像、配置、Agent、模式、插件和端口')
|
|
1270
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1271
|
+
.option('--port <port>', '检查指定监听端口')
|
|
1272
|
+
.option('--json', '以 JSON 输出稳定诊断结果')
|
|
1273
|
+
.action(options => selectAction('doctor', { ...options, doctor: true }));
|
|
1274
|
+
|
|
1291
1275
|
program.command('update')
|
|
1292
1276
|
.description('更新 MANYOYO(若检测为本地 file 安装则跳过)')
|
|
1293
1277
|
.action(() => selectAction('update', { update: true }));
|
|
@@ -1334,6 +1318,7 @@ Notes:
|
|
|
1334
1318
|
const isPruneMode = selectedAction === 'prune';
|
|
1335
1319
|
const isShowConfigMode = selectedAction === 'config-show';
|
|
1336
1320
|
const isShowCommandMode = selectedAction === 'config-command';
|
|
1321
|
+
const isDoctorMode = selectedAction === 'doctor';
|
|
1337
1322
|
const isServerMode = options.server !== undefined;
|
|
1338
1323
|
const isServerStopMode = Boolean(selectedAction === 'serve' && options.stop);
|
|
1339
1324
|
const isServerRestartMode = Boolean(selectedAction === 'serve' && options.restart);
|
|
@@ -1342,7 +1327,7 @@ Notes:
|
|
|
1342
1327
|
throw new Error('serve --stop 与 --restart 不能同时使用');
|
|
1343
1328
|
}
|
|
1344
1329
|
|
|
1345
|
-
const noDockerActions = new Set(['init', 'update', 'install', 'config-show', 'plugin']);
|
|
1330
|
+
const noDockerActions = new Set(['init', 'update', 'install', 'config-show', 'plugin', 'doctor']);
|
|
1346
1331
|
if (isServerStopMode) {
|
|
1347
1332
|
noDockerActions.add('serve');
|
|
1348
1333
|
}
|
|
@@ -1528,6 +1513,31 @@ Notes:
|
|
|
1528
1513
|
process.exit(0);
|
|
1529
1514
|
}
|
|
1530
1515
|
|
|
1516
|
+
if (isDoctorMode) {
|
|
1517
|
+
const parsedPort = options.port === undefined ? null : Number(options.port);
|
|
1518
|
+
const portStatus = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
|
1519
|
+
? await checkPortAvailability(parsedPort)
|
|
1520
|
+
: undefined;
|
|
1521
|
+
const report = runDoctorChecks({
|
|
1522
|
+
runCommand: runCmd,
|
|
1523
|
+
configExists: fs.existsSync(getManyoyoConfigPath()),
|
|
1524
|
+
imageName: IMAGE_NAME,
|
|
1525
|
+
imageVersion: IMAGE_VERSION,
|
|
1526
|
+
agentCommand: EXEC_COMMAND,
|
|
1527
|
+
containerMode: contModeValue || 'common',
|
|
1528
|
+
pluginConfig: config.plugins,
|
|
1529
|
+
portStatus
|
|
1530
|
+
});
|
|
1531
|
+
if (options.json) {
|
|
1532
|
+
console.log(JSON.stringify(report, null, 4));
|
|
1533
|
+
} else {
|
|
1534
|
+
report.checks.forEach(check => {
|
|
1535
|
+
console.log(`[${check.status.toUpperCase()}] ${check.code}: ${check.summary}${check.action ? ` (${check.action})` : ''}`);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
process.exit(report.ok ? 0 : 1);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1531
1541
|
if (isPsMode) { getContList(); process.exit(0); }
|
|
1532
1542
|
if (isImagesMode) { getImageList(); process.exit(0); }
|
|
1533
1543
|
if (isPruneMode) { pruneDanglingImages(); process.exit(0); }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const AGENT_ADAPTERS = {
|
|
4
|
+
claude: {
|
|
5
|
+
id: 'claude',
|
|
6
|
+
aliases: ['claude', 'cc', 'c'],
|
|
7
|
+
yoloCommand: 'IS_SANDBOX=1 claude --dangerously-skip-permissions'
|
|
8
|
+
},
|
|
9
|
+
gemini: {
|
|
10
|
+
id: 'gemini',
|
|
11
|
+
aliases: ['gemini', 'gm', 'g'],
|
|
12
|
+
yoloCommand: 'gemini --yolo'
|
|
13
|
+
},
|
|
14
|
+
codex: {
|
|
15
|
+
id: 'codex',
|
|
16
|
+
aliases: ['codex', 'cx'],
|
|
17
|
+
yoloCommand: 'codex --dangerously-bypass-approvals-and-sandbox'
|
|
18
|
+
},
|
|
19
|
+
opencode: {
|
|
20
|
+
id: 'opencode',
|
|
21
|
+
aliases: ['opencode', 'oc'],
|
|
22
|
+
yoloCommand: 'OPENCODE_PERMISSION=\'{"*":"allow"}\' opencode'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function findAdapterByAlias(value) {
|
|
27
|
+
const key = String(value || '').trim().toLowerCase();
|
|
28
|
+
if (!key) return null;
|
|
29
|
+
return Object.values(AGENT_ADAPTERS).find(adapter => adapter.aliases.includes(key)) || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveYoloCommand(yolo) {
|
|
33
|
+
const key = String(yolo || '').trim();
|
|
34
|
+
if (!key) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
const adapter = findAdapterByAlias(key);
|
|
38
|
+
if (!adapter) {
|
|
39
|
+
throw new Error(`未知 yolo 值: ${yolo}`);
|
|
40
|
+
}
|
|
41
|
+
return adapter.yoloCommand;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
AGENT_ADAPTERS,
|
|
46
|
+
resolveYoloCommand
|
|
47
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CONTAINER_MODE_ALIASES = {
|
|
4
|
+
common: 'common',
|
|
5
|
+
'docker-in-docker': 'dind',
|
|
6
|
+
dind: 'dind',
|
|
7
|
+
d: 'dind',
|
|
8
|
+
'mount-docker-socket': 'sock',
|
|
9
|
+
sock: 'sock',
|
|
10
|
+
s: 'sock'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const CONTAINER_MODE_ARGS = {
|
|
14
|
+
common: [],
|
|
15
|
+
dind: ['--privileged'],
|
|
16
|
+
sock: [
|
|
17
|
+
'--privileged',
|
|
18
|
+
'--volume', '/var/run/docker.sock:/var/run/docker.sock',
|
|
19
|
+
'--env', 'DOCKER_HOST=unix:///var/run/docker.sock',
|
|
20
|
+
'--env', 'CONTAINER_HOST=unix:///var/run/docker.sock'
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function resolveContainerMode(mode) {
|
|
25
|
+
const normalizedMode = CONTAINER_MODE_ALIASES[String(mode || '').trim().toLowerCase()];
|
|
26
|
+
if (!normalizedMode) {
|
|
27
|
+
throw new Error(`未知 containerMode: ${mode}`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
mode: normalizedMode,
|
|
31
|
+
args: CONTAINER_MODE_ARGS[normalizedMode].slice()
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
resolveContainerMode
|
|
37
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
class AppError extends Error {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
const summary = String(options.summary || '服务暂时不可用');
|
|
8
|
+
super(summary);
|
|
9
|
+
this.name = 'AppError';
|
|
10
|
+
this.code = String(options.code || 'INTERNAL_ERROR');
|
|
11
|
+
this.summary = summary;
|
|
12
|
+
this.detail = typeof options.detail === 'string' ? options.detail : '';
|
|
13
|
+
this.retryable = options.retryable === true;
|
|
14
|
+
this.action = typeof options.action === 'string' ? options.action : '';
|
|
15
|
+
this.correlationId = String(options.correlationId || crypto.randomUUID());
|
|
16
|
+
this.statusCode = Number.isInteger(options.statusCode) ? options.statusCode : 500;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toAppError(error) {
|
|
21
|
+
if (error instanceof AppError) {
|
|
22
|
+
return error;
|
|
23
|
+
}
|
|
24
|
+
return new AppError({
|
|
25
|
+
code: 'INTERNAL_ERROR',
|
|
26
|
+
summary: '服务暂时不可用',
|
|
27
|
+
retryable: true,
|
|
28
|
+
action: '请稍后重试',
|
|
29
|
+
statusCode: 500
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toErrorPayload(error) {
|
|
34
|
+
const appError = toAppError(error);
|
|
35
|
+
return {
|
|
36
|
+
error: {
|
|
37
|
+
code: appError.code,
|
|
38
|
+
summary: appError.summary,
|
|
39
|
+
detail: appError.detail,
|
|
40
|
+
retryable: appError.retryable,
|
|
41
|
+
action: appError.action,
|
|
42
|
+
correlationId: appError.correlationId
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toHttpAppError(statusCode, payload = {}) {
|
|
48
|
+
if (payload.error instanceof AppError) {
|
|
49
|
+
return payload.error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const message = typeof payload.error === 'string' ? payload.error : '';
|
|
53
|
+
const definitions = {
|
|
54
|
+
400: ['INVALID_REQUEST', message || '请求参数错误', false],
|
|
55
|
+
401: ['UNAUTHORIZED', message === 'UNAUTHORIZED' ? '未登录或登录已过期' : (message || '未登录或登录已过期'), false],
|
|
56
|
+
403: ['FORBIDDEN', message || '没有执行此操作的权限', false],
|
|
57
|
+
404: ['NOT_FOUND', message || '请求的资源不存在', false],
|
|
58
|
+
409: ['CONFLICT', message || '请求与当前状态冲突', false],
|
|
59
|
+
429: ['RATE_LIMITED', message || '请求过于频繁,请稍后重试', true]
|
|
60
|
+
};
|
|
61
|
+
const definition = definitions[statusCode];
|
|
62
|
+
if (definition) {
|
|
63
|
+
return new AppError({
|
|
64
|
+
code: definition[0],
|
|
65
|
+
summary: definition[1],
|
|
66
|
+
detail: typeof payload.detail === 'string' ? payload.detail : '',
|
|
67
|
+
retryable: definition[2],
|
|
68
|
+
action: definition[2] ? '请稍后重试' : '',
|
|
69
|
+
statusCode
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return toAppError(payload.error instanceof Error ? payload.error : new Error(message));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
AppError,
|
|
78
|
+
toAppError,
|
|
79
|
+
toErrorPayload,
|
|
80
|
+
toHttpAppError
|
|
81
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
validateControlEvent,
|
|
7
|
+
selectEventsAfterCursor,
|
|
8
|
+
projectSessionEvents
|
|
9
|
+
} = require('./events');
|
|
10
|
+
|
|
11
|
+
function getAggregateFileName(aggregateId) {
|
|
12
|
+
return encodeURIComponent(String(aggregateId || '').trim());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class FileEventStore {
|
|
16
|
+
constructor(rootDir) {
|
|
17
|
+
this.rootDir = path.resolve(rootDir);
|
|
18
|
+
this.eventsDir = path.join(this.rootDir, 'events');
|
|
19
|
+
this.projectionsDir = path.join(this.rootDir, 'projections');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getEventFilePath(aggregateId) {
|
|
23
|
+
return path.join(this.eventsDir, `${getAggregateFileName(aggregateId)}.jsonl`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getProjectionFilePath(aggregateId) {
|
|
27
|
+
return path.join(this.projectionsDir, `${getAggregateFileName(aggregateId)}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
read(aggregateId) {
|
|
31
|
+
const normalizedAggregateId = String(aggregateId || '').trim();
|
|
32
|
+
const filePath = this.getEventFilePath(normalizedAggregateId);
|
|
33
|
+
if (!fs.existsSync(filePath)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
38
|
+
const events = [];
|
|
39
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
40
|
+
const line = lines[index].trim();
|
|
41
|
+
if (!line) continue;
|
|
42
|
+
try {
|
|
43
|
+
const event = JSON.parse(line);
|
|
44
|
+
validateControlEvent(event);
|
|
45
|
+
if (event.aggregateId !== normalizedAggregateId) {
|
|
46
|
+
throw new Error('事件 aggregateId 与日志不一致');
|
|
47
|
+
}
|
|
48
|
+
events.push(event);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
const isFinalLine = index === lines.length - 1;
|
|
51
|
+
if (isFinalLine) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
selectEventsAfterCursor(events, 0);
|
|
58
|
+
return events;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
append(event) {
|
|
62
|
+
validateControlEvent(event);
|
|
63
|
+
const events = this.read(event.aggregateId);
|
|
64
|
+
const expectedSeq = events.length ? events[events.length - 1].seq + 1 : event.seq;
|
|
65
|
+
if (event.seq !== expectedSeq) {
|
|
66
|
+
throw new Error(`seq 必须连续递增,期望 ${expectedSeq},实际 ${event.seq}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(this.eventsDir, { recursive: true });
|
|
70
|
+
fs.appendFileSync(this.getEventFilePath(event.aggregateId), `${JSON.stringify(event)}\n`);
|
|
71
|
+
const projection = projectSessionEvents([...events, event]);
|
|
72
|
+
this.saveProjection(event.aggregateId, projection);
|
|
73
|
+
return event;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
loadProjection(aggregateId) {
|
|
77
|
+
const events = this.read(aggregateId);
|
|
78
|
+
const projection = projectSessionEvents(events);
|
|
79
|
+
const projectionPath = this.getProjectionFilePath(aggregateId);
|
|
80
|
+
if (!fs.existsSync(projectionPath)) {
|
|
81
|
+
return projection;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const saved = JSON.parse(fs.readFileSync(projectionPath, 'utf-8'));
|
|
85
|
+
if (saved && saved.aggregateId === projection.aggregateId && saved.lastSeq === projection.lastSeq) {
|
|
86
|
+
return saved;
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// 以事件日志为准重建损坏的投影快照。
|
|
90
|
+
}
|
|
91
|
+
this.saveProjection(aggregateId, projection);
|
|
92
|
+
return projection;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
saveProjection(aggregateId, projection) {
|
|
96
|
+
fs.mkdirSync(this.projectionsDir, { recursive: true });
|
|
97
|
+
const targetPath = this.getProjectionFilePath(aggregateId);
|
|
98
|
+
const temporaryPath = `${targetPath}.${process.pid}.tmp`;
|
|
99
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(projection)}\n`);
|
|
100
|
+
fs.renameSync(temporaryPath, targetPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
FileEventStore
|
|
106
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const EVENT_VERSION = 1;
|
|
6
|
+
const EVENT_TYPES = new Set([
|
|
7
|
+
'session.created',
|
|
8
|
+
'session.ready',
|
|
9
|
+
'session.stopping',
|
|
10
|
+
'session.stopped',
|
|
11
|
+
'session.failed',
|
|
12
|
+
'process.started',
|
|
13
|
+
'process.stdout',
|
|
14
|
+
'process.stderr',
|
|
15
|
+
'process.exited',
|
|
16
|
+
'process.interrupted',
|
|
17
|
+
'agent.turn.started',
|
|
18
|
+
'agent.turn.delta',
|
|
19
|
+
'agent.message.completed',
|
|
20
|
+
'agent.turn.failed',
|
|
21
|
+
'agent.tool.observed',
|
|
22
|
+
'agent.child.observed',
|
|
23
|
+
'agent.child.completed',
|
|
24
|
+
'agent.child.failed',
|
|
25
|
+
'agent.child.interrupted',
|
|
26
|
+
'artifact.created',
|
|
27
|
+
'artifact.changed',
|
|
28
|
+
'artifact.deleted',
|
|
29
|
+
'terminal.opened',
|
|
30
|
+
'terminal.resized',
|
|
31
|
+
'terminal.closed'
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function createControlEvent(input = {}) {
|
|
35
|
+
const aggregateId = String(input.aggregateId || input.sessionId || '').trim();
|
|
36
|
+
const event = {
|
|
37
|
+
id: input.id || crypto.randomUUID(),
|
|
38
|
+
type: input.type,
|
|
39
|
+
version: input.version === undefined ? EVENT_VERSION : input.version,
|
|
40
|
+
aggregateId,
|
|
41
|
+
sessionId: input.sessionId || aggregateId,
|
|
42
|
+
seq: input.seq,
|
|
43
|
+
timestamp: input.timestamp || new Date().toISOString(),
|
|
44
|
+
data: input.data || {}
|
|
45
|
+
};
|
|
46
|
+
return validateControlEvent(event);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateControlEvent(event) {
|
|
50
|
+
if (!event || typeof event !== 'object') {
|
|
51
|
+
throw new Error('事件必须为对象');
|
|
52
|
+
}
|
|
53
|
+
if (!EVENT_TYPES.has(event.type)) {
|
|
54
|
+
throw new Error(`未知事件类型: ${event.type}`);
|
|
55
|
+
}
|
|
56
|
+
if (event.version !== EVENT_VERSION) {
|
|
57
|
+
throw new Error(`不支持的事件版本: ${event.version}`);
|
|
58
|
+
}
|
|
59
|
+
if (!String(event.id || '').trim()) {
|
|
60
|
+
throw new Error('事件 id 不能为空');
|
|
61
|
+
}
|
|
62
|
+
if (!String(event.aggregateId || '').trim()) {
|
|
63
|
+
throw new Error('aggregateId 不能为空');
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isInteger(event.seq) || event.seq <= 0) {
|
|
66
|
+
throw new Error('seq 必须为正整数');
|
|
67
|
+
}
|
|
68
|
+
if (!String(event.timestamp || '').trim()) {
|
|
69
|
+
throw new Error('timestamp 不能为空');
|
|
70
|
+
}
|
|
71
|
+
if (!event.data || typeof event.data !== 'object' || Array.isArray(event.data)) {
|
|
72
|
+
throw new Error('data 必须为对象');
|
|
73
|
+
}
|
|
74
|
+
return event;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function selectEventsAfterCursor(events, cursor = 0) {
|
|
78
|
+
const normalizedCursor = Number(cursor || 0);
|
|
79
|
+
if (!Number.isInteger(normalizedCursor) || normalizedCursor < 0) {
|
|
80
|
+
throw new Error('cursor 必须为非负整数');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let previousSeq = 0;
|
|
84
|
+
let aggregateId = '';
|
|
85
|
+
const result = [];
|
|
86
|
+
for (const event of events || []) {
|
|
87
|
+
validateControlEvent(event);
|
|
88
|
+
if (!aggregateId) {
|
|
89
|
+
aggregateId = event.aggregateId;
|
|
90
|
+
} else if (aggregateId !== event.aggregateId) {
|
|
91
|
+
throw new Error('事件 aggregateId 必须一致');
|
|
92
|
+
}
|
|
93
|
+
if (event.seq <= previousSeq) {
|
|
94
|
+
throw new Error('seq 必须严格递增');
|
|
95
|
+
}
|
|
96
|
+
previousSeq = event.seq;
|
|
97
|
+
if (event.seq > normalizedCursor) {
|
|
98
|
+
result.push(event);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function projectSessionEvents(events) {
|
|
105
|
+
const orderedEvents = selectEventsAfterCursor(events, 0);
|
|
106
|
+
const projection = {
|
|
107
|
+
aggregateId: orderedEvents.length ? orderedEvents[0].aggregateId : '',
|
|
108
|
+
status: 'idle',
|
|
109
|
+
lastSeq: orderedEvents.length ? orderedEvents[orderedEvents.length - 1].seq : 0,
|
|
110
|
+
childSessions: []
|
|
111
|
+
};
|
|
112
|
+
const childSessions = new Map();
|
|
113
|
+
|
|
114
|
+
for (const event of orderedEvents) {
|
|
115
|
+
if (event.type === 'session.created') projection.status = 'starting';
|
|
116
|
+
if (event.type === 'session.ready' || event.type === 'process.started' || event.type === 'agent.turn.started') projection.status = 'running';
|
|
117
|
+
if (event.type === 'session.stopping') projection.status = 'stopping';
|
|
118
|
+
if (event.type === 'session.stopped' || event.type === 'process.interrupted') projection.status = 'interrupted';
|
|
119
|
+
if (event.type === 'session.failed' || event.type === 'agent.turn.failed') projection.status = 'failed';
|
|
120
|
+
if (event.type === 'process.exited') {
|
|
121
|
+
projection.status = Number(event.data.exitCode) === 0 ? 'completed' : 'failed';
|
|
122
|
+
}
|
|
123
|
+
if (event.type.startsWith('agent.child.')) {
|
|
124
|
+
const childSessionId = String(event.data.childSessionId || '').trim();
|
|
125
|
+
if (!childSessionId) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const current = childSessions.get(childSessionId) || { id: childSessionId, agentProgram: '', status: 'observed' };
|
|
129
|
+
if (event.data.agentProgram) {
|
|
130
|
+
current.agentProgram = String(event.data.agentProgram);
|
|
131
|
+
}
|
|
132
|
+
if (event.type === 'agent.child.completed') current.status = 'completed';
|
|
133
|
+
if (event.type === 'agent.child.failed') current.status = 'failed';
|
|
134
|
+
if (event.type === 'agent.child.interrupted') current.status = 'interrupted';
|
|
135
|
+
childSessions.set(childSessionId, current);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
projection.childSessions = Array.from(childSessions.values());
|
|
139
|
+
return projection;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
EVENT_VERSION,
|
|
144
|
+
EVENT_TYPES,
|
|
145
|
+
createControlEvent,
|
|
146
|
+
validateControlEvent,
|
|
147
|
+
selectEventsAfterCursor,
|
|
148
|
+
projectSessionEvents
|
|
149
|
+
};
|
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveContainerMode } = require('./container-modes');
|
|
4
|
+
const { resolveAgentProgram } = require('./agent-resume');
|
|
5
|
+
|
|
6
|
+
function createCheck(code, status, summary, action = '', detail = '') {
|
|
7
|
+
return { code, status, summary, action, detail };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function runDoctorChecks(options = {}) {
|
|
11
|
+
const checks = [];
|
|
12
|
+
const runCommand = typeof options.runCommand === 'function' ? options.runCommand : () => {
|
|
13
|
+
throw new Error('未配置命令执行器');
|
|
14
|
+
};
|
|
15
|
+
let runtimeCommand = '';
|
|
16
|
+
|
|
17
|
+
for (const candidate of (options.runtimeCandidates || ['docker', 'podman'])) {
|
|
18
|
+
try {
|
|
19
|
+
const detail = String(runCommand(candidate, ['--version']) || '').trim();
|
|
20
|
+
runtimeCommand = candidate;
|
|
21
|
+
checks.push(createCheck('RUNTIME_AVAILABLE', 'ok', `检测到 ${candidate}`, '', detail));
|
|
22
|
+
break;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// 尝试下一个受支持运行时。
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!runtimeCommand) {
|
|
28
|
+
checks.push(createCheck('RUNTIME_UNAVAILABLE', 'error', '未找到 Docker 或 Podman', '安装并启动 Docker Desktop 或 Podman。'));
|
|
29
|
+
} else {
|
|
30
|
+
try {
|
|
31
|
+
const detail = String(runCommand(runtimeCommand, ['info']) || '').trim();
|
|
32
|
+
checks.push(createCheck('DAEMON_AVAILABLE', 'ok', `${runtimeCommand} daemon 可用`, '', detail));
|
|
33
|
+
} catch (error) {
|
|
34
|
+
checks.push(createCheck('DAEMON_UNAVAILABLE', 'error', `${runtimeCommand} daemon 不可用`, `启动 ${runtimeCommand} daemon 后重试。`, error.message || ''));
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const image = `${options.imageName || ''}:${options.imageVersion || ''}`;
|
|
38
|
+
const detail = String(runCommand(runtimeCommand, ['image', 'inspect', image]) || '').trim();
|
|
39
|
+
checks.push(createCheck('IMAGE_AVAILABLE', 'ok', `镜像可用: ${image}`, '', detail));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
checks.push(createCheck('IMAGE_MISSING', 'warning', '目标镜像尚不可用', '执行 manyoyo build,或拉取匹配镜像。', error.message || ''));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
checks.push(options.configExists === true
|
|
46
|
+
? createCheck('CONFIG_AVAILABLE', 'ok', '配置文件可用')
|
|
47
|
+
: createCheck('CONFIG_MISSING', 'warning', '未找到配置文件', '执行 manyoyo init 或通过 run 参数指定配置。'));
|
|
48
|
+
|
|
49
|
+
const agentProgram = resolveAgentProgram(options.agentCommand || '');
|
|
50
|
+
checks.push(agentProgram
|
|
51
|
+
? createCheck('AGENT_CONFIGURED', 'ok', `已配置 Agent: ${agentProgram}`)
|
|
52
|
+
: createCheck('AGENT_NOT_CONFIGURED', 'warning', '未配置 Agent 命令', '设置 shell、yolo 或 agentPromptCommand。'));
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const mode = resolveContainerMode(options.containerMode || 'common');
|
|
56
|
+
checks.push(createCheck('MODE_VALID', 'ok', `容器模式有效: ${mode.mode}`));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
checks.push(createCheck('MODE_INVALID', 'error', '容器模式无效', '使用 common、dind 或 sock。', error.message || ''));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pluginConfig = options.pluginConfig;
|
|
62
|
+
checks.push(pluginConfig && typeof pluginConfig === 'object' && !Array.isArray(pluginConfig)
|
|
63
|
+
? createCheck('PLUGIN_CONFIG_VALID', 'ok', '插件配置有效')
|
|
64
|
+
: createCheck('PLUGIN_CONFIG_INVALID', 'warning', '插件配置不存在或格式无效', '将 plugins 配置为对象(map)。'));
|
|
65
|
+
|
|
66
|
+
if (options.portStatus === 'available') {
|
|
67
|
+
checks.push(createCheck('PORT_AVAILABLE', 'ok', '监听端口可用'));
|
|
68
|
+
} else if (options.portStatus === 'occupied') {
|
|
69
|
+
checks.push(createCheck('PORT_OCCUPIED', 'warning', '监听端口已占用', '选择其他 serve 端口或停止占用进程。'));
|
|
70
|
+
} else {
|
|
71
|
+
checks.push(createCheck('PORT_NOT_CHECKED', 'warning', '未检查监听端口', '使用 doctor --port <port> 检查端口。'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
runtimeCommand: runtimeCommand || null,
|
|
77
|
+
ok: !checks.some(check => check.status === 'error'),
|
|
78
|
+
checks
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
runDoctorChecks
|
|
84
|
+
};
|