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.
Files changed (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
@@ -0,0 +1,285 @@
1
+ const { spawn } = require("child_process");
2
+ const fs = require("fs");
3
+ const net = require("net");
4
+ const path = require("path");
5
+ const { subscriberToSafeName, isValidTty } = require("./utils");
6
+
7
+ const SHOULD_LOG_INJECT = process.env.UFOO_INJECT_DEBUG === "1";
8
+ const logInject = (message) => {
9
+ if (SHOULD_LOG_INJECT) {
10
+ console.log(message);
11
+ }
12
+ };
13
+
14
+ /**
15
+ * 命令注入器 - 将命令注入到终端
16
+ *
17
+ * 支持的方式:
18
+ * 1. PTY socket(直接写入,无需macOS权限)
19
+ * 2. tmux send-keys(无需权限)
20
+ */
21
+ class Injector {
22
+ constructor(busDir, agentsFile) {
23
+ this.busDir = busDir;
24
+ this.agentsFile = agentsFile;
25
+ }
26
+
27
+ /**
28
+ * 获取订阅者的 tty 文件路径
29
+ */
30
+ getTtyPath(subscriber) {
31
+ const safeName = subscriberToSafeName(subscriber);
32
+ return path.join(this.busDir, "queues", safeName, "tty");
33
+ }
34
+
35
+ /**
36
+ * 获取订阅者的 tmux pane ID(从 all-agents.json)
37
+ */
38
+ getTmuxPane(subscriber) {
39
+ const agentsFile = this.agentsFile;
40
+ if (!agentsFile || !fs.existsSync(agentsFile)) return null;
41
+
42
+ try {
43
+ const busData = JSON.parse(fs.readFileSync(agentsFile, "utf8"));
44
+ return busData.agents?.[subscriber]?.tmux_pane || null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 读取 tty 设备路径
52
+ */
53
+ readTty(subscriber) {
54
+ const ttyPath = this.getTtyPath(subscriber);
55
+ if (!fs.existsSync(ttyPath)) {
56
+ return null;
57
+ }
58
+ return fs.readFileSync(ttyPath, "utf8").trim();
59
+ }
60
+
61
+ /**
62
+ * 检查 tmux pane 是否存在
63
+ */
64
+ async checkTmuxPane(paneId) {
65
+ return new Promise((resolve) => {
66
+ const proc = spawn("tmux", ["list-panes", "-a", "-F", "#{pane_id}"]);
67
+ let output = "";
68
+
69
+ proc.stdout.on("data", (data) => {
70
+ output += data.toString();
71
+ });
72
+
73
+ proc.on("close", (code) => {
74
+ if (code !== 0) {
75
+ resolve(false);
76
+ return;
77
+ }
78
+ const panes = output.trim().split("\n");
79
+ resolve(panes.includes(paneId));
80
+ });
81
+
82
+ proc.on("error", () => resolve(false));
83
+ });
84
+ }
85
+
86
+ /**
87
+ * 根据 tty 查找 tmux pane
88
+ */
89
+ async findTmuxPaneByTty(tty) {
90
+ return new Promise((resolve) => {
91
+ const proc = spawn("tmux", ["list-panes", "-a", "-F", "#{pane_id} #{pane_tty}"]);
92
+ let output = "";
93
+
94
+ proc.stdout.on("data", (data) => {
95
+ output += data.toString();
96
+ });
97
+
98
+ proc.on("close", (code) => {
99
+ if (code !== 0) {
100
+ resolve(null);
101
+ return;
102
+ }
103
+ const lines = output.trim().split("\n");
104
+ for (const line of lines) {
105
+ const parts = line.trim().split(/\s+/);
106
+ if (parts.length >= 2 && parts[1] === tty) {
107
+ resolve(parts[0]);
108
+ return;
109
+ }
110
+ }
111
+ resolve(null);
112
+ });
113
+
114
+ proc.on("error", () => resolve(null));
115
+ });
116
+ }
117
+
118
+ /**
119
+ * 使用 tmux send-keys 注入命令
120
+ */
121
+ async injectTmux(paneId, command) {
122
+ return new Promise((resolve, reject) => {
123
+ // 检查是否需要发送中断信号
124
+ if (process.env.UFOO_INJECT_INTERRUPT === "1") {
125
+ spawn("tmux", ["send-keys", "-t", paneId, "C-c"]);
126
+ setTimeout(() => {
127
+ this.sendTmuxKeys(paneId, command, resolve, reject);
128
+ }, 100);
129
+ } else {
130
+ this.sendTmuxKeys(paneId, command, resolve, reject);
131
+ }
132
+ });
133
+ }
134
+
135
+ /**
136
+ * 发送 tmux 按键(先发文本,延迟后发 Enter)
137
+ */
138
+ sendTmuxKeys(paneId, command, resolve, reject) {
139
+ const textProc = spawn("tmux", ["send-keys", "-t", paneId, command]);
140
+ let stderr = "";
141
+
142
+ textProc.stderr.on("data", (data) => {
143
+ stderr += data.toString();
144
+ });
145
+
146
+ textProc.on("close", (code) => {
147
+ if (code !== 0) {
148
+ reject(new Error(stderr || "tmux send-keys failed"));
149
+ return;
150
+ }
151
+ // Delay before sending Enter — gives the target app time to process input
152
+ setTimeout(() => {
153
+ const enterProc = spawn("tmux", ["send-keys", "-t", paneId, "Enter"]);
154
+ enterProc.on("close", (enterCode) => {
155
+ if (enterCode === 0) resolve();
156
+ else reject(new Error("tmux send-keys Enter failed"));
157
+ });
158
+ enterProc.on("error", reject);
159
+ }, 150);
160
+ });
161
+
162
+ textProc.on("error", reject);
163
+ }
164
+
165
+ /**
166
+ * 获取订阅者的 inject socket 路径
167
+ */
168
+ getInjectSockPath(subscriber) {
169
+ const safeName = subscriberToSafeName(subscriber);
170
+ return path.join(this.busDir, "queues", safeName, "inject.sock");
171
+ }
172
+
173
+ /**
174
+ * 使用 PTY socket 直接注入命令(无需macOS权限)
175
+ */
176
+ async injectPty(subscriber, command) {
177
+ const sockPath = this.getInjectSockPath(subscriber);
178
+
179
+ if (!fs.existsSync(sockPath)) {
180
+ throw new Error(`Inject socket not found: ${sockPath}`);
181
+ }
182
+
183
+ return new Promise((resolve, reject) => {
184
+ const client = net.createConnection(sockPath, () => {
185
+ // 发送inject请求
186
+ client.write(JSON.stringify({ type: "inject", command }) + "\n");
187
+ });
188
+
189
+ let buffer = "";
190
+ const timeout = setTimeout(() => {
191
+ client.destroy();
192
+ reject(new Error("PTY inject timeout"));
193
+ }, 5000);
194
+
195
+ client.on("data", (data) => {
196
+ buffer += data.toString("utf8");
197
+ const lines = buffer.split("\n");
198
+ buffer = lines.pop() || "";
199
+
200
+ for (const line of lines) {
201
+ if (!line.trim()) continue;
202
+ clearTimeout(timeout);
203
+ try {
204
+ const res = JSON.parse(line);
205
+ client.end();
206
+ if (res.ok) {
207
+ resolve();
208
+ } else {
209
+ reject(new Error(res.error || "PTY inject failed"));
210
+ }
211
+ } catch (err) {
212
+ client.end();
213
+ reject(err);
214
+ }
215
+ return;
216
+ }
217
+ });
218
+
219
+ client.on("error", (err) => {
220
+ clearTimeout(timeout);
221
+ reject(err);
222
+ });
223
+
224
+ client.on("close", () => {
225
+ clearTimeout(timeout);
226
+ });
227
+ });
228
+ }
229
+
230
+ /**
231
+ * 注入命令到订阅者的终端
232
+ *
233
+ * 优先级:
234
+ * 1. PTY socket(直接写入,无需macOS权限)
235
+ * 2. tmux send-keys(无需权限)
236
+ */
237
+ async inject(subscriber, commandOverride = "") {
238
+ // 确定注入命令(codex 用 "ubus",claude-code 用 "/ubus")
239
+ const command = commandOverride
240
+ ? String(commandOverride)
241
+ : (subscriber.startsWith("codex:") ? "ubus" : "/ubus");
242
+
243
+ // 1. 优先尝试 PTY socket(无需任何macOS权限)
244
+ const injectSockPath = this.getInjectSockPath(subscriber);
245
+ if (fs.existsSync(injectSockPath)) {
246
+ try {
247
+ logInject(`[inject] Using PTY socket: ${injectSockPath}`);
248
+ await this.injectPty(subscriber, command);
249
+ logInject("[inject] PTY inject success");
250
+ return;
251
+ } catch (err) {
252
+ logInject(`[inject] PTY socket failed: ${err.message}, trying tmux`);
253
+ }
254
+ }
255
+
256
+ // 读取 tty(tmux 需要)
257
+ const tty = this.readTty(subscriber);
258
+
259
+ // 2. 尝试 tmux(无需权限)
260
+ const tmuxPane = this.getTmuxPane(subscriber);
261
+ if (tmuxPane) {
262
+ const paneExists = await this.checkTmuxPane(tmuxPane);
263
+ if (paneExists) {
264
+ logInject(`[inject] Using tmux send-keys for pane: ${tmuxPane}`);
265
+ await this.injectTmux(tmuxPane, command);
266
+ return;
267
+ }
268
+ }
269
+
270
+ // 尝试通过 tty 查找 tmux pane
271
+ if (tty && isValidTty(tty)) {
272
+ const fallbackPane = await this.findTmuxPaneByTty(tty);
273
+ if (fallbackPane) {
274
+ logInject(`[inject] Using tmux send-keys for pane: ${fallbackPane}`);
275
+ await this.injectTmux(fallbackPane, command);
276
+ return;
277
+ }
278
+ }
279
+
280
+ // 没有可用的注入方式
281
+ throw new Error(`No inject method available for ${subscriber}. PTY socket or tmux required.`);
282
+ }
283
+ }
284
+
285
+ module.exports = Injector;
@@ -0,0 +1,302 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ getTimestamp,
5
+ getDate,
6
+ readJSONL,
7
+ appendJSONL,
8
+ readLastLine,
9
+ readJSON,
10
+ isMetaActive,
11
+ } = require("./utils");
12
+ const NicknameManager = require("./nickname");
13
+
14
+ /**
15
+ * 消息管理器
16
+ */
17
+ class MessageManager {
18
+ constructor(busDir, busData, queueManager) {
19
+ this.busDir = busDir;
20
+ this.busData = busData;
21
+ this.queueManager = queueManager;
22
+ this.eventsDir = path.join(busDir, "events");
23
+ }
24
+
25
+ /**
26
+ * 获取下一个全局序号
27
+ */
28
+ async getNextSeq() {
29
+ // 读取所有 events/*.jsonl 文件,找到最大的 seq
30
+ let maxSeq = 0;
31
+
32
+ if (!fs.existsSync(this.eventsDir)) {
33
+ return 1;
34
+ }
35
+
36
+ const files = fs.readdirSync(this.eventsDir)
37
+ .filter((f) => f.endsWith(".jsonl"))
38
+ .sort()
39
+ .reverse(); // 从最新的文件开始读
40
+
41
+ for (const file of files) {
42
+ const filePath = path.join(this.eventsDir, file);
43
+ const lastLine = readLastLine(filePath);
44
+
45
+ if (lastLine) {
46
+ try {
47
+ const event = JSON.parse(lastLine);
48
+ if (event.seq && event.seq > maxSeq) {
49
+ maxSeq = event.seq;
50
+ break; // 找到最大 seq 后立即退出
51
+ }
52
+ } catch {
53
+ // 忽略解析错误
54
+ }
55
+ }
56
+ }
57
+
58
+ return maxSeq + 1;
59
+ }
60
+
61
+ /**
62
+ * 解析目标(支持昵称、代理类型、订阅者 ID)
63
+ */
64
+ resolveTarget(target) {
65
+ const nicknameManager = new NicknameManager(this.busData);
66
+
67
+ // 1. 尝试作为订阅者 ID
68
+ if (target.includes(":")) {
69
+ return [target];
70
+ }
71
+
72
+ // 2. 尝试作为昵称
73
+ const byNickname = nicknameManager.resolveNickname(target);
74
+ if (byNickname) {
75
+ return [byNickname];
76
+ }
77
+
78
+ // 3. 尝试作为代理类型(匹配所有该类型的活跃订阅者)
79
+ const subscribers = this.busData.agents || {};
80
+ const byType = Object.entries(subscribers)
81
+ .filter(([, meta]) => meta.agent_type === target && isMetaActive(meta))
82
+ .map(([id]) => id);
83
+
84
+ if (byType.length > 0) {
85
+ return byType;
86
+ }
87
+
88
+ // 4. 通配符(所有活跃订阅者)
89
+ if (target === "*") {
90
+ return Object.entries(subscribers)
91
+ .filter(([, meta]) => isMetaActive(meta))
92
+ .map(([id]) => id);
93
+ }
94
+
95
+ // 未找到目标
96
+ return [];
97
+ }
98
+
99
+ /**
100
+ * 检查目标是否匹配订阅者
101
+ */
102
+ targetMatches(target, subscriber) {
103
+ // 精确匹配
104
+ if (target === subscriber) return true;
105
+
106
+ // 代理类型匹配
107
+ const meta = this.busData.agents?.[subscriber];
108
+ if (meta && target === meta.agent_type) return true;
109
+
110
+ // 昵称匹配
111
+ if (meta && target === meta.nickname) return true;
112
+
113
+ // 通配符
114
+ if (target === "*") return true;
115
+
116
+ return false;
117
+ }
118
+
119
+ /**
120
+ * 发送消息
121
+ */
122
+ async send(target, message, publisher = "unknown") {
123
+ const seq = await this.getNextSeq();
124
+ const timestamp = getTimestamp();
125
+ const date = getDate();
126
+
127
+ // 解析目标
128
+ const targets = this.resolveTarget(target);
129
+ if (targets.length === 0) {
130
+ throw new Error(`Target "${target}" not found`);
131
+ }
132
+
133
+ // 构建事件
134
+ const event = {
135
+ seq,
136
+ timestamp,
137
+ type: "message/targeted",
138
+ event: "message",
139
+ publisher,
140
+ target,
141
+ data: { message },
142
+ };
143
+
144
+ // 写入事件日志
145
+ const eventFile = path.join(this.eventsDir, `${date}.jsonl`);
146
+ appendJSONL(eventFile, event);
147
+
148
+ // 为每个目标订阅者添加到待处理队列
149
+ for (const targetSubscriber of targets) {
150
+ // 检查订阅者的 offset,如果已经消费过这个 seq,不再添加
151
+ const offset = await this.queueManager.getOffset(targetSubscriber);
152
+ if (seq > offset) {
153
+ await this.queueManager.appendPending(targetSubscriber, event);
154
+ }
155
+ }
156
+
157
+ return { seq, targets };
158
+ }
159
+
160
+ /**
161
+ * 广播消息
162
+ */
163
+ async broadcast(message, publisher = "unknown") {
164
+ return this.send("*", message, publisher);
165
+ }
166
+
167
+ /**
168
+ * 发送系统事件(非消息)
169
+ */
170
+ async emit(target, eventName, data = {}, publisher = "unknown", type = "status/agent") {
171
+ const seq = await this.getNextSeq();
172
+ const timestamp = getTimestamp();
173
+ const date = getDate();
174
+
175
+ // 解析目标
176
+ const targets = this.resolveTarget(target);
177
+ if (targets.length === 0) {
178
+ throw new Error(`Target "${target}" not found`);
179
+ }
180
+
181
+ const event = {
182
+ seq,
183
+ timestamp,
184
+ type,
185
+ event: eventName,
186
+ publisher,
187
+ target,
188
+ data,
189
+ };
190
+
191
+ const eventFile = path.join(this.eventsDir, `${date}.jsonl`);
192
+ appendJSONL(eventFile, event);
193
+
194
+ for (const targetSubscriber of targets) {
195
+ const offset = await this.queueManager.getOffset(targetSubscriber);
196
+ if (seq > offset) {
197
+ await this.queueManager.appendPending(targetSubscriber, event);
198
+ }
199
+ }
200
+
201
+ return { seq, targets };
202
+ }
203
+
204
+ /**
205
+ * 检查待处理消息
206
+ */
207
+ async check(subscriber) {
208
+ const pending = await this.queueManager.readPending(subscriber);
209
+ return pending;
210
+ }
211
+
212
+ /**
213
+ * 确认消息(清空待处理队列)
214
+ */
215
+ async ack(subscriber) {
216
+ const pending = await this.queueManager.readPending(subscriber);
217
+ const count = pending.length;
218
+
219
+ if (count > 0) {
220
+ await this.queueManager.clearPending(subscriber);
221
+ }
222
+
223
+ return count;
224
+ }
225
+
226
+ /**
227
+ * 消费事件(从 offset 开始)
228
+ */
229
+ async consume(subscriber, fromBeginning = false) {
230
+ let offset = fromBeginning ? 0 : await this.queueManager.getOffset(subscriber);
231
+ const consumed = [];
232
+
233
+ // 读取所有事件文件
234
+ if (!fs.existsSync(this.eventsDir)) {
235
+ return { consumed, newOffset: offset };
236
+ }
237
+
238
+ const files = fs.readdirSync(this.eventsDir)
239
+ .filter((f) => f.endsWith(".jsonl"))
240
+ .sort(); // 按日期排序
241
+
242
+ for (const file of files) {
243
+ const filePath = path.join(this.eventsDir, file);
244
+ const events = readJSONL(filePath);
245
+
246
+ for (const event of events) {
247
+ if (event.seq <= offset) continue;
248
+
249
+ // 检查是否针对此订阅者
250
+ if (
251
+ this.targetMatches(event.target, subscriber) ||
252
+ event.target === "*"
253
+ ) {
254
+ consumed.push(event);
255
+ offset = Math.max(offset, event.seq);
256
+ }
257
+ }
258
+ }
259
+
260
+ // 更新 offset
261
+ if (consumed.length > 0) {
262
+ await this.queueManager.setOffset(subscriber, offset);
263
+ }
264
+
265
+ return { consumed, newOffset: offset };
266
+ }
267
+
268
+ /**
269
+ * 智能路由解析(找出所有匹配的候选者)
270
+ */
271
+ async resolve(myId, targetType) {
272
+ const subscribers = this.busData.agents || {};
273
+ const candidates = Object.entries(subscribers)
274
+ .filter(([id, meta]) => {
275
+ if (id === myId) return false; // 排除自己
276
+ if (meta.status !== "active") return false;
277
+
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;
282
+
283
+ return false;
284
+ })
285
+ .map(([id, meta]) => ({
286
+ id,
287
+ nickname: meta.nickname,
288
+ agent_type: meta.agent_type,
289
+ last_seen: meta.last_seen,
290
+ }));
291
+
292
+ // 如果只有一个候选者,直接返回
293
+ if (candidates.length === 1) {
294
+ return { single: candidates[0].id, candidates };
295
+ }
296
+
297
+ // 多个候选者,返回列表供调用者选择
298
+ return { single: null, candidates };
299
+ }
300
+ }
301
+
302
+ module.exports = MessageManager;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 昵称管理和解析
3
+ */
4
+ class NicknameManager {
5
+ constructor(busData) {
6
+ this.busData = busData;
7
+ }
8
+
9
+ /**
10
+ * 解析昵称到订阅者 ID
11
+ * @param {string} nickname - 昵称
12
+ * @returns {string|null} - 订阅者 ID 或 null
13
+ */
14
+ resolveNickname(nickname) {
15
+ const subscribers = this.busData.agents || {};
16
+ for (const [id, meta] of Object.entries(subscribers)) {
17
+ if (meta.nickname === nickname) {
18
+ return id;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * 检查昵称是否已存在
26
+ * @param {string} nickname - 昵称
27
+ * @param {string} excludeSubscriber - 排除的订阅者 ID(用于重命名时)
28
+ * @returns {boolean} - 是否已存在
29
+ */
30
+ nicknameExists(nickname, excludeSubscriber = null) {
31
+ const subscribers = this.busData.agents || {};
32
+ for (const [id, meta] of Object.entries(subscribers)) {
33
+ if (id !== excludeSubscriber && meta.nickname === nickname) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * 生成自动昵称
42
+ * @param {string} agentType - 代理类型(codex, claude-code)
43
+ * @returns {string} - 自动生成的昵称(如 codex-1, claude-1)
44
+ */
45
+ generateAutoNickname(agentType) {
46
+ const subscribers = this.busData.agents || {};
47
+ const prefix = agentType === "claude-code" ? "claude" : agentType;
48
+
49
+ // 找出所有相同前缀的昵称
50
+ const existing = Object.values(subscribers)
51
+ .map((meta) => meta.nickname)
52
+ .filter((nick) => nick && nick.startsWith(`${prefix}-`))
53
+ .map((nick) => {
54
+ const match = nick.match(/^[^-]+-(\d+)$/);
55
+ return match ? parseInt(match[1], 10) : 0;
56
+ })
57
+ .filter((n) => !isNaN(n));
58
+
59
+ // 找到下一个可用的编号
60
+ const maxNumber = existing.length > 0 ? Math.max(...existing) : 0;
61
+ return `${prefix}-${maxNumber + 1}`;
62
+ }
63
+
64
+ /**
65
+ * 获取订阅者的昵称
66
+ */
67
+ getNickname(subscriber) {
68
+ const meta = this.busData.agents?.[subscriber];
69
+ return meta?.nickname || null;
70
+ }
71
+
72
+ /**
73
+ * 设置订阅者的昵称
74
+ */
75
+ setNickname(subscriber, nickname) {
76
+ if (!this.busData.agents) {
77
+ this.busData.agents = {};
78
+ }
79
+ if (!this.busData.agents[subscriber]) {
80
+ this.busData.agents[subscriber] = {};
81
+ }
82
+ this.busData.agents[subscriber].nickname = nickname;
83
+ }
84
+ }
85
+
86
+ module.exports = NicknameManager;