agent-window 1.0.0 → 1.0.2
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/README.md +21 -8
- package/README.zh-CN.md +151 -0
- package/bin/cli.js +45 -0
- package/docs/WEB_UI_GUIDE.md +249 -0
- package/package.json +15 -3
- package/scripts/test-platform.js +109 -0
- package/src/api/routes/index.js +25 -0
- package/src/api/routes/instances.js +252 -0
- package/src/api/routes/operations.js +118 -0
- package/src/api/routes/system.js +42 -0
- package/src/api/server.js +147 -0
- package/src/api/websocket/index.js +16 -0
- package/src/api/websocket/logs.js +127 -0
- package/src/cli/commands/add.js +80 -0
- package/src/cli/commands/config.js +192 -0
- package/src/cli/commands/index.js +89 -0
- package/src/cli/commands/info.js +94 -0
- package/src/cli/commands/list.js +72 -0
- package/src/cli/commands/logs.js +67 -0
- package/src/cli/commands/remove.js +97 -0
- package/src/cli/commands/restart.js +67 -0
- package/src/cli/commands/start.js +101 -0
- package/src/cli/commands/status.js +95 -0
- package/src/cli/commands/stop.js +53 -0
- package/src/cli/commands/ui.js +51 -0
- package/src/cli/index.js +110 -0
- package/src/core/config.js +5 -10
- package/src/core/instance/backup-manager.js +172 -0
- package/src/core/instance/config-manager.js +279 -0
- package/src/core/instance/index.js +62 -0
- package/src/core/instance/manager.js +220 -0
- package/src/core/instance/pm2-bridge.js +205 -0
- package/src/core/instance/validator.js +161 -0
- package/src/core/platform/detector.js +142 -0
- package/src/core/platform/docker-bridge.js +372 -0
- package/src/core/platform/index.js +27 -0
- package/src/core/platform/paths.js +112 -0
- package/src/core/platform/pm2-bridge.js +314 -0
- package/web/dist/assets/Dashboard-C1smB9Nj.js +1 -0
- package/web/dist/assets/Dashboard-ezbZMSpZ.css +1 -0
- package/web/dist/assets/InstanceDetail-CRPMV7rg.css +1 -0
- package/web/dist/assets/InstanceDetail-C_Ddtrog.js +3 -0
- package/web/dist/assets/Instances-CvnH8iDv.css +1 -0
- package/web/dist/assets/Instances-_u2__M83.js +1 -0
- package/web/dist/assets/Settings-CAu3R9RW.css +1 -0
- package/web/dist/assets/Settings-CIa9MX7m.js +1 -0
- package/web/dist/assets/_plugin-vue_export-helper-DlAUqK2U.js +1 -0
- package/web/dist/assets/element-plus-Jr6qTeY5.js +37 -0
- package/web/dist/assets/main-CalRvcyG.css +1 -0
- package/web/dist/assets/main-D3cdXAiV.js +7 -0
- package/web/dist/assets/vue-vendor-CGSlMM3Y.js +29 -0
- package/web/dist/index.html +16 -0
- package/SECURITY.md +0 -31
- package/docs/legacy/DEVELOPMENT.md +0 -174
- package/docs/legacy/HANDOVER.md +0 -149
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages configuration files for AgentWindow instances.
|
|
5
|
+
* Handles reading, writing, validation, and external modification detection.
|
|
6
|
+
* Uses platform abstraction layer for cross-platform compatibility.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { paths } from '../platform/index.js';
|
|
14
|
+
|
|
15
|
+
/** @private */
|
|
16
|
+
const BACKUP_DIR = path.join(paths.getAgentWindowHome(), 'backups', 'configs');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ensure backup directory exists
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
async function ensureBackupDir() {
|
|
23
|
+
if (!existsSync(BACKUP_DIR)) {
|
|
24
|
+
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculate file checksum for change detection
|
|
30
|
+
* @param {string} content - File content
|
|
31
|
+
* @returns {string} SHA256 hash
|
|
32
|
+
*/
|
|
33
|
+
function calculateChecksum(content) {
|
|
34
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read config file
|
|
39
|
+
* @param {string} configPath - Path to config file
|
|
40
|
+
* @returns {Promise<Object>} Config object
|
|
41
|
+
*/
|
|
42
|
+
export async function readConfig(configPath) {
|
|
43
|
+
if (!existsSync(configPath)) {
|
|
44
|
+
throw new Error(`配置文件不存在: ${configPath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write config file with backup
|
|
53
|
+
* @param {string} configPath - Path to config file
|
|
54
|
+
* @param {Object} config - Config object to write
|
|
55
|
+
* @param {Object} options - Options
|
|
56
|
+
* @param {boolean} options.backup - Create backup before writing
|
|
57
|
+
* @returns {Promise<Object>} Result with checksum
|
|
58
|
+
*/
|
|
59
|
+
export async function writeConfig(configPath, config, options = {}) {
|
|
60
|
+
const content = JSON.stringify(config, null, 2);
|
|
61
|
+
const checksum = calculateChecksum(content);
|
|
62
|
+
|
|
63
|
+
// Create backup if requested and file exists
|
|
64
|
+
if (options.backup !== false && existsSync(configPath)) {
|
|
65
|
+
await backupConfig(configPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Write config atomically
|
|
69
|
+
const tmpPath = `${configPath}.tmp`;
|
|
70
|
+
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
71
|
+
await fs.rename(tmpPath, configPath);
|
|
72
|
+
|
|
73
|
+
return { checksum };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Backup config file
|
|
78
|
+
* @param {string} configPath - Path to config file
|
|
79
|
+
* @returns {Promise<string>} Backup file path
|
|
80
|
+
*/
|
|
81
|
+
export async function backupConfig(configPath) {
|
|
82
|
+
await ensureBackupDir();
|
|
83
|
+
|
|
84
|
+
if (!existsSync(configPath)) {
|
|
85
|
+
throw new Error(`无法备份不存在的文件: ${configPath}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
89
|
+
const basename = path.basename(configPath, '.json');
|
|
90
|
+
const backupPath = path.join(BACKUP_DIR, `${basename}-${timestamp}.json`);
|
|
91
|
+
|
|
92
|
+
await fs.copyFile(configPath, backupPath);
|
|
93
|
+
return backupPath;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* List available backups for a config
|
|
98
|
+
* @param {string} instanceName - Instance name
|
|
99
|
+
* @returns {Promise<Array>} List of backup files with metadata
|
|
100
|
+
*/
|
|
101
|
+
export async function listBackups(instanceName) {
|
|
102
|
+
await ensureBackupDir();
|
|
103
|
+
|
|
104
|
+
if (!existsSync(BACKUP_DIR)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const files = await fs.readdir(BACKUP_DIR);
|
|
109
|
+
const pattern = new RegExp(`^${instanceName}-config-(\\d{4}-\\d{2}-\\d{2}T.+?)\\.json$`);
|
|
110
|
+
|
|
111
|
+
const backups = [];
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const match = file.match(pattern);
|
|
114
|
+
if (match) {
|
|
115
|
+
const filePath = path.join(BACKUP_DIR, file);
|
|
116
|
+
const stats = await fs.stat(filePath);
|
|
117
|
+
backups.push({
|
|
118
|
+
instance: instanceName,
|
|
119
|
+
file,
|
|
120
|
+
path: filePath,
|
|
121
|
+
createdAt: new Date(stats.mtime),
|
|
122
|
+
size: stats.size
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return backups.sort((a, b) => b.createdAt - a.createdAt);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Restore config from backup
|
|
132
|
+
* @param {string} backupPath - Path to backup file
|
|
133
|
+
* @param {string} targetPath - Target config path
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
export async function restoreBackup(backupPath, targetPath) {
|
|
137
|
+
if (!existsSync(backupPath)) {
|
|
138
|
+
throw new Error(`备份文件不存在: ${backupPath}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create backup of current before restoring
|
|
142
|
+
if (existsSync(targetPath)) {
|
|
143
|
+
await backupConfig(targetPath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await fs.copyFile(backupPath, targetPath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get a config value by key path
|
|
151
|
+
* @param {Object} config - Config object
|
|
152
|
+
* @param {string} keyPath - Dot-separated key path (e.g., 'workspace.containerName')
|
|
153
|
+
* @returns {*} Config value
|
|
154
|
+
*/
|
|
155
|
+
export function getConfigValue(config, keyPath) {
|
|
156
|
+
const keys = keyPath.split('.');
|
|
157
|
+
let value = config;
|
|
158
|
+
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
if (value && typeof value === 'object') {
|
|
161
|
+
value = value[key];
|
|
162
|
+
} else {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set a config value by key path
|
|
172
|
+
* @param {Object} config - Config object
|
|
173
|
+
* @param {string} keyPath - Dot-separated key path
|
|
174
|
+
* @param {*} value - Value to set
|
|
175
|
+
* @returns {Object} Modified config
|
|
176
|
+
*/
|
|
177
|
+
export function setConfigValue(config, keyPath, value) {
|
|
178
|
+
const keys = keyPath.split('.');
|
|
179
|
+
const result = { ...config };
|
|
180
|
+
let current = result;
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
183
|
+
const key = keys[i];
|
|
184
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
185
|
+
current[key] = {};
|
|
186
|
+
}
|
|
187
|
+
current = current[key];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
current[keys[keys.length - 1]] = value;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detect if config was modified externally
|
|
196
|
+
* @param {string} configPath - Path to config file
|
|
197
|
+
* @param {string} storedChecksum - Previously stored checksum
|
|
198
|
+
* @returns {Promise<boolean>} True if modified
|
|
199
|
+
*/
|
|
200
|
+
export async function detectExternalModification(configPath, storedChecksum) {
|
|
201
|
+
if (!existsSync(configPath)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
206
|
+
const currentChecksum = calculateChecksum(content);
|
|
207
|
+
|
|
208
|
+
return currentChecksum !== storedChecksum;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Config metadata tracker (stores checksums for external change detection)
|
|
213
|
+
* @private */
|
|
214
|
+
const METADATA_FILE = path.join(paths.getAgentWindowHome(), 'config-metadata.json');
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Read config metadata
|
|
218
|
+
* @private
|
|
219
|
+
* @returns {Promise<Object>}
|
|
220
|
+
*/
|
|
221
|
+
async function readMetadata() {
|
|
222
|
+
if (!existsSync(METADATA_FILE)) {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
const content = await fs.readFile(METADATA_FILE, 'utf-8');
|
|
226
|
+
return JSON.parse(content);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Write config metadata
|
|
231
|
+
* @private
|
|
232
|
+
* @param {Object} metadata
|
|
233
|
+
* @returns {Promise<void>}
|
|
234
|
+
*/
|
|
235
|
+
async function writeMetadata(metadata) {
|
|
236
|
+
const dir = path.dirname(METADATA_FILE);
|
|
237
|
+
if (!existsSync(dir)) {
|
|
238
|
+
await fs.mkdir(dir, { recursive: true });
|
|
239
|
+
}
|
|
240
|
+
await fs.writeFile(METADATA_FILE, JSON.stringify(metadata, null, 2));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update config metadata after write
|
|
245
|
+
* @param {string} instanceName - Instance name
|
|
246
|
+
* @param {string} checksum - Config checksum
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
export async function updateConfigMetadata(instanceName, checksum) {
|
|
250
|
+
const metadata = await readMetadata();
|
|
251
|
+
metadata[instanceName] = {
|
|
252
|
+
checksum,
|
|
253
|
+
updatedAt: new Date().toISOString()
|
|
254
|
+
};
|
|
255
|
+
await writeMetadata(metadata);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check for external config modifications
|
|
260
|
+
* @param {string} instanceName - Instance name
|
|
261
|
+
* @param {string} configPath - Path to config file
|
|
262
|
+
* @returns {Promise<Object>} Result with modified flag
|
|
263
|
+
*/
|
|
264
|
+
export async function checkExternalModification(instanceName, configPath) {
|
|
265
|
+
const metadata = await readMetadata();
|
|
266
|
+
const stored = metadata[instanceName];
|
|
267
|
+
|
|
268
|
+
if (!stored) {
|
|
269
|
+
return { modified: false, reason: 'no_metadata' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const modified = await detectExternalModification(configPath, stored.checksum);
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
modified,
|
|
276
|
+
checksum: stored.checksum,
|
|
277
|
+
updatedAt: stored.updatedAt
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance Management Module
|
|
3
|
+
*
|
|
4
|
+
* Unified exports for instance management functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Validator
|
|
8
|
+
export {
|
|
9
|
+
validateInstance,
|
|
10
|
+
formatValidationResult
|
|
11
|
+
} from './validator.js';
|
|
12
|
+
|
|
13
|
+
// Manager
|
|
14
|
+
export {
|
|
15
|
+
getInstancesFilePath,
|
|
16
|
+
readInstances,
|
|
17
|
+
writeInstances,
|
|
18
|
+
addInstance,
|
|
19
|
+
removeInstance,
|
|
20
|
+
getInstance,
|
|
21
|
+
listInstances,
|
|
22
|
+
updateInstance
|
|
23
|
+
} from './manager.js';
|
|
24
|
+
|
|
25
|
+
// Config Manager
|
|
26
|
+
export {
|
|
27
|
+
readConfig,
|
|
28
|
+
writeConfig,
|
|
29
|
+
backupConfig as backupConfigFile,
|
|
30
|
+
restoreBackup as restoreConfigBackup,
|
|
31
|
+
listBackups as listConfigBackups,
|
|
32
|
+
getConfigValue,
|
|
33
|
+
setConfigValue,
|
|
34
|
+
detectExternalModification,
|
|
35
|
+
checkExternalModification,
|
|
36
|
+
updateConfigMetadata
|
|
37
|
+
} from './config-manager.js';
|
|
38
|
+
|
|
39
|
+
// PM2 Bridge
|
|
40
|
+
export {
|
|
41
|
+
getProcesses,
|
|
42
|
+
getProcess,
|
|
43
|
+
getProcessesByPattern,
|
|
44
|
+
startProcess,
|
|
45
|
+
stopProcess,
|
|
46
|
+
restartProcess,
|
|
47
|
+
deleteProcess,
|
|
48
|
+
getLogs,
|
|
49
|
+
getStatus,
|
|
50
|
+
formatStatus
|
|
51
|
+
} from './pm2-bridge.js';
|
|
52
|
+
|
|
53
|
+
// Backup Manager
|
|
54
|
+
export {
|
|
55
|
+
getBackupDir,
|
|
56
|
+
ensureBackupDir,
|
|
57
|
+
createBackup,
|
|
58
|
+
listBackups,
|
|
59
|
+
restoreBackup,
|
|
60
|
+
pruneBackups,
|
|
61
|
+
getLatestBackup
|
|
62
|
+
} from './backup-manager.js';
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instance Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages AgentWindow BMAD plugin instances - add, list, remove, get info.
|
|
5
|
+
* Uses platform abstraction layer for cross-platform compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import { validateInstance } from './validator.js';
|
|
12
|
+
import { paths } from '../platform/index.js';
|
|
13
|
+
|
|
14
|
+
/** @private */
|
|
15
|
+
const INSTANCES_DIR = paths.getAgentWindowHome();
|
|
16
|
+
/** @private */
|
|
17
|
+
const INSTANCES_FILE = path.join(INSTANCES_DIR, 'instances.json');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get instances file path
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function getInstancesFilePath() {
|
|
24
|
+
return INSTANCES_FILE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ensure instances directory and file exist
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
async function ensureInstancesFile() {
|
|
32
|
+
if (!existsSync(INSTANCES_DIR)) {
|
|
33
|
+
await fs.mkdir(INSTANCES_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
if (!existsSync(INSTANCES_FILE)) {
|
|
36
|
+
await fs.writeFile(INSTANCES_FILE, JSON.stringify({
|
|
37
|
+
version: '1.0',
|
|
38
|
+
instances: []
|
|
39
|
+
}, null, 2));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read instances from file
|
|
45
|
+
* @returns {Promise<Object>} Instances data
|
|
46
|
+
*/
|
|
47
|
+
export async function readInstances() {
|
|
48
|
+
await ensureInstancesFile();
|
|
49
|
+
const content = await fs.readFile(INSTANCES_FILE, 'utf-8');
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Write instances to file
|
|
55
|
+
* @param {Object} data - Instances data
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
export async function writeInstances(data) {
|
|
59
|
+
await ensureInstancesFile();
|
|
60
|
+
await fs.writeFile(INSTANCES_FILE, JSON.stringify(data, null, 2));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add a new instance
|
|
65
|
+
* @param {string} name - Instance name (identifier)
|
|
66
|
+
* @param {string} projectPath - Path to BMAD project
|
|
67
|
+
* @param {Object} options - Additional options
|
|
68
|
+
* @param {string} options.displayName - Display name
|
|
69
|
+
* @param {string[]} options.tags - Tags for categorization
|
|
70
|
+
* @param {string} options.configPath - Path to config file (optional)
|
|
71
|
+
* @returns {Promise<Object>} Result with success and message
|
|
72
|
+
*/
|
|
73
|
+
export async function addInstance(name, projectPath, options = {}) {
|
|
74
|
+
// Validate input
|
|
75
|
+
if (!name || !name.match(/^[a-z0-9-]+$/)) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: '实例名称只能包含小写字母、数字和连字符'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if instance already exists
|
|
83
|
+
const data = await readInstances();
|
|
84
|
+
if (data.instances.find(i => i.name === name)) {
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: `实例 "${name}" 已存在`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Validate the instance
|
|
92
|
+
const validation = await validateInstance(projectPath);
|
|
93
|
+
if (!validation.valid) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: '验证失败:项目不是有效的 AgentWindow BMAD 插件',
|
|
97
|
+
validation
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Determine config path
|
|
102
|
+
const botsDir = paths.getBotsDir();
|
|
103
|
+
const defaultConfigPath = path.join(botsDir, name, 'config.json');
|
|
104
|
+
const configPath = options.configPath ||
|
|
105
|
+
(existsSync(defaultConfigPath) ? defaultConfigPath : null);
|
|
106
|
+
|
|
107
|
+
// Add instance
|
|
108
|
+
const newInstance = {
|
|
109
|
+
name,
|
|
110
|
+
displayName: options.displayName || name,
|
|
111
|
+
projectPath: validation.projectPath,
|
|
112
|
+
pluginPath: validation.pluginPath,
|
|
113
|
+
configPath,
|
|
114
|
+
botName: `bot-${name}`,
|
|
115
|
+
addedAt: new Date().toISOString(),
|
|
116
|
+
tags: options.tags || [],
|
|
117
|
+
enabled: true
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
data.instances.push(newInstance);
|
|
121
|
+
await writeInstances(data);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
instance: newInstance
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Remove an instance
|
|
131
|
+
* @param {string} name - Instance name
|
|
132
|
+
* @returns {Promise<Object>} Result with success and message
|
|
133
|
+
*/
|
|
134
|
+
export async function removeInstance(name) {
|
|
135
|
+
const data = await readInstances();
|
|
136
|
+
const index = data.instances.findIndex(i => i.name === name);
|
|
137
|
+
|
|
138
|
+
if (index === -1) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: `实例 "${name}" 不存在`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const removed = data.instances.splice(index, 1)[0];
|
|
146
|
+
await writeInstances(data);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
instance: removed
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get an instance by name
|
|
156
|
+
* @param {string} name - Instance name
|
|
157
|
+
* @returns {Promise<Object|null>} Instance or null
|
|
158
|
+
*/
|
|
159
|
+
export async function getInstance(name) {
|
|
160
|
+
const data = await readInstances();
|
|
161
|
+
return data.instances.find(i => i.name === name) || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* List all instances
|
|
166
|
+
* @param {Object} options - Filter options
|
|
167
|
+
* @param {boolean} options.enabledOnly - Only show enabled instances
|
|
168
|
+
* @param {string} options.tag - Filter by tag
|
|
169
|
+
* @returns {Promise<Array>} List of instances
|
|
170
|
+
*/
|
|
171
|
+
export async function listInstances(options = {}) {
|
|
172
|
+
const data = await readInstances();
|
|
173
|
+
let instances = data.instances;
|
|
174
|
+
|
|
175
|
+
if (options.enabledOnly) {
|
|
176
|
+
instances = instances.filter(i => i.enabled);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (options.tag) {
|
|
180
|
+
instances = instances.filter(i => i.tags?.includes(options.tag));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return instances;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Update an instance
|
|
188
|
+
* @param {string} name - Instance name
|
|
189
|
+
* @param {Object} updates - Fields to update
|
|
190
|
+
* @returns {Promise<Object>} Result with success and message
|
|
191
|
+
*/
|
|
192
|
+
export async function updateInstance(name, updates) {
|
|
193
|
+
const data = await readInstances();
|
|
194
|
+
const index = data.instances.findIndex(i => i.name === name);
|
|
195
|
+
|
|
196
|
+
if (index === -1) {
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
error: `实例 "${name}" 不存在`
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Allowed fields to update
|
|
204
|
+
const allowedFields = ['displayName', 'tags', 'enabled', 'configPath'];
|
|
205
|
+
const instance = data.instances[index];
|
|
206
|
+
|
|
207
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
208
|
+
if (allowedFields.includes(key)) {
|
|
209
|
+
instance[key] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
instance.updatedAt = new Date().toISOString();
|
|
214
|
+
await writeInstances(data);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
instance
|
|
219
|
+
};
|
|
220
|
+
}
|