agent-window 1.3.3 → 1.3.5
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/package.json
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
removeInstance,
|
|
17
17
|
updateInstance,
|
|
18
18
|
discoverInstances,
|
|
19
|
-
importInstance
|
|
19
|
+
importInstance,
|
|
20
|
+
importFromPath
|
|
20
21
|
} from '../../core/instance/manager.js';
|
|
21
22
|
import {
|
|
22
23
|
getStatus,
|
|
@@ -244,7 +245,10 @@ export async function registerInstanceRoutes(fastify) {
|
|
|
244
245
|
|
|
245
246
|
// Use botName for PM2 lookup, default to bot-{name}
|
|
246
247
|
const botName = instance.botName || `bot-${name}`;
|
|
247
|
-
const status = await getStatus(botName, {
|
|
248
|
+
const status = await getStatus(botName, {
|
|
249
|
+
instanceType: instance.instanceType,
|
|
250
|
+
configPath: instance.configPath
|
|
251
|
+
});
|
|
248
252
|
return { ...status, instanceName: name };
|
|
249
253
|
} catch (error) {
|
|
250
254
|
reply.code(500).send({
|
|
@@ -352,6 +356,54 @@ export async function registerInstanceRoutes(fastify) {
|
|
|
352
356
|
}
|
|
353
357
|
});
|
|
354
358
|
|
|
359
|
+
/**
|
|
360
|
+
* POST /api/instances/import-from-path
|
|
361
|
+
* Manually import a BMAD plugin by providing its path
|
|
362
|
+
*
|
|
363
|
+
* This is the second registration method for BMAD plugins:
|
|
364
|
+
* - User manually provides the project path
|
|
365
|
+
* - System validates it's a valid BMAD plugin
|
|
366
|
+
* - If valid, registers it in instances.json
|
|
367
|
+
*/
|
|
368
|
+
fastify.post('/api/instances/import-from-path', {
|
|
369
|
+
schema: {
|
|
370
|
+
description: 'Manually import a BMAD plugin by path',
|
|
371
|
+
body: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
required: ['name', 'projectPath'],
|
|
374
|
+
properties: {
|
|
375
|
+
name: { type: 'string' },
|
|
376
|
+
projectPath: { type: 'string' },
|
|
377
|
+
displayName: { type: 'string' },
|
|
378
|
+
tags: { type: 'array', items: { type: 'string' } }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}, async (request, reply) => {
|
|
383
|
+
try {
|
|
384
|
+
const { name, projectPath, displayName, tags } = request.body;
|
|
385
|
+
|
|
386
|
+
const result = await importFromPath(name, projectPath, {
|
|
387
|
+
displayName,
|
|
388
|
+
tags
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (!result.success) {
|
|
392
|
+
return reply.code(400).send({
|
|
393
|
+
error: result.error,
|
|
394
|
+
validation: result.validation
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
reply.code(201).send(result.instance);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
reply.code(500).send({
|
|
401
|
+
error: 'Failed to import from path',
|
|
402
|
+
message: error.message
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
355
407
|
/**
|
|
356
408
|
* GET /api/instances/:name/config
|
|
357
409
|
* Get instance configuration
|
|
@@ -246,9 +246,73 @@ export async function removeInstance(name) {
|
|
|
246
246
|
* @param {string} name - Instance name
|
|
247
247
|
* @returns {Promise<Object|null>} Instance or null
|
|
248
248
|
*/
|
|
249
|
+
/**
|
|
250
|
+
* Get a specific instance by name
|
|
251
|
+
* Also includes auto-discovered instances from PM2
|
|
252
|
+
*
|
|
253
|
+
* @param {string} name - Instance name
|
|
254
|
+
* @returns {Promise<Object|null>} Instance or null
|
|
255
|
+
*/
|
|
249
256
|
export async function getInstance(name) {
|
|
257
|
+
// First, try to find in registered instances
|
|
250
258
|
const data = await readInstances();
|
|
251
|
-
|
|
259
|
+
const registered = data.instances.find(i => i.name === name);
|
|
260
|
+
if (registered) {
|
|
261
|
+
return registered;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// If not found, check if it exists as an auto-discovered instance in PM2
|
|
265
|
+
// This allows status checks for simple-config instances that aren't permanently registered
|
|
266
|
+
try {
|
|
267
|
+
const processes = await getProcesses();
|
|
268
|
+
const botName = `bot-${name}`;
|
|
269
|
+
const proc = processes.find(p => p.name === botName);
|
|
270
|
+
|
|
271
|
+
if (proc) {
|
|
272
|
+
// Determine instance type
|
|
273
|
+
let projectPath = null;
|
|
274
|
+
let configPath = proc.configPath;
|
|
275
|
+
let instanceType = 'simple-config';
|
|
276
|
+
|
|
277
|
+
if (configPath && existsSync(configPath)) {
|
|
278
|
+
try {
|
|
279
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
280
|
+
const config = JSON.parse(configContent);
|
|
281
|
+
projectPath = config.PROJECT_DIR || path.dirname(configPath);
|
|
282
|
+
|
|
283
|
+
// Detect instance type
|
|
284
|
+
if (projectPath && existsSync(projectPath)) {
|
|
285
|
+
instanceType = detectInstanceType(projectPath);
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
projectPath = proc.cwd || null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Only return simple-config instances (BMAD plugins need explicit registration)
|
|
293
|
+
if (instanceType === 'simple-config') {
|
|
294
|
+
return {
|
|
295
|
+
name,
|
|
296
|
+
displayName: name.charAt(0).toUpperCase() + name.slice(1),
|
|
297
|
+
projectPath,
|
|
298
|
+
configPath,
|
|
299
|
+
pluginPath: null,
|
|
300
|
+
botName,
|
|
301
|
+
instanceType,
|
|
302
|
+
platform: 'discord',
|
|
303
|
+
aiProvider: null,
|
|
304
|
+
addedAt: new Date().toISOString(),
|
|
305
|
+
tags: ['auto-discovered'],
|
|
306
|
+
enabled: true,
|
|
307
|
+
_unregistered: true
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.debug('[Manager] Failed to look up auto-discovered instance:', error.message);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
252
316
|
}
|
|
253
317
|
|
|
254
318
|
/**
|
|
@@ -258,10 +322,95 @@ export async function getInstance(name) {
|
|
|
258
322
|
* @param {string} options.tag - Filter by tag
|
|
259
323
|
* @returns {Promise<Array>} List of instances
|
|
260
324
|
*/
|
|
325
|
+
/**
|
|
326
|
+
* List all instances
|
|
327
|
+
*
|
|
328
|
+
* For BMAD-plugin instances: returns only registered instances from instances.json
|
|
329
|
+
* For simple-config instances: automatically scans PM2 and includes all bot-* processes
|
|
330
|
+
*
|
|
331
|
+
* This ensures simple-config instances (managed by AgentWindow) appear automatically
|
|
332
|
+
* while BMAD-plugin instances require explicit registration.
|
|
333
|
+
*
|
|
334
|
+
* @param {Object} options - Options
|
|
335
|
+
* @param {boolean} options.enabledOnly - Filter to enabled instances only
|
|
336
|
+
* @param {string} options.tag - Filter by tag
|
|
337
|
+
* @param {boolean} options.includeUnregistered - Include PM2 processes not in instances.json
|
|
338
|
+
* @returns {Promise<Array>} List of instances
|
|
339
|
+
*/
|
|
261
340
|
export async function listInstances(options = {}) {
|
|
262
341
|
const data = await readInstances();
|
|
263
|
-
|
|
342
|
+
const registeredNames = new Set(data.instances.map(i => i.botName || `bot-${i.name}`));
|
|
343
|
+
|
|
344
|
+
// Start with registered instances
|
|
345
|
+
let instances = [...data.instances];
|
|
346
|
+
|
|
347
|
+
// Auto-discover simple-config instances from PM2 (unless explicitly disabled)
|
|
348
|
+
// Simple-config instances should appear automatically as they are managed by AgentWindow
|
|
349
|
+
if (options.includeUnregistered !== false) {
|
|
350
|
+
try {
|
|
351
|
+
const processes = await getProcesses();
|
|
352
|
+
const botProcesses = processes.filter(p => p.name && p.name.startsWith('bot-'));
|
|
353
|
+
|
|
354
|
+
for (const proc of botProcesses) {
|
|
355
|
+
// Skip if already registered
|
|
356
|
+
if (registeredNames.has(proc.name)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Extract instance name from bot-name
|
|
361
|
+
const instanceName = proc.name.replace(/^bot-/, '');
|
|
362
|
+
|
|
363
|
+
// Determine project path and config path from PM2 process
|
|
364
|
+
let projectPath = null;
|
|
365
|
+
let configPath = null;
|
|
366
|
+
let hasValidConfig = false;
|
|
367
|
+
|
|
368
|
+
// Get config path from PM2 environment
|
|
369
|
+
if (proc.configPath && existsSync(proc.configPath)) {
|
|
370
|
+
configPath = proc.configPath;
|
|
371
|
+
try {
|
|
372
|
+
const configContent = await fs.readFile(proc.configPath, 'utf8');
|
|
373
|
+
const config = JSON.parse(configContent);
|
|
374
|
+
projectPath = config.PROJECT_DIR || path.dirname(configPath);
|
|
375
|
+
hasValidConfig = true;
|
|
376
|
+
} catch {
|
|
377
|
+
projectPath = proc.cwd || null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Detect instance type - if it has _agent-bridge, it's a BMAD plugin
|
|
382
|
+
// BMAD plugins should NOT be auto-added (require explicit import)
|
|
383
|
+
if (projectPath && existsSync(projectPath)) {
|
|
384
|
+
const detectedType = detectInstanceType(projectPath);
|
|
385
|
+
|
|
386
|
+
// Only auto-add simple-config instances, skip BMAD plugins
|
|
387
|
+
if (detectedType === 'simple-config' && hasValidConfig) {
|
|
388
|
+
instances.push({
|
|
389
|
+
name: instanceName,
|
|
390
|
+
displayName: instanceName.charAt(0).toUpperCase() + instanceName.slice(1),
|
|
391
|
+
projectPath,
|
|
392
|
+
configPath,
|
|
393
|
+
pluginPath: null,
|
|
394
|
+
botName: proc.name,
|
|
395
|
+
instanceType: 'simple-config',
|
|
396
|
+
platform: 'discord',
|
|
397
|
+
aiProvider: null,
|
|
398
|
+
addedAt: new Date().toISOString(),
|
|
399
|
+
tags: ['auto-discovered'],
|
|
400
|
+
enabled: true,
|
|
401
|
+
// Mark as unregistered so frontend knows it's not permanently saved
|
|
402
|
+
_unregistered: true
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (error) {
|
|
408
|
+
// If PM2 scan fails, just return registered instances
|
|
409
|
+
console.debug('[Manager] PM2 scan failed, returning registered instances only:', error.message);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
264
412
|
|
|
413
|
+
// Apply filters
|
|
265
414
|
if (options.enabledOnly) {
|
|
266
415
|
instances = instances.filter(i => i.enabled);
|
|
267
416
|
}
|
|
@@ -416,16 +565,18 @@ export async function discoverInstances() {
|
|
|
416
565
|
|
|
417
566
|
// Detect instance type based on project structure
|
|
418
567
|
// Business Logic:
|
|
419
|
-
// - simple-config: Auto-discovered
|
|
420
|
-
// - bmad-plugin:
|
|
568
|
+
// - simple-config: Auto-discovered in listInstances() - NOT in discover
|
|
569
|
+
// - bmad-plugin: Only shown here for user to import
|
|
421
570
|
let instanceType = 'unknown';
|
|
422
571
|
if (projectPath && existsSync(projectPath)) {
|
|
423
572
|
instanceType = detectInstanceType(projectPath);
|
|
424
573
|
}
|
|
425
574
|
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
|
|
575
|
+
// IMPORTANT: Only return bmad-plugin instances in discover
|
|
576
|
+
// simple-config instances are auto-shown in listInstances()
|
|
577
|
+
if (instanceType !== 'bmad-plugin') {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
429
580
|
|
|
430
581
|
discovered.push({
|
|
431
582
|
botName: proc.name,
|
|
@@ -434,7 +585,7 @@ export async function discoverInstances() {
|
|
|
434
585
|
projectPath,
|
|
435
586
|
configPath,
|
|
436
587
|
pluginPath,
|
|
437
|
-
instanceType
|
|
588
|
+
instanceType: 'bmad-plugin',
|
|
438
589
|
status: proc.status || 'unknown',
|
|
439
590
|
pid: proc.pid,
|
|
440
591
|
memory: proc.memory || 0,
|
|
@@ -451,6 +602,91 @@ export async function discoverInstances() {
|
|
|
451
602
|
return discovered;
|
|
452
603
|
}
|
|
453
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Import a BMAD plugin by manually providing its path
|
|
607
|
+
*
|
|
608
|
+
* This is the second registration method for BMAD plugins.
|
|
609
|
+
* Unlike discover+import, this allows users to directly specify a path.
|
|
610
|
+
*
|
|
611
|
+
* @param {string} name - Instance name
|
|
612
|
+
* @param {string} projectPath - Path to the BMAD project
|
|
613
|
+
* @param {Object} options - Additional options
|
|
614
|
+
* @param {string} options.displayName - Display name
|
|
615
|
+
* @param {string[]} options.tags - Tags
|
|
616
|
+
* @returns {Promise<Object>} Result with success and instance
|
|
617
|
+
*/
|
|
618
|
+
export async function importFromPath(name, projectPath, options = {}) {
|
|
619
|
+
// Validate name format
|
|
620
|
+
if (!name || !name.match(/^[a-z0-9-]+$/)) {
|
|
621
|
+
return {
|
|
622
|
+
success: false,
|
|
623
|
+
error: '实例名称只能包含小写字母、数字和连字符'
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check if instance already exists
|
|
628
|
+
const data = await readInstances();
|
|
629
|
+
if (data.instances.find(i => i.name === name)) {
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
error: `实例 "${name}" 已存在`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Validate project path exists
|
|
637
|
+
if (!existsSync(projectPath)) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: '项目路径不存在'
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Validate it's a valid BMAD plugin
|
|
645
|
+
const validation = await validateInstance(projectPath);
|
|
646
|
+
if (!validation.valid) {
|
|
647
|
+
return {
|
|
648
|
+
success: false,
|
|
649
|
+
error: '不是有效的 BMAD 插件:' + (validation.error || '验证失败'),
|
|
650
|
+
validation
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Only BMAD plugins can be imported via manual path
|
|
655
|
+
if (validation.instanceType !== 'bmad-plugin') {
|
|
656
|
+
return {
|
|
657
|
+
success: false,
|
|
658
|
+
error: '此功能仅支持导入 BMAD 插件。基础版实例请使用 "Discover" 功能。',
|
|
659
|
+
validation
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Determine config path for BMAD plugin
|
|
664
|
+
const agentBridgeConfigPath = path.join(validation.projectPath, '_agent-bridge', 'config', 'config.json');
|
|
665
|
+
|
|
666
|
+
const newInstance = {
|
|
667
|
+
name,
|
|
668
|
+
displayName: options.displayName || name,
|
|
669
|
+
projectPath: validation.projectPath,
|
|
670
|
+
pluginPath: validation.pluginPath,
|
|
671
|
+
configPath: agentBridgeConfigPath,
|
|
672
|
+
botName: `bot-${name}`,
|
|
673
|
+
instanceType: 'bmad-plugin',
|
|
674
|
+
platform: 'discord',
|
|
675
|
+
aiProvider: null,
|
|
676
|
+
addedAt: new Date().toISOString(),
|
|
677
|
+
tags: [...(options.tags || []), 'manually-imported'],
|
|
678
|
+
enabled: true
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
data.instances.push(newInstance);
|
|
682
|
+
await writeInstances(data);
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
instance: newInstance
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
454
690
|
/**
|
|
455
691
|
* Import a discovered instance into instances.json
|
|
456
692
|
* @param {Object} discovered - Discovered instance object
|
|
@@ -157,10 +157,11 @@ export async function getLogs(name, options = {}) {
|
|
|
157
157
|
* @param {string} name - Process name
|
|
158
158
|
* @param {Object} options - Options
|
|
159
159
|
* @param {string} options.instanceType - Instance type for type-aware checks
|
|
160
|
+
* @param {string} options.configPath - Config path for Docker container name
|
|
160
161
|
* @returns {Promise<Object>} Status info
|
|
161
162
|
*/
|
|
162
163
|
export async function getStatus(name, options = {}) {
|
|
163
|
-
const { instanceType } = options;
|
|
164
|
+
const { instanceType, configPath } = options;
|
|
164
165
|
const proc = await getProcess(name);
|
|
165
166
|
|
|
166
167
|
if (!proc) {
|
|
@@ -189,12 +190,13 @@ export async function getStatus(name, options = {}) {
|
|
|
189
190
|
const procStatus = proc.status || 'stopped';
|
|
190
191
|
|
|
191
192
|
// Get container name from config for Docker check
|
|
193
|
+
// Priority: options.configPath > proc.configPath
|
|
192
194
|
try {
|
|
193
195
|
const { readFileSync } = await import('fs');
|
|
194
|
-
const
|
|
196
|
+
const effectiveConfigPath = configPath || proc.configPath;
|
|
195
197
|
|
|
196
|
-
if (
|
|
197
|
-
const configContent = JSON.parse(readFileSync(
|
|
198
|
+
if (effectiveConfigPath) {
|
|
199
|
+
const configContent = JSON.parse(readFileSync(effectiveConfigPath, 'utf-8'));
|
|
198
200
|
containerName = configContent.workspace?.containerName || null;
|
|
199
201
|
}
|
|
200
202
|
} catch (e) {
|
|
@@ -224,12 +226,14 @@ export async function getStatus(name, options = {}) {
|
|
|
224
226
|
if (containerName && procStatus === 'online') {
|
|
225
227
|
try {
|
|
226
228
|
const { execSync } = await import('child_process');
|
|
229
|
+
// Use shorter timeout (2s) to avoid blocking status check
|
|
227
230
|
const result = execSync(
|
|
228
231
|
`docker inspect -f '{{.State.Running}}' ${containerName} 2>/dev/null`,
|
|
229
|
-
{ encoding: 'utf-8', timeout:
|
|
232
|
+
{ encoding: 'utf-8', timeout: 2000 }
|
|
230
233
|
).trim();
|
|
231
234
|
dockerRunning = result === 'true';
|
|
232
235
|
} catch (e) {
|
|
236
|
+
// Docker inspect failed (container not running or Docker daemon issue)
|
|
233
237
|
dockerRunning = false;
|
|
234
238
|
}
|
|
235
239
|
}
|