u-foo 1.2.16 → 1.4.0
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/modules/online/README.md +18 -0
- package/package.json +2 -1
- package/src/agent/cliRunner.js +1 -1
- package/src/agent/launcher.js +23 -4
- package/src/agent/ptyRunner.js +39 -16
- package/src/agent/ufooAgent.js +2 -1
- package/src/assistant/agent.js +2 -1
- package/src/assistant/bridge.js +9 -3
- package/src/assistant/constants.js +15 -0
- package/src/assistant/engine.js +7 -2
- package/src/assistant/ufooEngineCli.js +9 -3
- package/src/chat/commandExecutor.js +188 -13
- package/src/chat/commands.js +11 -0
- package/src/chat/daemonMessageRouter.js +107 -0
- package/src/cli/groupCoreCommands.js +246 -0
- package/src/cli/onlineCoreCommands.js +8 -0
- package/src/cli.js +325 -2
- package/src/daemon/groupOrchestrator.js +557 -0
- package/src/daemon/index.js +319 -1
- package/src/daemon/status.js +48 -0
- package/src/group/diagram.js +222 -0
- package/src/group/templates.js +280 -0
- package/src/group/validateTemplate.js +234 -0
- package/src/online/server.js +320 -28
- package/src/shared/eventContract.js +5 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/dev-basic.json +78 -0
- package/templates/groups/research-quick.json +49 -0
package/src/daemon/index.js
CHANGED
|
@@ -14,6 +14,8 @@ const { getUfooPaths } = require("../ufoo/paths");
|
|
|
14
14
|
const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
|
|
15
15
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
16
16
|
const { createDaemonCronController } = require("./cronOps");
|
|
17
|
+
const { createGroupOrchestrator } = require("./groupOrchestrator");
|
|
18
|
+
const { normalizeFormat, renderGroupDiagramFromTemplate, renderGroupDiagramFromRuntime } = require("../group/diagram");
|
|
17
19
|
const { runAssistantTask } = require("../assistant/bridge");
|
|
18
20
|
const { runPromptWithAssistant } = require("./promptLoop");
|
|
19
21
|
const { handlePromptRequest } = require("./promptRequest");
|
|
@@ -22,6 +24,7 @@ const { recordAgentReport } = require("./reporting");
|
|
|
22
24
|
let providerSessions = null;
|
|
23
25
|
let probeHandles = new Map();
|
|
24
26
|
let daemonCronController = null;
|
|
27
|
+
let daemonGroupOrchestrator = null;
|
|
25
28
|
|
|
26
29
|
function sleep(ms) {
|
|
27
30
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -314,6 +317,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
314
317
|
nickname: nickname || undefined,
|
|
315
318
|
agent_id: existing,
|
|
316
319
|
skipped: true,
|
|
320
|
+
cleaned: Boolean(cleaned),
|
|
317
321
|
message: `Agent '${nickname}' already exists`,
|
|
318
322
|
});
|
|
319
323
|
continue;
|
|
@@ -354,7 +358,8 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
354
358
|
ok: true,
|
|
355
359
|
agent,
|
|
356
360
|
count,
|
|
357
|
-
nickname: nickname || undefined
|
|
361
|
+
nickname: nickname || undefined,
|
|
362
|
+
subscriber_ids: Array.isArray(launchResult.subscriberIds) ? launchResult.subscriberIds.slice() : [],
|
|
358
363
|
});
|
|
359
364
|
if (nickname) {
|
|
360
365
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -680,6 +685,11 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
680
685
|
},
|
|
681
686
|
log,
|
|
682
687
|
});
|
|
688
|
+
daemonGroupOrchestrator = createGroupOrchestrator({
|
|
689
|
+
projectRoot,
|
|
690
|
+
handleOps,
|
|
691
|
+
processManager,
|
|
692
|
+
});
|
|
683
693
|
|
|
684
694
|
const buildRuntimeStatus = () =>
|
|
685
695
|
buildStatus(projectRoot, {
|
|
@@ -992,6 +1002,313 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
992
1002
|
type: IPC_RESPONSE_TYPES.ERROR,
|
|
993
1003
|
error: err.message || "launch_agent failed",
|
|
994
1004
|
})}
|
|
1005
|
+
`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (req.type === IPC_REQUEST_TYPES.LAUNCH_GROUP) {
|
|
1011
|
+
if (!daemonGroupOrchestrator) {
|
|
1012
|
+
socket.write(
|
|
1013
|
+
`${JSON.stringify({
|
|
1014
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1015
|
+
error: "group orchestrator unavailable",
|
|
1016
|
+
})}
|
|
1017
|
+
`,
|
|
1018
|
+
);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const alias = req.alias || req.template || "";
|
|
1022
|
+
const instance = req.instance || req.group_id || "";
|
|
1023
|
+
const dryRun = req.dry_run === true || req.dryRun === true;
|
|
1024
|
+
try {
|
|
1025
|
+
const result = await daemonGroupOrchestrator.runGroup({
|
|
1026
|
+
alias,
|
|
1027
|
+
instance,
|
|
1028
|
+
dry_run: dryRun,
|
|
1029
|
+
});
|
|
1030
|
+
const ok = result && result.ok !== false;
|
|
1031
|
+
let reply = "";
|
|
1032
|
+
if (!ok) {
|
|
1033
|
+
reply = `Group run failed: ${result?.error || "unknown error"}`;
|
|
1034
|
+
} else if (result.dry_run) {
|
|
1035
|
+
reply = `Group dry-run ${result.group_id}: ${Array.isArray(result.members) ? result.members.length : 0} member(s)`;
|
|
1036
|
+
} else {
|
|
1037
|
+
reply = `Group started ${result.group_id}`;
|
|
1038
|
+
}
|
|
1039
|
+
socket.write(
|
|
1040
|
+
`${JSON.stringify({
|
|
1041
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1042
|
+
data: {
|
|
1043
|
+
reply,
|
|
1044
|
+
group: result,
|
|
1045
|
+
},
|
|
1046
|
+
})}
|
|
1047
|
+
`,
|
|
1048
|
+
);
|
|
1049
|
+
if (!dryRun) {
|
|
1050
|
+
cleanupInactiveSubscribers();
|
|
1051
|
+
ipcServer.sendToSockets({
|
|
1052
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1053
|
+
data: buildRuntimeStatus(),
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
socket.write(
|
|
1058
|
+
`${JSON.stringify({
|
|
1059
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1060
|
+
error: err.message || "launch_group failed",
|
|
1061
|
+
})}
|
|
1062
|
+
`,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (req.type === IPC_REQUEST_TYPES.STOP_GROUP) {
|
|
1068
|
+
if (!daemonGroupOrchestrator) {
|
|
1069
|
+
socket.write(
|
|
1070
|
+
`${JSON.stringify({
|
|
1071
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1072
|
+
error: "group orchestrator unavailable",
|
|
1073
|
+
})}
|
|
1074
|
+
`,
|
|
1075
|
+
);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const groupId = req.group_id || req.groupId || req.instance || "";
|
|
1079
|
+
try {
|
|
1080
|
+
const result = await daemonGroupOrchestrator.stopGroup({ group_id: groupId });
|
|
1081
|
+
const ok = result && result.ok !== false;
|
|
1082
|
+
const reply = ok
|
|
1083
|
+
? `Stopped group ${result.group_id}`
|
|
1084
|
+
: `Group stop failed: ${result?.error || "unknown error"}`;
|
|
1085
|
+
socket.write(
|
|
1086
|
+
`${JSON.stringify({
|
|
1087
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1088
|
+
data: {
|
|
1089
|
+
reply,
|
|
1090
|
+
group: result,
|
|
1091
|
+
},
|
|
1092
|
+
})}
|
|
1093
|
+
`,
|
|
1094
|
+
);
|
|
1095
|
+
cleanupInactiveSubscribers();
|
|
1096
|
+
ipcServer.sendToSockets({
|
|
1097
|
+
type: IPC_RESPONSE_TYPES.STATUS,
|
|
1098
|
+
data: buildRuntimeStatus(),
|
|
1099
|
+
});
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
socket.write(
|
|
1102
|
+
`${JSON.stringify({
|
|
1103
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1104
|
+
error: err.message || "stop_group failed",
|
|
1105
|
+
})}
|
|
1106
|
+
`,
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (req.type === IPC_REQUEST_TYPES.GROUP_STATUS) {
|
|
1112
|
+
if (!daemonGroupOrchestrator) {
|
|
1113
|
+
socket.write(
|
|
1114
|
+
`${JSON.stringify({
|
|
1115
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1116
|
+
error: "group orchestrator unavailable",
|
|
1117
|
+
})}
|
|
1118
|
+
`,
|
|
1119
|
+
);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const groupId = req.group_id || req.groupId || req.instance || "";
|
|
1123
|
+
try {
|
|
1124
|
+
const result = daemonGroupOrchestrator.getStatus({ group_id: groupId });
|
|
1125
|
+
const ok = result && result.ok !== false;
|
|
1126
|
+
const reply = ok
|
|
1127
|
+
? (groupId
|
|
1128
|
+
? `Group ${groupId}: ${result.group?.status || "unknown"}`
|
|
1129
|
+
: `Group instances: ${result.count || 0}`)
|
|
1130
|
+
: `Group status failed: ${result?.error || "unknown error"}`;
|
|
1131
|
+
socket.write(
|
|
1132
|
+
`${JSON.stringify({
|
|
1133
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1134
|
+
data: {
|
|
1135
|
+
reply,
|
|
1136
|
+
group: result,
|
|
1137
|
+
},
|
|
1138
|
+
})}
|
|
1139
|
+
`,
|
|
1140
|
+
);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
socket.write(
|
|
1143
|
+
`${JSON.stringify({
|
|
1144
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1145
|
+
error: err.message || "group_status failed",
|
|
1146
|
+
})}
|
|
1147
|
+
`,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (req.type === IPC_REQUEST_TYPES.GROUP_TEMPLATE_VALIDATE) {
|
|
1153
|
+
if (!daemonGroupOrchestrator) {
|
|
1154
|
+
socket.write(
|
|
1155
|
+
`${JSON.stringify({
|
|
1156
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1157
|
+
error: "group orchestrator unavailable",
|
|
1158
|
+
})}
|
|
1159
|
+
`,
|
|
1160
|
+
);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const target = req.alias || req.path || req.target || "";
|
|
1164
|
+
try {
|
|
1165
|
+
const result = daemonGroupOrchestrator.validateTemplateTarget(target);
|
|
1166
|
+
const reply = result.ok
|
|
1167
|
+
? `Template valid: ${result.entry?.alias || target}`
|
|
1168
|
+
: `Template invalid: ${result.error || "unknown error"}`;
|
|
1169
|
+
socket.write(
|
|
1170
|
+
`${JSON.stringify({
|
|
1171
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1172
|
+
data: {
|
|
1173
|
+
reply,
|
|
1174
|
+
group: {
|
|
1175
|
+
ok: result.ok,
|
|
1176
|
+
target,
|
|
1177
|
+
alias: result.entry?.alias || "",
|
|
1178
|
+
source: result.entry?.source || "",
|
|
1179
|
+
filePath: result.entry?.filePath || "",
|
|
1180
|
+
errors: result.errors || [],
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
})}
|
|
1184
|
+
`,
|
|
1185
|
+
);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
socket.write(
|
|
1188
|
+
`${JSON.stringify({
|
|
1189
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1190
|
+
error: err.message || "group_template_validate failed",
|
|
1191
|
+
})}
|
|
1192
|
+
`,
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (req.type === IPC_REQUEST_TYPES.GROUP_DIAGRAM) {
|
|
1198
|
+
if (!daemonGroupOrchestrator) {
|
|
1199
|
+
socket.write(
|
|
1200
|
+
`${JSON.stringify({
|
|
1201
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1202
|
+
error: "group orchestrator unavailable",
|
|
1203
|
+
})}
|
|
1204
|
+
`,
|
|
1205
|
+
);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const target = req.group_id || req.groupId || req.instance || req.alias || req.target || "";
|
|
1209
|
+
if (!target) {
|
|
1210
|
+
socket.write(
|
|
1211
|
+
`${JSON.stringify({
|
|
1212
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1213
|
+
error: "group diagram requires alias|group_id",
|
|
1214
|
+
})}
|
|
1215
|
+
`,
|
|
1216
|
+
);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const format = normalizeFormat(req.format || (req.mermaid ? "mermaid" : "ascii"));
|
|
1220
|
+
try {
|
|
1221
|
+
const runtimeState = daemonGroupOrchestrator.getStatus({ group_id: target });
|
|
1222
|
+
if (runtimeState && runtimeState.ok === false && runtimeState.error === "invalid group_id") {
|
|
1223
|
+
socket.write(
|
|
1224
|
+
`${JSON.stringify({
|
|
1225
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1226
|
+
data: {
|
|
1227
|
+
reply: "Group diagram failed: invalid group_id",
|
|
1228
|
+
group: {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
mode: "runtime",
|
|
1231
|
+
target,
|
|
1232
|
+
format,
|
|
1233
|
+
error: "invalid group_id",
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
})}
|
|
1237
|
+
`,
|
|
1238
|
+
);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (runtimeState && runtimeState.ok && runtimeState.group) {
|
|
1242
|
+
const diagram = renderGroupDiagramFromRuntime(runtimeState.group, { format });
|
|
1243
|
+
socket.write(
|
|
1244
|
+
`${JSON.stringify({
|
|
1245
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1246
|
+
data: {
|
|
1247
|
+
reply: `Group diagram (${format}) for runtime ${target}`,
|
|
1248
|
+
group: {
|
|
1249
|
+
ok: true,
|
|
1250
|
+
mode: "runtime",
|
|
1251
|
+
target,
|
|
1252
|
+
format,
|
|
1253
|
+
diagram,
|
|
1254
|
+
group_id: runtimeState.group.group_id || target,
|
|
1255
|
+
status: runtimeState.group.status || "",
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
})}
|
|
1259
|
+
`,
|
|
1260
|
+
);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
const templateState = daemonGroupOrchestrator.validateTemplateTarget(target, { allowPath: false });
|
|
1265
|
+
if (!templateState || !templateState.ok || !templateState.entry) {
|
|
1266
|
+
socket.write(
|
|
1267
|
+
`${JSON.stringify({
|
|
1268
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1269
|
+
data: {
|
|
1270
|
+
reply: `Group diagram failed: ${templateState?.error || "template not found"}`,
|
|
1271
|
+
group: {
|
|
1272
|
+
ok: false,
|
|
1273
|
+
mode: "template",
|
|
1274
|
+
target,
|
|
1275
|
+
format,
|
|
1276
|
+
error: templateState?.error || "template not found",
|
|
1277
|
+
errors: templateState?.errors || [],
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
})}
|
|
1281
|
+
`,
|
|
1282
|
+
);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const diagram = renderGroupDiagramFromTemplate(templateState.entry.data, { format });
|
|
1287
|
+
socket.write(
|
|
1288
|
+
`${JSON.stringify({
|
|
1289
|
+
type: IPC_RESPONSE_TYPES.RESPONSE,
|
|
1290
|
+
data: {
|
|
1291
|
+
reply: `Group diagram (${format}) for template ${templateState.entry.alias || target}`,
|
|
1292
|
+
group: {
|
|
1293
|
+
ok: true,
|
|
1294
|
+
mode: "template",
|
|
1295
|
+
target,
|
|
1296
|
+
format,
|
|
1297
|
+
diagram,
|
|
1298
|
+
alias: templateState.entry.alias || "",
|
|
1299
|
+
source: templateState.entry.source || "",
|
|
1300
|
+
filePath: templateState.entry.filePath || "",
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
})}
|
|
1304
|
+
`,
|
|
1305
|
+
);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
socket.write(
|
|
1308
|
+
`${JSON.stringify({
|
|
1309
|
+
type: IPC_RESPONSE_TYPES.ERROR,
|
|
1310
|
+
error: err.message || "group_diagram failed",
|
|
1311
|
+
})}
|
|
995
1312
|
`,
|
|
996
1313
|
);
|
|
997
1314
|
}
|
|
@@ -1309,6 +1626,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1309
1626
|
daemonCronController.stopAll();
|
|
1310
1627
|
daemonCronController = null;
|
|
1311
1628
|
}
|
|
1629
|
+
daemonGroupOrchestrator = null;
|
|
1312
1630
|
|
|
1313
1631
|
// 清理所有子进程
|
|
1314
1632
|
processManager.cleanup();
|
package/src/daemon/status.js
CHANGED
|
@@ -79,11 +79,58 @@ function normalizeCronTasks(raw = []) {
|
|
|
79
79
|
}));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function readGroups(projectRoot) {
|
|
83
|
+
const groupsDir = getUfooPaths(projectRoot).groupsDir;
|
|
84
|
+
if (!groupsDir || !fs.existsSync(groupsDir)) {
|
|
85
|
+
return { count: 0, active: 0, failed: 0, stopped: 0, items: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const items = [];
|
|
89
|
+
try {
|
|
90
|
+
const files = fs.readdirSync(groupsDir)
|
|
91
|
+
.filter((name) => name.endsWith(".json"))
|
|
92
|
+
.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const filePath = path.join(groupsDir, file);
|
|
95
|
+
try {
|
|
96
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
97
|
+
const members = Array.isArray(raw.members) ? raw.members : [];
|
|
98
|
+
const membersActive = members.filter((item) => item && (item.status === "active" || item.status === "reused")).length;
|
|
99
|
+
items.push({
|
|
100
|
+
group_id: String(raw.group_id || path.basename(file, ".json")),
|
|
101
|
+
status: String(raw.status || "unknown"),
|
|
102
|
+
template_alias: String(raw.template_alias || ""),
|
|
103
|
+
updated_at: String(raw.updated_at || ""),
|
|
104
|
+
members_total: members.length,
|
|
105
|
+
members_active: membersActive,
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore malformed group state
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
return { count: 0, active: 0, failed: 0, stopped: 0, items: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
items.sort((a, b) => String(b.updated_at || "").localeCompare(String(a.updated_at || "")));
|
|
116
|
+
const active = items.filter((item) => item.status === "active").length;
|
|
117
|
+
const failed = items.filter((item) => item.status === "failed").length;
|
|
118
|
+
const stopped = items.filter((item) => item.status === "stopped").length;
|
|
119
|
+
return {
|
|
120
|
+
count: items.length,
|
|
121
|
+
active,
|
|
122
|
+
failed,
|
|
123
|
+
stopped,
|
|
124
|
+
items,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
82
128
|
function buildStatus(projectRoot, options = {}) {
|
|
83
129
|
const bus = readBus(projectRoot);
|
|
84
130
|
const decisions = readDecisions(projectRoot);
|
|
85
131
|
const unread = readUnread(projectRoot);
|
|
86
132
|
const reports = readReportSummary(projectRoot);
|
|
133
|
+
const groups = readGroups(projectRoot);
|
|
87
134
|
const subscribers = bus ? Object.keys(bus.agents || {}) : [];
|
|
88
135
|
const cronTasks = normalizeCronTasks(options.cronTasks || []);
|
|
89
136
|
|
|
@@ -115,6 +162,7 @@ function buildStatus(projectRoot, options = {}) {
|
|
|
115
162
|
count: cronTasks.length,
|
|
116
163
|
tasks: cronTasks,
|
|
117
164
|
},
|
|
165
|
+
groups,
|
|
118
166
|
};
|
|
119
167
|
}
|
|
120
168
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function asTrimmedString(value) {
|
|
4
|
+
if (typeof value !== "string") return "";
|
|
5
|
+
return value.trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeFormat(format = "") {
|
|
9
|
+
const value = asTrimmedString(format).toLowerCase();
|
|
10
|
+
return value === "mermaid" ? "mermaid" : "ascii";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toMemberNickname(member = {}, fallback = "") {
|
|
14
|
+
return asTrimmedString(member.nickname) || asTrimmedString(member.id) || fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function collectTemplateMembers(templateDoc = {}) {
|
|
18
|
+
const agents = Array.isArray(templateDoc.agents) ? templateDoc.agents : [];
|
|
19
|
+
return agents
|
|
20
|
+
.map((agent, index) => ({
|
|
21
|
+
nickname: toMemberNickname(agent, `agent_${index + 1}`),
|
|
22
|
+
id: asTrimmedString(agent.id),
|
|
23
|
+
type: asTrimmedString(agent.type),
|
|
24
|
+
startup_order: Number.isInteger(agent.startup_order) ? agent.startup_order : null,
|
|
25
|
+
depends_on: Array.isArray(agent.depends_on)
|
|
26
|
+
? agent.depends_on.map((dep) => asTrimmedString(dep)).filter(Boolean)
|
|
27
|
+
: [],
|
|
28
|
+
status: asTrimmedString(agent.status),
|
|
29
|
+
subscriber_id: asTrimmedString(agent.subscriber_id),
|
|
30
|
+
}))
|
|
31
|
+
.filter((item) => item.nickname);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function collectRuntimeMembers(runtime = {}) {
|
|
35
|
+
const members = Array.isArray(runtime.members) ? runtime.members : [];
|
|
36
|
+
return members
|
|
37
|
+
.map((member, index) => ({
|
|
38
|
+
nickname: toMemberNickname(member, `agent_${index + 1}`),
|
|
39
|
+
id: asTrimmedString(member.template_agent_id) || asTrimmedString(member.id),
|
|
40
|
+
type: asTrimmedString(member.type),
|
|
41
|
+
startup_order: Number.isInteger(member.startup_order) ? member.startup_order : null,
|
|
42
|
+
depends_on: Array.isArray(member.depends_on)
|
|
43
|
+
? member.depends_on.map((dep) => asTrimmedString(dep)).filter(Boolean)
|
|
44
|
+
: [],
|
|
45
|
+
status: asTrimmedString(member.status),
|
|
46
|
+
subscriber_id: asTrimmedString(member.subscriber_id),
|
|
47
|
+
}))
|
|
48
|
+
.filter((item) => item.nickname);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collectTemplateEdges(templateDoc = {}, members = []) {
|
|
52
|
+
const known = new Set(members.map((member) => member.nickname));
|
|
53
|
+
const edges = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
|
|
56
|
+
function addEdge(from, to, kind) {
|
|
57
|
+
const source = asTrimmedString(from);
|
|
58
|
+
const target = asTrimmedString(to);
|
|
59
|
+
if (!source || !target) return;
|
|
60
|
+
if (!known.has(source) || !known.has(target)) return;
|
|
61
|
+
const edgeKind = asTrimmedString(kind);
|
|
62
|
+
const key = `${source}->${target}:${edgeKind}`;
|
|
63
|
+
if (seen.has(key)) return;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
edges.push({ from: source, to: target, kind: edgeKind });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const rawEdges = Array.isArray(templateDoc.edges) ? templateDoc.edges : [];
|
|
69
|
+
rawEdges.forEach((edge) => {
|
|
70
|
+
if (!edge || typeof edge !== "object") return;
|
|
71
|
+
addEdge(edge.from, edge.to, edge.kind);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
members.forEach((member) => {
|
|
75
|
+
member.depends_on.forEach((dep) => addEdge(dep, member.nickname, "depends_on"));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return edges;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function collectRuntimeEdges(members = []) {
|
|
82
|
+
const known = new Set(members.map((member) => member.nickname));
|
|
83
|
+
const edges = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
members.forEach((member) => {
|
|
86
|
+
member.depends_on.forEach((dep) => {
|
|
87
|
+
if (!known.has(dep)) return;
|
|
88
|
+
const key = `${dep}->${member.nickname}`;
|
|
89
|
+
if (seen.has(key)) return;
|
|
90
|
+
seen.add(key);
|
|
91
|
+
edges.push({ from: dep, to: member.nickname, kind: "depends_on" });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
return edges;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatMemberLine(member = {}) {
|
|
98
|
+
const type = member.type || "unknown";
|
|
99
|
+
const order = Number.isInteger(member.startup_order) ? member.startup_order : "-";
|
|
100
|
+
const deps = Array.isArray(member.depends_on) && member.depends_on.length > 0
|
|
101
|
+
? member.depends_on.join(", ")
|
|
102
|
+
: "-";
|
|
103
|
+
const status = member.status ? ` status=${member.status}` : "";
|
|
104
|
+
const subscriber = member.subscriber_id ? ` sub=${member.subscriber_id}` : "";
|
|
105
|
+
return `- ${member.nickname} [${type}] order=${order} deps=${deps}${status}${subscriber}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function renderAsciiDiagram(metadata = {}, members = [], edges = []) {
|
|
109
|
+
const mode = metadata.mode || "template";
|
|
110
|
+
const name = metadata.name || "unknown";
|
|
111
|
+
const lines = [`Group Diagram (${mode}: ${name})`];
|
|
112
|
+
if (metadata.status) {
|
|
113
|
+
lines.push(`Status: ${metadata.status}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push(`Members (${members.length}):`);
|
|
116
|
+
members.forEach((member) => {
|
|
117
|
+
lines.push(formatMemberLine(member));
|
|
118
|
+
});
|
|
119
|
+
if (edges.length === 0) {
|
|
120
|
+
lines.push("Edges: none");
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(`Edges (${edges.length}):`);
|
|
123
|
+
edges.forEach((edge) => {
|
|
124
|
+
const suffix = edge.kind ? ` (${edge.kind})` : "";
|
|
125
|
+
lines.push(`- ${edge.from} -> ${edge.to}${suffix}`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeMermaidId(value = "", fallback = "node") {
|
|
132
|
+
const normalized = String(value || "")
|
|
133
|
+
.replace(/[^A-Za-z0-9_]/g, "_")
|
|
134
|
+
.replace(/^[^A-Za-z_]+/, "");
|
|
135
|
+
return normalized || fallback;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function escapeMermaidLabel(value = "") {
|
|
139
|
+
return String(value || "")
|
|
140
|
+
.replace(/"/g, "\\\"")
|
|
141
|
+
.replace(/\n/g, "\\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderMermaidDiagram(members = [], edges = []) {
|
|
145
|
+
const lines = ["flowchart LR"];
|
|
146
|
+
const nodeIdMap = new Map();
|
|
147
|
+
const taken = new Set();
|
|
148
|
+
|
|
149
|
+
members.forEach((member, index) => {
|
|
150
|
+
const base = normalizeMermaidId(member.nickname, `node_${index + 1}`);
|
|
151
|
+
let id = base;
|
|
152
|
+
let suffix = 1;
|
|
153
|
+
while (taken.has(id)) {
|
|
154
|
+
id = `${base}_${suffix}`;
|
|
155
|
+
suffix += 1;
|
|
156
|
+
}
|
|
157
|
+
taken.add(id);
|
|
158
|
+
nodeIdMap.set(member.nickname, id);
|
|
159
|
+
const labelParts = [member.nickname];
|
|
160
|
+
if (member.type) labelParts.push(member.type);
|
|
161
|
+
if (member.status) labelParts.push(member.status);
|
|
162
|
+
lines.push(` ${id}["${escapeMermaidLabel(labelParts.join("\n"))}"]`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
edges.forEach((edge) => {
|
|
166
|
+
const fromId = nodeIdMap.get(edge.from);
|
|
167
|
+
const toId = nodeIdMap.get(edge.to);
|
|
168
|
+
if (!fromId || !toId) return;
|
|
169
|
+
const edgeLabel = edge.kind ? `|${escapeMermaidLabel(edge.kind)}|` : "";
|
|
170
|
+
lines.push(` ${fromId} -->${edgeLabel} ${toId}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveTemplateName(templateDoc = {}, fallback = "") {
|
|
177
|
+
const templateMeta = templateDoc && templateDoc.template && typeof templateDoc.template === "object"
|
|
178
|
+
? templateDoc.template
|
|
179
|
+
: {};
|
|
180
|
+
return asTrimmedString(templateMeta.alias) || asTrimmedString(templateMeta.id) || asTrimmedString(templateMeta.name) || fallback;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderGroupDiagramFromTemplate(templateDoc = {}, options = {}) {
|
|
184
|
+
const format = normalizeFormat(options.format);
|
|
185
|
+
const members = collectTemplateMembers(templateDoc);
|
|
186
|
+
const edges = collectTemplateEdges(templateDoc, members);
|
|
187
|
+
if (format === "mermaid") {
|
|
188
|
+
return renderMermaidDiagram(members, edges);
|
|
189
|
+
}
|
|
190
|
+
return renderAsciiDiagram(
|
|
191
|
+
{
|
|
192
|
+
mode: "template",
|
|
193
|
+
name: resolveTemplateName(templateDoc, "template"),
|
|
194
|
+
},
|
|
195
|
+
members,
|
|
196
|
+
edges
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderGroupDiagramFromRuntime(runtime = {}, options = {}) {
|
|
201
|
+
const format = normalizeFormat(options.format);
|
|
202
|
+
const members = collectRuntimeMembers(runtime);
|
|
203
|
+
const edges = collectRuntimeEdges(members);
|
|
204
|
+
if (format === "mermaid") {
|
|
205
|
+
return renderMermaidDiagram(members, edges);
|
|
206
|
+
}
|
|
207
|
+
return renderAsciiDiagram(
|
|
208
|
+
{
|
|
209
|
+
mode: "runtime",
|
|
210
|
+
name: asTrimmedString(runtime.group_id) || "runtime",
|
|
211
|
+
status: asTrimmedString(runtime.status),
|
|
212
|
+
},
|
|
213
|
+
members,
|
|
214
|
+
edges
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
normalizeFormat,
|
|
220
|
+
renderGroupDiagramFromTemplate,
|
|
221
|
+
renderGroupDiagramFromRuntime,
|
|
222
|
+
};
|