agent-window 1.2.1 → 1.2.4
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/config/config.json.backup +12 -0
- package/package.json +1 -1
- package/src/api/routes/instances.js +120 -19
- package/src/api/routes/operations.js +221 -1
- package/src/bot.js +200 -2
- package/src/core/instance/manager.js +114 -15
- package/src/core/instance/pm2-bridge.js +76 -6
- package/src/core/instance/validator.js +7 -7
- package/src/core/platform/pm2-bridge.js +5 -0
- package/web/dist/assets/Dashboard-1TTWybTq.js +1 -0
- package/web/dist/assets/Dashboard-DRg2tFpk.css +1 -0
- package/web/dist/assets/{InstanceDetail-B2M2l7Dy.css → InstanceDetail-BRMjUfAV.css} +1 -1
- package/web/dist/assets/InstanceDetail-BWV1wz24.js +3 -0
- package/web/dist/assets/{Instances-VQZar42B.js → Instances-D4SbRoep.js} +1 -1
- package/web/dist/assets/{Settings-BCwW_7gk.js → Settings-CdoSWOhM.js} +1 -1
- package/web/dist/assets/{main-BGxPBmf2.js → main-BKf0mqau.js} +4 -4
- package/web/dist/index.html +1 -1
- package/web/dist/assets/Dashboard-BTrinn8p.js +0 -1
- package/web/dist/assets/Dashboard-BicWOr3D.css +0 -1
- package/web/dist/assets/InstanceDetail-DAERYy59.js +0 -3
package/src/bot.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Permission system with chat-based approval buttons
|
|
7
7
|
* - Session management with channel binding
|
|
8
8
|
* - Multi-agent support (Claude Code, Codex, OpenCode)
|
|
9
|
+
* - Model selection (opus/sonnet/haiku, default: opus)
|
|
9
10
|
*
|
|
10
11
|
* Commands:
|
|
11
12
|
* @bot <message> - Send message (continues last conversation)
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* @bot /pick - Choose session (button UI)
|
|
15
16
|
* @bot /sessions - List recent sessions
|
|
16
17
|
* @bot /status - Check bot status
|
|
18
|
+
* @bot /model [model] - View or change AI model (opus/sonnet/haiku)
|
|
17
19
|
* @bot /help - Show help
|
|
18
20
|
*/
|
|
19
21
|
|
|
@@ -67,6 +69,59 @@ const pendingPermissions = new Map(); // requestId -> { channelId, message }
|
|
|
67
69
|
|
|
68
70
|
let containerReady = false;
|
|
69
71
|
|
|
72
|
+
// Model configuration
|
|
73
|
+
const AVAILABLE_MODELS = {
|
|
74
|
+
opus: 'claude-opus-4-20250514',
|
|
75
|
+
sonnet: 'claude-sonnet-4-20250514',
|
|
76
|
+
haiku: 'claude-haiku-4-20250514'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Per-channel model selection (channelId -> modelKey)
|
|
80
|
+
const channelModels = new Map();
|
|
81
|
+
|
|
82
|
+
// Default model (opus as requested)
|
|
83
|
+
const DEFAULT_MODEL = 'opus';
|
|
84
|
+
|
|
85
|
+
// Health status tracking
|
|
86
|
+
const HEALTH_STATUS_FILE = join(PROJECT_DIR, '.health-status.json');
|
|
87
|
+
const healthStatus = {
|
|
88
|
+
pm2: false,
|
|
89
|
+
discord: false,
|
|
90
|
+
docker: false,
|
|
91
|
+
startTime: Date.now(),
|
|
92
|
+
lastUpdate: Date.now()
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Update health status
|
|
96
|
+
function updateHealthStatus(component, status) {
|
|
97
|
+
healthStatus[component] = status;
|
|
98
|
+
healthStatus.lastUpdate = Date.now();
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(HEALTH_STATUS_FILE, JSON.stringify(healthStatus, null, 2));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error('[Health] Failed to write status file:', e.message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get overall health status
|
|
107
|
+
function getOverallHealth() {
|
|
108
|
+
const checks = [
|
|
109
|
+
healthStatus.pm2 ? '✓' : '✗',
|
|
110
|
+
healthStatus.discord ? '✓' : '✗',
|
|
111
|
+
healthStatus.docker ? '✓' : '✗'
|
|
112
|
+
].join(' ');
|
|
113
|
+
|
|
114
|
+
if (healthStatus.pm2 && healthStatus.discord && healthStatus.docker) {
|
|
115
|
+
return 'healthy';
|
|
116
|
+
} else if (healthStatus.pm2 && healthStatus.discord) {
|
|
117
|
+
return 'degraded'; // Running but Docker failed
|
|
118
|
+
} else if (healthStatus.pm2) {
|
|
119
|
+
return 'unhealthy'; // PM2 running but Discord disconnected
|
|
120
|
+
} else {
|
|
121
|
+
return 'failed';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
70
125
|
// Ensure pending directory exists
|
|
71
126
|
try {
|
|
72
127
|
mkdirSync(PENDING_DIR, { recursive: true });
|
|
@@ -147,9 +202,26 @@ function ensureContainer() {
|
|
|
147
202
|
|
|
148
203
|
execSync(dockerCmd);
|
|
149
204
|
console.log('[Docker] Container started successfully');
|
|
205
|
+
updateHealthStatus('docker', true);
|
|
150
206
|
return true;
|
|
151
207
|
} catch (e) {
|
|
152
|
-
|
|
208
|
+
const errorMsg = e.message || '';
|
|
209
|
+
console.error('[Docker] Failed to start container:', errorMsg);
|
|
210
|
+
|
|
211
|
+
// Provide helpful guidance for common Docker issues
|
|
212
|
+
if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('Cannot connect') || errorMsg.includes('connect')) {
|
|
213
|
+
console.error('[Docker] Docker daemon is not running!');
|
|
214
|
+
console.error('[Docker] Please restart Docker Desktop:');
|
|
215
|
+
console.error('[Docker] 1. Click Docker Desktop icon in menu bar');
|
|
216
|
+
console.error('[Docker] 2. Select "Restart" or wait for daemon to start');
|
|
217
|
+
} else if (errorMsg.includes('image not found') || errorMsg.includes('no such image')) {
|
|
218
|
+
console.error('[Docker] Docker image not found.');
|
|
219
|
+
console.error('[Docker] Pull the image first: docker pull', DOCKER_IMAGE);
|
|
220
|
+
} else if (errorMsg.includes('permission denied')) {
|
|
221
|
+
console.error('[Docker] Permission denied. Try: sudo docker pull', DOCKER_IMAGE);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
updateHealthStatus('docker', false);
|
|
153
225
|
return false;
|
|
154
226
|
}
|
|
155
227
|
}
|
|
@@ -742,6 +814,7 @@ const commands = {
|
|
|
742
814
|
\`@bot /pick\` - Choose session (new or resume)
|
|
743
815
|
\`@bot /sessions\` - List all recent sessions
|
|
744
816
|
\`@bot /status\` - Check bot status
|
|
817
|
+
\`@bot /model\` - View or change AI model (opus/sonnet/haiku)
|
|
745
818
|
\`@bot /help\` - Show this help`;
|
|
746
819
|
|
|
747
820
|
await message.channel.send({
|
|
@@ -762,9 +835,11 @@ const commands = {
|
|
|
762
835
|
const hours = Math.floor(uptime / 3600);
|
|
763
836
|
const minutes = Math.floor((uptime % 3600) / 60);
|
|
764
837
|
const containerRunning = isContainerRunning();
|
|
838
|
+
const currentModel = channelModels.get(message.channel.id) || DEFAULT_MODEL;
|
|
765
839
|
|
|
766
840
|
const statusText = `**Status**: Online
|
|
767
841
|
**Mode**: Docker Sandbox + Hooks Permission
|
|
842
|
+
**Model**: ${currentModel} (${AVAILABLE_MODELS[currentModel]})
|
|
768
843
|
**Uptime**: ${hours}h ${minutes}m
|
|
769
844
|
**Container**: ${CONTAINER_NAME} (${containerRunning ? '🟢 running' : '🔴 stopped'})
|
|
770
845
|
**Project**: ${PROJECT_DIR}
|
|
@@ -824,6 +899,56 @@ const commands = {
|
|
|
824
899
|
}
|
|
825
900
|
},
|
|
826
901
|
|
|
902
|
+
async model(message, args) {
|
|
903
|
+
const channelId = message.channel.id;
|
|
904
|
+
const action = args[0];
|
|
905
|
+
|
|
906
|
+
// Show current model
|
|
907
|
+
if (!action) {
|
|
908
|
+
const currentModel = channelModels.get(channelId) || DEFAULT_MODEL;
|
|
909
|
+
const modelId = AVAILABLE_MODELS[currentModel];
|
|
910
|
+
|
|
911
|
+
const modelText = `**Current Model:** ${currentModel} (${modelId})
|
|
912
|
+
|
|
913
|
+
**Available Models:**
|
|
914
|
+
• **opus** - Most capable, best for complex tasks
|
|
915
|
+
• **sonnet** - Balanced performance and speed
|
|
916
|
+
• **haiku** - Fastest, for simple tasks
|
|
917
|
+
|
|
918
|
+
**Usage:**
|
|
919
|
+
\`@bot /model opus\` - Switch to Opus
|
|
920
|
+
\`@bot /model sonnet\` - Switch to Sonnet
|
|
921
|
+
\`@bot /model haiku\` - Switch to Haiku`;
|
|
922
|
+
|
|
923
|
+
await message.channel.send({
|
|
924
|
+
embeds: [{
|
|
925
|
+
title: 'AI Model Configuration',
|
|
926
|
+
description: modelText,
|
|
927
|
+
color: currentModel === 'opus' ? 0x9B59B6 : currentModel === 'sonnet' ? 0x3498DB : 0x2ECC71,
|
|
928
|
+
}]
|
|
929
|
+
});
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Switch model
|
|
934
|
+
const newModel = action.toLowerCase();
|
|
935
|
+
if (!AVAILABLE_MODELS[newModel]) {
|
|
936
|
+
await message.channel.send(`Invalid model: ${action}\nAvailable models: opus, sonnet, haiku`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
channelModels.set(channelId, newModel);
|
|
941
|
+
const modelId = AVAILABLE_MODELS[newModel];
|
|
942
|
+
|
|
943
|
+
await message.channel.send({
|
|
944
|
+
embeds: [{
|
|
945
|
+
title: 'Model Changed',
|
|
946
|
+
description: `Switched to **${newModel}** (${modelId})\n\nThis model will be used for new conversations in this channel.`,
|
|
947
|
+
color: newModel === 'opus' ? 0x9B59B6 : newModel === 'sonnet' ? 0x3498DB : 0x2ECC71,
|
|
948
|
+
}]
|
|
949
|
+
});
|
|
950
|
+
},
|
|
951
|
+
|
|
827
952
|
// Show session selection card with buttons
|
|
828
953
|
async showSessionPicker(message) {
|
|
829
954
|
try {
|
|
@@ -1016,6 +1141,12 @@ const commands = {
|
|
|
1016
1141
|
'--settings', CONTAINER_SETTINGS_FILE,
|
|
1017
1142
|
];
|
|
1018
1143
|
|
|
1144
|
+
// Add model selection (defaults to opus)
|
|
1145
|
+
const selectedModel = channelModels.get(channelId) || DEFAULT_MODEL;
|
|
1146
|
+
const modelId = AVAILABLE_MODELS[selectedModel];
|
|
1147
|
+
cliArgs.push('--model', modelId);
|
|
1148
|
+
console.log('[Claude] Using model:', selectedModel, `(${modelId})`);
|
|
1149
|
+
|
|
1019
1150
|
// Track if we're forcing a new session or resuming a specific one
|
|
1020
1151
|
if (userMessage.startsWith('-n ') || userMessage.startsWith('--new ')) {
|
|
1021
1152
|
// Force new session
|
|
@@ -1439,7 +1570,7 @@ client.on(Events.MessageCreate, async (message) => {
|
|
|
1439
1570
|
});
|
|
1440
1571
|
|
|
1441
1572
|
// Ready event
|
|
1442
|
-
client.on(Events.ClientReady, () => {
|
|
1573
|
+
client.on(Events.ClientReady, async () => {
|
|
1443
1574
|
console.log(`AgentBridge logged in as ${client.user.tag}`);
|
|
1444
1575
|
console.log(`Bot User ID: ${client.user.id}`);
|
|
1445
1576
|
console.log(`Project: ${PROJECT_DIR}`);
|
|
@@ -1447,6 +1578,10 @@ client.on(Events.ClientReady, () => {
|
|
|
1447
1578
|
console.log(`Container: ${CONTAINER_NAME}`);
|
|
1448
1579
|
console.log(`OAuth: ${OAUTH_TOKEN ? 'configured' : 'not set'}`);
|
|
1449
1580
|
|
|
1581
|
+
// Update PM2 and Discord health status
|
|
1582
|
+
updateHealthStatus('pm2', true);
|
|
1583
|
+
updateHealthStatus('discord', true);
|
|
1584
|
+
|
|
1450
1585
|
// Start persistent container (non-blocking)
|
|
1451
1586
|
console.log('[Docker] Attempting to start container...');
|
|
1452
1587
|
containerReady = ensureContainer();
|
|
@@ -1455,6 +1590,12 @@ client.on(Events.ClientReady, () => {
|
|
|
1455
1590
|
console.warn('[Docker] AI commands will not work without the container.');
|
|
1456
1591
|
console.warn('[Docker] To fix: 1) Login to ghcr.io, 2) Restart bot');
|
|
1457
1592
|
}
|
|
1593
|
+
|
|
1594
|
+
// Log overall health status
|
|
1595
|
+
const overallHealth = getOverallHealth();
|
|
1596
|
+
console.log(`[Health] Overall status: ${overallHealth.toUpperCase()}`);
|
|
1597
|
+
console.log(`[Health] Components: PM2=${healthStatus.pm2 ? '✓' : '✗'} Discord=${healthStatus.discord ? '✓' : '✗'} Docker=${healthStatus.docker ? '✓' : '✗'}`);
|
|
1598
|
+
|
|
1458
1599
|
console.log(`Allowed Channels Config: ${ALLOWED_CHANNELS ? ALLOWED_CHANNELS.join(', ') : 'All'}`);
|
|
1459
1600
|
console.log(`Guilds: ${client.guilds.cache.size}`);
|
|
1460
1601
|
client.guilds.cache.forEach(guild => {
|
|
@@ -1471,6 +1612,63 @@ client.on(Events.ClientReady, () => {
|
|
|
1471
1612
|
startPermissionWatcher();
|
|
1472
1613
|
|
|
1473
1614
|
console.log(`Ready! @${client.user.username} to interact`);
|
|
1615
|
+
|
|
1616
|
+
// Send startup notification to Discord channels
|
|
1617
|
+
try {
|
|
1618
|
+
const uptime = Math.floor((Date.now() - healthStatus.startTime) / 1000);
|
|
1619
|
+
const dockerStatus = healthStatus.docker ? '✓ Connected' : '✗ Not Available';
|
|
1620
|
+
const overallStatus = overallHealth.toUpperCase();
|
|
1621
|
+
|
|
1622
|
+
// Build startup message
|
|
1623
|
+
const startupMessage = {
|
|
1624
|
+
embeds: [{
|
|
1625
|
+
title: '🤖 AgentWindow Bot Started',
|
|
1626
|
+
color: overallStatus === 'HEALTHY' ? 0x00FF00 : overallStatus === 'DEGRADED' ? 0xFFFF00 : 0xFF0000,
|
|
1627
|
+
fields: [
|
|
1628
|
+
{
|
|
1629
|
+
name: 'Status',
|
|
1630
|
+
value: overallStatus,
|
|
1631
|
+
inline: true
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
name: 'Docker',
|
|
1635
|
+
value: dockerStatus,
|
|
1636
|
+
inline: true
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
name: 'Startup Time',
|
|
1640
|
+
value: `${uptime}s`,
|
|
1641
|
+
inline: true
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: 'Components',
|
|
1645
|
+
value: `PM2: ✓\nDiscord: ✓\nDocker: ${healthStatus.docker ? '✓' : '✗'}`,
|
|
1646
|
+
inline: false
|
|
1647
|
+
}
|
|
1648
|
+
],
|
|
1649
|
+
timestamp: new Date().toISOString(),
|
|
1650
|
+
footer: {
|
|
1651
|
+
text: `AgentWindow v1.2.1`
|
|
1652
|
+
}
|
|
1653
|
+
}]
|
|
1654
|
+
};
|
|
1655
|
+
|
|
1656
|
+
// Send to all allowed channels
|
|
1657
|
+
const channels = ALLOWED_CHANNELS || [];
|
|
1658
|
+
for (const channelId of channels) {
|
|
1659
|
+
try {
|
|
1660
|
+
const channel = await client.channels.fetch(channelId);
|
|
1661
|
+
if (channel && channel.isTextBased()) {
|
|
1662
|
+
await channel.send(startupMessage);
|
|
1663
|
+
console.log(`[Notification] Startup message sent to #${channel.name}`);
|
|
1664
|
+
}
|
|
1665
|
+
} catch (e) {
|
|
1666
|
+
console.error(`[Notification] Failed to send to channel ${channelId}:`, e.message);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
console.error('[Notification] Failed to send startup notification:', e.message);
|
|
1671
|
+
}
|
|
1474
1672
|
});
|
|
1475
1673
|
|
|
1476
1674
|
// Error handling
|
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Instance Manager
|
|
3
3
|
*
|
|
4
|
-
* Manages AgentWindow
|
|
4
|
+
* Manages AgentWindow bot instances with clear separation between two types:
|
|
5
|
+
*
|
|
6
|
+
* ## Instance Types
|
|
7
|
+
*
|
|
8
|
+
* ### 1. Simple-Config (基础版)
|
|
9
|
+
* - **Discovery**: Auto-discovered from PM2 processes
|
|
10
|
+
* - **Config Location**: ~/.agent-window/bots/{name}/config.json
|
|
11
|
+
* - **Management**: Unified management by AgentWindow
|
|
12
|
+
* - **Use Case**: Simple bots managed through Web UI
|
|
13
|
+
* - **Import Method**: Discovered via "Discover" button in UI
|
|
14
|
+
*
|
|
15
|
+
* ### 2. BMAD-Plugin (BMAD 插件版)
|
|
16
|
+
* - **Discovery**: Manually added by user
|
|
17
|
+
* - **Config Location**: User's project directory
|
|
18
|
+
* - **Management**: User manages their BMAD project
|
|
19
|
+
* - **Use Case**: Development projects with BMAD framework
|
|
20
|
+
* - **Import Method**: Added via "Add Instance" button with validation
|
|
21
|
+
* - **Requirements**:
|
|
22
|
+
* - Must have _agent-bridge/ directory
|
|
23
|
+
* - Must have _agent-bridge/src/bot.js
|
|
24
|
+
* - Must pass validateInstance() check
|
|
25
|
+
*
|
|
26
|
+
* ## Business Rules
|
|
27
|
+
*
|
|
28
|
+
* 1. Simple-config instances are auto-discovered from PM2
|
|
29
|
+
* 2. BMAD-plugin instances must be manually added through UI
|
|
30
|
+
* 3. BMAD-plugin instances require strict validation
|
|
31
|
+
* 4. Simple-config instances cannot be manually added (must be discovered)
|
|
32
|
+
*
|
|
5
33
|
* Uses platform abstraction layer for cross-platform compatibility.
|
|
6
34
|
*/
|
|
7
35
|
|
|
@@ -63,12 +91,19 @@ export async function writeInstances(data) {
|
|
|
63
91
|
|
|
64
92
|
/**
|
|
65
93
|
* Add a new instance
|
|
94
|
+
*
|
|
95
|
+
* Business Logic:
|
|
96
|
+
* - For creating NEW simple-config instances: Pass createConfig option with config data
|
|
97
|
+
* - For importing EXISTING BMAD plugins: Pass projectPath, will be validated
|
|
98
|
+
*
|
|
66
99
|
* @param {string} name - Instance name (identifier)
|
|
67
|
-
* @param {string} projectPath - Path to BMAD
|
|
100
|
+
* @param {string} projectPath - Path to project (for BMAD plugins) or working directory
|
|
68
101
|
* @param {Object} options - Additional options
|
|
69
102
|
* @param {string} options.displayName - Display name
|
|
70
103
|
* @param {string[]} options.tags - Tags for categorization
|
|
71
104
|
* @param {string} options.configPath - Path to config file (optional)
|
|
105
|
+
* @param {string} options.instanceType - Explicit instance type ('simple-config' | 'bmad-plugin')
|
|
106
|
+
* @param {boolean} options.createConfig - Whether this is creating a new simple-config instance
|
|
72
107
|
* @returns {Promise<Object>} Result with success and message
|
|
73
108
|
*/
|
|
74
109
|
export async function addInstance(name, projectPath, options = {}) {
|
|
@@ -89,14 +124,49 @@ export async function addInstance(name, projectPath, options = {}) {
|
|
|
89
124
|
};
|
|
90
125
|
}
|
|
91
126
|
|
|
127
|
+
const botsDir = paths.getBotsDir();
|
|
128
|
+
let newInstance;
|
|
129
|
+
let configPath;
|
|
130
|
+
|
|
131
|
+
// Case 1: Creating a NEW simple-config instance from scratch
|
|
132
|
+
if (options.instanceType === 'simple-config' || options.createConfig) {
|
|
133
|
+
// Config path for simple-config instances
|
|
134
|
+
configPath = options.configPath || path.join(botsDir, name, 'config.json');
|
|
135
|
+
|
|
136
|
+
// Ensure config directory exists
|
|
137
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
138
|
+
|
|
139
|
+
newInstance = {
|
|
140
|
+
name,
|
|
141
|
+
displayName: options.displayName || name,
|
|
142
|
+
projectPath: projectPath, // For simple-config, this is the working directory
|
|
143
|
+
configPath,
|
|
144
|
+
pluginPath: null,
|
|
145
|
+
botName: `bot-${name}`,
|
|
146
|
+
instanceType: 'simple-config',
|
|
147
|
+
addedAt: new Date().toISOString(),
|
|
148
|
+
tags: options.tags || [],
|
|
149
|
+
enabled: true
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
data.instances.push(newInstance);
|
|
153
|
+
await writeInstances(data);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
instance: newInstance
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Case 2: Importing an EXISTING BMAD plugin
|
|
92
162
|
// Validate the instance
|
|
93
163
|
const validation = await validateInstance(projectPath);
|
|
94
164
|
if (!validation.valid) {
|
|
95
|
-
//
|
|
165
|
+
// Only BMAD plugins can be manually added
|
|
96
166
|
const typeMsg = validation.instanceType === 'simple-config'
|
|
97
|
-
? '简单配置实例无法通过"添加实例"
|
|
167
|
+
? '简单配置实例无法通过"添加实例"功能添加。请使用"Discover"功能导入。'
|
|
98
168
|
: validation.instanceType === 'unknown'
|
|
99
|
-
? '
|
|
169
|
+
? '无法识别的项目类型,必须是有效的 BMAD 插件'
|
|
100
170
|
: '项目不是有效的 AgentWindow BMAD 插件';
|
|
101
171
|
|
|
102
172
|
return {
|
|
@@ -106,21 +176,26 @@ export async function addInstance(name, projectPath, options = {}) {
|
|
|
106
176
|
};
|
|
107
177
|
}
|
|
108
178
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
179
|
+
// Only allow importing BMAD plugins via Add Instance
|
|
180
|
+
if (validation.instanceType !== 'bmad-plugin') {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: '只能通过"添加实例"功能导入 BMAD 插件。基础版实例请使用"Discover"功能导入。',
|
|
184
|
+
validation
|
|
185
|
+
};
|
|
186
|
+
}
|
|
114
187
|
|
|
115
|
-
//
|
|
116
|
-
|
|
188
|
+
// Determine config path for BMAD plugin
|
|
189
|
+
configPath = options.configPath || null;
|
|
190
|
+
|
|
191
|
+
newInstance = {
|
|
117
192
|
name,
|
|
118
193
|
displayName: options.displayName || name,
|
|
119
194
|
projectPath: validation.projectPath,
|
|
120
195
|
pluginPath: validation.pluginPath,
|
|
121
196
|
configPath,
|
|
122
197
|
botName: `bot-${name}`,
|
|
123
|
-
instanceType: validation.instanceType,
|
|
198
|
+
instanceType: validation.instanceType,
|
|
124
199
|
addedAt: new Date().toISOString(),
|
|
125
200
|
tags: options.tags || [],
|
|
126
201
|
enabled: true
|
|
@@ -256,6 +331,7 @@ export async function discoverInstances() {
|
|
|
256
331
|
let projectPath = null;
|
|
257
332
|
let configPath = null;
|
|
258
333
|
let pluginPath = null;
|
|
334
|
+
let hasValidConfig = false;
|
|
259
335
|
|
|
260
336
|
// Priority 1: Read CONFIG_PATH and extract PROJECT_DIR from config
|
|
261
337
|
if (proc.configPath && existsSync(proc.configPath)) {
|
|
@@ -271,6 +347,7 @@ export async function discoverInstances() {
|
|
|
271
347
|
// Fallback to config directory
|
|
272
348
|
projectPath = path.dirname(configPath);
|
|
273
349
|
}
|
|
350
|
+
hasValidConfig = true;
|
|
274
351
|
} catch {
|
|
275
352
|
// If config read fails, use directory name
|
|
276
353
|
projectPath = path.dirname(configPath);
|
|
@@ -296,17 +373,33 @@ export async function discoverInstances() {
|
|
|
296
373
|
// Fallback to cwd if available
|
|
297
374
|
projectPath = proc.cwd || null;
|
|
298
375
|
}
|
|
376
|
+
hasValidConfig = true;
|
|
299
377
|
} catch {
|
|
300
378
|
// If config read fails, use cwd
|
|
301
379
|
projectPath = proc.cwd || null;
|
|
302
380
|
}
|
|
303
381
|
}
|
|
304
382
|
}
|
|
305
|
-
// Priority 3: Use cwd
|
|
383
|
+
// Priority 3: Use cwd (but this is NOT a valid AgentWindow instance)
|
|
306
384
|
else if (proc.cwd) {
|
|
307
385
|
projectPath = proc.cwd;
|
|
308
386
|
}
|
|
309
387
|
|
|
388
|
+
// CRITICAL: Skip processes without a valid configuration
|
|
389
|
+
// A valid AgentWindow instance MUST have either:
|
|
390
|
+
// 1. A CONFIG_PATH env variable pointing to a valid config file, OR
|
|
391
|
+
// 2. An inferred config file that exists
|
|
392
|
+
if (!hasValidConfig || !configPath) {
|
|
393
|
+
console.debug(`[Discover] Skipping ${proc.name}: no valid config file (CONFIG_PATH not set, no inferred config found)`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Additional validation: project path must exist
|
|
398
|
+
if (!projectPath || !existsSync(projectPath)) {
|
|
399
|
+
console.debug(`[Discover] Skipping ${proc.name}: project path does not exist (${projectPath})`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
310
403
|
// Check if it's a BMAD project (has _bmad directory)
|
|
311
404
|
if (projectPath && existsSync(path.join(projectPath, '_bmad'))) {
|
|
312
405
|
pluginPath = path.join(projectPath, '_agent-bridge', 'src');
|
|
@@ -316,12 +409,18 @@ export async function discoverInstances() {
|
|
|
316
409
|
}
|
|
317
410
|
|
|
318
411
|
// Detect instance type based on project structure
|
|
319
|
-
//
|
|
412
|
+
// Business Logic:
|
|
413
|
+
// - simple-config: Auto-discovered, managed by AgentWindow
|
|
414
|
+
// - bmad-plugin: Discovered but needs user confirmation
|
|
320
415
|
let instanceType = 'unknown';
|
|
321
416
|
if (projectPath && existsSync(projectPath)) {
|
|
322
417
|
instanceType = detectInstanceType(projectPath);
|
|
323
418
|
}
|
|
324
419
|
|
|
420
|
+
// NOTE: We allow both types to be discovered
|
|
421
|
+
// - simple-config: Can be imported directly
|
|
422
|
+
// - bmad-plugin: Shown in discover list for user to review and import
|
|
423
|
+
|
|
325
424
|
discovered.push({
|
|
326
425
|
botName: proc.name,
|
|
327
426
|
name: instanceName,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { pm2 as platformPM2 } from '../platform/index.js';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* PM2 process info
|
|
@@ -162,21 +163,90 @@ export async function getStatus(name) {
|
|
|
162
163
|
if (!proc) {
|
|
163
164
|
return {
|
|
164
165
|
name,
|
|
165
|
-
status: '
|
|
166
|
-
exists: false
|
|
166
|
+
status: 'stopped',
|
|
167
|
+
exists: false,
|
|
168
|
+
health: {
|
|
169
|
+
pm2: false,
|
|
170
|
+
discord: false,
|
|
171
|
+
docker: false,
|
|
172
|
+
overall: 'stopped'
|
|
173
|
+
}
|
|
167
174
|
};
|
|
168
175
|
}
|
|
169
176
|
|
|
177
|
+
// Try to read health status file
|
|
178
|
+
let healthStatus = {
|
|
179
|
+
pm2: proc.status === 'online',
|
|
180
|
+
discord: false,
|
|
181
|
+
docker: false,
|
|
182
|
+
overall: 'stopped'
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
let containerName = null;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const { readFileSync } = await import('fs');
|
|
189
|
+
const { join } = await import('path');
|
|
190
|
+
const configPath = proc.configPath; // Use configPath from platform abstraction layer
|
|
191
|
+
|
|
192
|
+
if (configPath) {
|
|
193
|
+
// Read the config file to get PROJECT_DIR and container name
|
|
194
|
+
try {
|
|
195
|
+
const configContent = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
196
|
+
const projectDir = configContent.PROJECT_DIR;
|
|
197
|
+
containerName = configContent.workspace?.containerName || null;
|
|
198
|
+
|
|
199
|
+
if (projectDir) {
|
|
200
|
+
// Health file is in the project directory
|
|
201
|
+
const healthFile = join(projectDir, '.health-status.json');
|
|
202
|
+
|
|
203
|
+
if (existsSync(healthFile)) {
|
|
204
|
+
const healthData = JSON.parse(readFileSync(healthFile, 'utf-8'));
|
|
205
|
+
healthStatus = {
|
|
206
|
+
pm2: proc.status === 'online',
|
|
207
|
+
discord: healthData.discord || false,
|
|
208
|
+
docker: healthData.docker || false,
|
|
209
|
+
overall: healthData.docker ? 'healthy' : 'degraded'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.debug('[Health] Could not read project from config:', e.message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// If health file doesn't exist or can't be read, use PM2 status only
|
|
219
|
+
console.debug('[Health] Could not read health status file:', e.message);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If Docker status is still false and we have a container name, check directly
|
|
223
|
+
if (!healthStatus.docker && containerName && proc.status === 'online') {
|
|
224
|
+
try {
|
|
225
|
+
const { execSync } = await import('child_process');
|
|
226
|
+
const result = execSync(
|
|
227
|
+
`docker inspect -f '{{.State.Running}}' ${containerName} 2>/dev/null`,
|
|
228
|
+
{ encoding: 'utf-8' }
|
|
229
|
+
).trim();
|
|
230
|
+
healthStatus.docker = result === 'true';
|
|
231
|
+
healthStatus.overall = healthStatus.docker ? 'healthy' : 'degraded';
|
|
232
|
+
} catch (e) {
|
|
233
|
+
// Docker container not running or doesn't exist
|
|
234
|
+
healthStatus.docker = false;
|
|
235
|
+
healthStatus.overall = 'degraded';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
170
239
|
// platformPM2 returns simplified format with direct properties
|
|
171
240
|
return {
|
|
172
241
|
name: proc.name,
|
|
173
|
-
status: proc.status || '
|
|
242
|
+
status: proc.status || 'stopped',
|
|
174
243
|
exists: true,
|
|
175
244
|
pid: proc.pid,
|
|
176
245
|
uptime: proc.uptime || 0,
|
|
177
246
|
memory: proc.memory || 0,
|
|
178
247
|
cpu: proc.cpu || 0,
|
|
179
|
-
restarts: proc.restarts || 0
|
|
248
|
+
restarts: proc.restarts || 0,
|
|
249
|
+
health: healthStatus
|
|
180
250
|
};
|
|
181
251
|
}
|
|
182
252
|
|
|
@@ -194,8 +264,8 @@ export function formatStatus(status) {
|
|
|
194
264
|
online: '✓',
|
|
195
265
|
stopped: '○',
|
|
196
266
|
errored: '✗',
|
|
197
|
-
|
|
198
|
-
}[status.status] || '
|
|
267
|
+
restarting: '⟳'
|
|
268
|
+
}[status.status] || '○';
|
|
199
269
|
|
|
200
270
|
const memoryMB = Math.round(status.memory / 1024 / 1024);
|
|
201
271
|
const uptimeSec = Math.floor((Date.now() - status.uptime) / 1000);
|
|
@@ -19,18 +19,18 @@ import { existsSync } from 'fs';
|
|
|
19
19
|
export function detectInstanceType(projectPath) {
|
|
20
20
|
const normalizedPath = path.resolve(projectPath);
|
|
21
21
|
|
|
22
|
-
// Check if it has BMAD
|
|
23
|
-
const
|
|
22
|
+
// Check if it has BMAD plugin (_agent-bridge directory with bot.js)
|
|
23
|
+
const agentBridgePath = path.join(normalizedPath, '_agent-bridge');
|
|
24
|
+
const hasPlugin = existsSync(agentBridgePath) && existsSync(path.join(agentBridgePath, 'src', 'bot.js'));
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// BMAD plugin instance (has either _bmad or _agent-bridge)
|
|
29
|
-
if (hasBmad || hasPlugin) {
|
|
26
|
+
// BMAD plugin instance (has _agent-bridge with bot.js)
|
|
27
|
+
if (hasPlugin) {
|
|
30
28
|
return 'bmad-plugin';
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
// Check if it has config.json (simple config instance)
|
|
32
|
+
// This takes precedence over _bmad directory, as many BMAD projects
|
|
33
|
+
// are run as simple-config bots
|
|
34
34
|
const hasConfig = existsSync(path.join(normalizedPath, 'config.json'));
|
|
35
35
|
if (hasConfig) {
|
|
36
36
|
return 'simple-config';
|
|
@@ -129,6 +129,11 @@ async function start(scriptPath, options = {}) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Note: PM2 CLI doesn't support --max-restarts or --min-uptime as direct options
|
|
133
|
+
// These must be set via ecosystem config file or app configuration
|
|
134
|
+
// For now, we skip these to avoid errors
|
|
135
|
+
// Users can configure restart behavior in ecosystem.config.js
|
|
136
|
+
|
|
132
137
|
if (options.instances) {
|
|
133
138
|
args.push('-i', String(options.instances));
|
|
134
139
|
}
|