@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 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
- const key = String(cli || '').trim().toLowerCase();
653
- const mappedCommand = YOLO_COMMAND_MAP[key];
654
- if (!mappedCommand) {
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
- const modeAliasMap = {
667
- common: 'common',
668
- 'docker-in-docker': 'dind',
669
- dind: 'dind',
670
- d: 'dind',
671
- 'mount-docker-socket': 'sock',
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
- if (normalizedMode === 'dind') {
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 (normalizedMode === 'sock') {
689
- CONT_MODE_ARGS = [
690
- '--privileged',
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
+ };