u-foo 1.0.6 → 1.2.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/README.md +247 -23
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +168 -28
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +157 -0
- package/src/chat/index.js +938 -2910
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +133 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +741 -238
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1587 -0
- package/src/config.js +50 -2
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +662 -489
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/status/index.js +50 -17
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
package/src/bus/index.js
CHANGED
|
@@ -2,10 +2,9 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { spawn } = require("child_process");
|
|
4
4
|
const {
|
|
5
|
-
getTimestamp,
|
|
6
|
-
ensureDir,
|
|
7
5
|
logInfo,
|
|
8
6
|
logOk,
|
|
7
|
+
ensureDir,
|
|
9
8
|
logWarn,
|
|
10
9
|
logError,
|
|
11
10
|
colors,
|
|
@@ -14,6 +13,7 @@ const {
|
|
|
14
13
|
isPidAlive,
|
|
15
14
|
truncateFile,
|
|
16
15
|
getCurrentTty,
|
|
16
|
+
sleep,
|
|
17
17
|
} = require("./utils");
|
|
18
18
|
const { shakeTerminalByTty } = require("./shake");
|
|
19
19
|
const QueueManager = require("./queue");
|
|
@@ -22,8 +22,7 @@ const MessageManager = require("./message");
|
|
|
22
22
|
const NicknameManager = require("./nickname");
|
|
23
23
|
const BusDaemon = require("./daemon");
|
|
24
24
|
const Injector = require("./inject");
|
|
25
|
-
const {
|
|
26
|
-
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
25
|
+
const { BusStore } = require("./store");
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Event Bus - 项目级 Agent 事件总线
|
|
@@ -31,11 +30,12 @@ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
|
31
30
|
class EventBus {
|
|
32
31
|
constructor(projectRoot) {
|
|
33
32
|
this.projectRoot = projectRoot;
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
33
|
+
this.store = new BusStore(projectRoot);
|
|
34
|
+
this.paths = this.store.paths;
|
|
35
|
+
this.busDir = this.store.busDir;
|
|
36
|
+
this.agentsFile = this.store.agentsFile;
|
|
37
|
+
this.eventsDir = this.store.eventsDir;
|
|
38
|
+
this.logsDir = this.store.logsDir;
|
|
39
39
|
|
|
40
40
|
this.busData = null;
|
|
41
41
|
this.queueManager = null;
|
|
@@ -47,18 +47,14 @@ class EventBus {
|
|
|
47
47
|
* 确保 bus 已初始化
|
|
48
48
|
*/
|
|
49
49
|
ensureBus() {
|
|
50
|
-
|
|
51
|
-
throw new Error(
|
|
52
|
-
"Event bus not initialized. Please run: ufoo bus init or /uinit"
|
|
53
|
-
);
|
|
54
|
-
}
|
|
50
|
+
this.store.ensure();
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/**
|
|
58
54
|
* 加载 bus 数据
|
|
59
55
|
*/
|
|
60
56
|
loadBusData() {
|
|
61
|
-
this.busData =
|
|
57
|
+
this.busData = this.store.load();
|
|
62
58
|
|
|
63
59
|
this.queueManager = new QueueManager(this.busDir);
|
|
64
60
|
this.subscriberManager = new SubscriberManager(
|
|
@@ -79,101 +75,100 @@ class EventBus {
|
|
|
79
75
|
* 保存 bus 数据
|
|
80
76
|
*/
|
|
81
77
|
saveBusData() {
|
|
82
|
-
|
|
83
|
-
saveAgentsData(this.agentsFile, this.busData);
|
|
84
|
-
}
|
|
78
|
+
this.store.save(this.busData);
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
/**
|
|
88
82
|
* 获取当前订阅者 ID
|
|
89
83
|
*/
|
|
90
84
|
getCurrentSubscriber() {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return process.env.UFOO_SUBSCRIBER_ID;
|
|
94
|
-
}
|
|
85
|
+
return this.store.getCurrentSubscriber(this.busData);
|
|
86
|
+
}
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
/**
|
|
89
|
+
* 解析订阅者 ID
|
|
90
|
+
*/
|
|
91
|
+
parseSubscriber(subscriber) {
|
|
92
|
+
if (!subscriber || typeof subscriber !== "string") return null;
|
|
93
|
+
if (subscriber === "ufoo-agent") {
|
|
94
|
+
return { agentType: "codex", sessionId: "ufoo-agent" };
|
|
95
|
+
}
|
|
96
|
+
const parts = subscriber.split(":");
|
|
97
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
|
|
98
|
+
return {
|
|
99
|
+
agentType: parts[0],
|
|
100
|
+
sessionId: parts[1],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return sessionId;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
104
|
+
/**
|
|
105
|
+
* 推断 join 所需的 agentType
|
|
106
|
+
*/
|
|
107
|
+
resolveJoinAgentType(explicitAgentType, currentSubscriber = "") {
|
|
108
|
+
if (explicitAgentType) return explicitAgentType;
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
const ttyPath = fs.realpathSync("/dev/tty");
|
|
113
|
-
if (ttyPath && ttyPath.startsWith("/dev/")) {
|
|
114
|
-
currentTty = ttyPath;
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
// tty 不可用
|
|
118
|
-
}
|
|
110
|
+
const parsedCurrent = this.parseSubscriber(currentSubscriber);
|
|
111
|
+
if (parsedCurrent && parsedCurrent.agentType) return parsedCurrent.agentType;
|
|
119
112
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (meta.tty === currentTty) {
|
|
123
|
-
return id;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
113
|
+
const envAgentType = (process.env.UFOO_AGENT_TYPE || "").trim();
|
|
114
|
+
if (envAgentType) return envAgentType;
|
|
127
115
|
|
|
128
|
-
|
|
116
|
+
const parsedEnv = this.parseSubscriber(process.env.UFOO_SUBSCRIBER_ID || "");
|
|
117
|
+
if (parsedEnv && parsedEnv.agentType) return parsedEnv.agentType;
|
|
118
|
+
|
|
119
|
+
// 最后回退(手动场景)
|
|
120
|
+
return "claude-code";
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
/**
|
|
132
124
|
* 初始化事件总线
|
|
133
125
|
*/
|
|
134
126
|
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
|
-
}
|
|
127
|
+
this.store.init();
|
|
153
128
|
logOk("Event bus initialized");
|
|
154
129
|
}
|
|
155
130
|
|
|
156
131
|
/**
|
|
157
132
|
* 加入总线
|
|
158
133
|
*/
|
|
159
|
-
async join(sessionId, agentType, nickname = null) {
|
|
134
|
+
async join(sessionId, agentType, nickname = null, options = {}) {
|
|
160
135
|
this.ensureBus();
|
|
161
136
|
this.loadBusData();
|
|
162
137
|
|
|
138
|
+
const currentSubscriber = this.getCurrentSubscriber();
|
|
139
|
+
const currentMeta = currentSubscriber && this.busData.agents
|
|
140
|
+
? this.busData.agents[currentSubscriber]
|
|
141
|
+
: null;
|
|
142
|
+
const currentActive = currentMeta
|
|
143
|
+
&& currentMeta.status === "active"
|
|
144
|
+
&& (!currentMeta.pid || isPidAlive(currentMeta.pid));
|
|
145
|
+
|
|
146
|
+
// 已在总线中且无显式参数时,直接复用当前身份(避免二次 join 产生新 ID)
|
|
147
|
+
if (!sessionId && !agentType && currentSubscriber && currentActive) {
|
|
148
|
+
this.subscriberManager.updateLastSeen(currentSubscriber);
|
|
149
|
+
this.saveBusData();
|
|
150
|
+
const currentNickname = currentMeta.nickname ? ` (${currentMeta.nickname})` : "";
|
|
151
|
+
logInfo(`Already joined event bus: ${currentSubscriber}${currentNickname}`);
|
|
152
|
+
return currentSubscriber;
|
|
153
|
+
}
|
|
154
|
+
|
|
163
155
|
// 自动检测 session ID 和 agent type
|
|
156
|
+
const parsedCurrent = this.parseSubscriber(currentSubscriber);
|
|
157
|
+
if (!sessionId && parsedCurrent && parsedCurrent.sessionId) {
|
|
158
|
+
sessionId = parsedCurrent.sessionId;
|
|
159
|
+
}
|
|
160
|
+
|
|
164
161
|
if (!sessionId) {
|
|
165
162
|
sessionId = generateInstanceId();
|
|
166
163
|
}
|
|
167
164
|
|
|
168
|
-
|
|
169
|
-
// 默认为 claude-code(手动启动情况)
|
|
170
|
-
agentType = "claude-code";
|
|
171
|
-
}
|
|
165
|
+
agentType = this.resolveJoinAgentType(agentType, currentSubscriber);
|
|
172
166
|
|
|
173
167
|
const result = await this.subscriberManager.join(
|
|
174
168
|
sessionId,
|
|
175
169
|
agentType,
|
|
176
|
-
nickname
|
|
170
|
+
nickname,
|
|
171
|
+
options
|
|
177
172
|
);
|
|
178
173
|
|
|
179
174
|
this.saveBusData();
|
|
@@ -266,7 +261,7 @@ class EventBus {
|
|
|
266
261
|
/**
|
|
267
262
|
* 发送消息
|
|
268
263
|
*/
|
|
269
|
-
async send(target, message, publisher = null) {
|
|
264
|
+
async send(target, message, publisher = null, options = {}) {
|
|
270
265
|
this.ensureBus();
|
|
271
266
|
this.loadBusData();
|
|
272
267
|
|
|
@@ -308,7 +303,11 @@ class EventBus {
|
|
|
308
303
|
}
|
|
309
304
|
|
|
310
305
|
try {
|
|
311
|
-
const
|
|
306
|
+
const eventName = options.event || "message";
|
|
307
|
+
const data = options.data || { message };
|
|
308
|
+
const result = eventName === "message"
|
|
309
|
+
? await this.messageManager.send(target, message, publisher)
|
|
310
|
+
: await this.messageManager.emit(target, eventName, data, publisher);
|
|
312
311
|
logOk(
|
|
313
312
|
`Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
|
|
314
313
|
);
|
|
@@ -322,8 +321,8 @@ class EventBus {
|
|
|
322
321
|
/**
|
|
323
322
|
* 广播消息
|
|
324
323
|
*/
|
|
325
|
-
async broadcast(message, publisher = null) {
|
|
326
|
-
return this.send("*", message, publisher);
|
|
324
|
+
async broadcast(message, publisher = null, options = {}) {
|
|
325
|
+
return this.send("*", message, publisher, options);
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
/**
|
|
@@ -506,13 +505,25 @@ class EventBus {
|
|
|
506
505
|
|
|
507
506
|
// 检查是否已经 join
|
|
508
507
|
const currentSubscriber = this.getCurrentSubscriber();
|
|
509
|
-
|
|
508
|
+
const currentMeta = currentSubscriber && this.busData.agents
|
|
509
|
+
? this.busData.agents[currentSubscriber]
|
|
510
|
+
: null;
|
|
511
|
+
const currentActive = currentMeta
|
|
512
|
+
&& currentMeta.status === "active"
|
|
513
|
+
&& (!currentMeta.pid || isPidAlive(currentMeta.pid));
|
|
514
|
+
if (currentSubscriber && currentActive) {
|
|
510
515
|
// 已经 join,只需更新心跳
|
|
511
516
|
this.subscriberManager.updateLastSeen(currentSubscriber);
|
|
512
517
|
this.saveBusData();
|
|
513
518
|
return currentSubscriber;
|
|
514
519
|
}
|
|
515
520
|
|
|
521
|
+
// 当前身份可解析但元数据缺失/失效时,复用同一身份重新注册
|
|
522
|
+
const parsedCurrent = this.parseSubscriber(currentSubscriber || "");
|
|
523
|
+
if (parsedCurrent) {
|
|
524
|
+
return this.join(parsedCurrent.sessionId, parsedCurrent.agentType, null);
|
|
525
|
+
}
|
|
526
|
+
|
|
516
527
|
// 还没有 join,自动 join
|
|
517
528
|
const sessionId = null; // 自动生成
|
|
518
529
|
const agentType = null; // 自动检测
|
|
@@ -670,6 +681,52 @@ class EventBus {
|
|
|
670
681
|
}
|
|
671
682
|
}
|
|
672
683
|
|
|
684
|
+
/**
|
|
685
|
+
* 远程唤醒本地 agent(触发 /ubus 注入)
|
|
686
|
+
*/
|
|
687
|
+
async wake(subscriber, options = {}) {
|
|
688
|
+
this.ensureBus();
|
|
689
|
+
this.loadBusData();
|
|
690
|
+
|
|
691
|
+
const publisher =
|
|
692
|
+
options.publisher ||
|
|
693
|
+
process.env.AI_BUS_PUBLISHER ||
|
|
694
|
+
this.getDefaultPublisher() ||
|
|
695
|
+
this.getCurrentSubscriber() ||
|
|
696
|
+
"unknown";
|
|
697
|
+
|
|
698
|
+
const targets = this.messageManager.resolveTarget(subscriber || "");
|
|
699
|
+
if (targets.length === 0) {
|
|
700
|
+
throw new Error(`Target "${subscriber}" not found`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
for (const target of targets) {
|
|
704
|
+
const safeName = subscriberToSafeName(target);
|
|
705
|
+
const queueDir = path.join(this.busDir, "queues", safeName);
|
|
706
|
+
const pendingFile = path.join(queueDir, "pending.jsonl");
|
|
707
|
+
ensureDir(queueDir);
|
|
708
|
+
|
|
709
|
+
const before = fs.existsSync(pendingFile) ? fs.readFileSync(pendingFile, "utf8") : "";
|
|
710
|
+
const countBefore = before.trim() ? before.trim().split(/\r?\n/).length : 0;
|
|
711
|
+
await this.messageManager.emit(target, "wake", { reason: options.reason || "remote" }, publisher, "status/wake");
|
|
712
|
+
const after = fs.existsSync(pendingFile) ? fs.readFileSync(pendingFile, "utf8") : "";
|
|
713
|
+
const countAfter = after.trim() ? after.trim().split(/\r?\n/).length : 0;
|
|
714
|
+
|
|
715
|
+
if (countAfter > countBefore) {
|
|
716
|
+
await sleep(50);
|
|
717
|
+
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000);
|
|
718
|
+
await daemon.injector.inject(target, options.command || "");
|
|
719
|
+
if (options.shake !== false) {
|
|
720
|
+
const tty = daemon.injector.readTty(target);
|
|
721
|
+
if (tty) shakeTerminalByTty(tty, { skipFrontmost: true });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
logOk(`Wake sent -> ${targets.join(", ")}`);
|
|
727
|
+
return { ok: true, targets };
|
|
728
|
+
}
|
|
729
|
+
|
|
673
730
|
/**
|
|
674
731
|
* 前台消息监听
|
|
675
732
|
*/
|
package/src/bus/inject.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require("fs");
|
|
|
3
3
|
const net = require("net");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { subscriberToSafeName, isValidTty } = require("./utils");
|
|
6
|
+
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
6
7
|
|
|
7
8
|
const SHOULD_LOG_INJECT = process.env.UFOO_INJECT_DEBUG === "1";
|
|
8
9
|
const logInject = (message) => {
|
|
@@ -35,6 +36,18 @@ class Injector {
|
|
|
35
36
|
/**
|
|
36
37
|
* 获取订阅者的 tmux pane ID(从 all-agents.json)
|
|
37
38
|
*/
|
|
39
|
+
getAgentMeta(subscriber) {
|
|
40
|
+
const agentsFile = this.agentsFile;
|
|
41
|
+
if (!agentsFile || !fs.existsSync(agentsFile)) return null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const busData = JSON.parse(fs.readFileSync(agentsFile, "utf8"));
|
|
45
|
+
return busData.agents?.[subscriber] || null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
getTmuxPane(subscriber) {
|
|
39
52
|
const agentsFile = this.agentsFile;
|
|
40
53
|
if (!agentsFile || !fs.existsSync(agentsFile)) return null;
|
|
@@ -235,16 +248,31 @@ class Injector {
|
|
|
235
248
|
* 2. tmux send-keys(无需权限)
|
|
236
249
|
*/
|
|
237
250
|
async inject(subscriber, commandOverride = "") {
|
|
251
|
+
if (String(subscriber || "").startsWith("ufoo-code:")) {
|
|
252
|
+
throw new Error(`Inject disabled for ${subscriber}. ufoo-code consumes bus internally.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
238
255
|
// 确定注入命令(codex 用 "ubus",claude-code 用 "/ubus")
|
|
239
256
|
const command = commandOverride
|
|
240
257
|
? String(commandOverride)
|
|
241
258
|
: (subscriber.startsWith("codex:") ? "ubus" : "/ubus");
|
|
242
259
|
|
|
260
|
+
const meta = this.getAgentMeta(subscriber) || {};
|
|
261
|
+
const launchMode = meta.launch_mode || "";
|
|
262
|
+
const adapterRouter = createTerminalAdapterRouter();
|
|
263
|
+
const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriber });
|
|
264
|
+
const supportsSocket = adapter.capabilities.supportsSocketProtocol;
|
|
265
|
+
const supportsNotifier = adapter.capabilities.supportsNotifierInjector;
|
|
266
|
+
|
|
243
267
|
// 1. 优先尝试 PTY socket(无需任何macOS权限)
|
|
244
268
|
const injectSockPath = this.getInjectSockPath(subscriber);
|
|
245
269
|
if (fs.existsSync(injectSockPath)) {
|
|
246
270
|
try {
|
|
247
|
-
|
|
271
|
+
if (!supportsSocket) {
|
|
272
|
+
logInject(`[inject] PTY socket present but unsupported for launch_mode=${launchMode}`);
|
|
273
|
+
} else {
|
|
274
|
+
logInject(`[inject] Using PTY socket: ${injectSockPath}`);
|
|
275
|
+
}
|
|
248
276
|
await this.injectPty(subscriber, command);
|
|
249
277
|
logInject("[inject] PTY inject success");
|
|
250
278
|
return;
|
|
@@ -256,24 +284,26 @@ class Injector {
|
|
|
256
284
|
// 读取 tty(tmux 需要)
|
|
257
285
|
const tty = this.readTty(subscriber);
|
|
258
286
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
287
|
+
if (supportsNotifier) {
|
|
288
|
+
// 2. 尝试 tmux(无需权限)
|
|
289
|
+
const tmuxPane = meta.tmux_pane || this.getTmuxPane(subscriber);
|
|
290
|
+
if (tmuxPane) {
|
|
291
|
+
const paneExists = await this.checkTmuxPane(tmuxPane);
|
|
292
|
+
if (paneExists) {
|
|
293
|
+
logInject(`[inject] Using tmux send-keys for pane: ${tmuxPane}`);
|
|
294
|
+
await this.injectTmux(tmuxPane, command);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
267
297
|
}
|
|
268
|
-
}
|
|
269
298
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
// 尝试通过 tty 查找 tmux pane
|
|
300
|
+
if (tty && isValidTty(tty)) {
|
|
301
|
+
const fallbackPane = await this.findTmuxPaneByTty(tty);
|
|
302
|
+
if (fallbackPane) {
|
|
303
|
+
logInject(`[inject] Using tmux send-keys for pane: ${fallbackPane}`);
|
|
304
|
+
await this.injectTmux(fallbackPane, command);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
277
307
|
}
|
|
278
308
|
}
|
|
279
309
|
|
package/src/bus/message.js
CHANGED
|
@@ -6,11 +6,23 @@ const {
|
|
|
6
6
|
readJSONL,
|
|
7
7
|
appendJSONL,
|
|
8
8
|
readLastLine,
|
|
9
|
-
|
|
10
|
-
isMetaActive,
|
|
9
|
+
isPidAlive,
|
|
11
10
|
} = require("./utils");
|
|
12
11
|
const NicknameManager = require("./nickname");
|
|
13
12
|
|
|
13
|
+
const SEQ_LOCK_TIMEOUT_MS = 5000;
|
|
14
|
+
const SEQ_LOCK_POLL_MS = 25;
|
|
15
|
+
const SEQ_LOCK_STALE_MS = 30000;
|
|
16
|
+
|
|
17
|
+
function normalizeAgentTypeAlias(value = "") {
|
|
18
|
+
const text = String(value || "").trim().toLowerCase();
|
|
19
|
+
if (!text) return "";
|
|
20
|
+
if (text === "codex") return "codex";
|
|
21
|
+
if (text === "claude" || text === "claude-code") return "claude-code";
|
|
22
|
+
if (text === "ufoo" || text === "ucode" || text === "ufoo-code") return "ufoo-code";
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
/**
|
|
15
27
|
* 消息管理器
|
|
16
28
|
*/
|
|
@@ -20,17 +32,17 @@ class MessageManager {
|
|
|
20
32
|
this.busData = busData;
|
|
21
33
|
this.queueManager = queueManager;
|
|
22
34
|
this.eventsDir = path.join(busDir, "events");
|
|
35
|
+
this.seqFile = path.join(busDir, "seq.counter");
|
|
36
|
+
this.seqLockFile = path.join(busDir, "seq.counter.lock");
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
/**
|
|
26
|
-
*
|
|
40
|
+
* 从 events 日志中恢复最大序号(仅用于 counter 缺失时)
|
|
27
41
|
*/
|
|
28
|
-
|
|
29
|
-
// 读取所有 events/*.jsonl 文件,找到最大的 seq
|
|
42
|
+
readMaxSeqFromEvents() {
|
|
30
43
|
let maxSeq = 0;
|
|
31
|
-
|
|
32
44
|
if (!fs.existsSync(this.eventsDir)) {
|
|
33
|
-
return
|
|
45
|
+
return maxSeq;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
const files = fs.readdirSync(this.eventsDir)
|
|
@@ -55,7 +67,116 @@ class MessageManager {
|
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
69
|
|
|
58
|
-
return maxSeq
|
|
70
|
+
return maxSeq;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
readSeqCounter() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = fs.readFileSync(this.seqFile, "utf8").trim();
|
|
76
|
+
const parsed = parseInt(raw, 10);
|
|
77
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore
|
|
82
|
+
}
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
writeSeqCounter(seq) {
|
|
87
|
+
fs.mkdirSync(path.dirname(this.seqFile), { recursive: true });
|
|
88
|
+
fs.writeFileSync(this.seqFile, `${seq}\n`, "utf8");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cleanupStaleSeqLock() {
|
|
92
|
+
if (!fs.existsSync(this.seqLockFile)) return;
|
|
93
|
+
let shouldRemove = false;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const raw = fs.readFileSync(this.seqLockFile, "utf8").trim();
|
|
97
|
+
const pid = parseInt(raw, 10);
|
|
98
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
99
|
+
shouldRemove = true;
|
|
100
|
+
} else if (!isPidAlive(pid)) {
|
|
101
|
+
shouldRemove = true;
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
shouldRemove = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!shouldRemove) {
|
|
108
|
+
try {
|
|
109
|
+
const stat = fs.statSync(this.seqLockFile);
|
|
110
|
+
if (Date.now() - stat.mtimeMs > SEQ_LOCK_STALE_MS) {
|
|
111
|
+
shouldRemove = true;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
shouldRemove = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (shouldRemove) {
|
|
119
|
+
try {
|
|
120
|
+
fs.unlinkSync(this.seqLockFile);
|
|
121
|
+
} catch {
|
|
122
|
+
// ignore stale lock cleanup errors
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async acquireSeqLock() {
|
|
128
|
+
const deadline = Date.now() + SEQ_LOCK_TIMEOUT_MS;
|
|
129
|
+
while (Date.now() < deadline) {
|
|
130
|
+
try {
|
|
131
|
+
const fd = fs.openSync(this.seqLockFile, "wx");
|
|
132
|
+
fs.writeSync(fd, `${process.pid}\n`);
|
|
133
|
+
return fd;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err && err.code === "EEXIST") {
|
|
136
|
+
this.cleanupStaleSeqLock();
|
|
137
|
+
// eslint-disable-next-line no-await-in-loop
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, SEQ_LOCK_POLL_MS));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw new Error("Failed to acquire sequence lock");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
releaseSeqLock(lockFd) {
|
|
148
|
+
try {
|
|
149
|
+
if (typeof lockFd === "number") {
|
|
150
|
+
fs.closeSync(lockFd);
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// ignore
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
if (fs.existsSync(this.seqLockFile)) {
|
|
157
|
+
fs.unlinkSync(this.seqLockFile);
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 获取下一个全局序号(文件锁保证跨进程原子递增)
|
|
166
|
+
*/
|
|
167
|
+
async getNextSeq() {
|
|
168
|
+
const lockFd = await this.acquireSeqLock();
|
|
169
|
+
try {
|
|
170
|
+
let current = this.readSeqCounter();
|
|
171
|
+
if (current === 0) {
|
|
172
|
+
current = this.readMaxSeqFromEvents();
|
|
173
|
+
}
|
|
174
|
+
const next = current + 1;
|
|
175
|
+
this.writeSeqCounter(next);
|
|
176
|
+
return next;
|
|
177
|
+
} finally {
|
|
178
|
+
this.releaseSeqLock(lockFd);
|
|
179
|
+
}
|
|
59
180
|
}
|
|
60
181
|
|
|
61
182
|
/**
|
|
@@ -63,6 +184,13 @@ class MessageManager {
|
|
|
63
184
|
*/
|
|
64
185
|
resolveTarget(target) {
|
|
65
186
|
const nicknameManager = new NicknameManager(this.busData);
|
|
187
|
+
const normalizedTarget = normalizeAgentTypeAlias(target);
|
|
188
|
+
|
|
189
|
+
// 0. Exact subscriber ID match (allows ids without ":" e.g. "ufoo-agent")
|
|
190
|
+
const subscribers = this.busData.agents || {};
|
|
191
|
+
if (target && typeof target === "string" && subscribers[target]) {
|
|
192
|
+
return [target];
|
|
193
|
+
}
|
|
66
194
|
|
|
67
195
|
// 1. 尝试作为订阅者 ID
|
|
68
196
|
if (target.includes(":")) {
|
|
@@ -75,10 +203,11 @@ class MessageManager {
|
|
|
75
203
|
return [byNickname];
|
|
76
204
|
}
|
|
77
205
|
|
|
78
|
-
// 3.
|
|
79
|
-
const
|
|
206
|
+
// 3. 尝试作为代理类型(匹配所有该类型的订阅者)
|
|
207
|
+
const isActive = (meta) => !meta || meta.status === "active";
|
|
208
|
+
|
|
80
209
|
const byType = Object.entries(subscribers)
|
|
81
|
-
.filter(([, meta]) => meta.agent_type ===
|
|
210
|
+
.filter(([, meta]) => normalizeAgentTypeAlias(meta.agent_type) === normalizedTarget && isActive(meta))
|
|
82
211
|
.map(([id]) => id);
|
|
83
212
|
|
|
84
213
|
if (byType.length > 0) {
|
|
@@ -88,7 +217,7 @@ class MessageManager {
|
|
|
88
217
|
// 4. 通配符(所有活跃订阅者)
|
|
89
218
|
if (target === "*") {
|
|
90
219
|
return Object.entries(subscribers)
|
|
91
|
-
.filter(([, meta]) =>
|
|
220
|
+
.filter(([, meta]) => isActive(meta))
|
|
92
221
|
.map(([id]) => id);
|
|
93
222
|
}
|
|
94
223
|
|
|
@@ -100,12 +229,13 @@ class MessageManager {
|
|
|
100
229
|
* 检查目标是否匹配订阅者
|
|
101
230
|
*/
|
|
102
231
|
targetMatches(target, subscriber) {
|
|
232
|
+
const normalizedTarget = normalizeAgentTypeAlias(target);
|
|
103
233
|
// 精确匹配
|
|
104
234
|
if (target === subscriber) return true;
|
|
105
235
|
|
|
106
236
|
// 代理类型匹配
|
|
107
237
|
const meta = this.busData.agents?.[subscriber];
|
|
108
|
-
if (meta &&
|
|
238
|
+
if (meta && normalizedTarget === normalizeAgentTypeAlias(meta.agent_type)) return true;
|
|
109
239
|
|
|
110
240
|
// 昵称匹配
|
|
111
241
|
if (meta && target === meta.nickname) return true;
|
|
@@ -269,16 +399,14 @@ class MessageManager {
|
|
|
269
399
|
* 智能路由解析(找出所有匹配的候选者)
|
|
270
400
|
*/
|
|
271
401
|
async resolve(myId, targetType) {
|
|
402
|
+
const normalizedTargetType = normalizeAgentTypeAlias(targetType);
|
|
272
403
|
const subscribers = this.busData.agents || {};
|
|
273
404
|
const candidates = Object.entries(subscribers)
|
|
274
405
|
.filter(([id, meta]) => {
|
|
275
406
|
if (id === myId) return false; // 排除自己
|
|
276
407
|
if (meta.status !== "active") return false;
|
|
277
408
|
|
|
278
|
-
|
|
279
|
-
if (targetType === "codex" && meta.agent_type === "codex") return true;
|
|
280
|
-
if (targetType === "claude" && meta.agent_type === "claude-code") return true;
|
|
281
|
-
if (targetType === meta.agent_type) return true;
|
|
409
|
+
if (normalizeAgentTypeAlias(meta.agent_type) === normalizedTargetType) return true;
|
|
282
410
|
|
|
283
411
|
return false;
|
|
284
412
|
})
|
package/src/bus/nickname.js
CHANGED
|
@@ -44,7 +44,9 @@ class NicknameManager {
|
|
|
44
44
|
*/
|
|
45
45
|
generateAutoNickname(agentType) {
|
|
46
46
|
const subscribers = this.busData.agents || {};
|
|
47
|
-
const prefix = agentType === "claude-code" ? "claude"
|
|
47
|
+
const prefix = agentType === "claude-code" ? "claude"
|
|
48
|
+
: agentType === "ufoo-code" ? "ucode"
|
|
49
|
+
: agentType;
|
|
48
50
|
|
|
49
51
|
// 找出所有相同前缀的昵称
|
|
50
52
|
const existing = Object.values(subscribers)
|