u-foo 1.0.3 → 1.0.6
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 +67 -8
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +117 -0
- package/SKILLS/uinit/SKILL.md +73 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo.js +13 -0
- package/modules/AGENTS.template.md +15 -7
- package/modules/bus/README.md +28 -23
- package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +61 -1
- package/package.json +16 -4
- package/scripts/.archived/bash-to-js-migration/README.md +46 -0
- package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
- package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
- package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
- package/scripts/banner.sh +2 -89
- package/scripts/postinstall.js +59 -0
- package/src/agent/cliRunner.js +33 -5
- package/src/agent/internalRunner.js +78 -51
- package/src/agent/launcher.js +702 -0
- package/src/agent/notifier.js +200 -0
- package/src/agent/ptyRunner.js +377 -0
- package/src/agent/ptyWrapper.js +354 -0
- package/src/agent/readyDetector.js +159 -0
- package/src/agent/ufooAgent.js +37 -42
- package/src/bus/API_DESIGN.md +204 -0
- package/src/bus/activate.js +156 -0
- package/src/bus/daemon.js +308 -0
- package/src/bus/index.js +785 -0
- package/src/bus/inject.js +285 -0
- package/src/bus/message.js +302 -0
- package/src/bus/nickname.js +86 -0
- package/src/bus/queue.js +131 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/subscriber.js +296 -0
- package/src/bus/utils.js +357 -0
- package/src/chat/index.js +1842 -249
- package/src/cli.js +658 -95
- package/src/config.js +9 -2
- package/src/context/decisions.js +314 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +38 -0
- package/src/daemon/index.js +749 -94
- package/src/daemon/ops.js +395 -51
- package/src/daemon/providerSessions.js +291 -0
- package/src/daemon/run.js +34 -1
- package/src/daemon/status.js +24 -7
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +264 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +252 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +41 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +73 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
- /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
- /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
- /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
- /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
- /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
- /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
- /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
- /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
- /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
- /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
package/src/bus/index.js
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const {
|
|
5
|
+
getTimestamp,
|
|
6
|
+
ensureDir,
|
|
7
|
+
logInfo,
|
|
8
|
+
logOk,
|
|
9
|
+
logWarn,
|
|
10
|
+
logError,
|
|
11
|
+
colors,
|
|
12
|
+
generateInstanceId,
|
|
13
|
+
subscriberToSafeName,
|
|
14
|
+
isPidAlive,
|
|
15
|
+
truncateFile,
|
|
16
|
+
getCurrentTty,
|
|
17
|
+
} = require("./utils");
|
|
18
|
+
const { shakeTerminalByTty } = require("./shake");
|
|
19
|
+
const QueueManager = require("./queue");
|
|
20
|
+
const SubscriberManager = require("./subscriber");
|
|
21
|
+
const MessageManager = require("./message");
|
|
22
|
+
const NicknameManager = require("./nickname");
|
|
23
|
+
const BusDaemon = require("./daemon");
|
|
24
|
+
const Injector = require("./inject");
|
|
25
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
26
|
+
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Event Bus - 项目级 Agent 事件总线
|
|
30
|
+
*/
|
|
31
|
+
class EventBus {
|
|
32
|
+
constructor(projectRoot) {
|
|
33
|
+
this.projectRoot = projectRoot;
|
|
34
|
+
this.paths = getUfooPaths(projectRoot);
|
|
35
|
+
this.busDir = this.paths.busDir;
|
|
36
|
+
this.agentsFile = this.paths.agentsFile;
|
|
37
|
+
this.eventsDir = this.paths.busEventsDir;
|
|
38
|
+
this.logsDir = this.paths.busLogsDir;
|
|
39
|
+
|
|
40
|
+
this.busData = null;
|
|
41
|
+
this.queueManager = null;
|
|
42
|
+
this.subscriberManager = null;
|
|
43
|
+
this.messageManager = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 确保 bus 已初始化
|
|
48
|
+
*/
|
|
49
|
+
ensureBus() {
|
|
50
|
+
if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Event bus not initialized. Please run: ufoo bus init or /uinit"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 加载 bus 数据
|
|
59
|
+
*/
|
|
60
|
+
loadBusData() {
|
|
61
|
+
this.busData = loadAgentsData(this.agentsFile);
|
|
62
|
+
|
|
63
|
+
this.queueManager = new QueueManager(this.busDir);
|
|
64
|
+
this.subscriberManager = new SubscriberManager(
|
|
65
|
+
this.busData,
|
|
66
|
+
this.queueManager
|
|
67
|
+
);
|
|
68
|
+
this.messageManager = new MessageManager(
|
|
69
|
+
this.busDir,
|
|
70
|
+
this.busData,
|
|
71
|
+
this.queueManager
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// 自动清理不活跃的 agents
|
|
75
|
+
this.subscriberManager.cleanupInactive();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 保存 bus 数据
|
|
80
|
+
*/
|
|
81
|
+
saveBusData() {
|
|
82
|
+
if (this.busData) {
|
|
83
|
+
saveAgentsData(this.agentsFile, this.busData);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 获取当前订阅者 ID
|
|
89
|
+
*/
|
|
90
|
+
getCurrentSubscriber() {
|
|
91
|
+
// 优先使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
|
|
92
|
+
if (process.env.UFOO_SUBSCRIBER_ID) {
|
|
93
|
+
return process.env.UFOO_SUBSCRIBER_ID;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!fs.existsSync(this.agentsFile)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 尝试从 session.txt 读取
|
|
101
|
+
const sessionFile = path.join(this.paths.agentDir, "session.txt");
|
|
102
|
+
if (fs.existsSync(sessionFile)) {
|
|
103
|
+
const sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
104
|
+
if (sessionId) {
|
|
105
|
+
return sessionId;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 尝试通过 tty 查找订阅者
|
|
110
|
+
let currentTty = null;
|
|
111
|
+
try {
|
|
112
|
+
const ttyPath = fs.realpathSync("/dev/tty");
|
|
113
|
+
if (ttyPath && ttyPath.startsWith("/dev/")) {
|
|
114
|
+
currentTty = ttyPath;
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// tty 不可用
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (currentTty && this.busData && this.busData.agents) {
|
|
121
|
+
for (const [id, meta] of Object.entries(this.busData.agents)) {
|
|
122
|
+
if (meta.tty === currentTty) {
|
|
123
|
+
return id;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 初始化事件总线
|
|
133
|
+
*/
|
|
134
|
+
async init() {
|
|
135
|
+
// 创建目录结构
|
|
136
|
+
ensureDir(this.busDir);
|
|
137
|
+
ensureDir(this.paths.agentDir);
|
|
138
|
+
ensureDir(this.eventsDir);
|
|
139
|
+
ensureDir(path.join(this.busDir, "queues"));
|
|
140
|
+
ensureDir(this.logsDir);
|
|
141
|
+
ensureDir(path.join(this.busDir, "offsets"));
|
|
142
|
+
ensureDir(this.paths.busDaemonDir);
|
|
143
|
+
ensureDir(this.paths.busDaemonCountsDir);
|
|
144
|
+
|
|
145
|
+
// 创建初始 agents 文件(如不存在)
|
|
146
|
+
if (!fs.existsSync(this.agentsFile)) {
|
|
147
|
+
const busData = {
|
|
148
|
+
created_at: getTimestamp(),
|
|
149
|
+
agents: {},
|
|
150
|
+
};
|
|
151
|
+
saveAgentsData(this.agentsFile, busData);
|
|
152
|
+
}
|
|
153
|
+
logOk("Event bus initialized");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 加入总线
|
|
158
|
+
*/
|
|
159
|
+
async join(sessionId, agentType, nickname = null) {
|
|
160
|
+
this.ensureBus();
|
|
161
|
+
this.loadBusData();
|
|
162
|
+
|
|
163
|
+
// 自动检测 session ID 和 agent type
|
|
164
|
+
if (!sessionId) {
|
|
165
|
+
sessionId = generateInstanceId();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!agentType) {
|
|
169
|
+
// 默认为 claude-code(手动启动情况)
|
|
170
|
+
agentType = "claude-code";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await this.subscriberManager.join(
|
|
174
|
+
sessionId,
|
|
175
|
+
agentType,
|
|
176
|
+
nickname
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
this.saveBusData();
|
|
180
|
+
|
|
181
|
+
logOk(
|
|
182
|
+
`Joined event bus: ${result.subscriber}${result.nickname ? ` (${result.nickname})` : ""}`
|
|
183
|
+
);
|
|
184
|
+
return result.subscriber;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 离开总线
|
|
189
|
+
*/
|
|
190
|
+
async leave(subscriber) {
|
|
191
|
+
this.ensureBus();
|
|
192
|
+
this.loadBusData();
|
|
193
|
+
|
|
194
|
+
const success = await this.subscriberManager.leave(subscriber);
|
|
195
|
+
|
|
196
|
+
if (success) {
|
|
197
|
+
this.saveBusData();
|
|
198
|
+
logOk(`Left event bus: ${subscriber}`);
|
|
199
|
+
} else {
|
|
200
|
+
logError(`Subscriber not found: ${subscriber}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return success;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 重命名订阅者
|
|
208
|
+
*/
|
|
209
|
+
async rename(subscriber, newNickname, publisher = null) {
|
|
210
|
+
this.ensureBus();
|
|
211
|
+
this.loadBusData();
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const result = await this.subscriberManager.rename(
|
|
215
|
+
subscriber,
|
|
216
|
+
newNickname
|
|
217
|
+
);
|
|
218
|
+
this.saveBusData();
|
|
219
|
+
const pub = publisher || this.getDefaultPublisher() || "unknown";
|
|
220
|
+
try {
|
|
221
|
+
await this.messageManager.emit(
|
|
222
|
+
"*",
|
|
223
|
+
"agent_renamed",
|
|
224
|
+
{
|
|
225
|
+
agent_id: result.subscriber,
|
|
226
|
+
old_nickname: result.oldNickname,
|
|
227
|
+
new_nickname: result.newNickname,
|
|
228
|
+
},
|
|
229
|
+
pub
|
|
230
|
+
);
|
|
231
|
+
} catch {
|
|
232
|
+
// ignore event emit failures
|
|
233
|
+
}
|
|
234
|
+
logOk(
|
|
235
|
+
`Renamed ${result.subscriber}: "${result.oldNickname}" -> "${result.newNickname}"`
|
|
236
|
+
);
|
|
237
|
+
return result;
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logError(err.message);
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 获取当前订阅者 ID
|
|
246
|
+
*/
|
|
247
|
+
async whoami() {
|
|
248
|
+
this.ensureBus();
|
|
249
|
+
this.loadBusData();
|
|
250
|
+
|
|
251
|
+
// 优先使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
|
|
252
|
+
if (process.env.UFOO_SUBSCRIBER_ID) {
|
|
253
|
+
const subscriber = process.env.UFOO_SUBSCRIBER_ID;
|
|
254
|
+
const meta = this.subscriberManager.getSubscriber(subscriber);
|
|
255
|
+
|
|
256
|
+
if (meta) {
|
|
257
|
+
console.log(subscriber);
|
|
258
|
+
return subscriber;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
logError("Not joined to bus. Please run: ufoo bus join");
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 发送消息
|
|
268
|
+
*/
|
|
269
|
+
async send(target, message, publisher = null) {
|
|
270
|
+
this.ensureBus();
|
|
271
|
+
this.loadBusData();
|
|
272
|
+
|
|
273
|
+
// 自动检测 publisher
|
|
274
|
+
if (!publisher) {
|
|
275
|
+
publisher =
|
|
276
|
+
process.env.AI_BUS_PUBLISHER ||
|
|
277
|
+
this.getDefaultPublisher() ||
|
|
278
|
+
this.getCurrentSubscriber() ||
|
|
279
|
+
"unknown";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 如果 publisher 还是 unknown,尝试从命令行参数或环境推断
|
|
283
|
+
if (publisher === "unknown") {
|
|
284
|
+
// 尝试从 tty 查找可能的 subscriber
|
|
285
|
+
const possibleSubscriber = this.getCurrentSubscriber();
|
|
286
|
+
if (possibleSubscriber) {
|
|
287
|
+
publisher = possibleSubscriber;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 如果 publisher 不在 agents 列表中,自动注册它(懒加载模式)
|
|
292
|
+
if (publisher !== "unknown" && this.busData.agents && !this.busData.agents[publisher]) {
|
|
293
|
+
// 解析 agent 信息
|
|
294
|
+
const parts = publisher.split(":");
|
|
295
|
+
const agentType = parts[0] || "unknown-agent";
|
|
296
|
+
const sessionId = parts[1] || require("./utils").generateInstanceId();
|
|
297
|
+
|
|
298
|
+
// 自动加入总线(静默模式,不输出日志)
|
|
299
|
+
const subscriber = await this.subscriberManager.join(sessionId, agentType, null);
|
|
300
|
+
this.saveBusData();
|
|
301
|
+
publisher = subscriber; // 使用规范化的 subscriber ID
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 更新 publisher 的心跳
|
|
305
|
+
if (publisher !== "unknown" && this.busData.agents && this.busData.agents[publisher]) {
|
|
306
|
+
this.subscriberManager.updateLastSeen(publisher);
|
|
307
|
+
this.saveBusData();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const result = await this.messageManager.send(target, message, publisher);
|
|
312
|
+
logOk(
|
|
313
|
+
`Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
|
|
314
|
+
);
|
|
315
|
+
return result;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
logError(err.message);
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 广播消息
|
|
324
|
+
*/
|
|
325
|
+
async broadcast(message, publisher = null) {
|
|
326
|
+
return this.send("*", message, publisher);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 检查待处理消息
|
|
331
|
+
*/
|
|
332
|
+
async check(subscriber, autoAck = false) {
|
|
333
|
+
this.ensureBus();
|
|
334
|
+
this.loadBusData();
|
|
335
|
+
|
|
336
|
+
// 更新心跳
|
|
337
|
+
this.subscriberManager.updateLastSeen(subscriber);
|
|
338
|
+
this.saveBusData();
|
|
339
|
+
|
|
340
|
+
const pending = await this.messageManager.check(subscriber);
|
|
341
|
+
|
|
342
|
+
if (pending.length === 0) {
|
|
343
|
+
logOk("No pending messages");
|
|
344
|
+
return pending;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
logWarn(`You have ${pending.length} pending event(s):`);
|
|
348
|
+
console.log();
|
|
349
|
+
|
|
350
|
+
for (const event of pending) {
|
|
351
|
+
console.log(` ${colors.yellow}@you${colors.reset} from ${colors.cyan}${event.publisher}${colors.reset}`);
|
|
352
|
+
console.log(` Type: ${event.type}/${event.event}`);
|
|
353
|
+
console.log(` Content: ${JSON.stringify(event.data)}`);
|
|
354
|
+
console.log();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log(`${colors.cyan}After handling, run: ufoo bus ack ${subscriber}${colors.reset}`);
|
|
358
|
+
|
|
359
|
+
if (autoAck) {
|
|
360
|
+
await this.ack(subscriber);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return pending;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 确认消息
|
|
368
|
+
*/
|
|
369
|
+
async ack(subscriber) {
|
|
370
|
+
this.ensureBus();
|
|
371
|
+
this.loadBusData();
|
|
372
|
+
|
|
373
|
+
const count = await this.messageManager.ack(subscriber);
|
|
374
|
+
|
|
375
|
+
if (count > 0) {
|
|
376
|
+
logOk(`Acknowledged and cleared ${count} message(s)`);
|
|
377
|
+
} else {
|
|
378
|
+
logOk("No pending messages to acknowledge");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return count;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* 消费事件
|
|
386
|
+
*/
|
|
387
|
+
async consume(subscriber, fromBeginning = false) {
|
|
388
|
+
this.ensureBus();
|
|
389
|
+
this.loadBusData();
|
|
390
|
+
|
|
391
|
+
const result = await this.messageManager.consume(subscriber, fromBeginning);
|
|
392
|
+
|
|
393
|
+
for (const event of result.consumed) {
|
|
394
|
+
console.log(JSON.stringify(event));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
logInfo(`Consumed ${result.consumed.length} events, new offset: ${result.newOffset}`);
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 查看总线状态
|
|
404
|
+
*/
|
|
405
|
+
async status() {
|
|
406
|
+
this.ensureBus();
|
|
407
|
+
this.loadBusData();
|
|
408
|
+
|
|
409
|
+
// 清理不活跃的订阅者
|
|
410
|
+
this.subscriberManager.cleanupInactive();
|
|
411
|
+
|
|
412
|
+
// 尝试获取当前 subscriber 并更新 last_seen + 重新激活(保持心跳)
|
|
413
|
+
const currentSubscriber = this.getCurrentSubscriber();
|
|
414
|
+
if (currentSubscriber && this.busData.agents && this.busData.agents[currentSubscriber]) {
|
|
415
|
+
this.subscriberManager.updateLastSeen(currentSubscriber);
|
|
416
|
+
this.busData.agents[currentSubscriber].status = "active";
|
|
417
|
+
this.saveBusData();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
console.log(`${colors.cyan}=== Event Bus Status ===${colors.reset}`);
|
|
421
|
+
console.log();
|
|
422
|
+
|
|
423
|
+
// 显示 bus ID
|
|
424
|
+
const busId = path.basename(this.projectRoot) || "ai-workspace";
|
|
425
|
+
console.log(`Bus ID: ${busId}`);
|
|
426
|
+
console.log();
|
|
427
|
+
|
|
428
|
+
// 显示在线订阅者
|
|
429
|
+
const active = this.subscriberManager.getActiveSubscribers();
|
|
430
|
+
console.log(`${colors.cyan}Online agents:${colors.reset}`);
|
|
431
|
+
if (active.length === 0) {
|
|
432
|
+
console.log(" (none)");
|
|
433
|
+
} else {
|
|
434
|
+
for (const sub of active) {
|
|
435
|
+
const nickname = sub.nickname ? ` (${sub.nickname})` : "";
|
|
436
|
+
console.log(` ${sub.id}${nickname}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
console.log();
|
|
440
|
+
|
|
441
|
+
// 显示事件统计
|
|
442
|
+
console.log(`${colors.cyan}Event statistics:${colors.reset}`);
|
|
443
|
+
if (fs.existsSync(this.eventsDir)) {
|
|
444
|
+
const files = fs.readdirSync(this.eventsDir)
|
|
445
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
446
|
+
.sort();
|
|
447
|
+
|
|
448
|
+
let totalEvents = 0;
|
|
449
|
+
for (const file of files) {
|
|
450
|
+
const filePath = path.join(this.eventsDir, file);
|
|
451
|
+
const lines = fs.readFileSync(filePath, "utf8").trim().split("\n").filter(Boolean);
|
|
452
|
+
const count = lines.length;
|
|
453
|
+
totalEvents += count;
|
|
454
|
+
console.log(` ${file}: ${count} events`);
|
|
455
|
+
}
|
|
456
|
+
console.log(` Total: ${totalEvents} events`);
|
|
457
|
+
} else {
|
|
458
|
+
console.log(" (no events yet)");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return { active, busId };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 智能路由
|
|
466
|
+
*/
|
|
467
|
+
async resolve(myId, targetType) {
|
|
468
|
+
this.ensureBus();
|
|
469
|
+
this.loadBusData();
|
|
470
|
+
|
|
471
|
+
const result = await this.messageManager.resolve(myId, targetType);
|
|
472
|
+
|
|
473
|
+
if (result.single) {
|
|
474
|
+
console.log(result.single);
|
|
475
|
+
return result.single;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (result.candidates.length === 0) {
|
|
479
|
+
logError(`No ${targetType} agents found`);
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log(`Multiple ${targetType} agents found:`);
|
|
484
|
+
for (const candidate of result.candidates) {
|
|
485
|
+
const nickname = candidate.nickname ? ` (${candidate.nickname})` : "";
|
|
486
|
+
console.log(` ${candidate.id}${nickname}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 获取默认发布者
|
|
494
|
+
*/
|
|
495
|
+
getDefaultPublisher() {
|
|
496
|
+
// 使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
|
|
497
|
+
return process.env.UFOO_SUBSCRIBER_ID || null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* 确保当前 agent 已经 join 总线(如果没有则自动 join)
|
|
502
|
+
*/
|
|
503
|
+
async ensureJoined() {
|
|
504
|
+
this.ensureBus();
|
|
505
|
+
this.loadBusData();
|
|
506
|
+
|
|
507
|
+
// 检查是否已经 join
|
|
508
|
+
const currentSubscriber = this.getCurrentSubscriber();
|
|
509
|
+
if (currentSubscriber && this.busData.agents && this.busData.agents[currentSubscriber]) {
|
|
510
|
+
// 已经 join,只需更新心跳
|
|
511
|
+
this.subscriberManager.updateLastSeen(currentSubscriber);
|
|
512
|
+
this.saveBusData();
|
|
513
|
+
return currentSubscriber;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 还没有 join,自动 join
|
|
517
|
+
const sessionId = null; // 自动生成
|
|
518
|
+
const agentType = null; // 自动检测
|
|
519
|
+
const nickname = null; // 自动生成
|
|
520
|
+
const subscriber = await this.join(sessionId, agentType, nickname);
|
|
521
|
+
|
|
522
|
+
// 静默加入(不输出 "Joined event bus" 信息)
|
|
523
|
+
return subscriber;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* 后台消息提醒
|
|
528
|
+
*/
|
|
529
|
+
async alert(subscriber, intervalSeconds = 2, options = {}) {
|
|
530
|
+
this.ensureBus();
|
|
531
|
+
this.loadBusData();
|
|
532
|
+
|
|
533
|
+
if (!subscriber) {
|
|
534
|
+
throw new Error("alert requires <subscriber-id>");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const interval = Math.max(1, parseInt(intervalSeconds, 10) || 2);
|
|
538
|
+
const intervalMs = interval * 1000;
|
|
539
|
+
const useNotify = Boolean(options.notify);
|
|
540
|
+
const useTitle = options.title !== false;
|
|
541
|
+
const useBell = options.bell !== false;
|
|
542
|
+
const daemon = Boolean(options.daemon);
|
|
543
|
+
const stop = Boolean(options.stop);
|
|
544
|
+
|
|
545
|
+
const safeName = subscriberToSafeName(subscriber);
|
|
546
|
+
const pidDir = path.join(this.busDir, "pids");
|
|
547
|
+
const pidFile = path.join(pidDir, `alert-${safeName}.pid`);
|
|
548
|
+
const logDir = path.join(this.busDir, "logs");
|
|
549
|
+
const logFile = path.join(logDir, `alert-${safeName}.log`);
|
|
550
|
+
|
|
551
|
+
ensureDir(pidDir);
|
|
552
|
+
|
|
553
|
+
if (stop) {
|
|
554
|
+
if (fs.existsSync(pidFile)) {
|
|
555
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
|
|
556
|
+
if (pid && isPidAlive(pid)) {
|
|
557
|
+
try {
|
|
558
|
+
process.kill(pid);
|
|
559
|
+
console.log(`[alert] Stopped ${subscriber} (pid=${pid})`);
|
|
560
|
+
} catch {
|
|
561
|
+
console.log("[alert] Not running");
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
console.log(`[alert] Not running for ${subscriber}`);
|
|
565
|
+
}
|
|
566
|
+
fs.rmSync(pidFile, { force: true });
|
|
567
|
+
} else {
|
|
568
|
+
console.log(`[alert] Not running for ${subscriber}`);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (daemon) {
|
|
574
|
+
if (fs.existsSync(pidFile)) {
|
|
575
|
+
const existing = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
|
|
576
|
+
if (existing && isPidAlive(existing)) {
|
|
577
|
+
console.log(`[alert] Already running for ${subscriber} (pid=${existing})`);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
ensureDir(logDir);
|
|
583
|
+
|
|
584
|
+
const args = [
|
|
585
|
+
path.join(__dirname, "..", "..", "bin", "ufoo.js"),
|
|
586
|
+
"bus",
|
|
587
|
+
"alert",
|
|
588
|
+
subscriber,
|
|
589
|
+
String(interval),
|
|
590
|
+
];
|
|
591
|
+
if (useNotify) args.push("--notify");
|
|
592
|
+
if (!useTitle) args.push("--no-title");
|
|
593
|
+
if (!useBell) args.push("--no-bell");
|
|
594
|
+
|
|
595
|
+
const logStream = fs.openSync(logFile, "a");
|
|
596
|
+
const child = spawn(process.execPath, args, {
|
|
597
|
+
detached: true,
|
|
598
|
+
stdio: ["ignore", logStream, logStream],
|
|
599
|
+
cwd: process.cwd(),
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
child.unref();
|
|
603
|
+
fs.writeFileSync(pidFile, `${child.pid}\n`, "utf8");
|
|
604
|
+
console.log(`[alert] Started for ${subscriber} (pid=${child.pid}, log=${logFile})`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
fs.writeFileSync(pidFile, `${process.pid}\n`, "utf8");
|
|
609
|
+
const cleanup = () => {
|
|
610
|
+
if (fs.existsSync(pidFile)) fs.rmSync(pidFile, { force: true });
|
|
611
|
+
};
|
|
612
|
+
process.on("exit", cleanup);
|
|
613
|
+
process.on("SIGINT", () => {
|
|
614
|
+
cleanup();
|
|
615
|
+
process.exit(0);
|
|
616
|
+
});
|
|
617
|
+
process.on("SIGTERM", () => {
|
|
618
|
+
cleanup();
|
|
619
|
+
process.exit(0);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const queuePath = this.queueManager.getPendingPath(subscriber);
|
|
623
|
+
this.queueManager.ensureQueueDir(subscriber);
|
|
624
|
+
|
|
625
|
+
const countLines = () => {
|
|
626
|
+
if (!fs.existsSync(queuePath)) return 0;
|
|
627
|
+
const content = fs.readFileSync(queuePath, "utf8").trim();
|
|
628
|
+
if (!content) return 0;
|
|
629
|
+
return content.split("\n").filter(Boolean).length;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
let lastCount = countLines();
|
|
633
|
+
console.log(`[alert] Watching ${subscriber} (interval=${interval}s)`);
|
|
634
|
+
|
|
635
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
636
|
+
|
|
637
|
+
while (true) {
|
|
638
|
+
const count = countLines();
|
|
639
|
+
if (count > lastCount) {
|
|
640
|
+
const newCount = count - lastCount;
|
|
641
|
+
const now = new Date().toISOString().split("T")[1].slice(0, 8);
|
|
642
|
+
console.log(`[alert] ${now} +${newCount} new message(s)`);
|
|
643
|
+
|
|
644
|
+
if (useBell) {
|
|
645
|
+
const tty = getCurrentTty();
|
|
646
|
+
if (tty) shakeTerminalByTty(tty);
|
|
647
|
+
}
|
|
648
|
+
if (useTitle) {
|
|
649
|
+
process.stdout.write(`\x1b]0;[${count}] ${subscriber}\x07`);
|
|
650
|
+
}
|
|
651
|
+
if (useNotify && process.platform === "darwin") {
|
|
652
|
+
const message = `${newCount} new message(s)`;
|
|
653
|
+
spawn(
|
|
654
|
+
"osascript",
|
|
655
|
+
[
|
|
656
|
+
"-e",
|
|
657
|
+
`display notification "${message}" with title "ufoo bus" subtitle "${subscriber}"`,
|
|
658
|
+
],
|
|
659
|
+
{ detached: true, stdio: "ignore" }
|
|
660
|
+
).unref();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (useTitle && count > 0) {
|
|
665
|
+
process.stdout.write(`\x1b]0;[${count}] ${subscriber}\x07`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
lastCount = count;
|
|
669
|
+
await sleep(intervalMs);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* 前台消息监听
|
|
675
|
+
*/
|
|
676
|
+
async listen(subscriber, options = {}) {
|
|
677
|
+
this.ensureBus();
|
|
678
|
+
this.loadBusData();
|
|
679
|
+
|
|
680
|
+
let target = subscriber;
|
|
681
|
+
if (!target && options.autoJoin) {
|
|
682
|
+
target = await this.join();
|
|
683
|
+
console.log(`[listen] Auto-joined as: ${target}`);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!target) {
|
|
687
|
+
throw new Error("listen requires <subscriber-id> (or --auto-join)");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const queuePath = this.queueManager.getPendingPath(target);
|
|
691
|
+
this.queueManager.ensureQueueDir(target);
|
|
692
|
+
if (!fs.existsSync(queuePath)) {
|
|
693
|
+
fs.writeFileSync(queuePath, "", "utf8");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (options.reset) {
|
|
697
|
+
console.log("[listen] Resetting queue...");
|
|
698
|
+
truncateFile(queuePath);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const readLines = () => {
|
|
702
|
+
if (!fs.existsSync(queuePath)) return [];
|
|
703
|
+
const content = fs.readFileSync(queuePath, "utf8").trim();
|
|
704
|
+
if (!content) return [];
|
|
705
|
+
return content.split("\n").filter(Boolean);
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const formatLine = (line) => {
|
|
709
|
+
let data = null;
|
|
710
|
+
try {
|
|
711
|
+
data = JSON.parse(line);
|
|
712
|
+
} catch {
|
|
713
|
+
data = null;
|
|
714
|
+
}
|
|
715
|
+
const msg = data?.data?.message ?? data?.data ?? line;
|
|
716
|
+
const from = data?.publisher ?? "unknown";
|
|
717
|
+
const ts = data?.ts || data?.timestamp || "";
|
|
718
|
+
const shortTs = ts ? ts.slice(11, 19) : "";
|
|
719
|
+
const prefix = shortTs ? `[${shortTs}] ` : "";
|
|
720
|
+
console.log(`${prefix}<${from}> ${msg}`);
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
if (options.fromBeginning) {
|
|
724
|
+
const lines = readLines();
|
|
725
|
+
if (lines.length > 0) {
|
|
726
|
+
console.log("[listen] Existing messages:");
|
|
727
|
+
console.log("---");
|
|
728
|
+
lines.forEach((line) => formatLine(line));
|
|
729
|
+
console.log("---");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
console.log("[listen] Listening for new messages... (Ctrl+C to stop)");
|
|
734
|
+
|
|
735
|
+
let lastLines = readLines().length;
|
|
736
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
737
|
+
|
|
738
|
+
while (true) {
|
|
739
|
+
const lines = readLines();
|
|
740
|
+
if (lines.length > lastLines) {
|
|
741
|
+
const newLines = lines.slice(lastLines);
|
|
742
|
+
const tty = getCurrentTty();
|
|
743
|
+
if (tty) shakeTerminalByTty(tty);
|
|
744
|
+
newLines.forEach((line) => {
|
|
745
|
+
formatLine(line);
|
|
746
|
+
});
|
|
747
|
+
lastLines = lines.length;
|
|
748
|
+
}
|
|
749
|
+
await sleep(1000);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Daemon 管理
|
|
755
|
+
*/
|
|
756
|
+
async daemon(action, options = {}) {
|
|
757
|
+
const interval = options.interval || 2000;
|
|
758
|
+
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval);
|
|
759
|
+
|
|
760
|
+
switch (action) {
|
|
761
|
+
case "start":
|
|
762
|
+
await daemon.start(options.background || false);
|
|
763
|
+
break;
|
|
764
|
+
case "stop":
|
|
765
|
+
daemon.stop();
|
|
766
|
+
break;
|
|
767
|
+
case "status":
|
|
768
|
+
daemon.status();
|
|
769
|
+
break;
|
|
770
|
+
default:
|
|
771
|
+
throw new Error(`Unknown daemon action: ${action}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* 注入命令到订阅者终端
|
|
777
|
+
*/
|
|
778
|
+
async inject(subscriber, commandOverride = "") {
|
|
779
|
+
this.ensureBus();
|
|
780
|
+
const injector = new Injector(this.busDir, this.agentsFile);
|
|
781
|
+
await injector.inject(subscriber, commandOverride);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
module.exports = EventBus;
|