u-foo 1.0.3 → 1.1.9
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 +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -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 +46 -42
- 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 +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- 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 +154 -0
- package/src/chat/index.js +1011 -1392
- 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 +132 -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 +1162 -96
- 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 +1580 -0
- package/src/config.js +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- 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 +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- 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/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- 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/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -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/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
package/src/bus/store.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { getTimestamp, ensureDir, safeNameToSubscriber, getTtyProcessInfo } = require("./utils");
|
|
6
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
8
|
+
|
|
9
|
+
function readQueueTty(queueDir) {
|
|
10
|
+
try {
|
|
11
|
+
const value = fs.readFileSync(path.join(queueDir, "tty"), "utf8").trim();
|
|
12
|
+
return value || "";
|
|
13
|
+
} catch {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nicknamePrefixForType(agentType = "") {
|
|
19
|
+
return agentType === "claude-code" ? "claude"
|
|
20
|
+
: agentType === "ufoo-code" ? "ucode"
|
|
21
|
+
: String(agentType || "agent");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecoverableSessionId(sessionId = "") {
|
|
25
|
+
const text = String(sessionId || "").trim();
|
|
26
|
+
if (!text) return false;
|
|
27
|
+
if (text.includes(":") || text.includes("_")) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildUsedNicknameSet(agents = {}) {
|
|
32
|
+
const set = new Set();
|
|
33
|
+
for (const meta of Object.values(agents || {})) {
|
|
34
|
+
if (!meta || meta.status !== "active") continue;
|
|
35
|
+
const nick = meta && typeof meta.nickname === "string" ? meta.nickname : "";
|
|
36
|
+
if (nick) set.add(nick);
|
|
37
|
+
}
|
|
38
|
+
return set;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function allocateRecoveredNickname(agentType, used) {
|
|
42
|
+
const prefix = nicknamePrefixForType(agentType);
|
|
43
|
+
let idx = 1;
|
|
44
|
+
while (used.has(`${prefix}-${idx}`)) idx += 1;
|
|
45
|
+
const nick = `${prefix}-${idx}`;
|
|
46
|
+
used.add(nick);
|
|
47
|
+
return nick;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class BusStore {
|
|
51
|
+
constructor(projectRoot) {
|
|
52
|
+
this.projectRoot = projectRoot;
|
|
53
|
+
this.paths = getUfooPaths(projectRoot);
|
|
54
|
+
this.busDir = this.paths.busDir;
|
|
55
|
+
this.agentsFile = this.paths.agentsFile;
|
|
56
|
+
this.eventsDir = this.paths.busEventsDir;
|
|
57
|
+
this.logsDir = this.paths.busLogsDir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ensure() {
|
|
61
|
+
if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Event bus not initialized. Please run: ufoo bus init or /uinit"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
load() {
|
|
69
|
+
const data = loadAgentsData(this.agentsFile);
|
|
70
|
+
if (!data.agents || typeof data.agents !== "object") {
|
|
71
|
+
data.agents = {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const queueRoot = path.join(this.busDir, "queues");
|
|
75
|
+
if (!fs.existsSync(queueRoot)) return data;
|
|
76
|
+
|
|
77
|
+
const usedNicknames = buildUsedNicknameSet(data.agents);
|
|
78
|
+
const now = getTimestamp();
|
|
79
|
+
let recovered = false;
|
|
80
|
+
|
|
81
|
+
for (const entry of fs.readdirSync(queueRoot)) {
|
|
82
|
+
const queueDir = path.join(queueRoot, entry);
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = fs.statSync(queueDir);
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isDirectory()) continue;
|
|
90
|
+
|
|
91
|
+
const subscriber = safeNameToSubscriber(entry);
|
|
92
|
+
const parts = subscriber.split(":");
|
|
93
|
+
if (parts.length !== 2) continue;
|
|
94
|
+
const [agentType, sessionId] = parts;
|
|
95
|
+
if (!agentType || !sessionId) continue;
|
|
96
|
+
if (!isRecoverableSessionId(sessionId)) continue;
|
|
97
|
+
if (data.agents[subscriber]) continue;
|
|
98
|
+
|
|
99
|
+
const tty = readQueueTty(queueDir);
|
|
100
|
+
const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
|
|
101
|
+
const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
|
|
102
|
+
const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
|
|
103
|
+
|
|
104
|
+
data.agents[subscriber] = {
|
|
105
|
+
agent_type: agentType,
|
|
106
|
+
nickname,
|
|
107
|
+
status: activeByTty ? "active" : "inactive",
|
|
108
|
+
joined_at: now,
|
|
109
|
+
last_seen: now,
|
|
110
|
+
pid: 0,
|
|
111
|
+
tty,
|
|
112
|
+
tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
|
|
113
|
+
tmux_pane: "",
|
|
114
|
+
launch_mode: "",
|
|
115
|
+
};
|
|
116
|
+
recovered = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (recovered) {
|
|
120
|
+
saveAgentsData(this.agentsFile, data);
|
|
121
|
+
}
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
save(busData) {
|
|
126
|
+
if (busData) {
|
|
127
|
+
saveAgentsData(this.agentsFile, busData);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
init() {
|
|
132
|
+
ensureDir(this.busDir);
|
|
133
|
+
ensureDir(this.paths.agentDir);
|
|
134
|
+
ensureDir(this.eventsDir);
|
|
135
|
+
ensureDir(path.join(this.busDir, "queues"));
|
|
136
|
+
ensureDir(this.logsDir);
|
|
137
|
+
ensureDir(path.join(this.busDir, "offsets"));
|
|
138
|
+
ensureDir(this.paths.busDaemonDir);
|
|
139
|
+
ensureDir(this.paths.busDaemonCountsDir);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(this.agentsFile)) {
|
|
142
|
+
const busData = {
|
|
143
|
+
created_at: getTimestamp(),
|
|
144
|
+
agents: {},
|
|
145
|
+
};
|
|
146
|
+
saveAgentsData(this.agentsFile, busData);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getCurrentSubscriber(busData) {
|
|
151
|
+
if (process.env.UFOO_SUBSCRIBER_ID) {
|
|
152
|
+
return process.env.UFOO_SUBSCRIBER_ID;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(this.agentsFile)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sessionFile = path.join(this.paths.agentDir, "session.txt");
|
|
160
|
+
if (fs.existsSync(sessionFile)) {
|
|
161
|
+
const sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
162
|
+
if (sessionId) {
|
|
163
|
+
return sessionId;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let currentTty = null;
|
|
168
|
+
try {
|
|
169
|
+
const ttyPath = fs.realpathSync("/dev/tty");
|
|
170
|
+
if (ttyPath && ttyPath.startsWith("/dev/")) {
|
|
171
|
+
currentTty = ttyPath;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// tty not available
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (currentTty && busData && busData.agents) {
|
|
178
|
+
for (const [id, meta] of Object.entries(busData.agents)) {
|
|
179
|
+
if (meta.tty === currentTty) {
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { BusStore };
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessInfo } = require("./utils");
|
|
3
|
+
const NicknameManager = require("./nickname");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
function detectTerminalAppFromEnv() {
|
|
7
|
+
const termProgram = process.env.TERM_PROGRAM || "";
|
|
8
|
+
if (process.env.ITERM_SESSION_ID || termProgram === "iTerm.app") return "iterm2";
|
|
9
|
+
if (termProgram === "Apple_Terminal") return "terminal";
|
|
10
|
+
return termProgram || "";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取当前终端的 tty 路径
|
|
15
|
+
*/
|
|
16
|
+
function resolveTtyFromPath(fdPath) {
|
|
17
|
+
try {
|
|
18
|
+
const real = fs.realpathSync(fdPath);
|
|
19
|
+
if (real && real.startsWith("/dev/")) {
|
|
20
|
+
return real;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTty(ttyPath) {
|
|
29
|
+
if (!ttyPath) return "";
|
|
30
|
+
const trimmed = String(ttyPath).trim();
|
|
31
|
+
if (!trimmed || trimmed === "not a tty") return "";
|
|
32
|
+
if (trimmed === "/dev/tty") return "";
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tryTtyWithStdin(fd) {
|
|
37
|
+
try {
|
|
38
|
+
const res = spawnSync("tty", {
|
|
39
|
+
stdio: [fd, "pipe", "ignore"],
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
});
|
|
42
|
+
if (res && res.status === 0) {
|
|
43
|
+
const out = normalizeTty(res.stdout || "");
|
|
44
|
+
if (out) return out;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTtyPath() {
|
|
53
|
+
// 0) Honor explicit ttyPath from node stdio if present (useful for tests)
|
|
54
|
+
const stdinTtyPath = normalizeTty(process.stdin?.ttyPath || "");
|
|
55
|
+
if (stdinTtyPath) return stdinTtyPath;
|
|
56
|
+
|
|
57
|
+
// 1) Try stdin directly (inherits real tty if present)
|
|
58
|
+
let ttyPath = tryTtyWithStdin(0);
|
|
59
|
+
if (ttyPath) return ttyPath;
|
|
60
|
+
|
|
61
|
+
// 2) Try controlling tty explicitly (works even if stdin is detached)
|
|
62
|
+
try {
|
|
63
|
+
const fd = fs.openSync("/dev/tty", "r");
|
|
64
|
+
ttyPath = tryTtyWithStdin(fd);
|
|
65
|
+
fs.closeSync(fd);
|
|
66
|
+
if (ttyPath) return ttyPath;
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3) Fallback to stdout/stderr device paths
|
|
72
|
+
if (process.stdout.isTTY) {
|
|
73
|
+
ttyPath = normalizeTty(resolveTtyFromPath("/dev/stdout"));
|
|
74
|
+
if (ttyPath) return ttyPath;
|
|
75
|
+
}
|
|
76
|
+
if (process.stderr.isTTY) {
|
|
77
|
+
ttyPath = normalizeTty(resolveTtyFromPath("/dev/stderr"));
|
|
78
|
+
if (ttyPath) return ttyPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Final fallback to controlling tty path (may be /dev/tty)
|
|
82
|
+
return normalizeTty(resolveTtyFromPath("/dev/tty"));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getJoinedPid() {
|
|
86
|
+
const raw = process.env.UFOO_PARENT_PID || "";
|
|
87
|
+
const parsed = parseInt(raw, 10);
|
|
88
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
89
|
+
return process.pid;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 订阅者管理
|
|
94
|
+
*/
|
|
95
|
+
class SubscriberManager {
|
|
96
|
+
constructor(busData, queueManager) {
|
|
97
|
+
this.busData = busData;
|
|
98
|
+
this.queueManager = queueManager;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async cleanupDuplicateTty(currentSubscriber, ttyPath) {
|
|
102
|
+
if (!ttyPath) return;
|
|
103
|
+
if (!this.busData.agents) return;
|
|
104
|
+
|
|
105
|
+
const entries = Object.entries(this.busData.agents);
|
|
106
|
+
for (const [id, meta] of entries) {
|
|
107
|
+
if (id === currentSubscriber) continue;
|
|
108
|
+
const metaTtyRaw = meta?.tty || "";
|
|
109
|
+
const metaTty = isValidTty(metaTtyRaw)
|
|
110
|
+
? metaTtyRaw
|
|
111
|
+
: (await this.queueManager.readTty(id));
|
|
112
|
+
if (!metaTty) continue;
|
|
113
|
+
if (metaTty === ttyPath) {
|
|
114
|
+
// Remove stale subscriber using same tty
|
|
115
|
+
delete this.busData.agents[id];
|
|
116
|
+
try {
|
|
117
|
+
const queueDir = this.queueManager.getQueueDir(id);
|
|
118
|
+
if (queueDir) {
|
|
119
|
+
fs.rmSync(queueDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
const offsetPath = this.queueManager.getOffsetPath(id);
|
|
122
|
+
if (offsetPath) fs.rmSync(offsetPath, { force: true });
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore cleanup errors
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 加入总线
|
|
132
|
+
*/
|
|
133
|
+
async join(sessionId, agentType, nickname = null, options = {}) {
|
|
134
|
+
// Special case: ufoo-agent uses fixed ID without suffix
|
|
135
|
+
const subscriber = (sessionId === "ufoo-agent")
|
|
136
|
+
? "ufoo-agent"
|
|
137
|
+
: `${agentType}:${sessionId}`;
|
|
138
|
+
|
|
139
|
+
if (!this.busData.agents) {
|
|
140
|
+
this.busData.agents = {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const nicknameManager = new NicknameManager(this.busData);
|
|
144
|
+
|
|
145
|
+
// 检查是否是重新加入(rejoin)
|
|
146
|
+
const existingMeta = this.busData.agents[subscriber];
|
|
147
|
+
let finalNickname = nickname;
|
|
148
|
+
|
|
149
|
+
if (existingMeta && existingMeta.nickname) {
|
|
150
|
+
// 重新加入,保留原昵称
|
|
151
|
+
finalNickname = existingMeta.nickname;
|
|
152
|
+
} else if (nickname) {
|
|
153
|
+
// 新昵称,检查冲突
|
|
154
|
+
if (nicknameManager.nicknameExists(nickname, subscriber)) {
|
|
155
|
+
throw new Error(`Nickname "${nickname}" already exists`);
|
|
156
|
+
}
|
|
157
|
+
finalNickname = nickname;
|
|
158
|
+
} else {
|
|
159
|
+
// 自动生成昵称(并标记占用,避免并发重复)
|
|
160
|
+
finalNickname = nicknameManager.generateAutoNickname(agentType);
|
|
161
|
+
nicknameManager.setNickname(subscriber, finalNickname);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const launchMode = options.launchMode || process.env.UFOO_LAUNCH_MODE || "";
|
|
165
|
+
const overridePid = Number.isFinite(options.parentPid) && options.parentPid > 0
|
|
166
|
+
? options.parentPid
|
|
167
|
+
: null;
|
|
168
|
+
const hasOverrideTty = Object.prototype.hasOwnProperty.call(options, "tty");
|
|
169
|
+
const overrideTty = (typeof options.tty === "string" && isValidTty(options.tty.trim()))
|
|
170
|
+
? options.tty.trim()
|
|
171
|
+
: "";
|
|
172
|
+
const detectedTty = hasOverrideTty ? overrideTty : getTtyPath();
|
|
173
|
+
const tty = overrideTty || (isValidTty(detectedTty) ? detectedTty : "");
|
|
174
|
+
const preservedTty = !tty && launchMode !== "internal" && isValidTty(existingMeta?.tty)
|
|
175
|
+
? existingMeta.tty
|
|
176
|
+
: "";
|
|
177
|
+
const finalTty = tty || preservedTty;
|
|
178
|
+
const ttyInfo = finalTty ? getTtyProcessInfo(finalTty) : null;
|
|
179
|
+
|
|
180
|
+
// 清理同一 tty 的旧订阅者(避免重复启动污染)
|
|
181
|
+
await this.cleanupDuplicateTty(subscriber, finalTty);
|
|
182
|
+
|
|
183
|
+
// 更新订阅者信息(保留已有字段,如 provider_session_*)
|
|
184
|
+
const preserved = existingMeta && typeof existingMeta === "object"
|
|
185
|
+
? { ...existingMeta }
|
|
186
|
+
: {};
|
|
187
|
+
this.busData.agents[subscriber] = {
|
|
188
|
+
...preserved,
|
|
189
|
+
agent_type: agentType,
|
|
190
|
+
nickname: finalNickname,
|
|
191
|
+
status: "active",
|
|
192
|
+
joined_at: existingMeta?.joined_at || getTimestamp(),
|
|
193
|
+
last_seen: getTimestamp(),
|
|
194
|
+
pid: overridePid || getJoinedPid(),
|
|
195
|
+
tty: finalTty,
|
|
196
|
+
tty_shell_pid: ttyInfo?.shellPid || 0,
|
|
197
|
+
tmux_pane: options.tmuxPane || process.env.TMUX_PANE || "",
|
|
198
|
+
launch_mode: launchMode,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const terminalApp = options.terminalApp || detectTerminalAppFromEnv();
|
|
202
|
+
if (terminalApp) {
|
|
203
|
+
this.busData.agents[subscriber].terminal_app = terminalApp;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 如果传入了 providerSessionId(从旧 session 恢复),设置它
|
|
207
|
+
if (options.providerSessionId) {
|
|
208
|
+
this.busData.agents[subscriber].provider_session_id = options.providerSessionId;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 保存 tty 信息
|
|
212
|
+
if (this.busData.agents[subscriber].tty) {
|
|
213
|
+
await this.queueManager.saveTty(
|
|
214
|
+
subscriber,
|
|
215
|
+
this.busData.agents[subscriber].tty
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
// 清理旧 tty 文件,避免错误注入
|
|
219
|
+
try {
|
|
220
|
+
const ttyPath = this.queueManager.getTtyPath(subscriber);
|
|
221
|
+
if (ttyPath && fs.existsSync(ttyPath)) {
|
|
222
|
+
fs.rmSync(ttyPath, { force: true });
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// ignore
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 创建队列目录
|
|
230
|
+
this.queueManager.ensureQueueDir(subscriber);
|
|
231
|
+
|
|
232
|
+
return { subscriber, nickname: finalNickname };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 离开总线
|
|
237
|
+
*/
|
|
238
|
+
async leave(subscriber) {
|
|
239
|
+
if (!this.busData.agents || !this.busData.agents[subscriber]) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.busData.agents[subscriber].status = "inactive";
|
|
244
|
+
this.busData.agents[subscriber].last_seen = getTimestamp();
|
|
245
|
+
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 重命名订阅者
|
|
251
|
+
*/
|
|
252
|
+
async rename(subscriber, newNickname) {
|
|
253
|
+
if (!this.busData.agents || !this.busData.agents[subscriber]) {
|
|
254
|
+
throw new Error(`Subscriber "${subscriber}" not found`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const nicknameManager = new NicknameManager(this.busData);
|
|
258
|
+
|
|
259
|
+
// 检查昵称冲突
|
|
260
|
+
if (nicknameManager.nicknameExists(newNickname, subscriber)) {
|
|
261
|
+
throw new Error(`Nickname "${newNickname}" already exists`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const oldNickname = this.busData.agents[subscriber].nickname;
|
|
265
|
+
this.busData.agents[subscriber].nickname = newNickname;
|
|
266
|
+
|
|
267
|
+
return { subscriber, oldNickname, newNickname };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 获取所有在线订阅者
|
|
272
|
+
*/
|
|
273
|
+
getActiveSubscribers() {
|
|
274
|
+
if (!this.busData.agents) return [];
|
|
275
|
+
|
|
276
|
+
return Object.entries(this.busData.agents)
|
|
277
|
+
.filter(([, meta]) => isMetaActive(meta))
|
|
278
|
+
.map(([id, meta]) => ({ id, ...meta }));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 获取订阅者信息
|
|
283
|
+
*/
|
|
284
|
+
getSubscriber(subscriber) {
|
|
285
|
+
return this.busData.agents?.[subscriber] || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 更新订阅者的最后活动时间
|
|
290
|
+
*/
|
|
291
|
+
updateLastSeen(subscriber) {
|
|
292
|
+
if (this.busData.agents && this.busData.agents[subscriber]) {
|
|
293
|
+
this.busData.agents[subscriber].last_seen = getTimestamp();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 清理不活跃的订阅者
|
|
299
|
+
*/
|
|
300
|
+
cleanupInactive() {
|
|
301
|
+
if (!this.busData.agents) return;
|
|
302
|
+
|
|
303
|
+
for (const [id, meta] of Object.entries(this.busData.agents)) {
|
|
304
|
+
if (meta.status === "active" && !isMetaActive(meta)) {
|
|
305
|
+
meta.status = "inactive";
|
|
306
|
+
meta.last_seen = getTimestamp();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = SubscriberManager;
|